CLI Fixes & Tweaks (#2846)

Fixes scrolling on vscode using simple fix
 Adds scroll modifier, scroll at 5 lines per scroll while holding shift key.
 Adds error handling for manganis failure that still lets the build run.
 Revises TUI rendering code.
 Move TUI "info bars" to the bottom of the terminal.
 Revised logging system with tracing
 Working [c] clear keybind. This has been removed.
 Removal of [h] hide keybind text (it does nothing)
 Some opinionated cleanups and renames to make tui logic easier to understand.
 Rolling log file & maybe add some more "internal" logging. Closes CLI Rolling Log File #2764
 Removes log tabs. Closes CLI: Color-code logs and get rid of tabs #2857
 Combines info bars.
 Working and good text selection.
 Print launch URL in console.
 Handle log lines properly and add formatting.
 Move MessageSource to a more reasonable location.
 Add some background styling to powerline (info bar) - Tried this and it doesn't look the greatest.
 Log Filtering
 Final Cleaning & Changes - I could do this forever
 Test Linux

---------

Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
This commit is contained in:
Miles MurgawandJonathan Kelley authored and GitHub committed 2024-09-13 01:34:19 -07:00
1 parent 87c2f64f13
commit 8d68886310
22 files changed
+1302 -718

No files matched your search

+17 -7
View File
@@ -1,11 +1,14 @@
use std::future::{poll_fn, Future, IntoFuture};
use std::task::Poll;
use crate::builder::{Stage, TargetPlatform, UpdateBuildProgress, UpdateStage};
use crate::cli::serve::Serve;
use crate::dioxus_crate::DioxusCrate;
use crate::tracer::CLILogControl;
use crate::Result;
use crate::{
builder::{Stage, TargetPlatform, UpdateBuildProgress, UpdateStage},
TraceSrc,
};
use futures_util::FutureExt;
use tokio::task::yield_now;
@@ -54,6 +57,8 @@ pub async fn serve_all(
dioxus_crate: DioxusCrate,
log_control: CLILogControl,
) -> Result<()> {
// Start the screen first so we collect build logs.
let mut screen = Output::start(&serve, log_control).expect("Failed to open terminal logger");
let mut builder = Builder::new(&dioxus_crate, &serve);
// Start the first build
@@ -61,7 +66,6 @@ pub async fn serve_all(
let mut server = Server::start(&serve, &dioxus_crate);
let mut watcher = Watcher::start(&serve, &dioxus_crate);
let mut screen = Output::start(&serve, log_control).expect("Failed to open terminal logger");
let is_hot_reload = serve.server_arguments.hot_reload.unwrap_or(true);
@@ -81,15 +85,21 @@ pub async fn serve_all(
}
let changed_files = watcher.dequeue_changed_files(&dioxus_crate);
let changed = changed_files.first().cloned();
// if change is hotreloadable, hotreload it
// and then send that update to all connected clients
if let Some(hr) = watcher.attempt_hot_reload(&dioxus_crate, changed_files) {
// Only send a hotreload message for templates and assets - otherwise we'll just get a full rebuild
if hr.templates.is_empty() && hr.assets.is_empty() {
if hr.templates.is_empty() && hr.assets.is_empty() && hr.unknown_files.is_empty() {
continue
}
if let Some(changed_path) = changed {
let path_relative = changed_path.strip_prefix(dioxus_crate.crate_dir()).map(|p| p.display().to_string()).unwrap_or_else(|_| changed_path.display().to_string());
tracing::info!(dx_src = ?TraceSrc::Dev, "Hotreloaded {}", path_relative);
}
server.send_hotreload(hr).await;
} else {
// If the change is not binary patchable, rebuild the project
@@ -129,7 +139,7 @@ pub async fn serve_all(
match application {
Ok(BuilderUpdate::Progress { platform, update }) => {
let update_clone = update.clone();
screen.new_build_logs(platform, update_clone);
screen.new_build_progress(platform, update_clone);
server.update_build_status(screen.build_progress.progress(), update.stage.to_string()).await;
match update {
@@ -151,7 +161,7 @@ pub async fn serve_all(
match child {
Ok(Some(child_proc)) => builder.children.push((build_result.target_platform, child_proc)),
Err(e) => {
tracing::error!("Failed to open build result: {e}");
tracing::error!(dx_src = ?TraceSrc::Build, "Failed to open build result: {e}");
break;
},
_ => {}
@@ -176,11 +186,11 @@ pub async fn serve_all(
break;
}
else {
tracing::error!("Application exited with status: {status}");
tracing::error!(dx_src = ?TraceSrc::Dev, "Application exited with status: {status}");
}
},
Err(e) => {
tracing::error!("Application exited with error: {e}");
tracing::error!(dx_src = ?TraceSrc::Dev, "Application exited with error: {e}");
}
}
}
File diff suppressed because it is too large. Load diff
+487
View File
@@ -0,0 +1,487 @@
use super::{BuildProgress, TraceMsg, TraceSrc};
use ansi_to_tui::IntoText as _;
use dioxus_cli_config::Platform;
use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Style, Stylize},
text::{Line, Span, Text},
widgets::{Block, Borders, Clear, List, ListState, Paragraph, Widget, Wrap},
Frame,
};
use regex::Regex;
use std::fmt::Write as _;
use std::rc::Rc;
use tracing::Level;
pub struct TuiLayout {
/// The entire TUI body.
_body: Rc<[Rect]>,
/// The console where build logs are displayed.
console: Rc<[Rect]>,
// The filter drawer if the drawer is open.
filter_drawer: Option<Rc<[Rect]>>,
// The border that separates the console and info bars.
border_sep: Rect,
// The status bar that displays build status, platform, versions, etc.
status_bar: Rc<[Rect]>,
// Misc
filter_list_state: ListState,
}
impl TuiLayout {
pub fn new(frame_size: Rect, filter_open: bool) -> Self {
// The full layout
let body = Layout::default()
.direction(Direction::Vertical)
.constraints([
// Footer Status
Constraint::Length(1),
// Border Separator
Constraint::Length(1),
// Console
Constraint::Fill(1),
// Padding
Constraint::Length(1),
])
.split(frame_size);
let mut console_constraints = vec![Constraint::Fill(1)];
if filter_open {
console_constraints.push(Constraint::Length(1));
console_constraints.push(Constraint::Length(25));
}
// Build the console, where logs go.
let console = Layout::default()
.direction(Direction::Horizontal)
.constraints(console_constraints)
.split(body[2]);
let filter_drawer = match filter_open {
false => None,
true => Some(
Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Length(1),
Constraint::Fill(1),
Constraint::Length(1),
])
.split(console[2]),
),
};
// Build the status bar.
let status_bar = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Fill(1), Constraint::Fill(1)])
.split(body[0]);
// Specify borders
let border_sep_top = body[1];
Self {
_body: body,
console,
filter_drawer,
border_sep: border_sep_top,
status_bar,
filter_list_state: ListState::default(),
}
}
/// Render all decorations.
pub fn render_decor(&self, frame: &mut Frame, filter_open: bool) {
frame.render_widget(
Block::new()
.borders(Borders::TOP)
.border_style(Style::new().white()),
self.border_sep,
);
if filter_open {
frame.render_widget(
Block::new()
.borders(Borders::LEFT)
.border_style(Style::new().white()),
self.console[1],
);
}
}
/// Render the console and it's logs, returning the number of lines required to render the entire log output.
pub fn render_console(
&self,
frame: &mut Frame,
scroll_position: u16,
messages: &[TraceMsg],
enabled_filters: &[String],
) -> u16 {
const LEVEL_MAX: usize = "BUILD: ".len();
let mut out_text = Text::default();
// Assemble the messages
for msg in messages.iter() {
let mut sub_line_padding = 0;
let text = msg.content.trim_end().into_text().unwrap_or_default();
for (idx, line) in text.lines.into_iter().enumerate() {
// Don't add any formatting for cargo messages.
let out_line = if msg.source != TraceSrc::Cargo {
if idx == 0 {
match msg.source {
TraceSrc::Dev => {
let mut spans = vec![Span::from(" DEV: ").light_magenta()];
for span in line.spans {
spans.push(span);
}
spans
}
TraceSrc::Build => {
let mut spans = vec![Span::from("BUILD: ").light_blue()];
for span in line.spans {
spans.push(span);
}
spans
}
_ => {
// Build level tag: `INFO:``
// We don't subtract 1 here for `:` because we still want at least 1 padding.
let padding =
build_msg_padding(LEVEL_MAX - msg.level.to_string().len() - 2);
let level = format!("{padding}{}: ", msg.level);
sub_line_padding += level.len();
let level_span = Span::from(level);
let level_span = match msg.level {
Level::TRACE => level_span.black(),
Level::DEBUG => level_span.light_magenta(),
Level::INFO => level_span.light_green(),
Level::WARN => level_span.light_yellow(),
Level::ERROR => level_span.light_red(),
};
let mut out_line = vec![level_span];
for span in line.spans {
out_line.push(span);
}
out_line
}
}
} else {
// Not the first line. Append the padding and merge into list.
let padding = build_msg_padding(sub_line_padding);
let mut out_line = vec![Span::from(padding)];
for span in line.spans {
out_line.push(span);
}
out_line
}
} else {
line.spans
};
out_text.push_line(Line::from(out_line));
}
}
// Only show messages for filters that are enabled.
let mut included_line_ids = Vec::new();
for filter in enabled_filters {
let re = Regex::new(filter);
for (index, line) in out_text.lines.iter().enumerate() {
let line_str = line.to_string();
match re {
Ok(ref re) => {
// sort by provided regex
if re.is_match(&line_str) {
included_line_ids.push(index);
}
}
Err(_) => {
// default to basic string storing
if line_str.contains(filter) {
included_line_ids.push(index);
}
}
}
}
}
included_line_ids.sort_unstable();
included_line_ids.dedup();
let out_lines = out_text.lines;
let mut out_text = Text::default();
if enabled_filters.is_empty() {
for line in out_lines {
out_text.push_line(line.clone());
}
} else {
for id in included_line_ids {
if let Some(line) = out_lines.get(id) {
out_text.push_line(line.clone());
}
}
}
let (console_width, _console_height) = self.get_console_size();
let paragraph = Paragraph::new(out_text)
.left_aligned()
.wrap(Wrap { trim: false });
let num_lines_wrapping = paragraph.line_count(console_width) as u16;
paragraph
.scroll((scroll_position, 0))
.render(self.console[0], frame.buffer_mut());
num_lines_wrapping
}
/// Render the status bar.
pub fn render_status_bar(
&self,
frame: &mut Frame,
_platform: Platform,
build_progress: &BuildProgress,
more_modal_open: bool,
filter_menu_open: bool,
dx_version: &str,
) {
// left aligned text
let mut spans = vec![
Span::from("🧬 dx").white(),
Span::from(" ").white(),
Span::from(dx_version).white(),
Span::from(" | ").dark_gray(),
];
// If there is build progress, render the current status.
let is_build_progress = !build_progress.current_builds.is_empty();
if is_build_progress {
// If the build failed, show a failed status.
// Otherwise, render current status.
let build_failed = build_progress
.current_builds
.values()
.any(|b| b.failed.is_some());
if build_failed {
spans.push(Span::from("Build failed ❌").red());
} else {
// spans.push(Span::from("status: ").gray());
let build = build_progress
.current_builds
.values()
.min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.unwrap();
spans.extend_from_slice(&build.make_spans(Rect::new(
0,
0,
build.max_layout_size(),
1,
)));
}
}
// right aligned text
let more_span = Span::from("[/] more");
let more_span = match more_modal_open {
true => more_span.light_yellow(),
false => more_span.gray(),
};
let filter_span = Span::from("[f] filter");
let filter_span = match filter_menu_open {
true => filter_span.light_yellow(),
false => filter_span.gray(),
};
// Right-aligned text
let right_line = Line::from(vec![
Span::from("[o] open").gray(),
Span::from(" | ").gray(),
Span::from("[r] rebuild").gray(),
Span::from(" | ").gray(),
filter_span,
Span::from(" | ").dark_gray(),
more_span,
]);
frame.render_widget(
Paragraph::new(Line::from(spans)).left_aligned(),
self.status_bar[0],
);
// Render the info
frame.render_widget(
Paragraph::new(right_line).right_aligned(),
self.status_bar[1],
);
}
/// Renders the "more" modal to show extra info/keybinds accessible via the more keybind.
pub fn render_more_modal(&self, frame: &mut Frame) {
let modal = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Fill(1), Constraint::Length(5)])
.split(self.console[0])[1];
frame.render_widget(Clear, modal);
frame.render_widget(Block::default().borders(Borders::ALL), modal);
// Render under construction message
frame.render_widget(
Paragraph::new("Under construction, please check back at a later date!")
.alignment(Alignment::Center),
modal,
);
}
/// Render the filter drawer menu.
pub fn render_filter_menu(
&mut self,
frame: &mut Frame,
filters: &[(String, bool)],
selected_filter_index: usize,
search_mode: bool,
search_input: Option<&String>,
) {
let Some(ref filter_drawer) = self.filter_drawer else {
return;
};
// Vertical layout
let container = Layout::default()
.constraints([
Constraint::Length(4),
Constraint::Fill(1),
Constraint::Length(7),
])
.direction(Direction::Vertical)
.split(filter_drawer[1]);
// Render the search section.
let top_area = Layout::default()
.constraints([
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
])
.direction(Direction::Vertical)
.split(container[0]);
let search_title = Line::from("Search").gray();
let search_input_block = Block::new().bg(Color::White);
let search_text = match search_input {
Some(s) => s,
None => {
if search_mode {
"..."
} else {
"[enter] to type..."
}
}
};
let search_input = Paragraph::new(Line::from(search_text))
.fg(Color::Black)
.block(search_input_block);
frame.render_widget(search_title, top_area[1]);
frame.render_widget(search_input, top_area[2]);
// Render the filters
let list_area = container[1];
let mut list_items = Vec::new();
for (filter, enabled) in filters {
let filter = Span::from(filter);
let filter = match enabled {
true => filter.light_yellow(),
false => filter.dark_gray(),
};
list_items.push(filter);
}
list_items.reverse();
let list = List::new(list_items).highlight_symbol("» ");
self.filter_list_state.select(Some(selected_filter_index));
frame.render_stateful_widget(list, list_area, &mut self.filter_list_state);
// Render the keybind list at the bottom.
let keybinds = container[2];
let lines = vec![
Line::from(""),
Line::from("[↑] Up").white(),
Line::from("[↓] Down").white(),
Line::from("[←] Remove").white(),
Line::from("[→] Toggle").white(),
Line::from("[enter] Type / Submit").white(),
];
let text = Text::from(lines);
frame.render_widget(text, keybinds);
}
/// Returns the height of the console TUI area in number of lines.
pub fn get_console_size(&self) -> (u16, u16) {
(self.console[0].width, self.console[0].height)
}
/// Render the current scroll position at the top right corner of the frame
pub(crate) fn render_current_scroll(
&self,
scroll_position: u16,
lines: u16,
console_height: u16,
frame: &mut Frame<'_>,
) {
let mut row = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(1)])
.split(self.console[0])[0];
// Hack: shove upwards the text to overlap with the border so text selection doesn't accidentally capture the number
row.y -= 1;
let max_scroll = lines.saturating_sub(console_height);
if max_scroll == 0 {
return;
}
let remaining_lines = max_scroll.saturating_sub(scroll_position);
if remaining_lines != 0 {
let text = vec![Span::from(format!(" {remaining_lines}⬇ ")).dark_gray()];
frame.render_widget(
Paragraph::new(Line::from(text))
.alignment(Alignment::Right)
.block(Block::default()),
row,
);
}
}
}
/// Generate a string with a specified number of spaces.
fn build_msg_padding(padding_len: usize) -> String {
let mut padding = String::new();
for _ in 0..padding_len {
_ = write!(padding, " ");
}
padding
}
+2 -1
View File
@@ -1,3 +1,4 @@
use crate::TraceSrc;
use crate::{Error, Result};
use dioxus_cli_config::WebProxyConfig;
@@ -123,7 +124,7 @@ pub(crate) fn proxy_to(
}
fn handle_proxy_error(e: Error) -> axum::http::Response<axum::body::Body> {
tracing::error!("Proxy error: {}", e);
tracing::error!(dx_src = ?TraceSrc::Dev, "Proxy error: {}", e);
axum::http::Response::builder()
.status(axum::http::StatusCode::INTERNAL_SERVER_ERROR)
.body(axum::body::Body::from(format!(
+9 -5
View File
@@ -1,5 +1,5 @@
use crate::dioxus_crate::DioxusCrate;
use crate::serve::{next_or_pending, Serve};
use crate::{dioxus_crate::DioxusCrate, TraceSrc};
use crate::{Error, Result};
use axum::extract::{Request, State};
use axum::middleware::{self, Next};
@@ -7,7 +7,7 @@ use axum::{
body::Body,
extract::{
ws::{Message, WebSocket},
Extension, WebSocketUpgrade,
WebSocketUpgrade,
},
http::{
header::{HeaderName, HeaderValue, CACHE_CONTROL, EXPIRES, PRAGMA},
@@ -15,7 +15,7 @@ use axum::{
},
response::IntoResponse,
routing::{get, get_service},
Router,
Extension, Router,
};
use axum_server::tls_rustls::RustlsConfig;
use dioxus_cli_config::{Platform, WebHttpsConfig};
@@ -569,8 +569,12 @@ pub fn get_rustls_with_mkcert(web_config: &WebHttpsConfig) -> Result<(String, St
match cmd {
Err(e) => {
match e.kind() {
io::ErrorKind::NotFound => tracing::error!("mkcert is not installed. See https://github.com/FiloSottile/mkcert#installation for installation instructions."),
e => tracing::error!("an error occurred while generating mkcert certificates: {}", e.to_string()),
io::ErrorKind::NotFound => {
tracing::error!(dx_src = ?TraceSrc::Dev, "`mkcert` is not installed. See https://github.com/FiloSottile/mkcert#installation for installation instructions.")
}
e => {
tracing::error!(dx_src = ?TraceSrc::Dev, "An error occurred while generating mkcert certificates: {}", e.to_string())
}
};
return Err("failed to generate mkcert certificates".into());
}
+2 -1
View File
@@ -3,6 +3,7 @@ use std::{fs, path::PathBuf, time::Duration};
use super::hot_reloading_file_map::HotreloadError;
use crate::serve::hot_reloading_file_map::FileMap;
use crate::TraceSrc;
use crate::{cli::serve::Serve, dioxus_crate::DioxusCrate};
use dioxus_hot_reload::HotReloadMsg;
use dioxus_html::HtmlCtx;
@@ -258,7 +259,7 @@ impl Watcher {
Err(HotreloadError::Parse) => {}
// Otherwise just log the error
Err(err) => {
tracing::error!("Error hotreloading file {rust_file:?}: {err}")
tracing::error!(dx_src = ?TraceSrc::Dev, "Error hotreloading file {rust_file:?}: {err}")
}
}
}