Configuration in `explore` has always been confusing to me. This PR overhauls (and simplifies, I think) how configuration is done. # Details 1. Configuration is now strongly typed. In `Explore::run()` we create an `ExploreConfig` struct from the more general Nu configuration and arguments to `explore`, then pass that struct to other parts of `explore` that need configuration. IMO this is a lot easier to reason about and trace than the previous approach of creating a `HashMap<String, Value>` and then using that to make various structs elsewhere. 2. We now inherit more configuration from the config used for regular Nu tables 1. Border/line styling now uses the `separator` style used for regular Nu tables, the special `explore.split_line` config point has been retired. 2. Cell padding in tables is now controlled by `table.padding` instead of the undocumented `column_padding_left`/`column_padding_right` config 3. The (optional, previously not enabled by default) `selected_row` and `selected_column` configuration has been removed. We now only highlight the selected cell. I could re-add this if people really like the feature but I'm guessing nobody uses it. The interface still looks the same with a default/empty config.nu: 
233 lines
6.2 KiB
Rust
233 lines
6.2 KiB
Rust
// todo: 3 cursor modes one for section
|
|
|
|
mod binary_widget;
|
|
|
|
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
|
use nu_protocol::{
|
|
engine::{EngineState, Stack},
|
|
Value,
|
|
};
|
|
use ratatui::layout::Rect;
|
|
|
|
use crate::{
|
|
explore::ExploreConfig,
|
|
nu_common::NuText,
|
|
pager::{
|
|
report::{Report, Severity},
|
|
Frame, Transition, ViewInfo,
|
|
},
|
|
views::cursor::Position,
|
|
};
|
|
|
|
use self::binary_widget::{BinarySettings, BinaryStyle, BinaryWidget};
|
|
|
|
use super::{cursor::WindowCursor2D, Layout, View, ViewConfig};
|
|
|
|
/// An interactive view that displays binary data in a hex dump format.
|
|
/// Not finished; many aspects are still WIP.
|
|
#[derive(Debug, Clone)]
|
|
pub struct BinaryView {
|
|
data: Vec<u8>,
|
|
// HACK: we are only using the vertical dimension of the cursor, should we use a plain old WindowCursor?
|
|
cursor: WindowCursor2D,
|
|
settings: Settings,
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone)]
|
|
struct Settings {
|
|
opts: BinarySettings,
|
|
style: BinaryStyle,
|
|
}
|
|
|
|
impl BinaryView {
|
|
pub fn new(data: Vec<u8>) -> Self {
|
|
Self {
|
|
data,
|
|
cursor: WindowCursor2D::default(),
|
|
settings: Settings::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl View for BinaryView {
|
|
fn draw(&mut self, f: &mut Frame, area: Rect, _cfg: ViewConfig<'_>, _layout: &mut Layout) {
|
|
let widget = create_binary_widget(self);
|
|
f.render_widget(widget, area);
|
|
}
|
|
|
|
fn handle_input(
|
|
&mut self,
|
|
_: &EngineState,
|
|
_: &mut Stack,
|
|
_: &Layout,
|
|
info: &mut ViewInfo,
|
|
key: KeyEvent,
|
|
) -> Option<Transition> {
|
|
let result = handle_event_view_mode(self, &key);
|
|
|
|
if matches!(&result, Some(Transition::Ok)) {
|
|
let report = create_report(self.cursor);
|
|
info.status = Some(report);
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
fn collect_data(&self) -> Vec<NuText> {
|
|
// todo: impl to allow search
|
|
vec![]
|
|
}
|
|
|
|
fn show_data(&mut self, _pos: usize) -> bool {
|
|
// todo: impl to allow search
|
|
false
|
|
}
|
|
|
|
fn exit(&mut self) -> Option<Value> {
|
|
// todo: impl Cursor + peek of a value
|
|
None
|
|
}
|
|
|
|
fn setup(&mut self, cfg: ViewConfig<'_>) {
|
|
self.settings = settings_from_config(cfg.explore_config);
|
|
|
|
let count_rows =
|
|
BinaryWidget::new(&self.data, self.settings.opts, Default::default()).count_lines();
|
|
// TODO: refactor View so setup() is fallible and we don't have to panic here
|
|
self.cursor = WindowCursor2D::new(count_rows, 1).expect("Failed to create XYCursor");
|
|
}
|
|
}
|
|
|
|
fn create_binary_widget(v: &BinaryView) -> BinaryWidget<'_> {
|
|
let start_line = v.cursor.window_origin().row;
|
|
let count_elements =
|
|
BinaryWidget::new(&[], v.settings.opts, Default::default()).count_elements();
|
|
let index = start_line * count_elements;
|
|
let data = &v.data[index..];
|
|
|
|
let mut w = BinaryWidget::new(data, v.settings.opts, v.settings.style.clone());
|
|
w.set_row_offset(index);
|
|
|
|
w
|
|
}
|
|
|
|
fn handle_event_view_mode(view: &mut BinaryView, key: &KeyEvent) -> Option<Transition> {
|
|
match key {
|
|
KeyEvent {
|
|
code: KeyCode::Char('u'),
|
|
modifiers: KeyModifiers::CONTROL,
|
|
..
|
|
}
|
|
| KeyEvent {
|
|
code: KeyCode::PageUp,
|
|
..
|
|
} => {
|
|
view.cursor.prev_row_page();
|
|
|
|
return Some(Transition::Ok);
|
|
}
|
|
KeyEvent {
|
|
code: KeyCode::Char('d'),
|
|
modifiers: KeyModifiers::CONTROL,
|
|
..
|
|
}
|
|
| KeyEvent {
|
|
code: KeyCode::PageDown,
|
|
..
|
|
} => {
|
|
view.cursor.next_row_page();
|
|
|
|
return Some(Transition::Ok);
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
match key.code {
|
|
KeyCode::Esc => Some(Transition::Exit),
|
|
KeyCode::Up | KeyCode::Char('k') => {
|
|
view.cursor.prev_row_i();
|
|
|
|
Some(Transition::Ok)
|
|
}
|
|
KeyCode::Down | KeyCode::Char('j') => {
|
|
view.cursor.next_row_i();
|
|
|
|
Some(Transition::Ok)
|
|
}
|
|
KeyCode::Left | KeyCode::Char('h') => {
|
|
view.cursor.prev_column_i();
|
|
|
|
Some(Transition::Ok)
|
|
}
|
|
KeyCode::Right | KeyCode::Char('l') => {
|
|
view.cursor.next_column_i();
|
|
|
|
Some(Transition::Ok)
|
|
}
|
|
KeyCode::Home | KeyCode::Char('g') => {
|
|
view.cursor.row_move_to_start();
|
|
|
|
Some(Transition::Ok)
|
|
}
|
|
KeyCode::End | KeyCode::Char('G') => {
|
|
view.cursor.row_move_to_end();
|
|
|
|
Some(Transition::Ok)
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn settings_from_config(config: &ExploreConfig) -> Settings {
|
|
// Most of this is hardcoded for now, add it to the config later if needed
|
|
Settings {
|
|
opts: BinarySettings::new(2, 8),
|
|
style: BinaryStyle::new(
|
|
None,
|
|
config.table.column_padding_left as u16,
|
|
config.table.column_padding_right as u16,
|
|
),
|
|
}
|
|
}
|
|
|
|
fn create_report(cursor: WindowCursor2D) -> Report {
|
|
let covered_percent = report_row_position(cursor);
|
|
let cursor = report_cursor_position(cursor);
|
|
let mode = report_mode_name();
|
|
let msg = String::new();
|
|
|
|
Report::new(msg, Severity::Info, mode, cursor, covered_percent)
|
|
}
|
|
|
|
fn report_mode_name() -> String {
|
|
String::from("VIEW")
|
|
}
|
|
|
|
fn report_row_position(cursor: WindowCursor2D) -> String {
|
|
if cursor.window_origin().row == 0 {
|
|
return String::from("Top");
|
|
}
|
|
|
|
// todo: there's some bug in XYCursor; when we hit PgDOWN/UP and general move it exceeds the limit
|
|
// not sure when it was introduced and if present in original view.
|
|
// but it just requires a refactoring as these method names are just ..... not perfect.
|
|
let row = cursor.row().min(cursor.row_limit());
|
|
let count_rows = cursor.row_limit();
|
|
let percent_rows = get_percentage(row, count_rows);
|
|
match percent_rows {
|
|
100 => String::from("All"),
|
|
value => format!("{value}%"),
|
|
}
|
|
}
|
|
|
|
fn report_cursor_position(cursor: WindowCursor2D) -> String {
|
|
let Position { row, column } = cursor.window_origin();
|
|
format!("{row},{column}")
|
|
}
|
|
|
|
fn get_percentage(value: usize, max: usize) -> usize {
|
|
debug_assert!(value <= max, "{value:?} {max:?}");
|
|
|
|
((value as f32 / max as f32) * 100.0).floor() as usize
|
|
}
|