nu-explore: Refactorings (#10247)

1. Added mode to the status bar right most corner
2. Added a command name with a status when run

ref #8582 
cc: @fdncred
This commit is contained in:
Maxim Zhiburt 2023-09-06 18:24:24 +00:00 committed by GitHub
parent 7486850357
commit 99caad7d60
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 356 additions and 195 deletions

View File

@ -182,6 +182,10 @@ fn style_from_config(config: &HashMap<String, Value>) -> StyleConfig {
style.status_info = *s;
}
if let Some(s) = colors.get("success") {
style.status_success = *s;
}
if let Some(s) = colors.get("warn") {
style.status_warn = *s;
}
@ -208,6 +212,8 @@ fn prepare_default_config(config: &mut HashMap<String, Value>) {
const STATUS_INFO: Style = color(None, None);
const STATUS_SUCCESS: Style = color(Some(Color::Black), Some(Color::Green));
const STATUS_WARN: Style = color(None, None);
const TABLE_SPLIT_LINE: Style = color(Some(Color::Rgb(64, 64, 64)), None);
@ -245,6 +251,7 @@ fn prepare_default_config(config: &mut HashMap<String, Value>) {
.unwrap_or_default();
insert_style(&mut hm, "info", STATUS_INFO);
insert_style(&mut hm, "success", STATUS_SUCCESS);
insert_style(&mut hm, "warn", STATUS_WARN);
insert_style(&mut hm, "error", STATUS_ERROR);

View File

@ -134,6 +134,7 @@ fn create_config_command(commands: &[Command]) -> ConfigCmd {
let mut config = ConfigCmd::from_commands(commands.to_vec());
config.register_group(ConfigOption::new(GROUP, "Status bar information color", "status.info", default_color_list()));
config.register_group(ConfigOption::new(GROUP, "Status bar success color", "status.success", default_color_list()));
config.register_group(ConfigOption::new(GROUP, "Status bar warning color", "status.warn", default_color_list()));
config.register_group(ConfigOption::new(GROUP, "Status bar error color", "status.error", default_color_list()));

View File

@ -17,7 +17,7 @@ pub type CtrlC = Option<Arc<AtomicBool>>;
pub use command::{is_ignored_command, run_command_with_value, run_nu_command};
pub use lscolor::{create_lscolors, lscolorize};
pub use string::truncate_str;
pub use string::{string_width, truncate_str};
pub use table::try_build_table;
pub use value::{collect_input, collect_pipeline, create_map, map_into_value, nu_str};

View File

@ -1,4 +1,6 @@
use nu_table::{string_truncate, string_width};
use nu_table::string_truncate;
pub use nu_table::string_width;
pub fn truncate_str(text: &mut String, width: usize) {
if width == 0 {

View File

@ -6,6 +6,7 @@ mod status_bar;
use std::{
cmp::min,
io::{self, Result, Stdout},
result,
sync::atomic::Ordering,
};
@ -80,6 +81,7 @@ struct CommandBuf {
#[derive(Debug, Default, Clone)]
pub struct StyleConfig {
pub status_info: NuStyle,
pub status_success: NuStyle,
pub status_warn: NuStyle,
pub status_error: NuStyle,
pub status_bar_background: NuStyle,
@ -125,6 +127,7 @@ impl<'a> Pager<'a> {
}
["highlight"] => value_as_style(&mut self.config.style.highlight, &value),
["status", "info"] => value_as_style(&mut self.config.style.status_info, &value),
["status", "success"] => value_as_style(&mut self.config.style.status_success, &value),
["status", "warn"] => value_as_style(&mut self.config.style.status_warn, &value),
["status", "error"] => value_as_style(&mut self.config.style.status_error, &value),
path => set_config(&mut self.config.config, path, value),
@ -245,13 +248,12 @@ fn render_ui(
ctrlc: CtrlC,
pager: &mut Pager<'_>,
info: &mut ViewInfo,
mut view: Option<Page>,
view: Option<Page>,
commands: CommandRegistry,
) -> Result<Option<Value>> {
let events = UIEvents::new();
let mut view_stack = Vec::new();
let mut view_stack = ViewStack::new(view, Vec::new());
// let mut command_view = None;
loop {
// handle CTRLC event
if let Some(ctrlc) = ctrlc.clone() {
@ -264,39 +266,11 @@ fn render_ui(
{
let info = info.clone();
term.draw(|f| {
let area = f.size();
let available_area =
Rect::new(area.x, area.y, area.width, area.height.saturating_sub(2));
if let Some(page) = &mut view {
let cfg = ViewConfig::new(
pager.config.nu_config,
pager.config.style_computer,
&pager.config.config,
pager.config.lscolors,
);
page.view.draw(f, available_area, cfg, &mut layout);
}
if let Some(report) = info.status {
let last_2nd_line = area.bottom().saturating_sub(2);
let area = Rect::new(area.left(), last_2nd_line, area.width, 1);
render_status_bar(f, area, report, &pager.config.style);
}
{
let last_line = area.bottom().saturating_sub(1);
let area = Rect::new(area.left(), last_line, area.width, 1);
render_cmd_bar(f, area, pager, info.report, &pager.config.style);
}
highlight_search_results(f, pager, &layout, pager.config.style.highlight);
set_cursor_cmd_bar(f, area, pager);
draw_frame(f, &mut view_stack.view, pager, &mut layout, info);
})?;
}
let status = handle_events(
let transition = handle_events(
engine_state,
stack,
&events,
@ -304,42 +278,36 @@ fn render_ui(
info,
&mut pager.search_buf,
&mut pager.cmd_buf,
view.as_mut().map(|p| &mut p.view),
view_stack.view.as_mut().map(|p| &mut p.view),
);
if let Some(status) = status {
match status {
Transition::Exit => {
break Ok(try_to_peek_value(pager, view.as_mut().map(|p| &mut p.view)));
}
Transition::Ok => {
if view_stack.is_empty() && pager.config.exit_esc {
break Ok(try_to_peek_value(pager, view.as_mut().map(|p| &mut p.view)));
}
if let Some(transition) = transition {
let (exit, cmd_name) = react_to_event_result(
transition,
engine_state,
&commands,
pager,
&mut view_stack,
stack,
info,
);
// try to pop the view stack
if let Some(v) = view_stack.pop() {
view = Some(v);
}
}
Transition::Cmd(command) => {
let out = pager_run_command(
engine_state,
stack,
pager,
&mut view,
&mut view_stack,
&commands,
command,
);
match out {
Ok(false) => {}
Ok(true) => {
break Ok(try_to_peek_value(pager, view.as_mut().map(|p| &mut p.view)))
}
Err(err) => info.report = Some(Report::error(err)),
}
if let Some(value) = exit {
break Ok(value);
}
if !cmd_name.is_empty() {
if let Some(r) = info.report.as_mut() {
r.message = cmd_name;
r.level = Severity::Success;
} else {
info.report = Some(Report::success(cmd_name));
}
let info = info.clone();
term.draw(|f| {
draw_info(f, pager, info);
})?;
}
}
@ -348,50 +316,144 @@ fn render_ui(
pager.cmd_buf.run_cmd = false;
pager.cmd_buf.buf_cmd2 = String::new();
let out = pager_run_command(
engine_state,
stack,
pager,
&mut view,
&mut view_stack,
&commands,
args,
);
let out =
pager_run_command(engine_state, stack, pager, &mut view_stack, &commands, args);
match out {
Ok(false) => {}
Ok(true) => break Ok(try_to_peek_value(pager, view.as_mut().map(|p| &mut p.view))),
Ok(result) => {
if result.exit {
break Ok(peak_value_from_view(&mut view_stack.view, pager));
}
if result.view_change && !result.cmd_name.is_empty() {
if let Some(r) = info.report.as_mut() {
r.message = result.cmd_name;
r.level = Severity::Success;
} else {
info.report = Some(Report::success(result.cmd_name));
}
let info = info.clone();
term.draw(|f| {
draw_info(f, pager, info);
})?;
}
}
Err(err) => info.report = Some(Report::error(err)),
}
}
}
}
#[allow(clippy::too_many_arguments)]
fn react_to_event_result(
status: Transition,
engine_state: &EngineState,
commands: &CommandRegistry,
pager: &mut Pager<'_>,
view_stack: &mut ViewStack,
stack: &mut Stack,
info: &mut ViewInfo,
) -> (Option<Option<Value>>, String) {
match status {
Transition::Exit => (
Some(peak_value_from_view(&mut view_stack.view, pager)),
String::default(),
),
Transition::Ok => {
let exit = view_stack.stack.is_empty() && pager.config.exit_esc;
if exit {
return (
Some(peak_value_from_view(&mut view_stack.view, pager)),
String::default(),
);
}
// try to pop the view stack
if let Some(v) = view_stack.stack.pop() {
view_stack.view = Some(v);
}
(None, String::default())
}
Transition::Cmd(cmd) => {
let out = pager_run_command(engine_state, stack, pager, view_stack, commands, cmd);
match out {
Ok(result) if result.exit => (
Some(peak_value_from_view(&mut view_stack.view, pager)),
String::default(),
),
Ok(result) => (None, result.cmd_name),
Err(err) => {
info.report = Some(Report::error(err));
(None, String::default())
}
}
}
}
}
fn peak_value_from_view(view: &mut Option<Page>, pager: &mut Pager<'_>) -> Option<Value> {
let view = view.as_mut().map(|p| &mut p.view);
try_to_peek_value(pager, view)
}
fn draw_frame(
f: &mut Frame,
view: &mut Option<Page>,
pager: &mut Pager<'_>,
layout: &mut Layout,
info: ViewInfo,
) {
let area = f.size();
let available_area = Rect::new(area.x, area.y, area.width, area.height.saturating_sub(2));
if let Some(page) = view {
let cfg = create_view_config(pager);
page.view.draw(f, available_area, cfg, layout);
}
draw_info(f, pager, info);
highlight_search_results(f, pager, layout, pager.config.style.highlight);
set_cursor_cmd_bar(f, area, pager);
}
fn draw_info(f: &mut Frame, pager: &mut Pager<'_>, info: ViewInfo) {
let area = f.size();
if let Some(report) = info.status {
let last_2nd_line = area.bottom().saturating_sub(2);
let area = Rect::new(area.left(), last_2nd_line, area.width, 1);
render_status_bar(f, area, report, &pager.config.style);
}
{
let last_line = area.bottom().saturating_sub(1);
let area = Rect::new(area.left(), last_line, area.width, 1);
render_cmd_bar(f, area, pager, info.report, &pager.config.style);
}
}
fn create_view_config<'a>(pager: &'a Pager<'_>) -> ViewConfig<'a> {
let cfg = &pager.config;
ViewConfig::new(cfg.nu_config, cfg.style_computer, &cfg.config, cfg.lscolors)
}
fn pager_run_command(
engine_state: &EngineState,
stack: &mut Stack,
pager: &mut Pager,
view: &mut Option<Page>,
view_stack: &mut Vec<Page>,
view_stack: &mut ViewStack,
commands: &CommandRegistry,
args: String,
) -> std::result::Result<bool, String> {
) -> result::Result<CmdResult, String> {
let command = commands.find(&args);
handle_command(engine_state, stack, pager, view, view_stack, command, &args)
}
fn handle_command(
engine_state: &EngineState,
stack: &mut Stack,
pager: &mut Pager,
view: &mut Option<Page>,
view_stack: &mut Vec<Page>,
command: Option<Result<Command>>,
args: &str,
) -> std::result::Result<bool, String> {
match command {
Some(Ok(command)) => {
run_command(engine_state, stack, pager, view, view_stack, command, args)
let result = run_command(engine_state, stack, pager, view_stack, command);
match result {
Ok(value) => Ok(value),
Err(err) => Err(format!("Error: command {args:?} failed: {err}")),
}
}
Some(Err(err)) => Err(format!(
"Error: command {args:?} was not provided with correct arguments: {err}"
@ -404,80 +466,62 @@ fn run_command(
engine_state: &EngineState,
stack: &mut Stack,
pager: &mut Pager,
view: &mut Option<Page>,
view_stack: &mut Vec<Page>,
view_stack: &mut ViewStack,
command: Command,
args: &str,
) -> std::result::Result<bool, String> {
) -> Result<CmdResult> {
match command {
Command::Reactive(mut command) => {
// what we do we just replace the view.
let value = view.as_mut().and_then(|p| p.view.exit());
let result = command.react(engine_state, stack, pager, value);
match result {
Ok(transition) => match transition {
Transition::Ok => {
// so we basically allow a change of a config inside a command,
// and cause of this we wanna update all of our views.
//
// THOUGH: MOST LIKELY IT WON'T BE CHANGED AND WE DO A WASTE.......
let value = view_stack.view.as_mut().and_then(|p| p.view.exit());
let transition = command.react(engine_state, stack, pager, value)?;
match transition {
Transition::Ok => {
// so we basically allow a change of a config inside a command,
// and cause of this we wanna update all of our views.
//
// THOUGH: MOST LIKELY IT WON'T BE CHANGED AND WE DO A WASTE.......
{
if let Some(page) = view.as_mut() {
page.view.setup(ViewConfig::new(
pager.config.nu_config,
pager.config.style_computer,
&pager.config.config,
pager.config.lscolors,
));
}
update_view_stack_setup(view_stack, &pager.config);
for page in view_stack {
page.view.setup(ViewConfig::new(
pager.config.nu_config,
pager.config.style_computer,
&pager.config.config,
pager.config.lscolors,
));
}
}
Ok(false)
}
Transition::Exit => Ok(true),
Transition::Cmd { .. } => todo!("not used so far"),
},
Err(err) => Err(format!("Error: command {args:?} failed: {err}")),
Ok(CmdResult::new(false, false, String::new()))
}
Transition::Exit => Ok(CmdResult::new(true, false, String::new())),
Transition::Cmd { .. } => todo!("not used so far"),
}
}
Command::View { mut cmd, is_light } => {
// what we do we just replace the view.
let value = view.as_mut().and_then(|p| p.view.exit());
let result = cmd.spawn(engine_state, stack, value);
match result {
Ok(mut new_view) => {
if let Some(view) = view.take() {
if !view.is_light {
view_stack.push(view);
}
}
new_view.setup(ViewConfig::new(
pager.config.nu_config,
pager.config.style_computer,
&pager.config.config,
pager.config.lscolors,
));
*view = Some(Page::raw(new_view, is_light));
Ok(false)
let value = view_stack.view.as_mut().and_then(|p| p.view.exit());
let mut new_view = cmd.spawn(engine_state, stack, value)?;
if let Some(view) = view_stack.view.take() {
if !view.is_light {
view_stack.stack.push(view);
}
Err(err) => Err(format!("Error: command {args:?} failed: {err}")),
}
update_view_setup(&mut new_view, &pager.config);
view_stack.view = Some(Page::raw(new_view, is_light));
Ok(CmdResult::new(false, true, cmd.name().to_owned()))
}
}
}
fn update_view_stack_setup(view_stack: &mut ViewStack, cfg: &PagerConfig<'_>) {
if let Some(page) = view_stack.view.as_mut() {
update_view_setup(&mut page.view, cfg);
}
for page in &mut view_stack.stack {
update_view_setup(&mut page.view, cfg);
}
}
fn update_view_setup(view: &mut Box<dyn View>, cfg: &PagerConfig<'_>) {
let cfg = ViewConfig::new(cfg.nu_config, cfg.style_computer, &cfg.config, cfg.lscolors);
view.setup(cfg);
}
fn set_cursor_cmd_bar(f: &mut Frame, area: Rect, pager: &Pager) {
if pager.cmd_buf.is_cmd_input {
// todo: deal with a situation where we exceed the bar width
@ -509,15 +553,25 @@ where
fn render_status_bar(f: &mut Frame, area: Rect, report: Report, theme: &StyleConfig) {
let msg_style = report_msg_style(&report, theme, theme.status_bar_text);
let mut status_bar = StatusBar::new(report.message, report.context, report.context2);
let mut status_bar = create_status_bar(report);
status_bar.set_background_style(theme.status_bar_background);
status_bar.set_message_style(msg_style);
status_bar.set_ctx_style(theme.status_bar_text);
status_bar.set_ctx1_style(theme.status_bar_text);
status_bar.set_ctx2_style(theme.status_bar_text);
status_bar.set_ctx3_style(theme.status_bar_text);
f.render_widget(status_bar, area);
}
fn create_status_bar(report: Report) -> StatusBar {
StatusBar::new(
report.message,
report.context1,
report.context2,
report.context3,
)
}
fn report_msg_style(report: &Report, theme: &StyleConfig, style: NuStyle) -> NuStyle {
if matches!(report.level, Severity::Info) {
style
@ -537,7 +591,7 @@ fn render_cmd_bar(
let style = report_msg_style(&report, theme, theme.cmd_bar_text);
let bar = CommandBar::new(
&report.message,
&report.context,
&report.context1,
style,
theme.cmd_bar_background,
);
@ -1041,6 +1095,7 @@ fn set_config(hm: &mut HashMap<String, Value>, path: &[&str], value: Value) -> b
fn report_level_style(level: Severity, theme: &StyleConfig) -> NuStyle {
match level {
Severity::Info => theme.status_info,
Severity::Success => theme.status_success,
Severity::Warn => theme.status_warn,
Severity::Err => theme.status_error,
}
@ -1082,3 +1137,30 @@ impl Page {
Self::raw(Box::new(view), is_light)
}
}
struct ViewStack {
view: Option<Page>,
stack: Vec<Page>,
}
impl ViewStack {
fn new(view: Option<Page>, stack: Vec<Page>) -> Self {
Self { view, stack }
}
}
struct CmdResult {
exit: bool,
view_change: bool,
cmd_name: String,
}
impl CmdResult {
fn new(exit: bool, view_change: bool, cmd_name: String) -> Self {
Self {
exit,
view_change,
cmd_name,
}
}
}

View File

@ -2,43 +2,61 @@
pub struct Report {
pub message: String,
pub level: Severity,
pub context: String,
pub context1: String,
pub context2: String,
pub context3: String,
}
impl Report {
pub fn new(message: String, level: Severity, context: String, context2: String) -> Self {
pub fn new(message: String, level: Severity, ctx1: String, ctx2: String, ctx3: String) -> Self {
Self {
message,
level,
context,
context2,
context1: ctx1,
context2: ctx2,
context3: ctx3,
}
}
pub fn message(message: impl Into<String>, level: Severity) -> Self {
Self::new(message.into(), level, String::new(), String::new())
Self::new(
message.into(),
level,
String::new(),
String::new(),
String::new(),
)
}
pub fn info(message: impl Into<String>) -> Self {
Self::new(message.into(), Severity::Info, String::new(), String::new())
Self::message(message.into(), Severity::Info)
}
pub fn success(message: impl Into<String>) -> Self {
Self::message(message.into(), Severity::Success)
}
pub fn error(message: impl Into<String>) -> Self {
Self::new(message.into(), Severity::Err, String::new(), String::new())
Self::message(message.into(), Severity::Err)
}
}
impl Default for Report {
fn default() -> Self {
Self::new(String::new(), Severity::Info, String::new(), String::new())
Self::new(
String::new(),
Severity::Info,
String::new(),
String::new(),
String::new(),
)
}
}
#[derive(Debug, Clone, Copy)]
pub enum Severity {
Info,
#[allow(dead_code)]
Success,
Warn,
Err,
}

View File

@ -6,7 +6,7 @@ use ratatui::{
};
use crate::{
nu_common::NuStyle,
nu_common::{string_width, NuStyle},
views::util::{nu_style_to_tui, set_span},
};
@ -14,15 +14,17 @@ pub struct StatusBar {
text: (String, Style),
ctx1: (String, Style),
ctx2: (String, Style),
ctx3: (String, Style),
back_s: Style,
}
impl StatusBar {
pub fn new(text: String, ctx: String, ctx2: String) -> Self {
pub fn new(text: String, ctx1: String, ctx2: String, ctx3: String) -> Self {
Self {
text: (text, Style::default()),
ctx1: (ctx, Style::default()),
ctx1: (ctx1, Style::default()),
ctx2: (ctx2, Style::default()),
ctx3: (ctx3, Style::default()),
back_s: Style::default(),
}
}
@ -31,7 +33,7 @@ impl StatusBar {
self.text.1 = nu_style_to_tui(style).add_modifier(Modifier::BOLD);
}
pub fn set_ctx_style(&mut self, style: NuStyle) {
pub fn set_ctx1_style(&mut self, style: NuStyle) {
self.ctx1.1 = nu_style_to_tui(style).add_modifier(Modifier::BOLD);
}
@ -39,6 +41,10 @@ impl StatusBar {
self.ctx2.1 = nu_style_to_tui(style).add_modifier(Modifier::BOLD);
}
pub fn set_ctx3_style(&mut self, style: NuStyle) {
self.ctx3.1 = nu_style_to_tui(style).add_modifier(Modifier::BOLD);
}
pub fn set_background_style(&mut self, style: NuStyle) {
self.back_s = nu_style_to_tui(style);
}
@ -46,8 +52,9 @@ impl StatusBar {
impl Widget for StatusBar {
fn render(self, area: Rect, buf: &mut Buffer) {
const MAX_CONTEXT_WIDTH: u16 = 12;
const MAX_CONTEXT2_WIDTH: u16 = 12;
const MAX_CTX1_WIDTH: u16 = 12;
const MAX_CTX2_WIDTH: u16 = 12;
const MAX_CTX3_WIDTH: u16 = 12;
// colorize the line
let block = Block::default().style(self.back_s);
@ -55,26 +62,70 @@ impl Widget for StatusBar {
let mut used_width = 0;
let (text, style) = &self.ctx1;
if !text.is_empty() && area.width > MAX_CONTEXT_WIDTH {
let x = area.right().saturating_sub(MAX_CONTEXT_WIDTH);
set_span(buf, (x, area.y), text, *style, MAX_CONTEXT_WIDTH);
let (text, style) = self.ctx1;
let text_width = (string_width(&text) as u16).min(MAX_CTX1_WIDTH);
used_width +=
try_render_text_from_right_most(area, buf, &text, style, used_width, text_width);
used_width += MAX_CONTEXT_WIDTH;
}
let (text, style) = self.ctx2;
used_width +=
try_render_text_from_right_most(area, buf, &text, style, used_width, MAX_CTX2_WIDTH);
let (text, style) = &self.ctx2;
if !text.is_empty() && area.width > MAX_CONTEXT2_WIDTH + used_width {
let x = area.right().saturating_sub(MAX_CONTEXT2_WIDTH + used_width);
set_span(buf, (x, area.y), text, *style, MAX_CONTEXT2_WIDTH);
let (text, style) = self.ctx3;
used_width +=
try_render_text_from_right_most(area, buf, &text, style, used_width, MAX_CTX3_WIDTH);
used_width += MAX_CONTEXT2_WIDTH;
}
let (text, style) = &self.text;
if !text.is_empty() && area.width > used_width {
let rest_width = area.width - used_width;
set_span(buf, (area.x, area.y), text, *style, rest_width);
}
let (text, style) = self.text;
try_render_text_from_left(area, buf, &text, style, used_width);
}
}
fn try_render_text_from_right_most(
area: Rect,
buf: &mut Buffer,
text: &str,
style: Style,
used_width: u16,
span_width: u16,
) -> u16 {
let dis = span_width + used_width;
try_render_text_from_right(area, buf, text, style, dis, used_width, span_width)
}
fn try_render_text_from_right(
area: Rect,
buf: &mut Buffer,
text: &str,
style: Style,
distance_from_right: u16,
used_width: u16,
span_width: u16,
) -> u16 {
let has_space = !text.is_empty() && area.width > used_width;
if !has_space {
return 0;
}
let x = area.right().saturating_sub(distance_from_right);
set_span(buf, (x, area.y), text, style, span_width);
span_width
}
fn try_render_text_from_left(
area: Rect,
buf: &mut Buffer,
text: &str,
style: Style,
used_width: u16,
) -> u16 {
let has_space = !text.is_empty() && area.width > used_width;
if !has_space {
return 0;
}
let rest_width = area.width - used_width;
set_span(buf, (area.x, area.y), text, style, rest_width);
rest_width
}

View File

@ -246,13 +246,13 @@ impl<'a> RecordView<'a> {
let covered_percent = report_row_position(layer.cursor);
let cursor = report_cursor_position(self.mode, layer.cursor);
let message = layer.name.clone().unwrap_or_default();
// note: maybe came up with a better short names? E/V/N?
let mode = match self.mode {
UIMode::Cursor => String::from("EDIT"),
UIMode::View => String::from("VIEW"),
};
Report {
message,
context: covered_percent,
context2: cursor,
level: Severity::Info,
}
Report::new(message, Severity::Info, mode, cursor, covered_percent)
}
}