mirror of
https://github.com/nushell/nushell.git
synced 2024-11-28 03:13:44 +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",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ansi-str"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21b1ed1c166829a0ccb5d79caa0f75cb4abd4adb2ce2c096755b7ad5ffdb0990"
|
||||
dependencies = [
|
||||
"ansitok",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ansitok"
|
||||
version = "0.2.0"
|
||||
@ -443,6 +452,12 @@ dependencies = [
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cassowary"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
|
||||
|
||||
[[package]]
|
||||
name = "cast"
|
||||
version = "0.3.0"
|
||||
@ -2597,6 +2612,7 @@ dependencies = [
|
||||
"nu-ansi-term",
|
||||
"nu-color-config",
|
||||
"nu-engine",
|
||||
"nu-explore",
|
||||
"nu-glob",
|
||||
"nu-json",
|
||||
"nu-parser",
|
||||
@ -2664,6 +2680,23 @@ dependencies = [
|
||||
"sysinfo",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-explore"
|
||||
version = "0.72.1"
|
||||
dependencies = [
|
||||
"ansi-str 0.7.2",
|
||||
"crossterm 0.24.0",
|
||||
"nu-ansi-term",
|
||||
"nu-color-config",
|
||||
"nu-engine",
|
||||
"nu-parser",
|
||||
"nu-protocol",
|
||||
"nu-table",
|
||||
"strip-ansi-escapes",
|
||||
"terminal_size 0.2.1",
|
||||
"tui",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-glob"
|
||||
version = "0.72.1"
|
||||
@ -3129,7 +3162,7 @@ version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1526bb6aa9f10ec339fb10360f22c57edf81d5678d0278e93bc12a47ffbe4b01"
|
||||
dependencies = [
|
||||
"ansi-str",
|
||||
"ansi-str 0.5.0",
|
||||
"ansitok",
|
||||
"bytecount",
|
||||
"fnv",
|
||||
@ -4831,7 +4864,7 @@ version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56c3ee73732ffceaea7b8f6b719ce3bb17f253fa27461ffeaf568ebd0cdb4b85"
|
||||
dependencies = [
|
||||
"ansi-str",
|
||||
"ansi-str 0.5.0",
|
||||
"papergrid",
|
||||
"tabled_derive",
|
||||
"unicode-width",
|
||||
@ -5128,6 +5161,19 @@ version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
|
||||
|
||||
[[package]]
|
||||
name = "tui"
|
||||
version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccdd26cbd674007e649a272da4475fb666d3aa0ad0531da7136db6fab0e5bad1"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cassowary",
|
||||
"crossterm 0.25.0",
|
||||
"unicode-segmentation",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typed-arena"
|
||||
version = "1.7.0"
|
||||
|
@ -1,175 +1,19 @@
|
||||
use crate::nu_style::{color_from_hex, color_string_to_nustyle};
|
||||
use crate::nu_style::{color_from_hex, color_string_to_nustyle, lookup_style};
|
||||
use nu_ansi_term::{Color, Style};
|
||||
use nu_protocol::Config;
|
||||
use nu_protocol::{Config, Value};
|
||||
use nu_table::{Alignment, TextStyle};
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub fn lookup_ansi_color_style(s: &str) -> Style {
|
||||
if s.starts_with('#') {
|
||||
match color_from_hex(s) {
|
||||
Ok(c) => match c {
|
||||
Some(c) => c.normal(),
|
||||
None => Style::default(),
|
||||
},
|
||||
Err(_) => Style::default(),
|
||||
}
|
||||
color_from_hex(s)
|
||||
.ok()
|
||||
.and_then(|c| c.map(|c| c.normal()))
|
||||
.unwrap_or_default()
|
||||
} else if s.starts_with('{') {
|
||||
color_string_to_nustyle(s.to_string())
|
||||
} else {
|
||||
match s {
|
||||
"g" | "green" => Color::Green.normal(),
|
||||
"gb" | "green_bold" => Color::Green.bold(),
|
||||
"gu" | "green_underline" => Color::Green.underline(),
|
||||
"gi" | "green_italic" => Color::Green.italic(),
|
||||
"gd" | "green_dimmed" => Color::Green.dimmed(),
|
||||
"gr" | "green_reverse" => Color::Green.reverse(),
|
||||
"gbl" | "green_blink" => Color::Green.blink(),
|
||||
"gst" | "green_strike" => Color::Green.strikethrough(),
|
||||
|
||||
"lg" | "light_green" => Color::LightGreen.normal(),
|
||||
"lgb" | "light_green_bold" => Color::LightGreen.bold(),
|
||||
"lgu" | "light_green_underline" => Color::LightGreen.underline(),
|
||||
"lgi" | "light_green_italic" => Color::LightGreen.italic(),
|
||||
"lgd" | "light_green_dimmed" => Color::LightGreen.dimmed(),
|
||||
"lgr" | "light_green_reverse" => Color::LightGreen.reverse(),
|
||||
"lgbl" | "light_green_blink" => Color::LightGreen.blink(),
|
||||
"lgst" | "light_green_strike" => Color::LightGreen.strikethrough(),
|
||||
|
||||
"r" | "red" => Color::Red.normal(),
|
||||
"rb" | "red_bold" => Color::Red.bold(),
|
||||
"ru" | "red_underline" => Color::Red.underline(),
|
||||
"ri" | "red_italic" => Color::Red.italic(),
|
||||
"rd" | "red_dimmed" => Color::Red.dimmed(),
|
||||
"rr" | "red_reverse" => Color::Red.reverse(),
|
||||
"rbl" | "red_blink" => Color::Red.blink(),
|
||||
"rst" | "red_strike" => Color::Red.strikethrough(),
|
||||
|
||||
"lr" | "light_red" => Color::LightRed.normal(),
|
||||
"lrb" | "light_red_bold" => Color::LightRed.bold(),
|
||||
"lru" | "light_red_underline" => Color::LightRed.underline(),
|
||||
"lri" | "light_red_italic" => Color::LightRed.italic(),
|
||||
"lrd" | "light_red_dimmed" => Color::LightRed.dimmed(),
|
||||
"lrr" | "light_red_reverse" => Color::LightRed.reverse(),
|
||||
"lrbl" | "light_red_blink" => Color::LightRed.blink(),
|
||||
"lrst" | "light_red_strike" => Color::LightRed.strikethrough(),
|
||||
|
||||
"u" | "blue" => Color::Blue.normal(),
|
||||
"ub" | "blue_bold" => Color::Blue.bold(),
|
||||
"uu" | "blue_underline" => Color::Blue.underline(),
|
||||
"ui" | "blue_italic" => Color::Blue.italic(),
|
||||
"ud" | "blue_dimmed" => Color::Blue.dimmed(),
|
||||
"ur" | "blue_reverse" => Color::Blue.reverse(),
|
||||
"ubl" | "blue_blink" => Color::Blue.blink(),
|
||||
"ust" | "blue_strike" => Color::Blue.strikethrough(),
|
||||
|
||||
"lu" | "light_blue" => Color::LightBlue.normal(),
|
||||
"lub" | "light_blue_bold" => Color::LightBlue.bold(),
|
||||
"luu" | "light_blue_underline" => Color::LightBlue.underline(),
|
||||
"lui" | "light_blue_italic" => Color::LightBlue.italic(),
|
||||
"lud" | "light_blue_dimmed" => Color::LightBlue.dimmed(),
|
||||
"lur" | "light_blue_reverse" => Color::LightBlue.reverse(),
|
||||
"lubl" | "light_blue_blink" => Color::LightBlue.blink(),
|
||||
"lust" | "light_blue_strike" => Color::LightBlue.strikethrough(),
|
||||
|
||||
"b" | "black" => Color::Black.normal(),
|
||||
"bb" | "black_bold" => Color::Black.bold(),
|
||||
"bu" | "black_underline" => Color::Black.underline(),
|
||||
"bi" | "black_italic" => Color::Black.italic(),
|
||||
"bd" | "black_dimmed" => Color::Black.dimmed(),
|
||||
"br" | "black_reverse" => Color::Black.reverse(),
|
||||
"bbl" | "black_blink" => Color::Black.blink(),
|
||||
"bst" | "black_strike" => Color::Black.strikethrough(),
|
||||
|
||||
"ligr" | "light_gray" => Color::LightGray.normal(),
|
||||
"ligrb" | "light_gray_bold" => Color::LightGray.bold(),
|
||||
"ligru" | "light_gray_underline" => Color::LightGray.underline(),
|
||||
"ligri" | "light_gray_italic" => Color::LightGray.italic(),
|
||||
"ligrd" | "light_gray_dimmed" => Color::LightGray.dimmed(),
|
||||
"ligrr" | "light_gray_reverse" => Color::LightGray.reverse(),
|
||||
"ligrbl" | "light_gray_blink" => Color::LightGray.blink(),
|
||||
"ligrst" | "light_gray_strike" => Color::LightGray.strikethrough(),
|
||||
|
||||
"y" | "yellow" => Color::Yellow.normal(),
|
||||
"yb" | "yellow_bold" => Color::Yellow.bold(),
|
||||
"yu" | "yellow_underline" => Color::Yellow.underline(),
|
||||
"yi" | "yellow_italic" => Color::Yellow.italic(),
|
||||
"yd" | "yellow_dimmed" => Color::Yellow.dimmed(),
|
||||
"yr" | "yellow_reverse" => Color::Yellow.reverse(),
|
||||
"ybl" | "yellow_blink" => Color::Yellow.blink(),
|
||||
"yst" | "yellow_strike" => Color::Yellow.strikethrough(),
|
||||
|
||||
"ly" | "light_yellow" => Color::LightYellow.normal(),
|
||||
"lyb" | "light_yellow_bold" => Color::LightYellow.bold(),
|
||||
"lyu" | "light_yellow_underline" => Color::LightYellow.underline(),
|
||||
"lyi" | "light_yellow_italic" => Color::LightYellow.italic(),
|
||||
"lyd" | "light_yellow_dimmed" => Color::LightYellow.dimmed(),
|
||||
"lyr" | "light_yellow_reverse" => Color::LightYellow.reverse(),
|
||||
"lybl" | "light_yellow_blink" => Color::LightYellow.blink(),
|
||||
"lyst" | "light_yellow_strike" => Color::LightYellow.strikethrough(),
|
||||
|
||||
"p" | "purple" => Color::Purple.normal(),
|
||||
"pb" | "purple_bold" => Color::Purple.bold(),
|
||||
"pu" | "purple_underline" => Color::Purple.underline(),
|
||||
"pi" | "purple_italic" => Color::Purple.italic(),
|
||||
"pd" | "purple_dimmed" => Color::Purple.dimmed(),
|
||||
"pr" | "purple_reverse" => Color::Purple.reverse(),
|
||||
"pbl" | "purple_blink" => Color::Purple.blink(),
|
||||
"pst" | "purple_strike" => Color::Purple.strikethrough(),
|
||||
|
||||
"lp" | "light_purple" => Color::LightPurple.normal(),
|
||||
"lpb" | "light_purple_bold" => Color::LightPurple.bold(),
|
||||
"lpu" | "light_purple_underline" => Color::LightPurple.underline(),
|
||||
"lpi" | "light_purple_italic" => Color::LightPurple.italic(),
|
||||
"lpd" | "light_purple_dimmed" => Color::LightPurple.dimmed(),
|
||||
"lpr" | "light_purple_reverse" => Color::LightPurple.reverse(),
|
||||
"lpbl" | "light_purple_blink" => Color::LightPurple.blink(),
|
||||
"lpst" | "light_purple_strike" => Color::LightPurple.strikethrough(),
|
||||
|
||||
"c" | "cyan" => Color::Cyan.normal(),
|
||||
"cb" | "cyan_bold" => Color::Cyan.bold(),
|
||||
"cu" | "cyan_underline" => Color::Cyan.underline(),
|
||||
"ci" | "cyan_italic" => Color::Cyan.italic(),
|
||||
"cd" | "cyan_dimmed" => Color::Cyan.dimmed(),
|
||||
"cr" | "cyan_reverse" => Color::Cyan.reverse(),
|
||||
"cbl" | "cyan_blink" => Color::Cyan.blink(),
|
||||
"cst" | "cyan_strike" => Color::Cyan.strikethrough(),
|
||||
|
||||
"lc" | "light_cyan" => Color::LightCyan.normal(),
|
||||
"lcb" | "light_cyan_bold" => Color::LightCyan.bold(),
|
||||
"lcu" | "light_cyan_underline" => Color::LightCyan.underline(),
|
||||
"lci" | "light_cyan_italic" => Color::LightCyan.italic(),
|
||||
"lcd" | "light_cyan_dimmed" => Color::LightCyan.dimmed(),
|
||||
"lcr" | "light_cyan_reverse" => Color::LightCyan.reverse(),
|
||||
"lcbl" | "light_cyan_blink" => Color::LightCyan.blink(),
|
||||
"lcst" | "light_cyan_strike" => Color::LightCyan.strikethrough(),
|
||||
|
||||
"w" | "white" => Color::White.normal(),
|
||||
"wb" | "white_bold" => Color::White.bold(),
|
||||
"wu" | "white_underline" => Color::White.underline(),
|
||||
"wi" | "white_italic" => Color::White.italic(),
|
||||
"wd" | "white_dimmed" => Color::White.dimmed(),
|
||||
"wr" | "white_reverse" => Color::White.reverse(),
|
||||
"wbl" | "white_blink" => Color::White.blink(),
|
||||
"wst" | "white_strike" => Color::White.strikethrough(),
|
||||
|
||||
"dgr" | "dark_gray" => Color::DarkGray.normal(),
|
||||
"dgrb" | "dark_gray_bold" => Color::DarkGray.bold(),
|
||||
"dgru" | "dark_gray_underline" => Color::DarkGray.underline(),
|
||||
"dgri" | "dark_gray_italic" => Color::DarkGray.italic(),
|
||||
"dgrd" | "dark_gray_dimmed" => Color::DarkGray.dimmed(),
|
||||
"dgrr" | "dark_gray_reverse" => Color::DarkGray.reverse(),
|
||||
"dgrbl" | "dark_gray_blink" => Color::DarkGray.blink(),
|
||||
"dgrst" | "dark_gray_strike" => Color::DarkGray.strikethrough(),
|
||||
|
||||
"def" | "default" => Color::Default.normal(),
|
||||
"defb" | "default_bold" => Color::Default.bold(),
|
||||
"defu" | "default_underline" => Color::Default.underline(),
|
||||
"defi" | "default_italic" => Color::Default.italic(),
|
||||
"defd" | "default_dimmed" => Color::Default.dimmed(),
|
||||
"defr" | "default_reverse" => Color::Default.reverse(),
|
||||
|
||||
_ => Color::White.normal(),
|
||||
}
|
||||
lookup_style(s)
|
||||
}
|
||||
}
|
||||
|
||||
@ -226,6 +70,18 @@ pub fn get_color_config(config: &Config) -> HashMap<String, Style> {
|
||||
hm
|
||||
}
|
||||
|
||||
pub fn get_color_map(colors: &HashMap<String, Value>) -> HashMap<String, Style> {
|
||||
let mut hm: HashMap<String, Style> = HashMap::new();
|
||||
|
||||
for (key, value) in colors {
|
||||
if let Value::String { val, .. } = value {
|
||||
update_hashmap(key, val, &mut hm);
|
||||
}
|
||||
}
|
||||
|
||||
hm
|
||||
}
|
||||
|
||||
// This function will assign a text style to a primitive, or really any string that's
|
||||
// in the hashmap. The hashmap actually contains the style to be applied.
|
||||
pub fn style_primitive(primitive: &str, color_hm: &HashMap<String, Style>) -> TextStyle {
|
||||
|
@ -9,62 +9,17 @@ pub struct NuStyle {
|
||||
}
|
||||
|
||||
pub fn parse_nustyle(nu_style: NuStyle) -> Style {
|
||||
// get the nu_ansi_term::Color foreground color
|
||||
let fg_color = match nu_style.fg {
|
||||
Some(fg) => color_from_hex(&fg).unwrap_or_default(),
|
||||
_ => None,
|
||||
};
|
||||
// get the nu_ansi_term::Color background color
|
||||
let bg_color = match nu_style.bg {
|
||||
Some(bg) => color_from_hex(&bg).unwrap_or_default(),
|
||||
_ => None,
|
||||
};
|
||||
// get the attributes
|
||||
let color_attr = match nu_style.attr {
|
||||
Some(attr) => attr,
|
||||
_ => "".to_string(),
|
||||
let mut style = Style {
|
||||
foreground: nu_style.fg.and_then(|fg| lookup_color_str(&fg)),
|
||||
background: nu_style.bg.and_then(|bg| lookup_color_str(&bg)),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// setup the attributes available in nu_ansi_term::Style
|
||||
let mut bold = false;
|
||||
let mut dimmed = false;
|
||||
let mut italic = false;
|
||||
let mut underline = false;
|
||||
let mut blink = false;
|
||||
let mut reverse = false;
|
||||
let mut hidden = false;
|
||||
let mut strikethrough = false;
|
||||
|
||||
// since we can combine styles like bold-italic, iterate through the chars
|
||||
// and set the bools for later use in the nu_ansi_term::Style application
|
||||
for ch in color_attr.to_lowercase().chars() {
|
||||
match ch {
|
||||
'l' => blink = true,
|
||||
'b' => bold = true,
|
||||
'd' => dimmed = true,
|
||||
'h' => hidden = true,
|
||||
'i' => italic = true,
|
||||
'r' => reverse = true,
|
||||
's' => strikethrough = true,
|
||||
'u' => underline = true,
|
||||
'n' => (),
|
||||
_ => (),
|
||||
}
|
||||
if let Some(attrs) = nu_style.attr {
|
||||
fill_modifiers(&attrs, &mut style)
|
||||
}
|
||||
|
||||
// here's where we build the nu_ansi_term::Style
|
||||
Style {
|
||||
foreground: fg_color,
|
||||
background: bg_color,
|
||||
is_blink: blink,
|
||||
is_bold: bold,
|
||||
is_dimmed: dimmed,
|
||||
is_hidden: hidden,
|
||||
is_italic: italic,
|
||||
is_reverse: reverse,
|
||||
is_strikethrough: strikethrough,
|
||||
is_underline: underline,
|
||||
}
|
||||
style
|
||||
}
|
||||
|
||||
pub fn color_string_to_nustyle(color_string: String) -> Style {
|
||||
@ -101,3 +56,214 @@ pub fn color_from_hex(
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lookup_style(s: &str) -> Style {
|
||||
match s {
|
||||
"g" | "green" => Color::Green.normal(),
|
||||
"gb" | "green_bold" => Color::Green.bold(),
|
||||
"gu" | "green_underline" => Color::Green.underline(),
|
||||
"gi" | "green_italic" => Color::Green.italic(),
|
||||
"gd" | "green_dimmed" => Color::Green.dimmed(),
|
||||
"gr" | "green_reverse" => Color::Green.reverse(),
|
||||
"gbl" | "green_blink" => Color::Green.blink(),
|
||||
"gst" | "green_strike" => Color::Green.strikethrough(),
|
||||
|
||||
"lg" | "light_green" => Color::LightGreen.normal(),
|
||||
"lgb" | "light_green_bold" => Color::LightGreen.bold(),
|
||||
"lgu" | "light_green_underline" => Color::LightGreen.underline(),
|
||||
"lgi" | "light_green_italic" => Color::LightGreen.italic(),
|
||||
"lgd" | "light_green_dimmed" => Color::LightGreen.dimmed(),
|
||||
"lgr" | "light_green_reverse" => Color::LightGreen.reverse(),
|
||||
"lgbl" | "light_green_blink" => Color::LightGreen.blink(),
|
||||
"lgst" | "light_green_strike" => Color::LightGreen.strikethrough(),
|
||||
|
||||
"r" | "red" => Color::Red.normal(),
|
||||
"rb" | "red_bold" => Color::Red.bold(),
|
||||
"ru" | "red_underline" => Color::Red.underline(),
|
||||
"ri" | "red_italic" => Color::Red.italic(),
|
||||
"rd" | "red_dimmed" => Color::Red.dimmed(),
|
||||
"rr" | "red_reverse" => Color::Red.reverse(),
|
||||
"rbl" | "red_blink" => Color::Red.blink(),
|
||||
"rst" | "red_strike" => Color::Red.strikethrough(),
|
||||
|
||||
"lr" | "light_red" => Color::LightRed.normal(),
|
||||
"lrb" | "light_red_bold" => Color::LightRed.bold(),
|
||||
"lru" | "light_red_underline" => Color::LightRed.underline(),
|
||||
"lri" | "light_red_italic" => Color::LightRed.italic(),
|
||||
"lrd" | "light_red_dimmed" => Color::LightRed.dimmed(),
|
||||
"lrr" | "light_red_reverse" => Color::LightRed.reverse(),
|
||||
"lrbl" | "light_red_blink" => Color::LightRed.blink(),
|
||||
"lrst" | "light_red_strike" => Color::LightRed.strikethrough(),
|
||||
|
||||
"u" | "blue" => Color::Blue.normal(),
|
||||
"ub" | "blue_bold" => Color::Blue.bold(),
|
||||
"uu" | "blue_underline" => Color::Blue.underline(),
|
||||
"ui" | "blue_italic" => Color::Blue.italic(),
|
||||
"ud" | "blue_dimmed" => Color::Blue.dimmed(),
|
||||
"ur" | "blue_reverse" => Color::Blue.reverse(),
|
||||
"ubl" | "blue_blink" => Color::Blue.blink(),
|
||||
"ust" | "blue_strike" => Color::Blue.strikethrough(),
|
||||
|
||||
"lu" | "light_blue" => Color::LightBlue.normal(),
|
||||
"lub" | "light_blue_bold" => Color::LightBlue.bold(),
|
||||
"luu" | "light_blue_underline" => Color::LightBlue.underline(),
|
||||
"lui" | "light_blue_italic" => Color::LightBlue.italic(),
|
||||
"lud" | "light_blue_dimmed" => Color::LightBlue.dimmed(),
|
||||
"lur" | "light_blue_reverse" => Color::LightBlue.reverse(),
|
||||
"lubl" | "light_blue_blink" => Color::LightBlue.blink(),
|
||||
"lust" | "light_blue_strike" => Color::LightBlue.strikethrough(),
|
||||
|
||||
"b" | "black" => Color::Black.normal(),
|
||||
"bb" | "black_bold" => Color::Black.bold(),
|
||||
"bu" | "black_underline" => Color::Black.underline(),
|
||||
"bi" | "black_italic" => Color::Black.italic(),
|
||||
"bd" | "black_dimmed" => Color::Black.dimmed(),
|
||||
"br" | "black_reverse" => Color::Black.reverse(),
|
||||
"bbl" | "black_blink" => Color::Black.blink(),
|
||||
"bst" | "black_strike" => Color::Black.strikethrough(),
|
||||
|
||||
"ligr" | "light_gray" => Color::LightGray.normal(),
|
||||
"ligrb" | "light_gray_bold" => Color::LightGray.bold(),
|
||||
"ligru" | "light_gray_underline" => Color::LightGray.underline(),
|
||||
"ligri" | "light_gray_italic" => Color::LightGray.italic(),
|
||||
"ligrd" | "light_gray_dimmed" => Color::LightGray.dimmed(),
|
||||
"ligrr" | "light_gray_reverse" => Color::LightGray.reverse(),
|
||||
"ligrbl" | "light_gray_blink" => Color::LightGray.blink(),
|
||||
"ligrst" | "light_gray_strike" => Color::LightGray.strikethrough(),
|
||||
|
||||
"y" | "yellow" => Color::Yellow.normal(),
|
||||
"yb" | "yellow_bold" => Color::Yellow.bold(),
|
||||
"yu" | "yellow_underline" => Color::Yellow.underline(),
|
||||
"yi" | "yellow_italic" => Color::Yellow.italic(),
|
||||
"yd" | "yellow_dimmed" => Color::Yellow.dimmed(),
|
||||
"yr" | "yellow_reverse" => Color::Yellow.reverse(),
|
||||
"ybl" | "yellow_blink" => Color::Yellow.blink(),
|
||||
"yst" | "yellow_strike" => Color::Yellow.strikethrough(),
|
||||
|
||||
"ly" | "light_yellow" => Color::LightYellow.normal(),
|
||||
"lyb" | "light_yellow_bold" => Color::LightYellow.bold(),
|
||||
"lyu" | "light_yellow_underline" => Color::LightYellow.underline(),
|
||||
"lyi" | "light_yellow_italic" => Color::LightYellow.italic(),
|
||||
"lyd" | "light_yellow_dimmed" => Color::LightYellow.dimmed(),
|
||||
"lyr" | "light_yellow_reverse" => Color::LightYellow.reverse(),
|
||||
"lybl" | "light_yellow_blink" => Color::LightYellow.blink(),
|
||||
"lyst" | "light_yellow_strike" => Color::LightYellow.strikethrough(),
|
||||
|
||||
"p" | "purple" => Color::Purple.normal(),
|
||||
"pb" | "purple_bold" => Color::Purple.bold(),
|
||||
"pu" | "purple_underline" => Color::Purple.underline(),
|
||||
"pi" | "purple_italic" => Color::Purple.italic(),
|
||||
"pd" | "purple_dimmed" => Color::Purple.dimmed(),
|
||||
"pr" | "purple_reverse" => Color::Purple.reverse(),
|
||||
"pbl" | "purple_blink" => Color::Purple.blink(),
|
||||
"pst" | "purple_strike" => Color::Purple.strikethrough(),
|
||||
|
||||
"lp" | "light_purple" => Color::LightPurple.normal(),
|
||||
"lpb" | "light_purple_bold" => Color::LightPurple.bold(),
|
||||
"lpu" | "light_purple_underline" => Color::LightPurple.underline(),
|
||||
"lpi" | "light_purple_italic" => Color::LightPurple.italic(),
|
||||
"lpd" | "light_purple_dimmed" => Color::LightPurple.dimmed(),
|
||||
"lpr" | "light_purple_reverse" => Color::LightPurple.reverse(),
|
||||
"lpbl" | "light_purple_blink" => Color::LightPurple.blink(),
|
||||
"lpst" | "light_purple_strike" => Color::LightPurple.strikethrough(),
|
||||
|
||||
"c" | "cyan" => Color::Cyan.normal(),
|
||||
"cb" | "cyan_bold" => Color::Cyan.bold(),
|
||||
"cu" | "cyan_underline" => Color::Cyan.underline(),
|
||||
"ci" | "cyan_italic" => Color::Cyan.italic(),
|
||||
"cd" | "cyan_dimmed" => Color::Cyan.dimmed(),
|
||||
"cr" | "cyan_reverse" => Color::Cyan.reverse(),
|
||||
"cbl" | "cyan_blink" => Color::Cyan.blink(),
|
||||
"cst" | "cyan_strike" => Color::Cyan.strikethrough(),
|
||||
|
||||
"lc" | "light_cyan" => Color::LightCyan.normal(),
|
||||
"lcb" | "light_cyan_bold" => Color::LightCyan.bold(),
|
||||
"lcu" | "light_cyan_underline" => Color::LightCyan.underline(),
|
||||
"lci" | "light_cyan_italic" => Color::LightCyan.italic(),
|
||||
"lcd" | "light_cyan_dimmed" => Color::LightCyan.dimmed(),
|
||||
"lcr" | "light_cyan_reverse" => Color::LightCyan.reverse(),
|
||||
"lcbl" | "light_cyan_blink" => Color::LightCyan.blink(),
|
||||
"lcst" | "light_cyan_strike" => Color::LightCyan.strikethrough(),
|
||||
|
||||
"w" | "white" => Color::White.normal(),
|
||||
"wb" | "white_bold" => Color::White.bold(),
|
||||
"wu" | "white_underline" => Color::White.underline(),
|
||||
"wi" | "white_italic" => Color::White.italic(),
|
||||
"wd" | "white_dimmed" => Color::White.dimmed(),
|
||||
"wr" | "white_reverse" => Color::White.reverse(),
|
||||
"wbl" | "white_blink" => Color::White.blink(),
|
||||
"wst" | "white_strike" => Color::White.strikethrough(),
|
||||
|
||||
"dgr" | "dark_gray" => Color::DarkGray.normal(),
|
||||
"dgrb" | "dark_gray_bold" => Color::DarkGray.bold(),
|
||||
"dgru" | "dark_gray_underline" => Color::DarkGray.underline(),
|
||||
"dgri" | "dark_gray_italic" => Color::DarkGray.italic(),
|
||||
"dgrd" | "dark_gray_dimmed" => Color::DarkGray.dimmed(),
|
||||
"dgrr" | "dark_gray_reverse" => Color::DarkGray.reverse(),
|
||||
"dgrbl" | "dark_gray_blink" => Color::DarkGray.blink(),
|
||||
"dgrst" | "dark_gray_strike" => Color::DarkGray.strikethrough(),
|
||||
|
||||
"def" | "default" => Color::Default.normal(),
|
||||
"defb" | "default_bold" => Color::Default.bold(),
|
||||
"defu" | "default_underline" => Color::Default.underline(),
|
||||
"defi" | "default_italic" => Color::Default.italic(),
|
||||
"defd" | "default_dimmed" => Color::Default.dimmed(),
|
||||
"defr" | "default_reverse" => Color::Default.reverse(),
|
||||
|
||||
_ => Color::White.normal(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lookup_color(s: &str) -> Option<Color> {
|
||||
let color = match s {
|
||||
"g" | "green" => Color::Green,
|
||||
"lg" | "light_green" => Color::LightGreen,
|
||||
"r" | "red" => Color::Red,
|
||||
"lr" | "light_red" => Color::LightRed,
|
||||
"u" | "blue" => Color::Blue,
|
||||
"lu" | "light_blue" => Color::LightBlue,
|
||||
"b" | "black" => Color::Black,
|
||||
"ligr" | "light_gray" => Color::LightGray,
|
||||
"y" | "yellow" => Color::Yellow,
|
||||
"ly" | "light_yellow" => Color::LightYellow,
|
||||
"p" | "purple" => Color::Purple,
|
||||
"lp" | "light_purple" => Color::LightPurple,
|
||||
"c" | "cyan" => Color::Cyan,
|
||||
"lc" | "light_cyan" => Color::LightCyan,
|
||||
"w" | "white" => Color::White,
|
||||
"dgr" | "dark_gray" => Color::DarkGray,
|
||||
"def" | "default" => Color::Default,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
Some(color)
|
||||
}
|
||||
|
||||
fn fill_modifiers(attrs: &str, style: &mut Style) {
|
||||
// setup the attributes available in nu_ansi_term::Style
|
||||
//
|
||||
// since we can combine styles like bold-italic, iterate through the chars
|
||||
// and set the bools for later use in the nu_ansi_term::Style application
|
||||
for ch in attrs.to_lowercase().chars() {
|
||||
match ch {
|
||||
'l' => style.is_blink = true,
|
||||
'b' => style.is_bold = true,
|
||||
'd' => style.is_dimmed = true,
|
||||
'h' => style.is_hidden = true,
|
||||
'i' => style.is_italic = true,
|
||||
'r' => style.is_reverse = true,
|
||||
's' => style.is_strikethrough = true,
|
||||
'u' => style.is_underline = true,
|
||||
'n' => (),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn lookup_color_str(s: &str) -> Option<Color> {
|
||||
if s.starts_with('#') {
|
||||
color_from_hex(s).ok().and_then(|c| c)
|
||||
} else {
|
||||
lookup_color(s)
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ nu-system = { path = "../nu-system", version = "0.72.1" }
|
||||
nu-table = { path = "../nu-table", version = "0.72.1" }
|
||||
nu-term-grid = { path = "../nu-term-grid", version = "0.72.1" }
|
||||
nu-utils = { path = "../nu-utils", version = "0.72.1" }
|
||||
nu-explore = { path = "../nu-explore", version = "0.72.1" }
|
||||
nu-ansi-term = "0.46.0"
|
||||
num-format = { version = "0.4.3" }
|
||||
|
||||
|
@ -356,6 +356,7 @@ pub fn create_default_context() -> EngineState {
|
||||
bind_command! {
|
||||
Griddle,
|
||||
Table,
|
||||
Explore,
|
||||
};
|
||||
|
||||
// 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 icons;
|
||||
mod table;
|
||||
|
||||
pub use explore::Explore;
|
||||
pub use griddle::Griddle;
|
||||
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_clickable_links_in_ls: bool,
|
||||
pub render_right_prompt_on_last_line: bool,
|
||||
pub explore_config: HashMap<String, Value>,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
@ -125,6 +126,7 @@ impl Default for Config {
|
||||
show_banner: true,
|
||||
show_clickable_links_in_ls: true,
|
||||
render_right_prompt_on_last_line: false,
|
||||
explore_config: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -183,6 +185,11 @@ pub enum TrimStrategy {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ExploreConfig {
|
||||
pub color_config: HashMap<String, Value>,
|
||||
}
|
||||
|
||||
impl Value {
|
||||
pub fn into_config(self) -> Result<Config, ShellError> {
|
||||
let v = self.as_record();
|
||||
@ -806,6 +813,13 @@ impl Value {
|
||||
eprintln!("$env.config.filesize_format is not a string")
|
||||
}
|
||||
}
|
||||
"explore_config" => {
|
||||
if let Ok(map) = create_map(value, &config) {
|
||||
config.explore_config = map;
|
||||
} else {
|
||||
eprintln!("$env.config.explore_config is not a map")
|
||||
}
|
||||
}
|
||||
// End legacy options
|
||||
x => {
|
||||
eprintln!("$env.config.{} is an unknown config setting", x)
|
||||
|
@ -24,3 +24,32 @@ pub fn wrap_string(text: &str, width: usize) -> String {
|
||||
.with(Width::wrap(width))
|
||||
.to_string()
|
||||
}
|
||||
|
||||
pub fn string_truncate(text: &str, width: usize) -> String {
|
||||
// todo: change me...
|
||||
|
||||
match text.lines().next() {
|
||||
Some(first_line) => tabled::builder::Builder::from_iter([[first_line]])
|
||||
.build()
|
||||
.with(tabled::Style::empty())
|
||||
.with(tabled::Padding::zero())
|
||||
.with(tabled::Width::truncate(width))
|
||||
.to_string(),
|
||||
None => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn string_wrap(text: &str, width: usize) -> String {
|
||||
// todo: change me...
|
||||
|
||||
if text.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
tabled::builder::Builder::from_iter([[text]])
|
||||
.build()
|
||||
.with(tabled::Style::empty())
|
||||
.with(tabled::Padding::zero())
|
||||
.with(tabled::Width::wrap(width))
|
||||
.to_string()
|
||||
}
|
||||
|
@ -292,6 +292,23 @@ let-env config = {
|
||||
shell_integration: true # enables terminal markers and a workaround to arrow keys stop working issue
|
||||
show_banner: true # true or false to enable or disable the banner
|
||||
render_right_prompt_on_last_line: false # true or false to enable or disable right prompt to be rendered on last line of the prompt.
|
||||
|
||||
# A 'explore' utility config
|
||||
explore_config: {
|
||||
highlight: { bg: 'yellow', fg: 'black' }
|
||||
status_bar: { bg: '#C4C9C6', fg: '#1D1F21' }
|
||||
command_bar: { fg: '#C4C9C6' }
|
||||
split_line: '#404040'
|
||||
cursor: true
|
||||
# selected_column: 'blue'
|
||||
# selected_row: { fg: 'yellow', bg: '#C1C2A3' }
|
||||
# selected_cell: { fg: 'white', bg: '#777777' }
|
||||
# line_shift: false,
|
||||
# line_index: false,
|
||||
# line_head_top: false,
|
||||
# line_head_bottom: false,
|
||||
}
|
||||
|
||||
hooks: {
|
||||
pre_prompt: [{
|
||||
$nothing # replace with source code to run before the prompt is shown
|
||||
|
Loading…
Reference in New Issue
Block a user