[MVP][WIP] less like pager (#6984)

Run it as `explore`.

#### example

```nu
ls | explore
```

Configuration points in `config.nu` file.
```
  # A 'explore' utility config
   explore_config: {
     highlight: { bg: 'yellow', fg: 'black' }
     status_bar: { bg: '#C4C9C6', fg: '#1D1F21' }
     command_bar: { fg: '#C4C9C6' }
     split_line: '#404040'
     cursor: true
     # selected_column: 'blue'
     # selected_row: { fg: 'yellow', bg: '#C1C2A3' }
     # selected_cell: { fg: 'white', bg: '#777777' }
     # line_shift: false,
     # line_index: false,
     # line_head_top: false,
     # line_head_bottom: false,
   }
```

You can start without a pipeline and type `explore` and it'll give you a
few tips.

![image](https://user-images.githubusercontent.com/343840/205088971-a8c0262f-f222-4641-b13a-027fbd4f5e1a.png)

If you type `:help` you an see the help screen with some information on
what tui keybindings are available.

![image](https://user-images.githubusercontent.com/343840/205089461-c4c54217-7ec4-4fa0-96c0-643d68dc0062.png)

From the `:help` screen you can now hit `i` and that puts you in
`cursor` aka `inspection` mode and you can move the cursor left right up
down and it you put it on an area such as `[table 5 rows]` and hit the
enter key, you'll see something like this, which shows all the `:`
commands. If you hit `esc` it will take you to the previous screen.

![image](https://user-images.githubusercontent.com/343840/205090155-3558a14b-87b7-4072-8dfb-dc8cc2ef4943.png)

If you then type `:try` you'll get this type of window where you can
type in the top portion and see results in the bottom.

![image](https://user-images.githubusercontent.com/343840/205089185-3c065551-0792-43d6-a13c-a52762856209.png)

The `:nu` command is interesting because you can type pipelines like
`:nu ls | sort-by type size` or another pipeline of your choosing such
as `:nu sys` and that will show the table that looks like this, which
we're calling "table mode".

![image](https://user-images.githubusercontent.com/343840/205090809-e686ff0f-6d0b-4347-8ed0-8c59adfbd741.png)

If you hit the `t` key it will now transpose the view to look like this.

![image](https://user-images.githubusercontent.com/343840/205090948-a834d7f2-1713-4dfe-92fe-5432f287df3d.png)

In table mode or transposed table mode you can use the `i` key to
inspect any collapsed field like `{record 8 fields}`, `[table 16 rows]`,
`[list x]`, etc.

One of the original benefits was that when you're in a view that has a
lot of columns, `explore` gives you the ability to scroll left, right,
up, and down.

`explore` is also smart enough to know when you're in table mode versus
preview mode. If you do `open Cargo.toml | explore` you get this.

![image](https://user-images.githubusercontent.com/343840/205091822-cac79130-3a52-4ca8-9210-eba5be30ed58.png)

If you type `open --raw Cargo.toml | explore` you get this where you can
scroll left, right, up, down. This is called preview mode.

![image](https://user-images.githubusercontent.com/343840/205091990-69455191-ab78-4fea-a961-feafafc16d70.png)

When you're in table mode, you can also type `:preview`. So, with `open
--raw Cargo.toml | explore`, if you type `:preview`, it will look like
this.

![image](https://user-images.githubusercontent.com/343840/205092569-436aa55a-0474-48d5-ab71-baddb1f43027.png)

Signed-off-by: Maxim Zhiburt <zhiburt@gmail.com>
Co-authored-by: Darren Schroeder <343840+fdncred@users.noreply.github.com>
This commit is contained in:
Maxim Zhiburt 2022-12-01 18:32:10 +03:00 committed by GitHub
parent e92678ea2c
commit 718ee3d545
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 5774 additions and 217 deletions

50
Cargo.lock generated
View File

@ -89,6 +89,15 @@ dependencies = [
"ansitok",
]
[[package]]
name = "ansi-str"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21b1ed1c166829a0ccb5d79caa0f75cb4abd4adb2ce2c096755b7ad5ffdb0990"
dependencies = [
"ansitok",
]
[[package]]
name = "ansitok"
version = "0.2.0"
@ -443,6 +452,12 @@ dependencies = [
"zip",
]
[[package]]
name = "cassowary"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
[[package]]
name = "cast"
version = "0.3.0"
@ -2597,6 +2612,7 @@ dependencies = [
"nu-ansi-term",
"nu-color-config",
"nu-engine",
"nu-explore",
"nu-glob",
"nu-json",
"nu-parser",
@ -2664,6 +2680,23 @@ dependencies = [
"sysinfo",
]
[[package]]
name = "nu-explore"
version = "0.72.1"
dependencies = [
"ansi-str 0.7.2",
"crossterm 0.24.0",
"nu-ansi-term",
"nu-color-config",
"nu-engine",
"nu-parser",
"nu-protocol",
"nu-table",
"strip-ansi-escapes",
"terminal_size 0.2.1",
"tui",
]
[[package]]
name = "nu-glob"
version = "0.72.1"
@ -3129,7 +3162,7 @@ version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1526bb6aa9f10ec339fb10360f22c57edf81d5678d0278e93bc12a47ffbe4b01"
dependencies = [
"ansi-str",
"ansi-str 0.5.0",
"ansitok",
"bytecount",
"fnv",
@ -4831,7 +4864,7 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56c3ee73732ffceaea7b8f6b719ce3bb17f253fa27461ffeaf568ebd0cdb4b85"
dependencies = [
"ansi-str",
"ansi-str 0.5.0",
"papergrid",
"tabled_derive",
"unicode-width",
@ -5128,6 +5161,19 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
[[package]]
name = "tui"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccdd26cbd674007e649a272da4475fb666d3aa0ad0531da7136db6fab0e5bad1"
dependencies = [
"bitflags",
"cassowary",
"crossterm 0.25.0",
"unicode-segmentation",
"unicode-width",
]
[[package]]
name = "typed-arena"
version = "1.7.0"

View File

@ -1,175 +1,19 @@
use crate::nu_style::{color_from_hex, color_string_to_nustyle};
use crate::nu_style::{color_from_hex, color_string_to_nustyle, lookup_style};
use nu_ansi_term::{Color, Style};
use nu_protocol::Config;
use nu_protocol::{Config, Value};
use nu_table::{Alignment, TextStyle};
use std::collections::HashMap;
pub fn lookup_ansi_color_style(s: &str) -> Style {
if s.starts_with('#') {
match color_from_hex(s) {
Ok(c) => match c {
Some(c) => c.normal(),
None => Style::default(),
},
Err(_) => Style::default(),
}
color_from_hex(s)
.ok()
.and_then(|c| c.map(|c| c.normal()))
.unwrap_or_default()
} else if s.starts_with('{') {
color_string_to_nustyle(s.to_string())
} else {
match s {
"g" | "green" => Color::Green.normal(),
"gb" | "green_bold" => Color::Green.bold(),
"gu" | "green_underline" => Color::Green.underline(),
"gi" | "green_italic" => Color::Green.italic(),
"gd" | "green_dimmed" => Color::Green.dimmed(),
"gr" | "green_reverse" => Color::Green.reverse(),
"gbl" | "green_blink" => Color::Green.blink(),
"gst" | "green_strike" => Color::Green.strikethrough(),
"lg" | "light_green" => Color::LightGreen.normal(),
"lgb" | "light_green_bold" => Color::LightGreen.bold(),
"lgu" | "light_green_underline" => Color::LightGreen.underline(),
"lgi" | "light_green_italic" => Color::LightGreen.italic(),
"lgd" | "light_green_dimmed" => Color::LightGreen.dimmed(),
"lgr" | "light_green_reverse" => Color::LightGreen.reverse(),
"lgbl" | "light_green_blink" => Color::LightGreen.blink(),
"lgst" | "light_green_strike" => Color::LightGreen.strikethrough(),
"r" | "red" => Color::Red.normal(),
"rb" | "red_bold" => Color::Red.bold(),
"ru" | "red_underline" => Color::Red.underline(),
"ri" | "red_italic" => Color::Red.italic(),
"rd" | "red_dimmed" => Color::Red.dimmed(),
"rr" | "red_reverse" => Color::Red.reverse(),
"rbl" | "red_blink" => Color::Red.blink(),
"rst" | "red_strike" => Color::Red.strikethrough(),
"lr" | "light_red" => Color::LightRed.normal(),
"lrb" | "light_red_bold" => Color::LightRed.bold(),
"lru" | "light_red_underline" => Color::LightRed.underline(),
"lri" | "light_red_italic" => Color::LightRed.italic(),
"lrd" | "light_red_dimmed" => Color::LightRed.dimmed(),
"lrr" | "light_red_reverse" => Color::LightRed.reverse(),
"lrbl" | "light_red_blink" => Color::LightRed.blink(),
"lrst" | "light_red_strike" => Color::LightRed.strikethrough(),
"u" | "blue" => Color::Blue.normal(),
"ub" | "blue_bold" => Color::Blue.bold(),
"uu" | "blue_underline" => Color::Blue.underline(),
"ui" | "blue_italic" => Color::Blue.italic(),
"ud" | "blue_dimmed" => Color::Blue.dimmed(),
"ur" | "blue_reverse" => Color::Blue.reverse(),
"ubl" | "blue_blink" => Color::Blue.blink(),
"ust" | "blue_strike" => Color::Blue.strikethrough(),
"lu" | "light_blue" => Color::LightBlue.normal(),
"lub" | "light_blue_bold" => Color::LightBlue.bold(),
"luu" | "light_blue_underline" => Color::LightBlue.underline(),
"lui" | "light_blue_italic" => Color::LightBlue.italic(),
"lud" | "light_blue_dimmed" => Color::LightBlue.dimmed(),
"lur" | "light_blue_reverse" => Color::LightBlue.reverse(),
"lubl" | "light_blue_blink" => Color::LightBlue.blink(),
"lust" | "light_blue_strike" => Color::LightBlue.strikethrough(),
"b" | "black" => Color::Black.normal(),
"bb" | "black_bold" => Color::Black.bold(),
"bu" | "black_underline" => Color::Black.underline(),
"bi" | "black_italic" => Color::Black.italic(),
"bd" | "black_dimmed" => Color::Black.dimmed(),
"br" | "black_reverse" => Color::Black.reverse(),
"bbl" | "black_blink" => Color::Black.blink(),
"bst" | "black_strike" => Color::Black.strikethrough(),
"ligr" | "light_gray" => Color::LightGray.normal(),
"ligrb" | "light_gray_bold" => Color::LightGray.bold(),
"ligru" | "light_gray_underline" => Color::LightGray.underline(),
"ligri" | "light_gray_italic" => Color::LightGray.italic(),
"ligrd" | "light_gray_dimmed" => Color::LightGray.dimmed(),
"ligrr" | "light_gray_reverse" => Color::LightGray.reverse(),
"ligrbl" | "light_gray_blink" => Color::LightGray.blink(),
"ligrst" | "light_gray_strike" => Color::LightGray.strikethrough(),
"y" | "yellow" => Color::Yellow.normal(),
"yb" | "yellow_bold" => Color::Yellow.bold(),
"yu" | "yellow_underline" => Color::Yellow.underline(),
"yi" | "yellow_italic" => Color::Yellow.italic(),
"yd" | "yellow_dimmed" => Color::Yellow.dimmed(),
"yr" | "yellow_reverse" => Color::Yellow.reverse(),
"ybl" | "yellow_blink" => Color::Yellow.blink(),
"yst" | "yellow_strike" => Color::Yellow.strikethrough(),
"ly" | "light_yellow" => Color::LightYellow.normal(),
"lyb" | "light_yellow_bold" => Color::LightYellow.bold(),
"lyu" | "light_yellow_underline" => Color::LightYellow.underline(),
"lyi" | "light_yellow_italic" => Color::LightYellow.italic(),
"lyd" | "light_yellow_dimmed" => Color::LightYellow.dimmed(),
"lyr" | "light_yellow_reverse" => Color::LightYellow.reverse(),
"lybl" | "light_yellow_blink" => Color::LightYellow.blink(),
"lyst" | "light_yellow_strike" => Color::LightYellow.strikethrough(),
"p" | "purple" => Color::Purple.normal(),
"pb" | "purple_bold" => Color::Purple.bold(),
"pu" | "purple_underline" => Color::Purple.underline(),
"pi" | "purple_italic" => Color::Purple.italic(),
"pd" | "purple_dimmed" => Color::Purple.dimmed(),
"pr" | "purple_reverse" => Color::Purple.reverse(),
"pbl" | "purple_blink" => Color::Purple.blink(),
"pst" | "purple_strike" => Color::Purple.strikethrough(),
"lp" | "light_purple" => Color::LightPurple.normal(),
"lpb" | "light_purple_bold" => Color::LightPurple.bold(),
"lpu" | "light_purple_underline" => Color::LightPurple.underline(),
"lpi" | "light_purple_italic" => Color::LightPurple.italic(),
"lpd" | "light_purple_dimmed" => Color::LightPurple.dimmed(),
"lpr" | "light_purple_reverse" => Color::LightPurple.reverse(),
"lpbl" | "light_purple_blink" => Color::LightPurple.blink(),
"lpst" | "light_purple_strike" => Color::LightPurple.strikethrough(),
"c" | "cyan" => Color::Cyan.normal(),
"cb" | "cyan_bold" => Color::Cyan.bold(),
"cu" | "cyan_underline" => Color::Cyan.underline(),
"ci" | "cyan_italic" => Color::Cyan.italic(),
"cd" | "cyan_dimmed" => Color::Cyan.dimmed(),
"cr" | "cyan_reverse" => Color::Cyan.reverse(),
"cbl" | "cyan_blink" => Color::Cyan.blink(),
"cst" | "cyan_strike" => Color::Cyan.strikethrough(),
"lc" | "light_cyan" => Color::LightCyan.normal(),
"lcb" | "light_cyan_bold" => Color::LightCyan.bold(),
"lcu" | "light_cyan_underline" => Color::LightCyan.underline(),
"lci" | "light_cyan_italic" => Color::LightCyan.italic(),
"lcd" | "light_cyan_dimmed" => Color::LightCyan.dimmed(),
"lcr" | "light_cyan_reverse" => Color::LightCyan.reverse(),
"lcbl" | "light_cyan_blink" => Color::LightCyan.blink(),
"lcst" | "light_cyan_strike" => Color::LightCyan.strikethrough(),
"w" | "white" => Color::White.normal(),
"wb" | "white_bold" => Color::White.bold(),
"wu" | "white_underline" => Color::White.underline(),
"wi" | "white_italic" => Color::White.italic(),
"wd" | "white_dimmed" => Color::White.dimmed(),
"wr" | "white_reverse" => Color::White.reverse(),
"wbl" | "white_blink" => Color::White.blink(),
"wst" | "white_strike" => Color::White.strikethrough(),
"dgr" | "dark_gray" => Color::DarkGray.normal(),
"dgrb" | "dark_gray_bold" => Color::DarkGray.bold(),
"dgru" | "dark_gray_underline" => Color::DarkGray.underline(),
"dgri" | "dark_gray_italic" => Color::DarkGray.italic(),
"dgrd" | "dark_gray_dimmed" => Color::DarkGray.dimmed(),
"dgrr" | "dark_gray_reverse" => Color::DarkGray.reverse(),
"dgrbl" | "dark_gray_blink" => Color::DarkGray.blink(),
"dgrst" | "dark_gray_strike" => Color::DarkGray.strikethrough(),
"def" | "default" => Color::Default.normal(),
"defb" | "default_bold" => Color::Default.bold(),
"defu" | "default_underline" => Color::Default.underline(),
"defi" | "default_italic" => Color::Default.italic(),
"defd" | "default_dimmed" => Color::Default.dimmed(),
"defr" | "default_reverse" => Color::Default.reverse(),
_ => Color::White.normal(),
}
lookup_style(s)
}
}
@ -226,6 +70,18 @@ pub fn get_color_config(config: &Config) -> HashMap<String, Style> {
hm
}
pub fn get_color_map(colors: &HashMap<String, Value>) -> HashMap<String, Style> {
let mut hm: HashMap<String, Style> = HashMap::new();
for (key, value) in colors {
if let Value::String { val, .. } = value {
update_hashmap(key, val, &mut hm);
}
}
hm
}
// This function will assign a text style to a primitive, or really any string that's
// in the hashmap. The hashmap actually contains the style to be applied.
pub fn style_primitive(primitive: &str, color_hm: &HashMap<String, Style>) -> TextStyle {

View File

@ -9,62 +9,17 @@ pub struct NuStyle {
}
pub fn parse_nustyle(nu_style: NuStyle) -> Style {
// get the nu_ansi_term::Color foreground color
let fg_color = match nu_style.fg {
Some(fg) => color_from_hex(&fg).unwrap_or_default(),
_ => None,
};
// get the nu_ansi_term::Color background color
let bg_color = match nu_style.bg {
Some(bg) => color_from_hex(&bg).unwrap_or_default(),
_ => None,
};
// get the attributes
let color_attr = match nu_style.attr {
Some(attr) => attr,
_ => "".to_string(),
let mut style = Style {
foreground: nu_style.fg.and_then(|fg| lookup_color_str(&fg)),
background: nu_style.bg.and_then(|bg| lookup_color_str(&bg)),
..Default::default()
};
// setup the attributes available in nu_ansi_term::Style
let mut bold = false;
let mut dimmed = false;
let mut italic = false;
let mut underline = false;
let mut blink = false;
let mut reverse = false;
let mut hidden = false;
let mut strikethrough = false;
// since we can combine styles like bold-italic, iterate through the chars
// and set the bools for later use in the nu_ansi_term::Style application
for ch in color_attr.to_lowercase().chars() {
match ch {
'l' => blink = true,
'b' => bold = true,
'd' => dimmed = true,
'h' => hidden = true,
'i' => italic = true,
'r' => reverse = true,
's' => strikethrough = true,
'u' => underline = true,
'n' => (),
_ => (),
}
if let Some(attrs) = nu_style.attr {
fill_modifiers(&attrs, &mut style)
}
// here's where we build the nu_ansi_term::Style
Style {
foreground: fg_color,
background: bg_color,
is_blink: blink,
is_bold: bold,
is_dimmed: dimmed,
is_hidden: hidden,
is_italic: italic,
is_reverse: reverse,
is_strikethrough: strikethrough,
is_underline: underline,
}
style
}
pub fn color_string_to_nustyle(color_string: String) -> Style {
@ -101,3 +56,214 @@ pub fn color_from_hex(
)))
}
}
pub fn lookup_style(s: &str) -> Style {
match s {
"g" | "green" => Color::Green.normal(),
"gb" | "green_bold" => Color::Green.bold(),
"gu" | "green_underline" => Color::Green.underline(),
"gi" | "green_italic" => Color::Green.italic(),
"gd" | "green_dimmed" => Color::Green.dimmed(),
"gr" | "green_reverse" => Color::Green.reverse(),
"gbl" | "green_blink" => Color::Green.blink(),
"gst" | "green_strike" => Color::Green.strikethrough(),
"lg" | "light_green" => Color::LightGreen.normal(),
"lgb" | "light_green_bold" => Color::LightGreen.bold(),
"lgu" | "light_green_underline" => Color::LightGreen.underline(),
"lgi" | "light_green_italic" => Color::LightGreen.italic(),
"lgd" | "light_green_dimmed" => Color::LightGreen.dimmed(),
"lgr" | "light_green_reverse" => Color::LightGreen.reverse(),
"lgbl" | "light_green_blink" => Color::LightGreen.blink(),
"lgst" | "light_green_strike" => Color::LightGreen.strikethrough(),
"r" | "red" => Color::Red.normal(),
"rb" | "red_bold" => Color::Red.bold(),
"ru" | "red_underline" => Color::Red.underline(),
"ri" | "red_italic" => Color::Red.italic(),
"rd" | "red_dimmed" => Color::Red.dimmed(),
"rr" | "red_reverse" => Color::Red.reverse(),
"rbl" | "red_blink" => Color::Red.blink(),
"rst" | "red_strike" => Color::Red.strikethrough(),
"lr" | "light_red" => Color::LightRed.normal(),
"lrb" | "light_red_bold" => Color::LightRed.bold(),
"lru" | "light_red_underline" => Color::LightRed.underline(),
"lri" | "light_red_italic" => Color::LightRed.italic(),
"lrd" | "light_red_dimmed" => Color::LightRed.dimmed(),
"lrr" | "light_red_reverse" => Color::LightRed.reverse(),
"lrbl" | "light_red_blink" => Color::LightRed.blink(),
"lrst" | "light_red_strike" => Color::LightRed.strikethrough(),
"u" | "blue" => Color::Blue.normal(),
"ub" | "blue_bold" => Color::Blue.bold(),
"uu" | "blue_underline" => Color::Blue.underline(),
"ui" | "blue_italic" => Color::Blue.italic(),
"ud" | "blue_dimmed" => Color::Blue.dimmed(),
"ur" | "blue_reverse" => Color::Blue.reverse(),
"ubl" | "blue_blink" => Color::Blue.blink(),
"ust" | "blue_strike" => Color::Blue.strikethrough(),
"lu" | "light_blue" => Color::LightBlue.normal(),
"lub" | "light_blue_bold" => Color::LightBlue.bold(),
"luu" | "light_blue_underline" => Color::LightBlue.underline(),
"lui" | "light_blue_italic" => Color::LightBlue.italic(),
"lud" | "light_blue_dimmed" => Color::LightBlue.dimmed(),
"lur" | "light_blue_reverse" => Color::LightBlue.reverse(),
"lubl" | "light_blue_blink" => Color::LightBlue.blink(),
"lust" | "light_blue_strike" => Color::LightBlue.strikethrough(),
"b" | "black" => Color::Black.normal(),
"bb" | "black_bold" => Color::Black.bold(),
"bu" | "black_underline" => Color::Black.underline(),
"bi" | "black_italic" => Color::Black.italic(),
"bd" | "black_dimmed" => Color::Black.dimmed(),
"br" | "black_reverse" => Color::Black.reverse(),
"bbl" | "black_blink" => Color::Black.blink(),
"bst" | "black_strike" => Color::Black.strikethrough(),
"ligr" | "light_gray" => Color::LightGray.normal(),
"ligrb" | "light_gray_bold" => Color::LightGray.bold(),
"ligru" | "light_gray_underline" => Color::LightGray.underline(),
"ligri" | "light_gray_italic" => Color::LightGray.italic(),
"ligrd" | "light_gray_dimmed" => Color::LightGray.dimmed(),
"ligrr" | "light_gray_reverse" => Color::LightGray.reverse(),
"ligrbl" | "light_gray_blink" => Color::LightGray.blink(),
"ligrst" | "light_gray_strike" => Color::LightGray.strikethrough(),
"y" | "yellow" => Color::Yellow.normal(),
"yb" | "yellow_bold" => Color::Yellow.bold(),
"yu" | "yellow_underline" => Color::Yellow.underline(),
"yi" | "yellow_italic" => Color::Yellow.italic(),
"yd" | "yellow_dimmed" => Color::Yellow.dimmed(),
"yr" | "yellow_reverse" => Color::Yellow.reverse(),
"ybl" | "yellow_blink" => Color::Yellow.blink(),
"yst" | "yellow_strike" => Color::Yellow.strikethrough(),
"ly" | "light_yellow" => Color::LightYellow.normal(),
"lyb" | "light_yellow_bold" => Color::LightYellow.bold(),
"lyu" | "light_yellow_underline" => Color::LightYellow.underline(),
"lyi" | "light_yellow_italic" => Color::LightYellow.italic(),
"lyd" | "light_yellow_dimmed" => Color::LightYellow.dimmed(),
"lyr" | "light_yellow_reverse" => Color::LightYellow.reverse(),
"lybl" | "light_yellow_blink" => Color::LightYellow.blink(),
"lyst" | "light_yellow_strike" => Color::LightYellow.strikethrough(),
"p" | "purple" => Color::Purple.normal(),
"pb" | "purple_bold" => Color::Purple.bold(),
"pu" | "purple_underline" => Color::Purple.underline(),
"pi" | "purple_italic" => Color::Purple.italic(),
"pd" | "purple_dimmed" => Color::Purple.dimmed(),
"pr" | "purple_reverse" => Color::Purple.reverse(),
"pbl" | "purple_blink" => Color::Purple.blink(),
"pst" | "purple_strike" => Color::Purple.strikethrough(),
"lp" | "light_purple" => Color::LightPurple.normal(),
"lpb" | "light_purple_bold" => Color::LightPurple.bold(),
"lpu" | "light_purple_underline" => Color::LightPurple.underline(),
"lpi" | "light_purple_italic" => Color::LightPurple.italic(),
"lpd" | "light_purple_dimmed" => Color::LightPurple.dimmed(),
"lpr" | "light_purple_reverse" => Color::LightPurple.reverse(),
"lpbl" | "light_purple_blink" => Color::LightPurple.blink(),
"lpst" | "light_purple_strike" => Color::LightPurple.strikethrough(),
"c" | "cyan" => Color::Cyan.normal(),
"cb" | "cyan_bold" => Color::Cyan.bold(),
"cu" | "cyan_underline" => Color::Cyan.underline(),
"ci" | "cyan_italic" => Color::Cyan.italic(),
"cd" | "cyan_dimmed" => Color::Cyan.dimmed(),
"cr" | "cyan_reverse" => Color::Cyan.reverse(),
"cbl" | "cyan_blink" => Color::Cyan.blink(),
"cst" | "cyan_strike" => Color::Cyan.strikethrough(),
"lc" | "light_cyan" => Color::LightCyan.normal(),
"lcb" | "light_cyan_bold" => Color::LightCyan.bold(),
"lcu" | "light_cyan_underline" => Color::LightCyan.underline(),
"lci" | "light_cyan_italic" => Color::LightCyan.italic(),
"lcd" | "light_cyan_dimmed" => Color::LightCyan.dimmed(),
"lcr" | "light_cyan_reverse" => Color::LightCyan.reverse(),
"lcbl" | "light_cyan_blink" => Color::LightCyan.blink(),
"lcst" | "light_cyan_strike" => Color::LightCyan.strikethrough(),
"w" | "white" => Color::White.normal(),
"wb" | "white_bold" => Color::White.bold(),
"wu" | "white_underline" => Color::White.underline(),
"wi" | "white_italic" => Color::White.italic(),
"wd" | "white_dimmed" => Color::White.dimmed(),
"wr" | "white_reverse" => Color::White.reverse(),
"wbl" | "white_blink" => Color::White.blink(),
"wst" | "white_strike" => Color::White.strikethrough(),
"dgr" | "dark_gray" => Color::DarkGray.normal(),
"dgrb" | "dark_gray_bold" => Color::DarkGray.bold(),
"dgru" | "dark_gray_underline" => Color::DarkGray.underline(),
"dgri" | "dark_gray_italic" => Color::DarkGray.italic(),
"dgrd" | "dark_gray_dimmed" => Color::DarkGray.dimmed(),
"dgrr" | "dark_gray_reverse" => Color::DarkGray.reverse(),
"dgrbl" | "dark_gray_blink" => Color::DarkGray.blink(),
"dgrst" | "dark_gray_strike" => Color::DarkGray.strikethrough(),
"def" | "default" => Color::Default.normal(),
"defb" | "default_bold" => Color::Default.bold(),
"defu" | "default_underline" => Color::Default.underline(),
"defi" | "default_italic" => Color::Default.italic(),
"defd" | "default_dimmed" => Color::Default.dimmed(),
"defr" | "default_reverse" => Color::Default.reverse(),
_ => Color::White.normal(),
}
}
pub fn lookup_color(s: &str) -> Option<Color> {
let color = match s {
"g" | "green" => Color::Green,
"lg" | "light_green" => Color::LightGreen,
"r" | "red" => Color::Red,
"lr" | "light_red" => Color::LightRed,
"u" | "blue" => Color::Blue,
"lu" | "light_blue" => Color::LightBlue,
"b" | "black" => Color::Black,
"ligr" | "light_gray" => Color::LightGray,
"y" | "yellow" => Color::Yellow,
"ly" | "light_yellow" => Color::LightYellow,
"p" | "purple" => Color::Purple,
"lp" | "light_purple" => Color::LightPurple,
"c" | "cyan" => Color::Cyan,
"lc" | "light_cyan" => Color::LightCyan,
"w" | "white" => Color::White,
"dgr" | "dark_gray" => Color::DarkGray,
"def" | "default" => Color::Default,
_ => return None,
};
Some(color)
}
fn fill_modifiers(attrs: &str, style: &mut Style) {
// setup the attributes available in nu_ansi_term::Style
//
// since we can combine styles like bold-italic, iterate through the chars
// and set the bools for later use in the nu_ansi_term::Style application
for ch in attrs.to_lowercase().chars() {
match ch {
'l' => style.is_blink = true,
'b' => style.is_bold = true,
'd' => style.is_dimmed = true,
'h' => style.is_hidden = true,
'i' => style.is_italic = true,
'r' => style.is_reverse = true,
's' => style.is_strikethrough = true,
'u' => style.is_underline = true,
'n' => (),
_ => (),
}
}
}
fn lookup_color_str(s: &str) -> Option<Color> {
if s.starts_with('#') {
color_from_hex(s).ok().and_then(|c| c)
} else {
lookup_color(s)
}
}

View File

@ -23,6 +23,7 @@ nu-system = { path = "../nu-system", version = "0.72.1" }
nu-table = { path = "../nu-table", version = "0.72.1" }
nu-term-grid = { path = "../nu-term-grid", version = "0.72.1" }
nu-utils = { path = "../nu-utils", version = "0.72.1" }
nu-explore = { path = "../nu-explore", version = "0.72.1" }
nu-ansi-term = "0.46.0"
num-format = { version = "0.4.3" }

View File

@ -356,6 +356,7 @@ pub fn create_default_context() -> EngineState {
bind_command! {
Griddle,
Table,
Explore,
};
// Conversions

View File

@ -0,0 +1,213 @@
use std::collections::HashMap;
use nu_ansi_term::{Color, Style};
use nu_color_config::{get_color_config, get_color_map};
use nu_engine::CallExt;
use nu_explore::{StyleConfig, TableConfig, TableSplitLines, ViewConfig};
use nu_protocol::{
ast::Call,
engine::{Command, EngineState, Stack},
Category, Example, PipelineData, ShellError, Signature, SyntaxShape, Value,
};
/// A `less` like program to render a [Value] as a table.
#[derive(Clone)]
pub struct Explore;
impl Command for Explore {
fn name(&self) -> &str {
"explore"
}
fn usage(&self) -> &str {
"Explore acts as a table pager, just like `less` does for text"
}
fn signature(&self) -> nu_protocol::Signature {
// todo: Fix error message when it's empty
// if we set h i short flags it panics????
Signature::build("explore")
.named(
"head",
SyntaxShape::Boolean,
"Setting it to false makes it doesn't show column headers",
None,
)
.switch("index", "A flag to show a index beside the rows", Some('i'))
.switch(
"reverse",
"Makes it start from the end. (like `more`)",
Some('r'),
)
.switch("peek", "Return a last seen cell content", Some('p'))
.category(Category::Viewers)
}
fn extra_usage(&self) -> &str {
r#"Press <:> then <h> to get a help menu."#
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let show_head: bool = call.get_flag(engine_state, stack, "head")?.unwrap_or(true);
let show_index: bool = call.has_flag("index");
let is_reverse: bool = call.has_flag("reverse");
let peek_value: bool = call.has_flag("peek");
let table_cfg = TableConfig {
show_index,
show_head,
peek_value,
reverse: is_reverse,
show_help: false,
};
let ctrlc = engine_state.ctrlc.clone();
let config = engine_state.get_config();
let color_hm = get_color_config(config);
let style = theme_from_config(&config.explore_config);
let view_cfg = ViewConfig::new(config, &color_hm, &style);
let result = nu_explore::run_pager(engine_state, stack, ctrlc, table_cfg, view_cfg, input);
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 { error: err.into() },
None,
)),
}
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "List the files in current directory, an looking at them via explore.",
example: r#"ls | explore"#,
result: None,
},
Example {
description: "Inspect system information (explore with index).",
example: r#"sys | explore -i"#,
result: None,
},
Example {
description: "Inspect $nu information (explore with no column names).",
example: r#"$nu | explore --head false"#,
result: None,
},
Example {
description: "Inspect $nu information and return an entity where you've stopped.",
example: r#"$nu | explore --peek"#,
result: None,
},
]
}
}
fn theme_from_config(config: &HashMap<String, Value>) -> StyleConfig {
let colors = get_color_map(config);
let mut style = default_style();
if let Some(s) = colors.get("status_bar") {
style.status_bar = *s;
}
if let Some(s) = colors.get("command_bar") {
style.cmd_bar = *s;
}
if let Some(s) = colors.get("split_line") {
style.split_line = *s;
}
if let Some(s) = colors.get("highlight") {
style.highlight = *s;
}
if let Some(s) = colors.get("selected_cell") {
style.selected_cell = Some(*s);
}
if let Some(s) = colors.get("selected_row") {
style.selected_row = Some(*s);
}
if let Some(s) = colors.get("selected_column") {
style.selected_column = Some(*s);
}
if let Some(show_cursor) = config.get("cursor").and_then(|v| v.as_bool().ok()) {
style.show_cursow = show_cursor;
}
if let Some(b) = config.get("line_head_top").and_then(|v| v.as_bool().ok()) {
style.split_lines.header_top = b;
}
if let Some(b) = config
.get("line_head_bottom")
.and_then(|v| v.as_bool().ok())
{
style.split_lines.header_bottom = b;
}
if let Some(b) = config.get("line_shift").and_then(|v| v.as_bool().ok()) {
style.split_lines.shift_line = b;
}
if let Some(b) = config.get("line_index").and_then(|v| v.as_bool().ok()) {
style.split_lines.index_line = b;
}
style
}
fn default_style() -> StyleConfig {
StyleConfig {
status_bar: Style {
background: Some(Color::Rgb(196, 201, 198)),
foreground: Some(Color::Rgb(29, 31, 33)),
..Default::default()
},
highlight: Style {
background: Some(Color::Yellow),
foreground: Some(Color::Black),
..Default::default()
},
split_line: Style {
foreground: Some(Color::Rgb(64, 64, 64)),
..Default::default()
},
cmd_bar: Style {
foreground: Some(Color::Rgb(196, 201, 198)),
..Default::default()
},
status_error: Style {
background: Some(Color::Red),
foreground: Some(Color::White),
..Default::default()
},
status_info: Style::default(),
status_warn: Style::default(),
selected_cell: None,
selected_column: None,
selected_row: None,
show_cursow: true,
split_lines: TableSplitLines {
header_bottom: true,
header_top: true,
index_line: true,
shift_line: true,
},
}
}

View File

@ -1,6 +1,8 @@
mod explore;
mod griddle;
mod icons;
mod table;
pub use explore::Explore;
pub use griddle::Griddle;
pub use table::Table;

22
crates/nu-explore/.gitignore vendored Normal file
View File

@ -0,0 +1,22 @@
/target
/scratch
**/*.rs.bk
history.txt
tests/fixtures/nuplayground
crates/*/target
# Debian/Ubuntu
debian/.debhelper/
debian/debhelper-build-stamp
debian/files
debian/nu.substvars
debian/nu/
# macOS junk
.DS_Store
# JetBrains' IDE items
.idea/*
# VSCode's IDE items
.vscode/*

View File

@ -0,0 +1,22 @@
[package]
authors = ["The Nushell Project Developers"]
description = "Nushell table printing"
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-explore"
edition = "2021"
license = "MIT"
name = "nu-explore"
version = "0.72.1"
[dependencies]
nu-ansi-term = "0.46.0"
nu-protocol = { path = "../nu-protocol", version = "0.72.1" }
nu-parser = { path = "../nu-parser", version = "0.72.1" }
nu-color-config = { path = "../nu-color-config", version = "0.72.1" }
nu-engine = { path = "../nu-engine", version = "0.72.1" }
nu-table = { path = "../nu-table", version = "0.72.1" }
terminal_size = "0.2.1"
strip-ansi-escapes = "0.1.1"
crossterm = "0.24.0"
tui = "0.19.0"
ansi-str = "0.7.2"

21
crates/nu-explore/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 - 2022 The Nushell Project Developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,248 @@
use std::collections::HashMap;
use crate::{
commands::{
HelpCmd, HelpManual, NuCmd, PreviewCmd, QuitCmd, SimpleCommand, TryCmd, ViewCommand,
},
views::View,
TableConfig,
};
#[derive(Clone)]
pub enum Command {
Reactive(Box<dyn SCommand>),
View {
cmd: Box<dyn VCommand>,
is_light: bool,
},
}
pub struct CommandList {
commands: HashMap<&'static str, Command>,
aliases: HashMap<&'static str, &'static str>,
}
macro_rules! cmd_view {
($object:expr, $light:expr) => {{
let object = $object;
let name = object.name();
let cmd = Box::new(ViewCmd(object)) as Box<dyn VCommand>;
let cmd = Command::View {
cmd,
is_light: $light,
};
(name, cmd)
}};
($object:expr) => {
cmd_view!($object, false)
};
}
macro_rules! cmd_react {
($object:expr) => {{
let object = $object;
let name = object.name();
let cmd = Command::Reactive(Box::new($object) as Box<dyn SCommand>);
(name, cmd)
}};
}
impl CommandList {
pub fn create_commands(table_cfg: TableConfig) -> Vec<(&'static str, Command)> {
vec![
cmd_view!(NuCmd::new(table_cfg)),
cmd_view!(TryCmd::new(table_cfg), true),
cmd_view!(PreviewCmd::new(), true),
cmd_react!(QuitCmd::default()),
]
}
pub fn create_aliases() -> [(&'static str, &'static str); 2] {
[("h", HelpCmd::NAME), ("q", QuitCmd::NAME)]
}
pub fn new(table_cfg: TableConfig) -> Self {
let mut cmd_list = Self::create_commands(table_cfg);
let aliases = Self::create_aliases();
let help_cmd = create_help_command(&cmd_list, &aliases, table_cfg);
cmd_list.push(cmd_view!(help_cmd, true));
Self {
commands: HashMap::from_iter(cmd_list),
aliases: HashMap::from_iter(aliases),
}
}
pub fn find(&self, args: &str) -> Option<std::io::Result<Command>> {
let cmd = args.split_once(' ').map_or(args, |(cmd, _)| cmd);
let args = &args[cmd.len()..];
let command = self.find_command(cmd);
parse_command(command, args)
}
fn find_command(&self, cmd: &str) -> Option<Command> {
match self.commands.get(cmd).cloned() {
None => self
.aliases
.get(cmd)
.and_then(|cmd| self.commands.get(cmd).cloned()),
cmd => cmd,
}
}
}
fn create_help_command(
commands: &[(&str, Command)],
aliases: &[(&str, &str)],
table_cfg: TableConfig,
) -> HelpCmd {
let help_manuals = create_help_manuals(commands);
HelpCmd::new(help_manuals, aliases, table_cfg)
}
fn parse_command(command: Option<Command>, args: &str) -> Option<std::io::Result<Command>> {
match command {
Some(mut cmd) => {
let result = match &mut cmd {
Command::Reactive(cmd) => cmd.parse(args),
Command::View { cmd, .. } => cmd.parse(args),
};
Some(result.map(|_| cmd))
}
None => None,
}
}
fn create_help_manuals(cmd_list: &[(&str, Command)]) -> Vec<HelpManual> {
let mut help_manuals: Vec<_> = cmd_list
.iter()
.map(|(_, cmd)| cmd)
.map(create_help_manual)
.collect();
help_manuals.push(__create_help_manual(
HelpCmd::default().help(),
HelpCmd::NAME,
));
help_manuals
}
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 {
match manual {
Some(manual) => manual,
None => HelpManual {
name,
description: "",
arguments: Vec::new(),
examples: Vec::new(),
},
}
}
// type helper to deal with `Box`es
#[derive(Clone)]
struct ViewCmd<C>(C);
impl<C> ViewCommand for ViewCmd<C>
where
C: ViewCommand,
C::View: View + 'static,
{
type View = Box<dyn View>;
fn name(&self) -> &'static str {
self.0.name()
}
fn usage(&self) -> &'static str {
self.0.usage()
}
fn help(&self) -> Option<HelpManual> {
self.0.help()
}
fn parse(&mut self, args: &str) -> std::io::Result<()> {
self.0.parse(args)
}
fn spawn(
&mut self,
engine_state: &nu_protocol::engine::EngineState,
stack: &mut nu_protocol::engine::Stack,
value: Option<nu_protocol::Value>,
) -> std::io::Result<Self::View> {
let view = self.0.spawn(engine_state, stack, value)?;
Ok(Box::new(view) as Box<dyn View>)
}
}
pub trait SCommand: SimpleCommand + SCommandClone {}
impl<T> SCommand for T where T: 'static + SimpleCommand + Clone {}
pub trait SCommandClone {
fn clone_box(&self) -> Box<dyn SCommand>;
}
impl<T> SCommandClone for T
where
T: 'static + SCommand + Clone,
{
fn clone_box(&self) -> Box<dyn SCommand> {
Box::new(self.clone())
}
}
impl Clone for Box<dyn SCommand> {
fn clone(&self) -> Box<dyn SCommand> {
self.clone_box()
}
}
pub trait VCommand: ViewCommand<View = Box<dyn View>> + VCommandClone {}
impl<T> VCommand for T where T: 'static + ViewCommand<View = Box<dyn View>> + Clone {}
pub trait VCommandClone {
fn clone_box(&self) -> Box<dyn VCommand>;
}
impl<T> VCommandClone for T
where
T: 'static + VCommand + Clone,
{
fn clone_box(&self) -> Box<dyn VCommand> {
Box::new(self.clone())
}
}
impl Clone for Box<dyn VCommand> {
fn clone(&self) -> Box<dyn VCommand> {
self.clone_box()
}
}

View File

@ -0,0 +1,246 @@
use std::{
collections::HashMap,
io::{self, Result},
};
use nu_protocol::{
engine::{EngineState, Stack},
Value,
};
use crate::{nu_common::NuSpan, pager::TableConfig, views::RecordView};
use super::{HelpExample, HelpManual, ViewCommand};
#[derive(Debug, Default, Clone)]
pub struct HelpCmd {
input_command: String,
table_cfg: TableConfig,
supported_commands: Vec<HelpManual>,
aliases: HashMap<String, Vec<String>>,
}
impl HelpCmd {
pub const NAME: &'static str = "help";
pub fn new(
commands: Vec<HelpManual>,
aliases: &[(&str, &str)],
table_cfg: TableConfig,
) -> Self {
let aliases = collect_aliases(aliases);
Self {
input_command: String::new(),
supported_commands: commands,
aliases,
table_cfg,
}
}
}
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
}
impl ViewCommand for HelpCmd {
type View = RecordView<'static>;
fn name(&self) -> &'static str {
Self::NAME
}
fn usage(&self) -> &'static str {
""
}
fn help(&self) -> Option<HelpManual> {
Some(HelpManual {
name: "help",
description: "Looks up a help information about a command or a `explore`",
arguments: vec![],
examples: vec![
HelpExample {
example: "help",
description: "Open a help information about the `explore`",
},
HelpExample {
example: "help nu",
description: "Find a help list of `nu` command",
},
HelpExample {
example: "help help",
description: "...It was supposed to be hidden....until...now...",
},
],
})
}
fn parse(&mut self, args: &str) -> Result<()> {
self.input_command = args.trim().to_owned();
Ok(())
}
fn spawn(&mut self, _: &EngineState, _: &mut Stack, _: Option<Value>) -> Result<Self::View> {
if self.input_command.is_empty() {
let (headers, data) = help_frame_data(&self.supported_commands, &self.aliases);
let view = RecordView::new(headers, data, self.table_cfg);
return Ok(view);
}
let manual = self
.supported_commands
.iter()
.find(|manual| manual.name == self.input_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, self.table_cfg);
Ok(view)
}
}
fn help_frame_data(
supported_commands: &[HelpManual],
aliases: &HashMap<String, Vec<String>>,
) -> (Vec<String>, Vec<Vec<Value>>) {
macro_rules! null {
() => {
Value::Nothing {
span: NuSpan::unknown(),
}
};
}
macro_rules! nu_str {
($text:expr) => {
Value::String {
val: $text.to_string(),
span: NuSpan::unknown(),
}
};
}
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,
vals,
span: NuSpan::unknown(),
}
})
.collect();
let commands = Value::List {
vals: commands,
span: NuSpan::unknown(),
};
let headers = vec!["name", "mode", "information", "description"];
#[rustfmt::skip]
let shortcuts = [
(":", "view", commands, "Run a command"),
("/", "view", null!(), "Search via pattern"),
("?", "view", null!(), "Search via pattern but results will be reversed when you press <n>"),
("n", "view", null!(), "Gets to the next found element in search"),
("i", "view", null!(), "Turn on a cursor mode so you can inspect values"),
("t", "view", null!(), "Transpose table, so columns became rows and vice versa"),
("Up", "", null!(), "Moves to an element above"),
("Down", "", null!(), "Moves to an element bellow"),
("Left", "", null!(), "Moves to an element to the left"),
("Right", "", null!(), "Moves to an element to the right"),
("PgDown", "view", null!(), "Moves to an a bunch of elements bellow"),
("PgUp", "view", null!(), "Moves to an a bunch of elements above"),
("Esc", "", null!(), "Exits a cursor mode. Exists an expected element."),
("Enter", "cursor", null!(), "Inspect a chosen element"),
];
let headers = headers.iter().map(|s| s.to_string()).collect();
let data = shortcuts
.iter()
.map(|(name, mode, info, desc)| {
vec![nu_str!(name), nu_str!(mode), info.clone(), nu_str!(desc)]
})
.collect();
(headers, data)
}
fn help_manual_data(manual: &HelpManual, aliases: &[String]) -> (Vec<String>, Vec<Vec<Value>>) {
macro_rules! nu_str {
($text:expr) => {
Value::String {
val: $text.to_string(),
span: NuSpan::unknown(),
}
};
}
let arguments = manual
.arguments
.iter()
.map(|e| Value::Record {
cols: vec![String::from("example"), String::from("description")],
vals: vec![nu_str!(e.example), nu_str!(e.description)],
span: NuSpan::unknown(),
})
.collect();
let arguments = Value::List {
vals: arguments,
span: NuSpan::unknown(),
};
let examples = manual
.examples
.iter()
.map(|e| Value::Record {
cols: vec![String::from("example"), String::from("description")],
vals: vec![nu_str!(e.example), nu_str!(e.description)],
span: NuSpan::unknown(),
})
.collect();
let examples = Value::List {
vals: examples,
span: 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("examples"),
String::from("description"),
];
let data = vec![vec![name, aliases, arguments, examples, desc]];
(headers, data)
}

View File

@ -0,0 +1,71 @@
use nu_protocol::{
engine::{EngineState, Stack},
Value,
};
use super::pager::{Pager, Transition};
use std::io::Result;
mod help;
mod nu;
mod preview;
mod quit;
mod r#try;
pub use help::HelpCmd;
pub use nu::NuCmd;
pub use preview::PreviewCmd;
pub use quit::QuitCmd;
pub use r#try::TryCmd;
pub trait SimpleCommand {
fn name(&self) -> &'static str;
fn usage(&self) -> &'static str;
fn help(&self) -> Option<HelpManual>;
fn parse(&mut self, args: &str) -> Result<()>;
fn react(
&mut self,
engine_state: &EngineState,
stack: &mut Stack,
pager: &mut Pager<'_>,
value: Option<Value>,
) -> Result<Transition>;
}
pub trait ViewCommand {
type View;
fn name(&self) -> &'static str;
fn usage(&self) -> &'static str;
fn help(&self) -> Option<HelpManual>;
fn parse(&mut self, args: &str) -> Result<()>;
fn spawn(
&mut self,
engine_state: &EngineState,
stack: &mut Stack,
value: Option<Value>,
) -> 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>,
}
#[derive(Debug, Default, Clone)]
pub struct HelpExample {
pub example: &'static str,
pub description: &'static str,
}

View File

@ -0,0 +1,153 @@
use std::io::{self, Result};
use nu_protocol::{
engine::{EngineState, Stack},
PipelineData, Value,
};
use crate::{
nu_common::{collect_pipeline, run_nu_command},
pager::TableConfig,
views::{Preview, RecordView, View},
};
use super::{HelpExample, HelpManual, ViewCommand};
#[derive(Debug, Default, Clone)]
pub struct NuCmd {
command: String,
table_cfg: TableConfig,
}
impl NuCmd {
pub fn new(table_cfg: TableConfig) -> Self {
Self {
command: String::new(),
table_cfg,
}
}
pub const NAME: &'static str = "nu";
}
impl ViewCommand for NuCmd {
type View = NuView<'static>;
fn name(&self) -> &'static str {
Self::NAME
}
fn usage(&self) -> &'static str {
""
}
fn help(&self) -> Option<HelpManual> {
Some(HelpManual {
name: "nu",
description: "Run a nu command. You can use a presented table as an input",
arguments: vec![],
examples: vec![
HelpExample {
example: "where type == 'file'",
description: "Filter data to get only entries with a type being a 'file'",
},
HelpExample {
example: "get scope | get examples",
description: "Get a inner values",
},
HelpExample {
example: "open Cargo.toml",
description: "Open a Cargo.toml file",
},
],
})
}
fn parse(&mut self, args: &str) -> Result<()> {
self.command = args.trim().to_owned();
Ok(())
}
fn spawn(
&mut self,
engine_state: &EngineState,
stack: &mut Stack,
value: Option<Value>,
) -> Result<Self::View> {
let value = value.unwrap_or_default();
let pipeline = PipelineData::Value(value, None);
let pipeline = run_nu_command(engine_state, stack, &self.command, pipeline)
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
let (columns, values) = collect_pipeline(pipeline);
let has_single_value = values.len() == 1 && values[0].len() == 1;
let is_simple_type = !matches!(&values[0][0], Value::List { .. } | Value::Record { .. });
if has_single_value && is_simple_type {
let config = &engine_state.config;
let text = values[0][0].into_abbreviated_string(config);
return Ok(NuView::Preview(Preview::new(&text)));
}
let view = RecordView::new(columns, values, self.table_cfg);
Ok(NuView::Records(view))
}
}
pub enum NuView<'a> {
Records(RecordView<'a>),
Preview(Preview),
}
impl View for NuView<'_> {
fn draw(
&mut self,
f: &mut crate::pager::Frame,
area: tui::layout::Rect,
cfg: &crate::ViewConfig,
layout: &mut crate::views::Layout,
) {
match self {
NuView::Records(v) => v.draw(f, area, cfg, layout),
NuView::Preview(v) => v.draw(f, area, cfg, layout),
}
}
fn handle_input(
&mut self,
engine_state: &EngineState,
stack: &mut Stack,
layout: &crate::views::Layout,
info: &mut crate::pager::ViewInfo,
key: crossterm::event::KeyEvent,
) -> Option<crate::pager::Transition> {
match self {
NuView::Records(v) => v.handle_input(engine_state, stack, layout, info, key),
NuView::Preview(v) => v.handle_input(engine_state, stack, layout, info, key),
}
}
fn show_data(&mut self, i: usize) -> bool {
match self {
NuView::Records(v) => v.show_data(i),
NuView::Preview(v) => v.show_data(i),
}
}
fn collect_data(&self) -> Vec<crate::nu_common::NuText> {
match self {
NuView::Records(v) => v.collect_data(),
NuView::Preview(v) => v.collect_data(),
}
}
fn exit(&mut self) -> Option<Value> {
match self {
NuView::Records(v) => v.exit(),
NuView::Preview(v) => v.exit(),
}
}
}

View File

@ -0,0 +1,81 @@
use std::io::Result;
use nu_color_config::get_color_config;
use nu_protocol::{
engine::{EngineState, Stack},
Value,
};
use crate::{
nu_common::{self, collect_input},
views::Preview,
};
use super::{HelpManual, ViewCommand};
#[derive(Default, Clone)]
pub struct PreviewCmd;
impl PreviewCmd {
pub fn new() -> Self {
Self
}
}
impl PreviewCmd {
pub const NAME: &'static str = "preview";
}
impl ViewCommand for PreviewCmd {
type View = Preview;
fn name(&self) -> &'static str {
Self::NAME
}
fn usage(&self) -> &'static str {
""
}
fn help(&self) -> Option<HelpManual> {
Some(HelpManual {
name: "preview",
description: "Preview current value/table if any is currently in use",
arguments: vec![],
examples: vec![],
})
}
fn parse(&mut self, _: &str) -> Result<()> {
Ok(())
}
fn spawn(
&mut self,
engine_state: &EngineState,
_stack: &mut Stack,
value: Option<Value>,
) -> Result<Self::View> {
let value = match value {
Some(value) => {
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].into_abbreviated_string(config)
} else {
let ctrlc = engine_state.ctrlc.clone();
let config = engine_state.get_config();
let color_hm = get_color_config(config);
nu_common::try_build_table(ctrlc, config, &color_hm, value)
}
}
None => String::new(),
};
Ok(Preview::new(&value))
}
}

View File

@ -0,0 +1,50 @@
use std::io::Result;
use nu_protocol::{
engine::{EngineState, Stack},
Value,
};
use crate::pager::{Pager, Transition};
use super::{HelpManual, SimpleCommand};
#[derive(Default, Clone)]
pub struct QuitCmd;
impl QuitCmd {
pub const NAME: &'static str = "quit";
}
impl SimpleCommand for QuitCmd {
fn name(&self) -> &'static str {
Self::NAME
}
fn usage(&self) -> &'static str {
""
}
fn help(&self) -> Option<HelpManual> {
Some(HelpManual {
name: "quit",
description: "Quit",
arguments: vec![],
examples: vec![],
})
}
fn parse(&mut self, _: &str) -> Result<()> {
Ok(())
}
fn react(
&mut self,
_: &EngineState,
_: &mut Stack,
_: &mut Pager<'_>,
_: Option<Value>,
) -> Result<Transition> {
Ok(Transition::Exit)
}
}

View File

@ -0,0 +1,70 @@
use std::io::Result;
use nu_protocol::{
engine::{EngineState, Stack},
Value,
};
use crate::{pager::TableConfig, views::InteractiveView};
use super::{HelpExample, HelpManual, ViewCommand};
#[derive(Debug, Default, Clone)]
pub struct TryCmd {
command: String,
table_cfg: TableConfig,
}
impl TryCmd {
pub fn new(table_cfg: TableConfig) -> Self {
Self {
command: String::new(),
table_cfg,
}
}
pub const NAME: &'static str = "try";
}
impl ViewCommand for TryCmd {
type View = InteractiveView<'static>;
fn name(&self) -> &'static str {
Self::NAME
}
fn usage(&self) -> &'static str {
""
}
fn help(&self) -> Option<HelpManual> {
Some(HelpManual {
name: "try",
description: "Opens a dynamic REPL to run nu commands",
arguments: vec![],
examples: vec![HelpExample {
example: "try open Cargo.toml",
description: "Optionally you can provide a command which will be run right away",
}],
})
}
fn parse(&mut self, args: &str) -> Result<()> {
self.command = args.trim().to_owned();
Ok(())
}
fn spawn(
&mut self,
_: &EngineState,
_: &mut Stack,
value: Option<Value>,
) -> Result<Self::View> {
let value = value.unwrap_or_default();
let mut view = InteractiveView::new(value, self.table_cfg);
view.init(self.command.clone());
Ok(view)
}
}

View File

@ -0,0 +1,51 @@
use std::{
io::Result,
time::{Duration, Instant},
};
use crossterm::event::{poll, read, Event, KeyEvent};
pub struct UIEvents {
tick_rate: Duration,
}
pub struct Cfg {
pub tick_rate: Duration,
}
impl Default for Cfg {
fn default() -> Cfg {
Cfg {
tick_rate: Duration::from_millis(250),
}
}
}
impl UIEvents {
pub fn new() -> UIEvents {
UIEvents::with_config(Cfg::default())
}
pub fn with_config(config: Cfg) -> UIEvents {
UIEvents {
tick_rate: config.tick_rate,
}
}
pub fn next(&self) -> Result<Option<KeyEvent>> {
let now = Instant::now();
match poll(self.tick_rate) {
Ok(true) => match read()? {
Event::Key(event) => Ok(Some(event)),
_ => {
let time_spent = now.elapsed();
let rest = self.tick_rate - time_spent;
Self { tick_rate: rest }.next()
}
},
Ok(false) => Ok(None),
Err(err) => Err(err),
}
}
}

View File

@ -0,0 +1,60 @@
mod command;
mod commands;
mod events;
mod nu_common;
mod pager;
mod views;
use std::io;
use nu_common::{collect_pipeline, CtrlC};
use nu_protocol::{
engine::{EngineState, Stack},
PipelineData, Value,
};
use pager::{Page, Pager};
use terminal_size::{Height, Width};
use views::{InformationView, Preview, RecordView};
pub use pager::{StyleConfig, TableConfig, TableSplitLines, ViewConfig};
pub fn run_pager(
engine_state: &EngineState,
stack: &mut Stack,
ctrlc: CtrlC,
table_cfg: TableConfig,
view_cfg: ViewConfig,
input: PipelineData,
) -> io::Result<Option<Value>> {
let commands = command::CommandList::new(table_cfg);
let mut p = Pager::new(table_cfg, view_cfg.clone());
let (columns, data) = collect_pipeline(input);
let has_no_input = columns.is_empty() && data.is_empty();
if has_no_input {
let view = Some(Page::new(InformationView, true));
return p.run(engine_state, stack, ctrlc, view, commands);
}
let has_single_value = data.len() == 1 && data[0].len() == 1;
let is_simple_type = !matches!(&data[0][0], Value::List { .. } | Value::Record { .. });
if has_single_value && is_simple_type {
let text = data[0][0].into_abbreviated_string(view_cfg.config);
let view = Some(Page::new(Preview::new(&text), true));
return p.run(engine_state, stack, ctrlc, view, commands);
}
let mut view = RecordView::new(columns, data, table_cfg);
if table_cfg.reverse {
if let Some((Width(w), Height(h))) = terminal_size::terminal_size() {
view.reverse(w, h);
}
}
let view = Some(Page::new(view, false));
p.run(engine_state, stack, ctrlc, view, commands)
}

View File

@ -0,0 +1,42 @@
use nu_engine::eval_block;
use nu_parser::parse;
use nu_protocol::{
engine::{EngineState, Stack, StateWorkingSet},
PipelineData, ShellError,
};
pub fn run_nu_command(
engine_state: &EngineState,
stack: &mut Stack,
cmd: &str,
current: PipelineData,
) -> std::result::Result<PipelineData, ShellError> {
let engine_state = engine_state.clone();
eval_source2(&engine_state, stack, cmd.as_bytes(), "", current)
}
fn eval_source2(
engine_state: &EngineState,
stack: &mut Stack,
source: &[u8],
fname: &str,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let (block, _) = {
let mut working_set = StateWorkingSet::new(engine_state);
let (output, err) = parse(
&mut working_set,
Some(fname), // format!("entry #{}", entry_num)
source,
false,
&[],
);
if let Some(err) = err {
return Err(ShellError::IOError(err.to_string()));
}
(output, working_set.render())
};
eval_block(engine_state, stack, &block, input, false, false)
}

View File

@ -0,0 +1,21 @@
mod command;
mod table;
mod value;
use std::{
collections::HashMap,
sync::{atomic::AtomicBool, Arc},
};
use nu_table::TextStyle;
pub use nu_ansi_term::{Color as NuColor, Style as NuStyle};
pub use nu_protocol::{Config as NuConfig, Span as NuSpan};
pub type NuText = (String, TextStyle);
pub type CtrlC = Option<Arc<AtomicBool>>;
pub type NuStyleTable = HashMap<String, NuStyle>;
pub use command::run_nu_command;
pub use table::try_build_table;
pub use value::{collect_input, collect_pipeline};

View File

@ -0,0 +1,844 @@
use nu_color_config::{get_color_config, style_primitive};
use nu_engine::column::get_columns;
use nu_protocol::{ast::PathMember, Config, ShellError, Span, TableIndexMode, Value};
use nu_table::{string_width, Alignment, Alignments, Table as NuTable, TableTheme, TextStyle};
use std::sync::Arc;
use std::{
cmp::max,
collections::HashMap,
sync::atomic::{AtomicBool, Ordering},
};
const INDEX_COLUMN_NAME: &str = "index";
type NuText = (String, TextStyle);
type NuColorMap = HashMap<String, nu_ansi_term::Style>;
use crate::nu_common::{NuConfig, NuStyleTable};
pub fn try_build_table(
ctrlc: Option<Arc<AtomicBool>>,
config: &NuConfig,
color_hm: &NuStyleTable,
value: Value,
) -> String {
let theme = load_theme_from_config(config);
match value {
Value::List { vals, span } => try_build_list(vals, &ctrlc, config, span, color_hm, theme),
Value::Record { cols, vals, span } => {
try_build_map(cols, vals, span, ctrlc, config, color_hm)
}
val => value_to_styled_string(&val, config, color_hm).0,
}
}
fn try_build_map(
cols: Vec<String>,
vals: Vec<Value>,
span: Span,
ctrlc: Option<Arc<AtomicBool>>,
config: &NuConfig,
color_hm: &HashMap<String, nu_ansi_term::Style>,
) -> String {
let result = build_expanded_table(
cols.clone(),
vals.clone(),
span,
ctrlc,
config,
usize::MAX,
None,
false,
"",
);
match result {
Ok(Some(result)) => result,
Ok(None) | Err(_) => {
value_to_styled_string(&Value::Record { cols, vals, span }, config, color_hm).0
}
}
}
fn try_build_list(
vals: Vec<Value>,
ctrlc: &Option<Arc<AtomicBool>>,
config: &NuConfig,
span: Span,
color_hm: &HashMap<String, nu_ansi_term::Style>,
theme: TableTheme,
) -> String {
let table = convert_to_table2(
0,
vals.iter(),
ctrlc.clone(),
config,
span,
color_hm,
&theme,
None,
false,
"",
usize::MAX,
);
match table {
Ok(Some(table)) => {
let val = table.draw_table(
config,
color_hm,
Alignments::default(),
&theme,
usize::MAX,
false,
);
match val {
Some(result) => result,
None => value_to_styled_string(&Value::List { vals, span }, config, color_hm).0,
}
}
Ok(None) | Err(_) => {
// it means that the list is empty
value_to_styled_string(&Value::List { vals, span }, config, color_hm).0
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn build_expanded_table(
cols: Vec<String>,
vals: Vec<Value>,
span: Span,
ctrlc: Option<Arc<AtomicBool>>,
config: &Config,
term_width: usize,
expand_limit: Option<usize>,
flatten: bool,
flatten_sep: &str,
) -> Result<Option<String>, ShellError> {
let theme = load_theme_from_config(config);
let color_hm = get_color_config(config);
let alignments = Alignments::default();
// calculate the width of a key part + the rest of table so we know the rest of the table width available for value.
let key_width = cols.iter().map(|col| string_width(col)).max().unwrap_or(0);
let key = NuTable::create_cell(" ".repeat(key_width), TextStyle::default());
let key_table = NuTable::new(vec![vec![key]], (1, 2), term_width, false, false);
let key_width = key_table
.draw_table(config, &color_hm, alignments, &theme, usize::MAX, false)
.map(|table| string_width(&table))
.unwrap_or(0);
// 3 - count borders (left, center, right)
// 2 - padding
if key_width + 3 + 2 > term_width {
return Ok(None);
}
let remaining_width = term_width - key_width - 3 - 2;
let mut data = Vec::with_capacity(cols.len());
for (key, value) in cols.into_iter().zip(vals) {
// handle CTRLC event
if let Some(ctrlc) = &ctrlc {
if ctrlc.load(Ordering::SeqCst) {
return Ok(None);
}
}
let is_limited = matches!(expand_limit, Some(0));
let mut is_expanded = false;
let value = if is_limited {
value_to_styled_string(&value, config, &color_hm).0
} else {
let deep = expand_limit.map(|i| i - 1);
match value {
Value::List { vals, .. } => {
let table = convert_to_table2(
0,
vals.iter(),
ctrlc.clone(),
config,
span,
&color_hm,
&theme,
deep,
flatten,
flatten_sep,
remaining_width,
)?;
match table {
Some(mut table) => {
// controll width via removing table columns.
let theme = load_theme_from_config(config);
table.truncate(remaining_width, &theme);
is_expanded = true;
let val = table.draw_table(
config,
&color_hm,
alignments,
&theme,
remaining_width,
false,
);
match val {
Some(result) => result,
None => return Ok(None),
}
}
None => {
// it means that the list is empty
let value = Value::List { vals, span };
value_to_styled_string(&value, config, &color_hm).0
}
}
}
Value::Record { cols, vals, span } => {
let result = build_expanded_table(
cols.clone(),
vals.clone(),
span,
ctrlc.clone(),
config,
remaining_width,
deep,
flatten,
flatten_sep,
)?;
match result {
Some(result) => {
is_expanded = true;
result
}
None => {
let failed_value = value_to_styled_string(
&Value::Record { cols, vals, span },
config,
&color_hm,
);
nu_table::wrap_string(&failed_value.0, remaining_width)
}
}
}
val => {
let text = value_to_styled_string(&val, config, &color_hm).0;
nu_table::wrap_string(&text, remaining_width)
}
}
};
// we want to have a key being aligned to 2nd line,
// we could use Padding for it but,
// the easiest way to do so is just push a new_line char before
let mut key = key;
if !key.is_empty() && is_expanded && theme.has_top_line() {
key.insert(0, '\n');
}
let key = NuTable::create_cell(key, TextStyle::default_field());
let val = NuTable::create_cell(value, TextStyle::default());
let row = vec![key, val];
data.push(row);
}
let data_len = data.len();
let table = NuTable::new(data, (data_len, 2), term_width, false, false);
let table_s = table
.clone()
.draw_table(config, &color_hm, alignments, &theme, term_width, false);
let table = match table_s {
Some(s) => {
// check whether we need to expand table or not,
// todo: we can make it more effitient
const EXPAND_TREASHHOLD: f32 = 0.80;
let width = string_width(&s);
let used_percent = width as f32 / term_width as f32;
if width < term_width && used_percent > EXPAND_TREASHHOLD {
table.draw_table(config, &color_hm, alignments, &theme, term_width, true)
} else {
Some(s)
}
}
None => None,
};
Ok(table)
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::into_iter_on_ref)]
fn convert_to_table2<'a>(
row_offset: usize,
input: impl Iterator<Item = &'a Value> + ExactSizeIterator + Clone,
ctrlc: Option<Arc<AtomicBool>>,
config: &Config,
head: Span,
color_hm: &NuColorMap,
theme: &TableTheme,
deep: Option<usize>,
flatten: bool,
flatten_sep: &str,
available_width: usize,
) -> Result<Option<NuTable>, ShellError> {
const PADDING_SPACE: usize = 2;
const SPLIT_LINE_SPACE: usize = 1;
const ADDITIONAL_CELL_SPACE: usize = PADDING_SPACE + SPLIT_LINE_SPACE;
const TRUNCATE_CELL_WIDTH: usize = 3;
const MIN_CELL_CONTENT_WIDTH: usize = 1;
const OK_CELL_CONTENT_WIDTH: usize = 25;
if input.len() == 0 {
return Ok(None);
}
// 2 - split lines
let mut available_width = available_width.saturating_sub(SPLIT_LINE_SPACE + SPLIT_LINE_SPACE);
if available_width < MIN_CELL_CONTENT_WIDTH {
return Ok(None);
}
let headers = get_columns(input.clone());
let with_index = match config.table_index_mode {
TableIndexMode::Always => true,
TableIndexMode::Never => false,
TableIndexMode::Auto => headers.iter().any(|header| header == INDEX_COLUMN_NAME),
};
// The header with the INDEX is removed from the table headers since
// it is added to the natural table index
let headers: Vec<_> = headers
.into_iter()
.filter(|header| header != INDEX_COLUMN_NAME)
.collect();
let with_header = !headers.is_empty();
let mut data = vec![vec![]; input.len()];
if !headers.is_empty() {
data.push(vec![]);
};
if with_index {
let mut column_width = 0;
if with_header {
data[0].push(NuTable::create_cell("#", header_style(color_hm)));
}
for (row, item) in input.clone().into_iter().enumerate() {
let row = if with_header { row + 1 } else { row };
if let Some(ctrlc) = &ctrlc {
if ctrlc.load(Ordering::SeqCst) {
return Ok(None);
}
}
if let Value::Error { error } = item {
return Err(error.clone());
}
let index = row + row_offset;
let text = matches!(item, Value::Record { .. })
.then(|| lookup_index_value(item, config).unwrap_or_else(|| index.to_string()))
.unwrap_or_else(|| index.to_string());
let value = make_index_string(text, color_hm);
let width = string_width(&value.0);
column_width = max(column_width, width);
let value = NuTable::create_cell(value.0, value.1);
data[row].push(value);
}
if column_width + ADDITIONAL_CELL_SPACE > available_width {
available_width = 0;
} else {
available_width -= column_width + ADDITIONAL_CELL_SPACE;
}
}
if !with_header {
for (row, item) in input.into_iter().enumerate() {
if let Some(ctrlc) = &ctrlc {
if ctrlc.load(Ordering::SeqCst) {
return Ok(None);
}
}
if let Value::Error { error } = item {
return Err(error.clone());
}
let value = convert_to_table2_entry(
item,
config,
&ctrlc,
color_hm,
theme,
deep,
flatten,
flatten_sep,
available_width,
);
let value = NuTable::create_cell(value.0, value.1);
data[row].push(value);
}
let count_columns = if with_index { 2 } else { 1 };
let size = (data.len(), count_columns);
let table = NuTable::new(data, size, usize::MAX, with_header, with_index);
return Ok(Some(table));
}
let mut widths = Vec::new();
let mut truncate = false;
let count_columns = headers.len();
for (col, header) in headers.into_iter().enumerate() {
let is_last_col = col + 1 == count_columns;
let mut nessary_space = PADDING_SPACE;
if !is_last_col {
nessary_space += SPLIT_LINE_SPACE;
}
if available_width == 0 || available_width <= nessary_space {
// MUST NEVER HAPPEN (ideally)
// but it does...
truncate = true;
break;
}
available_width -= nessary_space;
let mut column_width = string_width(&header);
data[0].push(NuTable::create_cell(&header, header_style(color_hm)));
for (row, item) in input.clone().into_iter().enumerate() {
if let Some(ctrlc) = &ctrlc {
if ctrlc.load(Ordering::SeqCst) {
return Ok(None);
}
}
if let Value::Error { error } = item {
return Err(error.clone());
}
let value = create_table2_entry(
item,
&header,
head,
config,
&ctrlc,
color_hm,
theme,
deep,
flatten,
flatten_sep,
available_width,
);
let value_width = string_width(&value.0);
column_width = max(column_width, value_width);
let value = NuTable::create_cell(value.0, value.1);
data[row + 1].push(value);
}
if column_width >= available_width
|| (!is_last_col && column_width + nessary_space >= available_width)
{
// so we try to do soft landing
// by doing a truncating in case there will be enough space for it.
column_width = string_width(&header);
for (row, item) in input.clone().into_iter().enumerate() {
if let Some(ctrlc) = &ctrlc {
if ctrlc.load(Ordering::SeqCst) {
return Ok(None);
}
}
let value = create_table2_entry_basic(item, &header, head, config, color_hm);
let value = wrap_nu_text(value, available_width);
let value_width = string_width(&value.0);
column_width = max(column_width, value_width);
let value = NuTable::create_cell(value.0, value.1);
*data[row + 1].last_mut().expect("unwrap") = value;
}
}
let is_suitable_for_wrap =
available_width >= string_width(&header) && available_width >= OK_CELL_CONTENT_WIDTH;
if column_width >= available_width && is_suitable_for_wrap {
// so we try to do soft landing ONCE AGAIN
// but including a wrap
column_width = string_width(&header);
for (row, item) in input.clone().into_iter().enumerate() {
if let Some(ctrlc) = &ctrlc {
if ctrlc.load(Ordering::SeqCst) {
return Ok(None);
}
}
let value = create_table2_entry_basic(item, &header, head, config, color_hm);
let value = wrap_nu_text(value, OK_CELL_CONTENT_WIDTH);
let value = NuTable::create_cell(value.0, value.1);
*data[row + 1].last_mut().expect("unwrap") = value;
}
}
if column_width > available_width {
// remove just added column
for row in &mut data {
row.pop();
}
available_width += nessary_space;
truncate = true;
break;
}
available_width -= column_width;
widths.push(column_width);
}
if truncate {
if available_width <= TRUNCATE_CELL_WIDTH + PADDING_SPACE {
// back up by removing last column.
// it's ALWAYS MUST has us enough space for a shift column
while let Some(width) = widths.pop() {
for row in &mut data {
row.pop();
}
available_width += width + PADDING_SPACE + SPLIT_LINE_SPACE;
if available_width > TRUNCATE_CELL_WIDTH + PADDING_SPACE {
break;
}
}
}
// this must be a RARE case or even NEVER happen,
// but we do check it just in case.
if widths.is_empty() {
return Ok(None);
}
let shift = NuTable::create_cell(String::from("..."), TextStyle::default());
for row in &mut data {
row.push(shift.clone());
}
widths.push(3);
}
let count_columns = widths.len() + with_index as usize;
let count_rows = data.len();
let size = (count_rows, count_columns);
let table = NuTable::new(data, size, usize::MAX, with_header, with_index);
Ok(Some(table))
}
fn lookup_index_value(item: &Value, config: &Config) -> Option<String> {
item.get_data_by_key(INDEX_COLUMN_NAME)
.map(|value| value.into_string("", config))
}
fn header_style(color_hm: &NuColorMap) -> TextStyle {
TextStyle {
alignment: Alignment::Center,
color_style: Some(color_hm["header"]),
}
}
#[allow(clippy::too_many_arguments)]
fn create_table2_entry_basic(
item: &Value,
header: &str,
head: Span,
config: &Config,
color_hm: &NuColorMap,
) -> NuText {
match item {
Value::Record { .. } => {
let val = header.to_owned();
let path = PathMember::String { val, span: head };
let val = item.clone().follow_cell_path(&[path], false);
match val {
Ok(val) => value_to_styled_string(&val, config, color_hm),
Err(_) => error_sign(color_hm),
}
}
_ => value_to_styled_string(item, config, color_hm),
}
}
#[allow(clippy::too_many_arguments)]
fn create_table2_entry(
item: &Value,
header: &str,
head: Span,
config: &Config,
ctrlc: &Option<Arc<AtomicBool>>,
color_hm: &NuColorMap,
theme: &TableTheme,
deep: Option<usize>,
flatten: bool,
flatten_sep: &str,
width: usize,
) -> NuText {
match item {
Value::Record { .. } => {
let val = header.to_owned();
let path = PathMember::String { val, span: head };
let val = item.clone().follow_cell_path(&[path], false);
match val {
Ok(val) => convert_to_table2_entry(
&val,
config,
ctrlc,
color_hm,
theme,
deep,
flatten,
flatten_sep,
width,
),
Err(_) => wrap_nu_text(error_sign(color_hm), width),
}
}
_ => convert_to_table2_entry(
item,
config,
ctrlc,
color_hm,
theme,
deep,
flatten,
flatten_sep,
width,
),
}
}
fn error_sign(color_hm: &HashMap<String, nu_ansi_term::Style>) -> (String, TextStyle) {
make_styled_string(String::from(""), "empty", color_hm, 0)
}
fn wrap_nu_text(mut text: NuText, width: usize) -> NuText {
text.0 = nu_table::wrap_string(&text.0, width);
text
}
#[allow(clippy::too_many_arguments)]
fn convert_to_table2_entry(
item: &Value,
config: &Config,
ctrlc: &Option<Arc<AtomicBool>>,
color_hm: &NuColorMap,
theme: &TableTheme,
deep: Option<usize>,
flatten: bool,
flatten_sep: &str,
width: usize,
) -> NuText {
let is_limit_reached = matches!(deep, Some(0));
if is_limit_reached {
return wrap_nu_text(value_to_styled_string(item, config, color_hm), width);
}
match &item {
Value::Record { span, cols, vals } => {
if cols.is_empty() && vals.is_empty() {
wrap_nu_text(value_to_styled_string(item, config, color_hm), width)
} else {
let table = convert_to_table2(
0,
std::iter::once(item),
ctrlc.clone(),
config,
*span,
color_hm,
theme,
deep.map(|i| i - 1),
flatten,
flatten_sep,
width,
);
let inner_table = table.map(|table| {
table.and_then(|table| {
let alignments = Alignments::default();
table.draw_table(config, color_hm, alignments, theme, usize::MAX, false)
})
});
if let Ok(Some(table)) = inner_table {
(table, TextStyle::default())
} else {
// error so back down to the default
wrap_nu_text(value_to_styled_string(item, config, color_hm), width)
}
}
}
Value::List { vals, span } => {
let is_simple_list = vals
.iter()
.all(|v| !matches!(v, Value::Record { .. } | Value::List { .. }));
if flatten && is_simple_list {
wrap_nu_text(
convert_value_list_to_string(vals, config, color_hm, flatten_sep),
width,
)
} else {
let table = convert_to_table2(
0,
vals.iter(),
ctrlc.clone(),
config,
*span,
color_hm,
theme,
deep.map(|i| i - 1),
flatten,
flatten_sep,
width,
);
let inner_table = table.map(|table| {
table.and_then(|table| {
let alignments = Alignments::default();
table.draw_table(config, color_hm, alignments, theme, usize::MAX, false)
})
});
if let Ok(Some(table)) = inner_table {
(table, TextStyle::default())
} else {
// error so back down to the default
wrap_nu_text(value_to_styled_string(item, config, color_hm), width)
}
}
}
_ => wrap_nu_text(value_to_styled_string(item, config, color_hm), width), // unknown type.
}
}
fn convert_value_list_to_string(
vals: &[Value],
config: &Config,
color_hm: &NuColorMap,
flatten_sep: &str,
) -> NuText {
let mut buf = Vec::new();
for value in vals {
let (text, _) = value_to_styled_string(value, config, color_hm);
buf.push(text);
}
let text = buf.join(flatten_sep);
(text, TextStyle::default())
}
fn value_to_styled_string(value: &Value, config: &Config, color_hm: &NuColorMap) -> NuText {
let float_precision = config.float_precision as usize;
make_styled_string(
value.into_abbreviated_string(config),
&value.get_type().to_string(),
color_hm,
float_precision,
)
}
fn make_styled_string(
text: String,
text_type: &str,
color_hm: &NuColorMap,
float_precision: usize,
) -> NuText {
if text_type == "float" {
// set dynamic precision from config
let precise_number = match convert_with_precision(&text, float_precision) {
Ok(num) => num,
Err(e) => e.to_string(),
};
(precise_number, style_primitive(text_type, color_hm))
} else {
(text, style_primitive(text_type, color_hm))
}
}
fn make_index_string(text: String, color_hm: &NuColorMap) -> NuText {
let style = TextStyle::new()
.alignment(Alignment::Right)
.style(color_hm["row_index"]);
(text, style)
}
fn convert_with_precision(val: &str, precision: usize) -> Result<String, ShellError> {
// vall will always be a f64 so convert it with precision formatting
let val_float = match val.trim().parse::<f64>() {
Ok(f) => f,
Err(e) => {
return Err(ShellError::GenericError(
format!("error converting string [{}] to f64", &val),
"".to_string(),
None,
Some(e.to_string()),
Vec::new(),
));
}
};
Ok(format!("{:.prec$}", val_float, prec = precision))
}
fn load_theme_from_config(config: &Config) -> TableTheme {
match config.table_mode.as_str() {
"basic" => nu_table::TableTheme::basic(),
"thin" => nu_table::TableTheme::thin(),
"light" => nu_table::TableTheme::light(),
"compact" => nu_table::TableTheme::compact(),
"with_love" => nu_table::TableTheme::with_love(),
"compact_double" => nu_table::TableTheme::compact_double(),
"rounded" => nu_table::TableTheme::rounded(),
"reinforced" => nu_table::TableTheme::reinforced(),
"heavy" => nu_table::TableTheme::heavy(),
"none" => nu_table::TableTheme::none(),
_ => nu_table::TableTheme::rounded(),
}
}

View File

@ -0,0 +1,170 @@
use nu_engine::get_columns;
use nu_protocol::{ast::PathMember, PipelineData, Value};
use super::NuSpan;
pub fn collect_pipeline(input: PipelineData) -> (Vec<String>, Vec<Vec<Value>>) {
match input {
PipelineData::Value(value, ..) => collect_input(value),
PipelineData::ListStream(mut stream, ..) => {
let mut records = vec![];
for item in stream.by_ref() {
records.push(item);
}
let mut cols = get_columns(&records);
let data = convert_records_to_dataset(&cols, records);
// trying to deal with 'not standart input'
if cols.is_empty() && !data.is_empty() {
let min_column_length = data.iter().map(|row| row.len()).min().unwrap_or(0);
if min_column_length > 0 {
cols = (0..min_column_length).map(|i| i.to_string()).collect();
}
}
(cols, data)
}
PipelineData::ExternalStream {
stdout,
stderr,
exit_code,
metadata,
span,
..
} => {
let mut columns = vec![];
let mut data = vec![];
if let Some(stdout) = stdout {
let value = stdout.into_string().map_or_else(
|error| Value::Error { error },
|string| Value::string(string.item, span),
);
columns.push(String::from("stdout"));
data.push(vec![value]);
}
if let Some(stderr) = stderr {
let value = stderr.into_string().map_or_else(
|error| Value::Error { error },
|string| Value::string(string.item, span),
);
columns.push(String::from("stderr"));
data.push(vec![value]);
}
if let Some(exit_code) = exit_code {
let list = exit_code.collect::<Vec<_>>();
columns.push(String::from("exit_code"));
data.push(list);
}
if metadata.is_some() {
columns.push(String::from("metadata"));
data.push(vec![Value::Record {
cols: vec![String::from("data_source")],
vals: vec![Value::String {
val: String::from("ls"),
span,
}],
span,
}]);
}
(columns, data)
}
}
}
/// Try to build column names and a table grid.
pub fn collect_input(value: Value) -> (Vec<String>, Vec<Vec<Value>>) {
match value {
Value::Record { cols, vals, .. } => (cols, vec![vals]),
Value::List { vals, .. } => {
let mut columns = get_columns(&vals);
let data = convert_records_to_dataset(&columns, vals);
if columns.is_empty() && !data.is_empty() {
columns = vec![String::from("")];
}
(columns, data)
}
Value::String { val, span } => {
let lines = val
.lines()
.map(|line| Value::String {
val: line.to_string(),
span,
})
.map(|val| vec![val])
.collect();
(vec![String::from("")], lines)
}
Value::Nothing { .. } => (vec![], vec![]),
value => (vec![String::from("")], vec![vec![value]]),
}
}
fn convert_records_to_dataset(cols: &Vec<String>, records: Vec<Value>) -> Vec<Vec<Value>> {
if !cols.is_empty() {
create_table_for_record(cols, &records)
} else if cols.is_empty() && records.is_empty() {
vec![]
} else if cols.len() == records.len() {
vec![records]
} else {
// I am not sure whether it's good to return records as its length LIKELY will not match columns,
// which makes no scense......
//
// BUT...
// we can represent it as a list; which we do
records.into_iter().map(|record| vec![record]).collect()
}
}
fn create_table_for_record(headers: &[String], items: &[Value]) -> Vec<Vec<Value>> {
let mut data = vec![Vec::new(); items.len()];
for (i, item) in items.iter().enumerate() {
let row = record_create_row(headers, item);
data[i] = row;
}
data
}
fn record_create_row(headers: &[String], item: &Value) -> Vec<Value> {
let mut rows = vec![Value::default(); headers.len()];
for (i, header) in headers.iter().enumerate() {
let value = record_lookup_value(item, header);
rows[i] = value;
}
rows
}
fn record_lookup_value(item: &Value, header: &str) -> Value {
match item {
Value::Record { .. } => {
let path = PathMember::String {
val: header.to_owned(),
span: NuSpan::unknown(),
};
let value = item.clone().follow_cell_path(&[path], false);
match value {
Ok(value) => value,
Err(_) => item.clone(),
}
}
item => item.clone(),
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,140 @@
use std::borrow::Cow;
use ansi_str::{get_blocks, AnsiStr};
use tui::{
layout::Rect,
style::{Color, Modifier, Style},
widgets::Widget,
};
pub struct ColoredTextW<'a> {
text: &'a str,
col: usize,
}
impl<'a> ColoredTextW<'a> {
pub fn new(text: &'a str, col: usize) -> Self {
Self { text, col }
}
pub fn what(&self, area: Rect) -> String {
let text = cut_string(self.text, area, self.col);
text.ansi_strip().into_owned()
}
}
impl Widget for ColoredTextW<'_> {
fn render(self, area: Rect, buf: &mut tui::buffer::Buffer) {
let text = cut_string(self.text, area, self.col);
let mut offset = 0;
for block in get_blocks(&text) {
let text = block.text();
let style = style_to_tui(block.style());
let x = area.x + offset as u16;
let (o, _) = buf.set_stringn(x, area.y, text, area.width as usize, style);
offset = o
}
}
}
fn cut_string(text: &str, area: Rect, skip: usize) -> Cow<'_, str> {
let mut text = Cow::Borrowed(text);
if skip > 0 {
let n = text
.ansi_strip()
.chars()
.map(|c| c.len_utf8())
.take(skip)
.sum::<usize>();
let s = text.ansi_get(n..).expect("must be OK").into_owned();
text = Cow::Owned(s);
}
if !text.is_empty() && text.len() > area.width as usize {
let n = text
.ansi_strip()
.chars()
.map(|c| c.len_utf8())
.take(area.width as usize)
.sum::<usize>();
let s = text.ansi_get(..n).expect("must be ok").into_owned();
text = Cow::Owned(s);
}
text
}
fn style_to_tui(style: ansi_str::Style) -> Style {
let mut out = Style::default();
if let Some(clr) = style.background() {
out.bg = ansi_color_to_tui_color(clr);
}
if let Some(clr) = style.foreground() {
out.fg = ansi_color_to_tui_color(clr);
}
if style.is_slow_blink() || style.is_rapid_blink() {
out.add_modifier |= Modifier::SLOW_BLINK;
}
if style.is_bold() {
out.add_modifier |= Modifier::BOLD;
}
if style.is_faint() {
out.add_modifier |= Modifier::DIM;
}
if style.is_hide() {
out.add_modifier |= Modifier::HIDDEN;
}
if style.is_italic() {
out.add_modifier |= Modifier::ITALIC;
}
if style.is_inverse() {
out.add_modifier |= Modifier::REVERSED;
}
if style.is_underline() {
out.add_modifier |= Modifier::UNDERLINED;
}
out
}
fn ansi_color_to_tui_color(clr: ansi_str::Color) -> Option<Color> {
use ansi_str::Color::*;
let clr = match clr {
Black => Color::Black,
BrightBlack => Color::DarkGray,
Red => Color::Red,
BrightRed => Color::LightRed,
Green => Color::Green,
BrightGreen => Color::LightGreen,
Yellow => Color::Yellow,
BrightYellow => Color::LightYellow,
Blue => Color::Blue,
BrightBlue => Color::LightBlue,
Magenta => Color::Magenta,
BrightMagenta => Color::LightMagenta,
Cyan => Color::Cyan,
BrightCyan => Color::LightCyan,
White => Color::White,
Fixed(i) => Color::Indexed(i),
Rgb(r, g, b) => Color::Rgb(r, g, b),
BrightWhite => Color::Gray,
BrightPurple => Color::LightMagenta,
Purple => Color::Magenta,
};
Some(clr)
}

View File

@ -0,0 +1,76 @@
use crossterm::event::KeyEvent;
use nu_protocol::engine::{EngineState, Stack};
use nu_table::TextStyle;
use tui::{layout::Rect, widgets::Paragraph};
use crate::{
nu_common::NuText,
pager::{Frame, Transition, ViewConfig, ViewInfo},
};
use super::{Layout, View};
#[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,
_: KeyEvent,
) -> Option<Transition> {
None
}
fn collect_data(&self) -> Vec<NuText> {
Self::MESSAGE
.into_iter()
.map(|line| (line.to_owned(), TextStyle::default()))
.collect::<Vec<_>>()
}
}

View File

@ -0,0 +1,216 @@
use std::cmp::min;
use crossterm::event::{KeyCode, KeyEvent};
use nu_protocol::{
engine::{EngineState, Stack},
PipelineData, Value,
};
use tui::{
layout::Rect,
style::{Modifier, Style},
widgets::{BorderType, Borders, Paragraph},
};
use crate::{
nu_common::{collect_pipeline, run_nu_command},
pager::{Frame, Report, TableConfig, Transition, ViewConfig, ViewInfo},
};
use super::{record::RecordView, Layout, View};
pub struct InteractiveView<'a> {
input: Value,
command: String,
table: Option<RecordView<'a>>,
view_mode: bool,
// todo: impl Debug for it
table_cfg: TableConfig,
}
impl<'a> InteractiveView<'a> {
pub fn new(input: Value, table_cfg: TableConfig) -> Self {
Self {
input,
table_cfg,
table: None,
view_mode: false,
command: String::new(),
}
}
pub fn init(&mut self, command: String) {
self.command = command;
}
}
impl View for InteractiveView<'_> {
fn draw(&mut self, f: &mut Frame, area: Rect, cfg: &ViewConfig, layout: &mut Layout) {
let cmd_block = tui::widgets::Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Plain);
let cmd_area = Rect::new(area.x + 1, area.y, area.width - 2, 3);
let cmd_block = if self.view_mode {
cmd_block
} else {
cmd_block
.border_style(Style::default().add_modifier(Modifier::BOLD))
.border_type(BorderType::Double)
};
f.render_widget(cmd_block, cmd_area);
let cmd_input_area = Rect::new(
cmd_area.x + 2,
cmd_area.y + 1,
cmd_area.width - 2 - 2 - 1,
1,
);
let mut input = self.command.as_str();
let max_cmd_len = min(input.len() as u16, cmd_input_area.width);
if input.len() as u16 > max_cmd_len {
// in such case we take last max_cmd_len chars
let take_bytes = input
.chars()
.rev()
.take(max_cmd_len as usize)
.map(|c| c.len_utf8())
.sum::<usize>();
let skip = input.len() - take_bytes;
input = &input[skip..];
}
let cmd_input = Paragraph::new(input);
f.render_widget(cmd_input, cmd_input_area);
if !self.view_mode {
let cur_w = area.x + 1 + 1 + 1 + max_cmd_len as u16;
let cur_w_max = area.x + 1 + 1 + 1 + area.width - 2 - 1 - 1 - 1 - 1;
if cur_w < cur_w_max {
f.set_cursor(area.x + 1 + 1 + 1 + max_cmd_len as u16, area.y + 1);
}
}
let table_block = tui::widgets::Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Plain);
let table_area = Rect::new(area.x + 1, area.y + 3, area.width - 2, area.height - 3);
let table_block = if self.view_mode {
table_block
.border_style(Style::default().add_modifier(Modifier::BOLD))
.border_type(BorderType::Double)
} else {
table_block
};
f.render_widget(table_block, table_area);
if let Some(table) = &mut self.table {
let area = Rect::new(
area.x + 2,
area.y + 4,
area.width - 3 - 1,
area.height - 3 - 1 - 1,
);
table.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> {
if self.view_mode {
let table = self
.table
.as_mut()
.expect("we know that we have a table cause of a flag");
let was_at_the_top = table.get_layer_last().index_row == 0 && table.cursor.y == 0;
if was_at_the_top && matches!(key.code, KeyCode::Up | KeyCode::PageUp) {
self.view_mode = false;
return Some(Transition::Ok);
}
let result = table.handle_input(engine_state, stack, layout, info, key);
return match result {
Some(Transition::Ok | Transition::Cmd { .. }) => Some(Transition::Ok),
Some(Transition::Exit) => {
self.view_mode = false;
Some(Transition::Ok)
}
None => None,
};
}
match &key.code {
KeyCode::Esc => Some(Transition::Exit),
KeyCode::Backspace => {
if !self.command.is_empty() {
self.command.pop();
}
Some(Transition::Ok)
}
KeyCode::Char(c) => {
self.command.push(*c);
Some(Transition::Ok)
}
KeyCode::Down => {
if self.table.is_some() {
self.view_mode = true;
}
Some(Transition::Ok)
}
KeyCode::Enter => {
let pipeline = PipelineData::Value(self.input.clone(), None);
let pipeline = run_nu_command(engine_state, stack, &self.command, pipeline);
match pipeline {
Ok(pipeline_data) => {
let (columns, values) = collect_pipeline(pipeline_data);
let view = RecordView::new(columns, values, self.table_cfg);
self.table = Some(view);
// in case there was a error before wanna reset it.
info.report = Some(Report::default());
}
Err(err) => {
info.report = Some(Report::error(format!("Error: {}", err)));
}
}
Some(Transition::Ok)
}
_ => None,
}
}
fn exit(&mut self) -> Option<Value> {
self.table.as_mut().and_then(|v| v.exit())
}
fn collect_data(&self) -> Vec<crate::nu_common::NuText> {
self.table
.as_ref()
.map_or_else(Vec::new, |v| v.collect_data())
}
fn show_data(&mut self, i: usize) -> bool {
self.table.as_mut().map_or(false, |v| v.show_data(i))
}
}

View File

@ -0,0 +1,104 @@
mod coloredtextw;
mod information;
mod interative;
mod preview;
mod record;
use crossterm::event::KeyEvent;
use nu_protocol::{
engine::{EngineState, Stack},
Value,
};
use tui::layout::Rect;
use super::{
nu_common::NuText,
pager::{Frame, Transition, ViewConfig, ViewInfo},
};
pub use information::InformationView;
pub use interative::InteractiveView;
pub use preview::Preview;
pub use record::{RecordView, RecordViewState};
#[derive(Debug, Default)]
pub struct Layout {
pub data: Vec<ElementInfo>,
}
impl Layout {
fn push(&mut self, text: &str, x: u16, y: u16, width: u16, height: u16) {
self.data.push(ElementInfo::new(text, x, y, width, height));
}
}
#[derive(Debug, Default, Clone)]
pub struct ElementInfo {
// todo: make it a Cow
pub text: String,
pub area: Rect,
}
impl ElementInfo {
pub fn new(text: impl Into<String>, x: u16, y: u16, width: u16, height: u16) -> Self {
Self {
text: text.into(),
area: Rect::new(x, y, width, height),
}
}
}
pub trait View {
fn draw(&mut self, f: &mut Frame, area: Rect, cfg: &ViewConfig, layout: &mut Layout);
fn handle_input(
&mut self,
engine_state: &EngineState,
stack: &mut Stack,
layout: &Layout,
info: &mut ViewInfo,
key: KeyEvent,
) -> Option<Transition>;
fn show_data(&mut self, _: usize) -> bool {
false
}
fn collect_data(&self) -> Vec<NuText> {
Vec::new()
}
fn exit(&mut self) -> Option<Value> {
None
}
}
impl View for Box<dyn View> {
fn draw(&mut self, f: &mut Frame, area: Rect, cfg: &ViewConfig, layout: &mut Layout) {
self.as_mut().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> {
self.as_mut()
.handle_input(engine_state, stack, layout, info, key)
}
fn collect_data(&self) -> Vec<NuText> {
self.as_ref().collect_data()
}
fn exit(&mut self) -> Option<Value> {
self.as_mut().exit()
}
fn show_data(&mut self, i: usize) -> bool {
self.as_mut().show_data(i)
}
}

View File

@ -0,0 +1,175 @@
use std::cmp::{max, min};
use crossterm::event::{KeyCode, KeyEvent};
use nu_protocol::{
engine::{EngineState, Stack},
Value,
};
use nu_table::TextStyle;
use tui::layout::Rect;
use crate::{
nu_common::{NuSpan, NuText},
pager::{Frame, Report, Severentity, Transition, ViewConfig, ViewInfo},
};
use super::{coloredtextw::ColoredTextW, Layout, View};
// todo: Add wrap option
#[derive(Debug)]
pub struct Preview {
lines: Vec<String>,
i_row: usize,
i_col: usize,
screen_size: u16,
}
impl Preview {
pub fn new(value: &str) -> Self {
let lines: Vec<String> = value
.lines()
.map(|line| line.replace('\t', " ")) // tui: doesn't support TAB
.collect();
Self {
lines,
i_col: 0,
i_row: 0,
screen_size: 0,
}
}
}
impl View for Preview {
fn draw(&mut self, f: &mut Frame, area: Rect, _: &ViewConfig, layout: &mut Layout) {
if self.i_row >= self.lines.len() {
f.render_widget(tui::widgets::Clear, area);
return;
}
let lines = &self.lines[self.i_row..];
for (i, line) in lines.iter().enumerate().take(area.height as usize) {
let text = ColoredTextW::new(line, self.i_col);
let area = Rect::new(area.x, area.y + i as u16, area.width, 1);
let s = text.what(area);
layout.push(&s, area.x, area.y, area.width, area.height);
f.render_widget(text, area)
}
self.screen_size = area.width;
}
fn handle_input(
&mut self,
_: &EngineState,
_: &mut Stack,
layout: &Layout,
info: &mut ViewInfo, // add this arg to draw too?
key: KeyEvent,
) -> Option<Transition> {
match key.code {
KeyCode::Left => {
if self.i_col > 0 {
self.i_col -= max(1, self.screen_size as usize / 2);
}
Some(Transition::Ok)
}
KeyCode::Right => {
self.i_col += max(1, self.screen_size as usize / 2);
Some(Transition::Ok)
}
KeyCode::Up => {
let page_size = layout.data.len();
let max = self.lines.len().saturating_sub(page_size);
let was_end = self.i_row == max;
if max != 0 && was_end {
info.status = Some(Report::default());
}
self.i_row = self.i_row.saturating_sub(1);
Some(Transition::Ok)
}
KeyCode::Down => {
let page_size = layout.data.len();
let max = self.lines.len().saturating_sub(page_size);
self.i_row = min(self.i_row + 1, max);
let is_end = self.i_row == max;
if is_end {
let report = Report::new(
String::from("END"),
Severentity::Info,
String::new(),
String::new(),
);
info.status = Some(report);
}
Some(Transition::Ok)
}
KeyCode::PageUp => {
let page_size = layout.data.len();
let max = self.lines.len().saturating_sub(page_size);
let was_end = self.i_row == max;
if max != 0 && was_end {
info.status = Some(Report::default());
}
self.i_row = self.i_row.saturating_sub(page_size);
Some(Transition::Ok)
}
KeyCode::PageDown => {
let page_size = layout.data.len();
let max = self.lines.len().saturating_sub(page_size);
self.i_row = min(self.i_row + page_size, max);
let is_end = self.i_row == max;
if is_end {
let report = Report::new(
String::from("END"),
Severentity::Info,
String::new(),
String::new(),
);
info.status = Some(report);
}
Some(Transition::Ok)
}
KeyCode::Esc => Some(Transition::Exit),
_ => None,
}
}
fn collect_data(&self) -> Vec<NuText> {
self.lines
.iter()
.map(|line| (line.to_owned(), TextStyle::default()))
.collect::<Vec<_>>()
}
fn show_data(&mut self, row: usize) -> bool {
// we can only go to the appropriate line, but we can't target column
//
// todo: improve somehow?
self.i_row = row;
true
}
fn exit(&mut self) -> Option<Value> {
let text = self.lines.join("\n");
Some(Value::string(text, NuSpan::unknown()))
}
}

View File

@ -0,0 +1,661 @@
mod tablew;
use std::{borrow::Cow, cmp::min, collections::HashMap};
use crossterm::event::{KeyCode, KeyEvent};
use nu_protocol::{
engine::{EngineState, Stack},
Value,
};
use tui::{layout::Rect, widgets::Block};
use crate::{
nu_common::{collect_input, NuConfig, NuSpan, NuStyleTable, NuText},
pager::{
make_styled_string, nu_style_to_tui, Frame, Position, Report, Severentity, StyleConfig,
TableConfig, Transition, ViewConfig, ViewInfo,
},
views::ElementInfo,
};
use self::tablew::{TableW, TableWState};
use super::{Layout, View};
#[derive(Debug, Clone)]
pub struct RecordView<'a> {
layer_stack: Vec<RecordLayer<'a>>,
mode: UIMode,
cfg: TableConfig,
pub(crate) cursor: Position,
state: RecordViewState,
}
impl<'a> RecordView<'a> {
pub fn new(
columns: impl Into<Cow<'a, [String]>>,
records: impl Into<Cow<'a, [Vec<Value>]>>,
table_cfg: TableConfig,
) -> Self {
Self {
layer_stack: vec![RecordLayer::new(columns, records)],
mode: UIMode::View,
cursor: Position::new(0, 0),
cfg: table_cfg,
state: RecordViewState::default(),
}
}
pub fn reverse(&mut self, width: u16, height: u16) {
let page_size = estimate_page_size(Rect::new(0, 0, width, height), self.cfg.show_head);
state_reverse_data(self, page_size as usize);
}
// todo: rename to get_layer
pub fn get_layer_last(&self) -> &RecordLayer<'a> {
self.layer_stack
.last()
.expect("we guarantee that 1 entry is always in a list")
}
pub fn get_layer_last_mut(&mut self) -> &mut RecordLayer<'a> {
self.layer_stack
.last_mut()
.expect("we guarantee that 1 entry is always in a list")
}
fn create_tablew<'b>(&self, layer: &'b RecordLayer, view_cfg: &'b ViewConfig) -> TableW<'b> {
let data = convert_records_to_string(&layer.records, view_cfg.config, view_cfg.color_hm);
let style = tablew::TableStyle {
show_index: self.cfg.show_index,
show_header: self.cfg.show_head,
splitline_style: view_cfg.theme.split_line,
header_bottom: view_cfg.theme.split_lines.header_bottom,
header_top: view_cfg.theme.split_lines.header_top,
index_line: view_cfg.theme.split_lines.index_line,
shift_line: view_cfg.theme.split_lines.shift_line,
};
let headers = layer.columns.as_ref();
let color_hm = view_cfg.color_hm;
let i_row = layer.index_row;
let i_column = layer.index_column;
TableW::new(headers, data, color_hm, i_row, i_column, style)
}
}
impl View for RecordView<'_> {
fn draw(&mut self, f: &mut Frame, area: Rect, cfg: &ViewConfig, layout: &mut Layout) {
let layer = self.get_layer_last();
let table = self.create_tablew(layer, cfg);
let mut table_layout = TableWState::default();
f.render_stateful_widget(table, area, &mut table_layout);
*layout = table_layout.layout;
self.state = RecordViewState {
count_rows: table_layout.count_rows,
count_columns: table_layout.count_columns,
data_index: table_layout.data_index,
};
if self.mode == UIMode::Cursor {
let cursor = get_cursor(self);
highlight_cell(f, area, &self.state, cursor, cfg.theme);
}
}
fn handle_input(
&mut self,
_: &EngineState,
_: &mut Stack,
_: &Layout,
info: &mut ViewInfo,
key: KeyEvent,
) -> Option<Transition> {
let result = match self.mode {
UIMode::View => handle_key_event_view_mode(self, &key),
UIMode::Cursor => {
// we handle a situation where we got resized and the old cursor is no longer valid
self.cursor = get_cursor(self);
handle_key_event_cursor_mode(self, &key)
}
};
if matches!(&result, Some(Transition::Ok) | Some(Transition::Cmd { .. })) {
// update status bar
let report =
create_records_report(self.get_layer_last(), &self.state, self.mode, self.cursor);
info.status = Some(report);
}
result
}
fn collect_data(&self) -> Vec<NuText> {
let data = convert_records_to_string(
&self.get_layer_last().records,
&NuConfig::default(),
&HashMap::default(),
);
data.iter().flatten().cloned().collect()
}
fn show_data(&mut self, pos: usize) -> bool {
let data = &self.get_layer_last().records;
let mut i = 0;
for (row, cells) in data.iter().enumerate() {
if pos > i + cells.len() {
i += cells.len();
continue;
}
for (column, _) in cells.iter().enumerate() {
if i == pos {
let layer = self.get_layer_last_mut();
layer.index_column = column;
layer.index_row = row;
return true;
}
i += 1;
}
}
false
}
fn exit(&mut self) -> Option<Value> {
Some(build_last_value(self))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum UIMode {
Cursor,
View,
}
#[derive(Debug, Clone)]
pub struct RecordLayer<'a> {
columns: Cow<'a, [String]>,
records: Cow<'a, [Vec<Value>]>,
pub(crate) index_row: usize,
pub(crate) index_column: usize,
name: Option<String>,
was_transposed: bool,
}
impl<'a> RecordLayer<'a> {
fn new(
columns: impl Into<Cow<'a, [String]>>,
records: impl Into<Cow<'a, [Vec<Value>]>>,
) -> Self {
Self {
columns: columns.into(),
records: records.into(),
index_row: 0,
index_column: 0,
name: None,
was_transposed: false,
}
}
fn set_name(&mut self, name: impl Into<String>) {
self.name = Some(name.into());
}
fn count_rows(&self) -> usize {
self.records.len()
}
fn count_columns(&self) -> usize {
self.columns.len()
}
fn get_current_value(&self, Position { x, y }: Position) -> Value {
let current_row = y as usize + self.index_row;
let current_column = x as usize + self.index_column;
let row = self.records[current_row].clone();
row[current_column].clone()
}
fn get_current_header(&self, Position { x, .. }: Position) -> Option<String> {
let col = x as usize + self.index_column;
self.columns.get(col).map(|header| header.to_string())
}
}
#[derive(Debug, Default, Clone)]
pub struct RecordViewState {
count_rows: usize,
count_columns: usize,
data_index: HashMap<(usize, usize), ElementInfo>,
}
fn handle_key_event_view_mode(view: &mut RecordView, key: &KeyEvent) -> Option<Transition> {
match key.code {
KeyCode::Esc => {
if view.layer_stack.len() > 1 {
view.layer_stack.pop();
Some(Transition::Ok)
} else {
Some(Transition::Exit)
}
}
KeyCode::Char('i') => {
view.mode = UIMode::Cursor;
view.cursor = Position::default();
Some(Transition::Ok)
}
KeyCode::Char('t') => {
let layer = view.get_layer_last_mut();
layer.index_column = 0;
layer.index_row = 0;
transpose_table(layer);
Some(Transition::Ok)
}
KeyCode::Up => {
let layer = view.get_layer_last_mut();
layer.index_row = layer.index_row.saturating_sub(1);
Some(Transition::Ok)
}
KeyCode::Down => {
let layer = view.get_layer_last_mut();
let max_index = layer.count_rows().saturating_sub(1);
layer.index_row = min(layer.index_row + 1, max_index);
Some(Transition::Ok)
}
KeyCode::Left => {
let layer = view.get_layer_last_mut();
layer.index_column = layer.index_column.saturating_sub(1);
Some(Transition::Ok)
}
KeyCode::Right => {
let layer = view.get_layer_last_mut();
let max_index = layer.count_columns().saturating_sub(1);
layer.index_column = min(layer.index_column + 1, max_index);
Some(Transition::Ok)
}
KeyCode::PageUp => {
let count_rows = view.state.count_rows;
let layer = view.get_layer_last_mut();
layer.index_row = layer.index_row.saturating_sub(count_rows as usize);
Some(Transition::Ok)
}
KeyCode::PageDown => {
let count_rows = view.state.count_rows;
let layer = view.get_layer_last_mut();
let max_index = layer.count_rows().saturating_sub(1);
layer.index_row = min(layer.index_row + count_rows as usize, max_index);
Some(Transition::Ok)
}
_ => None,
}
}
fn handle_key_event_cursor_mode(view: &mut RecordView, key: &KeyEvent) -> Option<Transition> {
match key.code {
KeyCode::Esc => {
view.mode = UIMode::View;
view.cursor = Position::default();
Some(Transition::Ok)
}
KeyCode::Up => {
if view.cursor.y == 0 {
let layer = view.get_layer_last_mut();
layer.index_row = layer.index_row.saturating_sub(1);
} else {
view.cursor.y -= 1
}
Some(Transition::Ok)
}
KeyCode::Down => {
let cursor = view.cursor;
let showed_rows = view.state.count_rows;
let layer = view.get_layer_last_mut();
let total_rows = layer.count_rows();
let row_index = layer.index_row + cursor.y as usize + 1;
if row_index < total_rows {
if cursor.y as usize + 1 == showed_rows {
layer.index_row += 1;
} else {
view.cursor.y += 1;
}
}
Some(Transition::Ok)
}
KeyCode::Left => {
let cursor = view.cursor;
let layer = view.get_layer_last_mut();
if cursor.x == 0 {
layer.index_column = layer.index_column.saturating_sub(1);
} else {
view.cursor.x -= 1
}
Some(Transition::Ok)
}
KeyCode::Right => {
let cursor = view.cursor;
let showed_columns = view.state.count_columns;
let layer = view.get_layer_last_mut();
let total_columns = layer.count_columns();
let column_index = layer.index_column + cursor.x as usize + 1;
if column_index < total_columns {
if cursor.x as usize + 1 == showed_columns {
layer.index_column += 1;
} else {
view.cursor.x += 1;
}
}
Some(Transition::Ok)
}
KeyCode::Enter => {
let next_layer = get_peeked_layer(view);
push_layer(view, next_layer);
Some(Transition::Ok)
}
_ => None,
}
}
fn get_peeked_layer(view: &RecordView) -> RecordLayer<'static> {
let layer = view.get_layer_last();
let value = layer.get_current_value(view.cursor);
let (columns, values) = collect_input(value);
RecordLayer::new(columns, values)
}
fn push_layer(view: &mut RecordView<'_>, mut next_layer: RecordLayer<'static>) {
let layer = view.get_layer_last();
let header = layer.get_current_header(view.cursor);
if let Some(header) = header {
next_layer.set_name(header);
}
view.layer_stack.push(next_layer);
view.mode = UIMode::View;
view.cursor = Position::default();
}
fn estimate_page_size(area: Rect, show_head: bool) -> u16 {
let mut available_height = area.height;
available_height -= 3; // status_bar
if show_head {
available_height -= 3; // head
}
available_height
}
fn state_reverse_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 as usize {
layer.index_row = count_rows - page_size as usize;
}
}
fn convert_records_to_string(
records: &[Vec<Value>],
cfg: &NuConfig,
color_hm: &NuStyleTable,
) -> Vec<Vec<NuText>> {
records
.iter()
.map(|row| {
row.iter()
.map(|value| {
let text = value.clone().into_abbreviated_string(cfg);
let tp = value.get_type().to_string();
let float_precision = cfg.float_precision as usize;
make_styled_string(text, &tp, 0, false, color_hm, float_precision)
})
.collect::<Vec<_>>()
})
.collect::<Vec<_>>()
}
fn highlight_cell(
f: &mut Frame,
area: Rect,
state: &RecordViewState,
cursor: Position,
theme: &StyleConfig,
) {
let Position { x: column, y: row } = cursor;
let info = state.data_index.get(&(row as usize, column as usize));
if let Some(info) = info {
if let Some(style) = theme.selected_column {
let hightlight_block = Block::default().style(nu_style_to_tui(style));
let area = Rect::new(info.area.x, area.y, info.area.width, area.height);
f.render_widget(hightlight_block.clone(), area);
}
if let Some(style) = theme.selected_row {
let hightlight_block = Block::default().style(nu_style_to_tui(style));
let area = Rect::new(area.x, info.area.y, area.width, 1);
f.render_widget(hightlight_block.clone(), area);
}
if let Some(style) = theme.selected_cell {
let hightlight_block = Block::default().style(nu_style_to_tui(style));
let area = Rect::new(info.area.x, info.area.y, info.area.width, 1);
f.render_widget(hightlight_block.clone(), area);
}
if theme.show_cursow {
f.set_cursor(info.area.x, info.area.y);
}
}
}
fn get_cursor(v: &RecordView<'_>) -> Position {
let count_rows = v.state.count_rows as u16;
let count_columns = v.state.count_columns as u16;
let mut cursor = v.cursor;
cursor.y = min(cursor.y, count_rows.saturating_sub(1) as u16);
cursor.x = min(cursor.x, count_columns.saturating_sub(1) as u16);
cursor
}
fn build_last_value(v: &RecordView) -> Value {
if v.mode == UIMode::Cursor {
peak_current_value(v)
} else if v.get_layer_last().count_rows() < 2 {
build_table_as_record(v)
} else {
build_table_as_list(v)
}
}
fn peak_current_value(v: &RecordView) -> Value {
let layer = v.get_layer_last();
let Position { x: column, y: row } = v.cursor;
let row = row as usize + layer.index_row;
let column = column as usize + layer.index_column;
let value = &layer.records[row][column];
value.clone()
}
fn build_table_as_list(v: &RecordView) -> Value {
let layer = v.get_layer_last();
let headers = layer.columns.to_vec();
let vals = layer
.records
.iter()
.cloned()
.map(|vals| Value::Record {
cols: headers.clone(),
vals,
span: NuSpan::unknown(),
})
.collect();
Value::List {
vals,
span: NuSpan::unknown(),
}
}
fn build_table_as_record(v: &RecordView) -> Value {
let layer = v.get_layer_last();
let cols = layer.columns.to_vec();
let vals = layer.records.get(0).map_or(Vec::new(), |row| row.clone());
Value::Record {
cols,
vals,
span: NuSpan::unknown(),
}
}
fn create_records_report(
layer: &RecordLayer,
state: &RecordViewState,
mode: UIMode,
cursor: Position,
) -> Report {
let seen_rows = layer.index_row + state.count_rows;
let seen_rows = min(seen_rows, layer.count_rows());
let percent_rows = get_percentage(seen_rows, layer.count_rows());
let covered_percent = match percent_rows {
100 => String::from("All"),
_ if layer.index_row == 0 => String::from("Top"),
value => format!("{}%", value),
};
let title = if let Some(name) = &layer.name {
name.clone()
} else {
String::new()
};
let cursor = {
if mode == UIMode::Cursor {
let row = layer.index_row + cursor.y as usize;
let column = layer.index_column + cursor.x as usize;
format!("{},{}", row, column)
} else {
format!("{},{}", layer.index_row, layer.index_column)
}
};
Report {
message: title,
context: covered_percent,
context2: cursor,
level: Severentity::Info,
}
}
fn get_percentage(value: usize, max: usize) -> usize {
debug_assert!(value <= max, "{:?} {:?}", value, max);
((value as f32 / max as f32) * 100.0).floor() as usize
}
fn transpose_table(layer: &mut RecordLayer<'_>) {
let count_rows = layer.count_rows();
let count_columns = layer.count_columns();
if layer.was_transposed {
let data = match &mut layer.records {
Cow::Owned(data) => data,
Cow::Borrowed(_) => unreachable!("must never happen"),
};
let headers = pop_first_column(data);
let headers = headers
.into_iter()
.map(|value| match value {
Value::String { val, .. } => val,
_ => unreachable!("must never happen"),
})
.collect();
let data = _transpose_table(data, count_rows, count_columns - 1);
layer.records = Cow::Owned(data);
layer.columns = Cow::Owned(headers);
} else {
let mut data = _transpose_table(&layer.records, count_rows, count_columns);
for (column, column_name) in layer.columns.iter().enumerate() {
let value = Value::String {
val: column_name.to_string(),
span: NuSpan::unknown(),
};
data[column].insert(0, value);
}
layer.records = Cow::Owned(data);
layer.columns = (1..count_rows + 1 + 1).map(|i| i.to_string()).collect();
}
layer.was_transposed = !layer.was_transposed;
}
fn pop_first_column(values: &mut [Vec<Value>]) -> Vec<Value> {
let mut data = vec![Value::default(); values.len()];
for (row, values) in values.iter_mut().enumerate() {
data[row] = values.remove(0);
}
data
}
fn _transpose_table(
values: &[Vec<Value>],
count_rows: usize,
count_columns: usize,
) -> Vec<Vec<Value>> {
let mut data = vec![vec![Value::default(); count_rows]; count_columns];
for (row, values) in values.iter().enumerate() {
for (column, value) in values.iter().enumerate() {
data[column][row] = value.to_owned();
}
}
data
}

View File

@ -0,0 +1,574 @@
use std::{borrow::Cow, cmp::max, collections::HashMap};
use nu_table::{string_width, Alignment, TextStyle};
use tui::{
buffer::Buffer,
layout::Rect,
text::Span,
widgets::{Block, Borders, Paragraph, StatefulWidget, Widget},
};
use crate::{
nu_common::{NuStyle, NuStyleTable, NuText},
pager::{nu_style_to_tui, text_style_to_tui_style},
views::ElementInfo,
};
use super::Layout;
pub struct TableW<'a> {
columns: Cow<'a, [String]>,
data: Cow<'a, [Vec<NuText>]>,
index_row: usize,
index_column: usize,
style: TableStyle,
color_hm: &'a NuStyleTable,
}
pub struct TableStyle {
pub show_index: bool,
pub show_header: bool,
pub splitline_style: NuStyle,
pub header_top: bool,
pub header_bottom: bool,
pub shift_line: bool,
pub index_line: bool,
}
impl<'a> TableW<'a> {
#[allow(clippy::too_many_arguments)]
pub fn new(
columns: impl Into<Cow<'a, [String]>>,
data: impl Into<Cow<'a, [Vec<NuText>]>>,
color_hm: &'a NuStyleTable,
index_row: usize,
index_column: usize,
style: TableStyle,
) -> Self {
Self {
columns: columns.into(),
data: data.into(),
color_hm,
index_row,
index_column,
style,
}
}
}
#[derive(Debug, Default)]
pub struct TableWState {
pub layout: Layout,
pub count_rows: usize,
pub count_columns: usize,
pub data_index: HashMap<(usize, usize), ElementInfo>,
}
impl StatefulWidget for TableW<'_> {
type State = TableWState;
fn render(
self,
area: tui::layout::Rect,
buf: &mut tui::buffer::Buffer,
state: &mut Self::State,
) {
const CELL_PADDING_LEFT: u16 = 2;
const CELL_PADDING_RIGHT: u16 = 2;
let show_index = self.style.show_index;
let show_head = self.style.show_header;
let splitline_s = self.style.splitline_style;
let mut data_y = area.y;
let mut data_height = area.height;
let mut head_y = area.y;
if show_head {
data_y += 1;
data_height -= 1;
if self.style.header_top {
data_y += 1;
data_height -= 1;
head_y += 1
}
if self.style.header_bottom {
data_y += 1;
data_height -= 1;
}
}
if area.width == 0 || area.height == 0 {
return;
}
let mut width = area.x;
let mut data = &self.data[self.index_row..];
if data.len() > data_height as usize {
data = &data[..data_height as usize];
}
// header lines
if show_head {
// fixme: color from config
let top = self.style.header_top;
let bottom = self.style.header_bottom;
if top || bottom {
render_header_borders(buf, area, 0, 1, splitline_s, top, bottom);
}
}
if show_index {
let area = Rect::new(width, data_y, area.width, data_height);
width += render_index(buf, area, self.color_hm, self.index_row);
if self.style.index_line {
let show_head = show_head && self.style.header_bottom;
width += render_vertical(buf, width, data_y, data_height, show_head, splitline_s);
}
}
let mut do_render_split_line = true;
let mut do_render_shift_column = false;
state.count_rows = data.len();
state.count_columns = 0;
for (i, col) in (self.index_column..self.columns.len()).enumerate() {
let mut head = String::from(&self.columns[col]);
let mut column = create_column(data, col);
let column_width = calculate_column_width(&column);
let mut use_space = column_width as u16;
if show_head {
let head_width = string_width(&head);
use_space = max(head_width as u16, use_space);
}
{
let available_space = area.width - width;
let head = show_head.then_some(&mut head);
let control = truncate_column(
&mut column,
head,
available_space,
col + 1 == self.columns.len(),
PrintControl {
break_everything: false,
print_shift_column: false,
print_split_line: true,
width: use_space,
},
);
use_space = control.width;
do_render_split_line = control.print_split_line;
do_render_shift_column = control.print_shift_column;
if control.break_everything {
break;
}
}
if show_head {
let header = &[head_row_text(&head, self.color_hm)];
let mut w = width;
w += render_space(buf, w, head_y, 1, CELL_PADDING_LEFT);
w += render_column(buf, w, head_y, use_space, header);
render_space(buf, w, head_y, 1, CELL_PADDING_RIGHT);
let x = w - CELL_PADDING_RIGHT - use_space;
state.layout.push(&header[0].0, x, head_y, use_space, 1);
// it would be nice to add it so it would be available on search
// state.state.data_index.insert((i, col), ElementInfo::new(text, x, data_y, use_space, 1));
}
width += render_space(buf, width, data_y, data_height, CELL_PADDING_LEFT);
width += render_column(buf, width, data_y, use_space, &column);
width += render_space(buf, width, data_y, data_height, CELL_PADDING_RIGHT);
for (row, (text, _)) in column.iter().enumerate() {
let x = width - CELL_PADDING_RIGHT - use_space;
let y = data_y + row as u16;
state.layout.push(text, x, y, use_space, 1);
let e = ElementInfo::new(text, x, y, use_space, 1);
state.data_index.insert((row, i), e);
}
state.count_columns += 1;
if do_render_shift_column {
break;
}
}
if do_render_shift_column {
// we actually want to show a shift only in header.
//
// render_shift_column(buf, used_width, head_offset, available_height);
if show_head {
width += render_space(buf, width, data_y, data_height, CELL_PADDING_LEFT);
width += render_shift_column(buf, width, head_y, 1, splitline_s);
width += render_space(buf, width, data_y, data_height, CELL_PADDING_RIGHT);
}
}
if do_render_split_line && self.style.shift_line {
let show_head = show_head && self.style.header_bottom;
width += render_vertical(buf, width, data_y, data_height, show_head, splitline_s);
}
// we try out best to cleanup the rest of the space cause it could be meassed.
let rest = area.width.saturating_sub(width);
if rest > 0 {
render_space(buf, width, data_y, data_height, rest);
if show_head {
render_space(buf, width, head_y, 1, rest);
}
}
}
}
struct IndexColumn<'a> {
color_hm: &'a NuStyleTable,
start: usize,
}
impl<'a> IndexColumn<'a> {
fn new(color_hm: &'a NuStyleTable, start: usize) -> Self {
Self { color_hm, start }
}
fn estimate_width(&self, height: u16) -> usize {
let last_row = self.start + height as usize;
last_row.to_string().len()
}
}
impl Widget for IndexColumn<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
let style = nu_style_to_tui(self.color_hm["row_index"]);
for row in 0..area.height {
let i = 1 + row as usize + self.start;
let text = i.to_string();
let p = Paragraph::new(text)
.style(style)
.alignment(tui::layout::Alignment::Right);
let area = Rect::new(area.x, area.y + row, area.width, 1);
p.render(area, buf);
}
}
}
fn render_header_borders(
buf: &mut Buffer,
area: Rect,
y: u16,
span: u16,
style: NuStyle,
top: bool,
bottom: bool,
) -> (u16, u16) {
let mut i = 0;
let mut borders = Borders::NONE;
if top {
borders |= Borders::TOP;
i += 1;
}
if bottom {
borders |= Borders::BOTTOM;
i += 1;
}
let block = Block::default()
.borders(borders)
.border_style(nu_style_to_tui(style));
let height = i + span;
let area = Rect::new(area.x, area.y + y, area.width, height);
block.render(area, buf);
// y pos of header text and next line
(height.saturating_sub(2), height)
}
fn render_index(buf: &mut Buffer, area: Rect, color_hm: &NuStyleTable, start_index: usize) -> u16 {
const PADDING_LEFT: u16 = 2;
const PADDING_RIGHT: u16 = 1;
let mut width = render_space(buf, area.x, area.y, area.height, PADDING_LEFT);
let index = IndexColumn::new(color_hm, start_index);
let w = index.estimate_width(area.height) as u16;
let area = Rect::new(area.x + width, area.y, w, area.height);
index.render(area, buf);
width += w;
width += render_space(buf, area.x + width, area.y, area.height, PADDING_RIGHT);
width
}
fn render_vertical(
buf: &mut Buffer,
x: u16,
y: u16,
height: u16,
show_header: bool,
style: NuStyle,
) -> u16 {
render_vertical_split(buf, x, y, height, style);
if show_header && y > 0 {
render_top_connector(buf, x, y - 1, style);
}
// render_bottom_connector(buf, x, height + y);
1
}
fn render_vertical_split(buf: &mut Buffer, x: u16, y: u16, height: u16, style: NuStyle) {
let style = TextStyle {
alignment: Alignment::Left,
color_style: Some(style),
};
repeat_vertical(buf, x, y, 1, height, '│', style);
}
fn render_space(buf: &mut Buffer, x: u16, y: u16, height: u16, padding: u16) -> u16 {
repeat_vertical(buf, x, y, padding, height, ' ', TextStyle::default());
padding
}
fn create_column(data: &[Vec<NuText>], col: usize) -> Vec<NuText> {
let mut column = vec![NuText::default(); data.len()];
for (row, values) in data.iter().enumerate() {
if values.is_empty() {
debug_assert!(false, "must never happen?");
continue;
}
let value = &values[col];
column[row] = value.clone();
}
column
}
fn repeat_vertical(
buf: &mut tui::buffer::Buffer,
x_offset: u16,
y_offset: u16,
width: u16,
height: u16,
c: char,
style: TextStyle,
) {
let text = std::iter::repeat(c)
.take(width as usize)
.collect::<String>();
let style = text_style_to_tui_style(style);
let span = Span::styled(text, style);
for row in 0..height {
buf.set_span(x_offset, y_offset + row as u16, &span, width);
}
}
#[derive(Debug, Default, Copy, Clone)]
struct PrintControl {
width: u16,
break_everything: bool,
print_split_line: bool,
print_shift_column: bool,
}
fn truncate_column(
column: &mut [NuText],
head: Option<&mut String>,
available_space: u16,
is_column_last: bool,
mut control: PrintControl,
) -> PrintControl {
const CELL_PADDING_LEFT: u16 = 2;
const CELL_PADDING_RIGHT: u16 = 2;
const VERTICAL_LINE_WIDTH: u16 = 1;
const CELL_MIN_WIDTH: u16 = 1;
let min_space_cell = CELL_PADDING_LEFT + CELL_PADDING_RIGHT + CELL_MIN_WIDTH;
let min_space = min_space_cell + VERTICAL_LINE_WIDTH;
if available_space < min_space {
// if there's not enough space at all just return; doing our best
if available_space < VERTICAL_LINE_WIDTH {
control.print_split_line = false;
}
control.break_everything = true;
return control;
}
let column_taking_space =
control.width + CELL_PADDING_LEFT + CELL_PADDING_RIGHT + VERTICAL_LINE_WIDTH;
let is_enough_space = available_space > column_taking_space;
if !is_enough_space {
if is_column_last {
// we can do nothing about it we need to truncate.
// we assume that there's always at least space for padding and 1 symbol. (5 chars)
let width = available_space
.saturating_sub(CELL_PADDING_LEFT + CELL_PADDING_RIGHT + VERTICAL_LINE_WIDTH);
if width == 0 {
control.break_everything = true;
return control;
}
if let Some(head) = head {
truncate_str(head, width as usize);
}
truncate_list(column, width as usize);
control.width = width;
} else {
let min_space_2cells = min_space + min_space_cell;
if available_space > min_space_2cells {
let width = available_space.saturating_sub(min_space_2cells);
if width == 0 {
control.break_everything = true;
return control;
}
truncate_list(column, width as usize);
if let Some(head) = head {
truncate_str(head, width as usize);
}
control.width = width;
control.print_shift_column = true;
} else {
control.break_everything = true;
control.print_shift_column = true;
}
}
} else if !is_column_last {
// even though we can safely render current column,
// we need to check whether there's enough space for AT LEAST a shift column
// (2 padding + 2 padding + 1 a char)
let left_space = available_space - column_taking_space;
if left_space < min_space {
let need_space = min_space_cell - left_space;
let min_left_width = 1;
let is_column_big_enough = control.width > need_space + min_left_width;
if is_column_big_enough {
let width = control.width.saturating_sub(need_space);
if width == 0 {
control.break_everything = true;
return control;
}
truncate_list(column, width as usize);
if let Some(head) = head {
truncate_str(head, width as usize);
}
control.width = width;
control.print_shift_column = true;
}
}
}
control
}
fn truncate_list(list: &mut [NuText], width: usize) {
for (text, _) in list {
truncate_str(text, width);
}
}
fn truncate_str(text: &mut String, width: usize) {
if width == 0 {
text.clear();
} else {
*text = nu_table::string_truncate(text, width - 1);
text.push('…');
}
}
fn render_shift_column(buf: &mut Buffer, x: u16, y: u16, height: u16, style: NuStyle) -> u16 {
let style = TextStyle {
alignment: Alignment::Left,
color_style: Some(style),
};
repeat_vertical(buf, x, y, 1, height, '…', style);
1
}
fn render_top_connector(buf: &mut Buffer, x: u16, y: u16, style: NuStyle) {
let style = nu_style_to_tui(style);
let span = Span::styled("", style);
buf.set_span(x, y, &span, 1);
}
fn calculate_column_width(column: &[NuText]) -> usize {
column
.iter()
.map(|(text, _)| text)
.map(|text| string_width(text))
.max()
.unwrap_or(0)
}
fn render_column(
buf: &mut tui::buffer::Buffer,
x: u16,
y: u16,
available_width: u16,
rows: &[NuText],
) -> u16 {
for (row, (text, style)) in rows.iter().enumerate() {
let style = text_style_to_tui_style(*style);
let text = strip_string(text);
let span = Span::styled(text, style);
buf.set_span(x, y + row as u16, &span, available_width);
}
available_width
}
fn strip_string(text: &str) -> String {
strip_ansi_escapes::strip(text)
.ok()
.and_then(|s| String::from_utf8(s).ok())
.unwrap_or_else(|| text.to_owned())
}
fn head_row_text(head: &str, color_hm: &NuStyleTable) -> NuText {
(
String::from(head),
TextStyle {
alignment: Alignment::Center,
color_style: Some(color_hm["header"]),
},
)
}

View File

@ -87,6 +87,7 @@ pub struct Config {
pub show_banner: bool,
pub show_clickable_links_in_ls: bool,
pub render_right_prompt_on_last_line: bool,
pub explore_config: HashMap<String, Value>,
}
impl Default for Config {
@ -125,6 +126,7 @@ impl Default for Config {
show_banner: true,
show_clickable_links_in_ls: true,
render_right_prompt_on_last_line: false,
explore_config: HashMap::new(),
}
}
}
@ -183,6 +185,11 @@ pub enum TrimStrategy {
},
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ExploreConfig {
pub color_config: HashMap<String, Value>,
}
impl Value {
pub fn into_config(self) -> Result<Config, ShellError> {
let v = self.as_record();
@ -806,6 +813,13 @@ impl Value {
eprintln!("$env.config.filesize_format is not a string")
}
}
"explore_config" => {
if let Ok(map) = create_map(value, &config) {
config.explore_config = map;
} else {
eprintln!("$env.config.explore_config is not a map")
}
}
// End legacy options
x => {
eprintln!("$env.config.{} is an unknown config setting", x)

View File

@ -24,3 +24,32 @@ pub fn wrap_string(text: &str, width: usize) -> String {
.with(Width::wrap(width))
.to_string()
}
pub fn string_truncate(text: &str, width: usize) -> String {
// todo: change me...
match text.lines().next() {
Some(first_line) => tabled::builder::Builder::from_iter([[first_line]])
.build()
.with(tabled::Style::empty())
.with(tabled::Padding::zero())
.with(tabled::Width::truncate(width))
.to_string(),
None => String::new(),
}
}
pub fn string_wrap(text: &str, width: usize) -> String {
// todo: change me...
if text.is_empty() {
return String::new();
}
tabled::builder::Builder::from_iter([[text]])
.build()
.with(tabled::Style::empty())
.with(tabled::Padding::zero())
.with(tabled::Width::wrap(width))
.to_string()
}

View File

@ -292,6 +292,23 @@ let-env config = {
shell_integration: true # enables terminal markers and a workaround to arrow keys stop working issue
show_banner: true # true or false to enable or disable the banner
render_right_prompt_on_last_line: false # true or false to enable or disable right prompt to be rendered on last line of the prompt.
# A 'explore' utility config
explore_config: {
highlight: { bg: 'yellow', fg: 'black' }
status_bar: { bg: '#C4C9C6', fg: '#1D1F21' }
command_bar: { fg: '#C4C9C6' }
split_line: '#404040'
cursor: true
# selected_column: 'blue'
# selected_row: { fg: 'yellow', bg: '#C1C2A3' }
# selected_cell: { fg: 'white', bg: '#777777' }
# line_shift: false,
# line_index: false,
# line_head_top: false,
# line_head_bottom: false,
}
hooks: {
pre_prompt: [{
$nothing # replace with source code to run before the prompt is shown