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:
Maxim Zhiburt 2022-12-16 18:47:07 +03:00 committed by GitHub
parent 2d07c6eedb
commit 9c1a3aa244
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 4498 additions and 1587 deletions

1
Cargo.lock generated
View File

@ -2691,6 +2691,7 @@ dependencies = [
"nu-ansi-term", "nu-ansi-term",
"nu-color-config", "nu-color-config",
"nu-engine", "nu-engine",
"nu-json",
"nu-parser", "nu-parser",
"nu-protocol", "nu-protocol",
"nu-table", "nu-table",

View File

@ -1,13 +1,74 @@
use nu_ansi_term::{Color, Style}; 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 struct NuStyle {
pub fg: Option<String>, pub fg: Option<String>,
pub bg: Option<String>, pub bg: Option<String>,
pub attr: 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 { pub fn parse_nustyle(nu_style: NuStyle) -> Style {
let mut style = Style { let mut style = Style {
foreground: nu_style.fg.and_then(|fg| lookup_color_str(&fg)), foreground: nu_style.fg.and_then(|fg| lookup_color_str(&fg)),

View File

@ -3,11 +3,15 @@ use std::collections::HashMap;
use nu_ansi_term::{Color, Style}; use nu_ansi_term::{Color, Style};
use nu_color_config::{get_color_config, get_color_map}; use nu_color_config::{get_color_config, get_color_map};
use nu_engine::CallExt; 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::{ use nu_protocol::{
ast::Call, ast::Call,
engine::{Command, EngineState, Stack}, 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. /// 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 show_index: bool = call.has_flag("index");
let is_reverse: bool = call.has_flag("reverse"); let is_reverse: bool = call.has_flag("reverse");
let peek_value: bool = call.has_flag("peek"); 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 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 mut config = nu_config.explore.clone();
let color_hm = get_color_config(config); prepare_default_config(&mut config);
let style = theme_from_config(&config.explore); 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 { match result {
Ok(Some(value)) => Ok(PipelineData::Value(value, None)), 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 colors = get_color_map(config);
let mut style = default_style(); if let Some(s) = colors.get("status_bar_text") {
style.status_bar_text = *s;
if let Some(s) = colors.get("status_bar") {
style.status_bar = *s;
} }
if let Some(s) = colors.get("command_bar") { if let Some(s) = colors.get("status_bar_background") {
style.cmd_bar = *s; style.status_bar_background = *s;
} }
if let Some(s) = colors.get("split_line") { if let Some(s) = colors.get("command_bar_text") {
style.split_line = *s; 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") { if let Some(s) = colors.get("highlight") {
style.highlight = *s; style.highlight = *s;
} }
if let Some(s) = colors.get("selected_cell") { if let Some(hm) = config.get("status").and_then(create_map) {
style.selected_cell = Some(*s); let colors = get_color_map(&hm);
if let Some(s) = colors.get("info") {
style.status_info = *s;
} }
if let Some(s) = colors.get("selected_row") { if let Some(s) = colors.get("warn") {
style.selected_row = Some(*s); style.status_warn = *s;
} }
if let Some(s) = colors.get("selected_column") { if let Some(s) = colors.get("error") {
style.selected_column = Some(*s); style.status_error = *s;
} }
if let Some(show_cursor) = config.get("cursor").and_then(|v| v.as_bool().ok()) {
style.show_cursow = show_cursor;
}
if let Some(b) = config.get("line_head_top").and_then(|v| v.as_bool().ok()) {
style.split_lines.header_top = b;
}
if let Some(b) = config
.get("line_head_bottom")
.and_then(|v| v.as_bool().ok())
{
style.split_lines.header_bottom = b;
}
if let Some(b) = config.get("line_shift").and_then(|v| v.as_bool().ok()) {
style.split_lines.shift_line = b;
}
if let Some(b) = config.get("line_index").and_then(|v| v.as_bool().ok()) {
style.split_lines.index_line = b;
} }
style style
} }
fn default_style() -> StyleConfig { fn prepare_default_config(config: &mut HashMap<String, Value>) {
StyleConfig { const STATUS_BAR: Style = color(
status_bar: Style { Some(Color::Rgb(29, 31, 33)),
background: Some(Color::Rgb(196, 201, 198)), Some(Color::Rgb(196, 201, 198)),
foreground: Some(Color::Rgb(29, 31, 33)), );
..Default::default()
}, const INPUT_BAR: Style = color(Some(Color::Rgb(196, 201, 198)), None);
highlight: Style {
background: Some(Color::Yellow), const HIGHLIGHT: Style = color(Some(Color::Black), Some(Color::Yellow));
foreground: Some(Color::Black),
..Default::default() const STATUS_ERROR: Style = color(Some(Color::White), Some(Color::Red));
},
split_line: Style { const STATUS_INFO: Style = color(None, None);
foreground: Some(Color::Rgb(64, 64, 64)),
..Default::default() const STATUS_WARN: Style = color(None, None);
},
cmd_bar: Style { const TABLE_SPLIT_LINE: Style = color(Some(Color::Rgb(64, 64, 64)), None);
foreground: Some(Color::Rgb(196, 201, 198)),
..Default::default() const TABLE_LINE_HEADER_TOP: bool = true;
},
status_error: Style { const TABLE_LINE_HEADER_BOTTOM: bool = true;
background: Some(Color::Red),
foreground: Some(Color::White), const TABLE_LINE_INDEX: bool = true;
..Default::default()
}, const TABLE_LINE_SHIFT: bool = true;
status_info: Style::default(),
status_warn: Style::default(), const TABLE_SELECT_CURSOR: bool = true;
selected_cell: None,
selected_column: None, const TABLE_SELECT_CELL: Style = color(None, None);
selected_row: None,
show_cursow: true, const TABLE_SELECT_ROW: Style = color(None, None);
split_lines: TableSplitLines {
header_bottom: true, const TABLE_SELECT_COLUMN: Style = color(None, None);
header_top: true,
index_line: true, const TRY_BORDER_COLOR: Style = color(None, None);
shift_line: true,
}, 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)
}
} }
} }

View File

@ -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-color-config = { path = "../nu-color-config", version = "0.72.2" }
nu-engine = { path = "../nu-engine", version = "0.72.2" } nu-engine = { path = "../nu-engine", version = "0.72.2" }
nu-table = { path = "../nu-table", 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" terminal_size = "0.2.1"
strip-ansi-escapes = "0.1.1" strip-ansi-escapes = "0.1.1"

View File

@ -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()
}
}

View 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,
}
}

View 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(),
}
}
}

View 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)
}
}

View File

@ -3,19 +3,24 @@ use std::{
io::{self, Result}, io::{self, Result},
}; };
use crossterm::event::KeyEvent;
use nu_protocol::{ use nu_protocol::{
engine::{EngineState, Stack}, engine::{EngineState, Stack},
Value, 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}; use super::{HelpExample, HelpManual, ViewCommand};
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
pub struct HelpCmd { pub struct HelpCmd {
input_command: String, input_command: String,
table_cfg: TableConfig,
supported_commands: Vec<HelpManual>, supported_commands: Vec<HelpManual>,
aliases: HashMap<String, Vec<String>>, aliases: HashMap<String, Vec<String>>,
} }
@ -23,18 +28,39 @@ pub struct HelpCmd {
impl HelpCmd { impl HelpCmd {
pub const NAME: &'static str = "help"; pub const NAME: &'static str = "help";
pub fn new( const HELP_MESSAGE: &'static str = r#" Explore - main help file
commands: Vec<HelpManual>,
aliases: &[(&str, &str)], Move around: Use the cursor keys.
table_cfg: TableConfig, Close this window: Use "<Esc>".
) -> Self { 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); let aliases = collect_aliases(aliases);
Self { Self {
input_command: String::new(), input_command: String::new(),
supported_commands: commands, supported_commands: commands,
aliases, aliases,
table_cfg,
} }
} }
} }
@ -51,7 +77,7 @@ fn collect_aliases(aliases: &[(&str, &str)]) -> HashMap<String, Vec<String>> {
} }
impl ViewCommand for HelpCmd { impl ViewCommand for HelpCmd {
type View = RecordView<'static>; type View = HelpView<'static>;
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
Self::NAME Self::NAME
@ -62,27 +88,32 @@ impl ViewCommand for HelpCmd {
} }
fn help(&self) -> Option<HelpManual> { 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 { Some(HelpManual {
name: "help", name: "help",
description: "Explore the help page for `explore`", description: "Explore the help page for `explore`",
arguments: vec![], arguments,
examples: vec![ examples,
HelpExample { input: vec![],
example: "help", config_options: vec![],
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...",
},
],
}) })
} }
fn display_config_option(&mut self, _: String, _: String, _: String) -> bool {
false
}
fn parse(&mut self, args: &str) -> Result<()> { fn parse(&mut self, args: &str) -> Result<()> {
self.input_command = args.trim().to_owned(); 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> { fn spawn(&mut self, _: &EngineState, _: &mut Stack, _: Option<Value>) -> Result<Self::View> {
if self.input_command.is_empty() { if self.input_command.is_empty() {
let (headers, data) = help_frame_data(&self.supported_commands, &self.aliases); return Ok(HelpView::Preview(Preview::new(Self::HELP_MESSAGE)));
let view = RecordView::new(headers, data, self.table_cfg);
return Ok(view);
} }
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 let manual = self
.supported_commands .supported_commands
.iter() .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"))?; .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "a given command was not found"))?;
let aliases = self let aliases = self
@ -108,9 +155,9 @@ impl ViewCommand for HelpCmd {
.map(|l| l.as_slice()) .map(|l| l.as_slice())
.unwrap_or(&[]); .unwrap_or(&[]);
let (headers, data) = help_manual_data(manual, aliases); 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], supported_commands: &[HelpManual],
aliases: &HashMap<String, Vec<String>>, aliases: &HashMap<String, Vec<String>>,
) -> (Vec<String>, Vec<Vec<Value>>) { ) -> (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 let commands = supported_commands
.iter() .iter()
.map(|manual| { .map(|manual| {
@ -154,41 +187,13 @@ fn help_frame_data(
span: NuSpan::unknown(), span: NuSpan::unknown(),
}; };
let headers = vec!["name", "mode", "information", "description"]; collect_input(commands)
#[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)
} }
fn help_manual_data(manual: &HelpManual, aliases: &[String]) -> (Vec<String>, Vec<Vec<Value>>) { fn help_manual_data(manual: &HelpManual, aliases: &[String]) -> (Vec<String>, Vec<Vec<Value>>) {
macro_rules! nu_str { macro_rules! nu_str {
($text:expr) => { ($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(), span: NuSpan::unknown(),
}) })
.collect(); .collect();
let examples = Value::List { let examples = Value::List {
vals: examples, vals: examples,
span: NuSpan::unknown(), 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 name = nu_str!(manual.name);
let aliases = nu_str!(aliases.join(", ")); let aliases = nu_str!(aliases.join(", "));
let desc = nu_str!(manual.description); 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("name"),
String::from("aliases"), String::from("aliases"),
String::from("arguments"), String::from("arguments"),
String::from("input"),
String::from("examples"), String::from("examples"),
String::from("configuration"),
String::from("description"), 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) (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),
}
}
}

View File

@ -5,19 +5,27 @@ use nu_protocol::{
use super::pager::{Pager, Transition}; use super::pager::{Pager, Transition};
use std::io::Result; use std::{borrow::Cow, io::Result};
mod expand;
mod help; mod help;
mod nu; mod nu;
mod preview;
mod quit; mod quit;
mod table;
mod r#try; 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 help::HelpCmd;
pub use nu::NuCmd; pub use nu::NuCmd;
pub use preview::PreviewCmd;
pub use quit::QuitCmd; pub use quit::QuitCmd;
pub use r#try::TryCmd; pub use r#try::TryCmd;
pub use table::TableCmd;
pub use tweak::TweakCmd;
pub trait SimpleCommand { pub trait SimpleCommand {
fn name(&self) -> &'static str; fn name(&self) -> &'static str;
@ -48,6 +56,8 @@ pub trait ViewCommand {
fn parse(&mut self, args: &str) -> Result<()>; fn parse(&mut self, args: &str) -> Result<()>;
fn display_config_option(&mut self, group: String, key: String, value: String) -> bool;
fn spawn( fn spawn(
&mut self, &mut self,
engine_state: &EngineState, engine_state: &EngineState,
@ -62,10 +72,110 @@ pub struct HelpManual {
pub description: &'static str, pub description: &'static str,
pub arguments: Vec<HelpExample>, pub arguments: Vec<HelpExample>,
pub examples: Vec<HelpExample>, pub examples: Vec<HelpExample>,
pub config_options: Vec<ConfigOption>,
pub input: Vec<Shortcode>,
} }
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
pub struct HelpExample { 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, 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()
}

View File

@ -4,11 +4,12 @@ use nu_protocol::{
engine::{EngineState, Stack}, engine::{EngineState, Stack},
PipelineData, Value, PipelineData, Value,
}; };
use tui::layout::Rect;
use crate::{ use crate::{
nu_common::{collect_pipeline, has_simple_value, is_ignored_command, run_nu_command}, nu_common::{collect_pipeline, has_simple_value, run_command_with_value},
pager::TableConfig, pager::Frame,
views::{Preview, RecordView, View}, views::{Layout, Orientation, Preview, RecordView, View, ViewConfig},
}; };
use super::{HelpExample, HelpManual, ViewCommand}; use super::{HelpExample, HelpManual, ViewCommand};
@ -16,14 +17,12 @@ use super::{HelpExample, HelpManual, ViewCommand};
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
pub struct NuCmd { pub struct NuCmd {
command: String, command: String,
table_cfg: TableConfig,
} }
impl NuCmd { impl NuCmd {
pub fn new(table_cfg: TableConfig) -> Self { pub fn new() -> Self {
Self { Self {
command: String::new(), command: String::new(),
table_cfg,
} }
} }
@ -42,28 +41,33 @@ impl ViewCommand for NuCmd {
} }
fn help(&self) -> Option<HelpManual> { 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 { Some(HelpManual {
name: "nu", name: "nu",
description: description:
"Run a Nushell command. The data currently being explored is piped into it.", "Run a Nushell command. The data currently being explored is piped into it.",
examples,
arguments: vec![], arguments: vec![],
examples: vec![ input: vec![],
HelpExample { config_options: vec![],
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",
},
],
}) })
} }
fn display_config_option(&mut self, _: String, _: String, _: String) -> bool {
false
}
fn parse(&mut self, args: &str) -> Result<()> { fn parse(&mut self, args: &str) -> Result<()> {
self.command = args.trim().to_owned(); self.command = args.trim().to_owned();
@ -76,28 +80,25 @@ impl ViewCommand for NuCmd {
stack: &mut Stack, stack: &mut Stack,
value: Option<Value>, value: Option<Value>,
) -> Result<Self::View> { ) -> 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 value = value.unwrap_or_default();
let pipeline = PipelineData::Value(value, None); let pipeline = run_command_with_value(&self.command, &value, engine_state, stack)
let pipeline = run_nu_command(engine_state, stack, &self.command, pipeline)
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; .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); let (columns, values) = collect_pipeline(pipeline);
if has_simple_value(&values) { if let Some(value) = has_simple_value(&values) {
let config = &engine_state.config; let text = value.into_abbreviated_string(&engine_state.config);
let text = values[0][0].into_abbreviated_string(config);
return Ok(NuView::Preview(Preview::new(&text))); 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)) Ok(NuView::Records(view))
} }
@ -109,13 +110,7 @@ pub enum NuView<'a> {
} }
impl View for NuView<'_> { impl View for NuView<'_> {
fn draw( fn draw(&mut self, f: &mut Frame, area: Rect, cfg: ViewConfig<'_>, layout: &mut Layout) {
&mut self,
f: &mut crate::pager::Frame,
area: tui::layout::Rect,
cfg: &crate::ViewConfig,
layout: &mut crate::views::Layout,
) {
match self { match self {
NuView::Records(v) => v.draw(f, area, cfg, layout), NuView::Records(v) => v.draw(f, area, cfg, layout),
NuView::Preview(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, &mut self,
engine_state: &EngineState, engine_state: &EngineState,
stack: &mut Stack, stack: &mut Stack,
layout: &crate::views::Layout, layout: &Layout,
info: &mut crate::pager::ViewInfo, info: &mut crate::pager::ViewInfo,
key: crossterm::event::KeyEvent, key: crossterm::event::KeyEvent,
) -> Option<crate::pager::Transition> { ) -> Option<crate::pager::Transition> {
@ -156,4 +151,11 @@ impl View for NuView<'_> {
NuView::Preview(v) => v.exit(), 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),
}
}
} }

View File

@ -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))
}
}

View File

@ -31,6 +31,8 @@ impl SimpleCommand for QuitCmd {
description: "Quit and return to Nushell", description: "Quit and return to Nushell",
arguments: vec![], arguments: vec![],
examples: vec![], examples: vec![],
input: vec![],
config_options: vec![],
}) })
} }

View 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,
}
}

View File

@ -1,25 +1,23 @@
use std::io::Result; use std::io::{Error, ErrorKind, Result};
use nu_protocol::{ use nu_protocol::{
engine::{EngineState, Stack}, engine::{EngineState, Stack},
Value, 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)] #[derive(Debug, Default, Clone)]
pub struct TryCmd { pub struct TryCmd {
command: String, command: String,
table_cfg: TableConfig,
} }
impl TryCmd { impl TryCmd {
pub fn new(table_cfg: TableConfig) -> Self { pub fn new() -> Self {
Self { Self {
command: String::new(), command: String::new(),
table_cfg,
} }
} }
@ -38,17 +36,41 @@ impl ViewCommand for TryCmd {
} }
fn help(&self) -> Option<HelpManual> { 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 { Some(HelpManual {
name: "try", 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![], arguments: vec![],
examples: vec![HelpExample { examples,
example: "try open Cargo.toml", input: shortcuts,
description: "Optionally, you can provide a command which will be run immediately", config_options,
}],
}) })
} }
fn display_config_option(&mut self, _: String, _: String, _: String) -> bool {
false
}
fn parse(&mut self, args: &str) -> Result<()> { fn parse(&mut self, args: &str) -> Result<()> {
self.command = args.trim().to_owned(); self.command = args.trim().to_owned();
@ -57,13 +79,15 @@ impl ViewCommand for TryCmd {
fn spawn( fn spawn(
&mut self, &mut self,
_: &EngineState, engine_state: &EngineState,
_: &mut Stack, stack: &mut Stack,
value: Option<Value>, value: Option<Value>,
) -> Result<Self::View> { ) -> Result<Self::View> {
let value = value.unwrap_or_default(); 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.init(self.command.clone());
view.try_run(engine_state, stack)
.map_err(|e| Error::new(ErrorKind::Other, e))?;
Ok(view) Ok(view)
} }

View 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()),
}
}

View File

@ -1,58 +1,185 @@
mod command;
mod commands; mod commands;
mod events;
mod nu_common; mod nu_common;
mod pager; mod pager;
mod registry;
mod views; mod views;
use std::io; 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_common::{collect_pipeline, has_simple_value, CtrlC};
use nu_protocol::{ use nu_protocol::{
engine::{EngineState, Stack}, engine::{EngineState, Stack},
PipelineData, Value, PipelineData, Value,
}; };
use pager::{Page, Pager}; use pager::{Page, Pager};
use registry::{Command, CommandRegistry};
use terminal_size::{Height, Width}; 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( pub fn run_pager(
engine_state: &EngineState, engine_state: &EngineState,
stack: &mut Stack, stack: &mut Stack,
ctrlc: CtrlC, ctrlc: CtrlC,
table_cfg: TableConfig,
view_cfg: ViewConfig,
input: PipelineData, input: PipelineData,
config: PagerConfig,
) -> io::Result<Option<Value>> { ) -> io::Result<Option<Value>> {
let commands = command::CommandList::new(table_cfg); let mut p = Pager::new(config.clone());
let mut p = Pager::new(table_cfg, view_cfg.clone());
let is_record = matches!(input, PipelineData::Value(Value::Record { .. }, ..));
let (columns, data) = collect_pipeline(input); let (columns, data) = collect_pipeline(input);
let commands = create_command_registry();
let has_no_input = columns.is_empty() && data.is_empty(); let has_no_input = columns.is_empty() && data.is_empty();
if has_no_input { if has_no_input {
let view = Some(Page::new(InformationView, true)); return p.run(engine_state, stack, ctrlc, information_view(), commands);
return p.run(engine_state, stack, ctrlc, view, commands);
} }
if has_simple_value(&data) { if config.show_banner {
let text = data[0][0].into_abbreviated_string(view_cfg.config); 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)); let view = Some(Page::new(Preview::new(&text), true));
return p.run(engine_state, stack, ctrlc, view, commands); 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() { if let Some((Width(w), Height(h))) = terminal_size::terminal_size() {
view.reverse(w, h); view.reverse(w, h);
} }
} }
let view = Some(Page::new(view, false)); Some(Page::new(view, false))
p.run(engine_state, stack, ctrlc, view, commands) }
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(),
},
}
} }

View File

@ -2,9 +2,30 @@ use nu_engine::eval_block;
use nu_parser::parse; use nu_parser::parse;
use nu_protocol::{ use nu_protocol::{
engine::{EngineState, Stack, StateWorkingSet}, 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( pub fn run_nu_command(
engine_state: &EngineState, engine_state: &EngineState,
stack: &mut Stack, stack: &mut Stack,
@ -16,7 +37,15 @@ pub fn run_nu_command(
} }
pub fn is_ignored_command(command: &str) -> bool { 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( fn eval_source2(
@ -56,5 +85,5 @@ fn eval_source2(
block.pipelines.drain(..block.pipelines.len() - 1); 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)
} }

View File

@ -1,4 +1,5 @@
mod command; mod command;
mod string;
mod table; mod table;
mod value; mod value;
@ -17,11 +18,17 @@ pub type NuText = (String, TextStyle);
pub type CtrlC = Option<Arc<AtomicBool>>; pub type CtrlC = Option<Arc<AtomicBool>>;
pub type NuStyleTable = HashMap<String, NuStyle>; 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 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; 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
}
} }

View 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('…');
}
}

View File

@ -1,3 +1,5 @@
use std::collections::HashMap;
use nu_engine::get_columns; use nu_engine::get_columns;
use nu_protocol::{ast::PathMember, PipelineData, Value}; 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")); columns.push(String::from("stdout"));
data.push(vec![value]); data.push(value);
} }
if let Some(stderr) = stderr { 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")); columns.push(String::from("stderr"));
data.push(vec![value]); data.push(value);
} }
if let Some(exit_code) = exit_code { if let Some(exit_code) = exit_code {
let list = exit_code.collect::<Vec<_>>(); let list = exit_code.collect::<Vec<_>>();
let val = Value::List { vals: list, span };
columns.push(String::from("exit_code")); columns.push(String::from("exit_code"));
data.push(list); data.push(val);
} }
if metadata.is_some() { if metadata.is_some() {
columns.push(String::from("metadata")); let val = Value::Record {
data.push(vec![Value::Record {
cols: vec![String::from("data_source")], cols: vec![String::from("data_source")],
vals: vec![Value::String { vals: vec![Value::String {
val: String::from("ls"), val: String::from("ls"),
span, span,
}], }],
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(), 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())
}

View 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);
}
}

View 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,
}

View 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);
}
}
}

View 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()
}
}

View 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,
}
}
}

View 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);
}
}

View 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
}
}

View 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())
}
}

View 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)
}
}

View File

@ -5,10 +5,10 @@ use tui::{layout::Rect, widgets::Paragraph};
use crate::{ use crate::{
nu_common::NuText, nu_common::NuText,
pager::{Frame, Transition, ViewConfig, ViewInfo}, pager::{Frame, Transition, ViewInfo},
}; };
use super::{Layout, View}; use super::{Layout, View, ViewConfig};
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct InformationView; pub struct InformationView;
@ -26,7 +26,7 @@ impl InformationView {
} }
impl View for 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; let count_lines = Self::MESSAGE.len() as u16;
if area.height < count_lines { if area.height < count_lines {

View File

@ -1,6 +1,7 @@
use std::cmp::min; use std::cmp::min;
use crossterm::event::{KeyCode, KeyEvent}; use crossterm::event::{KeyCode, KeyEvent};
use nu_color_config::get_color_map;
use nu_protocol::{ use nu_protocol::{
engine::{EngineState, Stack}, engine::{EngineState, Stack},
PipelineData, Value, PipelineData, Value,
@ -12,27 +13,37 @@ use tui::{
}; };
use crate::{ use crate::{
nu_common::{collect_pipeline, is_ignored_command, run_nu_command}, nu_common::{collect_pipeline, run_command_with_value},
pager::{Frame, Report, TableConfig, Transition, ViewConfig, ViewInfo}, 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> { pub struct InteractiveView<'a> {
input: Value, input: Value,
command: String, command: String,
imidiate: bool,
table: Option<RecordView<'a>>, table: Option<RecordView<'a>>,
table_theme: TableTheme,
view_mode: bool, view_mode: bool,
// todo: impl Debug for it border_color: Style,
table_cfg: TableConfig, highlighted_color: Style,
} }
impl<'a> InteractiveView<'a> { impl<'a> InteractiveView<'a> {
pub fn new(input: Value, table_cfg: TableConfig) -> Self { pub fn new(input: Value) -> Self {
Self { Self {
input, input,
table_cfg,
table: None, table: None,
imidiate: false,
table_theme: TableTheme::default(),
border_color: Style::default(),
highlighted_color: Style::default(),
view_mode: false, view_mode: false,
command: String::new(), command: String::new(),
} }
@ -41,13 +52,25 @@ impl<'a> InteractiveView<'a> {
pub fn init(&mut self, command: String) { pub fn init(&mut self, command: String) {
self.command = command; 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<'_> { 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() let cmd_block = tui::widgets::Block::default()
.borders(Borders::ALL) .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_area = Rect::new(area.x + 1, area.y, area.width - 2, 3);
let cmd_block = if self.view_mode { let cmd_block = if self.view_mode {
@ -56,6 +79,7 @@ impl View for InteractiveView<'_> {
cmd_block cmd_block
.border_style(Style::default().add_modifier(Modifier::BOLD)) .border_style(Style::default().add_modifier(Modifier::BOLD))
.border_type(BorderType::Double) .border_type(BorderType::Double)
.border_style(highlighted_color)
}; };
f.render_widget(cmd_block, cmd_area); f.render_widget(cmd_block, cmd_area);
@ -97,13 +121,15 @@ impl View for InteractiveView<'_> {
let table_block = tui::widgets::Block::default() let table_block = tui::widgets::Block::default()
.borders(Borders::ALL) .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_area = Rect::new(area.x + 1, area.y + 3, area.width - 2, area.height - 3);
let table_block = if self.view_mode { let table_block = if self.view_mode {
table_block table_block
.border_style(Style::default().add_modifier(Modifier::BOLD)) .border_style(Style::default().add_modifier(Modifier::BOLD))
.border_type(BorderType::Double) .border_type(BorderType::Double)
.border_style(highlighted_color)
} else { } else {
table_block table_block
}; };
@ -136,13 +162,18 @@ impl View for InteractiveView<'_> {
.as_mut() .as_mut()
.expect("we know that we have a table cause of a flag"); .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) { if was_at_the_top && matches!(key.code, KeyCode::Up | KeyCode::PageUp) {
self.view_mode = false; self.view_mode = false;
return Some(Transition::Ok); 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); let result = table.handle_input(engine_state, stack, layout, info, key);
return match result { return match result {
@ -160,15 +191,32 @@ impl View for InteractiveView<'_> {
KeyCode::Backspace => { KeyCode::Backspace => {
if !self.command.is_empty() { if !self.command.is_empty() {
self.command.pop(); 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) Some(Transition::Ok)
} }
KeyCode::Char(c) => { KeyCode::Char(c) => {
self.command.push(*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) Some(Transition::Ok)
} }
KeyCode::Down => { KeyCode::Down | KeyCode::Tab => {
if self.table.is_some() { if self.table.is_some() {
self.view_mode = true; self.view_mode = true;
} }
@ -176,27 +224,9 @@ impl View for InteractiveView<'_> {
Some(Transition::Ok) Some(Transition::Ok)
} }
KeyCode::Enter => { KeyCode::Enter => {
if is_ignored_command(&self.command) { match self.try_run(engine_state, stack) {
info.report = Some(Report::error(String::from("The command is ignored"))); Ok(_) => info.report = Some(Report::default()),
return Some(Transition::Ok); Err(err) => info.report = Some(Report::error(format!("Error: {}", err))),
}
let pipeline = PipelineData::Value(self.input.clone(), None);
let pipeline = run_nu_command(engine_state, stack, &self.command, pipeline);
match pipeline {
Ok(pipeline_data) => {
let (columns, values) = collect_pipeline(pipeline_data);
let view = RecordView::new(columns, values, self.table_cfg);
self.table = Some(view);
// in case there was a error before wanna reset it.
info.report = Some(Report::default());
}
Err(err) => {
info.report = Some(Report::error(format!("Error: {}", err)));
}
} }
Some(Transition::Ok) Some(Transition::Ok)
@ -218,4 +248,58 @@ impl View for InteractiveView<'_> {
fn show_data(&mut self, i: usize) -> bool { fn show_data(&mut self, i: usize) -> bool {
self.table.as_mut().map_or(false, |v| v.show_data(i)) 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)
} }

View File

@ -1,8 +1,10 @@
mod coloredtextw; mod coloredtextw;
mod cursor;
mod information; mod information;
mod interative; mod interative;
mod preview; mod preview;
mod record; mod record;
pub mod util;
use crossterm::event::KeyEvent; use crossterm::event::KeyEvent;
use nu_protocol::{ use nu_protocol::{
@ -11,15 +13,23 @@ use nu_protocol::{
}; };
use tui::layout::Rect; use tui::layout::Rect;
use super::{ use crate::{
nu_common::NuText, nu_common::{NuConfig, NuStyleTable},
pager::{Frame, Transition, ViewConfig, ViewInfo}, pager::ConfigMap,
}; };
use super::{
nu_common::NuText,
pager::{Frame, Transition, ViewInfo},
};
pub mod configuration;
pub use configuration::ConfigurationView;
pub use information::InformationView; pub use information::InformationView;
pub use interative::InteractiveView; pub use interative::InteractiveView;
pub use preview::Preview; pub use preview::Preview;
pub use record::{RecordView, RecordViewState}; pub use record::{Orientation, RecordView};
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct Layout { 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 { 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( fn handle_input(
&mut self, &mut self,
@ -71,10 +98,12 @@ pub trait View {
fn exit(&mut self) -> Option<Value> { fn exit(&mut self) -> Option<Value> {
None None
} }
fn setup(&mut self, _: ViewConfig<'_>) {}
} }
impl View for Box<dyn View> { 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) 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 { fn show_data(&mut self, i: usize) -> bool {
self.as_mut().show_data(i) self.as_mut().show_data(i)
} }
fn setup(&mut self, cfg: ViewConfig<'_>) {
self.as_mut().setup(cfg)
}
} }

View File

@ -1,4 +1,4 @@
use std::cmp::{max, min}; use std::cmp::max;
use crossterm::event::{KeyCode, KeyEvent}; use crossterm::event::{KeyCode, KeyEvent};
use nu_protocol::{ use nu_protocol::{
@ -10,18 +10,17 @@ use tui::layout::Rect;
use crate::{ use crate::{
nu_common::{NuSpan, NuText}, 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 // todo: Add wrap option
#[derive(Debug)] #[derive(Debug)]
pub struct Preview { pub struct Preview {
underlaying_value: Option<Value>,
lines: Vec<String>, lines: Vec<String>,
i_row: usize, cursor: XYCursor,
i_col: usize,
screen_size: u16,
} }
impl Preview { impl Preview {
@ -30,132 +29,98 @@ impl Preview {
.lines() .lines()
.map(|line| line.replace('\t', " ")) // tui: doesn't support TAB .map(|line| line.replace('\t', " ")) // tui: doesn't support TAB
.collect(); .collect();
let cursor = XYCursor::new(lines.len(), usize::MAX);
Self { Self {
lines, lines,
i_col: 0, cursor,
i_row: 0, underlaying_value: None,
screen_size: 0,
} }
} }
pub fn set_value(&mut self, value: Value) {
self.underlaying_value = Some(value);
}
} }
impl View for Preview { impl View for Preview {
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) {
if self.i_row >= self.lines.len() { self.cursor
f.render_widget(tui::widgets::Clear, area); .set_window(area.height as usize, area.width as usize);
return;
}
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) { 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); 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); layout.push(&s, area.x, area.y, area.width, area.height);
f.render_widget(text, area)
} }
self.screen_size = area.width;
} }
fn handle_input( fn handle_input(
&mut self, &mut self,
_: &EngineState, _: &EngineState,
_: &mut Stack, _: &mut Stack,
layout: &Layout, _: &Layout,
info: &mut ViewInfo, // add this arg to draw too? info: &mut ViewInfo, // add this arg to draw too?
key: KeyEvent, key: KeyEvent,
) -> Option<Transition> { ) -> Option<Transition> {
match key.code { match key.code {
KeyCode::Left => { KeyCode::Left => {
if self.i_col > 0 { self.cursor
self.i_col -= max(1, self.screen_size as usize / 2); .prev_column_by(max(1, self.cursor.column_window_size() / 2));
}
Some(Transition::Ok) Some(Transition::Ok)
} }
KeyCode::Right => { 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) Some(Transition::Ok)
} }
KeyCode::Up => { KeyCode::Up => {
let is_start = self.i_row == 0; self.cursor.prev_row_i();
if is_start {
// noop
return Some(Transition::Ok);
}
let page_size = layout.data.len(); if self.cursor.row_starts_at() == 0 {
let max = self.lines.len().saturating_sub(page_size); info.status = Some(Report::info("TOP"));
let was_end = self.i_row == max; } else {
if max != 0 && was_end {
info.status = Some(Report::default()); info.status = Some(Report::default());
} }
self.i_row = self.i_row.saturating_sub(1);
Some(Transition::Ok) Some(Transition::Ok)
} }
KeyCode::Down => { KeyCode::Down => {
let page_size = layout.data.len(); if self.cursor.row() + self.cursor.row_window_size() < self.cursor.row_limit() {
let max = self.lines.len().saturating_sub(page_size); self.cursor.next_row_i();
let is_end = self.i_row == max; info.status = Some(Report::info("END"));
if is_end { } else {
// noop info.status = Some(Report::default());
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);
} }
Some(Transition::Ok) Some(Transition::Ok)
} }
KeyCode::PageUp => { KeyCode::PageUp => {
let page_size = layout.data.len(); self.cursor.prev_row_page();
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()); info.status = Some(Report::default());
} }
self.i_row = self.i_row.saturating_sub(page_size);
Some(Transition::Ok) Some(Transition::Ok)
} }
KeyCode::PageDown => { KeyCode::PageDown => {
let page_size = layout.data.len(); self.cursor.next_row_page();
let max = self.lines.len().saturating_sub(page_size);
self.i_row = min(self.i_row + page_size, max);
let is_end = self.i_row == max; if self.cursor.row() + 1 == self.cursor.row_limit() {
if is_end { info.status = Some(Report::info("END"));
let report = Report::new( } else {
String::from("END"), info.status = Some(Report::default());
Severity::Info,
String::new(),
String::new(),
);
info.status = Some(report);
} }
Some(Transition::Ok) Some(Transition::Ok)
@ -177,12 +142,17 @@ impl View for Preview {
// //
// todo: improve somehow? // todo: improve somehow?
self.i_row = row; self.cursor.set_position(row, 0);
true true
} }
fn exit(&mut self) -> Option<Value> { fn exit(&mut self) -> Option<Value> {
match &self.underlaying_value {
Some(value) => Some(value.clone()),
None => {
let text = self.lines.join("\n"); let text = self.lines.join("\n");
Some(Value::string(text, NuSpan::unknown())) Some(Value::string(text, NuSpan::unknown()))
} }
}
}
} }

View File

@ -1,8 +1,9 @@
mod tablew; mod tablew;
use std::{borrow::Cow, cmp::min, collections::HashMap}; use std::{borrow::Cow, collections::HashMap};
use crossterm::event::{KeyCode, KeyEvent}; use crossterm::event::{KeyCode, KeyEvent};
use nu_color_config::get_color_map;
use nu_protocol::{ use nu_protocol::{
engine::{EngineState, Stack}, engine::{EngineState, Stack},
Value, Value,
@ -10,47 +11,127 @@ use nu_protocol::{
use tui::{layout::Rect, widgets::Block}; use tui::{layout::Rect, widgets::Block};
use crate::{ use crate::{
nu_common::{collect_input, NuConfig, NuSpan, NuStyleTable, NuText}, nu_common::{collect_input, NuConfig, NuSpan, NuStyle, NuStyleTable, NuText},
pager::{ pager::{
make_styled_string, nu_style_to_tui, Frame, Position, Report, Severity, StyleConfig, report::{Report, Severity},
TableConfig, Transition, ViewConfig, ViewInfo, ConfigMap, Frame, Transition, ViewInfo,
}, },
util::create_map,
views::ElementInfo, 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)] #[derive(Debug, Clone)]
pub struct RecordView<'a> { pub struct RecordView<'a> {
layer_stack: Vec<RecordLayer<'a>>, layer_stack: Vec<RecordLayer<'a>>,
mode: UIMode, mode: UIMode,
cfg: TableConfig, orientation: Orientation,
pub(crate) cursor: Position, theme: TableTheme,
state: RecordViewState,
} }
impl<'a> RecordView<'a> { impl<'a> RecordView<'a> {
pub fn new( pub fn new(
columns: impl Into<Cow<'a, [String]>>, columns: impl Into<Cow<'a, [String]>>,
records: impl Into<Cow<'a, [Vec<Value>]>>, records: impl Into<Cow<'a, [Vec<Value>]>>,
table_cfg: TableConfig,
) -> Self { ) -> Self {
Self { Self {
layer_stack: vec![RecordLayer::new(columns, records)], layer_stack: vec![RecordLayer::new(columns, records)],
mode: UIMode::View, mode: UIMode::View,
cursor: Position::new(0, 0), orientation: Orientation::Top,
cfg: table_cfg, theme: TableTheme::default(),
state: RecordViewState::default(),
} }
} }
pub fn reverse(&mut self, width: u16, height: u16) { 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); 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 // todo: rename to get_layer
pub fn get_layer_last(&self) -> &RecordLayer<'a> { pub fn get_layer_last(&self) -> &RecordLayer<'a> {
self.layer_stack self.layer_stack
@ -64,46 +145,137 @@ impl<'a> RecordView<'a> {
.expect("we guarantee that 1 entry is always in a list") .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> { pub fn get_orientation_current(&mut self) -> Orientation {
let data = convert_records_to_string(&layer.records, view_cfg.config, view_cfg.color_hm); self.get_layer_last().orientation
}
let style = tablew::TableStyle { pub fn set_orientation(&mut self, orientation: Orientation) {
show_index: self.cfg.show_index, self.orientation = orientation;
show_header: self.cfg.show_head,
splitline_style: view_cfg.theme.split_line, // we need to reset all indexes as we can't no more use them.
header_bottom: view_cfg.theme.split_lines.header_bottom, self.reset_cursors();
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, 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(); layer.records[row][column].clone()
let color_hm = view_cfg.color_hm; }
let i_row = layer.index_row;
let i_column = layer.index_column;
TableW::new(headers, data, color_hm, i_row, i_column, style) 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<'_> { impl View for RecordView<'_> {
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 layer = self.get_layer_last();
let table = self.create_tablew(layer, cfg);
let mut table_layout = TableWState::default(); let mut table_layout = TableWState::default();
let table = self.create_tablew(cfg);
f.render_stateful_widget(table, area, &mut table_layout); f.render_stateful_widget(table, area, &mut table_layout);
*layout = table_layout.layout; *layout = table_layout.layout;
self.state = RecordViewState {
count_rows: table_layout.count_rows, self.update_cursors(table_layout.count_rows, table_layout.count_columns);
count_columns: table_layout.count_columns,
data_index: table_layout.data_index,
};
if self.mode == UIMode::Cursor { if self.mode == UIMode::Cursor {
let cursor = get_cursor(self); let (row, column) = self.get_current_window();
highlight_cell(f, area, &self.state, cursor, cfg.theme); 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> { ) -> Option<Transition> {
let result = match self.mode { let result = match self.mode {
UIMode::View => handle_key_event_view_mode(self, &key), UIMode::View => handle_key_event_view_mode(self, &key),
UIMode::Cursor => { UIMode::Cursor => handle_key_event_cursor_mode(self, &key),
// we handle a situation where we got resized and the old cursor is no longer valid
self.cursor = get_cursor(self);
handle_key_event_cursor_mode(self, &key)
}
}; };
if matches!(&result, Some(Transition::Ok) | Some(Transition::Cmd { .. })) { if matches!(&result, Some(Transition::Ok) | Some(Transition::Cmd { .. })) {
// update status bar let report = self.create_records_report();
let report =
create_records_report(self.get_layer_last(), &self.state, self.mode, self.cursor);
info.status = Some(report); info.status = Some(report);
} }
@ -158,10 +322,7 @@ impl View for RecordView<'_> {
for (column, _) in cells.iter().enumerate() { for (column, _) in cells.iter().enumerate() {
if i == pos { if i == pos {
let layer = self.get_layer_last_mut(); self.get_layer_last_mut().cursor.set_position(row, column);
layer.index_column = column;
layer.index_row = row;
return true; return true;
} }
@ -175,6 +336,46 @@ impl View for RecordView<'_> {
fn exit(&mut self) -> Option<Value> { fn exit(&mut self) -> Option<Value> {
Some(build_last_value(self)) 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)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
@ -187,10 +388,10 @@ enum UIMode {
pub struct RecordLayer<'a> { pub struct RecordLayer<'a> {
columns: Cow<'a, [String]>, columns: Cow<'a, [String]>,
records: Cow<'a, [Vec<Value>]>, records: Cow<'a, [Vec<Value>]>,
pub(crate) index_row: usize, orientation: Orientation,
pub(crate) index_column: usize,
name: Option<String>, name: Option<String>,
was_transposed: bool, was_transposed: bool,
cursor: XYCursor,
} }
impl<'a> RecordLayer<'a> { impl<'a> RecordLayer<'a> {
@ -198,11 +399,15 @@ impl<'a> RecordLayer<'a> {
columns: impl Into<Cow<'a, [String]>>, columns: impl Into<Cow<'a, [String]>>,
records: impl Into<Cow<'a, [Vec<Value>]>>, records: impl Into<Cow<'a, [Vec<Value>]>>,
) -> Self { ) -> Self {
let columns = columns.into();
let records = records.into();
let cursor = XYCursor::new(records.len(), columns.len());
Self { Self {
columns: columns.into(), columns,
records: records.into(), records,
index_row: 0, cursor,
index_column: 0, orientation: Orientation::Top,
name: None, name: None,
was_transposed: false, was_transposed: false,
} }
@ -213,33 +418,27 @@ impl<'a> RecordLayer<'a> {
} }
fn count_rows(&self) -> usize { 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 { 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 { fn get_column_header(&self) -> Option<String> {
let current_row = y as usize + self.index_row; let col = self.cursor.column();
let current_column = x as usize + self.index_column;
let row = self.records[current_row].clone();
row[current_column].clone()
}
fn get_current_header(&self, Position { x, .. }: Position) -> Option<String> {
let col = x as usize + self.index_column;
self.columns.get(col).map(|header| header.to_string()) self.columns.get(col).map(|header| header.to_string())
} }
}
#[derive(Debug, Default, Clone)] fn reset_cursor(&mut self) {
pub struct RecordViewState { self.cursor = XYCursor::new(self.count_rows(), self.count_columns());
count_rows: usize, }
count_columns: usize,
data_index: HashMap<(usize, usize), ElementInfo>,
} }
fn handle_key_event_view_mode(view: &mut RecordView, key: &KeyEvent) -> Option<Transition> { 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 => { KeyCode::Esc => {
if view.layer_stack.len() > 1 { if view.layer_stack.len() > 1 {
view.layer_stack.pop(); view.layer_stack.pop();
view.mode = UIMode::Cursor;
Some(Transition::Ok) Some(Transition::Ok)
} else { } else {
Some(Transition::Exit) Some(Transition::Exit)
} }
} }
KeyCode::Char('i') => { KeyCode::Char('i') | KeyCode::Enter => {
view.mode = UIMode::Cursor; view.set_cursor_mode();
view.cursor = Position::default();
Some(Transition::Ok) Some(Transition::Ok)
} }
KeyCode::Char('t') => { KeyCode::Char('t') => {
let layer = view.get_layer_last_mut(); view.transpose();
layer.index_column = 0;
layer.index_row = 0;
transpose_table(layer);
Some(Transition::Ok) Some(Transition::Ok)
} }
KeyCode::Char('e') => Some(Transition::Cmd(String::from("expand"))),
KeyCode::Up => { KeyCode::Up => {
let layer = view.get_layer_last_mut(); view.get_layer_last_mut().cursor.prev_row_i();
layer.index_row = layer.index_row.saturating_sub(1);
Some(Transition::Ok) Some(Transition::Ok)
} }
KeyCode::Down => { KeyCode::Down => {
let layer = view.get_layer_last_mut(); view.get_layer_last_mut().cursor.next_row_i();
let max_index = layer.count_rows().saturating_sub(1);
layer.index_row = min(layer.index_row + 1, max_index);
Some(Transition::Ok) Some(Transition::Ok)
} }
KeyCode::Left => { KeyCode::Left => {
let layer = view.get_layer_last_mut(); view.get_layer_last_mut().cursor.prev_column_i();
layer.index_column = layer.index_column.saturating_sub(1);
Some(Transition::Ok) Some(Transition::Ok)
} }
KeyCode::Right => { KeyCode::Right => {
let layer = view.get_layer_last_mut(); view.get_layer_last_mut().cursor.next_column_i();
let max_index = layer.count_columns().saturating_sub(1);
layer.index_column = min(layer.index_column + 1, max_index);
Some(Transition::Ok) Some(Transition::Ok)
} }
KeyCode::PageUp => { KeyCode::PageUp => {
let count_rows = view.state.count_rows; view.get_layer_last_mut().cursor.prev_row_page();
let layer = view.get_layer_last_mut();
layer.index_row = layer.index_row.saturating_sub(count_rows as usize);
Some(Transition::Ok) Some(Transition::Ok)
} }
KeyCode::PageDown => { KeyCode::PageDown => {
let count_rows = view.state.count_rows; view.get_layer_last_mut().cursor.next_row_page();
let layer = view.get_layer_last_mut();
let max_index = layer.count_rows().saturating_sub(1);
layer.index_row = min(layer.index_row + count_rows as usize, max_index);
Some(Transition::Ok) 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> { fn handle_key_event_cursor_mode(view: &mut RecordView, key: &KeyEvent) -> Option<Transition> {
match key.code { match key.code {
KeyCode::Esc => { KeyCode::Esc => {
view.mode = UIMode::View; view.set_view_mode();
view.cursor = Position::default();
Some(Transition::Ok) Some(Transition::Ok)
} }
KeyCode::Up => { KeyCode::Up => {
if view.cursor.y == 0 { view.get_layer_last_mut().cursor.prev_row();
let layer = view.get_layer_last_mut();
layer.index_row = layer.index_row.saturating_sub(1);
} else {
view.cursor.y -= 1
}
Some(Transition::Ok) Some(Transition::Ok)
} }
KeyCode::Down => { KeyCode::Down => {
let cursor = view.cursor; view.get_layer_last_mut().cursor.next_row();
let showed_rows = view.state.count_rows;
let layer = view.get_layer_last_mut();
let total_rows = layer.count_rows();
let row_index = layer.index_row + cursor.y as usize + 1;
if row_index < total_rows {
if cursor.y as usize + 1 == showed_rows {
layer.index_row += 1;
} else {
view.cursor.y += 1;
}
}
Some(Transition::Ok) Some(Transition::Ok)
} }
KeyCode::Left => { KeyCode::Left => {
let cursor = view.cursor; view.get_layer_last_mut().cursor.prev_column();
let layer = view.get_layer_last_mut();
if cursor.x == 0 {
layer.index_column = layer.index_column.saturating_sub(1);
} else {
view.cursor.x -= 1
}
Some(Transition::Ok) Some(Transition::Ok)
} }
KeyCode::Right => { KeyCode::Right => {
let cursor = view.cursor; view.get_layer_last_mut().cursor.next_column();
let showed_columns = view.state.count_columns;
let layer = view.get_layer_last_mut();
let total_columns = layer.count_columns(); Some(Transition::Ok)
let column_index = layer.index_column + cursor.x as usize + 1; }
KeyCode::PageUp => {
view.get_layer_last_mut().cursor.prev_row_page();
if column_index < total_columns { Some(Transition::Ok)
if cursor.x as usize + 1 == showed_columns {
layer.index_column += 1;
} else {
view.cursor.x += 1;
}
} }
KeyCode::PageDown => {
view.get_layer_last_mut().cursor.next_row_page();
Some(Transition::Ok) Some(Transition::Ok)
} }
KeyCode::Enter => { 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); 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) Some(Transition::Ok)
} }
_ => None, _ => None,
} }
} }
fn get_peeked_layer(view: &RecordView) -> RecordLayer<'static> { fn create_layer(value: Value) -> RecordLayer<'static> {
let layer = view.get_layer_last();
let value = layer.get_current_value(view.cursor);
let (columns, values) = collect_input(value); let (columns, values) = collect_input(value);
RecordLayer::new(columns, values) 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>) { fn push_layer(view: &mut RecordView<'_>, mut next_layer: RecordLayer<'static>) {
let layer = view.get_layer_last(); 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 { if let Some(header) = header {
next_layer.set_name(header); next_layer.set_name(header);
} }
view.layer_stack.push(next_layer); view.layer_stack.push(next_layer);
view.mode = UIMode::View;
view.cursor = Position::default();
} }
fn estimate_page_size(area: Rect, show_head: bool) -> u16 { 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) { fn state_reverse_data(state: &mut RecordView<'_>, page_size: usize) {
let layer = state.get_layer_last_mut(); let layer = state.get_layer_last_mut();
let count_rows = layer.records.len(); let count_rows = layer.records.len();
if count_rows > page_size as usize { if count_rows > page_size {
layer.index_row = count_rows - page_size as usize; layer.cursor.set_position(count_rows - page_size, 0);
} }
} }
@ -451,18 +613,7 @@ fn convert_records_to_string(
.collect::<Vec<_>>() .collect::<Vec<_>>()
} }
fn highlight_cell( fn highlight_cell(f: &mut Frame, area: Rect, info: ElementInfo, theme: &CursorStyle) {
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 { if let Some(style) = theme.selected_column {
let hightlight_block = Block::default().style(nu_style_to_tui(style)); 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); let area = Rect::new(info.area.x, area.y, info.area.width, area.height);
@ -484,23 +635,11 @@ fn highlight_cell(
if theme.show_cursow { if theme.show_cursow {
f.set_cursor(info.area.x, info.area.y); f.set_cursor(info.area.x, info.area.y);
} }
}
}
fn get_cursor(v: &RecordView<'_>) -> Position {
let count_rows = v.state.count_rows as u16;
let count_columns = v.state.count_columns as u16;
let mut cursor = v.cursor;
cursor.y = min(cursor.y, count_rows.saturating_sub(1) as u16);
cursor.x = min(cursor.x, count_columns.saturating_sub(1) as u16);
cursor
} }
fn build_last_value(v: &RecordView) -> Value { fn build_last_value(v: &RecordView) -> Value {
if v.mode == UIMode::Cursor { if v.mode == UIMode::Cursor {
peak_current_value(v) v.get_current_value()
} else if v.get_layer_last().count_rows() < 2 { } else if v.get_layer_last().count_rows() < 2 {
build_table_as_record(v) build_table_as_record(v)
} else { } 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 { fn build_table_as_list(v: &RecordView) -> Value {
let layer = v.get_layer_last(); let layer = v.get_layer_last();
@ -551,40 +681,28 @@ fn build_table_as_record(v: &RecordView) -> Value {
} }
} }
fn create_records_report( fn report_cursor_position(mode: UIMode, cursor: XYCursor) -> String {
layer: &RecordLayer,
state: &RecordViewState,
mode: UIMode,
cursor: Position,
) -> Report {
let seen_rows = layer.index_row + state.count_rows;
let seen_rows = min(seen_rows, layer.count_rows());
let percent_rows = get_percentage(seen_rows, layer.count_rows());
let covered_percent = match percent_rows {
100 => String::from("All"),
_ if layer.index_row == 0 => String::from("Top"),
value => format!("{}%", value),
};
let title = if let Some(name) = &layer.name {
name.clone()
} else {
String::new()
};
let cursor = {
if mode == UIMode::Cursor { if mode == UIMode::Cursor {
let row = layer.index_row + cursor.y as usize; let row = cursor.row();
let column = layer.index_column + cursor.x as usize; let column = cursor.column();
format!("{},{}", row, column) format!("{},{}", row, column)
} else { } 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 { fn report_row_position(cursor: XYCursor) -> String {
message: title, if cursor.row_starts_at() == 0 {
context: covered_percent, String::from("Top")
context2: cursor, } else {
level: Severity::Info, 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<'_>) { fn transpose_table(layer: &mut RecordLayer<'_>) {
let count_rows = layer.count_rows(); let count_rows = layer.records.len();
let count_columns = layer.count_columns(); let count_columns = layer.columns.len();
if layer.was_transposed { if layer.was_transposed {
let data = match &mut layer.records { let data = match &mut layer.records {
@ -656,3 +774,62 @@ fn _transpose_table(
data 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,
}

View File

@ -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 nu_table::{string_width, Alignment, TextStyle};
use tui::{ use tui::{
@ -9,30 +12,45 @@ use tui::{
}; };
use crate::{ use crate::{
nu_common::{NuStyle, NuStyleTable, NuText}, nu_common::{truncate_str, NuStyle, NuStyleTable, NuText},
pager::{nu_style_to_tui, text_style_to_tui_style}, views::util::{nu_style_to_tui, text_style_to_tui_style},
views::ElementInfo,
}; };
use super::Layout; use super::Layout;
#[derive(Debug, Clone)]
pub struct TableW<'a> { pub struct TableW<'a> {
columns: Cow<'a, [String]>, columns: Cow<'a, [String]>,
data: Cow<'a, [Vec<NuText>]>, data: Cow<'a, [Vec<NuText>]>,
index_row: usize, index_row: usize,
index_column: usize, index_column: usize,
style: TableStyle, style: TableStyle,
head_position: Orientation,
color_hm: &'a NuStyleTable, 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 struct TableStyle {
pub splitline_style: NuStyle,
pub shift_line_style: NuStyle,
pub show_index: bool, pub show_index: bool,
pub show_header: bool, pub show_header: bool,
pub splitline_style: NuStyle,
pub header_top: bool, pub header_top: bool,
pub header_bottom: bool, pub header_bottom: bool,
pub shift_line: bool, pub shift_line: bool,
pub index_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> { impl<'a> TableW<'a> {
@ -44,14 +62,16 @@ impl<'a> TableW<'a> {
index_row: usize, index_row: usize,
index_column: usize, index_column: usize,
style: TableStyle, style: TableStyle,
head_position: Orientation,
) -> Self { ) -> Self {
Self { Self {
columns: columns.into(), columns: columns.into(),
data: data.into(), data: data.into(),
color_hm,
index_row, index_row,
index_column, index_column,
style, style,
head_position,
color_hm,
} }
} }
} }
@ -61,7 +81,7 @@ pub struct TableWState {
pub layout: Layout, pub layout: Layout,
pub count_rows: usize, pub count_rows: usize,
pub count_columns: usize, pub count_columns: usize,
pub data_index: HashMap<(usize, usize), ElementInfo>, pub data_height: u16,
} }
impl StatefulWidget for TableW<'_> { impl StatefulWidget for TableW<'_> {
@ -73,17 +93,42 @@ impl StatefulWidget for TableW<'_> {
buf: &mut tui::buffer::Buffer, buf: &mut tui::buffer::Buffer,
state: &mut Self::State, state: &mut Self::State,
) { ) {
const CELL_PADDING_LEFT: u16 = 2; if area.width < 5 {
const CELL_PADDING_RIGHT: u16 = 2; 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_index = self.style.show_index;
let show_head = self.style.show_header; let show_head = self.style.show_header;
let splitline_s = self.style.splitline_style;
let mut data_y = area.y; let splitline_s = self.style.splitline_style;
let shift_column_s = self.style.shift_line_style;
let mut data_height = area.height; let mut data_height = area.height;
let mut data_y = area.y;
let mut head_y = area.y; let mut head_y = area.y;
let is_head_top = matches!(self.head_position, Orientation::Top);
let is_head_bottom = matches!(self.head_position, Orientation::Bottom);
if show_head { if show_head {
if is_head_top {
data_y += 1; data_y += 1;
data_height -= 1; data_height -= 1;
@ -99,108 +144,130 @@ impl StatefulWidget for TableW<'_> {
} }
} }
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
}
}
}
if area.width == 0 || area.height == 0 { if area.width == 0 || area.height == 0 {
return; return;
} }
let mut width = area.x; let mut width = area.x;
let mut data = &self.data[self.index_row..]; let mut data = &self.data[self.index_row..];
if data.len() > data_height as usize { if data.len() > data_height as usize {
data = &data[..data_height as usize]; data = &data[..data_height as usize];
} }
// header lines
if show_head { if show_head {
// fixme: color from config // fixme: color from config
let top = self.style.header_top; let top = self.style.header_top;
let bottom = self.style.header_bottom; let bottom = self.style.header_bottom;
if top || 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 { if show_index {
let area = Rect::new(width, data_y, area.width, data_height); 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 { if self.style.index_line {
let show_head = show_head && self.style.header_bottom; let head_t = show_head && is_head_top && self.style.header_bottom;
width += render_vertical(buf, width, data_y, data_height, show_head, splitline_s); 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; let mut do_render_shift_column = false;
state.count_rows = data.len(); state.count_rows = data.len();
state.count_columns = 0; state.count_columns = 0;
state.data_height = data_height;
for (i, col) in (self.index_column..self.columns.len()).enumerate() { if width > area.width {
let mut head = String::from(&self.columns[col]); return;
}
for col in self.index_column..self.columns.len() {
let mut column = create_column(data, col); let mut column = create_column(data, col);
let column_width = calculate_column_width(&column); let column_width = calculate_column_width(&column);
let mut use_space = column_width as u16;
if show_head { let mut head = String::from(&self.columns[col]);
let head_width = string_width(&head); let head_width = string_width(&head);
let mut use_space = column_width as u16;
if show_head {
use_space = max(head_width as u16, use_space); use_space = max(head_width as u16, use_space);
} }
{ if use_space > 0 {
let available_space = area.width - width; 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 head = show_head.then_some(&mut head);
let control = truncate_column( let (w, ok, shift) =
&mut column, truncate_column_width(space, 1, use_space, pad, is_last, &mut column, head);
head,
available_space,
col + 1 == self.columns.len(),
PrintControl {
break_everything: false,
print_shift_column: false,
print_split_line: true,
width: use_space,
},
);
use_space = control.width; if shift {
do_render_split_line = control.print_split_line; do_render_shift_column = true;
do_render_shift_column = control.print_shift_column; }
if control.break_everything { if w == 0 && !ok {
break; break;
} }
use_space = w;
} }
if show_head { 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 {
let mut w = width; truncate_str(&mut header[0].0, use_space as usize)
w += render_space(buf, w, head_y, 1, CELL_PADDING_LEFT);
w += render_column(buf, w, head_y, use_space, header);
render_space(buf, w, head_y, 1, CELL_PADDING_RIGHT);
let x = w - CELL_PADDING_RIGHT - use_space;
state.layout.push(&header[0].0, x, head_y, use_space, 1);
// it would be nice to add it so it would be available on search
// state.state.data_index.insert((i, col), ElementInfo::new(text, x, data_y, use_space, 1));
} }
width += render_space(buf, width, data_y, data_height, CELL_PADDING_LEFT); let mut w = width;
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 - padding_cell_r - use_space;
state.layout.push(&header[0].0, x, head_y, use_space, 1);
}
width += render_space(buf, width, data_y, data_height, padding_cell_l);
width += render_column(buf, width, data_y, use_space, &column); 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() { 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; let y = data_y + row as u16;
state.layout.push(text, x, y, use_space, 1); 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; state.count_columns += 1;
@ -216,18 +283,18 @@ impl StatefulWidget for TableW<'_> {
// render_shift_column(buf, used_width, head_offset, available_height); // render_shift_column(buf, used_width, head_offset, available_height);
if show_head { if show_head {
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_shift_column(buf, width, head_y, 1, splitline_s); width += render_shift_column(buf, width, head_y, 1, shift_column_s);
width += render_space(buf, width, data_y, data_height, CELL_PADDING_RIGHT); width += render_space(buf, width, data_y, data_height, padding_cell_r);
} }
} }
if do_render_split_line && self.style.shift_line { if self.style.shift_line && width < area.width {
let show_head = show_head && self.style.header_bottom; let head_t = show_head && is_head_top && self.style.header_bottom;
width += render_vertical(buf, width, data_y, data_height, show_head, splitline_s); 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); let rest = area.width.saturating_sub(width);
if rest > 0 { if rest > 0 {
render_space(buf, width, data_y, data_height, rest); 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> { struct IndexColumn<'a> {
@ -275,7 +576,6 @@ impl Widget for IndexColumn<'_> {
fn render_header_borders( fn render_header_borders(
buf: &mut Buffer, buf: &mut Buffer,
area: Rect, area: Rect,
y: u16,
span: u16, span: u16,
style: NuStyle, style: NuStyle,
top: bool, top: bool,
@ -297,18 +597,22 @@ fn render_header_borders(
.borders(borders) .borders(borders)
.border_style(nu_style_to_tui(style)); .border_style(nu_style_to_tui(style));
let height = i + span; 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); block.render(area, buf);
// y pos of header text and next line // y pos of header text and next line
(height.saturating_sub(2), height) (height.saturating_sub(2), height)
} }
fn render_index(buf: &mut Buffer, area: Rect, color_hm: &NuStyleTable, start_index: usize) -> u16 { fn render_index(
const PADDING_LEFT: u16 = 2; buf: &mut Buffer,
const PADDING_RIGHT: u16 = 1; area: Rect,
color_hm: &NuStyleTable,
let mut width = render_space(buf, area.x, area.y, area.height, PADDING_LEFT); 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 index = IndexColumn::new(color_hm, start_index);
let w = index.estimate_width(area.height) as u16; 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); index.render(area, buf);
width += w; 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 width
} }
@ -327,16 +631,19 @@ fn render_vertical(
x: u16, x: u16,
y: u16, y: u16,
height: u16, height: u16,
show_header: bool, top_slit: bool,
bottom_slit: bool,
style: NuStyle, style: NuStyle,
) -> u16 { ) -> u16 {
render_vertical_split(buf, x, y, height, style); 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_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 1
} }
@ -364,7 +671,10 @@ fn create_column(data: &[Vec<NuText>], col: usize) -> Vec<NuText> {
} }
let value = &values[col]; let value = &values[col];
column[row] = value.clone();
let text = value.0.replace('\n', " ");
column[row] = (text, value.1);
} }
column column
@ -390,112 +700,11 @@ fn repeat_vertical(
} }
} }
#[derive(Debug, Default, Copy, Clone)] fn is_space_available(available: u16, got: u16) -> bool {
struct PrintControl { match available.cmp(&got) {
width: u16, Ordering::Less => false,
break_everything: bool, Ordering::Equal | Ordering::Greater => true,
print_split_line: bool,
print_shift_column: bool,
}
fn truncate_column(
column: &mut [NuText],
head: Option<&mut String>,
available_space: u16,
is_column_last: bool,
mut control: PrintControl,
) -> PrintControl {
const CELL_PADDING_LEFT: u16 = 2;
const CELL_PADDING_RIGHT: u16 = 2;
const VERTICAL_LINE_WIDTH: u16 = 1;
const CELL_MIN_WIDTH: u16 = 1;
let min_space_cell = CELL_PADDING_LEFT + CELL_PADDING_RIGHT + CELL_MIN_WIDTH;
let min_space = min_space_cell + VERTICAL_LINE_WIDTH;
if available_space < min_space {
// if there's not enough space at all just return; doing our best
if available_space < VERTICAL_LINE_WIDTH {
control.print_split_line = false;
} }
control.break_everything = true;
return control;
}
let column_taking_space =
control.width + CELL_PADDING_LEFT + CELL_PADDING_RIGHT + VERTICAL_LINE_WIDTH;
let is_enough_space = available_space > column_taking_space;
if !is_enough_space {
if is_column_last {
// we can do nothing about it we need to truncate.
// we assume that there's always at least space for padding and 1 symbol. (5 chars)
let width = available_space
.saturating_sub(CELL_PADDING_LEFT + CELL_PADDING_RIGHT + VERTICAL_LINE_WIDTH);
if width == 0 {
control.break_everything = true;
return control;
}
if let Some(head) = head {
truncate_str(head, width as usize);
}
truncate_list(column, width as usize);
control.width = width;
} else {
let min_space_2cells = min_space + min_space_cell;
if available_space > min_space_2cells {
let width = available_space.saturating_sub(min_space_2cells);
if width == 0 {
control.break_everything = true;
return control;
}
truncate_list(column, width as usize);
if let Some(head) = head {
truncate_str(head, width as usize);
}
control.width = width;
control.print_shift_column = true;
} else {
control.break_everything = true;
control.print_shift_column = true;
}
}
} else if !is_column_last {
// even though we can safely render current column,
// we need to check whether there's enough space for AT LEAST a shift column
// (2 padding + 2 padding + 1 a char)
let left_space = available_space - column_taking_space;
if left_space < min_space {
let need_space = min_space_cell - left_space;
let min_left_width = 1;
let is_column_big_enough = control.width > need_space + min_left_width;
if is_column_big_enough {
let width = control.width.saturating_sub(need_space);
if width == 0 {
control.break_everything = true;
return control;
}
truncate_list(column, width as usize);
if let Some(head) = head {
truncate_str(head, width as usize);
}
control.width = width;
control.print_shift_column = true;
}
}
}
control
} }
fn truncate_list(list: &mut [NuText], width: usize) { 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 { fn render_shift_column(buf: &mut Buffer, x: u16, y: u16, height: u16, style: NuStyle) -> u16 {
let style = TextStyle { let style = TextStyle {
alignment: Alignment::Left, 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); 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 { fn calculate_column_width(column: &[NuText]) -> usize {
column column
.iter() .iter()

View 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),
}
}

View File

@ -197,11 +197,6 @@ impl TrimStrategy {
} }
} }
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ExploreConfig {
pub color_config: HashMap<String, Value>,
}
impl Value { impl Value {
pub fn into_config(&mut self, config: &Config) -> (Config, Option<ShellError>) { pub fn into_config(&mut self, config: &Config) -> (Config, Option<ShellError>) {
// Clone the passed-in config rather than mutating it. // Clone the passed-in config rather than mutating it.

View File

@ -270,20 +270,64 @@ let-env config = {
truncating_suffix: "..." # A suffix used by the 'truncating' methodology truncating_suffix: "..." # A suffix used by the 'truncating' methodology
} }
} }
explore: { explore: {
highlight: { bg: 'yellow', fg: 'black' } help_banner: true
status_bar: { bg: '#C4C9C6', fg: '#1D1F21' } exit_esc: true
command_bar: { fg: '#C4C9C6' }
split_line: '#404040' command_bar_text: '#C4C9C6'
cursor: true # command_bar: {fg: '#C4C9C6' bg: '#223311' }
# selected_column: 'blue'
# selected_row: { fg: 'yellow', bg: '#C1C2A3' } status_bar_background: {fg: '#1D1F21' bg: '#C4C9C6' }
# selected_cell: { fg: 'white', bg: '#777777' } # status_bar_text: {fg: '#C4C9C6' bg: '#223311' }
# line_shift: false,
# line_index: false, highlight: {bg: 'yellow' fg: 'black' }
# line_head_top: false,
# line_head_bottom: false, 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: { history: {
max_size: 10000 # Session has to be reloaded for this to take effect 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 sync_on_enter: true # Enable to share history between multiple sessions, else you have to close the session to write history to file