forked from extern/nushell
nu-explore/ A few things (#7339)
ref #7332 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
2d07c6eedb
commit
9c1a3aa244
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -2691,6 +2691,7 @@ dependencies = [
|
||||
"nu-ansi-term",
|
||||
"nu-color-config",
|
||||
"nu-engine",
|
||||
"nu-json",
|
||||
"nu-parser",
|
||||
"nu-protocol",
|
||||
"nu-table",
|
||||
|
@ -1,13 +1,74 @@
|
||||
use nu_ansi_term::{Color, Style};
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Deserialize, PartialEq, Eq, Debug)]
|
||||
#[derive(Deserialize, Serialize, PartialEq, Eq, Debug)]
|
||||
pub struct NuStyle {
|
||||
pub fg: Option<String>,
|
||||
pub bg: Option<String>,
|
||||
pub attr: Option<String>,
|
||||
}
|
||||
|
||||
impl From<Style> for NuStyle {
|
||||
fn from(s: Style) -> Self {
|
||||
Self {
|
||||
bg: s.background.and_then(color_to_string),
|
||||
fg: s.foreground.and_then(color_to_string),
|
||||
attr: style_get_attr(s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn style_get_attr(s: Style) -> Option<String> {
|
||||
macro_rules! check {
|
||||
($attrs:expr, $b:expr, $c:expr) => {
|
||||
if $b {
|
||||
$attrs.push($c);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let mut attrs = String::new();
|
||||
|
||||
check!(attrs, s.is_blink, 'l');
|
||||
check!(attrs, s.is_bold, 'b');
|
||||
check!(attrs, s.is_dimmed, 'd');
|
||||
check!(attrs, s.is_reverse, 'r');
|
||||
check!(attrs, s.is_strikethrough, 's');
|
||||
check!(attrs, s.is_underline, 'u');
|
||||
|
||||
if attrs.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(attrs)
|
||||
}
|
||||
}
|
||||
|
||||
fn color_to_string(color: Color) -> Option<String> {
|
||||
match color {
|
||||
Color::Black => Some(String::from("black")),
|
||||
Color::DarkGray => Some(String::from("dark_gray")),
|
||||
Color::Red => Some(String::from("red")),
|
||||
Color::LightRed => Some(String::from("light_red")),
|
||||
Color::Green => Some(String::from("green")),
|
||||
Color::LightGreen => Some(String::from("light_green")),
|
||||
Color::Yellow => Some(String::from("yellow")),
|
||||
Color::LightYellow => Some(String::from("light_yellow")),
|
||||
Color::Blue => Some(String::from("blue")),
|
||||
Color::LightBlue => Some(String::from("light_blue")),
|
||||
Color::Purple => Some(String::from("purple")),
|
||||
Color::LightPurple => Some(String::from("light_purple")),
|
||||
Color::Magenta => Some(String::from("magenta")),
|
||||
Color::LightMagenta => Some(String::from("light_magenta")),
|
||||
Color::Cyan => Some(String::from("cyan")),
|
||||
Color::LightCyan => Some(String::from("light_cyan")),
|
||||
Color::White => Some(String::from("white")),
|
||||
Color::LightGray => Some(String::from("light_gray")),
|
||||
Color::Default => Some(String::from("default")),
|
||||
Color::Rgb(r, g, b) => Some(format!("#{:X}{:X}{:X}", r, g, b)),
|
||||
Color::Fixed(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_nustyle(nu_style: NuStyle) -> Style {
|
||||
let mut style = Style {
|
||||
foreground: nu_style.fg.and_then(|fg| lookup_color_str(&fg)),
|
||||
|
@ -3,11 +3,15 @@ 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_explore::{
|
||||
run_pager,
|
||||
util::{create_map, map_into_value},
|
||||
PagerConfig, StyleConfig,
|
||||
};
|
||||
use nu_protocol::{
|
||||
ast::Call,
|
||||
engine::{Command, EngineState, Stack},
|
||||
Category, Example, PipelineData, ShellError, Signature, SyntaxShape, Value,
|
||||
Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value,
|
||||
};
|
||||
|
||||
/// A `less` like program to render a [Value] as a table.
|
||||
@ -63,23 +67,29 @@ impl Command for Explore {
|
||||
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 nu_config = engine_state.get_config();
|
||||
let color_hm = get_color_config(nu_config);
|
||||
|
||||
let config = engine_state.get_config();
|
||||
let color_hm = get_color_config(config);
|
||||
let style = theme_from_config(&config.explore);
|
||||
let mut config = nu_config.explore.clone();
|
||||
prepare_default_config(&mut config);
|
||||
update_config(&mut config, show_index, show_head);
|
||||
|
||||
let view_cfg = ViewConfig::new(config, &color_hm, &style);
|
||||
let show_banner = is_need_banner(&config).unwrap_or(true);
|
||||
let exit_esc = is_need_esc_exit(&config).unwrap_or(false);
|
||||
|
||||
let result = nu_explore::run_pager(engine_state, stack, ctrlc, table_cfg, view_cfg, input);
|
||||
let style = style_from_config(&config);
|
||||
|
||||
let mut config = PagerConfig::new(nu_config, &color_hm, config);
|
||||
config.style = style;
|
||||
config.reverse = is_reverse;
|
||||
config.peek_value = peek_value;
|
||||
config.reverse = is_reverse;
|
||||
config.exit_esc = exit_esc;
|
||||
config.show_banner = show_banner;
|
||||
|
||||
let result = run_pager(engine_state, stack, ctrlc, input, config);
|
||||
|
||||
match result {
|
||||
Ok(Some(value)) => Ok(PipelineData::Value(value, None)),
|
||||
@ -118,101 +128,246 @@ impl Command for Explore {
|
||||
}
|
||||
}
|
||||
|
||||
fn theme_from_config(config: &HashMap<String, Value>) -> StyleConfig {
|
||||
fn is_need_banner(config: &HashMap<String, Value>) -> Option<bool> {
|
||||
config.get("help_banner").and_then(|v| v.as_bool().ok())
|
||||
}
|
||||
|
||||
fn is_need_esc_exit(config: &HashMap<String, Value>) -> Option<bool> {
|
||||
config.get("exit_esc").and_then(|v| v.as_bool().ok())
|
||||
}
|
||||
|
||||
fn update_config(config: &mut HashMap<String, Value>, show_index: bool, show_head: bool) {
|
||||
let mut hm = config.get("table").and_then(create_map).unwrap_or_default();
|
||||
if show_index {
|
||||
insert_bool(&mut hm, "show_index", show_index);
|
||||
}
|
||||
|
||||
if show_head {
|
||||
insert_bool(&mut hm, "show_head", show_head);
|
||||
}
|
||||
|
||||
config.insert(String::from("table"), map_into_value(hm));
|
||||
}
|
||||
|
||||
fn style_from_config(config: &HashMap<String, Value>) -> StyleConfig {
|
||||
let mut style = StyleConfig::default();
|
||||
|
||||
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("status_bar_text") {
|
||||
style.status_bar_text = *s;
|
||||
}
|
||||
|
||||
if let Some(s) = colors.get("command_bar") {
|
||||
style.cmd_bar = *s;
|
||||
if let Some(s) = colors.get("status_bar_background") {
|
||||
style.status_bar_background = *s;
|
||||
}
|
||||
|
||||
if let Some(s) = colors.get("split_line") {
|
||||
style.split_line = *s;
|
||||
if let Some(s) = colors.get("command_bar_text") {
|
||||
style.cmd_bar_text = *s;
|
||||
}
|
||||
|
||||
if let Some(s) = colors.get("command_bar_background") {
|
||||
style.cmd_bar_background = *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(hm) = config.get("status").and_then(create_map) {
|
||||
let colors = get_color_map(&hm);
|
||||
|
||||
if let Some(s) = colors.get("selected_row") {
|
||||
style.selected_row = Some(*s);
|
||||
}
|
||||
if let Some(s) = colors.get("info") {
|
||||
style.status_info = *s;
|
||||
}
|
||||
|
||||
if let Some(s) = colors.get("selected_column") {
|
||||
style.selected_column = Some(*s);
|
||||
}
|
||||
if let Some(s) = colors.get("warn") {
|
||||
style.status_warn = *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;
|
||||
if let Some(s) = colors.get("error") {
|
||||
style.status_error = *s;
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
fn prepare_default_config(config: &mut HashMap<String, Value>) {
|
||||
const STATUS_BAR: Style = color(
|
||||
Some(Color::Rgb(29, 31, 33)),
|
||||
Some(Color::Rgb(196, 201, 198)),
|
||||
);
|
||||
|
||||
const INPUT_BAR: Style = color(Some(Color::Rgb(196, 201, 198)), None);
|
||||
|
||||
const HIGHLIGHT: Style = color(Some(Color::Black), Some(Color::Yellow));
|
||||
|
||||
const STATUS_ERROR: Style = color(Some(Color::White), Some(Color::Red));
|
||||
|
||||
const STATUS_INFO: Style = color(None, None);
|
||||
|
||||
const STATUS_WARN: Style = color(None, None);
|
||||
|
||||
const TABLE_SPLIT_LINE: Style = color(Some(Color::Rgb(64, 64, 64)), None);
|
||||
|
||||
const TABLE_LINE_HEADER_TOP: bool = true;
|
||||
|
||||
const TABLE_LINE_HEADER_BOTTOM: bool = true;
|
||||
|
||||
const TABLE_LINE_INDEX: bool = true;
|
||||
|
||||
const TABLE_LINE_SHIFT: bool = true;
|
||||
|
||||
const TABLE_SELECT_CURSOR: bool = true;
|
||||
|
||||
const TABLE_SELECT_CELL: Style = color(None, None);
|
||||
|
||||
const TABLE_SELECT_ROW: Style = color(None, None);
|
||||
|
||||
const TABLE_SELECT_COLUMN: Style = color(None, None);
|
||||
|
||||
const TRY_BORDER_COLOR: Style = color(None, None);
|
||||
|
||||
const CONFIG_CURSOR_COLOR: Style = color(Some(Color::Black), Some(Color::LightYellow));
|
||||
|
||||
insert_style(config, "status_bar_background", STATUS_BAR);
|
||||
insert_style(config, "command_bar_text", INPUT_BAR);
|
||||
insert_style(config, "highlight", HIGHLIGHT);
|
||||
|
||||
// because how config works we need to parse a string into Value::Record
|
||||
|
||||
{
|
||||
let mut hm = config
|
||||
.get("status")
|
||||
.and_then(parse_hash_map)
|
||||
.unwrap_or_default();
|
||||
|
||||
insert_style(&mut hm, "info", STATUS_INFO);
|
||||
insert_style(&mut hm, "warn", STATUS_WARN);
|
||||
insert_style(&mut hm, "error", STATUS_ERROR);
|
||||
|
||||
config.insert(String::from("status"), map_into_value(hm));
|
||||
}
|
||||
{
|
||||
let mut hm = config
|
||||
.get("table")
|
||||
.and_then(parse_hash_map)
|
||||
.unwrap_or_default();
|
||||
|
||||
insert_style(&mut hm, "split_line", TABLE_SPLIT_LINE);
|
||||
insert_style(&mut hm, "selected_cell", TABLE_SELECT_CELL);
|
||||
insert_style(&mut hm, "selected_row", TABLE_SELECT_ROW);
|
||||
insert_style(&mut hm, "selected_column", TABLE_SELECT_COLUMN);
|
||||
insert_bool(&mut hm, "cursor", TABLE_SELECT_CURSOR);
|
||||
insert_bool(&mut hm, "line_head_top", TABLE_LINE_HEADER_TOP);
|
||||
insert_bool(&mut hm, "line_head_bottom", TABLE_LINE_HEADER_BOTTOM);
|
||||
insert_bool(&mut hm, "line_shift", TABLE_LINE_SHIFT);
|
||||
insert_bool(&mut hm, "line_index", TABLE_LINE_INDEX);
|
||||
|
||||
config.insert(String::from("table"), map_into_value(hm));
|
||||
}
|
||||
|
||||
{
|
||||
let mut hm = config
|
||||
.get("try")
|
||||
.and_then(parse_hash_map)
|
||||
.unwrap_or_default();
|
||||
|
||||
insert_style(&mut hm, "border_color", TRY_BORDER_COLOR);
|
||||
|
||||
config.insert(String::from("try"), map_into_value(hm));
|
||||
}
|
||||
|
||||
{
|
||||
let mut hm = config
|
||||
.get("config")
|
||||
.and_then(parse_hash_map)
|
||||
.unwrap_or_default();
|
||||
|
||||
insert_style(&mut hm, "cursor_color", CONFIG_CURSOR_COLOR);
|
||||
|
||||
config.insert(String::from("config"), map_into_value(hm));
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_hash_map(value: &Value) -> Option<HashMap<String, Value>> {
|
||||
value
|
||||
.as_string()
|
||||
.ok()
|
||||
.and_then(|s| nu_json::from_str::<nu_json::Value>(&s).ok())
|
||||
.map(convert_json_value_into_value)
|
||||
.and_then(|v| create_map(&v))
|
||||
}
|
||||
|
||||
const fn color(foreground: Option<Color>, background: Option<Color>) -> Style {
|
||||
Style {
|
||||
background,
|
||||
foreground,
|
||||
is_blink: false,
|
||||
is_bold: false,
|
||||
is_dimmed: false,
|
||||
is_hidden: false,
|
||||
is_italic: false,
|
||||
is_reverse: false,
|
||||
is_strikethrough: false,
|
||||
is_underline: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_style(map: &mut HashMap<String, Value>, key: &str, value: Style) {
|
||||
if map.contains_key(key) {
|
||||
return;
|
||||
}
|
||||
|
||||
if value == Style::default() {
|
||||
return;
|
||||
}
|
||||
|
||||
let value = nu_color_config::NuStyle::from(value);
|
||||
|
||||
if let Ok(val) = nu_json::to_string(&value) {
|
||||
map.insert(String::from(key), Value::string(val, Span::unknown()));
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_bool(map: &mut HashMap<String, Value>, key: &str, value: bool) {
|
||||
if map.contains_key(key) {
|
||||
return;
|
||||
}
|
||||
|
||||
map.insert(String::from(key), Value::boolean(value, Span::unknown()));
|
||||
}
|
||||
|
||||
fn convert_json_value_into_value(value: nu_json::Value) -> Value {
|
||||
match value {
|
||||
nu_json::Value::Null => Value::nothing(Span::unknown()),
|
||||
nu_json::Value::Bool(val) => Value::boolean(val, Span::unknown()),
|
||||
nu_json::Value::I64(val) => Value::int(val, Span::unknown()),
|
||||
nu_json::Value::U64(val) => Value::int(val as i64, Span::unknown()),
|
||||
nu_json::Value::F64(val) => Value::float(val, Span::unknown()),
|
||||
nu_json::Value::String(val) => Value::string(val, Span::unknown()),
|
||||
nu_json::Value::Array(val) => {
|
||||
let vals = val
|
||||
.into_iter()
|
||||
.map(convert_json_value_into_value)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Value::List {
|
||||
vals,
|
||||
span: Span::unknown(),
|
||||
}
|
||||
}
|
||||
nu_json::Value::Object(val) => {
|
||||
let hm = val
|
||||
.into_iter()
|
||||
.map(|(key, value)| {
|
||||
let val = convert_json_value_into_value(value);
|
||||
(key, val)
|
||||
})
|
||||
.collect();
|
||||
|
||||
map_into_value(hm)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ nu-parser = { path = "../nu-parser", version = "0.72.2" }
|
||||
nu-color-config = { path = "../nu-color-config", version = "0.72.2" }
|
||||
nu-engine = { path = "../nu-engine", version = "0.72.2" }
|
||||
nu-table = { path = "../nu-table", version = "0.72.2" }
|
||||
nu-json = { path="../nu-json", version = "0.72.2" }
|
||||
|
||||
terminal_size = "0.2.1"
|
||||
strip-ansi-escapes = "0.1.1"
|
||||
|
@ -1,252 +0,0 @@
|
||||
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); 3] {
|
||||
[
|
||||
("h", HelpCmd::NAME),
|
||||
("q", QuitCmd::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()
|
||||
}
|
||||
}
|
171
crates/nu-explore/src/commands/config.rs
Normal file
171
crates/nu-explore/src/commands/config.rs
Normal file
@ -0,0 +1,171 @@
|
||||
use std::io::Result;
|
||||
|
||||
use nu_protocol::{
|
||||
engine::{EngineState, Stack},
|
||||
Value,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
nu_common::{nu_str, NuSpan},
|
||||
registry::Command,
|
||||
views::{configuration, ConfigurationView, Preview},
|
||||
};
|
||||
|
||||
use super::{default_color_list, ConfigOption, HelpManual, ViewCommand};
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct ConfigCmd {
|
||||
commands: Vec<Command>,
|
||||
groups: Vec<ConfigOption>,
|
||||
}
|
||||
|
||||
impl ConfigCmd {
|
||||
pub const NAME: &'static str = "config";
|
||||
|
||||
pub fn from_commands(commands: Vec<Command>) -> Self {
|
||||
Self {
|
||||
commands,
|
||||
groups: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register_group(&mut self, group: ConfigOption) {
|
||||
self.groups.push(group);
|
||||
}
|
||||
}
|
||||
|
||||
impl ViewCommand for ConfigCmd {
|
||||
type View = ConfigurationView;
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
Self::NAME
|
||||
}
|
||||
|
||||
fn usage(&self) -> &'static str {
|
||||
""
|
||||
}
|
||||
|
||||
fn help(&self) -> Option<HelpManual> {
|
||||
let config_options = vec![
|
||||
ConfigOption::new(
|
||||
":config options",
|
||||
"A border color of menus",
|
||||
"config.border_color",
|
||||
default_color_list(),
|
||||
),
|
||||
ConfigOption::new(
|
||||
":config options",
|
||||
"Set a color of entries in a list",
|
||||
"config.list_color",
|
||||
default_color_list(),
|
||||
),
|
||||
ConfigOption::new(
|
||||
":config options",
|
||||
"Set a color of a chosen entry in a list",
|
||||
"config.cursor_color",
|
||||
default_color_list(),
|
||||
),
|
||||
];
|
||||
|
||||
Some(HelpManual {
|
||||
name: Self::NAME,
|
||||
description:
|
||||
"Interactive configuration manager.\nCan be used to set various explore settings.\n\nIt could be consired an interactive version of :tweak",
|
||||
config_options,
|
||||
arguments: vec![],
|
||||
examples: vec![],
|
||||
input: vec![],
|
||||
})
|
||||
}
|
||||
|
||||
fn parse(&mut self, _: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn display_config_option(&mut self, _: String, _: String, _: String) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn spawn(
|
||||
&mut self,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
_: Option<Value>,
|
||||
) -> Result<Self::View> {
|
||||
let mut options = vec![];
|
||||
|
||||
let default_table = create_default_value();
|
||||
for cmd in &self.commands {
|
||||
let cmd = match cmd {
|
||||
Command::Reactive(_) => continue,
|
||||
Command::View { cmd, .. } => cmd,
|
||||
};
|
||||
|
||||
let help = match cmd.help() {
|
||||
Some(help) => help,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
for opt in help.config_options {
|
||||
let mut values = vec![];
|
||||
for value in opt.values {
|
||||
let mut cmd = cmd.clone();
|
||||
|
||||
let can_be_displayed = cmd.display_config_option(
|
||||
opt.group.clone(),
|
||||
opt.key.clone(),
|
||||
value.example.to_string(),
|
||||
);
|
||||
let view = if can_be_displayed {
|
||||
cmd.spawn(engine_state, stack, Some(default_table.clone()))?
|
||||
} else {
|
||||
Box::new(Preview::new(&opt.description))
|
||||
};
|
||||
|
||||
let option = configuration::ConfigOption::new(value.example.to_string(), view);
|
||||
values.push(option);
|
||||
}
|
||||
|
||||
let group = configuration::ConfigGroup::new(opt.key, values, opt.description);
|
||||
options.push((opt.group, group));
|
||||
}
|
||||
}
|
||||
|
||||
for opt in &self.groups {
|
||||
let mut values = vec![];
|
||||
for value in &opt.values {
|
||||
let view = Box::new(Preview::new(&opt.description));
|
||||
|
||||
let option = configuration::ConfigOption::new(value.example.to_string(), view);
|
||||
values.push(option);
|
||||
}
|
||||
|
||||
let group =
|
||||
configuration::ConfigGroup::new(opt.key.clone(), values, opt.description.clone());
|
||||
options.push((opt.group.clone(), group));
|
||||
}
|
||||
|
||||
options.sort_by(|(group1, opt1), (group2, opt2)| {
|
||||
group1.cmp(group2).then(opt1.group().cmp(opt2.group()))
|
||||
});
|
||||
|
||||
let options = options.into_iter().map(|(_, opt)| opt).collect();
|
||||
|
||||
Ok(ConfigurationView::new(options))
|
||||
}
|
||||
}
|
||||
|
||||
fn create_default_value() -> Value {
|
||||
let span = NuSpan::unknown();
|
||||
|
||||
let record = |i: usize| Value::Record {
|
||||
cols: vec![String::from("key"), String::from("value")],
|
||||
vals: vec![nu_str(format!("key-{}", i)), nu_str(format!("{}", i))],
|
||||
span,
|
||||
};
|
||||
|
||||
Value::List {
|
||||
vals: vec![record(0), record(1), record(2)],
|
||||
span,
|
||||
}
|
||||
}
|
137
crates/nu-explore/src/commands/config_show.rs
Normal file
137
crates/nu-explore/src/commands/config_show.rs
Normal file
@ -0,0 +1,137 @@
|
||||
use std::io::Result;
|
||||
|
||||
use nu_protocol::{
|
||||
engine::{EngineState, Stack},
|
||||
Value,
|
||||
};
|
||||
use tui::layout::Rect;
|
||||
|
||||
use crate::{
|
||||
nu_common::try_build_table,
|
||||
pager::Frame,
|
||||
util::map_into_value,
|
||||
views::{Layout, Preview, View, ViewConfig},
|
||||
};
|
||||
|
||||
use super::{HelpExample, HelpManual, ViewCommand};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ConfigShowCmd {
|
||||
format: ConfigFormat,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum ConfigFormat {
|
||||
Table,
|
||||
Nu,
|
||||
}
|
||||
|
||||
impl ConfigShowCmd {
|
||||
pub fn new() -> Self {
|
||||
ConfigShowCmd {
|
||||
format: ConfigFormat::Table,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ConfigShowCmd {
|
||||
pub const NAME: &'static str = "config-show";
|
||||
}
|
||||
|
||||
impl ViewCommand for ConfigShowCmd {
|
||||
type View = ConfigView;
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
Self::NAME
|
||||
}
|
||||
|
||||
fn usage(&self) -> &'static str {
|
||||
""
|
||||
}
|
||||
|
||||
fn help(&self) -> Option<HelpManual> {
|
||||
Some(HelpManual {
|
||||
name: Self::NAME,
|
||||
description:
|
||||
"Return a currently used configuration.\nSome default fields might be missing.",
|
||||
arguments: vec![HelpExample::new("nu", "Use a nuon format instead")],
|
||||
config_options: vec![],
|
||||
input: vec![],
|
||||
examples: vec![],
|
||||
})
|
||||
}
|
||||
|
||||
fn display_config_option(&mut self, _: String, _: String, _: String) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn parse(&mut self, args: &str) -> Result<()> {
|
||||
if args.trim() == "nu" {
|
||||
self.format = ConfigFormat::Nu;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn spawn(&mut self, _: &EngineState, _: &mut Stack, _: Option<Value>) -> Result<Self::View> {
|
||||
Ok(ConfigView {
|
||||
preview: Preview::new(""),
|
||||
format: self.format.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ConfigView {
|
||||
preview: Preview,
|
||||
format: ConfigFormat,
|
||||
}
|
||||
|
||||
impl View for ConfigView {
|
||||
fn draw(&mut self, f: &mut Frame, area: Rect, cfg: ViewConfig<'_>, layout: &mut Layout) {
|
||||
self.preview.draw(f, area, cfg, layout)
|
||||
}
|
||||
|
||||
fn handle_input(
|
||||
&mut self,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
layout: &Layout,
|
||||
info: &mut crate::pager::ViewInfo,
|
||||
key: crossterm::event::KeyEvent,
|
||||
) -> Option<crate::pager::Transition> {
|
||||
self.preview
|
||||
.handle_input(engine_state, stack, layout, info, key)
|
||||
}
|
||||
|
||||
fn setup(&mut self, config: ViewConfig<'_>) {
|
||||
let text = self.create_output_string(config);
|
||||
|
||||
self.preview = Preview::new(&text);
|
||||
self.preview
|
||||
.set_value(map_into_value(config.config.clone()));
|
||||
}
|
||||
|
||||
fn exit(&mut self) -> Option<Value> {
|
||||
self.preview.exit()
|
||||
}
|
||||
|
||||
fn collect_data(&self) -> Vec<crate::nu_common::NuText> {
|
||||
self.preview.collect_data()
|
||||
}
|
||||
|
||||
fn show_data(&mut self, i: usize) -> bool {
|
||||
self.preview.show_data(i)
|
||||
}
|
||||
}
|
||||
|
||||
impl ConfigView {
|
||||
fn create_output_string(&mut self, config: ViewConfig) -> String {
|
||||
match self.format {
|
||||
ConfigFormat::Table => {
|
||||
let value = map_into_value(config.config.clone());
|
||||
try_build_table(None, config.nu_config, config.color_hm, value)
|
||||
}
|
||||
ConfigFormat::Nu => nu_json::to_string(&config.config).unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
100
crates/nu-explore/src/commands/expand.rs
Normal file
100
crates/nu-explore/src/commands/expand.rs
Normal file
@ -0,0 +1,100 @@
|
||||
use std::{io::Result, vec};
|
||||
|
||||
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, Shortcode, ViewCommand};
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct ExpandCmd;
|
||||
|
||||
impl ExpandCmd {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl ExpandCmd {
|
||||
pub const NAME: &'static str = "expand";
|
||||
}
|
||||
|
||||
impl ViewCommand for ExpandCmd {
|
||||
type View = Preview;
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
Self::NAME
|
||||
}
|
||||
|
||||
fn usage(&self) -> &'static str {
|
||||
""
|
||||
}
|
||||
|
||||
fn help(&self) -> Option<HelpManual> {
|
||||
#[rustfmt::skip]
|
||||
let shortcodes = vec![
|
||||
Shortcode::new("Up", "", "Moves the viewport one row up"),
|
||||
Shortcode::new("Down", "", "Moves the viewport one row down"),
|
||||
Shortcode::new("Left", "", "Moves the viewport one column left"),
|
||||
Shortcode::new("Right", "", "Moves the viewport one column right"),
|
||||
Shortcode::new("PgDown", "", "Moves the viewport one page of rows down"),
|
||||
Shortcode::new("PgUp", "", "Moves the cursor or viewport one page of rows up"),
|
||||
Shortcode::new("Esc", "", "Exits cursor mode. Exits the currently explored data."),
|
||||
];
|
||||
|
||||
Some(HelpManual {
|
||||
name: "expand",
|
||||
description:
|
||||
"View the currently selected cell's data using the `table` Nushell command",
|
||||
arguments: vec![],
|
||||
examples: vec![],
|
||||
config_options: vec![],
|
||||
input: shortcodes,
|
||||
})
|
||||
}
|
||||
|
||||
fn display_config_option(&mut self, _: String, _: String, _: String) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
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 = value
|
||||
.map(|v| convert_value_to_string(v, engine_state))
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(Preview::new(&value))
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_value_to_string(value: Value, engine_state: &EngineState) -> String {
|
||||
let (cols, vals) = collect_input(value.clone());
|
||||
|
||||
let has_no_head = cols.is_empty() || (cols.len() == 1 && cols[0].is_empty());
|
||||
let has_single_value = vals.len() == 1 && vals[0].len() == 1;
|
||||
if !has_no_head && has_single_value {
|
||||
let config = engine_state.get_config();
|
||||
vals[0][0].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)
|
||||
}
|
||||
}
|
@ -3,19 +3,24 @@ use std::{
|
||||
io::{self, Result},
|
||||
};
|
||||
|
||||
use crossterm::event::KeyEvent;
|
||||
use nu_protocol::{
|
||||
engine::{EngineState, Stack},
|
||||
Value,
|
||||
};
|
||||
use tui::layout::Rect;
|
||||
|
||||
use crate::{nu_common::NuSpan, pager::TableConfig, views::RecordView};
|
||||
use crate::{
|
||||
nu_common::{collect_input, NuSpan},
|
||||
pager::{Frame, Transition, ViewInfo},
|
||||
views::{Layout, Preview, RecordView, View, ViewConfig},
|
||||
};
|
||||
|
||||
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>>,
|
||||
}
|
||||
@ -23,18 +28,39 @@ pub struct HelpCmd {
|
||||
impl HelpCmd {
|
||||
pub const NAME: &'static str = "help";
|
||||
|
||||
pub fn new(
|
||||
commands: Vec<HelpManual>,
|
||||
aliases: &[(&str, &str)],
|
||||
table_cfg: TableConfig,
|
||||
) -> Self {
|
||||
const HELP_MESSAGE: &'static str = r#" Explore - main help file
|
||||
|
||||
Move around: Use the cursor keys.
|
||||
Close this window: Use "<Esc>".
|
||||
Get out of Explore: Use ":q<Enter>" (or <Ctrl> + <D>).
|
||||
|
||||
Get specific help: It is possible to go directly to whatewer you want help on,
|
||||
by adding an argument to the ":help" command.
|
||||
|
||||
Currently you can only get help on a few commands.
|
||||
To obtain a list of supported commands run ":help :<Enter>"
|
||||
|
||||
------------------------------------------------------------------------------------
|
||||
|
||||
Regular expressions ~
|
||||
|
||||
Most commands support regular expressions.
|
||||
|
||||
You can type "/" and type a pattern you want to search on.
|
||||
Then hit <Enter> and you will see the search results.
|
||||
|
||||
To go to the next hit use "<n>" key.
|
||||
|
||||
You also can do a reverse search by using "?" instead of "/".
|
||||
"#;
|
||||
|
||||
pub fn new(commands: Vec<HelpManual>, aliases: &[(&str, &str)]) -> Self {
|
||||
let aliases = collect_aliases(aliases);
|
||||
|
||||
Self {
|
||||
input_command: String::new(),
|
||||
supported_commands: commands,
|
||||
aliases,
|
||||
table_cfg,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -51,7 +77,7 @@ fn collect_aliases(aliases: &[(&str, &str)]) -> HashMap<String, Vec<String>> {
|
||||
}
|
||||
|
||||
impl ViewCommand for HelpCmd {
|
||||
type View = RecordView<'static>;
|
||||
type View = HelpView<'static>;
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
Self::NAME
|
||||
@ -62,27 +88,32 @@ impl ViewCommand for HelpCmd {
|
||||
}
|
||||
|
||||
fn help(&self) -> Option<HelpManual> {
|
||||
#[rustfmt::skip]
|
||||
let examples = vec![
|
||||
HelpExample::new("help", "Open the help page for all of `explore`"),
|
||||
HelpExample::new("help :nu", "Open the help page for the `nu` explore command"),
|
||||
HelpExample::new("help :help", "...It was supposed to be hidden....until...now..."),
|
||||
];
|
||||
|
||||
#[rustfmt::skip]
|
||||
let arguments = vec![
|
||||
HelpExample::new("help :command", "you can provide a command and a help information for it will be displayed")
|
||||
];
|
||||
|
||||
Some(HelpManual {
|
||||
name: "help",
|
||||
description: "Explore the help page for `explore`",
|
||||
arguments: vec![],
|
||||
examples: vec![
|
||||
HelpExample {
|
||||
example: "help",
|
||||
description: "Open the help page for all of `explore`",
|
||||
},
|
||||
HelpExample {
|
||||
example: "help nu",
|
||||
description: "Open the help page for the `nu` explore command",
|
||||
},
|
||||
HelpExample {
|
||||
example: "help help",
|
||||
description: "...It was supposed to be hidden....until...now...",
|
||||
},
|
||||
],
|
||||
arguments,
|
||||
examples,
|
||||
input: vec![],
|
||||
config_options: vec![],
|
||||
})
|
||||
}
|
||||
|
||||
fn display_config_option(&mut self, _: String, _: String, _: String) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn parse(&mut self, args: &str) -> Result<()> {
|
||||
self.input_command = args.trim().to_owned();
|
||||
|
||||
@ -91,15 +122,31 @@ impl ViewCommand for HelpCmd {
|
||||
|
||||
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);
|
||||
return Ok(HelpView::Preview(Preview::new(Self::HELP_MESSAGE)));
|
||||
}
|
||||
|
||||
if !self.input_command.starts_with(':') {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"unexpected help argument",
|
||||
));
|
||||
}
|
||||
|
||||
if self.input_command == ":" {
|
||||
let (headers, data) = help_frame_data(&self.supported_commands, &self.aliases);
|
||||
let view = RecordView::new(headers, data);
|
||||
return Ok(HelpView::Records(view));
|
||||
}
|
||||
|
||||
let command = self
|
||||
.input_command
|
||||
.strip_prefix(':')
|
||||
.expect("we just checked the prefix");
|
||||
|
||||
let manual = self
|
||||
.supported_commands
|
||||
.iter()
|
||||
.find(|manual| manual.name == self.input_command)
|
||||
.find(|manual| manual.name == command)
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "a given command was not found"))?;
|
||||
|
||||
let aliases = self
|
||||
@ -108,9 +155,9 @@ impl ViewCommand for HelpCmd {
|
||||
.map(|l| l.as_slice())
|
||||
.unwrap_or(&[]);
|
||||
let (headers, data) = help_manual_data(manual, aliases);
|
||||
let view = RecordView::new(headers, data, self.table_cfg);
|
||||
let view = RecordView::new(headers, data);
|
||||
|
||||
Ok(view)
|
||||
Ok(HelpView::Records(view))
|
||||
}
|
||||
}
|
||||
|
||||
@ -118,20 +165,6 @@ 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($text.to_string(), NuSpan::unknown())
|
||||
};
|
||||
}
|
||||
|
||||
let commands = supported_commands
|
||||
.iter()
|
||||
.map(|manual| {
|
||||
@ -154,41 +187,13 @@ fn help_frame_data(
|
||||
span: NuSpan::unknown(),
|
||||
};
|
||||
|
||||
let headers = vec!["name", "mode", "information", "description"];
|
||||
|
||||
#[rustfmt::skip]
|
||||
let shortcuts = [
|
||||
(":", "view", commands, "Run an explore command (explore the 'information' cell of this row to list commands)"),
|
||||
("/", "view", null!(), "Search for a pattern"),
|
||||
("?", "view", null!(), "Search for a pattern, but the <n> key now scrolls to the previous result"),
|
||||
("n", "view", null!(), "When searching, scroll to the next search result"),
|
||||
("i", "view", null!(), "Enters cursor mode to inspect individual cells"),
|
||||
("t", "view", null!(), "Transpose table, so that columns become rows and vice versa"),
|
||||
("Up", "", null!(), "Moves the cursor or viewport one row up"),
|
||||
("Down", "", null!(), "Moves the cursor or viewport one row down"),
|
||||
("Left", "", null!(), "Moves the cursor or viewport one column left"),
|
||||
("Right", "", null!(), "Moves the cursor or viewport one column right"),
|
||||
("PgDown", "view", null!(), "Moves the cursor or viewport one page of rows down"),
|
||||
("PgUp", "view", null!(), "Moves the cursor or viewport one page of rows up"),
|
||||
("Esc", "", null!(), "Exits cursor mode. Exits the currently explored data."),
|
||||
("Enter", "cursor", null!(), "In cursor mode, explore the data of the selected cell"),
|
||||
];
|
||||
|
||||
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)
|
||||
collect_input(commands)
|
||||
}
|
||||
|
||||
fn help_manual_data(manual: &HelpManual, aliases: &[String]) -> (Vec<String>, Vec<Vec<Value>>) {
|
||||
macro_rules! nu_str {
|
||||
($text:expr) => {
|
||||
Value::string($text, NuSpan::unknown())
|
||||
Value::string($text.to_string(), NuSpan::unknown())
|
||||
};
|
||||
}
|
||||
|
||||
@ -216,12 +221,69 @@ fn help_manual_data(manual: &HelpManual, aliases: &[String]) -> (Vec<String>, Ve
|
||||
span: NuSpan::unknown(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let examples = Value::List {
|
||||
vals: examples,
|
||||
span: NuSpan::unknown(),
|
||||
};
|
||||
|
||||
let inputs = manual
|
||||
.input
|
||||
.iter()
|
||||
.map(|e| Value::Record {
|
||||
cols: vec![
|
||||
String::from("name"),
|
||||
String::from("context"),
|
||||
String::from("description"),
|
||||
],
|
||||
vals: vec![nu_str!(e.code), nu_str!(e.context), nu_str!(e.description)],
|
||||
span: NuSpan::unknown(),
|
||||
})
|
||||
.collect();
|
||||
let inputs = Value::List {
|
||||
vals: inputs,
|
||||
span: NuSpan::unknown(),
|
||||
};
|
||||
|
||||
let configuration = manual
|
||||
.config_options
|
||||
.iter()
|
||||
.map(|o| {
|
||||
let values = o
|
||||
.values
|
||||
.iter()
|
||||
.map(|v| Value::Record {
|
||||
cols: vec![String::from("example"), String::from("description")],
|
||||
vals: vec![nu_str!(v.example), nu_str!(v.description)],
|
||||
span: NuSpan::unknown(),
|
||||
})
|
||||
.collect();
|
||||
let values = Value::List {
|
||||
vals: values,
|
||||
span: NuSpan::unknown(),
|
||||
};
|
||||
|
||||
Value::Record {
|
||||
cols: vec![
|
||||
String::from("name"),
|
||||
String::from("context"),
|
||||
String::from("description"),
|
||||
String::from("values"),
|
||||
],
|
||||
vals: vec![
|
||||
nu_str!(o.group),
|
||||
nu_str!(o.key),
|
||||
nu_str!(o.description),
|
||||
values,
|
||||
],
|
||||
span: NuSpan::unknown(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let configuration = Value::List {
|
||||
vals: configuration,
|
||||
span: NuSpan::unknown(),
|
||||
};
|
||||
|
||||
let name = nu_str!(manual.name);
|
||||
let aliases = nu_str!(aliases.join(", "));
|
||||
let desc = nu_str!(manual.description);
|
||||
@ -230,11 +292,76 @@ fn help_manual_data(manual: &HelpManual, aliases: &[String]) -> (Vec<String>, Ve
|
||||
String::from("name"),
|
||||
String::from("aliases"),
|
||||
String::from("arguments"),
|
||||
String::from("input"),
|
||||
String::from("examples"),
|
||||
String::from("configuration"),
|
||||
String::from("description"),
|
||||
];
|
||||
|
||||
let data = vec![vec![name, aliases, arguments, examples, desc]];
|
||||
let data = vec![vec![
|
||||
name,
|
||||
aliases,
|
||||
arguments,
|
||||
inputs,
|
||||
examples,
|
||||
configuration,
|
||||
desc,
|
||||
]];
|
||||
|
||||
(headers, data)
|
||||
}
|
||||
pub enum HelpView<'a> {
|
||||
Records(RecordView<'a>),
|
||||
Preview(Preview),
|
||||
}
|
||||
|
||||
impl View for HelpView<'_> {
|
||||
fn draw(&mut self, f: &mut Frame, area: Rect, cfg: ViewConfig<'_>, layout: &mut Layout) {
|
||||
match self {
|
||||
HelpView::Records(v) => v.draw(f, area, cfg, layout),
|
||||
HelpView::Preview(v) => v.draw(f, area, cfg, layout),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_input(
|
||||
&mut self,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
layout: &Layout,
|
||||
info: &mut ViewInfo,
|
||||
key: KeyEvent,
|
||||
) -> Option<Transition> {
|
||||
match self {
|
||||
HelpView::Records(v) => v.handle_input(engine_state, stack, layout, info, key),
|
||||
HelpView::Preview(v) => v.handle_input(engine_state, stack, layout, info, key),
|
||||
}
|
||||
}
|
||||
|
||||
fn show_data(&mut self, i: usize) -> bool {
|
||||
match self {
|
||||
HelpView::Records(v) => v.show_data(i),
|
||||
HelpView::Preview(v) => v.show_data(i),
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_data(&self) -> Vec<crate::nu_common::NuText> {
|
||||
match self {
|
||||
HelpView::Records(v) => v.collect_data(),
|
||||
HelpView::Preview(v) => v.collect_data(),
|
||||
}
|
||||
}
|
||||
|
||||
fn exit(&mut self) -> Option<Value> {
|
||||
match self {
|
||||
HelpView::Records(v) => v.exit(),
|
||||
HelpView::Preview(v) => v.exit(),
|
||||
}
|
||||
}
|
||||
|
||||
fn setup(&mut self, config: ViewConfig<'_>) {
|
||||
match self {
|
||||
HelpView::Records(v) => v.setup(config),
|
||||
HelpView::Preview(v) => v.setup(config),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,19 +5,27 @@ use nu_protocol::{
|
||||
|
||||
use super::pager::{Pager, Transition};
|
||||
|
||||
use std::io::Result;
|
||||
use std::{borrow::Cow, io::Result};
|
||||
|
||||
mod expand;
|
||||
mod help;
|
||||
mod nu;
|
||||
mod preview;
|
||||
mod quit;
|
||||
mod table;
|
||||
mod r#try;
|
||||
mod tweak;
|
||||
|
||||
pub mod config;
|
||||
mod config_show;
|
||||
|
||||
pub use config_show::ConfigShowCmd;
|
||||
pub use expand::ExpandCmd;
|
||||
pub use help::HelpCmd;
|
||||
pub use nu::NuCmd;
|
||||
pub use preview::PreviewCmd;
|
||||
pub use quit::QuitCmd;
|
||||
pub use r#try::TryCmd;
|
||||
pub use table::TableCmd;
|
||||
pub use tweak::TweakCmd;
|
||||
|
||||
pub trait SimpleCommand {
|
||||
fn name(&self) -> &'static str;
|
||||
@ -48,6 +56,8 @@ pub trait ViewCommand {
|
||||
|
||||
fn parse(&mut self, args: &str) -> Result<()>;
|
||||
|
||||
fn display_config_option(&mut self, group: String, key: String, value: String) -> bool;
|
||||
|
||||
fn spawn(
|
||||
&mut self,
|
||||
engine_state: &EngineState,
|
||||
@ -62,10 +72,110 @@ pub struct HelpManual {
|
||||
pub description: &'static str,
|
||||
pub arguments: Vec<HelpExample>,
|
||||
pub examples: Vec<HelpExample>,
|
||||
pub config_options: Vec<ConfigOption>,
|
||||
pub input: Vec<Shortcode>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct HelpExample {
|
||||
pub example: &'static str,
|
||||
pub example: Cow<'static, str>,
|
||||
pub description: Cow<'static, str>,
|
||||
}
|
||||
|
||||
impl HelpExample {
|
||||
pub fn new(
|
||||
example: impl Into<Cow<'static, str>>,
|
||||
description: impl Into<Cow<'static, str>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
example: example.into(),
|
||||
description: description.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Shortcode {
|
||||
pub code: &'static str,
|
||||
pub context: &'static str,
|
||||
pub description: &'static str,
|
||||
}
|
||||
|
||||
impl Shortcode {
|
||||
pub fn new(code: &'static str, context: &'static str, description: &'static str) -> Self {
|
||||
Self {
|
||||
code,
|
||||
context,
|
||||
description,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct ConfigOption {
|
||||
pub group: String,
|
||||
pub description: String,
|
||||
pub key: String,
|
||||
pub values: Vec<HelpExample>,
|
||||
}
|
||||
|
||||
impl ConfigOption {
|
||||
pub fn new<N, D, K>(group: N, description: D, key: K, values: Vec<HelpExample>) -> Self
|
||||
where
|
||||
N: Into<String>,
|
||||
D: Into<String>,
|
||||
K: Into<String>,
|
||||
{
|
||||
Self {
|
||||
group: group.into(),
|
||||
description: description.into(),
|
||||
key: key.into(),
|
||||
values,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn boolean<N, D, K>(group: N, description: D, key: K) -> Self
|
||||
where
|
||||
N: Into<String>,
|
||||
D: Into<String>,
|
||||
K: Into<String>,
|
||||
{
|
||||
Self {
|
||||
group: group.into(),
|
||||
description: description.into(),
|
||||
key: key.into(),
|
||||
values: vec![
|
||||
HelpExample::new("true", "Turn the flag on"),
|
||||
HelpExample::new("false", "Turn the flag on"),
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
pub fn default_color_list() -> Vec<HelpExample> {
|
||||
vec![
|
||||
HelpExample::new("red", "Red foreground"),
|
||||
HelpExample::new("blue", "Blue foreground"),
|
||||
HelpExample::new("green", "Green foreground"),
|
||||
HelpExample::new("yellow", "Yellow foreground"),
|
||||
HelpExample::new("magenta", "Magenta foreground"),
|
||||
HelpExample::new("black", "Black foreground"),
|
||||
HelpExample::new("white", "White foreground"),
|
||||
HelpExample::new("#AA4433", "#AA4433 HEX foreground"),
|
||||
HelpExample::new(r#"{bg: "red"}"#, "Red background"),
|
||||
HelpExample::new(r#"{bg: "blue"}"#, "Blue background"),
|
||||
HelpExample::new(r#"{bg: "green"}"#, "Green background"),
|
||||
HelpExample::new(r#"{bg: "yellow"}"#, "Yellow background"),
|
||||
HelpExample::new(r#"{bg: "magenta"}"#, "Magenta background"),
|
||||
HelpExample::new(r#"{bg: "black"}"#, "Black background"),
|
||||
HelpExample::new(r#"{bg: "white"}"#, "White background"),
|
||||
HelpExample::new(r##"{bg: "#AA4433"}"##, "#AA4433 HEX background"),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn default_int_list() -> Vec<HelpExample> {
|
||||
(0..20)
|
||||
.map(|i| HelpExample::new(i.to_string(), format!("A value equal to {}", i)))
|
||||
.collect()
|
||||
}
|
||||
|
@ -4,11 +4,12 @@ use nu_protocol::{
|
||||
engine::{EngineState, Stack},
|
||||
PipelineData, Value,
|
||||
};
|
||||
use tui::layout::Rect;
|
||||
|
||||
use crate::{
|
||||
nu_common::{collect_pipeline, has_simple_value, is_ignored_command, run_nu_command},
|
||||
pager::TableConfig,
|
||||
views::{Preview, RecordView, View},
|
||||
nu_common::{collect_pipeline, has_simple_value, run_command_with_value},
|
||||
pager::Frame,
|
||||
views::{Layout, Orientation, Preview, RecordView, View, ViewConfig},
|
||||
};
|
||||
|
||||
use super::{HelpExample, HelpManual, ViewCommand};
|
||||
@ -16,14 +17,12 @@ 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 {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
command: String::new(),
|
||||
table_cfg,
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,28 +41,33 @@ impl ViewCommand for NuCmd {
|
||||
}
|
||||
|
||||
fn help(&self) -> Option<HelpManual> {
|
||||
let examples = vec![
|
||||
HelpExample::new(
|
||||
"where type == 'file'",
|
||||
"Filter data to show only rows whose type is 'file'",
|
||||
),
|
||||
HelpExample::new(
|
||||
"get scope.examples",
|
||||
"Navigate to a deeper value inside the data",
|
||||
),
|
||||
HelpExample::new("open Cargo.toml", "Open a Cargo.toml file"),
|
||||
];
|
||||
|
||||
Some(HelpManual {
|
||||
name: "nu",
|
||||
description:
|
||||
"Run a Nushell command. The data currently being explored is piped into it.",
|
||||
examples,
|
||||
arguments: vec![],
|
||||
examples: vec![
|
||||
HelpExample {
|
||||
example: "where type == 'file'",
|
||||
description: "Filter data to show only rows whose type is 'file'",
|
||||
},
|
||||
HelpExample {
|
||||
example: "get scope.examples",
|
||||
description: "Navigate to a deeper value inside the data",
|
||||
},
|
||||
HelpExample {
|
||||
example: "open Cargo.toml",
|
||||
description: "Open a Cargo.toml file",
|
||||
},
|
||||
],
|
||||
input: vec![],
|
||||
config_options: vec![],
|
||||
})
|
||||
}
|
||||
|
||||
fn display_config_option(&mut self, _: String, _: String, _: String) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn parse(&mut self, args: &str) -> Result<()> {
|
||||
self.command = args.trim().to_owned();
|
||||
|
||||
@ -76,28 +80,25 @@ impl ViewCommand for NuCmd {
|
||||
stack: &mut Stack,
|
||||
value: Option<Value>,
|
||||
) -> Result<Self::View> {
|
||||
if is_ignored_command(&self.command) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"The command is ignored",
|
||||
));
|
||||
}
|
||||
|
||||
let value = value.unwrap_or_default();
|
||||
|
||||
let pipeline = PipelineData::Value(value, None);
|
||||
let pipeline = run_nu_command(engine_state, stack, &self.command, pipeline)
|
||||
let pipeline = run_command_with_value(&self.command, &value, engine_state, stack)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
|
||||
|
||||
let is_record = matches!(pipeline, PipelineData::Value(Value::Record { .. }, ..));
|
||||
|
||||
let (columns, values) = collect_pipeline(pipeline);
|
||||
|
||||
if has_simple_value(&values) {
|
||||
let config = &engine_state.config;
|
||||
let text = values[0][0].into_abbreviated_string(config);
|
||||
if let Some(value) = has_simple_value(&values) {
|
||||
let text = value.into_abbreviated_string(&engine_state.config);
|
||||
return Ok(NuView::Preview(Preview::new(&text)));
|
||||
}
|
||||
|
||||
let view = RecordView::new(columns, values, self.table_cfg);
|
||||
let mut view = RecordView::new(columns, values);
|
||||
|
||||
if is_record {
|
||||
view.set_orientation_current(Orientation::Left);
|
||||
}
|
||||
|
||||
Ok(NuView::Records(view))
|
||||
}
|
||||
@ -109,13 +110,7 @@ pub enum NuView<'a> {
|
||||
}
|
||||
|
||||
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,
|
||||
) {
|
||||
fn draw(&mut self, f: &mut Frame, area: Rect, cfg: ViewConfig<'_>, layout: &mut Layout) {
|
||||
match self {
|
||||
NuView::Records(v) => v.draw(f, area, cfg, layout),
|
||||
NuView::Preview(v) => v.draw(f, area, cfg, layout),
|
||||
@ -126,7 +121,7 @@ impl View for NuView<'_> {
|
||||
&mut self,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
layout: &crate::views::Layout,
|
||||
layout: &Layout,
|
||||
info: &mut crate::pager::ViewInfo,
|
||||
key: crossterm::event::KeyEvent,
|
||||
) -> Option<crate::pager::Transition> {
|
||||
@ -156,4 +151,11 @@ impl View for NuView<'_> {
|
||||
NuView::Preview(v) => v.exit(),
|
||||
}
|
||||
}
|
||||
|
||||
fn setup(&mut self, config: ViewConfig<'_>) {
|
||||
match self {
|
||||
NuView::Records(v) => v.setup(config),
|
||||
NuView::Preview(v) => v.setup(config),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,82 +0,0 @@
|
||||
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:
|
||||
"View the currently selected cell's data using the `table` Nushell command",
|
||||
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))
|
||||
}
|
||||
}
|
@ -31,6 +31,8 @@ impl SimpleCommand for QuitCmd {
|
||||
description: "Quit and return to Nushell",
|
||||
arguments: vec![],
|
||||
examples: vec![],
|
||||
input: vec![],
|
||||
config_options: vec![],
|
||||
})
|
||||
}
|
||||
|
||||
|
281
crates/nu-explore/src/commands/table.rs
Normal file
281
crates/nu-explore/src/commands/table.rs
Normal file
@ -0,0 +1,281 @@
|
||||
use std::io::Result;
|
||||
|
||||
use nu_ansi_term::Style;
|
||||
use nu_color_config::lookup_ansi_color_style;
|
||||
use nu_protocol::{
|
||||
engine::{EngineState, Stack},
|
||||
Value,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
nu_common::collect_input,
|
||||
views::{Orientation, RecordView},
|
||||
};
|
||||
|
||||
use super::{
|
||||
default_color_list, default_int_list, ConfigOption, HelpExample, HelpManual, Shortcode,
|
||||
ViewCommand,
|
||||
};
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct TableCmd {
|
||||
// todo: add arguments to override config right from CMD
|
||||
settings: TableSettings,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
struct TableSettings {
|
||||
orientation: Option<Orientation>,
|
||||
line_head_top: Option<bool>,
|
||||
line_head_bottom: Option<bool>,
|
||||
line_shift: Option<bool>,
|
||||
line_index: Option<bool>,
|
||||
split_line_s: Option<Style>,
|
||||
selected_cell_s: Option<Style>,
|
||||
selected_row_s: Option<Style>,
|
||||
selected_column_s: Option<Style>,
|
||||
show_cursor: Option<bool>,
|
||||
padding_column_left: Option<usize>,
|
||||
padding_column_right: Option<usize>,
|
||||
padding_index_left: Option<usize>,
|
||||
padding_index_right: Option<usize>,
|
||||
turn_on_cursor_mode: bool,
|
||||
}
|
||||
|
||||
impl TableCmd {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub const NAME: &'static str = "table";
|
||||
}
|
||||
|
||||
impl ViewCommand for TableCmd {
|
||||
type View = RecordView<'static>;
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
Self::NAME
|
||||
}
|
||||
|
||||
fn usage(&self) -> &'static str {
|
||||
""
|
||||
}
|
||||
|
||||
fn help(&self) -> Option<HelpManual> {
|
||||
#[rustfmt::skip]
|
||||
let shortcuts = vec![
|
||||
Shortcode::new("Up", "", "Moves the cursor or viewport one row up"),
|
||||
Shortcode::new("Down", "", "Moves the cursor or viewport one row down"),
|
||||
Shortcode::new("Left", "", "Moves the cursor or viewport one column left"),
|
||||
Shortcode::new("Right", "", "Moves the cursor or viewport one column right"),
|
||||
Shortcode::new("PgDown", "view", "Moves the cursor or viewport one page of rows down"),
|
||||
Shortcode::new("PgUp", "view", "Moves the cursor or viewport one page of rows up"),
|
||||
Shortcode::new("Esc", "", "Exits cursor mode. Exits the just explored dataset."),
|
||||
Shortcode::new("i", "view", "Enters cursor mode to inspect individual cells"),
|
||||
Shortcode::new("t", "view", "Transpose table, so that columns become rows and vice versa"),
|
||||
Shortcode::new("e", "view", "Open expand view (equvalent of :expand)"),
|
||||
Shortcode::new("Enter", "cursor", "In cursor mode, explore the data of the selected cell"),
|
||||
];
|
||||
|
||||
#[rustfmt::skip]
|
||||
let config_options = vec![
|
||||
ConfigOption::new(
|
||||
":table group",
|
||||
"Used to move column header",
|
||||
"table.orientation",
|
||||
vec![
|
||||
HelpExample::new("top", "Sticks column header to the top"),
|
||||
HelpExample::new("bottom", "Sticks column header to the bottom"),
|
||||
HelpExample::new("left", "Sticks column header to the left"),
|
||||
HelpExample::new("right", "Sticks column header to the right"),
|
||||
],
|
||||
),
|
||||
ConfigOption::boolean(":table group", "Show index", "table.show_index"),
|
||||
ConfigOption::boolean(":table group", "Show header", "table.show_head"),
|
||||
|
||||
ConfigOption::boolean(":table group", "Lines are lines", "table.line_head_top"),
|
||||
ConfigOption::boolean(":table group", "Lines are lines", "table.line_head_bottom"),
|
||||
ConfigOption::boolean(":table group", "Lines are lines", "table.line_shift"),
|
||||
ConfigOption::boolean(":table group", "Lines are lines", "table.line_index"),
|
||||
|
||||
ConfigOption::boolean(":table group", "Show cursor", "table.show_cursor"),
|
||||
|
||||
ConfigOption::new(":table group", "Color of selected cell", "table.selected_cell", default_color_list()),
|
||||
ConfigOption::new(":table group", "Color of selected row", "table.selected_row", default_color_list()),
|
||||
ConfigOption::new(":table group", "Color of selected column", "table.selected_column", default_color_list()),
|
||||
|
||||
ConfigOption::new(":table group", "Color of split line", "table.split_line", default_color_list()),
|
||||
|
||||
ConfigOption::new(":table group", "Padding column left", "table.padding_column_left", default_int_list()),
|
||||
ConfigOption::new(":table group", "Padding column right", "table.padding_column_right", default_int_list()),
|
||||
ConfigOption::new(":table group", "Padding index left", "table.padding_index_left", default_int_list()),
|
||||
ConfigOption::new(":table group", "Padding index right", "table.padding_index_right", default_int_list()),
|
||||
];
|
||||
|
||||
Some(HelpManual {
|
||||
name: "table",
|
||||
description: "Display a table view",
|
||||
arguments: vec![],
|
||||
examples: vec![],
|
||||
config_options,
|
||||
input: shortcuts,
|
||||
})
|
||||
}
|
||||
|
||||
fn display_config_option(&mut self, _group: String, key: String, value: String) -> bool {
|
||||
match key.as_str() {
|
||||
"table.orientation" => self.settings.orientation = orientation_from_str(&value),
|
||||
"table.line_head_top" => self.settings.line_head_top = bool_from_str(&value),
|
||||
"table.line_head_bottom" => self.settings.line_head_bottom = bool_from_str(&value),
|
||||
"table.line_shift" => self.settings.line_shift = bool_from_str(&value),
|
||||
"table.line_index" => self.settings.line_index = bool_from_str(&value),
|
||||
"table.show_cursor" => {
|
||||
self.settings.show_cursor = bool_from_str(&value);
|
||||
self.settings.turn_on_cursor_mode = true;
|
||||
}
|
||||
"table.split_line" => {
|
||||
self.settings.split_line_s = Some(lookup_ansi_color_style(&value));
|
||||
self.settings.turn_on_cursor_mode = true;
|
||||
}
|
||||
"table.selected_cell" => {
|
||||
self.settings.selected_cell_s = Some(lookup_ansi_color_style(&value));
|
||||
self.settings.turn_on_cursor_mode = true;
|
||||
}
|
||||
"table.selected_row" => {
|
||||
self.settings.selected_row_s = Some(lookup_ansi_color_style(&value));
|
||||
self.settings.turn_on_cursor_mode = true;
|
||||
}
|
||||
"table.selected_column" => {
|
||||
self.settings.selected_column_s = Some(lookup_ansi_color_style(&value));
|
||||
self.settings.turn_on_cursor_mode = true;
|
||||
}
|
||||
"table.padding_column_left" => {
|
||||
self.settings.padding_column_left = usize_from_str(&value);
|
||||
}
|
||||
"table.padding_column_right" => {
|
||||
self.settings.padding_column_right = usize_from_str(&value);
|
||||
}
|
||||
"table.padding_index_left" => {
|
||||
self.settings.padding_index_left = usize_from_str(&value);
|
||||
}
|
||||
"table.padding_index_right" => {
|
||||
self.settings.padding_index_right = usize_from_str(&value);
|
||||
}
|
||||
_ => return false,
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn parse(&mut self, _: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn spawn(
|
||||
&mut self,
|
||||
_: &EngineState,
|
||||
_: &mut Stack,
|
||||
value: Option<Value>,
|
||||
) -> Result<Self::View> {
|
||||
let value = value.unwrap_or_default();
|
||||
let is_record = matches!(value, Value::Record { .. });
|
||||
|
||||
let (columns, data) = collect_input(value);
|
||||
|
||||
let mut view = RecordView::new(columns, data);
|
||||
|
||||
// todo: use setup instead ????
|
||||
|
||||
if is_record {
|
||||
view.set_orientation_current(Orientation::Left);
|
||||
}
|
||||
|
||||
if let Some(o) = self.settings.orientation {
|
||||
view.set_orientation_current(o);
|
||||
}
|
||||
|
||||
if self.settings.line_head_bottom.unwrap_or(false) {
|
||||
view.set_line_head_bottom(true);
|
||||
}
|
||||
|
||||
if self.settings.line_head_top.unwrap_or(false) {
|
||||
view.set_line_head_top(true);
|
||||
}
|
||||
|
||||
if self.settings.line_index.unwrap_or(false) {
|
||||
view.set_line_index(true);
|
||||
}
|
||||
|
||||
if self.settings.line_shift.unwrap_or(false) {
|
||||
view.set_line_traling(true);
|
||||
}
|
||||
|
||||
if self.settings.show_cursor.unwrap_or(false) {
|
||||
view.show_cursor(true);
|
||||
}
|
||||
|
||||
if let Some(style) = self.settings.selected_cell_s {
|
||||
view.set_style_selected_cell(style);
|
||||
}
|
||||
|
||||
if let Some(style) = self.settings.selected_column_s {
|
||||
view.set_style_selected_column(style);
|
||||
}
|
||||
|
||||
if let Some(style) = self.settings.selected_row_s {
|
||||
view.set_style_selected_row(style);
|
||||
}
|
||||
|
||||
if let Some(style) = self.settings.split_line_s {
|
||||
view.set_style_split_line(style);
|
||||
}
|
||||
|
||||
if let Some(p) = self.settings.padding_column_left {
|
||||
let c = view.get_padding_column();
|
||||
view.set_padding_column((p, c.1))
|
||||
}
|
||||
|
||||
if let Some(p) = self.settings.padding_column_right {
|
||||
let c = view.get_padding_column();
|
||||
view.set_padding_column((c.0, p))
|
||||
}
|
||||
|
||||
if let Some(p) = self.settings.padding_index_left {
|
||||
let c = view.get_padding_index();
|
||||
view.set_padding_index((p, c.1))
|
||||
}
|
||||
|
||||
if let Some(p) = self.settings.padding_index_right {
|
||||
let c = view.get_padding_index();
|
||||
view.set_padding_index((c.0, p))
|
||||
}
|
||||
|
||||
if self.settings.turn_on_cursor_mode {
|
||||
view.set_cursor_mode();
|
||||
}
|
||||
|
||||
Ok(view)
|
||||
}
|
||||
}
|
||||
|
||||
fn bool_from_str(s: &str) -> Option<bool> {
|
||||
match s {
|
||||
"true" => Some(true),
|
||||
"false" => Some(false),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn usize_from_str(s: &str) -> Option<usize> {
|
||||
s.parse::<usize>().ok()
|
||||
}
|
||||
|
||||
fn orientation_from_str(s: &str) -> Option<Orientation> {
|
||||
match s {
|
||||
"left" => Some(Orientation::Left),
|
||||
"right" => Some(Orientation::Right),
|
||||
"top" => Some(Orientation::Top),
|
||||
"bottom" => Some(Orientation::Bottom),
|
||||
_ => None,
|
||||
}
|
||||
}
|
@ -1,25 +1,23 @@
|
||||
use std::io::Result;
|
||||
use std::io::{Error, ErrorKind, Result};
|
||||
|
||||
use nu_protocol::{
|
||||
engine::{EngineState, Stack},
|
||||
Value,
|
||||
};
|
||||
|
||||
use crate::{pager::TableConfig, views::InteractiveView};
|
||||
use crate::views::InteractiveView;
|
||||
|
||||
use super::{HelpExample, HelpManual, ViewCommand};
|
||||
use super::{default_color_list, ConfigOption, HelpExample, HelpManual, Shortcode, ViewCommand};
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct TryCmd {
|
||||
command: String,
|
||||
table_cfg: TableConfig,
|
||||
}
|
||||
|
||||
impl TryCmd {
|
||||
pub fn new(table_cfg: TableConfig) -> Self {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
command: String::new(),
|
||||
table_cfg,
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,17 +36,41 @@ impl ViewCommand for TryCmd {
|
||||
}
|
||||
|
||||
fn help(&self) -> Option<HelpManual> {
|
||||
#[rustfmt::skip]
|
||||
let shortcuts = vec![
|
||||
Shortcode::new("Up", "", "Switches between input and a output panes"),
|
||||
Shortcode::new("Down", "", "Switches between input and a output panes"),
|
||||
Shortcode::new("Esc", "", "Switches between input and a output panes"),
|
||||
Shortcode::new("Tab", "", "Switches between input and a output panes"),
|
||||
];
|
||||
|
||||
#[rustfmt::skip]
|
||||
let config_options = vec![
|
||||
ConfigOption::boolean(":try options", "Try makes running command on each input character", "try.reactive"),
|
||||
ConfigOption::new(":try options", "Change a border color of the menus", "try.border_color", default_color_list()),
|
||||
ConfigOption::new(":try options", "Change a highlighed menu color", "try.highlighted_color", default_color_list()),
|
||||
];
|
||||
|
||||
#[rustfmt::skip]
|
||||
let examples = vec![
|
||||
HelpExample::new("try", "Open a interactive :try command"),
|
||||
HelpExample::new("try open Cargo.toml", "Optionally, you can provide a command which will be run immediately"),
|
||||
];
|
||||
|
||||
Some(HelpManual {
|
||||
name: "try",
|
||||
description: "Opens a panel in which to run Nushell commands and explore their output",
|
||||
description: "Opens a panel in which to run Nushell commands and explore their output. The exporer acts liek `:table`.",
|
||||
arguments: vec![],
|
||||
examples: vec![HelpExample {
|
||||
example: "try open Cargo.toml",
|
||||
description: "Optionally, you can provide a command which will be run immediately",
|
||||
}],
|
||||
examples,
|
||||
input: shortcuts,
|
||||
config_options,
|
||||
})
|
||||
}
|
||||
|
||||
fn display_config_option(&mut self, _: String, _: String, _: String) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn parse(&mut self, args: &str) -> Result<()> {
|
||||
self.command = args.trim().to_owned();
|
||||
|
||||
@ -57,13 +79,15 @@ impl ViewCommand for TryCmd {
|
||||
|
||||
fn spawn(
|
||||
&mut self,
|
||||
_: &EngineState,
|
||||
_: &mut Stack,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
value: Option<Value>,
|
||||
) -> Result<Self::View> {
|
||||
let value = value.unwrap_or_default();
|
||||
let mut view = InteractiveView::new(value, self.table_cfg);
|
||||
let mut view = InteractiveView::new(value);
|
||||
view.init(self.command.clone());
|
||||
view.try_run(engine_state, stack)
|
||||
.map_err(|e| Error::new(ErrorKind::Other, e))?;
|
||||
|
||||
Ok(view)
|
||||
}
|
||||
|
96
crates/nu-explore/src/commands/tweak.rs
Normal file
96
crates/nu-explore/src/commands/tweak.rs
Normal file
@ -0,0 +1,96 @@
|
||||
use std::io::{self, Result};
|
||||
|
||||
use nu_protocol::{
|
||||
engine::{EngineState, Stack},
|
||||
Value,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
nu_common::NuSpan,
|
||||
pager::{Pager, Transition},
|
||||
};
|
||||
|
||||
use super::{HelpExample, HelpManual, SimpleCommand};
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct TweakCmd {
|
||||
path: Vec<String>,
|
||||
value: Value,
|
||||
}
|
||||
|
||||
impl TweakCmd {
|
||||
pub const NAME: &'static str = "tweak";
|
||||
}
|
||||
|
||||
impl SimpleCommand for TweakCmd {
|
||||
fn name(&self) -> &'static str {
|
||||
Self::NAME
|
||||
}
|
||||
|
||||
fn usage(&self) -> &'static str {
|
||||
""
|
||||
}
|
||||
|
||||
fn help(&self) -> Option<HelpManual> {
|
||||
Some(HelpManual {
|
||||
name: "tweak",
|
||||
description:
|
||||
"Set different settings.\nIt could be consired a not interactive version of :config",
|
||||
arguments: vec![],
|
||||
examples: vec![
|
||||
HelpExample::new(":tweak table.show_index false", "Don't show index anymore"),
|
||||
HelpExample::new(":tweak table.show_head false", "Don't show header anymore"),
|
||||
HelpExample::new(
|
||||
":tweak try.border_color {bg: '#FFFFFF', fg: '#F213F1'}",
|
||||
"Make a different color for borders in :try",
|
||||
),
|
||||
],
|
||||
config_options: vec![],
|
||||
input: vec![],
|
||||
})
|
||||
}
|
||||
|
||||
fn parse(&mut self, input: &str) -> Result<()> {
|
||||
let input = input.trim();
|
||||
|
||||
let args = input.split_once(' ');
|
||||
let (key, value) = match args {
|
||||
Some(args) => args,
|
||||
None => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"expected to get 2 arguments 'key value'",
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
self.value = parse_value(value);
|
||||
|
||||
self.path = key
|
||||
.split_terminator('.')
|
||||
.map(|s| s.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn react(
|
||||
&mut self,
|
||||
_: &EngineState,
|
||||
_: &mut Stack,
|
||||
p: &mut Pager<'_>,
|
||||
_: Option<Value>,
|
||||
) -> Result<Transition> {
|
||||
p.set_config(&self.path, self.value.clone());
|
||||
|
||||
Ok(Transition::Ok)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_value(value: &str) -> Value {
|
||||
match value {
|
||||
"true" => Value::boolean(true, NuSpan::unknown()),
|
||||
"false" => Value::boolean(false, NuSpan::unknown()),
|
||||
s => Value::string(s.to_owned(), NuSpan::unknown()),
|
||||
}
|
||||
}
|
@ -1,58 +1,185 @@
|
||||
mod command;
|
||||
mod commands;
|
||||
mod events;
|
||||
mod nu_common;
|
||||
mod pager;
|
||||
mod registry;
|
||||
mod views;
|
||||
|
||||
use std::io;
|
||||
|
||||
use commands::{
|
||||
config::ConfigCmd, default_color_list, ConfigOption, ConfigShowCmd, ExpandCmd, HelpCmd,
|
||||
HelpManual, NuCmd, QuitCmd, TableCmd, TryCmd, TweakCmd,
|
||||
};
|
||||
use nu_common::{collect_pipeline, has_simple_value, CtrlC};
|
||||
use nu_protocol::{
|
||||
engine::{EngineState, Stack},
|
||||
PipelineData, Value,
|
||||
};
|
||||
use pager::{Page, Pager};
|
||||
use registry::{Command, CommandRegistry};
|
||||
use terminal_size::{Height, Width};
|
||||
use views::{InformationView, Preview, RecordView};
|
||||
use views::{InformationView, Orientation, Preview, RecordView};
|
||||
|
||||
pub use pager::{StyleConfig, TableConfig, TableSplitLines, ViewConfig};
|
||||
pub use pager::{PagerConfig, StyleConfig};
|
||||
|
||||
pub mod util {
|
||||
pub use super::nu_common::{create_map, map_into_value};
|
||||
}
|
||||
|
||||
pub fn run_pager(
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
ctrlc: CtrlC,
|
||||
table_cfg: TableConfig,
|
||||
view_cfg: ViewConfig,
|
||||
input: PipelineData,
|
||||
config: PagerConfig,
|
||||
) -> io::Result<Option<Value>> {
|
||||
let commands = command::CommandList::new(table_cfg);
|
||||
|
||||
let mut p = Pager::new(table_cfg, view_cfg.clone());
|
||||
let mut p = Pager::new(config.clone());
|
||||
|
||||
let is_record = matches!(input, PipelineData::Value(Value::Record { .. }, ..));
|
||||
let (columns, data) = collect_pipeline(input);
|
||||
|
||||
let commands = create_command_registry();
|
||||
|
||||
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);
|
||||
return p.run(engine_state, stack, ctrlc, information_view(), commands);
|
||||
}
|
||||
|
||||
if has_simple_value(&data) {
|
||||
let text = data[0][0].into_abbreviated_string(view_cfg.config);
|
||||
if config.show_banner {
|
||||
p.show_message("For help type :help");
|
||||
}
|
||||
|
||||
if let Some(value) = has_simple_value(&data) {
|
||||
let text = value.into_abbreviated_string(config.nu_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);
|
||||
let view = create_record_view(columns, data, is_record, config);
|
||||
p.run(engine_state, stack, ctrlc, view, commands)
|
||||
}
|
||||
|
||||
if table_cfg.reverse {
|
||||
fn create_record_view(
|
||||
columns: Vec<String>,
|
||||
data: Vec<Vec<Value>>,
|
||||
is_record: bool,
|
||||
config: PagerConfig,
|
||||
) -> Option<Page> {
|
||||
let mut view = RecordView::new(columns, data);
|
||||
if is_record {
|
||||
view.set_orientation_current(Orientation::Left);
|
||||
}
|
||||
|
||||
if config.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)
|
||||
Some(Page::new(view, false))
|
||||
}
|
||||
|
||||
fn information_view() -> Option<Page> {
|
||||
Some(Page::new(InformationView, true))
|
||||
}
|
||||
|
||||
pub fn create_command_registry() -> CommandRegistry {
|
||||
let mut registry = CommandRegistry::new();
|
||||
create_commands(&mut registry);
|
||||
create_aliases(&mut registry);
|
||||
|
||||
// reregister help && config commands
|
||||
let commands = registry.get_commands().cloned().collect::<Vec<_>>();
|
||||
let aliases = registry.get_aliases().collect::<Vec<_>>();
|
||||
|
||||
let help_cmd = create_help_command(&commands, &aliases);
|
||||
let config_cmd = create_config_command(&commands);
|
||||
|
||||
registry.register_command_view(help_cmd, true);
|
||||
registry.register_command_view(config_cmd, true);
|
||||
|
||||
registry
|
||||
}
|
||||
|
||||
pub fn create_commands(registry: &mut CommandRegistry) {
|
||||
registry.register_command_view(NuCmd::new(), false);
|
||||
registry.register_command_view(TableCmd::new(), false);
|
||||
|
||||
registry.register_command_view(ExpandCmd::new(), true);
|
||||
registry.register_command_view(TryCmd::new(), true);
|
||||
registry.register_command_view(ConfigShowCmd::new(), true);
|
||||
registry.register_command_view(ConfigCmd::default(), true);
|
||||
registry.register_command_view(HelpCmd::default(), true);
|
||||
|
||||
registry.register_command_reactive(QuitCmd::default());
|
||||
registry.register_command_reactive(TweakCmd::default());
|
||||
}
|
||||
|
||||
pub fn create_aliases(regestry: &mut CommandRegistry) {
|
||||
regestry.create_aliase("h", HelpCmd::NAME);
|
||||
regestry.create_aliase("e", ExpandCmd::NAME);
|
||||
regestry.create_aliase("q", QuitCmd::NAME);
|
||||
regestry.create_aliase("q!", QuitCmd::NAME);
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
fn create_config_command(commands: &[Command]) -> ConfigCmd {
|
||||
const GROUP: &str = "Explore configuration";
|
||||
|
||||
let mut config = ConfigCmd::from_commands(commands.to_vec());
|
||||
|
||||
config.register_group(ConfigOption::new(GROUP, "Status bar information color", "status.info", default_color_list()));
|
||||
config.register_group(ConfigOption::new(GROUP, "Status bar warning color", "status.warn", default_color_list()));
|
||||
config.register_group(ConfigOption::new(GROUP, "Status bar error color", "status.error", default_color_list()));
|
||||
|
||||
config.register_group(ConfigOption::new(GROUP, "Status bar default text color", "status_bar_text", default_color_list()));
|
||||
config.register_group(ConfigOption::new(GROUP, "Status bar background", "status_bar_background", default_color_list()));
|
||||
|
||||
config.register_group(ConfigOption::new(GROUP, "Command bar text color", "command_bar_text", default_color_list()));
|
||||
config.register_group(ConfigOption::new(GROUP, "Command bar background", "command_bar_background", default_color_list()));
|
||||
|
||||
config.register_group(ConfigOption::new(GROUP, "Highlight color in search", "highlight", default_color_list()));
|
||||
|
||||
config.register_group(ConfigOption::boolean(GROUP, "Show help banner on open", "help_banner"));
|
||||
config.register_group(ConfigOption::boolean(GROUP, "Pressing ESC causes a program exit", "exit_esc"));
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
fn create_help_command(commands: &[Command], aliases: &[(&str, &str)]) -> HelpCmd {
|
||||
let help_manuals = create_help_manuals(commands);
|
||||
|
||||
HelpCmd::new(help_manuals, aliases)
|
||||
}
|
||||
|
||||
fn create_help_manuals(cmd_list: &[Command]) -> Vec<HelpManual> {
|
||||
cmd_list.iter().map(create_help_manual).collect()
|
||||
}
|
||||
|
||||
fn create_help_manual(cmd: &Command) -> HelpManual {
|
||||
let name = match cmd {
|
||||
Command::Reactive(cmd) => cmd.name(),
|
||||
Command::View { cmd, .. } => cmd.name(),
|
||||
};
|
||||
|
||||
let manual = match cmd {
|
||||
Command::Reactive(cmd) => cmd.help(),
|
||||
Command::View { cmd, .. } => cmd.help(),
|
||||
};
|
||||
|
||||
__create_help_manual(manual, name)
|
||||
}
|
||||
|
||||
fn __create_help_manual(manual: Option<HelpManual>, name: &'static str) -> HelpManual {
|
||||
match manual {
|
||||
Some(manual) => manual,
|
||||
None => HelpManual {
|
||||
name,
|
||||
description: "",
|
||||
arguments: Vec::new(),
|
||||
examples: Vec::new(),
|
||||
input: Vec::new(),
|
||||
config_options: Vec::new(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -2,9 +2,30 @@ use nu_engine::eval_block;
|
||||
use nu_parser::parse;
|
||||
use nu_protocol::{
|
||||
engine::{EngineState, Stack, StateWorkingSet},
|
||||
PipelineData, ShellError,
|
||||
PipelineData, ShellError, Value,
|
||||
};
|
||||
|
||||
pub fn run_command_with_value(
|
||||
command: &str,
|
||||
input: &Value,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
if is_ignored_command(command) {
|
||||
return Err(ShellError::IOError(String::from("the command is ignored")));
|
||||
}
|
||||
|
||||
let pipeline = PipelineData::Value(input.clone(), None);
|
||||
let pipeline = run_nu_command(engine_state, stack, command, pipeline);
|
||||
match pipeline {
|
||||
Ok(PipelineData::Value(Value::Error { error }, ..)) => {
|
||||
Err(ShellError::IOError(error.to_string()))
|
||||
}
|
||||
Ok(pipeline) => Ok(pipeline),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_nu_command(
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
@ -16,7 +37,15 @@ pub fn run_nu_command(
|
||||
}
|
||||
|
||||
pub fn is_ignored_command(command: &str) -> bool {
|
||||
command.starts_with("clear")
|
||||
let ignore_list = ["clear", "explore", "exit"];
|
||||
|
||||
for cmd in ignore_list {
|
||||
if command.starts_with(cmd) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn eval_source2(
|
||||
@ -56,5 +85,5 @@ fn eval_source2(
|
||||
block.pipelines.drain(..block.pipelines.len() - 1);
|
||||
}
|
||||
|
||||
eval_block(engine_state, stack, &block, input, false, false)
|
||||
eval_block(engine_state, stack, &block, input, true, true)
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
mod command;
|
||||
mod string;
|
||||
mod table;
|
||||
mod value;
|
||||
|
||||
@ -17,11 +18,17 @@ pub type NuText = (String, TextStyle);
|
||||
pub type CtrlC = Option<Arc<AtomicBool>>;
|
||||
pub type NuStyleTable = HashMap<String, NuStyle>;
|
||||
|
||||
pub use command::{is_ignored_command, run_nu_command};
|
||||
pub use command::{is_ignored_command, run_command_with_value, run_nu_command};
|
||||
pub use string::truncate_str;
|
||||
pub use table::try_build_table;
|
||||
pub use value::{collect_input, collect_pipeline};
|
||||
pub use value::{collect_input, collect_pipeline, create_map, map_into_value, nu_str};
|
||||
|
||||
pub fn has_simple_value(data: &[Vec<Value>]) -> bool {
|
||||
pub fn has_simple_value(data: &[Vec<Value>]) -> Option<&Value> {
|
||||
let has_single_value = data.len() == 1 && data[0].len() == 1;
|
||||
has_single_value && !matches!(&data[0][0], Value::List { .. } | Value::Record { .. })
|
||||
let is_complex_type = matches!(&data[0][0], Value::List { .. } | Value::Record { .. });
|
||||
if has_single_value && !is_complex_type {
|
||||
Some(&data[0][0])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
14
crates/nu-explore/src/nu_common/string.rs
Normal file
14
crates/nu-explore/src/nu_common/string.rs
Normal file
@ -0,0 +1,14 @@
|
||||
use nu_table::{string_truncate, string_width};
|
||||
|
||||
pub fn truncate_str(text: &mut String, width: usize) {
|
||||
if width == 0 {
|
||||
text.clear();
|
||||
} else {
|
||||
if string_width(text) < width {
|
||||
return;
|
||||
}
|
||||
|
||||
*text = string_truncate(text, width - 1);
|
||||
text.push('…');
|
||||
}
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use nu_engine::get_columns;
|
||||
use nu_protocol::{ast::PathMember, PipelineData, Value};
|
||||
|
||||
@ -44,7 +46,7 @@ pub fn collect_pipeline(input: PipelineData) -> (Vec<String>, Vec<Vec<Value>>) {
|
||||
);
|
||||
|
||||
columns.push(String::from("stdout"));
|
||||
data.push(vec![value]);
|
||||
data.push(value);
|
||||
}
|
||||
|
||||
if let Some(stderr) = stderr {
|
||||
@ -54,29 +56,32 @@ pub fn collect_pipeline(input: PipelineData) -> (Vec<String>, Vec<Vec<Value>>) {
|
||||
);
|
||||
|
||||
columns.push(String::from("stderr"));
|
||||
data.push(vec![value]);
|
||||
data.push(value);
|
||||
}
|
||||
|
||||
if let Some(exit_code) = exit_code {
|
||||
let list = exit_code.collect::<Vec<_>>();
|
||||
let val = Value::List { vals: list, span };
|
||||
|
||||
columns.push(String::from("exit_code"));
|
||||
data.push(list);
|
||||
data.push(val);
|
||||
}
|
||||
|
||||
if metadata.is_some() {
|
||||
columns.push(String::from("metadata"));
|
||||
data.push(vec![Value::Record {
|
||||
let val = Value::Record {
|
||||
cols: vec![String::from("data_source")],
|
||||
vals: vec![Value::String {
|
||||
val: String::from("ls"),
|
||||
span,
|
||||
}],
|
||||
span,
|
||||
}]);
|
||||
};
|
||||
|
||||
columns.push(String::from("metadata"));
|
||||
data.push(val);
|
||||
}
|
||||
|
||||
(columns, data)
|
||||
(columns, vec![data])
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -169,3 +174,34 @@ fn record_lookup_value(item: &Value, header: &str) -> Value {
|
||||
item => item.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_map(value: &Value) -> Option<HashMap<String, Value>> {
|
||||
let (cols, inner_vals) = value.as_record().ok()?;
|
||||
|
||||
let mut hm: HashMap<String, Value> = HashMap::new();
|
||||
for (k, v) in cols.iter().zip(inner_vals) {
|
||||
hm.insert(k.to_string(), v.clone());
|
||||
}
|
||||
|
||||
Some(hm)
|
||||
}
|
||||
|
||||
pub fn map_into_value(hm: HashMap<String, Value>) -> Value {
|
||||
let mut columns = Vec::with_capacity(hm.len());
|
||||
let mut values = Vec::with_capacity(hm.len());
|
||||
|
||||
for (key, value) in hm {
|
||||
columns.push(key);
|
||||
values.push(value);
|
||||
}
|
||||
|
||||
Value::Record {
|
||||
cols: columns,
|
||||
vals: values,
|
||||
span: NuSpan::unknown(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn nu_str<S: AsRef<str>>(s: S) -> Value {
|
||||
Value::string(s.as_ref().to_owned(), NuSpan::unknown())
|
||||
}
|
||||
|
54
crates/nu-explore/src/pager/command_bar.rs
Normal file
54
crates/nu-explore/src/pager/command_bar.rs
Normal file
@ -0,0 +1,54 @@
|
||||
use tui::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::{Modifier, Style},
|
||||
widgets::{Block, Widget},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
nu_common::NuStyle,
|
||||
views::util::{nu_style_to_tui, set_span},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CommandBar<'a> {
|
||||
text: &'a str,
|
||||
information: &'a str,
|
||||
text_s: Style,
|
||||
back_s: Style,
|
||||
}
|
||||
|
||||
impl<'a> CommandBar<'a> {
|
||||
pub fn new(text: &'a str, information: &'a str, text_s: NuStyle, back_s: NuStyle) -> Self {
|
||||
let text_s = nu_style_to_tui(text_s).add_modifier(Modifier::BOLD);
|
||||
let back_s = nu_style_to_tui(back_s);
|
||||
|
||||
Self {
|
||||
text,
|
||||
information,
|
||||
text_s,
|
||||
back_s,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for CommandBar<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
const INFO_WIDTH: u16 = 12;
|
||||
const INFO_PADDING: u16 = 12;
|
||||
|
||||
// colorize the line
|
||||
let block = Block::default().style(self.back_s);
|
||||
block.render(area, buf);
|
||||
|
||||
let text_width = set_span(buf, (area.x, area.y), self.text, self.text_s, area.width);
|
||||
let available_width = area.width.saturating_sub(text_width);
|
||||
|
||||
if available_width <= INFO_WIDTH + INFO_PADDING {
|
||||
return;
|
||||
}
|
||||
|
||||
let x = area.right().saturating_sub(INFO_WIDTH + INFO_PADDING);
|
||||
set_span(buf, (x, area.y), self.information, self.text_s, INFO_WIDTH);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
44
crates/nu-explore/src/pager/report.rs
Normal file
44
crates/nu-explore/src/pager/report.rs
Normal file
@ -0,0 +1,44 @@
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Report {
|
||||
pub message: String,
|
||||
pub level: Severity,
|
||||
pub context: String,
|
||||
pub context2: String,
|
||||
}
|
||||
|
||||
impl Report {
|
||||
pub fn new(message: String, level: Severity, context: String, context2: String) -> Self {
|
||||
Self {
|
||||
message,
|
||||
level,
|
||||
context,
|
||||
context2,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn message(message: impl Into<String>, level: Severity) -> Self {
|
||||
Self::new(message.into(), level, String::new(), String::new())
|
||||
}
|
||||
|
||||
pub fn info(message: impl Into<String>) -> Self {
|
||||
Self::new(message.into(), Severity::Info, String::new(), String::new())
|
||||
}
|
||||
|
||||
pub fn error(message: impl Into<String>) -> Self {
|
||||
Self::new(message.into(), Severity::Err, String::new(), String::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Report {
|
||||
fn default() -> Self {
|
||||
Self::new(String::new(), Severity::Info, String::new(), String::new())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Severity {
|
||||
Info,
|
||||
#[allow(dead_code)]
|
||||
Warn,
|
||||
Err,
|
||||
}
|
80
crates/nu-explore/src/pager/status_bar.rs
Normal file
80
crates/nu-explore/src/pager/status_bar.rs
Normal file
@ -0,0 +1,80 @@
|
||||
use tui::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::{Modifier, Style},
|
||||
widgets::{Block, Widget},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
nu_common::NuStyle,
|
||||
views::util::{nu_style_to_tui, set_span},
|
||||
};
|
||||
|
||||
pub struct StatusBar {
|
||||
text: (String, Style),
|
||||
ctx1: (String, Style),
|
||||
ctx2: (String, Style),
|
||||
back_s: Style,
|
||||
}
|
||||
|
||||
impl StatusBar {
|
||||
pub fn new(text: String, ctx: String, ctx2: String) -> Self {
|
||||
Self {
|
||||
text: (text, Style::default()),
|
||||
ctx1: (ctx, Style::default()),
|
||||
ctx2: (ctx2, Style::default()),
|
||||
back_s: Style::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_message_style(&mut self, style: NuStyle) {
|
||||
self.text.1 = nu_style_to_tui(style).add_modifier(Modifier::BOLD);
|
||||
}
|
||||
|
||||
pub fn set_ctx_style(&mut self, style: NuStyle) {
|
||||
self.ctx1.1 = nu_style_to_tui(style).add_modifier(Modifier::BOLD);
|
||||
}
|
||||
|
||||
pub fn set_ctx2_style(&mut self, style: NuStyle) {
|
||||
self.ctx2.1 = nu_style_to_tui(style).add_modifier(Modifier::BOLD);
|
||||
}
|
||||
|
||||
pub fn set_background_style(&mut self, style: NuStyle) {
|
||||
self.back_s = nu_style_to_tui(style);
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for StatusBar {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
const MAX_CONTEXT_WIDTH: u16 = 12;
|
||||
const MAX_CONTEXT2_WIDTH: u16 = 12;
|
||||
|
||||
// colorize the line
|
||||
let block = Block::default().style(self.back_s);
|
||||
block.render(area, buf);
|
||||
|
||||
let mut used_width = 0;
|
||||
|
||||
let (text, style) = &self.ctx1;
|
||||
if !text.is_empty() && area.width > MAX_CONTEXT_WIDTH {
|
||||
let x = area.right().saturating_sub(MAX_CONTEXT_WIDTH);
|
||||
set_span(buf, (x, area.y), text, *style, MAX_CONTEXT_WIDTH);
|
||||
|
||||
used_width += MAX_CONTEXT_WIDTH;
|
||||
}
|
||||
|
||||
let (text, style) = &self.ctx2;
|
||||
if !text.is_empty() && area.width > MAX_CONTEXT2_WIDTH + used_width {
|
||||
let x = area.right().saturating_sub(MAX_CONTEXT2_WIDTH + used_width);
|
||||
set_span(buf, (x, area.y), text, *style, MAX_CONTEXT2_WIDTH);
|
||||
|
||||
used_width += MAX_CONTEXT2_WIDTH;
|
||||
}
|
||||
|
||||
let (text, style) = &self.text;
|
||||
if !text.is_empty() && area.width > used_width {
|
||||
let rest_width = area.width - used_width;
|
||||
set_span(buf, (area.x, area.y), text, *style, rest_width);
|
||||
}
|
||||
}
|
||||
}
|
138
crates/nu-explore/src/registry/command.rs
Normal file
138
crates/nu-explore/src/registry/command.rs
Normal file
@ -0,0 +1,138 @@
|
||||
use crate::{
|
||||
commands::{HelpManual, SimpleCommand, ViewCommand},
|
||||
views::View,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Command {
|
||||
Reactive(Box<dyn SCommand>),
|
||||
View {
|
||||
cmd: Box<dyn VCommand>,
|
||||
is_light: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl Command {
|
||||
pub fn view<C>(command: C, is_light: bool) -> Self
|
||||
where
|
||||
C: ViewCommand + Clone + 'static,
|
||||
C::View: View,
|
||||
{
|
||||
let cmd = Box::new(ViewCmd(command)) as Box<dyn VCommand>;
|
||||
|
||||
Self::View { cmd, is_light }
|
||||
}
|
||||
|
||||
pub fn reactive<C>(command: C) -> Self
|
||||
where
|
||||
C: SimpleCommand + Clone + 'static,
|
||||
{
|
||||
let cmd = Box::new(command) as Box<dyn SCommand>;
|
||||
|
||||
Self::Reactive(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
impl Command {
|
||||
pub fn name(&self) -> &str {
|
||||
match self {
|
||||
Command::Reactive(cmd) => cmd.name(),
|
||||
Command::View { cmd, .. } => cmd.name(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(&mut self, args: &str) -> std::io::Result<()> {
|
||||
match self {
|
||||
Command::Reactive(cmd) => cmd.parse(args),
|
||||
Command::View { cmd, .. } => cmd.parse(args),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 display_config_option(&mut self, group: String, key: String, value: String) -> bool {
|
||||
self.0.display_config_option(group, key, value)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
87
crates/nu-explore/src/registry/mod.rs
Normal file
87
crates/nu-explore/src/registry/mod.rs
Normal file
@ -0,0 +1,87 @@
|
||||
mod command;
|
||||
|
||||
use std::{borrow::Cow, collections::HashMap};
|
||||
|
||||
use crate::{
|
||||
commands::{SimpleCommand, ViewCommand},
|
||||
views::View,
|
||||
};
|
||||
|
||||
pub use command::Command;
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct CommandRegistry {
|
||||
commands: HashMap<Cow<'static, str>, Command>,
|
||||
aliases: HashMap<Cow<'static, str>, Cow<'static, str>>,
|
||||
}
|
||||
|
||||
impl CommandRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn register(&mut self, command: Command) {
|
||||
self.commands
|
||||
.insert(Cow::Owned(command.name().to_owned()), command);
|
||||
}
|
||||
|
||||
pub fn register_command_view<C>(&mut self, command: C, is_light: bool)
|
||||
where
|
||||
C: ViewCommand + Clone + 'static,
|
||||
C::View: View,
|
||||
{
|
||||
self.commands.insert(
|
||||
Cow::Owned(command.name().to_owned()),
|
||||
Command::view(command, is_light),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn register_command_reactive<C>(&mut self, command: C)
|
||||
where
|
||||
C: SimpleCommand + Clone + 'static,
|
||||
{
|
||||
self.commands.insert(
|
||||
Cow::Owned(command.name().to_owned()),
|
||||
Command::reactive(command),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn create_aliase(&mut self, aliase: &str, command: &str) {
|
||||
self.aliases.insert(
|
||||
Cow::Owned(aliase.to_owned()),
|
||||
Cow::Owned(command.to_owned()),
|
||||
);
|
||||
}
|
||||
|
||||
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 mut command = self.find_command(cmd)?;
|
||||
if let Err(err) = command.parse(args) {
|
||||
return Some(Err(err));
|
||||
}
|
||||
|
||||
Some(Ok(command))
|
||||
}
|
||||
|
||||
pub fn get_commands(&self) -> impl Iterator<Item = &Command> {
|
||||
self.commands.values()
|
||||
}
|
||||
|
||||
pub fn get_aliases(&self) -> impl Iterator<Item = (&str, &str)> {
|
||||
self.aliases
|
||||
.iter()
|
||||
.map(|(key, value)| (key.as_ref(), value.as_ref()))
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
430
crates/nu-explore/src/views/configuration.rs
Normal file
430
crates/nu-explore/src/views/configuration.rs
Normal file
@ -0,0 +1,430 @@
|
||||
use std::{cmp::Ordering, fmt::Debug, ptr::addr_of};
|
||||
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use nu_color_config::get_color_map;
|
||||
use nu_protocol::{
|
||||
engine::{EngineState, Stack},
|
||||
Value,
|
||||
};
|
||||
use nu_table::TextStyle;
|
||||
use tui::{
|
||||
layout::Rect,
|
||||
style::Style,
|
||||
widgets::{BorderType, Borders, Paragraph},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
nu_common::{truncate_str, NuText},
|
||||
pager::{Frame, Transition, ViewInfo},
|
||||
util::create_map,
|
||||
views::util::nu_style_to_tui,
|
||||
};
|
||||
|
||||
use super::{cursor::WindowCursor, Layout, View, ViewConfig};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ConfigurationView {
|
||||
options: Vec<ConfigGroup>,
|
||||
peeked_cursor: Option<WindowCursor>,
|
||||
cursor: WindowCursor,
|
||||
border_color: Style,
|
||||
cursor_color: Style,
|
||||
list_color: Style,
|
||||
}
|
||||
|
||||
impl ConfigurationView {
|
||||
pub fn new(options: Vec<ConfigGroup>) -> Self {
|
||||
let cursor = WindowCursor::new(options.len(), options.len()).expect("...");
|
||||
|
||||
Self {
|
||||
options,
|
||||
cursor,
|
||||
peeked_cursor: None,
|
||||
border_color: Style::default(),
|
||||
cursor_color: Style::default(),
|
||||
list_color: Style::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn update_cursors(&mut self, height: usize) {
|
||||
self.cursor.set_window(height);
|
||||
|
||||
if let Some(cursor) = &mut self.peeked_cursor {
|
||||
cursor.set_window(height);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_option_list(
|
||||
&mut self,
|
||||
f: &mut Frame,
|
||||
area: Rect,
|
||||
list_color: Style,
|
||||
cursor_color: Style,
|
||||
layout: &mut Layout,
|
||||
) {
|
||||
let (data, data_c) = match self.peeked_cursor {
|
||||
Some(cursor) => {
|
||||
let i = self.cursor.index();
|
||||
let opt = &self.options[i];
|
||||
let data = opt
|
||||
.options
|
||||
.iter()
|
||||
.map(|e| e.name.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
(data, cursor)
|
||||
}
|
||||
None => {
|
||||
let data = self
|
||||
.options
|
||||
.iter()
|
||||
.map(|o| o.group.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
(data, self.cursor)
|
||||
}
|
||||
};
|
||||
|
||||
render_list(f, area, &data, data_c, list_color, cursor_color, layout);
|
||||
}
|
||||
|
||||
fn peek_current(&self) -> Option<(&ConfigGroup, &ConfigOption)> {
|
||||
let cursor = match self.peeked_cursor {
|
||||
Some(cursor) => cursor,
|
||||
None => return None,
|
||||
};
|
||||
|
||||
let i = self.cursor.index();
|
||||
let j = cursor.index();
|
||||
let group = &self.options[i];
|
||||
let opt = &group.options[j];
|
||||
|
||||
Some((group, opt))
|
||||
}
|
||||
|
||||
fn peek_current_group(&self) -> &ConfigGroup {
|
||||
let i = self.cursor.index();
|
||||
&self.options[i]
|
||||
}
|
||||
|
||||
fn peek_current_opt(&mut self) -> Option<&mut ConfigOption> {
|
||||
let cursor = match self.peeked_cursor {
|
||||
Some(cursor) => cursor,
|
||||
None => return None,
|
||||
};
|
||||
|
||||
let i = self.cursor.index();
|
||||
let j = cursor.index();
|
||||
|
||||
Some(&mut self.options[i].options[j])
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ConfigGroup {
|
||||
group: String,
|
||||
description: String,
|
||||
options: Vec<ConfigOption>,
|
||||
}
|
||||
|
||||
impl ConfigGroup {
|
||||
pub fn new(group: String, options: Vec<ConfigOption>, description: String) -> Self {
|
||||
Self {
|
||||
group,
|
||||
options,
|
||||
description,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn group(&self) -> &str {
|
||||
self.group.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ConfigOption {
|
||||
name: String,
|
||||
view: Box<dyn View>,
|
||||
}
|
||||
|
||||
impl ConfigOption {
|
||||
pub fn new(name: String, view: Box<dyn View>) -> Self {
|
||||
Self { name, view }
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for ConfigOption {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ConfigOption")
|
||||
.field("name", &self.name)
|
||||
.field("view", &addr_of!(self.view))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl View for ConfigurationView {
|
||||
fn draw(&mut self, f: &mut Frame, area: Rect, cfg: ViewConfig<'_>, layout: &mut Layout) {
|
||||
const LEFT_PADDING: u16 = 1;
|
||||
const BLOCK_PADDING: u16 = 1;
|
||||
const OPTION_BLOCK_WIDTH: u16 = 30;
|
||||
const USED_HEIGHT_BY_BORDERS: u16 = 2;
|
||||
|
||||
if area.width < 40 {
|
||||
return;
|
||||
}
|
||||
|
||||
let list_color = self.list_color;
|
||||
let border_color = self.border_color;
|
||||
let cursor_color = self.cursor_color;
|
||||
|
||||
let height = area.height - USED_HEIGHT_BY_BORDERS;
|
||||
|
||||
let option_b_x1 = area.x + LEFT_PADDING;
|
||||
let option_b_x2 = area.x + LEFT_PADDING + OPTION_BLOCK_WIDTH;
|
||||
|
||||
let view_b_x1 = option_b_x2 + BLOCK_PADDING;
|
||||
let view_b_w = area.width - (LEFT_PADDING + BLOCK_PADDING + OPTION_BLOCK_WIDTH);
|
||||
|
||||
let option_content_x1 = option_b_x1 + 1;
|
||||
let option_content_w = OPTION_BLOCK_WIDTH - 2;
|
||||
let option_content_h = height;
|
||||
|
||||
let option_content_area =
|
||||
Rect::new(option_content_x1, 1, option_content_w, option_content_h);
|
||||
|
||||
let view_content_x1 = view_b_x1 + 1;
|
||||
let view_content_w = view_b_w - 2;
|
||||
let view_content_h = height;
|
||||
|
||||
let view_content_area = Rect::new(view_content_x1, 1, view_content_w, view_content_h);
|
||||
|
||||
let option_block = tui::widgets::Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Plain)
|
||||
.border_style(border_color);
|
||||
let option_area = Rect::new(option_b_x1, area.y, OPTION_BLOCK_WIDTH, area.height);
|
||||
|
||||
let view_block = tui::widgets::Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Plain)
|
||||
.border_style(border_color);
|
||||
let view_area = Rect::new(view_b_x1, area.y, view_b_w, area.height);
|
||||
|
||||
f.render_widget(option_block, option_area);
|
||||
f.render_widget(view_block, view_area);
|
||||
|
||||
self.render_option_list(f, option_content_area, list_color, cursor_color, layout);
|
||||
|
||||
if let Some(opt) = self.peek_current_opt() {
|
||||
let mut layout = Layout::default();
|
||||
opt.view.draw(f, view_content_area, cfg, &mut layout);
|
||||
} else {
|
||||
let group = self.peek_current_group();
|
||||
let description = &group.description;
|
||||
|
||||
f.render_widget(Paragraph::new(description.as_str()), view_content_area);
|
||||
}
|
||||
|
||||
self.update_cursors(height as usize);
|
||||
}
|
||||
|
||||
fn handle_input(
|
||||
&mut self,
|
||||
_: &EngineState,
|
||||
_: &mut Stack,
|
||||
_: &Layout,
|
||||
_: &mut ViewInfo,
|
||||
key: KeyEvent,
|
||||
) -> Option<Transition> {
|
||||
match key.code {
|
||||
KeyCode::Esc => {
|
||||
if self.peeked_cursor.is_some() {
|
||||
self.peeked_cursor = None;
|
||||
Some(Transition::Ok)
|
||||
} else {
|
||||
Some(Transition::Exit)
|
||||
}
|
||||
}
|
||||
KeyCode::Up => {
|
||||
match &mut self.peeked_cursor {
|
||||
Some(cursor) => cursor.prev(1),
|
||||
None => self.cursor.prev(1),
|
||||
};
|
||||
|
||||
if let Some((group, opt)) = self.peek_current() {
|
||||
return Some(Transition::Cmd(build_tweak_cmd(group, opt)));
|
||||
}
|
||||
|
||||
Some(Transition::Ok)
|
||||
}
|
||||
KeyCode::Down => {
|
||||
match &mut self.peeked_cursor {
|
||||
Some(cursor) => cursor.next(1),
|
||||
None => self.cursor.next(1),
|
||||
};
|
||||
|
||||
if let Some((group, opt)) = self.peek_current() {
|
||||
return Some(Transition::Cmd(build_tweak_cmd(group, opt)));
|
||||
}
|
||||
|
||||
Some(Transition::Ok)
|
||||
}
|
||||
KeyCode::PageUp => {
|
||||
match &mut self.peeked_cursor {
|
||||
Some(cursor) => cursor.prev_window(),
|
||||
None => self.cursor.prev_window(),
|
||||
};
|
||||
|
||||
if let Some((group, opt)) = self.peek_current() {
|
||||
return Some(Transition::Cmd(build_tweak_cmd(group, opt)));
|
||||
}
|
||||
|
||||
Some(Transition::Ok)
|
||||
}
|
||||
KeyCode::PageDown => {
|
||||
match &mut self.peeked_cursor {
|
||||
Some(cursor) => cursor.next_window(),
|
||||
None => self.cursor.next_window(),
|
||||
};
|
||||
|
||||
if let Some((group, opt)) = self.peek_current() {
|
||||
return Some(Transition::Cmd(build_tweak_cmd(group, opt)));
|
||||
}
|
||||
|
||||
Some(Transition::Ok)
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
if self.peeked_cursor.is_some() {
|
||||
return Some(Transition::Ok);
|
||||
}
|
||||
|
||||
self.peeked_cursor = Some(WindowCursor::default());
|
||||
let length = self.peek_current().expect("...").0.options.len();
|
||||
|
||||
self.peeked_cursor = WindowCursor::new(length, length);
|
||||
|
||||
let (group, opt) = self.peek_current().expect("...");
|
||||
|
||||
Some(Transition::Cmd(build_tweak_cmd(group, opt)))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn exit(&mut self) -> Option<Value> {
|
||||
None
|
||||
}
|
||||
|
||||
fn collect_data(&self) -> Vec<NuText> {
|
||||
if self.peeked_cursor.is_some() {
|
||||
let i = self.cursor.index();
|
||||
let opt = &self.options[i];
|
||||
opt.options
|
||||
.iter()
|
||||
.map(|e| (e.name.clone(), TextStyle::default()))
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
self.options
|
||||
.iter()
|
||||
.map(|s| (s.group.to_string(), TextStyle::default()))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
fn show_data(&mut self, i: usize) -> bool {
|
||||
if let Some(c) = &mut self.peeked_cursor {
|
||||
let i = self.cursor.index();
|
||||
if i > self.options[i].options.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
loop {
|
||||
let p = c.index();
|
||||
match i.cmp(&p) {
|
||||
Ordering::Equal => return true,
|
||||
Ordering::Less => c.prev(1),
|
||||
Ordering::Greater => c.next(1),
|
||||
};
|
||||
}
|
||||
} else {
|
||||
if i > self.options.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
loop {
|
||||
let p = self.cursor.index();
|
||||
match i.cmp(&p) {
|
||||
Ordering::Equal => return true,
|
||||
Ordering::Less => self.cursor.prev(1),
|
||||
Ordering::Greater => self.cursor.next(1),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn setup(&mut self, config: ViewConfig<'_>) {
|
||||
if let Some(hm) = config.config.get("config").and_then(create_map) {
|
||||
let colors = get_color_map(&hm);
|
||||
|
||||
if let Some(style) = colors.get("border_color").copied() {
|
||||
self.border_color = nu_style_to_tui(style);
|
||||
}
|
||||
|
||||
if let Some(style) = colors.get("cursor_color").copied() {
|
||||
self.cursor_color = nu_style_to_tui(style);
|
||||
}
|
||||
|
||||
if let Some(style) = colors.get("list_color").copied() {
|
||||
self.list_color = nu_style_to_tui(style);
|
||||
}
|
||||
}
|
||||
|
||||
for group in &mut self.options {
|
||||
for opt in &mut group.options {
|
||||
opt.view.setup(config);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_tweak_cmd(group: &ConfigGroup, opt: &ConfigOption) -> String {
|
||||
format!("tweak {} {}", group.group(), opt.name)
|
||||
}
|
||||
|
||||
fn render_list(
|
||||
f: &mut Frame,
|
||||
area: Rect,
|
||||
data: &[String],
|
||||
cursor: WindowCursor,
|
||||
not_picked_s: Style,
|
||||
picked_s: Style,
|
||||
layout: &mut Layout,
|
||||
) {
|
||||
let height = area.height as usize;
|
||||
let width = area.width as usize;
|
||||
|
||||
let mut data = &data[cursor.starts_at()..];
|
||||
if data.len() > height {
|
||||
data = &data[..height];
|
||||
}
|
||||
|
||||
let selected_row = cursor.offset();
|
||||
|
||||
for (i, name) in data.iter().enumerate() {
|
||||
let mut name = name.to_owned();
|
||||
truncate_str(&mut name, width);
|
||||
|
||||
let area = Rect::new(area.x, area.y + i as u16, area.width, 1);
|
||||
|
||||
let mut text = Paragraph::new(name.clone());
|
||||
|
||||
if i == selected_row {
|
||||
text = text.style(picked_s);
|
||||
} else {
|
||||
text = text.style(not_picked_s);
|
||||
}
|
||||
|
||||
f.render_widget(text, area);
|
||||
|
||||
layout.push(&name, area.x, area.y, area.width, 1);
|
||||
}
|
||||
}
|
71
crates/nu-explore/src/views/cursor/mod.rs
Normal file
71
crates/nu-explore/src/views/cursor/mod.rs
Normal file
@ -0,0 +1,71 @@
|
||||
mod windowcursor;
|
||||
mod xycursor;
|
||||
|
||||
pub use windowcursor::WindowCursor;
|
||||
pub use xycursor::XYCursor;
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct Cursor {
|
||||
index: usize,
|
||||
limit: usize,
|
||||
}
|
||||
|
||||
impl Cursor {
|
||||
pub fn new(limit: usize) -> Self {
|
||||
Self { index: 0, limit }
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn index(&self) -> usize {
|
||||
self.index
|
||||
}
|
||||
|
||||
pub fn cap(&self) -> usize {
|
||||
self.limit - self.index
|
||||
}
|
||||
|
||||
pub fn set(&mut self, i: usize) -> bool {
|
||||
if i >= self.limit {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.index = i;
|
||||
true
|
||||
}
|
||||
|
||||
pub fn limit(&mut self, i: usize) -> bool {
|
||||
if self.index > self.limit {
|
||||
self.index = self.limit.saturating_sub(1);
|
||||
return false;
|
||||
}
|
||||
|
||||
self.limit = i;
|
||||
if self.index >= self.limit {
|
||||
self.index = self.limit.saturating_sub(1);
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn end(&self) -> usize {
|
||||
self.limit
|
||||
}
|
||||
|
||||
pub fn next(&mut self, i: usize) -> bool {
|
||||
if self.index + i == self.limit {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.index += i;
|
||||
true
|
||||
}
|
||||
|
||||
pub fn prev(&mut self, i: usize) -> bool {
|
||||
if self.index < i {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.index -= i;
|
||||
true
|
||||
}
|
||||
}
|
106
crates/nu-explore/src/views/cursor/windowcursor.rs
Normal file
106
crates/nu-explore/src/views/cursor/windowcursor.rs
Normal file
@ -0,0 +1,106 @@
|
||||
use super::Cursor;
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct WindowCursor {
|
||||
view: Cursor,
|
||||
window: Cursor,
|
||||
}
|
||||
|
||||
impl WindowCursor {
|
||||
pub fn new(limit: usize, window: usize) -> Option<Self> {
|
||||
if window > limit {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(Self {
|
||||
view: Cursor::new(limit),
|
||||
window: Cursor::new(window),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn index(&self) -> usize {
|
||||
self.view.index + self.window.index
|
||||
}
|
||||
|
||||
pub fn offset(&self) -> usize {
|
||||
self.window.index
|
||||
}
|
||||
|
||||
pub fn starts_at(&self) -> usize {
|
||||
self.view.index
|
||||
}
|
||||
|
||||
pub fn cap(&self) -> usize {
|
||||
self.view.cap()
|
||||
}
|
||||
|
||||
pub fn window(&self) -> usize {
|
||||
self.window.end()
|
||||
}
|
||||
|
||||
pub fn end(&self) -> usize {
|
||||
self.view.end()
|
||||
}
|
||||
|
||||
pub fn set_window_at(&mut self, i: usize) -> bool {
|
||||
self.view.set(i)
|
||||
}
|
||||
|
||||
pub fn set_window(&mut self, i: usize) -> bool {
|
||||
if i > self.view.end() {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.window.limit(i)
|
||||
}
|
||||
|
||||
pub fn next(&mut self, i: usize) -> bool {
|
||||
if i > self.cap() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut rest = 0;
|
||||
for y in 0..i {
|
||||
if !self.window.next(1) {
|
||||
rest = i - y;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for _ in 0..rest {
|
||||
if self.index() + 1 == self.end() {
|
||||
return rest != i;
|
||||
}
|
||||
|
||||
self.view.next(1);
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn next_window(&mut self) -> bool {
|
||||
let end_cursor = self.window() - self.offset();
|
||||
self.next(end_cursor);
|
||||
|
||||
let mut index_move = self.window();
|
||||
if index_move + self.starts_at() >= self.end() {
|
||||
index_move = self.end() - self.starts_at();
|
||||
}
|
||||
|
||||
self.next(index_move)
|
||||
}
|
||||
|
||||
pub fn prev(&mut self, i: usize) -> bool {
|
||||
for _ in 0..i {
|
||||
if !self.window.prev(1) {
|
||||
self.view.prev(1);
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn prev_window(&mut self) -> bool {
|
||||
self.prev(self.window() + self.offset())
|
||||
}
|
||||
}
|
143
crates/nu-explore/src/views/cursor/xycursor.rs
Normal file
143
crates/nu-explore/src/views/cursor/xycursor.rs
Normal file
@ -0,0 +1,143 @@
|
||||
use super::WindowCursor;
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub struct XYCursor {
|
||||
x: WindowCursor,
|
||||
y: WindowCursor,
|
||||
}
|
||||
|
||||
impl XYCursor {
|
||||
pub fn new(count_rows: usize, count_columns: usize) -> Self {
|
||||
Self {
|
||||
x: WindowCursor::new(count_columns, count_columns).expect("..."),
|
||||
y: WindowCursor::new(count_rows, count_rows).expect("..."),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_window(&mut self, count_rows: usize, count_columns: usize) {
|
||||
self.x.set_window(count_columns);
|
||||
self.y.set_window(count_rows);
|
||||
}
|
||||
|
||||
pub fn set_position(&mut self, row: usize, col: usize) {
|
||||
self.x.set_window_at(col);
|
||||
self.y.set_window_at(row);
|
||||
}
|
||||
|
||||
pub fn row(&self) -> usize {
|
||||
self.y.index()
|
||||
}
|
||||
|
||||
pub fn column(&self) -> usize {
|
||||
self.x.index()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn row_offset(&self) -> usize {
|
||||
self.y.offset()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn column_limit(&self) -> usize {
|
||||
self.x.end()
|
||||
}
|
||||
|
||||
pub fn row_limit(&self) -> usize {
|
||||
self.y.end()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn column_offset(&self) -> usize {
|
||||
self.x.offset()
|
||||
}
|
||||
|
||||
pub fn row_starts_at(&self) -> usize {
|
||||
self.y.starts_at()
|
||||
}
|
||||
|
||||
pub fn column_starts_at(&self) -> usize {
|
||||
self.x.starts_at()
|
||||
}
|
||||
|
||||
pub fn row_window(&self) -> usize {
|
||||
self.y.offset()
|
||||
}
|
||||
|
||||
pub fn column_window(&self) -> usize {
|
||||
self.x.offset()
|
||||
}
|
||||
|
||||
pub fn row_window_size(&self) -> usize {
|
||||
self.y.window()
|
||||
}
|
||||
|
||||
pub fn column_window_size(&self) -> usize {
|
||||
self.x.window()
|
||||
}
|
||||
|
||||
pub fn next_row(&mut self) -> bool {
|
||||
self.y.next(1)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn next_row_by(&mut self, i: usize) -> bool {
|
||||
self.y.next(i)
|
||||
}
|
||||
|
||||
pub fn next_row_page(&mut self) -> bool {
|
||||
self.y.next_window()
|
||||
}
|
||||
|
||||
pub fn prev_row(&mut self) -> bool {
|
||||
self.y.prev(1)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn prev_row_by(&mut self, i: usize) -> bool {
|
||||
self.y.prev(i)
|
||||
}
|
||||
|
||||
pub fn prev_row_page(&mut self) -> bool {
|
||||
self.y.prev_window()
|
||||
}
|
||||
|
||||
pub fn next_column(&mut self) -> bool {
|
||||
self.x.next(1)
|
||||
}
|
||||
|
||||
pub fn next_column_by(&mut self, i: usize) -> bool {
|
||||
self.x.next(i)
|
||||
}
|
||||
|
||||
pub fn prev_column(&mut self) -> bool {
|
||||
self.x.prev(1)
|
||||
}
|
||||
|
||||
pub fn prev_column_by(&mut self, i: usize) -> bool {
|
||||
self.x.prev(i)
|
||||
}
|
||||
|
||||
pub fn next_column_i(&mut self) -> bool {
|
||||
self.x.set_window_at(self.x.starts_at() + 1)
|
||||
}
|
||||
|
||||
pub fn prev_column_i(&mut self) -> bool {
|
||||
if self.x.starts_at() == 0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.x.set_window_at(self.x.starts_at() - 1)
|
||||
}
|
||||
|
||||
pub fn next_row_i(&mut self) -> bool {
|
||||
self.y.set_window_at(self.y.starts_at() + 1)
|
||||
}
|
||||
|
||||
pub fn prev_row_i(&mut self) -> bool {
|
||||
if self.y.starts_at() == 0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.y.set_window_at(self.y.starts_at() - 1)
|
||||
}
|
||||
}
|
@ -5,10 +5,10 @@ use tui::{layout::Rect, widgets::Paragraph};
|
||||
|
||||
use crate::{
|
||||
nu_common::NuText,
|
||||
pager::{Frame, Transition, ViewConfig, ViewInfo},
|
||||
pager::{Frame, Transition, ViewInfo},
|
||||
};
|
||||
|
||||
use super::{Layout, View};
|
||||
use super::{Layout, View, ViewConfig};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct InformationView;
|
||||
@ -26,7 +26,7 @@ impl InformationView {
|
||||
}
|
||||
|
||||
impl View for InformationView {
|
||||
fn draw(&mut self, f: &mut Frame, area: Rect, _: &ViewConfig, layout: &mut Layout) {
|
||||
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 {
|
||||
|
@ -1,6 +1,7 @@
|
||||
use std::cmp::min;
|
||||
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use nu_color_config::get_color_map;
|
||||
use nu_protocol::{
|
||||
engine::{EngineState, Stack},
|
||||
PipelineData, Value,
|
||||
@ -12,27 +13,37 @@ use tui::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
nu_common::{collect_pipeline, is_ignored_command, run_nu_command},
|
||||
pager::{Frame, Report, TableConfig, Transition, ViewConfig, ViewInfo},
|
||||
nu_common::{collect_pipeline, run_command_with_value},
|
||||
pager::{report::Report, Frame, Transition, ViewInfo},
|
||||
util::create_map,
|
||||
};
|
||||
|
||||
use super::{record::RecordView, Layout, View};
|
||||
use super::{
|
||||
record::{RecordView, TableTheme},
|
||||
util::nu_style_to_tui,
|
||||
Layout, Orientation, View, ViewConfig,
|
||||
};
|
||||
|
||||
pub struct InteractiveView<'a> {
|
||||
input: Value,
|
||||
command: String,
|
||||
imidiate: bool,
|
||||
table: Option<RecordView<'a>>,
|
||||
table_theme: TableTheme,
|
||||
view_mode: bool,
|
||||
// todo: impl Debug for it
|
||||
table_cfg: TableConfig,
|
||||
border_color: Style,
|
||||
highlighted_color: Style,
|
||||
}
|
||||
|
||||
impl<'a> InteractiveView<'a> {
|
||||
pub fn new(input: Value, table_cfg: TableConfig) -> Self {
|
||||
pub fn new(input: Value) -> Self {
|
||||
Self {
|
||||
input,
|
||||
table_cfg,
|
||||
table: None,
|
||||
imidiate: false,
|
||||
table_theme: TableTheme::default(),
|
||||
border_color: Style::default(),
|
||||
highlighted_color: Style::default(),
|
||||
view_mode: false,
|
||||
command: String::new(),
|
||||
}
|
||||
@ -41,13 +52,25 @@ impl<'a> InteractiveView<'a> {
|
||||
pub fn init(&mut self, command: String) {
|
||||
self.command = command;
|
||||
}
|
||||
|
||||
pub fn try_run(&mut self, engine_state: &EngineState, stack: &mut Stack) -> Result<(), String> {
|
||||
let mut view = run_command(&self.command, &self.input, engine_state, stack)?;
|
||||
view.set_theme(self.table_theme.clone());
|
||||
|
||||
self.table = Some(view);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl View for InteractiveView<'_> {
|
||||
fn draw(&mut self, f: &mut Frame, area: Rect, cfg: &ViewConfig, layout: &mut Layout) {
|
||||
fn draw(&mut self, f: &mut Frame, area: Rect, cfg: ViewConfig<'_>, layout: &mut Layout) {
|
||||
let border_color = self.border_color;
|
||||
let highlighted_color = self.highlighted_color;
|
||||
|
||||
let cmd_block = tui::widgets::Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Plain);
|
||||
.border_type(BorderType::Plain)
|
||||
.border_style(border_color);
|
||||
let cmd_area = Rect::new(area.x + 1, area.y, area.width - 2, 3);
|
||||
|
||||
let cmd_block = if self.view_mode {
|
||||
@ -56,6 +79,7 @@ impl View for InteractiveView<'_> {
|
||||
cmd_block
|
||||
.border_style(Style::default().add_modifier(Modifier::BOLD))
|
||||
.border_type(BorderType::Double)
|
||||
.border_style(highlighted_color)
|
||||
};
|
||||
|
||||
f.render_widget(cmd_block, cmd_area);
|
||||
@ -97,13 +121,15 @@ impl View for InteractiveView<'_> {
|
||||
|
||||
let table_block = tui::widgets::Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Plain);
|
||||
.border_type(BorderType::Plain)
|
||||
.border_style(border_color);
|
||||
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)
|
||||
.border_style(highlighted_color)
|
||||
} else {
|
||||
table_block
|
||||
};
|
||||
@ -136,13 +162,18 @@ impl View for InteractiveView<'_> {
|
||||
.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;
|
||||
let was_at_the_top = table.get_current_position().0 == 0;
|
||||
|
||||
if was_at_the_top && matches!(key.code, KeyCode::Up | KeyCode::PageUp) {
|
||||
self.view_mode = false;
|
||||
return Some(Transition::Ok);
|
||||
}
|
||||
|
||||
if matches!(key.code, KeyCode::Tab) {
|
||||
self.view_mode = false;
|
||||
return Some(Transition::Ok);
|
||||
}
|
||||
|
||||
let result = table.handle_input(engine_state, stack, layout, info, key);
|
||||
|
||||
return match result {
|
||||
@ -160,15 +191,32 @@ impl View for InteractiveView<'_> {
|
||||
KeyCode::Backspace => {
|
||||
if !self.command.is_empty() {
|
||||
self.command.pop();
|
||||
|
||||
if self.imidiate {
|
||||
match self.try_run(engine_state, stack) {
|
||||
Ok(_) => info.report = Some(Report::default()),
|
||||
Err(err) => {
|
||||
info.report = Some(Report::error(format!("Error: {}", err)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(Transition::Ok)
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
self.command.push(*c);
|
||||
|
||||
if self.imidiate {
|
||||
match self.try_run(engine_state, stack) {
|
||||
Ok(_) => info.report = Some(Report::default()),
|
||||
Err(err) => info.report = Some(Report::error(format!("Error: {}", err))),
|
||||
}
|
||||
}
|
||||
|
||||
Some(Transition::Ok)
|
||||
}
|
||||
KeyCode::Down => {
|
||||
KeyCode::Down | KeyCode::Tab => {
|
||||
if self.table.is_some() {
|
||||
self.view_mode = true;
|
||||
}
|
||||
@ -176,27 +224,9 @@ impl View for InteractiveView<'_> {
|
||||
Some(Transition::Ok)
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
if is_ignored_command(&self.command) {
|
||||
info.report = Some(Report::error(String::from("The command is ignored")));
|
||||
return Some(Transition::Ok);
|
||||
}
|
||||
|
||||
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)));
|
||||
}
|
||||
match self.try_run(engine_state, stack) {
|
||||
Ok(_) => info.report = Some(Report::default()),
|
||||
Err(err) => info.report = Some(Report::error(format!("Error: {}", err))),
|
||||
}
|
||||
|
||||
Some(Transition::Ok)
|
||||
@ -218,4 +248,58 @@ impl View for InteractiveView<'_> {
|
||||
fn show_data(&mut self, i: usize) -> bool {
|
||||
self.table.as_mut().map_or(false, |v| v.show_data(i))
|
||||
}
|
||||
|
||||
fn setup(&mut self, config: ViewConfig<'_>) {
|
||||
if let Some(hm) = config.config.get("try").and_then(create_map) {
|
||||
let colors = get_color_map(&hm);
|
||||
|
||||
if let Some(color) = colors.get("border_color").copied() {
|
||||
self.border_color = nu_style_to_tui(color);
|
||||
}
|
||||
|
||||
if let Some(color) = colors.get("highlighted_color").copied() {
|
||||
self.highlighted_color = nu_style_to_tui(color);
|
||||
}
|
||||
|
||||
if self.border_color != Style::default() && self.highlighted_color == Style::default() {
|
||||
self.highlighted_color = self.border_color;
|
||||
}
|
||||
|
||||
if let Some(val) = hm.get("reactive").and_then(|v| v.as_bool().ok()) {
|
||||
self.imidiate = val;
|
||||
}
|
||||
}
|
||||
|
||||
let mut r = RecordView::new(vec![], vec![]);
|
||||
r.setup(config);
|
||||
|
||||
self.table_theme = r.get_theme().clone();
|
||||
|
||||
if let Some(view) = &mut self.table {
|
||||
view.set_theme(self.table_theme.clone());
|
||||
view.set_orientation(r.get_orientation_current());
|
||||
view.set_orientation_current(r.get_orientation_current());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run_command(
|
||||
command: &str,
|
||||
input: &Value,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
) -> Result<RecordView<'static>, String> {
|
||||
let pipeline =
|
||||
run_command_with_value(command, input, engine_state, stack).map_err(|e| e.to_string())?;
|
||||
|
||||
let is_record = matches!(pipeline, PipelineData::Value(Value::Record { .. }, ..));
|
||||
|
||||
let (columns, values) = collect_pipeline(pipeline);
|
||||
|
||||
let mut view = RecordView::new(columns, values);
|
||||
if is_record {
|
||||
view.set_orientation_current(Orientation::Left);
|
||||
}
|
||||
|
||||
Ok(view)
|
||||
}
|
||||
|
@ -1,8 +1,10 @@
|
||||
mod coloredtextw;
|
||||
mod cursor;
|
||||
mod information;
|
||||
mod interative;
|
||||
mod preview;
|
||||
mod record;
|
||||
pub mod util;
|
||||
|
||||
use crossterm::event::KeyEvent;
|
||||
use nu_protocol::{
|
||||
@ -11,15 +13,23 @@ use nu_protocol::{
|
||||
};
|
||||
use tui::layout::Rect;
|
||||
|
||||
use super::{
|
||||
nu_common::NuText,
|
||||
pager::{Frame, Transition, ViewConfig, ViewInfo},
|
||||
use crate::{
|
||||
nu_common::{NuConfig, NuStyleTable},
|
||||
pager::ConfigMap,
|
||||
};
|
||||
|
||||
use super::{
|
||||
nu_common::NuText,
|
||||
pager::{Frame, Transition, ViewInfo},
|
||||
};
|
||||
|
||||
pub mod configuration;
|
||||
|
||||
pub use configuration::ConfigurationView;
|
||||
pub use information::InformationView;
|
||||
pub use interative::InteractiveView;
|
||||
pub use preview::Preview;
|
||||
pub use record::{RecordView, RecordViewState};
|
||||
pub use record::{Orientation, RecordView};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Layout {
|
||||
@ -48,8 +58,25 @@ impl ElementInfo {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ViewConfig<'a> {
|
||||
pub nu_config: &'a NuConfig,
|
||||
pub color_hm: &'a NuStyleTable,
|
||||
pub config: &'a ConfigMap,
|
||||
}
|
||||
|
||||
impl<'a> ViewConfig<'a> {
|
||||
pub fn new(nu_config: &'a NuConfig, color_hm: &'a NuStyleTable, config: &'a ConfigMap) -> Self {
|
||||
Self {
|
||||
nu_config,
|
||||
color_hm,
|
||||
config,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait View {
|
||||
fn draw(&mut self, f: &mut Frame, area: Rect, cfg: &ViewConfig, layout: &mut Layout);
|
||||
fn draw(&mut self, f: &mut Frame, area: Rect, cfg: ViewConfig<'_>, layout: &mut Layout);
|
||||
|
||||
fn handle_input(
|
||||
&mut self,
|
||||
@ -71,10 +98,12 @@ pub trait View {
|
||||
fn exit(&mut self) -> Option<Value> {
|
||||
None
|
||||
}
|
||||
|
||||
fn setup(&mut self, _: ViewConfig<'_>) {}
|
||||
}
|
||||
|
||||
impl View for Box<dyn View> {
|
||||
fn draw(&mut self, f: &mut Frame, area: Rect, cfg: &ViewConfig, layout: &mut Layout) {
|
||||
fn draw(&mut self, f: &mut Frame, area: Rect, cfg: ViewConfig<'_>, layout: &mut Layout) {
|
||||
self.as_mut().draw(f, area, cfg, layout)
|
||||
}
|
||||
|
||||
@ -101,4 +130,8 @@ impl View for Box<dyn View> {
|
||||
fn show_data(&mut self, i: usize) -> bool {
|
||||
self.as_mut().show_data(i)
|
||||
}
|
||||
|
||||
fn setup(&mut self, cfg: ViewConfig<'_>) {
|
||||
self.as_mut().setup(cfg)
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
use std::cmp::{max, min};
|
||||
use std::cmp::max;
|
||||
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use nu_protocol::{
|
||||
@ -10,18 +10,17 @@ use tui::layout::Rect;
|
||||
|
||||
use crate::{
|
||||
nu_common::{NuSpan, NuText},
|
||||
pager::{Frame, Report, Severity, Transition, ViewConfig, ViewInfo},
|
||||
pager::{report::Report, Frame, Transition, ViewInfo},
|
||||
};
|
||||
|
||||
use super::{coloredtextw::ColoredTextW, Layout, View};
|
||||
use super::{coloredtextw::ColoredTextW, cursor::XYCursor, Layout, View, ViewConfig};
|
||||
|
||||
// todo: Add wrap option
|
||||
#[derive(Debug)]
|
||||
pub struct Preview {
|
||||
underlaying_value: Option<Value>,
|
||||
lines: Vec<String>,
|
||||
i_row: usize,
|
||||
i_col: usize,
|
||||
screen_size: u16,
|
||||
cursor: XYCursor,
|
||||
}
|
||||
|
||||
impl Preview {
|
||||
@ -30,132 +29,98 @@ impl Preview {
|
||||
.lines()
|
||||
.map(|line| line.replace('\t', " ")) // tui: doesn't support TAB
|
||||
.collect();
|
||||
let cursor = XYCursor::new(lines.len(), usize::MAX);
|
||||
|
||||
Self {
|
||||
lines,
|
||||
i_col: 0,
|
||||
i_row: 0,
|
||||
screen_size: 0,
|
||||
cursor,
|
||||
underlaying_value: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_value(&mut self, value: Value) {
|
||||
self.underlaying_value = Some(value);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
fn draw(&mut self, f: &mut Frame, area: Rect, _: ViewConfig<'_>, layout: &mut Layout) {
|
||||
self.cursor
|
||||
.set_window(area.height as usize, area.width as usize);
|
||||
|
||||
let lines = &self.lines[self.i_row..];
|
||||
let lines = &self.lines[self.cursor.row_starts_at()..];
|
||||
for (i, line) in lines.iter().enumerate().take(area.height as usize) {
|
||||
let text = ColoredTextW::new(line, self.i_col);
|
||||
let text = ColoredTextW::new(line, self.cursor.column());
|
||||
let s = text.what(area);
|
||||
|
||||
let area = Rect::new(area.x, area.y + i as u16, area.width, 1);
|
||||
f.render_widget(text, area);
|
||||
|
||||
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,
|
||||
_: &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);
|
||||
}
|
||||
self.cursor
|
||||
.prev_column_by(max(1, self.cursor.column_window_size() / 2));
|
||||
|
||||
Some(Transition::Ok)
|
||||
}
|
||||
KeyCode::Right => {
|
||||
self.i_col += max(1, self.screen_size as usize / 2);
|
||||
self.cursor
|
||||
.next_column_by(max(1, self.cursor.column_window_size() / 2));
|
||||
|
||||
Some(Transition::Ok)
|
||||
}
|
||||
KeyCode::Up => {
|
||||
let is_start = self.i_row == 0;
|
||||
if is_start {
|
||||
// noop
|
||||
return Some(Transition::Ok);
|
||||
}
|
||||
self.cursor.prev_row_i();
|
||||
|
||||
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 {
|
||||
if self.cursor.row_starts_at() == 0 {
|
||||
info.status = Some(Report::info("TOP"));
|
||||
} else {
|
||||
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);
|
||||
if self.cursor.row() + self.cursor.row_window_size() < self.cursor.row_limit() {
|
||||
self.cursor.next_row_i();
|
||||
|
||||
let is_end = self.i_row == max;
|
||||
if is_end {
|
||||
// noop
|
||||
return Some(Transition::Ok);
|
||||
}
|
||||
|
||||
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"),
|
||||
Severity::Info,
|
||||
String::new(),
|
||||
String::new(),
|
||||
);
|
||||
|
||||
info.status = Some(report);
|
||||
info.status = Some(Report::info("END"));
|
||||
} else {
|
||||
info.status = Some(Report::default());
|
||||
}
|
||||
|
||||
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;
|
||||
self.cursor.prev_row_page();
|
||||
|
||||
if max != 0 && was_end {
|
||||
if self.cursor.row_starts_at() == 0 {
|
||||
info.status = Some(Report::info("TOP"));
|
||||
} else {
|
||||
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);
|
||||
self.cursor.next_row_page();
|
||||
|
||||
let is_end = self.i_row == max;
|
||||
if is_end {
|
||||
let report = Report::new(
|
||||
String::from("END"),
|
||||
Severity::Info,
|
||||
String::new(),
|
||||
String::new(),
|
||||
);
|
||||
|
||||
info.status = Some(report);
|
||||
if self.cursor.row() + 1 == self.cursor.row_limit() {
|
||||
info.status = Some(Report::info("END"));
|
||||
} else {
|
||||
info.status = Some(Report::default());
|
||||
}
|
||||
|
||||
Some(Transition::Ok)
|
||||
@ -177,12 +142,17 @@ impl View for Preview {
|
||||
//
|
||||
// todo: improve somehow?
|
||||
|
||||
self.i_row = row;
|
||||
self.cursor.set_position(row, 0);
|
||||
true
|
||||
}
|
||||
|
||||
fn exit(&mut self) -> Option<Value> {
|
||||
let text = self.lines.join("\n");
|
||||
Some(Value::string(text, NuSpan::unknown()))
|
||||
match &self.underlaying_value {
|
||||
Some(value) => Some(value.clone()),
|
||||
None => {
|
||||
let text = self.lines.join("\n");
|
||||
Some(Value::string(text, NuSpan::unknown()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,9 @@
|
||||
mod tablew;
|
||||
|
||||
use std::{borrow::Cow, cmp::min, collections::HashMap};
|
||||
use std::{borrow::Cow, collections::HashMap};
|
||||
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use nu_color_config::get_color_map;
|
||||
use nu_protocol::{
|
||||
engine::{EngineState, Stack},
|
||||
Value,
|
||||
@ -10,47 +11,127 @@ use nu_protocol::{
|
||||
use tui::{layout::Rect, widgets::Block};
|
||||
|
||||
use crate::{
|
||||
nu_common::{collect_input, NuConfig, NuSpan, NuStyleTable, NuText},
|
||||
nu_common::{collect_input, NuConfig, NuSpan, NuStyle, NuStyleTable, NuText},
|
||||
pager::{
|
||||
make_styled_string, nu_style_to_tui, Frame, Position, Report, Severity, StyleConfig,
|
||||
TableConfig, Transition, ViewConfig, ViewInfo,
|
||||
report::{Report, Severity},
|
||||
ConfigMap, Frame, Transition, ViewInfo,
|
||||
},
|
||||
util::create_map,
|
||||
views::ElementInfo,
|
||||
};
|
||||
|
||||
use self::tablew::{TableW, TableWState};
|
||||
use self::tablew::{TableStyle, TableW, TableWState};
|
||||
|
||||
use super::{Layout, View};
|
||||
use super::{
|
||||
cursor::XYCursor,
|
||||
util::{make_styled_string, nu_style_to_tui},
|
||||
Layout, View, ViewConfig,
|
||||
};
|
||||
|
||||
pub use self::tablew::Orientation;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RecordView<'a> {
|
||||
layer_stack: Vec<RecordLayer<'a>>,
|
||||
mode: UIMode,
|
||||
cfg: TableConfig,
|
||||
pub(crate) cursor: Position,
|
||||
state: RecordViewState,
|
||||
orientation: Orientation,
|
||||
theme: TableTheme,
|
||||
}
|
||||
|
||||
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(),
|
||||
orientation: Orientation::Top,
|
||||
theme: TableTheme::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);
|
||||
let page_size =
|
||||
estimate_page_size(Rect::new(0, 0, width, height), self.theme.table.show_header);
|
||||
state_reverse_data(self, page_size as usize);
|
||||
}
|
||||
|
||||
pub fn set_style_split_line(&mut self, style: NuStyle) {
|
||||
self.theme.table.splitline_style = style
|
||||
}
|
||||
|
||||
pub fn set_style_selected_cell(&mut self, style: NuStyle) {
|
||||
self.theme.cursor.selected_cell = Some(style)
|
||||
}
|
||||
|
||||
pub fn set_style_selected_row(&mut self, style: NuStyle) {
|
||||
self.theme.cursor.selected_row = Some(style)
|
||||
}
|
||||
|
||||
pub fn set_style_selected_column(&mut self, style: NuStyle) {
|
||||
self.theme.cursor.selected_column = Some(style)
|
||||
}
|
||||
|
||||
pub fn show_cursor(&mut self, b: bool) {
|
||||
self.theme.cursor.show_cursow = b;
|
||||
}
|
||||
|
||||
pub fn set_line_head_top(&mut self, b: bool) {
|
||||
self.theme.table.header_top = b;
|
||||
}
|
||||
|
||||
pub fn set_line_head_bottom(&mut self, b: bool) {
|
||||
self.theme.table.header_bottom = b;
|
||||
}
|
||||
|
||||
pub fn set_line_traling(&mut self, b: bool) {
|
||||
self.theme.table.shift_line = b;
|
||||
}
|
||||
|
||||
pub fn set_line_index(&mut self, b: bool) {
|
||||
self.theme.table.index_line = b;
|
||||
}
|
||||
|
||||
pub fn set_padding_column(&mut self, (left, right): (usize, usize)) {
|
||||
self.theme.table.padding_column_left = left;
|
||||
self.theme.table.padding_column_right = right;
|
||||
}
|
||||
|
||||
pub fn set_padding_index(&mut self, (left, right): (usize, usize)) {
|
||||
self.theme.table.padding_index_left = left;
|
||||
self.theme.table.padding_index_right = right;
|
||||
}
|
||||
|
||||
pub fn get_padding_column(&self) -> (usize, usize) {
|
||||
(
|
||||
self.theme.table.padding_column_left,
|
||||
self.theme.table.padding_column_right,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_padding_index(&self) -> (usize, usize) {
|
||||
(
|
||||
self.theme.table.padding_index_left,
|
||||
self.theme.table.padding_index_right,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_theme(&self) -> &TableTheme {
|
||||
&self.theme
|
||||
}
|
||||
|
||||
pub fn set_theme(&mut self, theme: TableTheme) {
|
||||
self.theme = theme;
|
||||
}
|
||||
|
||||
pub fn transpose(&mut self) {
|
||||
let layer = self.get_layer_last_mut();
|
||||
transpose_table(layer);
|
||||
|
||||
layer.reset_cursor();
|
||||
}
|
||||
|
||||
// todo: rename to get_layer
|
||||
pub fn get_layer_last(&self) -> &RecordLayer<'a> {
|
||||
self.layer_stack
|
||||
@ -64,46 +145,137 @@ impl<'a> RecordView<'a> {
|
||||
.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);
|
||||
pub fn get_orientation_current(&mut self) -> Orientation {
|
||||
self.get_layer_last().orientation
|
||||
}
|
||||
|
||||
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,
|
||||
pub fn set_orientation(&mut self, orientation: Orientation) {
|
||||
self.orientation = orientation;
|
||||
|
||||
// we need to reset all indexes as we can't no more use them.
|
||||
self.reset_cursors();
|
||||
}
|
||||
|
||||
fn reset_cursors(&mut self) {
|
||||
for layer in &mut self.layer_stack {
|
||||
layer.reset_cursor();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_orientation_current(&mut self, orientation: Orientation) {
|
||||
let layer = self.get_layer_last_mut();
|
||||
layer.orientation = orientation;
|
||||
layer.reset_cursor();
|
||||
}
|
||||
|
||||
pub fn get_current_position(&self) -> (usize, usize) {
|
||||
let layer = self.get_layer_last();
|
||||
(layer.cursor.row(), layer.cursor.column())
|
||||
}
|
||||
|
||||
pub fn get_current_window(&self) -> (usize, usize) {
|
||||
let layer = self.get_layer_last();
|
||||
(layer.cursor.row_window(), layer.cursor.column_window())
|
||||
}
|
||||
|
||||
pub fn get_current_offset(&self) -> (usize, usize) {
|
||||
let layer = self.get_layer_last();
|
||||
(
|
||||
layer.cursor.row_starts_at(),
|
||||
layer.cursor.column_starts_at(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_cursor_mode(&mut self) {
|
||||
self.mode = UIMode::Cursor;
|
||||
}
|
||||
|
||||
pub fn set_view_mode(&mut self) {
|
||||
self.mode = UIMode::View;
|
||||
}
|
||||
|
||||
pub fn get_current_value(&self) -> Value {
|
||||
let (row, column) = self.get_current_position();
|
||||
let layer = self.get_layer_last();
|
||||
|
||||
let (row, column) = match layer.orientation {
|
||||
Orientation::Top | Orientation::Bottom => (row, column),
|
||||
Orientation::Left | Orientation::Right => (column, row),
|
||||
};
|
||||
|
||||
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;
|
||||
layer.records[row][column].clone()
|
||||
}
|
||||
|
||||
TableW::new(headers, data, color_hm, i_row, i_column, style)
|
||||
fn create_tablew(&'a self, cfg: ViewConfig<'a>) -> TableW<'a> {
|
||||
let layer = self.get_layer_last();
|
||||
let data = convert_records_to_string(&layer.records, cfg.nu_config, cfg.color_hm);
|
||||
|
||||
let headers = layer.columns.as_ref();
|
||||
let color_hm = cfg.color_hm;
|
||||
let (row, column) = self.get_current_offset();
|
||||
|
||||
TableW::new(
|
||||
headers,
|
||||
data,
|
||||
color_hm,
|
||||
row,
|
||||
column,
|
||||
self.theme.table,
|
||||
layer.orientation,
|
||||
)
|
||||
}
|
||||
|
||||
fn update_cursors(&mut self, rows: usize, columns: usize) {
|
||||
match self.get_layer_last().orientation {
|
||||
Orientation::Top | Orientation::Bottom => {
|
||||
self.get_layer_last_mut().cursor.set_window(rows, columns);
|
||||
}
|
||||
|
||||
Orientation::Left | Orientation::Right => {
|
||||
self.get_layer_last_mut().cursor.set_window(rows, columns);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_records_report(&self) -> Report {
|
||||
let layer = self.get_layer_last();
|
||||
let covered_percent = report_row_position(layer.cursor);
|
||||
let cursor = report_cursor_position(self.mode, layer.cursor);
|
||||
let message = layer.name.clone().unwrap_or_default();
|
||||
|
||||
Report {
|
||||
message,
|
||||
context: covered_percent,
|
||||
context2: cursor,
|
||||
level: Severity::Info,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
fn draw(&mut self, f: &mut Frame, area: Rect, cfg: ViewConfig<'_>, layout: &mut Layout) {
|
||||
let mut table_layout = TableWState::default();
|
||||
let table = self.create_tablew(cfg);
|
||||
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,
|
||||
};
|
||||
|
||||
self.update_cursors(table_layout.count_rows, table_layout.count_columns);
|
||||
|
||||
if self.mode == UIMode::Cursor {
|
||||
let cursor = get_cursor(self);
|
||||
highlight_cell(f, area, &self.state, cursor, cfg.theme);
|
||||
let (row, column) = self.get_current_window();
|
||||
let info = get_element_info(
|
||||
layout,
|
||||
row,
|
||||
column,
|
||||
table_layout.count_rows,
|
||||
self.get_layer_last().orientation,
|
||||
self.theme.table.show_header,
|
||||
);
|
||||
|
||||
if let Some(info) = info {
|
||||
highlight_cell(f, area, info.clone(), &self.theme.cursor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,19 +289,11 @@ impl View for RecordView<'_> {
|
||||
) -> 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)
|
||||
}
|
||||
UIMode::Cursor => 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);
|
||||
|
||||
let report = self.create_records_report();
|
||||
info.status = Some(report);
|
||||
}
|
||||
|
||||
@ -158,10 +322,7 @@ impl View for RecordView<'_> {
|
||||
|
||||
for (column, _) in cells.iter().enumerate() {
|
||||
if i == pos {
|
||||
let layer = self.get_layer_last_mut();
|
||||
layer.index_column = column;
|
||||
layer.index_row = row;
|
||||
|
||||
self.get_layer_last_mut().cursor.set_position(row, column);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -175,6 +336,46 @@ impl View for RecordView<'_> {
|
||||
fn exit(&mut self) -> Option<Value> {
|
||||
Some(build_last_value(self))
|
||||
}
|
||||
|
||||
// todo: move the method to Command?
|
||||
fn setup(&mut self, cfg: ViewConfig<'_>) {
|
||||
if let Some(hm) = cfg.config.get("table").and_then(create_map) {
|
||||
self.theme = theme_from_config(&hm);
|
||||
|
||||
if let Some(orientation) = hm.get("orientation").and_then(|v| v.as_string().ok()) {
|
||||
let orientation = match orientation.as_str() {
|
||||
"left" => Some(Orientation::Left),
|
||||
"right" => Some(Orientation::Right),
|
||||
"top" => Some(Orientation::Top),
|
||||
"bottom" => Some(Orientation::Bottom),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(orientation) = orientation {
|
||||
self.set_orientation(orientation);
|
||||
self.set_orientation_current(orientation);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_element_info(
|
||||
layout: &mut Layout,
|
||||
row: usize,
|
||||
column: usize,
|
||||
count_rows: usize,
|
||||
orientation: Orientation,
|
||||
with_head: bool,
|
||||
) -> Option<&ElementInfo> {
|
||||
let with_head = with_head as usize;
|
||||
let index = match orientation {
|
||||
Orientation::Top | Orientation::Bottom => column * (count_rows + with_head) + row + 1,
|
||||
Orientation::Left => (column + with_head) * count_rows + row,
|
||||
Orientation::Right => column * count_rows + row,
|
||||
};
|
||||
|
||||
layout.data.get(index)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
@ -187,10 +388,10 @@ enum UIMode {
|
||||
pub struct RecordLayer<'a> {
|
||||
columns: Cow<'a, [String]>,
|
||||
records: Cow<'a, [Vec<Value>]>,
|
||||
pub(crate) index_row: usize,
|
||||
pub(crate) index_column: usize,
|
||||
orientation: Orientation,
|
||||
name: Option<String>,
|
||||
was_transposed: bool,
|
||||
cursor: XYCursor,
|
||||
}
|
||||
|
||||
impl<'a> RecordLayer<'a> {
|
||||
@ -198,11 +399,15 @@ impl<'a> RecordLayer<'a> {
|
||||
columns: impl Into<Cow<'a, [String]>>,
|
||||
records: impl Into<Cow<'a, [Vec<Value>]>>,
|
||||
) -> Self {
|
||||
let columns = columns.into();
|
||||
let records = records.into();
|
||||
let cursor = XYCursor::new(records.len(), columns.len());
|
||||
|
||||
Self {
|
||||
columns: columns.into(),
|
||||
records: records.into(),
|
||||
index_row: 0,
|
||||
index_column: 0,
|
||||
columns,
|
||||
records,
|
||||
cursor,
|
||||
orientation: Orientation::Top,
|
||||
name: None,
|
||||
was_transposed: false,
|
||||
}
|
||||
@ -213,33 +418,27 @@ impl<'a> RecordLayer<'a> {
|
||||
}
|
||||
|
||||
fn count_rows(&self) -> usize {
|
||||
self.records.len()
|
||||
match self.orientation {
|
||||
Orientation::Top | Orientation::Bottom => self.records.len(),
|
||||
Orientation::Left | Orientation::Right => self.columns.len(),
|
||||
}
|
||||
}
|
||||
|
||||
fn count_columns(&self) -> usize {
|
||||
self.columns.len()
|
||||
match self.orientation {
|
||||
Orientation::Top | Orientation::Bottom => self.columns.len(),
|
||||
Orientation::Left | Orientation::Right => self.records.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;
|
||||
|
||||
fn get_column_header(&self) -> Option<String> {
|
||||
let col = self.cursor.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 reset_cursor(&mut self) {
|
||||
self.cursor = XYCursor::new(self.count_rows(), self.count_columns());
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_key_event_view_mode(view: &mut RecordView, key: &KeyEvent) -> Option<Transition> {
|
||||
@ -247,64 +446,51 @@ fn handle_key_event_view_mode(view: &mut RecordView, key: &KeyEvent) -> Option<T
|
||||
KeyCode::Esc => {
|
||||
if view.layer_stack.len() > 1 {
|
||||
view.layer_stack.pop();
|
||||
view.mode = UIMode::Cursor;
|
||||
|
||||
Some(Transition::Ok)
|
||||
} else {
|
||||
Some(Transition::Exit)
|
||||
}
|
||||
}
|
||||
KeyCode::Char('i') => {
|
||||
view.mode = UIMode::Cursor;
|
||||
view.cursor = Position::default();
|
||||
KeyCode::Char('i') | KeyCode::Enter => {
|
||||
view.set_cursor_mode();
|
||||
|
||||
Some(Transition::Ok)
|
||||
}
|
||||
KeyCode::Char('t') => {
|
||||
let layer = view.get_layer_last_mut();
|
||||
layer.index_column = 0;
|
||||
layer.index_row = 0;
|
||||
|
||||
transpose_table(layer);
|
||||
view.transpose();
|
||||
|
||||
Some(Transition::Ok)
|
||||
}
|
||||
KeyCode::Char('e') => Some(Transition::Cmd(String::from("expand"))),
|
||||
KeyCode::Up => {
|
||||
let layer = view.get_layer_last_mut();
|
||||
layer.index_row = layer.index_row.saturating_sub(1);
|
||||
view.get_layer_last_mut().cursor.prev_row_i();
|
||||
|
||||
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);
|
||||
view.get_layer_last_mut().cursor.next_row_i();
|
||||
|
||||
Some(Transition::Ok)
|
||||
}
|
||||
KeyCode::Left => {
|
||||
let layer = view.get_layer_last_mut();
|
||||
layer.index_column = layer.index_column.saturating_sub(1);
|
||||
view.get_layer_last_mut().cursor.prev_column_i();
|
||||
|
||||
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);
|
||||
view.get_layer_last_mut().cursor.next_column_i();
|
||||
|
||||
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);
|
||||
view.get_layer_last_mut().cursor.prev_row_page();
|
||||
|
||||
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);
|
||||
view.get_layer_last_mut().cursor.next_row_page();
|
||||
|
||||
Some(Transition::Ok)
|
||||
}
|
||||
@ -315,83 +501,62 @@ fn handle_key_event_view_mode(view: &mut RecordView, key: &KeyEvent) -> Option<T
|
||||
fn handle_key_event_cursor_mode(view: &mut RecordView, key: &KeyEvent) -> Option<Transition> {
|
||||
match key.code {
|
||||
KeyCode::Esc => {
|
||||
view.mode = UIMode::View;
|
||||
view.cursor = Position::default();
|
||||
view.set_view_mode();
|
||||
|
||||
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
|
||||
}
|
||||
view.get_layer_last_mut().cursor.prev_row();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
view.get_layer_last_mut().cursor.next_row();
|
||||
|
||||
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
|
||||
}
|
||||
view.get_layer_last_mut().cursor.prev_column();
|
||||
|
||||
Some(Transition::Ok)
|
||||
}
|
||||
KeyCode::Right => {
|
||||
let cursor = view.cursor;
|
||||
let showed_columns = view.state.count_columns;
|
||||
let layer = view.get_layer_last_mut();
|
||||
view.get_layer_last_mut().cursor.next_column();
|
||||
|
||||
let total_columns = layer.count_columns();
|
||||
let column_index = layer.index_column + cursor.x as usize + 1;
|
||||
Some(Transition::Ok)
|
||||
}
|
||||
KeyCode::PageUp => {
|
||||
view.get_layer_last_mut().cursor.prev_row_page();
|
||||
|
||||
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::PageDown => {
|
||||
view.get_layer_last_mut().cursor.next_row_page();
|
||||
|
||||
Some(Transition::Ok)
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
let next_layer = get_peeked_layer(view);
|
||||
let value = view.get_current_value();
|
||||
let is_record = matches!(value, Value::Record { .. });
|
||||
let next_layer = create_layer(value);
|
||||
|
||||
push_layer(view, next_layer);
|
||||
|
||||
if is_record {
|
||||
view.set_orientation_current(Orientation::Left);
|
||||
} else if view.orientation == view.get_layer_last().orientation {
|
||||
view.get_layer_last_mut().orientation = view.orientation;
|
||||
} else {
|
||||
view.set_orientation_current(view.orientation);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
fn create_layer(value: Value) -> RecordLayer<'static> {
|
||||
let (columns, values) = collect_input(value);
|
||||
|
||||
RecordLayer::new(columns, values)
|
||||
@ -399,16 +564,13 @@ fn get_peeked_layer(view: &RecordView) -> RecordLayer<'static> {
|
||||
|
||||
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);
|
||||
let header = layer.get_column_header();
|
||||
|
||||
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 {
|
||||
@ -425,8 +587,8 @@ fn estimate_page_size(area: Rect, show_head: bool) -> u16 {
|
||||
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;
|
||||
if count_rows > page_size {
|
||||
layer.cursor.set_position(count_rows - page_size, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@ -451,56 +613,33 @@ fn convert_records_to_string(
|
||||
.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 highlight_cell(f: &mut Frame, area: Rect, info: ElementInfo, theme: &CursorStyle) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_cursor(v: &RecordView<'_>) -> Position {
|
||||
let count_rows = v.state.count_rows as u16;
|
||||
let count_columns = v.state.count_columns as u16;
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
cursor
|
||||
if theme.show_cursow {
|
||||
f.set_cursor(info.area.x, info.area.y);
|
||||
}
|
||||
}
|
||||
|
||||
fn build_last_value(v: &RecordView) -> Value {
|
||||
if v.mode == UIMode::Cursor {
|
||||
peak_current_value(v)
|
||||
v.get_current_value()
|
||||
} else if v.get_layer_last().count_rows() < 2 {
|
||||
build_table_as_record(v)
|
||||
} else {
|
||||
@ -508,15 +647,6 @@ fn build_last_value(v: &RecordView) -> Value {
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@ -551,40 +681,28 @@ fn build_table_as_record(v: &RecordView) -> Value {
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
fn report_cursor_position(mode: UIMode, cursor: XYCursor) -> String {
|
||||
if mode == UIMode::Cursor {
|
||||
let row = cursor.row();
|
||||
let column = cursor.column();
|
||||
format!("{},{}", row, column)
|
||||
} 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)
|
||||
}
|
||||
};
|
||||
let rows_seen = cursor.row_starts_at();
|
||||
let columns_seen = cursor.column_starts_at();
|
||||
format!("{},{}", rows_seen, columns_seen)
|
||||
}
|
||||
}
|
||||
|
||||
Report {
|
||||
message: title,
|
||||
context: covered_percent,
|
||||
context2: cursor,
|
||||
level: Severity::Info,
|
||||
fn report_row_position(cursor: XYCursor) -> String {
|
||||
if cursor.row_starts_at() == 0 {
|
||||
String::from("Top")
|
||||
} else {
|
||||
let percent_rows = get_percentage(cursor.row(), cursor.row_limit());
|
||||
|
||||
match percent_rows {
|
||||
100 => String::from("All"),
|
||||
value => format!("{}%", value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -595,8 +713,8 @@ fn get_percentage(value: usize, max: usize) -> usize {
|
||||
}
|
||||
|
||||
fn transpose_table(layer: &mut RecordLayer<'_>) {
|
||||
let count_rows = layer.count_rows();
|
||||
let count_columns = layer.count_columns();
|
||||
let count_rows = layer.records.len();
|
||||
let count_columns = layer.columns.len();
|
||||
|
||||
if layer.was_transposed {
|
||||
let data = match &mut layer.records {
|
||||
@ -656,3 +774,62 @@ fn _transpose_table(
|
||||
|
||||
data
|
||||
}
|
||||
|
||||
fn theme_from_config(config: &ConfigMap) -> TableTheme {
|
||||
let mut theme = TableTheme::default();
|
||||
|
||||
let colors = get_color_map(config);
|
||||
|
||||
if let Some(s) = colors.get("split_line") {
|
||||
theme.table.splitline_style = *s;
|
||||
}
|
||||
|
||||
theme.cursor.selected_cell = colors.get("selected_cell").cloned();
|
||||
theme.cursor.selected_row = colors.get("selected_row").cloned();
|
||||
theme.cursor.selected_column = colors.get("selected_column").cloned();
|
||||
theme.cursor.show_cursow = config_get_bool(config, "show_cursor", true);
|
||||
|
||||
theme.table.header_top = config_get_bool(config, "line_head_top", true);
|
||||
theme.table.header_bottom = config_get_bool(config, "line_head_bottom", true);
|
||||
theme.table.shift_line = config_get_bool(config, "line_shift", true);
|
||||
theme.table.index_line = config_get_bool(config, "line_index", true);
|
||||
|
||||
theme.table.show_header = config_get_bool(config, "show_head", true);
|
||||
theme.table.show_index = config_get_bool(config, "show_index", false);
|
||||
|
||||
theme.table.padding_index_left = config_get_usize(config, "padding_index_left", 2);
|
||||
theme.table.padding_index_right = config_get_usize(config, "padding_index_right", 1);
|
||||
theme.table.padding_column_left = config_get_usize(config, "padding_column_left", 2);
|
||||
theme.table.padding_column_right = config_get_usize(config, "padding_column_right", 2);
|
||||
|
||||
theme
|
||||
}
|
||||
|
||||
fn config_get_bool(config: &ConfigMap, key: &str, default: bool) -> bool {
|
||||
config
|
||||
.get(key)
|
||||
.and_then(|v| v.as_bool().ok())
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
fn config_get_usize(config: &ConfigMap, key: &str, default: usize) -> usize {
|
||||
config
|
||||
.get(key)
|
||||
.and_then(|v| v.as_string().ok())
|
||||
.and_then(|s| s.parse::<usize>().ok())
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct TableTheme {
|
||||
table: TableStyle,
|
||||
cursor: CursorStyle,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
struct CursorStyle {
|
||||
selected_cell: Option<NuStyle>,
|
||||
selected_column: Option<NuStyle>,
|
||||
selected_row: Option<NuStyle>,
|
||||
show_cursow: bool,
|
||||
}
|
||||
|
@ -1,4 +1,7 @@
|
||||
use std::{borrow::Cow, cmp::max, collections::HashMap};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cmp::{max, Ordering},
|
||||
};
|
||||
|
||||
use nu_table::{string_width, Alignment, TextStyle};
|
||||
use tui::{
|
||||
@ -9,30 +12,45 @@ use tui::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
nu_common::{NuStyle, NuStyleTable, NuText},
|
||||
pager::{nu_style_to_tui, text_style_to_tui_style},
|
||||
views::ElementInfo,
|
||||
nu_common::{truncate_str, NuStyle, NuStyleTable, NuText},
|
||||
views::util::{nu_style_to_tui, text_style_to_tui_style},
|
||||
};
|
||||
|
||||
use super::Layout;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TableW<'a> {
|
||||
columns: Cow<'a, [String]>,
|
||||
data: Cow<'a, [Vec<NuText>]>,
|
||||
index_row: usize,
|
||||
index_column: usize,
|
||||
style: TableStyle,
|
||||
head_position: Orientation,
|
||||
color_hm: &'a NuStyleTable,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Orientation {
|
||||
Top,
|
||||
Bottom,
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub struct TableStyle {
|
||||
pub splitline_style: NuStyle,
|
||||
pub shift_line_style: NuStyle,
|
||||
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,
|
||||
pub padding_index_left: usize,
|
||||
pub padding_index_right: usize,
|
||||
pub padding_column_left: usize,
|
||||
pub padding_column_right: usize,
|
||||
}
|
||||
|
||||
impl<'a> TableW<'a> {
|
||||
@ -44,14 +62,16 @@ impl<'a> TableW<'a> {
|
||||
index_row: usize,
|
||||
index_column: usize,
|
||||
style: TableStyle,
|
||||
head_position: Orientation,
|
||||
) -> Self {
|
||||
Self {
|
||||
columns: columns.into(),
|
||||
data: data.into(),
|
||||
color_hm,
|
||||
index_row,
|
||||
index_column,
|
||||
style,
|
||||
head_position,
|
||||
color_hm,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -61,7 +81,7 @@ pub struct TableWState {
|
||||
pub layout: Layout,
|
||||
pub count_rows: usize,
|
||||
pub count_columns: usize,
|
||||
pub data_index: HashMap<(usize, usize), ElementInfo>,
|
||||
pub data_height: u16,
|
||||
}
|
||||
|
||||
impl StatefulWidget for TableW<'_> {
|
||||
@ -73,29 +93,70 @@ impl StatefulWidget for TableW<'_> {
|
||||
buf: &mut tui::buffer::Buffer,
|
||||
state: &mut Self::State,
|
||||
) {
|
||||
const CELL_PADDING_LEFT: u16 = 2;
|
||||
const CELL_PADDING_RIGHT: u16 = 2;
|
||||
if area.width < 5 {
|
||||
return;
|
||||
}
|
||||
|
||||
let is_horizontal = matches!(self.head_position, Orientation::Top | Orientation::Bottom);
|
||||
if is_horizontal {
|
||||
self.render_table_horizontal(area, buf, state);
|
||||
} else {
|
||||
self.render_table_vertical(area, buf, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// todo: refactoring these to methods as they have quite a bit in common.
|
||||
impl<'a> TableW<'a> {
|
||||
fn render_table_horizontal(self, area: Rect, buf: &mut Buffer, state: &mut TableWState) {
|
||||
let padding_cell_l = self.style.padding_column_left as u16;
|
||||
let padding_cell_r = self.style.padding_column_right as u16;
|
||||
let padding_index_l = self.style.padding_index_left as u16;
|
||||
let padding_index_r = self.style.padding_index_right as u16;
|
||||
|
||||
let show_index = self.style.show_index;
|
||||
let show_head = self.style.show_header;
|
||||
|
||||
let splitline_s = self.style.splitline_style;
|
||||
let shift_column_s = self.style.shift_line_style;
|
||||
|
||||
let mut data_y = area.y;
|
||||
let mut data_height = area.height;
|
||||
let mut data_y = area.y;
|
||||
let mut head_y = area.y;
|
||||
if show_head {
|
||||
data_y += 1;
|
||||
data_height -= 1;
|
||||
|
||||
if self.style.header_top {
|
||||
let is_head_top = matches!(self.head_position, Orientation::Top);
|
||||
let is_head_bottom = matches!(self.head_position, Orientation::Bottom);
|
||||
|
||||
if show_head {
|
||||
if is_head_top {
|
||||
data_y += 1;
|
||||
data_height -= 1;
|
||||
head_y += 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 self.style.header_bottom {
|
||||
data_y += 1;
|
||||
if is_head_bottom {
|
||||
data_height -= 1;
|
||||
head_y = area.y + data_height;
|
||||
|
||||
if self.style.header_top && self.style.header_bottom {
|
||||
data_height -= 2;
|
||||
head_y -= 1
|
||||
} else if self.style.header_top {
|
||||
data_height -= 1;
|
||||
} else if self.style.header_bottom {
|
||||
data_height -= 1;
|
||||
head_y -= 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -104,103 +165,109 @@ impl StatefulWidget for TableW<'_> {
|
||||
}
|
||||
|
||||
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 is_head_top {
|
||||
render_header_borders(buf, area, 1, splitline_s, top, bottom);
|
||||
} else if is_head_bottom {
|
||||
let area = Rect::new(area.x, area.y + data_height, area.width, area.height);
|
||||
render_header_borders(buf, area, 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);
|
||||
width += render_index(
|
||||
buf,
|
||||
area,
|
||||
self.color_hm,
|
||||
self.index_row,
|
||||
padding_index_l,
|
||||
padding_index_r,
|
||||
);
|
||||
|
||||
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 head_t = show_head && is_head_top && self.style.header_bottom;
|
||||
let head_b = show_head && is_head_bottom && self.style.header_top;
|
||||
width +=
|
||||
render_vertical(buf, width, data_y, data_height, head_t, head_b, 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;
|
||||
state.data_height = data_height;
|
||||
|
||||
for (i, col) in (self.index_column..self.columns.len()).enumerate() {
|
||||
let mut head = String::from(&self.columns[col]);
|
||||
if width > area.width {
|
||||
return;
|
||||
}
|
||||
|
||||
for col in self.index_column..self.columns.len() {
|
||||
let mut column = create_column(data, col);
|
||||
|
||||
let column_width = calculate_column_width(&column);
|
||||
let mut use_space = column_width as u16;
|
||||
|
||||
let mut head = String::from(&self.columns[col]);
|
||||
let head_width = string_width(&head);
|
||||
|
||||
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;
|
||||
if use_space > 0 {
|
||||
let is_last = col + 1 == self.columns.len();
|
||||
let space = area.width - width;
|
||||
|
||||
let pad = padding_cell_l + padding_cell_r;
|
||||
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,
|
||||
},
|
||||
);
|
||||
let (w, ok, shift) =
|
||||
truncate_column_width(space, 1, use_space, pad, is_last, &mut column, head);
|
||||
|
||||
use_space = control.width;
|
||||
do_render_split_line = control.print_split_line;
|
||||
do_render_shift_column = control.print_shift_column;
|
||||
if shift {
|
||||
do_render_shift_column = true;
|
||||
}
|
||||
|
||||
if control.break_everything {
|
||||
if w == 0 && !ok {
|
||||
break;
|
||||
}
|
||||
|
||||
use_space = w;
|
||||
}
|
||||
|
||||
if show_head {
|
||||
let header = &[head_row_text(&head, self.color_hm)];
|
||||
let mut header = [head_row_text(&head, self.color_hm)];
|
||||
if head_width > use_space as usize {
|
||||
truncate_str(&mut header[0].0, use_space as usize)
|
||||
}
|
||||
|
||||
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);
|
||||
w += render_space(buf, w, head_y, 1, padding_cell_l);
|
||||
w += render_column(buf, w, head_y, use_space, &header);
|
||||
w += render_space(buf, w, head_y, 1, padding_cell_r);
|
||||
|
||||
let x = w - CELL_PADDING_RIGHT - use_space;
|
||||
let x = w - padding_cell_r - 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_space(buf, width, data_y, data_height, padding_cell_l);
|
||||
width += render_column(buf, width, data_y, use_space, &column);
|
||||
width += render_space(buf, width, data_y, data_height, CELL_PADDING_RIGHT);
|
||||
width += render_space(buf, width, data_y, data_height, padding_cell_r);
|
||||
|
||||
for (row, (text, _)) in column.iter().enumerate() {
|
||||
let x = width - CELL_PADDING_RIGHT - use_space;
|
||||
let x = width - padding_cell_r - 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;
|
||||
@ -216,18 +283,18 @@ impl StatefulWidget for TableW<'_> {
|
||||
// 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);
|
||||
width += render_space(buf, width, data_y, data_height, padding_cell_l);
|
||||
width += render_shift_column(buf, width, head_y, 1, shift_column_s);
|
||||
width += render_space(buf, width, data_y, data_height, padding_cell_r);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
if self.style.shift_line && width < area.width {
|
||||
let head_t = show_head && is_head_top && self.style.header_bottom;
|
||||
let head_b = show_head && is_head_bottom && self.style.header_top;
|
||||
width += render_vertical(buf, width, data_y, data_height, head_t, head_b, 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);
|
||||
@ -236,6 +303,240 @@ impl StatefulWidget for TableW<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_table_vertical(self, area: Rect, buf: &mut Buffer, state: &mut TableWState) {
|
||||
if area.width == 0 || area.height == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let padding_cell_l = self.style.padding_column_left as u16;
|
||||
let padding_cell_r = self.style.padding_column_right as u16;
|
||||
let padding_index_l = self.style.padding_index_left as u16;
|
||||
let padding_index_r = self.style.padding_index_right as u16;
|
||||
|
||||
let show_index = self.style.show_index;
|
||||
let show_head = self.style.show_header;
|
||||
let splitline_s = self.style.splitline_style;
|
||||
let shift_column_s = self.style.shift_line_style;
|
||||
|
||||
let is_head_left = matches!(self.head_position, Orientation::Left);
|
||||
let is_head_right = matches!(self.head_position, Orientation::Right);
|
||||
|
||||
let mut left_w = 0;
|
||||
let mut right_w = 0;
|
||||
|
||||
if show_index {
|
||||
let area = Rect::new(area.x, area.y, area.width, area.height);
|
||||
left_w += render_index(
|
||||
buf,
|
||||
area,
|
||||
self.color_hm,
|
||||
self.index_row,
|
||||
padding_index_l,
|
||||
padding_index_r,
|
||||
);
|
||||
|
||||
if self.style.index_line {
|
||||
let x = area.x + left_w;
|
||||
left_w += render_vertical(buf, x, area.y, area.height, false, false, splitline_s);
|
||||
}
|
||||
}
|
||||
|
||||
let mut columns = &self.columns[self.index_row..];
|
||||
if columns.len() > area.height as usize {
|
||||
columns = &columns[..area.height as usize];
|
||||
}
|
||||
|
||||
if show_head {
|
||||
let columns_width = columns.iter().map(|s| string_width(s)).max().unwrap_or(0);
|
||||
|
||||
let will_use_space =
|
||||
padding_cell_l as usize + padding_cell_r as usize + columns_width + left_w as usize;
|
||||
if will_use_space > area.width as usize {
|
||||
return;
|
||||
}
|
||||
|
||||
let columns = columns
|
||||
.iter()
|
||||
.map(|s| head_row_text(s, self.color_hm))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if is_head_left {
|
||||
let have_index_line = show_index && self.style.index_line;
|
||||
if !have_index_line && self.style.header_top {
|
||||
let x = area.x + left_w;
|
||||
left_w +=
|
||||
render_vertical(buf, x, area.y, area.height, false, false, splitline_s);
|
||||
}
|
||||
|
||||
let x = area.x + left_w;
|
||||
left_w += render_space(buf, x, area.y, 1, padding_cell_l);
|
||||
let x = area.x + left_w;
|
||||
left_w += render_column(buf, x, area.y, columns_width as u16, &columns);
|
||||
let x = area.x + left_w;
|
||||
left_w += render_space(buf, x, area.y, 1, padding_cell_r);
|
||||
|
||||
let layout_x = left_w - padding_cell_r - columns_width as u16;
|
||||
for (i, (text, _)) in columns.iter().enumerate() {
|
||||
state
|
||||
.layout
|
||||
.push(text, layout_x, area.y + i as u16, columns_width as u16, 1);
|
||||
}
|
||||
|
||||
if self.style.header_bottom {
|
||||
let x = area.x + left_w;
|
||||
left_w +=
|
||||
render_vertical(buf, x, area.y, area.height, false, false, splitline_s);
|
||||
}
|
||||
} else if is_head_right {
|
||||
if self.style.header_bottom {
|
||||
let x = area.x + area.width - 1;
|
||||
right_w +=
|
||||
render_vertical(buf, x, area.y, area.height, false, false, splitline_s);
|
||||
}
|
||||
|
||||
let x = area.x + area.width - right_w - padding_cell_r;
|
||||
right_w += render_space(buf, x, area.y, 1, padding_cell_r);
|
||||
let x = area.x + area.width - right_w - columns_width as u16;
|
||||
right_w += render_column(buf, x, area.y, columns_width as u16, &columns);
|
||||
let x = area.x + area.width - right_w - padding_cell_l;
|
||||
right_w += render_space(buf, x, area.y, 1, padding_cell_l);
|
||||
|
||||
if self.style.header_top {
|
||||
let x = area.x + area.width - right_w - 1;
|
||||
right_w +=
|
||||
render_vertical(buf, x, area.y, area.height, false, false, splitline_s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut do_render_shift_column = false;
|
||||
|
||||
state.count_rows = columns.len();
|
||||
state.count_columns = 0;
|
||||
|
||||
for col in self.index_column..self.data.len() {
|
||||
let mut column =
|
||||
self.data[col][self.index_row..self.index_row + columns.len()].to_vec();
|
||||
let column_width = calculate_column_width(&column);
|
||||
if column_width > u16::MAX as usize {
|
||||
break;
|
||||
}
|
||||
|
||||
let column_width = column_width as u16;
|
||||
let available = area.width - left_w - right_w;
|
||||
let is_last = col + 1 == self.data.len();
|
||||
let pad = padding_cell_l + padding_cell_r;
|
||||
let (column_width, ok, shift) =
|
||||
truncate_column_width(available, 1, column_width, pad, is_last, &mut column, None);
|
||||
|
||||
if shift {
|
||||
do_render_shift_column = true;
|
||||
}
|
||||
|
||||
if column_width == 0 && !ok {
|
||||
break;
|
||||
}
|
||||
|
||||
let x = area.x + left_w;
|
||||
left_w += render_space(buf, x, area.y, area.height, padding_cell_l);
|
||||
let x = area.x + left_w;
|
||||
left_w += render_column(buf, x, area.y, column_width, &column);
|
||||
let x = area.x + left_w;
|
||||
left_w += render_space(buf, x, area.y, area.height, padding_cell_r);
|
||||
|
||||
{
|
||||
for (row, (text, _)) in column.iter().enumerate() {
|
||||
let x = left_w - padding_cell_r - column_width;
|
||||
let y = area.y + row as u16;
|
||||
state.layout.push(text, x, y, column_width, 1);
|
||||
}
|
||||
|
||||
state.count_columns += 1;
|
||||
}
|
||||
|
||||
if do_render_shift_column {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if do_render_shift_column {
|
||||
let x = area.x + left_w;
|
||||
left_w += render_space(buf, x, area.y, area.height, padding_cell_l);
|
||||
let x = area.x + left_w;
|
||||
left_w += render_shift_column(buf, x, area.y, area.height, shift_column_s);
|
||||
let x = area.x + left_w;
|
||||
left_w += render_space(buf, x, area.y, area.height, padding_cell_r);
|
||||
}
|
||||
|
||||
_ = left_w;
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn truncate_column_width(
|
||||
space: u16,
|
||||
min: u16,
|
||||
w: u16,
|
||||
pad: u16,
|
||||
is_last: bool,
|
||||
column: &mut [(String, TextStyle)],
|
||||
head: Option<&mut String>,
|
||||
) -> (u16, bool, bool) {
|
||||
let result = check_column_width(space, min, w, pad, is_last);
|
||||
|
||||
let (width, shift_column) = match result {
|
||||
Some(result) => result,
|
||||
None => return (w, true, false),
|
||||
};
|
||||
|
||||
if width == 0 {
|
||||
return (0, false, shift_column);
|
||||
}
|
||||
|
||||
truncate_list(column, width as usize);
|
||||
if let Some(head) = head {
|
||||
truncate_str(head, width as usize);
|
||||
}
|
||||
|
||||
(width, false, shift_column)
|
||||
}
|
||||
|
||||
fn check_column_width(
|
||||
space: u16,
|
||||
min: u16,
|
||||
w: u16,
|
||||
pad: u16,
|
||||
is_last: bool,
|
||||
) -> Option<(u16, bool)> {
|
||||
if !is_space_available(space, pad) {
|
||||
return Some((0, false));
|
||||
}
|
||||
|
||||
if is_last {
|
||||
if !is_space_available(space, w + pad) {
|
||||
return Some((space - pad, false));
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
if !is_space_available(space, min + pad) {
|
||||
return Some((0, false));
|
||||
}
|
||||
|
||||
if !is_space_available(space, w + pad + min + pad) {
|
||||
let left_space = space - (min + pad);
|
||||
|
||||
if left_space > pad {
|
||||
let left = left_space - pad;
|
||||
return Some((left, true));
|
||||
} else {
|
||||
return Some((0, true));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
struct IndexColumn<'a> {
|
||||
@ -275,7 +576,6 @@ impl Widget for IndexColumn<'_> {
|
||||
fn render_header_borders(
|
||||
buf: &mut Buffer,
|
||||
area: Rect,
|
||||
y: u16,
|
||||
span: u16,
|
||||
style: NuStyle,
|
||||
top: bool,
|
||||
@ -297,18 +597,22 @@ fn render_header_borders(
|
||||
.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);
|
||||
let area = Rect::new(area.x, area.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);
|
||||
fn render_index(
|
||||
buf: &mut Buffer,
|
||||
area: Rect,
|
||||
color_hm: &NuStyleTable,
|
||||
start_index: usize,
|
||||
padding_left: u16,
|
||||
padding_right: u16,
|
||||
) -> u16 {
|
||||
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;
|
||||
@ -317,7 +621,7 @@ fn render_index(buf: &mut Buffer, area: Rect, color_hm: &NuStyleTable, start_ind
|
||||
index.render(area, buf);
|
||||
|
||||
width += w;
|
||||
width += render_space(buf, area.x + width, area.y, area.height, PADDING_RIGHT);
|
||||
width += render_space(buf, area.x + width, area.y, area.height, padding_right);
|
||||
|
||||
width
|
||||
}
|
||||
@ -327,16 +631,19 @@ fn render_vertical(
|
||||
x: u16,
|
||||
y: u16,
|
||||
height: u16,
|
||||
show_header: bool,
|
||||
top_slit: bool,
|
||||
bottom_slit: bool,
|
||||
style: NuStyle,
|
||||
) -> u16 {
|
||||
render_vertical_split(buf, x, y, height, style);
|
||||
|
||||
if show_header && y > 0 {
|
||||
if top_slit && y > 0 {
|
||||
render_top_connector(buf, x, y - 1, style);
|
||||
}
|
||||
|
||||
// render_bottom_connector(buf, x, height + y);
|
||||
if bottom_slit {
|
||||
render_bottom_connector(buf, x, y + height, style);
|
||||
}
|
||||
|
||||
1
|
||||
}
|
||||
@ -364,7 +671,10 @@ fn create_column(data: &[Vec<NuText>], col: usize) -> Vec<NuText> {
|
||||
}
|
||||
|
||||
let value = &values[col];
|
||||
column[row] = value.clone();
|
||||
|
||||
let text = value.0.replace('\n', " ");
|
||||
|
||||
column[row] = (text, value.1);
|
||||
}
|
||||
|
||||
column
|
||||
@ -390,112 +700,11 @@ fn repeat_vertical(
|
||||
}
|
||||
}
|
||||
|
||||
#[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;
|
||||
fn is_space_available(available: u16, got: u16) -> bool {
|
||||
match available.cmp(&got) {
|
||||
Ordering::Less => false,
|
||||
Ordering::Equal | Ordering::Greater => true,
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -504,15 +713,6 @@ fn truncate_list(list: &mut [NuText], width: usize) {
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
@ -530,6 +730,12 @@ fn render_top_connector(buf: &mut Buffer, x: u16, y: u16, style: NuStyle) {
|
||||
buf.set_span(x, y, &span, 1);
|
||||
}
|
||||
|
||||
fn render_bottom_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()
|
||||
|
156
crates/nu-explore/src/views/util.rs
Normal file
156
crates/nu-explore/src/views/util.rs
Normal file
@ -0,0 +1,156 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use nu_color_config::style_primitive;
|
||||
use nu_table::{string_width, Alignment, TextStyle};
|
||||
use tui::{
|
||||
buffer::Buffer,
|
||||
style::{Color, Modifier, Style},
|
||||
text::Span,
|
||||
};
|
||||
|
||||
use crate::nu_common::{truncate_str, NuColor, NuStyle, NuStyleTable, NuText};
|
||||
|
||||
pub fn set_span(
|
||||
buf: &mut Buffer,
|
||||
(x, y): (u16, u16),
|
||||
text: &str,
|
||||
style: Style,
|
||||
max_width: u16,
|
||||
) -> u16 {
|
||||
let mut text = Cow::Borrowed(text);
|
||||
let mut text_width = string_width(&text);
|
||||
if text_width > max_width as usize {
|
||||
let mut s = text.into_owned();
|
||||
truncate_str(&mut s, max_width as usize);
|
||||
text = Cow::Owned(s);
|
||||
text_width = max_width as usize;
|
||||
}
|
||||
|
||||
let span = Span::styled(text.as_ref(), style);
|
||||
buf.set_span(x, y, &span, text_width as u16);
|
||||
|
||||
text_width as u16
|
||||
}
|
||||
|
||||
pub fn nu_style_to_tui(style: NuStyle) -> tui::style::Style {
|
||||
let mut out = tui::style::Style::default();
|
||||
if let Some(clr) = style.background {
|
||||
out.bg = nu_ansi_color_to_tui_color(clr);
|
||||
}
|
||||
|
||||
if let Some(clr) = style.foreground {
|
||||
out.fg = nu_ansi_color_to_tui_color(clr);
|
||||
}
|
||||
|
||||
if style.is_blink {
|
||||
out.add_modifier |= Modifier::SLOW_BLINK;
|
||||
}
|
||||
|
||||
if style.is_bold {
|
||||
out.add_modifier |= Modifier::BOLD;
|
||||
}
|
||||
|
||||
if style.is_dimmed {
|
||||
out.add_modifier |= Modifier::DIM;
|
||||
}
|
||||
|
||||
if style.is_hidden {
|
||||
out.add_modifier |= Modifier::HIDDEN;
|
||||
}
|
||||
|
||||
if style.is_italic {
|
||||
out.add_modifier |= Modifier::ITALIC;
|
||||
}
|
||||
|
||||
if style.is_reverse {
|
||||
out.add_modifier |= Modifier::REVERSED;
|
||||
}
|
||||
|
||||
if style.is_underline {
|
||||
out.add_modifier |= Modifier::UNDERLINED;
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
pub fn nu_ansi_color_to_tui_color(clr: NuColor) -> Option<tui::style::Color> {
|
||||
use NuColor::*;
|
||||
|
||||
let clr = match clr {
|
||||
Black => Color::Black,
|
||||
DarkGray => Color::DarkGray,
|
||||
Red => Color::Red,
|
||||
LightRed => Color::LightRed,
|
||||
Green => Color::Green,
|
||||
LightGreen => Color::LightGreen,
|
||||
Yellow => Color::Yellow,
|
||||
LightYellow => Color::LightYellow,
|
||||
Blue => Color::Blue,
|
||||
LightBlue => Color::LightBlue,
|
||||
Magenta => Color::Magenta,
|
||||
LightMagenta => Color::LightMagenta,
|
||||
Cyan => Color::Cyan,
|
||||
LightCyan => Color::LightCyan,
|
||||
White => Color::White,
|
||||
Fixed(i) => Color::Indexed(i),
|
||||
Rgb(r, g, b) => tui::style::Color::Rgb(r, g, b),
|
||||
LightGray => Color::Gray,
|
||||
LightPurple => Color::LightMagenta,
|
||||
Purple => Color::Magenta,
|
||||
Default => return None,
|
||||
};
|
||||
|
||||
Some(clr)
|
||||
}
|
||||
|
||||
pub fn text_style_to_tui_style(style: TextStyle) -> tui::style::Style {
|
||||
let mut out = tui::style::Style::default();
|
||||
if let Some(style) = style.color_style {
|
||||
if let Some(clr) = style.background {
|
||||
out.bg = nu_ansi_color_to_tui_color(clr);
|
||||
}
|
||||
|
||||
if let Some(clr) = style.foreground {
|
||||
out.fg = nu_ansi_color_to_tui_color(clr);
|
||||
}
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
pub fn make_styled_string(
|
||||
text: String,
|
||||
text_type: &str,
|
||||
col: usize,
|
||||
with_index: bool,
|
||||
color_hm: &NuStyleTable,
|
||||
float_precision: usize,
|
||||
) -> NuText {
|
||||
if col == 0 && with_index {
|
||||
return (text, index_text_style(color_hm));
|
||||
}
|
||||
|
||||
let style = style_primitive(text_type, color_hm);
|
||||
|
||||
let mut text = text;
|
||||
if text_type == "float" {
|
||||
text = convert_with_precision(&text, float_precision);
|
||||
}
|
||||
|
||||
(text, style)
|
||||
}
|
||||
|
||||
fn index_text_style(color_hm: &std::collections::HashMap<String, NuStyle>) -> TextStyle {
|
||||
TextStyle {
|
||||
alignment: Alignment::Right,
|
||||
color_style: Some(color_hm["row_index"]),
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_with_precision(val: &str, precision: usize) -> String {
|
||||
// vall will always be a f64 so convert it with precision formatting
|
||||
match val.trim().parse::<f64>() {
|
||||
Ok(f) => format!("{:.prec$}", f, prec = precision),
|
||||
Err(err) => format!("error converting string [{}] to f64; {}", &val, err),
|
||||
}
|
||||
}
|
@ -197,11 +197,6 @@ impl TrimStrategy {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ExploreConfig {
|
||||
pub color_config: HashMap<String, Value>,
|
||||
}
|
||||
|
||||
impl Value {
|
||||
pub fn into_config(&mut self, config: &Config) -> (Config, Option<ShellError>) {
|
||||
// Clone the passed-in config rather than mutating it.
|
||||
|
@ -270,20 +270,64 @@ let-env config = {
|
||||
truncating_suffix: "..." # A suffix used by the 'truncating' methodology
|
||||
}
|
||||
}
|
||||
|
||||
explore: {
|
||||
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,
|
||||
help_banner: true
|
||||
exit_esc: true
|
||||
|
||||
command_bar_text: '#C4C9C6'
|
||||
# command_bar: {fg: '#C4C9C6' bg: '#223311' }
|
||||
|
||||
status_bar_background: {fg: '#1D1F21' bg: '#C4C9C6' }
|
||||
# status_bar_text: {fg: '#C4C9C6' bg: '#223311' }
|
||||
|
||||
highlight: {bg: 'yellow' fg: 'black' }
|
||||
|
||||
status: {
|
||||
# warn: {bg: 'yellow', fg: 'blue'}
|
||||
# error: {bg: 'yellow', fg: 'blue'}
|
||||
# info: {bg: 'yellow', fg: 'blue'}
|
||||
}
|
||||
|
||||
try: {
|
||||
# border_color: 'red'
|
||||
# highlighted_color: 'blue'
|
||||
|
||||
# reactive: false
|
||||
}
|
||||
|
||||
table: {
|
||||
split_line: '#404040'
|
||||
|
||||
cursor: true
|
||||
|
||||
line_index: true
|
||||
line_shift: true
|
||||
line_head_top: true
|
||||
line_head_bottom: true
|
||||
|
||||
show_head: true
|
||||
show_index: true
|
||||
|
||||
# selected_cell: {fg: 'white', bg: '#777777'}
|
||||
# selected_row: {fg: 'yellow', bg: '#C1C2A3'}
|
||||
# selected_column: blue
|
||||
|
||||
# padding_column_right: 2
|
||||
# padding_column_left: 2
|
||||
|
||||
# padding_index_left: 2
|
||||
# padding_index_right: 1
|
||||
}
|
||||
|
||||
config: {
|
||||
cursor_color: {bg: 'yellow' fg: 'black' }
|
||||
|
||||
# border_color: white
|
||||
# list_color: green
|
||||
}
|
||||
}
|
||||
|
||||
history: {
|
||||
max_size: 10000 # Session has to be reloaded for this to take effect
|
||||
sync_on_enter: true # Enable to share history between multiple sessions, else you have to close the session to write history to file
|
||||
|
Loading…
Reference in New Issue
Block a user