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

View File

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

View File

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

View File

@ -1,14 +1,14 @@
use super::{HelpManual, Shortcode, ViewCommand};
use super::ViewCommand;
use crate::{
nu_common::{self, collect_input},
views::Preview,
};
use anyhow::Result;
use nu_color_config::StyleComputer;
use nu_protocol::{
engine::{EngineState, Stack},
Value,
};
use std::{io::Result, vec};
#[derive(Default, Clone)]
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<()> {
Ok(())
}
@ -67,27 +44,37 @@ impl ViewCommand for ExpandCmd {
stack: &mut Stack,
value: Option<Value>,
) -> Result<Self::View> {
let value = value
.map(|v| convert_value_to_string(v, engine_state, stack))
.unwrap_or_default();
Ok(Preview::new(&value))
if let Some(value) = value {
let value_as_string = convert_value_to_string(value, engine_state, stack)?;
Ok(Preview::new(&value_as_string))
} else {
Ok(Preview::new(""))
}
}
}
fn convert_value_to_string(value: Value, engine_state: &EngineState, stack: &mut Stack) -> String {
let (cols, vals) = collect_input(value.clone());
fn convert_value_to_string(
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_single_value = vals.len() == 1 && vals[0].len() == 1;
if !has_no_head && has_single_value {
let config = engine_state.get_config();
vals[0][0].to_abbreviated_string(config)
Ok(vals[0][0].to_abbreviated_string(config))
} else {
let ctrlc = engine_state.ctrlc.clone();
let config = engine_state.get_config();
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 crate::{
nu_common::{collect_input, NuSpan},
pager::{Frame, Transition, ViewInfo},
views::{Layout, Preview, RecordView, View, ViewConfig},
};
use crossterm::event::KeyEvent;
use super::ViewCommand;
use crate::views::Preview;
use anyhow::Result;
use nu_ansi_term::Color;
use nu_protocol::{
engine::{EngineState, Stack},
record, Value,
};
use ratatui::layout::Rect;
use std::{
collections::HashMap,
io::{self, Result},
Value,
};
use once_cell::sync::Lazy;
#[derive(Debug, Default, Clone)]
pub struct HelpCmd {
input_command: String,
supported_commands: Vec<HelpManual>,
aliases: HashMap<String, Vec<String>>,
}
pub struct HelpCmd {}
impl HelpCmd {
pub const NAME: &'static str = "help";
const HELP_MESSAGE: &'static str = r#" Explore - main help file
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,
}
pub fn view() -> Preview {
Preview::new(&HELP_MESSAGE)
}
}
fn collect_aliases(aliases: &[(&str, &str)]) -> HashMap<String, Vec<String>> {
let mut out_aliases: HashMap<String, Vec<String>> = HashMap::new();
for (name, cmd) in aliases {
out_aliases
.entry(cmd.to_string())
.and_modify(|list| list.push(name.to_string()))
.or_insert_with(|| vec![name.to_string()]);
}
out_aliases
}
static HELP_MESSAGE: Lazy<String> = Lazy::new(|| {
let title = nu_ansi_term::Style::new().bold().underline();
let code = nu_ansi_term::Style::new().bold().fg(Color::Blue);
// There is probably a nicer way to do this formatting inline
format!(
r#"{}
Explore helps you dynamically navigate through your data!
{}
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 {
type View = HelpView<'static>;
type View = Preview;
fn name(&self) -> &'static str {
Self::NAME
@ -80,260 +95,11 @@ impl ViewCommand for HelpCmd {
""
}
fn help(&self) -> Option<HelpManual> {
#[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);
fn parse(&mut self, _: &str) -> Result<()> {
Ok(())
}
fn spawn(&mut self, _: &EngineState, _: &mut Stack, _: Option<Value>) -> Result<Self::View> {
if self.input_command.is_empty() {
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),
}
Ok(HelpCmd::view())
}
}

View File

@ -1,9 +1,9 @@
use super::pager::{Pager, Transition};
use anyhow::Result;
use nu_protocol::{
engine::{EngineState, Stack},
Value,
};
use std::{borrow::Cow, io::Result};
mod expand;
mod help;
@ -24,8 +24,6 @@ pub trait SimpleCommand {
fn usage(&self) -> &'static str;
fn help(&self) -> Option<HelpManual>;
fn parse(&mut self, args: &str) -> Result<()>;
fn react(
@ -44,8 +42,6 @@ pub trait ViewCommand {
fn usage(&self) -> &'static str;
fn help(&self) -> Option<HelpManual>;
fn parse(&mut self, args: &str) -> Result<()>;
fn spawn(
@ -56,116 +52,9 @@ pub trait ViewCommand {
) -> 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)]
pub struct Shortcode {
pub code: &'static str,
pub context: &'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::{
nu_common::{collect_pipeline, has_simple_value, run_command_with_value},
pager::Frame,
views::{Layout, Orientation, Preview, RecordView, View, ViewConfig},
};
use anyhow::Result;
use nu_protocol::{
engine::{EngineState, Stack},
PipelineData, Value,
};
use ratatui::layout::Rect;
use std::io::{self, Result};
#[derive(Debug, Default, Clone)]
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<()> {
args.trim().clone_into(&mut self.command);
@ -75,12 +51,11 @@ impl ViewCommand for NuCmd {
) -> Result<Self::View> {
let value = value.unwrap_or_default();
let pipeline = run_command_with_value(&self.command, &value, engine_state, stack)
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
let pipeline = run_command_with_value(&self.command, &value, engine_state, stack)?;
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) {
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 anyhow::Result;
use nu_protocol::{
engine::{EngineState, Stack},
Value,
};
use std::io::Result;
#[derive(Default, Clone)]
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<()> {
Ok(())
}

View File

@ -1,17 +1,14 @@
use super::{
default_color_list, default_int_list, ConfigOption, HelpExample, HelpManual, Shortcode,
ViewCommand,
};
use super::ViewCommand;
use crate::{
nu_common::collect_input,
views::{Orientation, RecordView},
};
use anyhow::Result;
use nu_ansi_term::Style;
use nu_protocol::{
engine::{EngineState, Stack},
Value,
};
use std::io::Result;
#[derive(Debug, Default, Clone)]
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<()> {
Ok(())
}
@ -119,7 +62,7 @@ impl ViewCommand for TableCmd {
let value = value.unwrap_or_default();
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);

View File

@ -1,10 +1,10 @@
use super::{default_color_list, ConfigOption, HelpExample, HelpManual, Shortcode, ViewCommand};
use super::ViewCommand;
use crate::views::InteractiveView;
use anyhow::Result;
use nu_protocol::{
engine::{EngineState, Stack},
Value,
};
use std::io::{Error, ErrorKind, Result};
#[derive(Debug, Default, Clone)]
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<()> {
args.trim().clone_into(&mut self.command);
@ -78,8 +47,7 @@ impl ViewCommand for TryCmd {
let value = value.unwrap_or_default();
let mut view = InteractiveView::new(value);
view.init(self.command.clone());
view.try_run(engine_state, stack)
.map_err(|e| Error::new(ErrorKind::Other, e))?;
view.try_run(engine_state, stack)?;
Ok(view)
}

View File

@ -36,9 +36,9 @@ impl Command for Explore {
)
.switch("index", "Show row indexes when viewing a list", Some('i'))
.switch(
"reverse",
"tail",
"Start with the viewport scrolled to the bottom",
Some('r'),
Some('t'),
)
.switch(
"peek",
@ -61,7 +61,7 @@ impl Command for Explore {
) -> Result<PipelineData, ShellError> {
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 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 ctrlc = engine_state.ctrlc.clone();
@ -79,19 +79,31 @@ impl Command for Explore {
let mut config = PagerConfig::new(nu_config, &style_computer, &lscolors, config);
config.style = style;
config.reverse = is_reverse;
config.peek_value = peek_value;
config.reverse = is_reverse;
config.tail = tail;
let result = run_pager(engine_state, &mut stack.clone(), ctrlc, input, config);
match result {
Ok(Some(value)) => Ok(PipelineData::Value(value, None)),
Ok(None) => Ok(PipelineData::Value(Value::default(), None)),
Err(err) => Ok(PipelineData::Value(
Value::error(err.into(), call.head),
None,
)),
Err(err) => {
let shell_error = match err.downcast::<ShellError>() {
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 views;
use anyhow::Result;
use commands::{ExpandCmd, HelpCmd, NuCmd, QuitCmd, TableCmd, TryCmd};
pub use default_context::add_explore_context;
pub use explore::Explore;
use commands::{ExpandCmd, HelpCmd, HelpManual, NuCmd, QuitCmd, TableCmd, TryCmd};
use nu_common::{collect_pipeline, has_simple_value, CtrlC};
use nu_protocol::{
engine::{EngineState, Stack},
PipelineData, Value,
};
use pager::{Page, Pager, PagerConfig, StyleConfig};
use registry::{Command, CommandRegistry};
use std::io;
use registry::CommandRegistry;
use terminal_size::{Height, Width};
use views::{BinaryView, InformationView, Orientation, Preview, RecordView};
use views::{BinaryView, Orientation, Preview, RecordView};
mod util {
pub use super::nu_common::{create_lscolors, create_map, map_into_value};
@ -31,7 +30,7 @@ fn run_pager(
ctrlc: CtrlC,
input: PipelineData,
config: PagerConfig,
) -> io::Result<Option<Value>> {
) -> Result<Option<Value>> {
let mut p = Pager::new(config.clone());
let commands = create_command_registry();
@ -45,18 +44,18 @@ fn run_pager(
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();
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");
if let Some(value) = has_simple_value(&data) {
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);
}
@ -67,6 +66,7 @@ fn run_pager(
fn create_record_view(
columns: Vec<String>,
data: Vec<Vec<Value>>,
// wait, why would we use RecordView for something that isn't a record?
is_record: bool,
config: PagerConfig,
) -> Option<Page> {
@ -75,17 +75,17 @@ fn create_record_view(
view.set_orientation_current(Orientation::Left);
}
if config.reverse {
if config.tail {
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> {
Some(Page::new(InformationView, true))
fn help_view() -> Option<Page> {
Some(Page::new(HelpCmd::view(), false))
}
fn binary_view(input: PipelineData) -> Option<Page> {
@ -96,7 +96,7 @@ fn binary_view(input: PipelineData) -> Option<Page> {
let view = BinaryView::new(data);
Some(Page::new(view, false))
Some(Page::new(view, true))
}
fn create_command_registry() -> CommandRegistry {
@ -104,24 +104,16 @@ fn create_command_registry() -> CommandRegistry {
create_commands(&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
}
fn create_commands(registry: &mut CommandRegistry) {
registry.register_command_view(NuCmd::new(), false);
registry.register_command_view(TableCmd::new(), false);
registry.register_command_view(NuCmd::new(), true);
registry.register_command_view(TableCmd::new(), true);
registry.register_command_view(ExpandCmd::new(), true);
registry.register_command_view(TryCmd::new(), true);
registry.register_command_view(HelpCmd::default(), true);
registry.register_command_view(ExpandCmd::new(), false);
registry.register_command_view(TryCmd::new(), false);
registry.register_command_view(HelpCmd::default(), false);
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);
}
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 anyhow::Result;
use nu_engine::get_columns;
use nu_protocol::{record, ListStream, PipelineData, PipelineMetadata, RawStream, Value};
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 {
PipelineData::Empty => (vec![], vec![]),
PipelineData::Empty => Ok((vec![], vec![])),
PipelineData::Value(value, ..) => collect_input(value),
PipelineData::ListStream(stream, ..) => collect_list_stream(stream),
PipelineData::ListStream(stream, ..) => Ok(collect_list_stream(stream)),
PipelineData::ExternalStream {
stdout,
stderr,
@ -15,7 +16,9 @@ pub fn collect_pipeline(input: PipelineData) -> (Vec<String>, Vec<Vec<Value>>) {
metadata,
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.
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();
match value {
Value::Record { val: record, .. } => {
let (key, val) = record.into_owned().into_iter().unzip();
(key, vec![val])
Ok((key, vec![val]))
}
Value::List { 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, data)
Ok((columns, data))
}
Value::String { val, .. } => {
let lines = val
@ -107,17 +110,18 @@ pub fn collect_input(value: Value) -> (Vec<String>, Vec<Vec<Value>>) {
.map(|val| vec![val])
.collect();
(vec![String::from("")], lines)
Ok((vec![String::from("")], lines))
}
Value::LazyRecord { val, .. } => match val.collect() {
Ok(value) => collect_input(value),
Err(_) => (
vec![String::from("")],
vec![vec![Value::lazy_record(val, span)]],
),
},
Value::Nothing { .. } => (vec![], vec![]),
value => (vec![String::from("")], vec![vec![value]]),
Value::LazyRecord { val, .. } => {
let materialized = val.collect()?;
collect_input(materialized)
}
Value::Nothing { .. } => Ok((vec![], vec![])),
Value::Custom { val, .. } => {
let materialized = val.to_base_value(span)?;
collect_input(materialized)
}
value => Ok((vec![String::from("")], vec![vec![value]])),
}
}

View File

@ -15,6 +15,7 @@ use crate::{
util::map_into_value,
views::{util::nu_style_to_tui, ViewConfig},
};
use anyhow::Result;
use crossterm::{
event::{KeyCode, KeyEvent, KeyModifiers},
execute,
@ -34,7 +35,7 @@ use ratatui::{backend::CrosstermBackend, layout::Rect, widgets::Block};
use std::{
cmp::min,
collections::HashMap,
io::{self, Result, Stdout},
io::{self, Stdout},
result,
sync::atomic::Ordering,
};
@ -143,6 +144,7 @@ impl<'a> Pager<'a> {
#[derive(Debug, Clone)]
pub enum Transition {
// TODO: should we add a noop transition instead of doing Option<Transition> everywhere?
Ok,
Exit,
Cmd(String),
@ -155,8 +157,9 @@ pub struct PagerConfig<'a> {
pub lscolors: &'a LsColors,
pub config: ConfigMap,
pub style: StyleConfig,
// If true, when quitting output the value of the cell the cursor was on
pub peek_value: bool,
pub reverse: bool,
pub tail: bool,
}
impl<'a> PagerConfig<'a> {
@ -172,7 +175,7 @@ impl<'a> PagerConfig<'a> {
config,
lscolors,
peek_value: false,
reverse: false,
tail: false,
style: StyleConfig::default(),
}
}
@ -247,7 +250,7 @@ fn render_ui(
{
let info = info.clone();
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,
&mut pager.search_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 {
@ -302,7 +305,7 @@ fn render_ui(
match out {
Ok(result) => {
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() {
@ -336,21 +339,21 @@ fn react_to_event_result(
) -> (Option<Option<Value>>, String) {
match status {
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(),
),
Transition::Ok => {
let exit = view_stack.stack.is_empty();
if exit {
return (
Some(peak_value_from_view(&mut view_stack.view, pager)),
Some(peek_value_from_view(&mut view_stack.curr_view, pager)),
String::default(),
);
}
// try to pop the view stack
if let Some(v) = view_stack.stack.pop() {
view_stack.view = Some(v);
view_stack.curr_view = Some(v);
}
(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);
match out {
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(),
),
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> {
let view = view.as_mut().map(|p| &mut p.view);
try_to_peek_value(pager, view)
fn peek_value_from_view(view: &mut Option<Page>, pager: &mut Pager<'_>) -> Option<Value> {
if pager.config.peek_value {
let view = view.as_mut().map(|p| &mut p.view);
view.and_then(|v| v.exit())
} else {
None
}
}
fn draw_frame(
@ -453,7 +460,7 @@ fn run_command(
match command {
Command::Reactive(mut command) => {
// 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)?;
match transition {
Transition::Ok => {
@ -470,18 +477,18 @@ fn run_command(
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.
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)?;
if let Some(view) = view_stack.view.take() {
if !view.is_light {
if let Some(view) = view_stack.curr_view.take() {
if !view.stackable {
view_stack.stack.push(view);
}
}
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()))
}
@ -489,7 +496,7 @@ fn run_command(
}
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);
}
@ -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) {
let msg_style = report_msg_style(&report, theme, theme.status_bar_text);
let mut status_bar = create_status_bar(report);
@ -1092,30 +1088,35 @@ impl Position {
pub struct Page {
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 {
pub fn raw(view: Box<dyn View>, is_light: bool) -> Self {
Self { view, is_light }
pub fn raw(view: Box<dyn View>, stackable: bool) -> Self {
Self { view, stackable }
}
pub fn new<V>(view: V, is_light: bool) -> Self
pub fn new<V>(view: V, stackable: bool) -> Self
where
V: View + 'static,
{
Self::raw(Box::new(view), is_light)
Self::raw(Box::new(view), stackable)
}
}
struct ViewStack {
view: Option<Page>,
curr_view: Option<Page>,
stack: Vec<Page>,
}
impl ViewStack {
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::{
commands::{HelpManual, SimpleCommand, ViewCommand},
commands::{SimpleCommand, ViewCommand},
views::View,
};
use anyhow::Result;
#[derive(Clone)]
pub enum Command {
Reactive(Box<dyn SCommand>),
View {
cmd: Box<dyn VCommand>,
is_light: bool,
stackable: bool,
},
}
impl Command {
pub fn view<C>(command: C, is_light: bool) -> Self
pub fn view<C>(command: C, stackable: bool) -> Self
where
C: ViewCommand + Clone + 'static,
C::View: View,
{
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
@ -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 {
Command::Reactive(cmd) => cmd.parse(args),
Command::View { cmd, .. } => cmd.parse(args),
@ -68,11 +69,7 @@ where
self.0.usage()
}
fn help(&self) -> Option<HelpManual> {
self.0.help()
}
fn parse(&mut self, args: &str) -> std::io::Result<()> {
fn parse(&mut self, args: &str) -> Result<()> {
self.0.parse(args)
}
@ -81,7 +78,7 @@ where
engine_state: &nu_protocol::engine::EngineState,
stack: &mut nu_protocol::engine::Stack,
value: Option<nu_protocol::Value>,
) -> std::io::Result<Self::View> {
) -> Result<Self::View> {
let view = self.0.spawn(engine_state, stack, value)?;
Ok(Box::new(view) as Box<dyn View>)
}

View File

@ -4,6 +4,7 @@ use crate::{
commands::{SimpleCommand, ViewCommand},
views::View,
};
use anyhow::Result;
use std::borrow::Cow;
use std::collections::HashMap;
@ -25,14 +26,14 @@ impl CommandRegistry {
.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
C: ViewCommand + Clone + 'static,
C::View: View,
{
self.commands.insert(
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 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},
util::create_map,
};
use anyhow::Result;
use crossterm::event::{KeyCode, KeyEvent};
use nu_color_config::get_color_map;
use nu_protocol::{
@ -50,7 +51,7 @@ impl<'a> InteractiveView<'a> {
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)?;
view.set_theme(self.table_theme.clone());
@ -281,13 +282,12 @@ fn run_command(
input: &Value,
engine_state: &EngineState,
stack: &mut Stack,
) -> Result<RecordView<'static>, String> {
let pipeline =
run_command_with_value(command, input, engine_state, stack).map_err(|e| e.to_string())?;
) -> Result<RecordView<'static>> {
let pipeline = run_command_with_value(command, input, engine_state, stack)?;
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);
if is_record {

View File

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

View File

@ -15,6 +15,7 @@ use crate::{
util::create_map,
views::ElementInfo,
};
use anyhow::Result;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use nu_color_config::{get_color_map, StyleComputer};
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 =
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) {
@ -266,16 +267,26 @@ impl View for RecordView<'_> {
key: KeyEvent,
) -> Option<Transition> {
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),
};
if matches!(&result, Some(Transition::Ok) | Some(Transition::Cmd { .. })) {
let report = self.create_records_report();
info.status = Some(report);
}
match result {
Ok(result) => {
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> {
@ -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 {
KeyEvent {
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();
return Some(Transition::Ok);
return Ok(Some(Transition::Ok));
}
KeyEvent {
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();
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 => {
view.set_view_mode();
Some(Transition::Ok)
Ok(Some(Transition::Ok))
}
KeyCode::Up | KeyCode::Char('k') => {
view.get_layer_last_mut().cursor.prev_row();
Some(Transition::Ok)
Ok(Some(Transition::Ok))
}
KeyCode::Down | KeyCode::Char('j') => {
view.get_layer_last_mut().cursor.next_row();
Some(Transition::Ok)
Ok(Some(Transition::Ok))
}
KeyCode::Left | KeyCode::Char('h') => {
view.get_layer_last_mut().cursor.prev_column();
Some(Transition::Ok)
Ok(Some(Transition::Ok))
}
KeyCode::Right | KeyCode::Char('l') => {
view.get_layer_last_mut().cursor.next_column();
Some(Transition::Ok)
Ok(Some(Transition::Ok))
}
KeyCode::Home | KeyCode::Char('g') => {
view.get_layer_last_mut().cursor.row_move_to_start();
Some(Transition::Ok)
Ok(Some(Transition::Ok))
}
KeyCode::End | KeyCode::Char('G') => {
view.get_layer_last_mut().cursor.row_move_to_end();
Some(Transition::Ok)
Ok(Some(Transition::Ok))
}
KeyCode::Enter => {
let value = view.get_current_value();
let is_record = matches!(value, Value::Record { .. });
let next_layer = create_layer(value);
let next_layer = create_layer(value)?;
push_layer(view, next_layer);
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);
}
Some(Transition::Ok)
Ok(Some(Transition::Ok))
}
_ => None,
_ => Ok(None),
}
}
fn create_layer(value: Value) -> RecordLayer<'static> {
let (columns, values) = collect_input(value);
fn create_layer(value: Value) -> Result<RecordLayer<'static>> {
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>) {
@ -624,7 +637,8 @@ fn estimate_page_size(area: Rect, show_head: bool) -> u16 {
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 count_rows = layer.records.len();
if count_rows > page_size {

BIN
foo.db Normal file

Binary file not shown.