|
@@ -1,30 +1,30 @@
|
|
|
use crate::{
|
|
|
- builder::{
|
|
|
- BuildMessage, MessageSource, MessageType, Stage, TargetPlatform, UpdateBuildProgress,
|
|
|
- },
|
|
|
+ builder::{BuildResult, UpdateStage},
|
|
|
+ builder::{Stage, TargetPlatform, UpdateBuildProgress},
|
|
|
dioxus_crate::DioxusCrate,
|
|
|
serve::next_or_pending,
|
|
|
- tracer::CLILogControl,
|
|
|
-};
|
|
|
-use crate::{
|
|
|
- builder::{BuildResult, UpdateStage},
|
|
|
serve::Serve,
|
|
|
+ serve::{Builder, Server, Watcher},
|
|
|
+ tracer::CLILogControl,
|
|
|
+ TraceMsg, TraceSrc,
|
|
|
};
|
|
|
-use core::panic;
|
|
|
use crossterm::{
|
|
|
- event::{Event, EventStream, KeyCode, KeyModifiers, MouseEventKind},
|
|
|
- terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
|
|
+ cursor::{Hide, Show},
|
|
|
+ event::{Event, EventStream, KeyCode, KeyEventKind, KeyModifiers},
|
|
|
+ terminal::{
|
|
|
+ disable_raw_mode, enable_raw_mode, Clear, ClearType, EnterAlternateScreen,
|
|
|
+ LeaveAlternateScreen,
|
|
|
+ },
|
|
|
tty::IsTty,
|
|
|
ExecutableCommand,
|
|
|
};
|
|
|
use dioxus_cli_config::{AddressArguments, Platform};
|
|
|
use dioxus_hot_reload::ClientMsg;
|
|
|
use futures_util::{future::select_all, Future, FutureExt, StreamExt};
|
|
|
-use ratatui::{prelude::*, widgets::*, TerminalOptions, Viewport};
|
|
|
+use ratatui::{prelude::*, TerminalOptions, Viewport};
|
|
|
use std::{
|
|
|
cell::RefCell,
|
|
|
collections::{HashMap, HashSet},
|
|
|
- fmt::Display,
|
|
|
io::{self, stdout},
|
|
|
rc::Rc,
|
|
|
sync::atomic::Ordering,
|
|
@@ -36,38 +36,23 @@ use tokio::{
|
|
|
};
|
|
|
use tracing::Level;
|
|
|
|
|
|
-use super::{Builder, Server, Watcher};
|
|
|
-
|
|
|
-#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
|
|
-pub enum LogSource {
|
|
|
- Internal,
|
|
|
- Target(TargetPlatform),
|
|
|
-}
|
|
|
+mod render;
|
|
|
|
|
|
-impl Display for LogSource {
|
|
|
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
- match self {
|
|
|
- LogSource::Internal => write!(f, "CLI"),
|
|
|
- LogSource::Target(platform) => write!(f, "{platform}"),
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-impl From<TargetPlatform> for LogSource {
|
|
|
- fn from(platform: TargetPlatform) -> Self {
|
|
|
- LogSource::Target(platform)
|
|
|
- }
|
|
|
-}
|
|
|
+// How many lines should be scroll on each mouse scroll or arrow key input.
|
|
|
+const SCROLL_SPEED: u16 = 2;
|
|
|
+// Speed added to `SCROLL_SPEED` when the modifier key is held during scroll.
|
|
|
+const SCROLL_MODIFIER: u16 = 4;
|
|
|
+// Scroll modifier key.
|
|
|
+const SCROLL_MODIFIER_KEY: KeyModifiers = KeyModifiers::SHIFT;
|
|
|
|
|
|
#[derive(Default)]
|
|
|
pub struct BuildProgress {
|
|
|
- internal_logs: Vec<BuildMessage>,
|
|
|
- build_logs: HashMap<TargetPlatform, ActiveBuild>,
|
|
|
+ current_builds: HashMap<TargetPlatform, ActiveBuild>,
|
|
|
}
|
|
|
|
|
|
impl BuildProgress {
|
|
|
pub fn progress(&self) -> f64 {
|
|
|
- self.build_logs
|
|
|
+ self.current_builds
|
|
|
.values()
|
|
|
.min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
|
|
|
.map(|build| match build.stage {
|
|
@@ -87,30 +72,35 @@ pub struct Output {
|
|
|
// optional since when there's no tty there's no eventstream to read from - just stdin
|
|
|
events: Option<EventStream>,
|
|
|
|
|
|
- _rustc_version: String,
|
|
|
- _rustc_nightly: bool,
|
|
|
- _dx_version: String,
|
|
|
- interactive: bool,
|
|
|
pub(crate) build_progress: BuildProgress,
|
|
|
running_apps: HashMap<TargetPlatform, RunningApp>,
|
|
|
- is_cli_release: bool,
|
|
|
- platform: Platform,
|
|
|
|
|
|
- num_lines_with_wrapping: u16,
|
|
|
- term_height: u16,
|
|
|
- scroll: u16,
|
|
|
- fly_modal_open: bool,
|
|
|
- anim_start: Instant,
|
|
|
+ // A list of all messages from build, dev, app, and more.
|
|
|
+ messages: Vec<TraceMsg>,
|
|
|
|
|
|
- tab: Tab,
|
|
|
+ num_lines_wrapping: u16,
|
|
|
+ scroll_position: u16,
|
|
|
+ console_width: u16,
|
|
|
+ console_height: u16,
|
|
|
|
|
|
+ more_modal_open: bool,
|
|
|
+ anim_start: Instant,
|
|
|
+
|
|
|
+ interactive: bool,
|
|
|
+ _is_cli_release: bool,
|
|
|
+ platform: Platform,
|
|
|
addr: AddressArguments,
|
|
|
-}
|
|
|
|
|
|
-#[derive(PartialEq, Eq, Clone, Copy)]
|
|
|
-enum Tab {
|
|
|
- Console,
|
|
|
- BuildLog,
|
|
|
+ // Filters
|
|
|
+ show_filter_menu: bool,
|
|
|
+ filters: Vec<(String, bool)>,
|
|
|
+ selected_filter_index: usize,
|
|
|
+ filter_search_mode: bool,
|
|
|
+ filter_search_input: Option<String>,
|
|
|
+
|
|
|
+ _rustc_version: String,
|
|
|
+ _rustc_nightly: bool,
|
|
|
+ _dx_version: String,
|
|
|
}
|
|
|
|
|
|
type TerminalBackend = Terminal<CrosstermBackend<io::Stdout>>;
|
|
@@ -122,9 +112,9 @@ impl Output {
|
|
|
let mut events = None;
|
|
|
|
|
|
if interactive {
|
|
|
- log_control.tui_enabled.store(true, Ordering::SeqCst);
|
|
|
+ log_control.output_enabled.store(true, Ordering::SeqCst);
|
|
|
enable_raw_mode()?;
|
|
|
- stdout().execute(EnterAlternateScreen)?;
|
|
|
+ stdout().execute(EnterAlternateScreen)?.execute(Hide)?;
|
|
|
|
|
|
// workaround for ci where the terminal is not fully initialized
|
|
|
// this stupid bug
|
|
@@ -135,6 +125,9 @@ impl Output {
|
|
|
// set the panic hook to fix the terminal
|
|
|
set_fix_term_hook();
|
|
|
|
|
|
+ // Fix the vscode scrollback issue
|
|
|
+ fix_xtermjs_scrollback();
|
|
|
+
|
|
|
let term: Option<TerminalBackend> = Terminal::with_options(
|
|
|
CrosstermBackend::new(stdout()),
|
|
|
TerminalOptions {
|
|
@@ -153,8 +146,8 @@ impl Output {
|
|
|
|
|
|
dx_version.push_str(env!("CARGO_PKG_VERSION"));
|
|
|
|
|
|
+ // todo: we want the binstalls / cargo installs to be exempt, but installs from git are not
|
|
|
let is_cli_release = crate::dx_build_info::PROFILE == "release";
|
|
|
-
|
|
|
if !is_cli_release {
|
|
|
if let Some(hash) = crate::dx_build_info::GIT_COMMIT_HASH_SHORT {
|
|
|
let hash = &hash.trim_start_matches('g')[..4];
|
|
@@ -173,24 +166,30 @@ impl Output {
|
|
|
_rustc_nightly,
|
|
|
_dx_version: dx_version,
|
|
|
interactive,
|
|
|
- is_cli_release,
|
|
|
+ _is_cli_release: is_cli_release,
|
|
|
platform,
|
|
|
- fly_modal_open: false,
|
|
|
+ messages: Vec::new(),
|
|
|
+ more_modal_open: false,
|
|
|
build_progress: Default::default(),
|
|
|
running_apps: HashMap::new(),
|
|
|
- scroll: 0,
|
|
|
- term_height: 0,
|
|
|
- num_lines_with_wrapping: 0,
|
|
|
+ scroll_position: 0,
|
|
|
+ num_lines_wrapping: 0,
|
|
|
+ console_width: 0,
|
|
|
+ console_height: 0,
|
|
|
anim_start: Instant::now(),
|
|
|
- tab: Tab::BuildLog,
|
|
|
addr: cfg.server_arguments.address.clone(),
|
|
|
+
|
|
|
+ // Filter
|
|
|
+ show_filter_menu: false,
|
|
|
+ filters: Vec::new(),
|
|
|
+ selected_filter_index: 0,
|
|
|
+ filter_search_input: None,
|
|
|
+ filter_search_mode: false,
|
|
|
})
|
|
|
}
|
|
|
|
|
|
/// Add a message from stderr to the logs
|
|
|
fn push_stderr(&mut self, platform: TargetPlatform, stderr: String) {
|
|
|
- self.set_tab(Tab::BuildLog);
|
|
|
-
|
|
|
self.running_apps
|
|
|
.get_mut(&platform)
|
|
|
.unwrap()
|
|
@@ -199,16 +198,16 @@ impl Output {
|
|
|
.unwrap()
|
|
|
.stderr_line
|
|
|
.push_str(&stderr);
|
|
|
- self.build_progress
|
|
|
- .build_logs
|
|
|
- .get_mut(&platform)
|
|
|
- .unwrap()
|
|
|
- .messages
|
|
|
- .push(BuildMessage {
|
|
|
- level: Level::ERROR,
|
|
|
- message: MessageType::Text(stderr),
|
|
|
- source: MessageSource::App,
|
|
|
- });
|
|
|
+
|
|
|
+ self.messages.push(TraceMsg {
|
|
|
+ source: TraceSrc::App(platform),
|
|
|
+ level: Level::ERROR,
|
|
|
+ content: stderr,
|
|
|
+ });
|
|
|
+
|
|
|
+ if self.is_snapped() {
|
|
|
+ self.scroll_to_bottom();
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/// Add a message from stdout to the logs
|
|
@@ -221,16 +220,16 @@ impl Output {
|
|
|
.unwrap()
|
|
|
.stdout_line
|
|
|
.push_str(&stdout);
|
|
|
- self.build_progress
|
|
|
- .build_logs
|
|
|
- .get_mut(&platform)
|
|
|
- .unwrap()
|
|
|
- .messages
|
|
|
- .push(BuildMessage {
|
|
|
- level: Level::INFO,
|
|
|
- message: MessageType::Text(stdout),
|
|
|
- source: MessageSource::App,
|
|
|
- });
|
|
|
+
|
|
|
+ self.messages.push(TraceMsg {
|
|
|
+ source: TraceSrc::App(platform),
|
|
|
+ level: Level::INFO,
|
|
|
+ content: stdout,
|
|
|
+ });
|
|
|
+
|
|
|
+ if self.is_snapped() {
|
|
|
+ self.scroll_to_bottom();
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/// Wait for either the ctrl_c handler or the next event
|
|
@@ -278,7 +277,7 @@ impl Output {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
- let tui_log_rx = &mut self.log_control.tui_rx;
|
|
|
+ let tui_log_rx = &mut self.log_control.output_rx;
|
|
|
let next_tui_log = next_or_pending(tui_log_rx.next());
|
|
|
|
|
|
tokio::select! {
|
|
@@ -293,11 +292,7 @@ impl Output {
|
|
|
|
|
|
// Handle internal CLI tracing logs.
|
|
|
log = next_tui_log => {
|
|
|
- self.push_log(LogSource::Internal, BuildMessage {
|
|
|
- level: Level::INFO,
|
|
|
- message: MessageType::Text(log),
|
|
|
- source: MessageSource::Dev,
|
|
|
- });
|
|
|
+ self.push_log(log);
|
|
|
}
|
|
|
|
|
|
event = user_input => {
|
|
@@ -313,9 +308,11 @@ impl Output {
|
|
|
pub fn shutdown(&mut self) -> io::Result<()> {
|
|
|
// if we're a tty then we need to disable the raw mode
|
|
|
if self.interactive {
|
|
|
- self.log_control.tui_enabled.store(false, Ordering::SeqCst);
|
|
|
+ self.log_control
|
|
|
+ .output_enabled
|
|
|
+ .store(false, Ordering::SeqCst);
|
|
|
disable_raw_mode()?;
|
|
|
- stdout().execute(LeaveAlternateScreen)?;
|
|
|
+ stdout().execute(LeaveAlternateScreen)?.execute(Show)?;
|
|
|
self.drain_print_logs();
|
|
|
}
|
|
|
|
|
@@ -328,37 +325,16 @@ impl Output {
|
|
|
/// versions of the cli would just eat build logs making debugging issues harder than they needed
|
|
|
/// to be.
|
|
|
fn drain_print_logs(&mut self) {
|
|
|
- fn log_build_message(platform: &LogSource, message: &BuildMessage) {
|
|
|
- match &message.message {
|
|
|
- MessageType::Text(text) => {
|
|
|
- for line in text.lines() {
|
|
|
- println!("{platform}: {line}");
|
|
|
- }
|
|
|
- }
|
|
|
- MessageType::Cargo(diagnostic) => {
|
|
|
- println!("{platform}: {diagnostic}");
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
+ let messages = self.messages.drain(..);
|
|
|
|
|
|
- // todo: print the build info here for the most recent build, and then the logs of the most recent build
|
|
|
- for (platform, build) in self.build_progress.build_logs.iter_mut() {
|
|
|
- if build.messages.is_empty() {
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
- let messages = build.messages.drain(0..);
|
|
|
-
|
|
|
- for message in messages {
|
|
|
- log_build_message(&LogSource::Target(*platform), &message);
|
|
|
+ for msg in messages {
|
|
|
+ // TODO: Better formatting for different content lengths.
|
|
|
+ if msg.source != TraceSrc::Cargo {
|
|
|
+ println!("[{}] {}: {}", msg.source, msg.level, msg.content);
|
|
|
+ } else {
|
|
|
+ println!("{}", msg.content);
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- // Log the internal logs
|
|
|
- let messaegs = self.build_progress.internal_logs.drain(..);
|
|
|
- for message in messaegs {
|
|
|
- log_build_message(&LogSource::Internal, &message);
|
|
|
- }
|
|
|
}
|
|
|
|
|
|
/// Handle an input event, returning `true` if the event should cause the program to restart.
|
|
@@ -372,59 +348,138 @@ impl Output {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- if let Event::Key(key) = input {
|
|
|
- if let KeyCode::Char('/') = key.code {
|
|
|
- self.fly_modal_open = !self.fly_modal_open;
|
|
|
+ // If we're in filter search mode we must capture all key inputs.
|
|
|
+ // This also handles when a filter is submitted.
|
|
|
+ if self.filter_search_mode {
|
|
|
+ if let Event::Key(key) = input {
|
|
|
+ if key.kind != KeyEventKind::Press {
|
|
|
+ return Ok(false);
|
|
|
+ }
|
|
|
+
|
|
|
+ match key.code {
|
|
|
+ KeyCode::Char(c) => {
|
|
|
+ if let Some(input) = self.filter_search_input.as_mut() {
|
|
|
+ input.push(c);
|
|
|
+ } else {
|
|
|
+ self.filter_search_input = Some(String::from(c));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ KeyCode::Enter => {
|
|
|
+ if let Some(search) = &self.filter_search_input {
|
|
|
+ self.filters.push((search.to_string(), true));
|
|
|
+ }
|
|
|
+ self.filter_search_input = None;
|
|
|
+ self.filter_search_mode = false;
|
|
|
+ }
|
|
|
+ KeyCode::Backspace => {
|
|
|
+ if let Some(search) = self.filter_search_input.as_mut() {
|
|
|
+ search.pop();
|
|
|
+ if search.is_empty() {
|
|
|
+ self.filter_search_input = None;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ _ => {}
|
|
|
+ }
|
|
|
+ return Ok(false);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
match input {
|
|
|
- Event::Mouse(mouse) if mouse.kind == MouseEventKind::ScrollUp => {
|
|
|
- self.scroll = self.scroll.saturating_sub(1);
|
|
|
+ Event::Key(key) if key.code == KeyCode::Up && key.kind == KeyEventKind::Press => {
|
|
|
+ // Select filter list item if filter is showing, otherwise scroll console.
|
|
|
+ if self.show_filter_menu {
|
|
|
+ self.selected_filter_index = self.selected_filter_index.saturating_sub(1);
|
|
|
+ } else {
|
|
|
+ // Scroll up
|
|
|
+ let mut scroll_speed = SCROLL_SPEED;
|
|
|
+ if key.modifiers.contains(SCROLL_MODIFIER_KEY) {
|
|
|
+ scroll_speed += SCROLL_MODIFIER;
|
|
|
+ }
|
|
|
+ self.scroll_position = self.scroll_position.saturating_sub(scroll_speed);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ Event::Key(key) if key.code == KeyCode::Down && key.kind == KeyEventKind::Press => {
|
|
|
+ // Select filter list item if filter is showing, otherwise scroll console.
|
|
|
+ if self.show_filter_menu {
|
|
|
+ let list_len = self.filters.len();
|
|
|
+ if self.selected_filter_index + 1 < list_len {
|
|
|
+ self.selected_filter_index += 1;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // Scroll down
|
|
|
+ let mut scroll_speed = SCROLL_SPEED;
|
|
|
+ if key.modifiers.contains(SCROLL_MODIFIER_KEY) {
|
|
|
+ scroll_speed += SCROLL_MODIFIER;
|
|
|
+ }
|
|
|
+ self.scroll_position += scroll_speed;
|
|
|
+ }
|
|
|
}
|
|
|
- Event::Mouse(mouse) if mouse.kind == MouseEventKind::ScrollDown => {
|
|
|
- self.scroll += 1;
|
|
|
+ Event::Key(key) if key.code == KeyCode::Left && key.kind == KeyEventKind::Press => {
|
|
|
+ // Remove selected filter if filter menu is shown.
|
|
|
+ if self.show_filter_menu {
|
|
|
+ let index = self.selected_filter_index;
|
|
|
+ if self.filters.get(index).is_some() {
|
|
|
+ self.filters.remove(index);
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
- Event::Key(key) if key.code == KeyCode::Up => {
|
|
|
- self.scroll = self.scroll.saturating_sub(1);
|
|
|
+ Event::Key(key) if key.code == KeyCode::Right && key.kind == KeyEventKind::Press => {
|
|
|
+ // Toggle filter if filter menu is shown.
|
|
|
+ if self.show_filter_menu {
|
|
|
+ let index = self.selected_filter_index;
|
|
|
+ self.filters.reverse();
|
|
|
+ if let Some(item) = self.filters.get_mut(index) {
|
|
|
+ item.1 = !item.1;
|
|
|
+ }
|
|
|
+ self.filters.reverse();
|
|
|
+ }
|
|
|
}
|
|
|
- Event::Key(key) if key.code == KeyCode::Down => {
|
|
|
- self.scroll += 1;
|
|
|
+ Event::Key(key) if key.code == KeyCode::Enter && key.kind == KeyEventKind::Press => {
|
|
|
+ // We only need to listen to the enter key when not in search mode
|
|
|
+ // as there is other logic that handles adding filters and disabling the mode.
|
|
|
+ if self.show_filter_menu {
|
|
|
+ self.filter_search_mode = !self.filter_search_mode;
|
|
|
+ }
|
|
|
}
|
|
|
- Event::Key(key) if key.code == KeyCode::Char('r') => {
|
|
|
- // todo: reload the app
|
|
|
+ Event::Key(key)
|
|
|
+ if key.code == KeyCode::Char('r') && key.kind == KeyEventKind::Press =>
|
|
|
+ {
|
|
|
+ // Reload the app
|
|
|
return Ok(true);
|
|
|
}
|
|
|
- Event::Key(key) if key.code == KeyCode::Char('o') => {
|
|
|
+ Event::Key(key)
|
|
|
+ if key.code == KeyCode::Char('o') && key.kind == KeyEventKind::Press =>
|
|
|
+ {
|
|
|
// Open the running app.
|
|
|
open::that(format!("http://{}:{}", self.addr.addr, self.addr.port))?;
|
|
|
}
|
|
|
- Event::Key(key) if key.code == KeyCode::Char('c') => {
|
|
|
- // Clear the currently selected build logs.
|
|
|
- for build in self.build_progress.build_logs.values_mut() {
|
|
|
- let msgs = match self.tab {
|
|
|
- Tab::Console => &mut build.stdout_logs,
|
|
|
- Tab::BuildLog => &mut build.messages,
|
|
|
- };
|
|
|
- msgs.clear();
|
|
|
+
|
|
|
+ Event::Key(key)
|
|
|
+ if key.code == KeyCode::Char('f') && key.kind == KeyEventKind::Press =>
|
|
|
+ {
|
|
|
+ // Show filter menu and enable filter selection mode.
|
|
|
+ if self.show_filter_menu {
|
|
|
+ // Reset inputs when filter menu is closed.
|
|
|
+ self.filter_search_mode = false;
|
|
|
+ self.filter_search_input = None;
|
|
|
}
|
|
|
+ self.show_filter_menu = !self.show_filter_menu;
|
|
|
+ }
|
|
|
+ Event::Key(key)
|
|
|
+ if key.code == KeyCode::Char('/') && key.kind == KeyEventKind::Press =>
|
|
|
+ {
|
|
|
+ // Toggle more modal
|
|
|
+ self.more_modal_open = !self.more_modal_open;
|
|
|
}
|
|
|
- Event::Key(key) if key.code == KeyCode::Char('1') => self.set_tab(Tab::Console),
|
|
|
- Event::Key(key) if key.code == KeyCode::Char('2') => self.set_tab(Tab::BuildLog),
|
|
|
Event::Resize(_width, _height) => {
|
|
|
// nothing, it should take care of itself
|
|
|
}
|
|
|
_ => {}
|
|
|
}
|
|
|
|
|
|
- if self.scroll
|
|
|
- > self
|
|
|
- .num_lines_with_wrapping
|
|
|
- .saturating_sub(self.term_height + 1)
|
|
|
- {
|
|
|
- self.scroll = self
|
|
|
- .num_lines_with_wrapping
|
|
|
- .saturating_sub(self.term_height + 1);
|
|
|
+ if self.scroll_position > self.num_lines_wrapping.saturating_sub(self.console_height) {
|
|
|
+ self.scroll_to_bottom();
|
|
|
}
|
|
|
|
|
|
Ok(false)
|
|
@@ -435,92 +490,56 @@ impl Output {
|
|
|
platform: TargetPlatform,
|
|
|
message: axum::extract::ws::Message,
|
|
|
) {
|
|
|
+ // Deccode the message and push it to our logs.
|
|
|
if let axum::extract::ws::Message::Text(text) = message {
|
|
|
let msg = serde_json::from_str::<ClientMsg>(text.as_str());
|
|
|
match msg {
|
|
|
Ok(ClientMsg::Log { level, messages }) => {
|
|
|
- self.push_log(
|
|
|
- platform,
|
|
|
- BuildMessage {
|
|
|
- level: match level.as_str() {
|
|
|
- "info" => Level::INFO,
|
|
|
- "warn" => Level::WARN,
|
|
|
- "error" => Level::ERROR,
|
|
|
- "debug" => Level::DEBUG,
|
|
|
- _ => Level::INFO,
|
|
|
- },
|
|
|
- message: MessageType::Text(
|
|
|
- // todo: the js console is giving us a list of params, not formatted text
|
|
|
- // we need to translate its styling into our own
|
|
|
- messages.first().unwrap_or(&String::new()).clone(),
|
|
|
- ),
|
|
|
- source: MessageSource::App,
|
|
|
- },
|
|
|
- );
|
|
|
+ let level = match level.as_str() {
|
|
|
+ "trace" => Level::TRACE,
|
|
|
+ "debug" => Level::DEBUG,
|
|
|
+ "info" => Level::INFO,
|
|
|
+ "warn" => Level::WARN,
|
|
|
+ "error" => Level::ERROR,
|
|
|
+ _ => Level::INFO,
|
|
|
+ };
|
|
|
+
|
|
|
+ let content = messages.first().unwrap_or(&String::new()).clone();
|
|
|
+
|
|
|
+ // We don't care about logging the app's message so we directly push it instead of using tracing.
|
|
|
+ self.push_log(TraceMsg::new(TraceSrc::App(platform), level, content));
|
|
|
}
|
|
|
Err(err) => {
|
|
|
- self.push_log(
|
|
|
- platform,
|
|
|
- BuildMessage {
|
|
|
- level: Level::ERROR,
|
|
|
- source: MessageSource::Dev,
|
|
|
- message: MessageType::Text(format!("Error parsing app message: {err}")),
|
|
|
- },
|
|
|
- );
|
|
|
+ tracing::error!(dx_src = ?TraceSrc::Dev, "Error parsing message from {}: {}", platform, err);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // todo: re-enable
|
|
|
- #[allow(unused)]
|
|
|
- fn is_snapped(&self, _platform: LogSource) -> bool {
|
|
|
+ fn is_snapped(&self) -> bool {
|
|
|
true
|
|
|
- // let prev_scrol = self
|
|
|
- // .num_lines_with_wrapping
|
|
|
- // .saturating_sub(self.term_height);
|
|
|
- // prev_scrol == self.scroll
|
|
|
}
|
|
|
|
|
|
pub fn scroll_to_bottom(&mut self) {
|
|
|
- self.scroll = (self.num_lines_with_wrapping).saturating_sub(self.term_height);
|
|
|
+ self.scroll_position = self.num_lines_wrapping.saturating_sub(self.console_height);
|
|
|
}
|
|
|
|
|
|
- pub fn push_log(&mut self, platform: impl Into<LogSource>, message: BuildMessage) {
|
|
|
- let source = platform.into();
|
|
|
- let snapped = self.is_snapped(source);
|
|
|
-
|
|
|
- match source {
|
|
|
- LogSource::Internal => self.build_progress.internal_logs.push(message),
|
|
|
- LogSource::Target(platform) => self
|
|
|
- .build_progress
|
|
|
- .build_logs
|
|
|
- .entry(platform)
|
|
|
- .or_default()
|
|
|
- .stdout_logs
|
|
|
- .push(message),
|
|
|
- }
|
|
|
+ pub fn push_log(&mut self, message: TraceMsg) {
|
|
|
+ self.messages.push(message);
|
|
|
|
|
|
- if snapped {
|
|
|
+ if self.is_snapped() {
|
|
|
self.scroll_to_bottom();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- pub fn new_build_logs(&mut self, platform: TargetPlatform, update: UpdateBuildProgress) {
|
|
|
- let snapped = self.is_snapped(LogSource::Target(platform));
|
|
|
-
|
|
|
- // when the build is finished, switch to the console
|
|
|
- if update.stage == Stage::Finished {
|
|
|
- self.tab = Tab::Console;
|
|
|
- }
|
|
|
-
|
|
|
+ pub fn new_build_progress(&mut self, platform: TargetPlatform, update: UpdateBuildProgress) {
|
|
|
self.build_progress
|
|
|
- .build_logs
|
|
|
+ .current_builds
|
|
|
.entry(platform)
|
|
|
.or_default()
|
|
|
.update(update);
|
|
|
|
|
|
- if snapped {
|
|
|
+ if self.is_snapped() {
|
|
|
self.scroll_to_bottom();
|
|
|
}
|
|
|
}
|
|
@@ -557,7 +576,7 @@ impl Output {
|
|
|
self.running_apps.insert(platform, app);
|
|
|
|
|
|
// Finish the build progress for the platform that just finished building
|
|
|
- if let Some(build) = self.build_progress.build_logs.get_mut(&platform) {
|
|
|
+ if let Some(build) = self.build_progress.current_builds.get_mut(&platform) {
|
|
|
build.stage = Stage::Finished;
|
|
|
}
|
|
|
}
|
|
@@ -568,7 +587,7 @@ impl Output {
|
|
|
_opts: &Serve,
|
|
|
_config: &DioxusCrate,
|
|
|
_build_engine: &Builder,
|
|
|
- server: &Server,
|
|
|
+ _server: &Server,
|
|
|
_watcher: &Watcher,
|
|
|
) {
|
|
|
// just drain the build logs
|
|
@@ -590,230 +609,59 @@ impl Output {
|
|
|
.as_mut()
|
|
|
.unwrap()
|
|
|
.draw(|frame| {
|
|
|
- // a layout that has a title with stats about the program and then the actual console itself
|
|
|
- let body = Layout::default()
|
|
|
- .direction(Direction::Vertical)
|
|
|
- .constraints(
|
|
|
- [
|
|
|
- // Title
|
|
|
- Constraint::Length(1),
|
|
|
- // Body
|
|
|
- Constraint::Min(0),
|
|
|
- ]
|
|
|
- .as_ref(),
|
|
|
- )
|
|
|
- .split(frame.size());
|
|
|
-
|
|
|
- // Split the body into a left and a right
|
|
|
- let console = Layout::default()
|
|
|
- .direction(Direction::Horizontal)
|
|
|
- .constraints([Constraint::Fill(1), Constraint::Length(14)].as_ref())
|
|
|
- .split(body[1]);
|
|
|
-
|
|
|
- let addr = format!("http://{}:{}", self.addr.addr, self.addr.port);
|
|
|
- let listening_len = format!("listening at {addr}").len() + 3;
|
|
|
- let listening_len = if listening_len > body[0].width as usize {
|
|
|
- 0
|
|
|
- } else {
|
|
|
- listening_len
|
|
|
- };
|
|
|
-
|
|
|
- let header = Layout::default()
|
|
|
- .direction(Direction::Horizontal)
|
|
|
- .constraints(
|
|
|
- [
|
|
|
- Constraint::Fill(1),
|
|
|
- Constraint::Length(listening_len as u16),
|
|
|
- ]
|
|
|
- .as_ref(),
|
|
|
- )
|
|
|
- .split(body[0]);
|
|
|
-
|
|
|
- // // Render a border for the header
|
|
|
- // frame.render_widget(Block::default().borders(Borders::BOTTOM), body[0]);
|
|
|
-
|
|
|
- // Render the metadata
|
|
|
- let mut spans = vec![
|
|
|
- Span::from(if self.is_cli_release { "dx" } else { "dx-dev" }).green(),
|
|
|
- Span::from(" ").green(),
|
|
|
- Span::from("serve").green(),
|
|
|
- Span::from(" | ").white(),
|
|
|
- Span::from(self.platform.to_string()).green(),
|
|
|
- Span::from(" | ").white(),
|
|
|
- ];
|
|
|
-
|
|
|
- // If there is build progress, display that next to the platform
|
|
|
- if !self.build_progress.build_logs.is_empty() {
|
|
|
- if self
|
|
|
- .build_progress
|
|
|
- .build_logs
|
|
|
- .values()
|
|
|
- .any(|b| b.failed.is_some())
|
|
|
- {
|
|
|
- spans.push(Span::from("build failed ❌").red());
|
|
|
- } else {
|
|
|
- spans.push(Span::from("status: ").green());
|
|
|
- let build = self
|
|
|
- .build_progress
|
|
|
- .build_logs
|
|
|
- .values()
|
|
|
- .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
|
|
|
- .unwrap();
|
|
|
- spans.extend_from_slice(&build.spans(Rect::new(
|
|
|
- 0,
|
|
|
- 0,
|
|
|
- build.max_layout_size(),
|
|
|
- 1,
|
|
|
- )));
|
|
|
- }
|
|
|
- }
|
|
|
+ let mut layout = render::TuiLayout::new(frame.size(), self.show_filter_menu);
|
|
|
+ let (console_width, console_height) = layout.get_console_size();
|
|
|
+ self.console_width = console_width;
|
|
|
+ self.console_height = console_height;
|
|
|
+
|
|
|
+ // Render the decor first as some of it (such as backgrounds) may be rendered on top of.
|
|
|
+ layout.render_decor(frame, self.show_filter_menu);
|
|
|
+
|
|
|
+ // Get only the enabled filters.
|
|
|
+ let mut enabled_filters = self.filters.clone();
|
|
|
+ enabled_filters.retain(|f| f.1);
|
|
|
+ let enabled_filters = enabled_filters
|
|
|
+ .iter()
|
|
|
+ .map(|f| f.0.clone())
|
|
|
+ .collect::<Vec<String>>();
|
|
|
+
|
|
|
+ // Render console, we need the number of wrapping lines for scroll.
|
|
|
+ self.num_lines_wrapping = layout.render_console(
|
|
|
+ frame,
|
|
|
+ self.scroll_position,
|
|
|
+ &self.messages,
|
|
|
+ &enabled_filters,
|
|
|
+ );
|
|
|
|
|
|
- frame.render_widget(Paragraph::new(Line::from(spans)).left_aligned(), header[0]);
|
|
|
-
|
|
|
- // Split apart the body into a center and a right side
|
|
|
- // We only want to show the sidebar if there's enough space
|
|
|
- if listening_len > 0 {
|
|
|
- frame.render_widget(
|
|
|
- Paragraph::new(Line::from(vec![
|
|
|
- Span::from("listening at ").dark_gray(),
|
|
|
- Span::from(format!("http://{}", server.ip).as_str()).gray(),
|
|
|
- ])),
|
|
|
- header[1],
|
|
|
+ if self.show_filter_menu {
|
|
|
+ layout.render_filter_menu(
|
|
|
+ frame,
|
|
|
+ &self.filters,
|
|
|
+ self.selected_filter_index,
|
|
|
+ self.filter_search_mode,
|
|
|
+ self.filter_search_input.as_ref(),
|
|
|
);
|
|
|
}
|
|
|
|
|
|
- // Draw the tabs in the right region of the console
|
|
|
- // First draw the left border
|
|
|
- frame.render_widget(
|
|
|
- Paragraph::new(vec![
|
|
|
- {
|
|
|
- let mut line = Line::from(" [1] console").dark_gray();
|
|
|
- if self.tab == Tab::Console {
|
|
|
- line.style = Style::default().fg(Color::LightYellow);
|
|
|
- }
|
|
|
- line
|
|
|
- },
|
|
|
- {
|
|
|
- let mut line = Line::from(" [2] build").dark_gray();
|
|
|
- if self.tab == Tab::BuildLog {
|
|
|
- line.style = Style::default().fg(Color::LightYellow);
|
|
|
- }
|
|
|
- line
|
|
|
- },
|
|
|
- Line::from(" ").gray(),
|
|
|
- Line::from(" [/] more").gray(),
|
|
|
- Line::from(" [r] rebuild").gray(),
|
|
|
- Line::from(" [c] clear").gray(),
|
|
|
- Line::from(" [o] open").gray(),
|
|
|
- Line::from(" [h] hide").gray(),
|
|
|
- ])
|
|
|
- .left_aligned()
|
|
|
- .block(
|
|
|
- Block::default()
|
|
|
- .borders(Borders::LEFT | Borders::TOP)
|
|
|
- .border_set(symbols::border::Set {
|
|
|
- top_left: symbols::line::NORMAL.horizontal_down,
|
|
|
- ..symbols::border::PLAIN
|
|
|
- }),
|
|
|
- ),
|
|
|
- console[1],
|
|
|
+ layout.render_status_bar(
|
|
|
+ frame,
|
|
|
+ self.platform,
|
|
|
+ &self.build_progress,
|
|
|
+ self.more_modal_open,
|
|
|
+ self.show_filter_menu,
|
|
|
+ &self._dx_version,
|
|
|
);
|
|
|
|
|
|
- // We're going to assemble a text buffer directly and then let the paragraph widgets
|
|
|
- // handle the wrapping and scrolling
|
|
|
- let mut paragraph_text: Text<'_> = Text::default();
|
|
|
-
|
|
|
- let mut add_build_message = |message: &BuildMessage| {
|
|
|
- use ansi_to_tui::IntoText;
|
|
|
- match &message.message {
|
|
|
- MessageType::Text(line) => {
|
|
|
- for line in line.lines() {
|
|
|
- let text = line.into_text().unwrap_or_default();
|
|
|
- for line in text.lines {
|
|
|
- let source = format!("[{}] ", message.source);
|
|
|
-
|
|
|
- let msg_span = Span::from(source);
|
|
|
- let msg_span = match message.source {
|
|
|
- MessageSource::App => msg_span.light_blue(),
|
|
|
- MessageSource::Dev => msg_span.dark_gray(),
|
|
|
- MessageSource::Build => msg_span.light_yellow(),
|
|
|
- };
|
|
|
-
|
|
|
- let mut out_line = vec![msg_span];
|
|
|
- for span in line.spans {
|
|
|
- out_line.push(span);
|
|
|
- }
|
|
|
- let newline = Line::from(out_line);
|
|
|
- paragraph_text.push_line(newline);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- MessageType::Cargo(diagnostic) => {
|
|
|
- let diagnostic = diagnostic.rendered.as_deref().unwrap_or_default();
|
|
|
-
|
|
|
- for line in diagnostic.lines() {
|
|
|
- paragraph_text.extend(line.into_text().unwrap_or_default());
|
|
|
- }
|
|
|
- }
|
|
|
- };
|
|
|
- };
|
|
|
-
|
|
|
- // First log each platform's build logs
|
|
|
- for platform in self.build_progress.build_logs.keys() {
|
|
|
- let build = self.build_progress.build_logs.get(platform).unwrap();
|
|
|
-
|
|
|
- let msgs = match self.tab {
|
|
|
- Tab::Console => &build.stdout_logs,
|
|
|
- Tab::BuildLog => &build.messages,
|
|
|
- };
|
|
|
-
|
|
|
- for span in msgs.iter() {
|
|
|
- add_build_message(span);
|
|
|
- }
|
|
|
- }
|
|
|
- // Then log the internal logs
|
|
|
- for message in self.build_progress.internal_logs.iter() {
|
|
|
- add_build_message(message);
|
|
|
+ if self.more_modal_open {
|
|
|
+ layout.render_more_modal(frame);
|
|
|
}
|
|
|
|
|
|
- let paragraph = Paragraph::new(paragraph_text)
|
|
|
- .left_aligned()
|
|
|
- .wrap(Wrap { trim: false });
|
|
|
-
|
|
|
- self.term_height = console[0].height;
|
|
|
- self.num_lines_with_wrapping = paragraph.line_count(console[0].width) as u16;
|
|
|
-
|
|
|
- let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
|
|
|
- .begin_symbol(None)
|
|
|
- .end_symbol(None)
|
|
|
- .track_symbol(None)
|
|
|
- .thumb_symbol("▐");
|
|
|
-
|
|
|
- let mut scrollbar_state = ScrollbarState::new(
|
|
|
- self.num_lines_with_wrapping
|
|
|
- .saturating_sub(self.term_height) as usize,
|
|
|
- )
|
|
|
- .position(self.scroll as usize);
|
|
|
-
|
|
|
- let paragraph = paragraph.scroll((self.scroll, 0));
|
|
|
- paragraph
|
|
|
- .block(Block::new().borders(Borders::TOP))
|
|
|
- .render(console[0], frame.buffer_mut());
|
|
|
-
|
|
|
- // and the scrollbar, those are separate widgets
|
|
|
- frame.render_stateful_widget(
|
|
|
- scrollbar,
|
|
|
- console[0].inner(Margin {
|
|
|
- // todo: dont use margin - just push down the body based on its top border
|
|
|
- // using an inner vertical margin of 1 unit makes the scrollbar inside the block
|
|
|
- vertical: 1,
|
|
|
- horizontal: 0,
|
|
|
- }),
|
|
|
- &mut scrollbar_state,
|
|
|
+ layout.render_current_scroll(
|
|
|
+ self.scroll_position,
|
|
|
+ self.num_lines_wrapping,
|
|
|
+ self.console_height,
|
|
|
+ frame,
|
|
|
);
|
|
|
-
|
|
|
- // render the fly modal
|
|
|
- self.render_fly_modal(frame, console[0]);
|
|
|
});
|
|
|
}
|
|
|
|
|
@@ -848,38 +696,11 @@ impl Output {
|
|
|
|
|
|
Ok(false)
|
|
|
}
|
|
|
-
|
|
|
- fn render_fly_modal(&mut self, frame: &mut Frame, area: Rect) {
|
|
|
- if !self.fly_modal_open {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // Create a frame slightly smaller than the area
|
|
|
- let panel = Layout::default()
|
|
|
- .direction(Direction::Vertical)
|
|
|
- .constraints([Constraint::Fill(1)].as_ref())
|
|
|
- .split(area)[0];
|
|
|
-
|
|
|
- // Wipe the panel
|
|
|
- frame.render_widget(Clear, panel);
|
|
|
- frame.render_widget(Block::default().borders(Borders::ALL), panel);
|
|
|
-
|
|
|
- let modal = Paragraph::new("Under construction, please check back at a later date!\n")
|
|
|
- .alignment(Alignment::Center);
|
|
|
- frame.render_widget(modal, panel);
|
|
|
- }
|
|
|
-
|
|
|
- fn set_tab(&mut self, tab: Tab) {
|
|
|
- self.tab = tab;
|
|
|
- self.scroll = 0;
|
|
|
- }
|
|
|
}
|
|
|
|
|
|
#[derive(Default, Debug, PartialEq)]
|
|
|
pub struct ActiveBuild {
|
|
|
stage: Stage,
|
|
|
- messages: Vec<BuildMessage>,
|
|
|
- stdout_logs: Vec<BuildMessage>,
|
|
|
progress: f64,
|
|
|
failed: Option<String>,
|
|
|
}
|
|
@@ -896,9 +717,6 @@ impl ActiveBuild {
|
|
|
self.progress = 0.0;
|
|
|
self.failed = None;
|
|
|
}
|
|
|
- UpdateStage::AddMessage(message) => {
|
|
|
- self.messages.push(message);
|
|
|
- }
|
|
|
UpdateStage::SetProgress(progress) => {
|
|
|
self.progress = progress;
|
|
|
}
|
|
@@ -909,21 +727,25 @@ impl ActiveBuild {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- fn spans(&self, area: Rect) -> Vec<Span> {
|
|
|
+ fn make_spans(&self, area: Rect) -> Vec<Span> {
|
|
|
let mut spans = Vec::new();
|
|
|
|
|
|
let message = match self.stage {
|
|
|
- Stage::Initializing => "initializing... ",
|
|
|
- Stage::InstallingWasmTooling => "installing wasm tools... ",
|
|
|
- Stage::Compiling => "compiling... ",
|
|
|
- Stage::OptimizingWasm => "optimizing wasm... ",
|
|
|
- Stage::OptimizingAssets => "optimizing assets... ",
|
|
|
- Stage::Finished => "finished! 🎉 ",
|
|
|
+ Stage::Initializing => "Initializing...",
|
|
|
+ Stage::InstallingWasmTooling => "Configuring...",
|
|
|
+ Stage::Compiling => "Compiling...",
|
|
|
+ Stage::OptimizingWasm => "Optimizing...",
|
|
|
+ Stage::OptimizingAssets => "Copying Assets...",
|
|
|
+ Stage::Finished => "Build finished! 🎉 ",
|
|
|
};
|
|
|
- let progress = format!("{}%", (self.progress * 100.0) as u8);
|
|
|
+
|
|
|
+ let progress = format!(" {}%", (self.progress * 100.0) as u8);
|
|
|
|
|
|
if area.width >= self.max_layout_size() {
|
|
|
- spans.push(Span::from(message).light_yellow());
|
|
|
+ match self.stage {
|
|
|
+ Stage::Finished => spans.push(Span::from(message).light_yellow()),
|
|
|
+ _ => spans.push(Span::from(message).light_yellow()),
|
|
|
+ }
|
|
|
|
|
|
if self.stage != Stage::Finished {
|
|
|
spans.push(Span::from(progress).white());
|
|
@@ -958,11 +780,19 @@ fn set_fix_term_hook() {
|
|
|
let original_hook = std::panic::take_hook();
|
|
|
std::panic::set_hook(Box::new(move |info| {
|
|
|
_ = disable_raw_mode();
|
|
|
- _ = stdout().execute(LeaveAlternateScreen);
|
|
|
+ let mut stdout = stdout();
|
|
|
+ _ = stdout.execute(LeaveAlternateScreen);
|
|
|
+ _ = stdout.execute(Show);
|
|
|
original_hook(info);
|
|
|
}));
|
|
|
}
|
|
|
|
|
|
+/// clearing and writing a new line fixes the xtermjs scrollback issue
|
|
|
+fn fix_xtermjs_scrollback() {
|
|
|
+ _ = crossterm::execute!(std::io::stdout(), Clear(ClearType::All));
|
|
|
+ println!();
|
|
|
+}
|
|
|
+
|
|
|
// todo: re-enable
|
|
|
#[allow(unused)]
|
|
|
async fn rustc_version() -> String {
|