mirror of
https://github.com/nushell/nushell.git
synced 2024-11-24 09:23:38 +01:00
[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:
parent
e92678ea2c
commit
718ee3d545
50
Cargo.lock
generated
50
Cargo.lock
generated
@ -89,6 +89,15 @@ dependencies = [
|
|||||||
"ansitok",
|
"ansitok",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ansi-str"
|
||||||
|
version = "0.7.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "21b1ed1c166829a0ccb5d79caa0f75cb4abd4adb2ce2c096755b7ad5ffdb0990"
|
||||||
|
dependencies = [
|
||||||
|
"ansitok",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ansitok"
|
name = "ansitok"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@ -443,6 +452,12 @@ dependencies = [
|
|||||||
"zip",
|
"zip",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cassowary"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cast"
|
name = "cast"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
@ -2597,6 +2612,7 @@ dependencies = [
|
|||||||
"nu-ansi-term",
|
"nu-ansi-term",
|
||||||
"nu-color-config",
|
"nu-color-config",
|
||||||
"nu-engine",
|
"nu-engine",
|
||||||
|
"nu-explore",
|
||||||
"nu-glob",
|
"nu-glob",
|
||||||
"nu-json",
|
"nu-json",
|
||||||
"nu-parser",
|
"nu-parser",
|
||||||
@ -2664,6 +2680,23 @@ dependencies = [
|
|||||||
"sysinfo",
|
"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]]
|
[[package]]
|
||||||
name = "nu-glob"
|
name = "nu-glob"
|
||||||
version = "0.72.1"
|
version = "0.72.1"
|
||||||
@ -3129,7 +3162,7 @@ version = "0.7.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1526bb6aa9f10ec339fb10360f22c57edf81d5678d0278e93bc12a47ffbe4b01"
|
checksum = "1526bb6aa9f10ec339fb10360f22c57edf81d5678d0278e93bc12a47ffbe4b01"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ansi-str",
|
"ansi-str 0.5.0",
|
||||||
"ansitok",
|
"ansitok",
|
||||||
"bytecount",
|
"bytecount",
|
||||||
"fnv",
|
"fnv",
|
||||||
@ -4831,7 +4864,7 @@ version = "0.10.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "56c3ee73732ffceaea7b8f6b719ce3bb17f253fa27461ffeaf568ebd0cdb4b85"
|
checksum = "56c3ee73732ffceaea7b8f6b719ce3bb17f253fa27461ffeaf568ebd0cdb4b85"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ansi-str",
|
"ansi-str 0.5.0",
|
||||||
"papergrid",
|
"papergrid",
|
||||||
"tabled_derive",
|
"tabled_derive",
|
||||||
"unicode-width",
|
"unicode-width",
|
||||||
@ -5128,6 +5161,19 @@ version = "0.2.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
|
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]]
|
[[package]]
|
||||||
name = "typed-arena"
|
name = "typed-arena"
|
||||||
version = "1.7.0"
|
version = "1.7.0"
|
||||||
|
@ -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_ansi_term::{Color, Style};
|
||||||
use nu_protocol::Config;
|
use nu_protocol::{Config, Value};
|
||||||
use nu_table::{Alignment, TextStyle};
|
use nu_table::{Alignment, TextStyle};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
pub fn lookup_ansi_color_style(s: &str) -> Style {
|
pub fn lookup_ansi_color_style(s: &str) -> Style {
|
||||||
if s.starts_with('#') {
|
if s.starts_with('#') {
|
||||||
match color_from_hex(s) {
|
color_from_hex(s)
|
||||||
Ok(c) => match c {
|
.ok()
|
||||||
Some(c) => c.normal(),
|
.and_then(|c| c.map(|c| c.normal()))
|
||||||
None => Style::default(),
|
.unwrap_or_default()
|
||||||
},
|
|
||||||
Err(_) => Style::default(),
|
|
||||||
}
|
|
||||||
} else if s.starts_with('{') {
|
} else if s.starts_with('{') {
|
||||||
color_string_to_nustyle(s.to_string())
|
color_string_to_nustyle(s.to_string())
|
||||||
} else {
|
} else {
|
||||||
match s {
|
lookup_style(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(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -226,6 +70,18 @@ pub fn get_color_config(config: &Config) -> HashMap<String, Style> {
|
|||||||
hm
|
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
|
// 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.
|
// in the hashmap. The hashmap actually contains the style to be applied.
|
||||||
pub fn style_primitive(primitive: &str, color_hm: &HashMap<String, Style>) -> TextStyle {
|
pub fn style_primitive(primitive: &str, color_hm: &HashMap<String, Style>) -> TextStyle {
|
||||||
|
@ -9,62 +9,17 @@ pub struct NuStyle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_nustyle(nu_style: NuStyle) -> Style {
|
pub fn parse_nustyle(nu_style: NuStyle) -> Style {
|
||||||
// get the nu_ansi_term::Color foreground color
|
let mut style = Style {
|
||||||
let fg_color = match nu_style.fg {
|
foreground: nu_style.fg.and_then(|fg| lookup_color_str(&fg)),
|
||||||
Some(fg) => color_from_hex(&fg).unwrap_or_default(),
|
background: nu_style.bg.and_then(|bg| lookup_color_str(&bg)),
|
||||||
_ => None,
|
..Default::default()
|
||||||
};
|
|
||||||
// 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(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// setup the attributes available in nu_ansi_term::Style
|
if let Some(attrs) = nu_style.attr {
|
||||||
let mut bold = false;
|
fill_modifiers(&attrs, &mut style)
|
||||||
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' => (),
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// here's where we build the nu_ansi_term::Style
|
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,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn color_string_to_nustyle(color_string: String) -> 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -23,6 +23,7 @@ nu-system = { path = "../nu-system", version = "0.72.1" }
|
|||||||
nu-table = { path = "../nu-table", 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-term-grid = { path = "../nu-term-grid", version = "0.72.1" }
|
||||||
nu-utils = { path = "../nu-utils", 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"
|
nu-ansi-term = "0.46.0"
|
||||||
num-format = { version = "0.4.3" }
|
num-format = { version = "0.4.3" }
|
||||||
|
|
||||||
|
@ -356,6 +356,7 @@ pub fn create_default_context() -> EngineState {
|
|||||||
bind_command! {
|
bind_command! {
|
||||||
Griddle,
|
Griddle,
|
||||||
Table,
|
Table,
|
||||||
|
Explore,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Conversions
|
// Conversions
|
||||||
|
213
crates/nu-command/src/viewers/explore.rs
Normal file
213
crates/nu-command/src/viewers/explore.rs
Normal 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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
|
mod explore;
|
||||||
mod griddle;
|
mod griddle;
|
||||||
mod icons;
|
mod icons;
|
||||||
mod table;
|
mod table;
|
||||||
|
|
||||||
|
pub use explore::Explore;
|
||||||
pub use griddle::Griddle;
|
pub use griddle::Griddle;
|
||||||
pub use table::Table;
|
pub use table::Table;
|
||||||
|
22
crates/nu-explore/.gitignore
vendored
Normal file
22
crates/nu-explore/.gitignore
vendored
Normal 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/*
|
22
crates/nu-explore/Cargo.toml
Normal file
22
crates/nu-explore/Cargo.toml
Normal 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
21
crates/nu-explore/LICENSE
Normal 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.
|
248
crates/nu-explore/src/command.rs
Normal file
248
crates/nu-explore/src/command.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
246
crates/nu-explore/src/commands/help.rs
Normal file
246
crates/nu-explore/src/commands/help.rs
Normal 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)
|
||||||
|
}
|
71
crates/nu-explore/src/commands/mod.rs
Normal file
71
crates/nu-explore/src/commands/mod.rs
Normal 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,
|
||||||
|
}
|
153
crates/nu-explore/src/commands/nu.rs
Normal file
153
crates/nu-explore/src/commands/nu.rs
Normal 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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
81
crates/nu-explore/src/commands/preview.rs
Normal file
81
crates/nu-explore/src/commands/preview.rs
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
50
crates/nu-explore/src/commands/quit.rs
Normal file
50
crates/nu-explore/src/commands/quit.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
70
crates/nu-explore/src/commands/try.rs
Normal file
70
crates/nu-explore/src/commands/try.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
51
crates/nu-explore/src/events.rs
Normal file
51
crates/nu-explore/src/events.rs
Normal 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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
60
crates/nu-explore/src/lib.rs
Normal file
60
crates/nu-explore/src/lib.rs
Normal 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)
|
||||||
|
}
|
42
crates/nu-explore/src/nu_common/command.rs
Normal file
42
crates/nu-explore/src/nu_common/command.rs
Normal 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)
|
||||||
|
}
|
21
crates/nu-explore/src/nu_common/mod.rs
Normal file
21
crates/nu-explore/src/nu_common/mod.rs
Normal 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};
|
844
crates/nu-explore/src/nu_common/table.rs
Normal file
844
crates/nu-explore/src/nu_common/table.rs
Normal 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(),
|
||||||
|
}
|
||||||
|
}
|
170
crates/nu-explore/src/nu_common/value.rs
Normal file
170
crates/nu-explore/src/nu_common/value.rs
Normal 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(),
|
||||||
|
}
|
||||||
|
}
|
1094
crates/nu-explore/src/pager.rs
Normal file
1094
crates/nu-explore/src/pager.rs
Normal file
File diff suppressed because it is too large
Load Diff
140
crates/nu-explore/src/views/coloredtextw.rs
Normal file
140
crates/nu-explore/src/views/coloredtextw.rs
Normal 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)
|
||||||
|
}
|
76
crates/nu-explore/src/views/information.rs
Normal file
76
crates/nu-explore/src/views/information.rs
Normal 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<_>>()
|
||||||
|
}
|
||||||
|
}
|
216
crates/nu-explore/src/views/interative.rs
Normal file
216
crates/nu-explore/src/views/interative.rs
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
104
crates/nu-explore/src/views/mod.rs
Normal file
104
crates/nu-explore/src/views/mod.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
175
crates/nu-explore/src/views/preview.rs
Normal file
175
crates/nu-explore/src/views/preview.rs
Normal 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()))
|
||||||
|
}
|
||||||
|
}
|
661
crates/nu-explore/src/views/record/mod.rs
Normal file
661
crates/nu-explore/src/views/record/mod.rs
Normal 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
|
||||||
|
}
|
574
crates/nu-explore/src/views/record/tablew.rs
Normal file
574
crates/nu-explore/src/views/record/tablew.rs
Normal 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"]),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
@ -87,6 +87,7 @@ pub struct Config {
|
|||||||
pub show_banner: bool,
|
pub show_banner: bool,
|
||||||
pub show_clickable_links_in_ls: bool,
|
pub show_clickable_links_in_ls: bool,
|
||||||
pub render_right_prompt_on_last_line: bool,
|
pub render_right_prompt_on_last_line: bool,
|
||||||
|
pub explore_config: HashMap<String, Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
@ -125,6 +126,7 @@ impl Default for Config {
|
|||||||
show_banner: true,
|
show_banner: true,
|
||||||
show_clickable_links_in_ls: true,
|
show_clickable_links_in_ls: true,
|
||||||
render_right_prompt_on_last_line: false,
|
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 {
|
impl Value {
|
||||||
pub fn into_config(self) -> Result<Config, ShellError> {
|
pub fn into_config(self) -> Result<Config, ShellError> {
|
||||||
let v = self.as_record();
|
let v = self.as_record();
|
||||||
@ -806,6 +813,13 @@ impl Value {
|
|||||||
eprintln!("$env.config.filesize_format is not a string")
|
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
|
// End legacy options
|
||||||
x => {
|
x => {
|
||||||
eprintln!("$env.config.{} is an unknown config setting", x)
|
eprintln!("$env.config.{} is an unknown config setting", x)
|
||||||
|
@ -24,3 +24,32 @@ pub fn wrap_string(text: &str, width: usize) -> String {
|
|||||||
.with(Width::wrap(width))
|
.with(Width::wrap(width))
|
||||||
.to_string()
|
.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()
|
||||||
|
}
|
||||||
|
@ -292,6 +292,23 @@ let-env config = {
|
|||||||
shell_integration: true # enables terminal markers and a workaround to arrow keys stop working issue
|
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
|
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.
|
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: {
|
hooks: {
|
||||||
pre_prompt: [{
|
pre_prompt: [{
|
||||||
$nothing # replace with source code to run before the prompt is shown
|
$nothing # replace with source code to run before the prompt is shown
|
||||||
|
Loading…
Reference in New Issue
Block a user