nushell/crates/nu-command/src/viewers/griddle.rs

304 lines
16 KiB
Rust
Raw Normal View History

// use super::icons::{icon_for_file, iconify_style_ansi_to_nu};
use super::icons::icon_for_file;
use lscolors::{LsColors, Style};
use nu_engine::env_to_string;
2021-10-05 15:43:20 +02:00
use nu_engine::CallExt;
use nu_protocol::{
ast::{Call, PathMember},
2021-10-25 18:58:58 +02:00
engine::{Command, EngineState, Stack},
2021-12-04 13:38:21 +01:00
Category, Config, IntoPipelineData, PipelineData, ShellError, Signature, Span, SyntaxShape,
Value,
2021-10-05 15:43:20 +02:00
};
use nu_term_grid::grid::{Alignment, Cell, Direction, Filling, Grid, GridOptions};
2021-10-07 18:00:49 +02:00
use terminal_size::{Height, Width};
2021-10-05 15:43:20 +02:00
2021-10-25 06:01:02 +02:00
#[derive(Clone)]
2021-10-05 15:43:20 +02:00
pub struct Griddle;
impl Command for Griddle {
fn name(&self) -> &str {
"grid"
}
fn usage(&self) -> &str {
2021-10-08 15:14:32 +02:00
"Renders the output to a textual terminal grid."
2021-10-05 15:43:20 +02:00
}
fn signature(&self) -> nu_protocol::Signature {
2021-10-08 16:53:26 +02:00
Signature::build("grid")
.named(
"width",
SyntaxShape::Int,
"number of columns wide",
Some('w'),
)
.switch("color", "draw output with color", Some('c'))
.named(
"separator",
SyntaxShape::String,
"character to separate grid with",
Some('s'),
)
.category(Category::Viewers)
2021-10-05 15:43:20 +02:00
}
2021-10-08 15:14:32 +02:00
fn extra_usage(&self) -> &str {
r#"grid was built to give a concise gridded layout for ls. however,
it determines what to put in the grid by looking for a column named
'name'. this works great for tables and records but for lists we
need to do something different. such as with '[one two three] | grid'
it creates a fake column called 'name' for these values so that it
prints out the list properly."#
}
2021-10-05 15:43:20 +02:00
fn run(
&self,
2021-10-25 08:31:39 +02:00
engine_state: &EngineState,
stack: &mut Stack,
2021-10-05 15:43:20 +02:00
call: &Call,
2021-10-25 06:24:10 +02:00
input: PipelineData,
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
2021-10-25 08:31:39 +02:00
let width_param: Option<String> = call.get_flag(engine_state, stack, "width")?;
2021-10-08 16:53:26 +02:00
let color_param: bool = call.has_flag("color");
2021-10-25 08:31:39 +02:00
let separator_param: Option<String> = call.get_flag(engine_state, stack, "separator")?;
let config = stack.get_config().unwrap_or_default();
Use only $nu.env.PWD for getting the current directory (#587) * Use only $nu.env.PWD for getting current directory Because setting and reading to/from std::env changes the global state shich is problematic if we call `cd` from multiple threads (e.g., in a `par-each` block). With this change, when engine-q starts, it will either inherit existing PWD env var, or create a new one from `std::env::current_dir()`. Otherwise, everything that needs the current directory will get it from `$nu.env.PWD`. Each spawned external command will get its current directory per-process which should be thread-safe. One thing left to do is to patch nu-path for this as well since it uses `std::env::current_dir()` in its expansions. * Rename nu-path functions *_with is not *_relative which should be more descriptive and frees "with" for use in a followup commit. * Clone stack every each iter; Fix some commands Cloning the stack each iteration of `each` makes sure we're not reusing PWD between iterations. Some fixes in commands to make them use the new PWD. * Post-rebase cleanup, fmt, clippy * Change back _relative to _with in nu-path funcs Didn't use the idea I had for the new "_with". * Remove leftover current_dir from rebase * Add cwd sync at merge_delta() This makes sure the parser and completer always have up-to-date cwd. * Always pass absolute path to glob in ls * Do not allow PWD a relative path; Allow recovery Makes it possible to recover PWD by proceeding with the REPL cycle. * Clone stack in each also for byte/string stream * (WIP) Start moving env variables to engine state * (WIP) Move env vars to engine state (ugly) Quick and dirty code. * (WIP) Remove unused mut and args; Fmt * (WIP) Fix dataframe tests * (WIP) Fix missing args after rebase * (WIP) Clone only env vars, not the whole stack * (WIP) Add env var clone to `for` loop as well * Minor edits * Refactor merge_delta() to include stack merging. Less error-prone than doing it manually. * Clone env for each `update` command iteration * Mark env var hidden only when found in eng. state * Fix clippt warnings * Add TODO about env var reading * Do not clone empty environment in loops * Remove extra cwd collection * Split current_dir() into str and path; Fix autocd * Make completions respect PWD env var
2022-01-04 23:30:34 +01:00
let env_str = match stack.get_env_var(engine_state, "LS_COLORS") {
Some(v) => Some(env_to_string("LS_COLORS", v, engine_state, stack, &config)?),
None => None,
};
let use_grid_icons = config.use_grid_icons;
2021-10-05 15:43:20 +02:00
match input {
PipelineData::Value(Value::List { vals, .. }, ..) => {
2021-10-07 18:00:49 +02:00
// dbg!("value::list");
2021-12-19 08:46:13 +01:00
let data = convert_to_list(vals, &config, call.head);
2021-10-07 18:00:49 +02:00
if let Some(items) = data {
Ok(create_grid_output(
items,
call,
width_param,
color_param,
2021-10-11 16:32:06 +02:00
separator_param,
env_str,
use_grid_icons,
2021-12-04 13:38:21 +01:00
)?)
2021-10-07 18:00:49 +02:00
} else {
Ok(PipelineData::new(call.head))
2021-10-07 18:00:49 +02:00
}
2021-10-05 15:43:20 +02:00
}
PipelineData::ListStream(stream, ..) => {
2021-10-05 15:43:20 +02:00
// dbg!("value::stream");
2021-12-19 08:46:13 +01:00
let data = convert_to_list(stream, &config, call.head);
2021-10-07 18:00:49 +02:00
if let Some(items) = data {
Ok(create_grid_output(
items,
call,
width_param,
color_param,
2021-10-11 16:32:06 +02:00
separator_param,
env_str,
use_grid_icons,
2021-12-04 13:38:21 +01:00
)?)
2021-10-05 15:43:20 +02:00
} else {
// dbg!(data);
Ok(PipelineData::new(call.head))
2021-10-05 15:43:20 +02:00
}
}
PipelineData::Value(Value::Record { cols, vals, .. }, ..) => {
2021-10-07 23:50:27 +02:00
// dbg!("value::record");
let mut items = vec![];
2021-10-05 15:43:20 +02:00
2021-10-07 23:50:27 +02:00
for (i, (c, v)) in cols.into_iter().zip(vals.into_iter()).enumerate() {
items.push((i, c, v.into_string(", ", &config)))
2021-10-07 23:50:27 +02:00
}
2021-10-05 15:43:20 +02:00
Ok(create_grid_output(
items,
call,
width_param,
color_param,
2021-10-11 16:32:06 +02:00
separator_param,
env_str,
use_grid_icons,
2021-12-04 13:38:21 +01:00
)?)
2021-10-05 15:43:20 +02:00
}
2021-10-07 18:00:49 +02:00
x => {
// dbg!("other value");
// dbg!(x.get_type());
Ok(x)
}
2021-10-05 15:43:20 +02:00
}
}
}
fn strip_ansi(astring: &str) -> String {
if let Ok(bytes) = strip_ansi_escapes::strip(astring) {
String::from_utf8_lossy(&bytes).to_string()
} else {
astring.to_string()
}
}
fn create_grid_output(
2021-10-07 23:50:27 +02:00
items: Vec<(usize, String, String)>,
call: &Call,
2021-10-08 16:53:26 +02:00
width_param: Option<String>,
color_param: bool,
separator_param: Option<String>,
env_str: Option<String>,
use_grid_icons: bool,
2021-12-04 13:38:21 +01:00
) -> Result<PipelineData, ShellError> {
let ls_colors = match env_str {
Some(s) => LsColors::from_string(&s),
2022-02-08 18:13:04 +01:00
None => LsColors::from_string("st=0:di=0;38;5;81:so=0;38;5;16;48;5;203:ln=0;38;5;203:cd=0;38;5;203;48;5;236:ex=1;38;5;203:or=0;38;5;16;48;5;203:fi=0:bd=0;38;5;81;48;5;236:ow=0:mi=0;38;5;16;48;5;203:*~=0;38;5;243:no=0:tw=0:pi=0;38;5;16;48;5;81:*.z=4;38;5;203:*.t=0;38;5;48:*.o=0;38;5;243:*.d=0;38;5;48:*.a=1;38;5;203:*.c=0;38;5;48:*.m=0;38;5;48:*.p=0;38;5;48:*.r=0;38;5;48:*.h=0;38;5;48:*.ml=0;38;5;48:*.ll=0;38;5;48:*.gv=0;38;5;48:*.cp=0;38;5;48:*.xz=4;38;5;203:*.hs=0;38;5;48:*css=0;38;5;48:*.ui=0;38;5;149:*.pl=0;38;5;48:*.ts=0;38;5;48:*.gz=4;38;5;203:*.so=1;38;5;203:*.cr=0;38;5;48:*.fs=0;38;5;48:*.bz=4;38;5;203:*.ko=1;38;5;203:*.as=0;38;5;48:*.sh=0;38;5;48:*.pp=0;38;5;48:*.el=0;38;5;48:*.py=0;38;5;48:*.lo=0;38;5;243:*.bc=0;38;5;243:*.cc=0;38;5;48:*.pm=0;38;5;48:*.rs=0;38;5;48:*.di=0;38;5;48:*.jl=0;38;5;48:*.rb=0;38;5;48:*.md=0;38;5;185:*.js=0;38;5;48:*.go=0;38;5;48:*.vb=0;38;5;48:*.hi=0;38;5;243:*.kt=0;38;5;48:*.hh=0;38;5;48:*.cs=0;38;5;48:*.mn=0;38;5;48:*.nb=0;38;5;48:*.7z=4;38;5;203:*.ex=0;38;5;48:*.rm=0;38;5;208:*.ps=0;38;5;186:*.td=0;38;5;48:*.la=0;38;5;243:*.aux=0;38;5;243:*.xmp=0;38;5;149:*.mp4=0;38;5;208:*.rpm=4;38;5;203:*.m4a=0;38;5;208:*.zip=4;38;5;203:*.dll=1;38;5;203:*.bcf=0;38;5;243:*.awk=0;38;5;48:*.aif=0;38;5;208:*.zst=4;38;5;203:*.bak=0;38;5;243:*.tgz=4;38;5;203:*.com=1;38;5;203:*.clj=0;38;5;48:*.sxw=0;38;5;186:*.vob=0;38;5;208:*.fsx=0;38;5;48:*.doc=0;38;5;186:*.mkv=0;38;5;208:*.tbz=4;38;5;203:*.ogg=0;38;5;208:*.wma=0;38;5;208:*.mid=0;38;5;208:*.kex=0;38;5;186:*.out=0;38;5;243:*.ltx=0;38;5;48:*.sql=0;38;5;48:*.ppt=0;38;5;186:*.tex=0;38;5;48:*.odp=0;38;5;186:*.log=0;38;5;243:*.arj=4;38;5;203:*.ipp=0;38;5;48:*.sbt=0;38;5;48:*.jpg=0;38;5;208:*.yml=0;38;5;149:*.txt=0;38;5;185:*.csv=0;38;5;185:*.dox=0;38;5;149:*.pro=0;38;5;149:*.bst=0;38;5;149:*TODO=1:*.mir=0;38;5;48:*.bat=1;38;5;203:*.m4v=0;38;5;208:*.pod=0;38;5;48:*.cfg=0;38;5;149:*.pas=0;38;5;48:*.tml=0;38;5;149:*.bib=0;38;5;149:*.ini=0;38;5;149:*.apk=4;38;5;203:*.h++=0;38;5;48:*.pyc=0;38;5;243:*.img=4;38;5;203:*.rst=0;38;5;185:*.swf=0;38;5;208:*.htm=0;38;5;185:*.ttf=0;38;5;208:*.elm=0;38;5;48:*hgrc=0;38;5;149:*.bmp=0;38;5;208:*.fsi=0;38;5;48:*.pgm=0;38;5;208:*.dpr=0;38;5;48:*.xls=0;38;5;186:*.tcl=0;38;5;48:*.mli=0;38;5;48:*.ppm=0;38;5;208:*.bbl=0;38;5;243:*.lua=0;38;5;48:*.asa=0;38;5;48:*.pbm=0;38;5;208:*.avi=0;38;5;208:*.def=0;38;5;48:*.mov=0;38;5;208:*.hxx=0;38;5;48:*.tif=0;38;5;208:*.fon=0;38;5;208:*.zsh=0;38;5;48:*.png=0;38;5;208:*.inc=0;38;5;48:*.jar=4;38;5;203:*.swp=0;38;5;243:*.pid=0;38;5;243:*.gif=0;38;5;208:*.ind=0;38;5;243:*.erl=0;38;5;48:*.ilg=0;38;5;243:*.eps=0;38;5;208:*.tsx=0;38;5;48:*.git=0;38;5;243:*.inl=0;38;5;48:*.rtf=0;38;5;186:*.hpp=0;38;5;48:*.kts=0;38;5;48:*.deb=4;38;5;203:*.svg=0;38;5;208:*.pps=0;38;5;186:*.ps1=0;38;5;48:*.c++=0;38;5;48:*.cpp=0;38;5;48:*.bsh=0;38;5;48:*.php=0;38;5;48:*.exs=0;38;5;48:*.toc=0;38;5;243:*.mp3=0;38;5;208:*.epp=0;38;5;48:*.rar=4;38;5;203:*.wav=0;38;5;208:*.xlr=0;38;5;186:*.tmp=0;38;5;243:*.cxx=0;38;5;48:*.iso=4;38;5;203:*.dmg=4;38;5;203:*.gvy=0;38;5;48:*.bin=4;38;5;203:*.wmv=0;38;5;208:*.blg=0;38;5;243:*.ods=0;38;5;186:*.psd=0;38;5;208:*.mpg=0;38;5;208:*.dot=0;38;5;48:*.cgi=0;38;5;48:*.xml=0;38;5;185:*.htc=0;38;5;48:*.ics=0;38;5;186:*.bz2=4;38;5;203:*.tar=4;38;5;203:*.csx=0;38;5;48:*.ico=0;38;5;208:*.sxi=0;38;5;186:*.nix=0;38;5;149:*.pkg=4;38;5;203:*.bag=4;38;5;203:*.fnt=0;38;5;208:*.idx=0;38;5;243:*.xcf=0;38;5;208:*.exe=1;38;5;203:*.flv=0;38;5;208:*.fls=0;38;5;243:*.otf=0;38;5;208:*.vcd=4;38;5;203:*.vim=0;38;5;48:*.sty=0;38;5;243:*.pdf=0;38;5;186:*.odt=0;38;5;186:*.purs=0;38;5;48:*.h264=0;38;5;208:*.jpeg=0;38;5;208:*.dart=0;38;5;48:*.pptx=0;38;5;186:*.lock=0;38;5;243:*.bash=0;38;5;48:*.rlib=0;38;5;243:*.hgrc=0;38;5;149:*.psm1=0;38;5;48:*.toml=0;38;5;149:*.tbz2=4;38;5;203:*.yaml=0;38;5;149:*.make=0;38;5;149:*.orig=0;38;5;243:*.html=0;38;5;185:*.fish=0;38;5;48:*.diff=0;38;5;48:*.xlsx=0;38;5;186:*.docx=0;38;5;186:*.json=0;38;5;149:*.psd1=0;38;5;48:*.tiff=0;38;5;208:*.flac=0;38;5;208:*.java=0;38;5;48:*.less=0;38;5;48:*.mpeg=0;38;5;208:*.conf=0;38;5;149:*.lisp=0;38;5;48:*.epub=0;38;5;186:*.cabal=0;38;5;48
};
let cols = if let Some(col) = width_param {
col.parse::<u16>().unwrap_or(80)
} else if let Some((Width(w), Height(_h))) = terminal_size::terminal_size() {
w
} else {
80u16
};
2021-10-11 16:32:06 +02:00
let sep = if let Some(separator) = separator_param {
separator
} else {
"".to_string()
};
2021-10-07 23:50:27 +02:00
let mut grid = Grid::new(GridOptions {
direction: Direction::TopToBottom,
filling: Filling::Text(sep),
2021-10-07 23:50:27 +02:00
});
for (_row_index, header, value) in items {
// only output value if the header name is 'name'
if header == "name" {
2021-10-08 16:53:26 +02:00
if color_param {
if use_grid_icons {
let no_ansi = strip_ansi(&value);
let path = std::path::Path::new(&no_ansi);
2021-12-19 08:46:13 +01:00
let icon = icon_for_file(path, call.head)?;
let ls_colors_style = ls_colors.style_for_path(path);
// eprintln!("ls_colors_style: {:?}", &ls_colors_style);
let icon_style = match ls_colors_style {
Some(c) => c.to_crossterm_style(),
None => crossterm::style::ContentStyle::default(),
};
// eprintln!("icon_style: {:?}", &icon_style);
let ansi_style = ls_colors_style
.map(Style::to_crossterm_style)
.unwrap_or_default();
// eprintln!("ansi_style: {:?}", &ansi_style);
let item = format!("{} {}", icon_style.apply(icon), ansi_style.apply(value));
let mut cell = Cell::from(item);
cell.alignment = Alignment::Left;
grid.add(cell);
} else {
let style = ls_colors.style_for_path(value.clone());
let ansi_style = style.map(Style::to_crossterm_style).unwrap_or_default();
let mut cell = Cell::from(ansi_style.apply(value).to_string());
cell.alignment = Alignment::Left;
grid.add(cell);
}
2021-10-08 16:53:26 +02:00
} else {
let mut cell = Cell::from(value);
cell.alignment = Alignment::Left;
2021-10-08 16:53:26 +02:00
grid.add(cell);
}
2021-10-07 23:50:27 +02:00
}
}
2021-12-04 13:38:21 +01:00
Ok(
if let Some(grid_display) = grid.fit_into_width(cols as usize) {
Value::String {
val: grid_display.to_string(),
span: call.head,
}
} else {
Value::String {
val: format!("Couldn't fit grid into {} columns!", cols),
span: call.head,
}
2021-10-07 23:50:27 +02:00
}
2021-12-04 13:38:21 +01:00
.into_pipeline_data(),
)
2021-10-07 23:50:27 +02:00
}
fn convert_to_list(
iter: impl IntoIterator<Item = Value>,
config: &Config,
2021-12-19 08:46:13 +01:00
head: Span,
) -> Option<Vec<(usize, String, String)>> {
2021-10-05 15:43:20 +02:00
let mut iter = iter.into_iter().peekable();
if let Some(first) = iter.peek() {
let mut headers = first.columns();
if !headers.is_empty() {
headers.insert(0, "#".into());
}
2021-10-07 23:50:27 +02:00
let mut data = vec![];
2021-10-05 15:43:20 +02:00
for (row_num, item) in iter.enumerate() {
let mut row = vec![row_num.to_string()];
if headers.is_empty() {
row.push(item.into_string(", ", config))
2021-10-05 15:43:20 +02:00
} else {
for header in headers.iter().skip(1) {
let result = match item {
Value::Record { .. } => {
item.clone().follow_cell_path(&[PathMember::String {
val: header.into(),
2021-12-19 08:46:13 +01:00
span: head,
2021-10-05 15:43:20 +02:00
}])
}
_ => Ok(item.clone()),
};
match result {
Ok(value) => row.push(value.into_string(", ", config)),
2021-10-05 15:43:20 +02:00
Err(_) => row.push(String::new()),
}
}
}
data.push(row);
}
2021-10-07 23:50:27 +02:00
let mut h: Vec<String> = headers.into_iter().collect();
// This is just a list
if h.is_empty() {
// let's fake the header
h.push("#".to_string());
h.push("name".to_string());
}
// this tuple is (row_index, header_name, value)
let mut interleaved = vec![];
2021-10-07 23:59:01 +02:00
for (i, v) in data.into_iter().enumerate() {
2021-10-07 23:50:27 +02:00
for (n, s) in v.into_iter().enumerate() {
if h.len() == 1 {
2021-10-08 15:14:32 +02:00
// always get the 1th element since this is a simple list
2021-10-07 23:50:27 +02:00
// and we hacked the header above because it was empty
2021-10-08 15:14:32 +02:00
// 0th element is an index, 1th element is the value
2021-10-07 23:50:27 +02:00
interleaved.push((i, h[1].clone(), s))
} else {
interleaved.push((i, h[n].clone(), s))
}
}
}
2021-10-08 15:14:32 +02:00
2021-10-07 23:50:27 +02:00
Some(interleaved)
2021-10-05 15:43:20 +02:00
} else {
None
}
}