explore: adopt anyhow, support CustomValue, remove help system (#12692)

This PR:
1. Adds basic support for `CustomValue` to `explore`. Previously `open
foo.db | explore` didn't really work, now we "materialize" the whole
database to a `Value` before loading it
2. Adopts `anyhow` for error handling in `explore`. Previously we were
kind of rolling our own version of `anyhow` by shoving all errors into a
`std::io::Error`; I think this is much nicer. This was necessary because
as part of 1), collecting input is now fallible...
3. Removes a lot of `explore`'s fancy command help system.
- Previously each command (`:help`, `:try`, etc.) had a sophisticated
help system with examples etc... but this was not very visible to users.
You had to know to run `:help :try` or view a list of commands with
`:help :`
- As discussed previously, we eventually want to move to a less modal
approach for `explore`, without the Vim-like commands. And so I don't
think it's worth keeping this command help system around (it's
intertwined with other stuff, and making these changes would have been
harder if keeping it).
4. Rename the `--reverse` flag to `--tail`. The flag scrolls to the end
of the data, which IMO is described better by "tail"
5. Does some renaming+commenting to clear up things I found difficult to
understand when navigating the `explore` code


I initially thought 1) would be just a few lines, and then this PR blew
up into much more extensive changes 😅


## Before
The whole database was being displayed as a single Nuon/JSON line 🤔 

![image](https://github.com/nushell/nushell/assets/26268125/6383f43b-fdff-48b4-9604-398438ad1499)


## After
The database gets displayed like a record

![image](https://github.com/nushell/nushell/assets/26268125/2f00ed7b-a3c4-47f4-a08c-98d07efc7bb4)


## Future work

It is sort of annoying that we have to load a whole SQLite database into
memory to make this work; it will be impractical for large databases.
I'd like to explore improvements to `CustomValue` that can make this
work more efficiently.
This commit is contained in:
Reilly Wood 2024-05-01 15:34:37 -07:00 committed by GitHub
parent bc18cc12d5
commit 3d340657b5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 283 additions and 842 deletions

11
Cargo.lock generated
View file

@ -165,6 +165,12 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "anyhow"
version = "1.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519"
[[package]] [[package]]
name = "arboard" name = "arboard"
version = "3.3.2" version = "3.3.2"
@ -398,7 +404,7 @@ dependencies = [
"bitflags 2.5.0", "bitflags 2.5.0",
"cexpr", "cexpr",
"clang-sys", "clang-sys",
"itertools 0.11.0", "itertools 0.12.1",
"lazy_static", "lazy_static",
"lazycell", "lazycell",
"proc-macro2", "proc-macro2",
@ -3158,7 +3164,9 @@ name = "nu-explore"
version = "0.93.1" version = "0.93.1"
dependencies = [ dependencies = [
"ansi-str", "ansi-str",
"anyhow",
"crossterm", "crossterm",
"log",
"lscolors", "lscolors",
"nu-ansi-term", "nu-ansi-term",
"nu-color-config", "nu-color-config",
@ -3169,6 +3177,7 @@ dependencies = [
"nu-protocol", "nu-protocol",
"nu-table", "nu-table",
"nu-utils", "nu-utils",
"once_cell",
"ratatui", "ratatui",
"strip-ansi-escapes", "strip-ansi-escapes",
"terminal_size", "terminal_size",

View file

@ -64,6 +64,7 @@ members = [
[workspace.dependencies] [workspace.dependencies]
alphanumeric-sort = "1.5" alphanumeric-sort = "1.5"
ansi-str = "0.8" ansi-str = "0.8"
anyhow = "1.0.82"
base64 = "0.22" base64 = "0.22"
bracoxide = "0.1.2" bracoxide = "0.1.2"
brotli = "5.0" brotli = "5.0"

View file

@ -21,9 +21,12 @@ nu-utils = { path = "../nu-utils", version = "0.93.1" }
nu-ansi-term = { workspace = true } nu-ansi-term = { workspace = true }
nu-pretty-hex = { path = "../nu-pretty-hex", version = "0.93.1" } nu-pretty-hex = { path = "../nu-pretty-hex", version = "0.93.1" }
anyhow = { workspace = true }
log = { workspace = true }
terminal_size = { workspace = true } terminal_size = { workspace = true }
strip-ansi-escapes = { workspace = true } strip-ansi-escapes = { workspace = true }
crossterm = { workspace = true } crossterm = { workspace = true }
once_cell = { workspace = true }
ratatui = { workspace = true } ratatui = { workspace = true }
ansi-str = { workspace = true } ansi-str = { workspace = true }
unicode-width = { workspace = true } unicode-width = { workspace = true }

View file

@ -1,14 +1,14 @@
use super::{HelpManual, Shortcode, ViewCommand}; use super::ViewCommand;
use crate::{ use crate::{
nu_common::{self, collect_input}, nu_common::{self, collect_input},
views::Preview, views::Preview,
}; };
use anyhow::Result;
use nu_color_config::StyleComputer; use nu_color_config::StyleComputer;
use nu_protocol::{ use nu_protocol::{
engine::{EngineState, Stack}, engine::{EngineState, Stack},
Value, Value,
}; };
use std::{io::Result, vec};
#[derive(Default, Clone)] #[derive(Default, Clone)]
pub struct ExpandCmd; pub struct ExpandCmd;
@ -34,29 +34,6 @@ impl ViewCommand for ExpandCmd {
"" ""
} }
fn help(&self) -> Option<HelpManual> {
#[rustfmt::skip]
let shortcodes = vec![
Shortcode::new("Up", "", "Moves the viewport one row up"),
Shortcode::new("Down", "", "Moves the viewport one row down"),
Shortcode::new("Left", "", "Moves the viewport one column left"),
Shortcode::new("Right", "", "Moves the viewport one column right"),
Shortcode::new("PgDown", "", "Moves the viewport one page of rows down"),
Shortcode::new("PgUp", "", "Moves the cursor or viewport one page of rows up"),
Shortcode::new("Esc", "", "Exits cursor mode. Exits the currently explored data."),
];
Some(HelpManual {
name: "expand",
description:
"View the currently selected cell's data using the `table` Nushell command",
arguments: vec![],
examples: vec![],
config_options: vec![],
input: shortcodes,
})
}
fn parse(&mut self, _: &str) -> Result<()> { fn parse(&mut self, _: &str) -> Result<()> {
Ok(()) Ok(())
} }
@ -67,27 +44,37 @@ impl ViewCommand for ExpandCmd {
stack: &mut Stack, stack: &mut Stack,
value: Option<Value>, value: Option<Value>,
) -> Result<Self::View> { ) -> Result<Self::View> {
let value = value if let Some(value) = value {
.map(|v| convert_value_to_string(v, engine_state, stack)) let value_as_string = convert_value_to_string(value, engine_state, stack)?;
.unwrap_or_default(); Ok(Preview::new(&value_as_string))
} else {
Ok(Preview::new(&value)) Ok(Preview::new(""))
}
} }
} }
fn convert_value_to_string(value: Value, engine_state: &EngineState, stack: &mut Stack) -> String { fn convert_value_to_string(
let (cols, vals) = collect_input(value.clone()); value: Value,
engine_state: &EngineState,
stack: &mut Stack,
) -> Result<String> {
let (cols, vals) = collect_input(value.clone())?;
let has_no_head = cols.is_empty() || (cols.len() == 1 && cols[0].is_empty()); let has_no_head = cols.is_empty() || (cols.len() == 1 && cols[0].is_empty());
let has_single_value = vals.len() == 1 && vals[0].len() == 1; let has_single_value = vals.len() == 1 && vals[0].len() == 1;
if !has_no_head && has_single_value { if !has_no_head && has_single_value {
let config = engine_state.get_config(); let config = engine_state.get_config();
vals[0][0].to_abbreviated_string(config) Ok(vals[0][0].to_abbreviated_string(config))
} else { } else {
let ctrlc = engine_state.ctrlc.clone(); let ctrlc = engine_state.ctrlc.clone();
let config = engine_state.get_config(); let config = engine_state.get_config();
let style_computer = StyleComputer::from_config(engine_state, stack); let style_computer = StyleComputer::from_config(engine_state, stack);
nu_common::try_build_table(ctrlc, config, &style_computer, value) Ok(nu_common::try_build_table(
ctrlc,
config,
&style_computer,
value,
))
} }
} }

View file

@ -1,76 +1,91 @@
use super::{HelpExample, HelpManual, ViewCommand}; use super::ViewCommand;
use crate::{ use crate::views::Preview;
nu_common::{collect_input, NuSpan}, use anyhow::Result;
pager::{Frame, Transition, ViewInfo}, use nu_ansi_term::Color;
views::{Layout, Preview, RecordView, View, ViewConfig},
};
use crossterm::event::KeyEvent;
use nu_protocol::{ use nu_protocol::{
engine::{EngineState, Stack}, engine::{EngineState, Stack},
record, Value, Value,
};
use ratatui::layout::Rect;
use std::{
collections::HashMap,
io::{self, Result},
}; };
use once_cell::sync::Lazy;
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
pub struct HelpCmd { pub struct HelpCmd {}
input_command: String,
supported_commands: Vec<HelpManual>,
aliases: HashMap<String, Vec<String>>,
}
impl HelpCmd { impl HelpCmd {
pub const NAME: &'static str = "help"; pub const NAME: &'static str = "help";
pub fn view() -> Preview {
const HELP_MESSAGE: &'static str = r#" Explore - main help file Preview::new(&HELP_MESSAGE)
Move around: Use the cursor keys.
Close help: Press "<Esc>".
Exit Explore: Type ":q" then then <Enter> (or press Ctrl+D).
Open an interactive REPL: Type ":try" then enter
List all sub-commands: Type ":help :" then <Enter>
------------------------------------------------------------------------------------
# Regular expressions
Most commands support regular expressions.
You can type "/" and type a pattern you want to search on.
Then hit <Enter> and you will see the search results.
To go to the next hit use "<n>" key.
You also can do a reverse search by using "?" instead of "/".
"#;
pub fn new(commands: Vec<HelpManual>, aliases: &[(&str, &str)]) -> Self {
let aliases = collect_aliases(aliases);
Self {
input_command: String::new(),
supported_commands: commands,
aliases,
}
} }
} }
fn collect_aliases(aliases: &[(&str, &str)]) -> HashMap<String, Vec<String>> { static HELP_MESSAGE: Lazy<String> = Lazy::new(|| {
let mut out_aliases: HashMap<String, Vec<String>> = HashMap::new(); let title = nu_ansi_term::Style::new().bold().underline();
for (name, cmd) in aliases { let code = nu_ansi_term::Style::new().bold().fg(Color::Blue);
out_aliases
.entry(cmd.to_string()) // There is probably a nicer way to do this formatting inline
.and_modify(|list| list.push(name.to_string())) format!(
.or_insert_with(|| vec![name.to_string()]); r#"{}
} Explore helps you dynamically navigate through your data!
out_aliases
} {}
Launch Explore by piping data into it: {}
Move around: Use the cursor keys
Drill down into records+tables: Press <Enter> to select a cell, move around with cursor keys, press <Enter> again
Go back/up a level: Press <Esc>
Transpose (flip rows+columns): Press "t"
Expand (show all nested data): Press "e"
Open this help page : Type ":help" then <Enter>
Open an interactive REPL: Type ":try" then <Enter>
Scroll up/down: Use the "Page Up" and "Page Down" keys
Exit Explore: Type ":q" then <Enter>, or Ctrl+D. Alternately, press <Esc> until Explore exits
{}
Most commands support search via regular expressions.
You can type "/" and type a pattern you want to search on. Then hit <Enter> and you will see the search results.
To go to the next hit use "<n>" key. You also can do a reverse search by using "?" instead of "/".
"#,
title.paint("Explore"),
title.paint("Basics"),
code.paint("ls | explore"),
title.paint("Search")
)
});
// TODO: search help could use some updating... search results get shown immediately after typing, don't need to press Enter
// const HELP_MESSAGE: &str = r#"# Explore
// Explore helps you dynamically navigate through your data
// ## Basics
// Move around: Use the cursor keys
// Drill down into records+tables: Press <Enter> to select a cell, move around with cursor keys, then press <Enter> again
// Go back/up a level: Press <Esc>
// Transpose data (flip rows and columns): Press "t"
// Expand data (show all nested data): Press "e"
// Open this help page : Type ":help" then <Enter>
// Open an interactive REPL: Type ":try" then <Enter>
// Scroll up/down: Use the "Page Up" and "Page Down" keys
// Exit Explore: Type ":q" then <Enter>, or Ctrl+D. Alternately, press <Esc> until Explore exits
// ## Search
// Most commands support search via regular expressions.
// You can type "/" and type a pattern you want to search on.
// Then hit <Enter> and you will see the search results.
// To go to the next hit use "<n>" key.
// You also can do a reverse search by using "?" instead of "/".
// "#;
impl ViewCommand for HelpCmd { impl ViewCommand for HelpCmd {
type View = HelpView<'static>; type View = Preview;
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
Self::NAME Self::NAME
@ -80,260 +95,11 @@ impl ViewCommand for HelpCmd {
"" ""
} }
fn help(&self) -> Option<HelpManual> { fn parse(&mut self, _: &str) -> Result<()> {
#[rustfmt::skip]
let examples = vec![
HelpExample::new("help", "Open the help page for all of `explore`"),
HelpExample::new("help :nu", "Open the help page for the `nu` explore command"),
HelpExample::new("help :help", "...It was supposed to be hidden....until...now..."),
];
#[rustfmt::skip]
let arguments = vec![
HelpExample::new("help :command", "you can provide a command and a help information for it will be displayed")
];
Some(HelpManual {
name: "help",
description: "Explore the help page for `explore`",
arguments,
examples,
input: vec![],
config_options: vec![],
})
}
fn parse(&mut self, args: &str) -> Result<()> {
args.trim().clone_into(&mut self.input_command);
Ok(()) Ok(())
} }
fn spawn(&mut self, _: &EngineState, _: &mut Stack, _: Option<Value>) -> Result<Self::View> { fn spawn(&mut self, _: &EngineState, _: &mut Stack, _: Option<Value>) -> Result<Self::View> {
if self.input_command.is_empty() { Ok(HelpCmd::view())
return Ok(HelpView::Preview(Preview::new(Self::HELP_MESSAGE)));
}
if !self.input_command.starts_with(':') {
return Err(io::Error::new(
io::ErrorKind::Other,
"unexpected help argument",
));
}
if self.input_command == ":" {
let (headers, data) = help_frame_data(&self.supported_commands, &self.aliases);
let view = RecordView::new(headers, data);
return Ok(HelpView::Records(view));
}
let command = self
.input_command
.strip_prefix(':')
.expect("we just checked the prefix");
let manual = self
.supported_commands
.iter()
.find(|manual| manual.name == command)
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "a given command was not found"))?;
let aliases = self
.aliases
.get(manual.name)
.map(|l| l.as_slice())
.unwrap_or(&[]);
let (headers, data) = help_manual_data(manual, aliases);
let view = RecordView::new(headers, data);
Ok(HelpView::Records(view))
}
}
fn help_frame_data(
supported_commands: &[HelpManual],
aliases: &HashMap<String, Vec<String>>,
) -> (Vec<String>, Vec<Vec<Value>>) {
let commands = supported_commands
.iter()
.map(|manual| {
let aliases = aliases
.get(manual.name)
.map(|l| l.as_slice())
.unwrap_or(&[]);
let (cols, mut vals) = help_manual_data(manual, aliases);
let vals = vals.remove(0);
Value::record(cols.into_iter().zip(vals).collect(), NuSpan::unknown())
})
.collect();
let commands = Value::list(commands, NuSpan::unknown());
collect_input(commands)
}
fn help_manual_data(manual: &HelpManual, aliases: &[String]) -> (Vec<String>, Vec<Vec<Value>>) {
fn nu_str(s: &impl ToString) -> Value {
Value::string(s.to_string(), NuSpan::unknown())
}
let arguments = manual
.arguments
.iter()
.map(|e| {
Value::record(
record! {
"example" => nu_str(&e.example),
"description" => nu_str(&e.description),
},
NuSpan::unknown(),
)
})
.collect();
let arguments = Value::list(arguments, NuSpan::unknown());
let examples = manual
.examples
.iter()
.map(|e| {
Value::record(
record! {
"example" => nu_str(&e.example),
"description" => nu_str(&e.description),
},
NuSpan::unknown(),
)
})
.collect();
let examples = Value::list(examples, NuSpan::unknown());
let inputs = manual
.input
.iter()
.map(|e| {
Value::record(
record! {
"name" => nu_str(&e.code),
"context" => nu_str(&e.context),
"description" => nu_str(&e.description),
},
NuSpan::unknown(),
)
})
.collect();
let inputs = Value::list(inputs, NuSpan::unknown());
let configuration = manual
.config_options
.iter()
.map(|o| {
let values = o
.values
.iter()
.map(|v| {
Value::record(
record! {
"example" => nu_str(&v.example),
"description" => nu_str(&v.description),
},
NuSpan::unknown(),
)
})
.collect();
let values = Value::list(values, NuSpan::unknown());
Value::record(
record! {
"name" => nu_str(&o.group),
"context" => nu_str(&o.key),
"description" => nu_str(&o.description),
"values" => values,
},
NuSpan::unknown(),
)
})
.collect();
let configuration = Value::list(configuration, NuSpan::unknown());
let name = nu_str(&manual.name);
let aliases = nu_str(&aliases.join(", "));
let desc = nu_str(&manual.description);
let headers = vec![
String::from("name"),
String::from("aliases"),
String::from("arguments"),
String::from("input"),
String::from("examples"),
String::from("configuration"),
String::from("description"),
];
let data = vec![vec![
name,
aliases,
arguments,
inputs,
examples,
configuration,
desc,
]];
(headers, data)
}
pub enum HelpView<'a> {
Records(RecordView<'a>),
Preview(Preview),
}
impl View for HelpView<'_> {
fn draw(&mut self, f: &mut Frame, area: Rect, cfg: ViewConfig<'_>, layout: &mut Layout) {
match self {
HelpView::Records(v) => v.draw(f, area, cfg, layout),
HelpView::Preview(v) => v.draw(f, area, cfg, layout),
}
}
fn handle_input(
&mut self,
engine_state: &EngineState,
stack: &mut Stack,
layout: &Layout,
info: &mut ViewInfo,
key: KeyEvent,
) -> Option<Transition> {
match self {
HelpView::Records(v) => v.handle_input(engine_state, stack, layout, info, key),
HelpView::Preview(v) => v.handle_input(engine_state, stack, layout, info, key),
}
}
fn show_data(&mut self, i: usize) -> bool {
match self {
HelpView::Records(v) => v.show_data(i),
HelpView::Preview(v) => v.show_data(i),
}
}
fn collect_data(&self) -> Vec<crate::nu_common::NuText> {
match self {
HelpView::Records(v) => v.collect_data(),
HelpView::Preview(v) => v.collect_data(),
}
}
fn exit(&mut self) -> Option<Value> {
match self {
HelpView::Records(v) => v.exit(),
HelpView::Preview(v) => v.exit(),
}
}
fn setup(&mut self, config: ViewConfig<'_>) {
match self {
HelpView::Records(v) => v.setup(config),
HelpView::Preview(v) => v.setup(config),
}
} }
} }

View file

@ -1,9 +1,9 @@
use super::pager::{Pager, Transition}; use super::pager::{Pager, Transition};
use anyhow::Result;
use nu_protocol::{ use nu_protocol::{
engine::{EngineState, Stack}, engine::{EngineState, Stack},
Value, Value,
}; };
use std::{borrow::Cow, io::Result};
mod expand; mod expand;
mod help; mod help;
@ -24,8 +24,6 @@ pub trait SimpleCommand {
fn usage(&self) -> &'static str; fn usage(&self) -> &'static str;
fn help(&self) -> Option<HelpManual>;
fn parse(&mut self, args: &str) -> Result<()>; fn parse(&mut self, args: &str) -> Result<()>;
fn react( fn react(
@ -44,8 +42,6 @@ pub trait ViewCommand {
fn usage(&self) -> &'static str; fn usage(&self) -> &'static str;
fn help(&self) -> Option<HelpManual>;
fn parse(&mut self, args: &str) -> Result<()>; fn parse(&mut self, args: &str) -> Result<()>;
fn spawn( fn spawn(
@ -56,116 +52,9 @@ pub trait ViewCommand {
) -> Result<Self::View>; ) -> Result<Self::View>;
} }
#[derive(Debug, Default, Clone)]
pub struct HelpManual {
pub name: &'static str,
pub description: &'static str,
pub arguments: Vec<HelpExample>,
pub examples: Vec<HelpExample>,
pub config_options: Vec<ConfigOption>,
pub input: Vec<Shortcode>,
}
#[derive(Debug, Default, Clone)]
pub struct HelpExample {
pub example: Cow<'static, str>,
pub description: Cow<'static, str>,
}
impl HelpExample {
pub fn new(
example: impl Into<Cow<'static, str>>,
description: impl Into<Cow<'static, str>>,
) -> Self {
Self {
example: example.into(),
description: description.into(),
}
}
}
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
pub struct Shortcode { pub struct Shortcode {
pub code: &'static str, pub code: &'static str,
pub context: &'static str, pub context: &'static str,
pub description: &'static str, pub description: &'static str,
} }
impl Shortcode {
pub fn new(code: &'static str, context: &'static str, description: &'static str) -> Self {
Self {
code,
context,
description,
}
}
}
#[derive(Debug, Default, Clone)]
pub struct ConfigOption {
pub group: String,
pub description: String,
pub key: String,
pub values: Vec<HelpExample>,
}
impl ConfigOption {
pub fn new<N, D, K>(group: N, description: D, key: K, values: Vec<HelpExample>) -> Self
where
N: Into<String>,
D: Into<String>,
K: Into<String>,
{
Self {
group: group.into(),
description: description.into(),
key: key.into(),
values,
}
}
pub fn boolean<N, D, K>(group: N, description: D, key: K) -> Self
where
N: Into<String>,
D: Into<String>,
K: Into<String>,
{
Self {
group: group.into(),
description: description.into(),
key: key.into(),
values: vec![
HelpExample::new("true", "Turn the flag on"),
HelpExample::new("false", "Turn the flag on"),
],
}
}
}
#[rustfmt::skip]
pub fn default_color_list() -> Vec<HelpExample> {
vec![
HelpExample::new("red", "Red foreground"),
HelpExample::new("blue", "Blue foreground"),
HelpExample::new("green", "Green foreground"),
HelpExample::new("yellow", "Yellow foreground"),
HelpExample::new("magenta", "Magenta foreground"),
HelpExample::new("black", "Black foreground"),
HelpExample::new("white", "White foreground"),
HelpExample::new("#AA4433", "#AA4433 HEX foreground"),
HelpExample::new(r#"{bg: "red"}"#, "Red background"),
HelpExample::new(r#"{bg: "blue"}"#, "Blue background"),
HelpExample::new(r#"{bg: "green"}"#, "Green background"),
HelpExample::new(r#"{bg: "yellow"}"#, "Yellow background"),
HelpExample::new(r#"{bg: "magenta"}"#, "Magenta background"),
HelpExample::new(r#"{bg: "black"}"#, "Black background"),
HelpExample::new(r#"{bg: "white"}"#, "White background"),
HelpExample::new(r##"{bg: "#AA4433"}"##, "#AA4433 HEX background"),
]
}
pub fn default_int_list() -> Vec<HelpExample> {
(0..20)
.map(|i| HelpExample::new(i.to_string(), format!("A value equal to {i}")))
.collect()
}

View file

@ -1,15 +1,15 @@
use super::{HelpExample, HelpManual, ViewCommand}; use super::ViewCommand;
use crate::{ use crate::{
nu_common::{collect_pipeline, has_simple_value, run_command_with_value}, nu_common::{collect_pipeline, has_simple_value, run_command_with_value},
pager::Frame, pager::Frame,
views::{Layout, Orientation, Preview, RecordView, View, ViewConfig}, views::{Layout, Orientation, Preview, RecordView, View, ViewConfig},
}; };
use anyhow::Result;
use nu_protocol::{ use nu_protocol::{
engine::{EngineState, Stack}, engine::{EngineState, Stack},
PipelineData, Value, PipelineData, Value,
}; };
use ratatui::layout::Rect; use ratatui::layout::Rect;
use std::io::{self, Result};
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
pub struct NuCmd { pub struct NuCmd {
@ -37,30 +37,6 @@ impl ViewCommand for NuCmd {
"" ""
} }
fn help(&self) -> Option<HelpManual> {
let examples = vec![
HelpExample::new(
"where type == 'file'",
"Filter data to show only rows whose type is 'file'",
),
HelpExample::new(
"get scope.examples",
"Navigate to a deeper value inside the data",
),
HelpExample::new("open Cargo.toml", "Open a Cargo.toml file"),
];
Some(HelpManual {
name: "nu",
description:
"Run a Nushell command. The data currently being explored is piped into it.",
examples,
arguments: vec![],
input: vec![],
config_options: vec![],
})
}
fn parse(&mut self, args: &str) -> Result<()> { fn parse(&mut self, args: &str) -> Result<()> {
args.trim().clone_into(&mut self.command); args.trim().clone_into(&mut self.command);
@ -75,12 +51,11 @@ impl ViewCommand for NuCmd {
) -> Result<Self::View> { ) -> Result<Self::View> {
let value = value.unwrap_or_default(); let value = value.unwrap_or_default();
let pipeline = run_command_with_value(&self.command, &value, engine_state, stack) let pipeline = run_command_with_value(&self.command, &value, engine_state, stack)?;
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
let is_record = matches!(pipeline, PipelineData::Value(Value::Record { .. }, ..)); let is_record = matches!(pipeline, PipelineData::Value(Value::Record { .. }, ..));
let (columns, values) = collect_pipeline(pipeline); let (columns, values) = collect_pipeline(pipeline)?;
if let Some(value) = has_simple_value(&values) { if let Some(value) = has_simple_value(&values) {
let text = value.to_abbreviated_string(&engine_state.config); let text = value.to_abbreviated_string(&engine_state.config);

View file

@ -1,10 +1,10 @@
use super::{HelpManual, SimpleCommand}; use super::SimpleCommand;
use crate::pager::{Pager, Transition}; use crate::pager::{Pager, Transition};
use anyhow::Result;
use nu_protocol::{ use nu_protocol::{
engine::{EngineState, Stack}, engine::{EngineState, Stack},
Value, Value,
}; };
use std::io::Result;
#[derive(Default, Clone)] #[derive(Default, Clone)]
pub struct QuitCmd; pub struct QuitCmd;
@ -22,17 +22,6 @@ impl SimpleCommand for QuitCmd {
"" ""
} }
fn help(&self) -> Option<HelpManual> {
Some(HelpManual {
name: "quit",
description: "Quit and return to Nushell",
arguments: vec![],
examples: vec![],
input: vec![],
config_options: vec![],
})
}
fn parse(&mut self, _: &str) -> Result<()> { fn parse(&mut self, _: &str) -> Result<()> {
Ok(()) Ok(())
} }

View file

@ -1,17 +1,14 @@
use super::{ use super::ViewCommand;
default_color_list, default_int_list, ConfigOption, HelpExample, HelpManual, Shortcode,
ViewCommand,
};
use crate::{ use crate::{
nu_common::collect_input, nu_common::collect_input,
views::{Orientation, RecordView}, views::{Orientation, RecordView},
}; };
use anyhow::Result;
use nu_ansi_term::Style; use nu_ansi_term::Style;
use nu_protocol::{ use nu_protocol::{
engine::{EngineState, Stack}, engine::{EngineState, Stack},
Value, Value,
}; };
use std::io::Result;
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
pub struct TableCmd { pub struct TableCmd {
@ -52,60 +49,6 @@ impl ViewCommand for TableCmd {
"" ""
} }
fn help(&self) -> Option<HelpManual> {
#[rustfmt::skip]
let shortcuts = vec![
Shortcode::new("Up", "", "Moves the cursor or viewport one row up"),
Shortcode::new("Down", "", "Moves the cursor or viewport one row down"),
Shortcode::new("Left", "", "Moves the cursor or viewport one column left"),
Shortcode::new("Right", "", "Moves the cursor or viewport one column right"),
Shortcode::new("PgDown", "view", "Moves the cursor or viewport one page of rows down"),
Shortcode::new("PgUp", "view", "Moves the cursor or viewport one page of rows up"),
Shortcode::new("Esc", "", "Exits cursor mode. Exits the just explored dataset."),
Shortcode::new("i", "view", "Enters cursor mode to inspect individual cells"),
Shortcode::new("t", "view", "Transpose table, so that columns become rows and vice versa"),
Shortcode::new("e", "view", "Open expand view (equivalent of :expand)"),
Shortcode::new("Enter", "cursor", "In cursor mode, explore the data of the selected cell"),
];
#[rustfmt::skip]
let config_options = vec![
ConfigOption::new(
":table group",
"Used to move column header",
"table.orientation",
vec![
HelpExample::new("top", "Sticks column header to the top"),
HelpExample::new("bottom", "Sticks column header to the bottom"),
HelpExample::new("left", "Sticks column header to the left"),
HelpExample::new("right", "Sticks column header to the right"),
],
),
ConfigOption::boolean(":table group", "Show index", "table.show_index"),
ConfigOption::boolean(":table group", "Show header", "table.show_head"),
ConfigOption::new(":table group", "Color of selected cell", "table.selected_cell", default_color_list()),
ConfigOption::new(":table group", "Color of selected row", "table.selected_row", default_color_list()),
ConfigOption::new(":table group", "Color of selected column", "table.selected_column", default_color_list()),
ConfigOption::new(":table group", "Color of split line", "table.split_line", default_color_list()),
ConfigOption::new(":table group", "Padding column left", "table.padding_column_left", default_int_list()),
ConfigOption::new(":table group", "Padding column right", "table.padding_column_right", default_int_list()),
ConfigOption::new(":table group", "Padding index left", "table.padding_index_left", default_int_list()),
ConfigOption::new(":table group", "Padding index right", "table.padding_index_right", default_int_list()),
];
Some(HelpManual {
name: "table",
description: "Display a table view",
arguments: vec![],
examples: vec![],
config_options,
input: shortcuts,
})
}
fn parse(&mut self, _: &str) -> Result<()> { fn parse(&mut self, _: &str) -> Result<()> {
Ok(()) Ok(())
} }
@ -119,7 +62,7 @@ impl ViewCommand for TableCmd {
let value = value.unwrap_or_default(); let value = value.unwrap_or_default();
let is_record = matches!(value, Value::Record { .. }); let is_record = matches!(value, Value::Record { .. });
let (columns, data) = collect_input(value); let (columns, data) = collect_input(value)?;
let mut view = RecordView::new(columns, data); let mut view = RecordView::new(columns, data);

View file

@ -1,10 +1,10 @@
use super::{default_color_list, ConfigOption, HelpExample, HelpManual, Shortcode, ViewCommand}; use super::ViewCommand;
use crate::views::InteractiveView; use crate::views::InteractiveView;
use anyhow::Result;
use nu_protocol::{ use nu_protocol::{
engine::{EngineState, Stack}, engine::{EngineState, Stack},
Value, Value,
}; };
use std::io::{Error, ErrorKind, Result};
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
pub struct TryCmd { pub struct TryCmd {
@ -32,37 +32,6 @@ impl ViewCommand for TryCmd {
"" ""
} }
fn help(&self) -> Option<HelpManual> {
#[rustfmt::skip]
let shortcuts = vec![
Shortcode::new("Up", "", "Switches between input and a output panes"),
Shortcode::new("Down", "", "Switches between input and a output panes"),
Shortcode::new("Esc", "", "Switches between input and a output panes"),
Shortcode::new("Tab", "", "Switches between input and a output panes"),
];
#[rustfmt::skip]
let config_options = vec![
ConfigOption::boolean(":try options", "In the `:try` REPL, attempt to run the command on every keypress", "try.reactive"),
ConfigOption::new(":try options", "Change a highlighted menu color", "try.highlighted_color", default_color_list()),
];
#[rustfmt::skip]
let examples = vec![
HelpExample::new("try", "Open a interactive :try command"),
HelpExample::new("try open Cargo.toml", "Optionally, you can provide a command which will be run immediately"),
];
Some(HelpManual {
name: "try",
description: "Opens a panel in which to run Nushell commands and explore their output. The explorer acts like `:table`.",
arguments: vec![],
examples,
input: shortcuts,
config_options,
})
}
fn parse(&mut self, args: &str) -> Result<()> { fn parse(&mut self, args: &str) -> Result<()> {
args.trim().clone_into(&mut self.command); args.trim().clone_into(&mut self.command);
@ -78,8 +47,7 @@ impl ViewCommand for TryCmd {
let value = value.unwrap_or_default(); let value = value.unwrap_or_default();
let mut view = InteractiveView::new(value); let mut view = InteractiveView::new(value);
view.init(self.command.clone()); view.init(self.command.clone());
view.try_run(engine_state, stack) view.try_run(engine_state, stack)?;
.map_err(|e| Error::new(ErrorKind::Other, e))?;
Ok(view) Ok(view)
} }

View file

@ -36,9 +36,9 @@ impl Command for Explore {
) )
.switch("index", "Show row indexes when viewing a list", Some('i')) .switch("index", "Show row indexes when viewing a list", Some('i'))
.switch( .switch(
"reverse", "tail",
"Start with the viewport scrolled to the bottom", "Start with the viewport scrolled to the bottom",
Some('r'), Some('t'),
) )
.switch( .switch(
"peek", "peek",
@ -61,7 +61,7 @@ impl Command for Explore {
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let show_head: bool = call.get_flag(engine_state, stack, "head")?.unwrap_or(true); let show_head: bool = call.get_flag(engine_state, stack, "head")?.unwrap_or(true);
let show_index: bool = call.has_flag(engine_state, stack, "index")?; let show_index: bool = call.has_flag(engine_state, stack, "index")?;
let is_reverse: bool = call.has_flag(engine_state, stack, "reverse")?; let tail: bool = call.has_flag(engine_state, stack, "tail")?;
let peek_value: bool = call.has_flag(engine_state, stack, "peek")?; let peek_value: bool = call.has_flag(engine_state, stack, "peek")?;
let ctrlc = engine_state.ctrlc.clone(); let ctrlc = engine_state.ctrlc.clone();
@ -79,19 +79,31 @@ impl Command for Explore {
let mut config = PagerConfig::new(nu_config, &style_computer, &lscolors, config); let mut config = PagerConfig::new(nu_config, &style_computer, &lscolors, config);
config.style = style; config.style = style;
config.reverse = is_reverse;
config.peek_value = peek_value; config.peek_value = peek_value;
config.reverse = is_reverse; config.tail = tail;
let result = run_pager(engine_state, &mut stack.clone(), ctrlc, input, config); let result = run_pager(engine_state, &mut stack.clone(), ctrlc, input, config);
match result { match result {
Ok(Some(value)) => Ok(PipelineData::Value(value, None)), Ok(Some(value)) => Ok(PipelineData::Value(value, None)),
Ok(None) => Ok(PipelineData::Value(Value::default(), None)), Ok(None) => Ok(PipelineData::Value(Value::default(), None)),
Err(err) => Ok(PipelineData::Value( Err(err) => {
Value::error(err.into(), call.head), let shell_error = match err.downcast::<ShellError>() {
None, Ok(e) => e,
)), Err(e) => ShellError::GenericError {
error: e.to_string(),
msg: "".into(),
span: None,
help: None,
inner: vec![],
},
};
Ok(PipelineData::Value(
Value::error(shell_error, call.head),
None,
))
}
} }
} }

View file

@ -6,20 +6,19 @@ mod pager;
mod registry; mod registry;
mod views; mod views;
use anyhow::Result;
use commands::{ExpandCmd, HelpCmd, NuCmd, QuitCmd, TableCmd, TryCmd};
pub use default_context::add_explore_context; pub use default_context::add_explore_context;
pub use explore::Explore; pub use explore::Explore;
use commands::{ExpandCmd, HelpCmd, HelpManual, NuCmd, QuitCmd, TableCmd, TryCmd};
use nu_common::{collect_pipeline, has_simple_value, CtrlC}; use nu_common::{collect_pipeline, has_simple_value, CtrlC};
use nu_protocol::{ use nu_protocol::{
engine::{EngineState, Stack}, engine::{EngineState, Stack},
PipelineData, Value, PipelineData, Value,
}; };
use pager::{Page, Pager, PagerConfig, StyleConfig}; use pager::{Page, Pager, PagerConfig, StyleConfig};
use registry::{Command, CommandRegistry}; use registry::CommandRegistry;
use std::io;
use terminal_size::{Height, Width}; use terminal_size::{Height, Width};
use views::{BinaryView, InformationView, Orientation, Preview, RecordView}; use views::{BinaryView, Orientation, Preview, RecordView};
mod util { mod util {
pub use super::nu_common::{create_lscolors, create_map, map_into_value}; pub use super::nu_common::{create_lscolors, create_map, map_into_value};
@ -31,7 +30,7 @@ fn run_pager(
ctrlc: CtrlC, ctrlc: CtrlC,
input: PipelineData, input: PipelineData,
config: PagerConfig, config: PagerConfig,
) -> io::Result<Option<Value>> { ) -> Result<Option<Value>> {
let mut p = Pager::new(config.clone()); let mut p = Pager::new(config.clone());
let commands = create_command_registry(); let commands = create_command_registry();
@ -45,18 +44,18 @@ fn run_pager(
return p.run(engine_state, stack, ctrlc, view, commands); return p.run(engine_state, stack, ctrlc, view, commands);
} }
let (columns, data) = collect_pipeline(input); let (columns, data) = collect_pipeline(input)?;
let has_no_input = columns.is_empty() && data.is_empty(); let has_no_input = columns.is_empty() && data.is_empty();
if has_no_input { if has_no_input {
return p.run(engine_state, stack, ctrlc, information_view(), commands); return p.run(engine_state, stack, ctrlc, help_view(), commands);
} }
p.show_message("For help type :help"); p.show_message("For help type :help");
if let Some(value) = has_simple_value(&data) { if let Some(value) = has_simple_value(&data) {
let text = value.to_abbreviated_string(config.nu_config); let text = value.to_abbreviated_string(config.nu_config);
let view = Some(Page::new(Preview::new(&text), true)); let view = Some(Page::new(Preview::new(&text), false));
return p.run(engine_state, stack, ctrlc, view, commands); return p.run(engine_state, stack, ctrlc, view, commands);
} }
@ -67,6 +66,7 @@ fn run_pager(
fn create_record_view( fn create_record_view(
columns: Vec<String>, columns: Vec<String>,
data: Vec<Vec<Value>>, data: Vec<Vec<Value>>,
// wait, why would we use RecordView for something that isn't a record?
is_record: bool, is_record: bool,
config: PagerConfig, config: PagerConfig,
) -> Option<Page> { ) -> Option<Page> {
@ -75,17 +75,17 @@ fn create_record_view(
view.set_orientation_current(Orientation::Left); view.set_orientation_current(Orientation::Left);
} }
if config.reverse { if config.tail {
if let Some((Width(w), Height(h))) = terminal_size::terminal_size() { if let Some((Width(w), Height(h))) = terminal_size::terminal_size() {
view.reverse(w, h); view.tail(w, h);
} }
} }
Some(Page::new(view, false)) Some(Page::new(view, true))
} }
fn information_view() -> Option<Page> { fn help_view() -> Option<Page> {
Some(Page::new(InformationView, true)) Some(Page::new(HelpCmd::view(), false))
} }
fn binary_view(input: PipelineData) -> Option<Page> { fn binary_view(input: PipelineData) -> Option<Page> {
@ -96,7 +96,7 @@ fn binary_view(input: PipelineData) -> Option<Page> {
let view = BinaryView::new(data); let view = BinaryView::new(data);
Some(Page::new(view, false)) Some(Page::new(view, true))
} }
fn create_command_registry() -> CommandRegistry { fn create_command_registry() -> CommandRegistry {
@ -104,24 +104,16 @@ fn create_command_registry() -> CommandRegistry {
create_commands(&mut registry); create_commands(&mut registry);
create_aliases(&mut registry); create_aliases(&mut registry);
// reregister help && config commands
let commands = registry.get_commands().cloned().collect::<Vec<_>>();
let aliases = registry.get_aliases().collect::<Vec<_>>();
let help_cmd = create_help_command(&commands, &aliases);
registry.register_command_view(help_cmd, true);
registry registry
} }
fn create_commands(registry: &mut CommandRegistry) { fn create_commands(registry: &mut CommandRegistry) {
registry.register_command_view(NuCmd::new(), false); registry.register_command_view(NuCmd::new(), true);
registry.register_command_view(TableCmd::new(), false); registry.register_command_view(TableCmd::new(), true);
registry.register_command_view(ExpandCmd::new(), true); registry.register_command_view(ExpandCmd::new(), false);
registry.register_command_view(TryCmd::new(), true); registry.register_command_view(TryCmd::new(), false);
registry.register_command_view(HelpCmd::default(), true); registry.register_command_view(HelpCmd::default(), false);
registry.register_command_reactive(QuitCmd); registry.register_command_reactive(QuitCmd);
} }
@ -132,34 +124,3 @@ fn create_aliases(registry: &mut CommandRegistry) {
registry.create_aliases("q", QuitCmd::NAME); registry.create_aliases("q", QuitCmd::NAME);
registry.create_aliases("q!", QuitCmd::NAME); registry.create_aliases("q!", QuitCmd::NAME);
} }
fn create_help_command(commands: &[Command], aliases: &[(&str, &str)]) -> HelpCmd {
let help_manuals = create_help_manuals(commands);
HelpCmd::new(help_manuals, aliases)
}
fn create_help_manuals(cmd_list: &[Command]) -> Vec<HelpManual> {
cmd_list.iter().map(create_help_manual).collect()
}
fn create_help_manual(cmd: &Command) -> HelpManual {
let name = match cmd {
Command::Reactive(cmd) => cmd.name(),
Command::View { cmd, .. } => cmd.name(),
};
let manual = match cmd {
Command::Reactive(cmd) => cmd.help(),
Command::View { cmd, .. } => cmd.help(),
};
__create_help_manual(manual, name)
}
fn __create_help_manual(manual: Option<HelpManual>, name: &'static str) -> HelpManual {
manual.unwrap_or(HelpManual {
name,
..HelpManual::default()
})
}

View file

@ -1,13 +1,14 @@
use super::NuSpan; use super::NuSpan;
use anyhow::Result;
use nu_engine::get_columns; use nu_engine::get_columns;
use nu_protocol::{record, ListStream, PipelineData, PipelineMetadata, RawStream, Value}; use nu_protocol::{record, ListStream, PipelineData, PipelineMetadata, RawStream, Value};
use std::collections::HashMap; use std::collections::HashMap;
pub fn collect_pipeline(input: PipelineData) -> (Vec<String>, Vec<Vec<Value>>) { pub fn collect_pipeline(input: PipelineData) -> Result<(Vec<String>, Vec<Vec<Value>>)> {
match input { match input {
PipelineData::Empty => (vec![], vec![]), PipelineData::Empty => Ok((vec![], vec![])),
PipelineData::Value(value, ..) => collect_input(value), PipelineData::Value(value, ..) => collect_input(value),
PipelineData::ListStream(stream, ..) => collect_list_stream(stream), PipelineData::ListStream(stream, ..) => Ok(collect_list_stream(stream)),
PipelineData::ExternalStream { PipelineData::ExternalStream {
stdout, stdout,
stderr, stderr,
@ -15,7 +16,9 @@ pub fn collect_pipeline(input: PipelineData) -> (Vec<String>, Vec<Vec<Value>>) {
metadata, metadata,
span, span,
.. ..
} => collect_external_stream(stdout, stderr, exit_code, metadata, span), } => Ok(collect_external_stream(
stdout, stderr, exit_code, metadata, span,
)),
} }
} }
@ -83,12 +86,12 @@ fn collect_external_stream(
} }
/// Try to build column names and a table grid. /// Try to build column names and a table grid.
pub fn collect_input(value: Value) -> (Vec<String>, Vec<Vec<Value>>) { pub fn collect_input(value: Value) -> Result<(Vec<String>, Vec<Vec<Value>>)> {
let span = value.span(); let span = value.span();
match value { match value {
Value::Record { val: record, .. } => { Value::Record { val: record, .. } => {
let (key, val) = record.into_owned().into_iter().unzip(); let (key, val) = record.into_owned().into_iter().unzip();
(key, vec![val]) Ok((key, vec![val]))
} }
Value::List { vals, .. } => { Value::List { vals, .. } => {
let mut columns = get_columns(&vals); let mut columns = get_columns(&vals);
@ -98,7 +101,7 @@ pub fn collect_input(value: Value) -> (Vec<String>, Vec<Vec<Value>>) {
columns = vec![String::from("")]; columns = vec![String::from("")];
} }
(columns, data) Ok((columns, data))
} }
Value::String { val, .. } => { Value::String { val, .. } => {
let lines = val let lines = val
@ -107,17 +110,18 @@ pub fn collect_input(value: Value) -> (Vec<String>, Vec<Vec<Value>>) {
.map(|val| vec![val]) .map(|val| vec![val])
.collect(); .collect();
(vec![String::from("")], lines) Ok((vec![String::from("")], lines))
} }
Value::LazyRecord { val, .. } => match val.collect() { Value::LazyRecord { val, .. } => {
Ok(value) => collect_input(value), let materialized = val.collect()?;
Err(_) => ( collect_input(materialized)
vec![String::from("")], }
vec![vec![Value::lazy_record(val, span)]], Value::Nothing { .. } => Ok((vec![], vec![])),
), Value::Custom { val, .. } => {
}, let materialized = val.to_base_value(span)?;
Value::Nothing { .. } => (vec![], vec![]), collect_input(materialized)
value => (vec![String::from("")], vec![vec![value]]), }
value => Ok((vec![String::from("")], vec![vec![value]])),
} }
} }

View file

@ -15,6 +15,7 @@ use crate::{
util::map_into_value, util::map_into_value,
views::{util::nu_style_to_tui, ViewConfig}, views::{util::nu_style_to_tui, ViewConfig},
}; };
use anyhow::Result;
use crossterm::{ use crossterm::{
event::{KeyCode, KeyEvent, KeyModifiers}, event::{KeyCode, KeyEvent, KeyModifiers},
execute, execute,
@ -34,7 +35,7 @@ use ratatui::{backend::CrosstermBackend, layout::Rect, widgets::Block};
use std::{ use std::{
cmp::min, cmp::min,
collections::HashMap, collections::HashMap,
io::{self, Result, Stdout}, io::{self, Stdout},
result, result,
sync::atomic::Ordering, sync::atomic::Ordering,
}; };
@ -143,6 +144,7 @@ impl<'a> Pager<'a> {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Transition { pub enum Transition {
// TODO: should we add a noop transition instead of doing Option<Transition> everywhere?
Ok, Ok,
Exit, Exit,
Cmd(String), Cmd(String),
@ -155,8 +157,9 @@ pub struct PagerConfig<'a> {
pub lscolors: &'a LsColors, pub lscolors: &'a LsColors,
pub config: ConfigMap, pub config: ConfigMap,
pub style: StyleConfig, pub style: StyleConfig,
// If true, when quitting output the value of the cell the cursor was on
pub peek_value: bool, pub peek_value: bool,
pub reverse: bool, pub tail: bool,
} }
impl<'a> PagerConfig<'a> { impl<'a> PagerConfig<'a> {
@ -172,7 +175,7 @@ impl<'a> PagerConfig<'a> {
config, config,
lscolors, lscolors,
peek_value: false, peek_value: false,
reverse: false, tail: false,
style: StyleConfig::default(), style: StyleConfig::default(),
} }
} }
@ -247,7 +250,7 @@ fn render_ui(
{ {
let info = info.clone(); let info = info.clone();
term.draw(|f| { term.draw(|f| {
draw_frame(f, &mut view_stack.view, pager, &mut layout, info); draw_frame(f, &mut view_stack.curr_view, pager, &mut layout, info);
})?; })?;
} }
@ -259,7 +262,7 @@ fn render_ui(
info, info,
&mut pager.search_buf, &mut pager.search_buf,
&mut pager.cmd_buf, &mut pager.cmd_buf,
view_stack.view.as_mut().map(|p| &mut p.view), view_stack.curr_view.as_mut().map(|p| &mut p.view),
); );
if let Some(transition) = transition { if let Some(transition) = transition {
@ -302,7 +305,7 @@ fn render_ui(
match out { match out {
Ok(result) => { Ok(result) => {
if result.exit { if result.exit {
break Ok(peak_value_from_view(&mut view_stack.view, pager)); break Ok(peek_value_from_view(&mut view_stack.curr_view, pager));
} }
if result.view_change && !result.cmd_name.is_empty() { if result.view_change && !result.cmd_name.is_empty() {
@ -336,21 +339,21 @@ fn react_to_event_result(
) -> (Option<Option<Value>>, String) { ) -> (Option<Option<Value>>, String) {
match status { match status {
Transition::Exit => ( Transition::Exit => (
Some(peak_value_from_view(&mut view_stack.view, pager)), Some(peek_value_from_view(&mut view_stack.curr_view, pager)),
String::default(), String::default(),
), ),
Transition::Ok => { Transition::Ok => {
let exit = view_stack.stack.is_empty(); let exit = view_stack.stack.is_empty();
if exit { if exit {
return ( return (
Some(peak_value_from_view(&mut view_stack.view, pager)), Some(peek_value_from_view(&mut view_stack.curr_view, pager)),
String::default(), String::default(),
); );
} }
// try to pop the view stack // try to pop the view stack
if let Some(v) = view_stack.stack.pop() { if let Some(v) = view_stack.stack.pop() {
view_stack.view = Some(v); view_stack.curr_view = Some(v);
} }
(None, String::default()) (None, String::default())
@ -359,7 +362,7 @@ fn react_to_event_result(
let out = pager_run_command(engine_state, stack, pager, view_stack, commands, cmd); let out = pager_run_command(engine_state, stack, pager, view_stack, commands, cmd);
match out { match out {
Ok(result) if result.exit => ( Ok(result) if result.exit => (
Some(peak_value_from_view(&mut view_stack.view, pager)), Some(peek_value_from_view(&mut view_stack.curr_view, pager)),
String::default(), String::default(),
), ),
Ok(result) => (None, result.cmd_name), Ok(result) => (None, result.cmd_name),
@ -372,9 +375,13 @@ fn react_to_event_result(
} }
} }
fn peak_value_from_view(view: &mut Option<Page>, pager: &mut Pager<'_>) -> Option<Value> { fn peek_value_from_view(view: &mut Option<Page>, pager: &mut Pager<'_>) -> Option<Value> {
let view = view.as_mut().map(|p| &mut p.view); if pager.config.peek_value {
try_to_peek_value(pager, view) let view = view.as_mut().map(|p| &mut p.view);
view.and_then(|v| v.exit())
} else {
None
}
} }
fn draw_frame( fn draw_frame(
@ -453,7 +460,7 @@ fn run_command(
match command { match command {
Command::Reactive(mut command) => { Command::Reactive(mut command) => {
// what we do we just replace the view. // what we do we just replace the view.
let value = view_stack.view.as_mut().and_then(|p| p.view.exit()); let value = view_stack.curr_view.as_mut().and_then(|p| p.view.exit());
let transition = command.react(engine_state, stack, pager, value)?; let transition = command.react(engine_state, stack, pager, value)?;
match transition { match transition {
Transition::Ok => { Transition::Ok => {
@ -470,18 +477,18 @@ fn run_command(
Transition::Cmd { .. } => todo!("not used so far"), Transition::Cmd { .. } => todo!("not used so far"),
} }
} }
Command::View { mut cmd, is_light } => { Command::View { mut cmd, stackable } => {
// what we do we just replace the view. // what we do we just replace the view.
let value = view_stack.view.as_mut().and_then(|p| p.view.exit()); let value = view_stack.curr_view.as_mut().and_then(|p| p.view.exit());
let mut new_view = cmd.spawn(engine_state, stack, value)?; let mut new_view = cmd.spawn(engine_state, stack, value)?;
if let Some(view) = view_stack.view.take() { if let Some(view) = view_stack.curr_view.take() {
if !view.is_light { if !view.stackable {
view_stack.stack.push(view); view_stack.stack.push(view);
} }
} }
update_view_setup(&mut new_view, &pager.config); update_view_setup(&mut new_view, &pager.config);
view_stack.view = Some(Page::raw(new_view, is_light)); view_stack.curr_view = Some(Page::raw(new_view, stackable));
Ok(CmdResult::new(false, true, cmd.name().to_owned())) Ok(CmdResult::new(false, true, cmd.name().to_owned()))
} }
@ -489,7 +496,7 @@ fn run_command(
} }
fn update_view_stack_setup(view_stack: &mut ViewStack, cfg: &PagerConfig<'_>) { fn update_view_stack_setup(view_stack: &mut ViewStack, cfg: &PagerConfig<'_>) {
if let Some(page) = view_stack.view.as_mut() { if let Some(page) = view_stack.curr_view.as_mut() {
update_view_setup(&mut page.view, cfg); update_view_setup(&mut page.view, cfg);
} }
@ -521,17 +528,6 @@ fn set_cursor_cmd_bar(f: &mut Frame, area: Rect, pager: &Pager) {
} }
} }
fn try_to_peek_value<V>(pager: &mut Pager, view: Option<&mut V>) -> Option<Value>
where
V: View,
{
if pager.config.peek_value {
view.and_then(|v| v.exit())
} else {
None
}
}
fn render_status_bar(f: &mut Frame, area: Rect, report: Report, theme: &StyleConfig) { 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 msg_style = report_msg_style(&report, theme, theme.status_bar_text);
let mut status_bar = create_status_bar(report); let mut status_bar = create_status_bar(report);
@ -1092,30 +1088,35 @@ impl Position {
pub struct Page { pub struct Page {
pub view: Box<dyn View>, pub view: Box<dyn View>,
pub is_light: bool, /// Controls what happens when this view is the current view and a new view is created.
/// If true, view will be pushed to the stack, otherwise, it will be deleted.
pub stackable: bool,
} }
impl Page { impl Page {
pub fn raw(view: Box<dyn View>, is_light: bool) -> Self { pub fn raw(view: Box<dyn View>, stackable: bool) -> Self {
Self { view, is_light } Self { view, stackable }
} }
pub fn new<V>(view: V, is_light: bool) -> Self pub fn new<V>(view: V, stackable: bool) -> Self
where where
V: View + 'static, V: View + 'static,
{ {
Self::raw(Box::new(view), is_light) Self::raw(Box::new(view), stackable)
} }
} }
struct ViewStack { struct ViewStack {
view: Option<Page>, curr_view: Option<Page>,
stack: Vec<Page>, stack: Vec<Page>,
} }
impl ViewStack { impl ViewStack {
fn new(view: Option<Page>, stack: Vec<Page>) -> Self { fn new(view: Option<Page>, stack: Vec<Page>) -> Self {
Self { view, stack } Self {
curr_view: view,
stack,
}
} }
} }

View file

@ -1,26 +1,27 @@
use crate::{ use crate::{
commands::{HelpManual, SimpleCommand, ViewCommand}, commands::{SimpleCommand, ViewCommand},
views::View, views::View,
}; };
use anyhow::Result;
#[derive(Clone)] #[derive(Clone)]
pub enum Command { pub enum Command {
Reactive(Box<dyn SCommand>), Reactive(Box<dyn SCommand>),
View { View {
cmd: Box<dyn VCommand>, cmd: Box<dyn VCommand>,
is_light: bool, stackable: bool,
}, },
} }
impl Command { impl Command {
pub fn view<C>(command: C, is_light: bool) -> Self pub fn view<C>(command: C, stackable: bool) -> Self
where where
C: ViewCommand + Clone + 'static, C: ViewCommand + Clone + 'static,
C::View: View, C::View: View,
{ {
let cmd = Box::new(ViewCmd(command)) as Box<dyn VCommand>; let cmd = Box::new(ViewCmd(command)) as Box<dyn VCommand>;
Self::View { cmd, is_light } Self::View { cmd, stackable }
} }
pub fn reactive<C>(command: C) -> Self pub fn reactive<C>(command: C) -> Self
@ -41,7 +42,7 @@ impl Command {
} }
} }
pub fn parse(&mut self, args: &str) -> std::io::Result<()> { pub fn parse(&mut self, args: &str) -> Result<()> {
match self { match self {
Command::Reactive(cmd) => cmd.parse(args), Command::Reactive(cmd) => cmd.parse(args),
Command::View { cmd, .. } => cmd.parse(args), Command::View { cmd, .. } => cmd.parse(args),
@ -68,11 +69,7 @@ where
self.0.usage() self.0.usage()
} }
fn help(&self) -> Option<HelpManual> { fn parse(&mut self, args: &str) -> Result<()> {
self.0.help()
}
fn parse(&mut self, args: &str) -> std::io::Result<()> {
self.0.parse(args) self.0.parse(args)
} }
@ -81,7 +78,7 @@ where
engine_state: &nu_protocol::engine::EngineState, engine_state: &nu_protocol::engine::EngineState,
stack: &mut nu_protocol::engine::Stack, stack: &mut nu_protocol::engine::Stack,
value: Option<nu_protocol::Value>, value: Option<nu_protocol::Value>,
) -> std::io::Result<Self::View> { ) -> Result<Self::View> {
let view = self.0.spawn(engine_state, stack, value)?; let view = self.0.spawn(engine_state, stack, value)?;
Ok(Box::new(view) as Box<dyn View>) Ok(Box::new(view) as Box<dyn View>)
} }

View file

@ -4,6 +4,7 @@ use crate::{
commands::{SimpleCommand, ViewCommand}, commands::{SimpleCommand, ViewCommand},
views::View, views::View,
}; };
use anyhow::Result;
use std::borrow::Cow; use std::borrow::Cow;
use std::collections::HashMap; use std::collections::HashMap;
@ -25,14 +26,14 @@ impl CommandRegistry {
.insert(Cow::Owned(command.name().to_owned()), command); .insert(Cow::Owned(command.name().to_owned()), command);
} }
pub fn register_command_view<C>(&mut self, command: C, is_light: bool) pub fn register_command_view<C>(&mut self, command: C, stackable: bool)
where where
C: ViewCommand + Clone + 'static, C: ViewCommand + Clone + 'static,
C::View: View, C::View: View,
{ {
self.commands.insert( self.commands.insert(
Cow::Owned(command.name().to_owned()), Cow::Owned(command.name().to_owned()),
Command::view(command, is_light), Command::view(command, stackable),
); );
} }
@ -53,7 +54,7 @@ impl CommandRegistry {
); );
} }
pub fn find(&self, args: &str) -> Option<std::io::Result<Command>> { pub fn find(&self, args: &str) -> Option<Result<Command>> {
let cmd = args.split_once(' ').map_or(args, |(cmd, _)| cmd); let cmd = args.split_once(' ').map_or(args, |(cmd, _)| cmd);
let args = &args[cmd.len()..]; let args = &args[cmd.len()..];

View file

@ -1,77 +0,0 @@
use super::{Layout, View, ViewConfig};
use crate::{
nu_common::NuText,
pager::{Frame, Transition, ViewInfo},
};
use crossterm::event::KeyEvent;
use nu_color_config::TextStyle;
use nu_protocol::engine::{EngineState, Stack};
use ratatui::{layout::Rect, widgets::Paragraph};
#[derive(Debug, Default)]
pub struct InformationView;
impl InformationView {
const MESSAGE: [&'static str; 7] = [
"Explore",
"",
"Explore helps you dynamically navigate through your data",
"",
"type :help<Enter> for help",
"type :q<Enter> to exit",
"type :try<Enter> to enter a REPL",
];
}
impl View for InformationView {
fn draw(&mut self, f: &mut Frame, area: Rect, _: ViewConfig<'_>, layout: &mut Layout) {
let count_lines = Self::MESSAGE.len() as u16;
if area.height < count_lines {
return;
}
let centerh = area.height / 2;
let centerw = area.width / 2;
let mut y = centerh.saturating_sub(count_lines);
for mut line in Self::MESSAGE {
let mut line_width = line.len() as u16;
if line_width > area.width {
line_width = area.width;
line = &line[..area.width as usize];
}
let x = centerw.saturating_sub(line_width / 2);
let area = Rect::new(area.x + x, area.y + y, line_width, 1);
let paragraph = Paragraph::new(line);
f.render_widget(paragraph, area);
layout.push(line, area.x, area.y, area.width, area.height);
y += 1;
}
}
fn handle_input(
&mut self,
_: &EngineState,
_: &mut Stack,
_: &Layout,
_: &mut ViewInfo,
event: KeyEvent,
) -> Option<Transition> {
match event.code {
crossterm::event::KeyCode::Esc => Some(Transition::Exit),
_ => None,
}
}
fn collect_data(&self) -> Vec<NuText> {
Self::MESSAGE
.into_iter()
.map(|line| (line.to_owned(), TextStyle::default()))
.collect::<Vec<_>>()
}
}

View file

@ -8,6 +8,7 @@ use crate::{
pager::{report::Report, Frame, Transition, ViewInfo}, pager::{report::Report, Frame, Transition, ViewInfo},
util::create_map, util::create_map,
}; };
use anyhow::Result;
use crossterm::event::{KeyCode, KeyEvent}; use crossterm::event::{KeyCode, KeyEvent};
use nu_color_config::get_color_map; use nu_color_config::get_color_map;
use nu_protocol::{ use nu_protocol::{
@ -50,7 +51,7 @@ impl<'a> InteractiveView<'a> {
self.command = command; self.command = command;
} }
pub fn try_run(&mut self, engine_state: &EngineState, stack: &mut Stack) -> Result<(), String> { pub fn try_run(&mut self, engine_state: &EngineState, stack: &mut Stack) -> Result<()> {
let mut view = run_command(&self.command, &self.input, engine_state, stack)?; let mut view = run_command(&self.command, &self.input, engine_state, stack)?;
view.set_theme(self.table_theme.clone()); view.set_theme(self.table_theme.clone());
@ -281,13 +282,12 @@ fn run_command(
input: &Value, input: &Value,
engine_state: &EngineState, engine_state: &EngineState,
stack: &mut Stack, stack: &mut Stack,
) -> Result<RecordView<'static>, String> { ) -> Result<RecordView<'static>> {
let pipeline = let pipeline = run_command_with_value(command, input, engine_state, stack)?;
run_command_with_value(command, input, engine_state, stack).map_err(|e| e.to_string())?;
let is_record = matches!(pipeline, PipelineData::Value(Value::Record { .. }, ..)); let is_record = matches!(pipeline, PipelineData::Value(Value::Record { .. }, ..));
let (columns, values) = collect_pipeline(pipeline); let (columns, values) = collect_pipeline(pipeline)?;
let mut view = RecordView::new(columns, values); let mut view = RecordView::new(columns, values);
if is_record { if is_record {

View file

@ -1,7 +1,6 @@
mod binary; mod binary;
mod coloredtextw; mod coloredtextw;
mod cursor; mod cursor;
mod information;
mod interactive; mod interactive;
mod preview; mod preview;
mod record; mod record;
@ -22,7 +21,6 @@ use nu_protocol::{
use ratatui::layout::Rect; use ratatui::layout::Rect;
pub use binary::BinaryView; pub use binary::BinaryView;
pub use information::InformationView;
pub use interactive::InteractiveView; pub use interactive::InteractiveView;
pub use preview::Preview; pub use preview::Preview;
pub use record::{Orientation, RecordView}; pub use record::{Orientation, RecordView};

View file

@ -15,6 +15,7 @@ use crate::{
util::create_map, util::create_map,
views::ElementInfo, views::ElementInfo,
}; };
use anyhow::Result;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use nu_color_config::{get_color_map, StyleComputer}; use nu_color_config::{get_color_map, StyleComputer};
use nu_protocol::{ use nu_protocol::{
@ -47,10 +48,10 @@ impl<'a> RecordView<'a> {
} }
} }
pub fn reverse(&mut self, width: u16, height: u16) { pub fn tail(&mut self, width: u16, height: u16) {
let page_size = let page_size =
estimate_page_size(Rect::new(0, 0, width, height), self.theme.table.show_header); estimate_page_size(Rect::new(0, 0, width, height), self.theme.table.show_header);
state_reverse_data(self, page_size as usize); tail_data(self, page_size as usize);
} }
pub fn set_style_split_line(&mut self, style: NuStyle) { pub fn set_style_split_line(&mut self, style: NuStyle) {
@ -266,16 +267,26 @@ impl View for RecordView<'_> {
key: KeyEvent, key: KeyEvent,
) -> Option<Transition> { ) -> Option<Transition> {
let result = match self.mode { let result = match self.mode {
UIMode::View => handle_key_event_view_mode(self, &key), UIMode::View => Ok(handle_key_event_view_mode(self, &key)),
UIMode::Cursor => handle_key_event_cursor_mode(self, &key), UIMode::Cursor => handle_key_event_cursor_mode(self, &key),
}; };
if matches!(&result, Some(Transition::Ok) | Some(Transition::Cmd { .. })) { match result {
let report = self.create_records_report(); Ok(result) => {
info.status = Some(report); if matches!(&result, Some(Transition::Ok) | Some(Transition::Cmd { .. })) {
} let report = self.create_records_report();
info.status = Some(report);
}
result result
}
Err(e) => {
log::error!("Error handling input in RecordView: {e}");
let report = Report::message(e.to_string(), Severity::Err);
info.status = Some(report);
None
}
}
} }
fn collect_data(&self) -> Vec<NuText> { fn collect_data(&self) -> Vec<NuText> {
@ -508,7 +519,10 @@ fn handle_key_event_view_mode(view: &mut RecordView, key: &KeyEvent) -> Option<T
} }
} }
fn handle_key_event_cursor_mode(view: &mut RecordView, key: &KeyEvent) -> Option<Transition> { fn handle_key_event_cursor_mode(
view: &mut RecordView,
key: &KeyEvent,
) -> Result<Option<Transition>> {
match key { match key {
KeyEvent { KeyEvent {
code: KeyCode::Char('u'), code: KeyCode::Char('u'),
@ -521,7 +535,7 @@ fn handle_key_event_cursor_mode(view: &mut RecordView, key: &KeyEvent) -> Option
} => { } => {
view.get_layer_last_mut().cursor.prev_row_page(); view.get_layer_last_mut().cursor.prev_row_page();
return Some(Transition::Ok); return Ok(Some(Transition::Ok));
} }
KeyEvent { KeyEvent {
code: KeyCode::Char('d'), code: KeyCode::Char('d'),
@ -534,7 +548,7 @@ fn handle_key_event_cursor_mode(view: &mut RecordView, key: &KeyEvent) -> Option
} => { } => {
view.get_layer_last_mut().cursor.next_row_page(); view.get_layer_last_mut().cursor.next_row_page();
return Some(Transition::Ok); return Ok(Some(Transition::Ok));
} }
_ => {} _ => {}
} }
@ -543,43 +557,42 @@ fn handle_key_event_cursor_mode(view: &mut RecordView, key: &KeyEvent) -> Option
KeyCode::Esc => { KeyCode::Esc => {
view.set_view_mode(); view.set_view_mode();
Some(Transition::Ok) Ok(Some(Transition::Ok))
} }
KeyCode::Up | KeyCode::Char('k') => { KeyCode::Up | KeyCode::Char('k') => {
view.get_layer_last_mut().cursor.prev_row(); view.get_layer_last_mut().cursor.prev_row();
Some(Transition::Ok) Ok(Some(Transition::Ok))
} }
KeyCode::Down | KeyCode::Char('j') => { KeyCode::Down | KeyCode::Char('j') => {
view.get_layer_last_mut().cursor.next_row(); view.get_layer_last_mut().cursor.next_row();
Some(Transition::Ok) Ok(Some(Transition::Ok))
} }
KeyCode::Left | KeyCode::Char('h') => { KeyCode::Left | KeyCode::Char('h') => {
view.get_layer_last_mut().cursor.prev_column(); view.get_layer_last_mut().cursor.prev_column();
Some(Transition::Ok) Ok(Some(Transition::Ok))
} }
KeyCode::Right | KeyCode::Char('l') => { KeyCode::Right | KeyCode::Char('l') => {
view.get_layer_last_mut().cursor.next_column(); view.get_layer_last_mut().cursor.next_column();
Some(Transition::Ok) Ok(Some(Transition::Ok))
} }
KeyCode::Home | KeyCode::Char('g') => { KeyCode::Home | KeyCode::Char('g') => {
view.get_layer_last_mut().cursor.row_move_to_start(); view.get_layer_last_mut().cursor.row_move_to_start();
Some(Transition::Ok) Ok(Some(Transition::Ok))
} }
KeyCode::End | KeyCode::Char('G') => { KeyCode::End | KeyCode::Char('G') => {
view.get_layer_last_mut().cursor.row_move_to_end(); view.get_layer_last_mut().cursor.row_move_to_end();
Some(Transition::Ok) Ok(Some(Transition::Ok))
} }
KeyCode::Enter => { KeyCode::Enter => {
let value = view.get_current_value(); let value = view.get_current_value();
let is_record = matches!(value, Value::Record { .. }); let is_record = matches!(value, Value::Record { .. });
let next_layer = create_layer(value); let next_layer = create_layer(value)?;
push_layer(view, next_layer); push_layer(view, next_layer);
if is_record { if is_record {
@ -590,16 +603,16 @@ fn handle_key_event_cursor_mode(view: &mut RecordView, key: &KeyEvent) -> Option
view.set_orientation_current(view.orientation); view.set_orientation_current(view.orientation);
} }
Some(Transition::Ok) Ok(Some(Transition::Ok))
} }
_ => None, _ => Ok(None),
} }
} }
fn create_layer(value: Value) -> RecordLayer<'static> { fn create_layer(value: Value) -> Result<RecordLayer<'static>> {
let (columns, values) = collect_input(value); let (columns, values) = collect_input(value)?;
RecordLayer::new(columns, values) Ok(RecordLayer::new(columns, values))
} }
fn push_layer(view: &mut RecordView<'_>, mut next_layer: RecordLayer<'static>) { fn push_layer(view: &mut RecordView<'_>, mut next_layer: RecordLayer<'static>) {
@ -624,7 +637,8 @@ fn estimate_page_size(area: Rect, show_head: bool) -> u16 {
available_height available_height
} }
fn state_reverse_data(state: &mut RecordView<'_>, page_size: usize) { /// scroll to the end of the data
fn tail_data(state: &mut RecordView<'_>, page_size: usize) {
let layer = state.get_layer_last_mut(); let layer = state.get_layer_last_mut();
let count_rows = layer.records.len(); let count_rows = layer.records.len();
if count_rows > page_size { if count_rows > page_size {

BIN
foo.db Normal file

Binary file not shown.