basementdevs / scyllash

13 stars 1 forks source link

feat: tree view #2

Open DanielHe4rt opened 3 weeks ago

DanielHe4rt commented 3 weeks ago

Feature

tree view sketch

The first area which we can work on in this project is the Left Bar, which will be placed a Tree View for navigate through.

First Approach

My first glimpse on it was something around the File Explorer, where you can list keyspaces and enter a Keyspace which will list all tables, mvs and udts. Fortunately @Daniel-Boll found in advance the lib ratatui-explorer, which works perfectly but we would need to fork it (I GUESS) to make it work based on our needs.

File Explorer Ratatui

Second Approach

Also we have the possibility to just bring a Tree View on it and make it work faster, since it's simple do add new nodes to it. @Daniel-Boll also brought this crate which is tui-rs-tree-widget and solves our problem in a short term.

tui-rs tree overview

So, what would you like to implement in a first moment?

Daniel-Boll commented 3 weeks ago

image

I've come up with this, visually I think it's pleasant, it lacks behavior as of right now, but I think that's the idea for this issue as of right now.

I just will just have to refactor a LOT of code to follow the Elm Architecture which works better to handle this stuff. To better test this approach I just create another project to try without having to refactor stuff here, @DanielHe4rt can you give me your thoughts on this? If it's worth and all.

sequenceDiagram
participant User
participant TUI Application

User->>TUI Application: Input/Event/Message
TUI Application->>TUI Application: Update (based on Model and Message)
TUI Application->>TUI Application: Render View (from Model)
TUI Application-->>User: Display UI

The current solution looks like this:

stateDiagram-v2
    [*] --> Running
    Running --> [*]: Quit

    state Running {
        [*] --> Navigation

        state Navigation {
            [*] --> NoHover
            NoHover --> HoverSidebar: Left
            NoHover --> HoverREPL: Right
            HoverSidebar --> NoHover: Right
            HoverREPL --> NoHover: Left
            HoverSidebar --> FocusSidebar: Enter
            HoverREPL --> FocusREPL: Enter
        }

        Navigation --> FocusSidebar: Enter on HoverSidebar
        Navigation --> FocusREPL: Enter on HoverREPL

        state FocusSidebar {
            [*] --> TreeNavigation
            TreeNavigation --> TreeToggle: Enter/Space
            TreeNavigation --> TreeLeft: Left
            TreeNavigation --> TreeRight: Right
            TreeNavigation --> TreeUp: Up
            TreeNavigation --> TreeDown: Down
            TreeNavigation --> TreeFirst: Home
            TreeNavigation --> TreeLast: End
            TreeNavigation --> TreeScrollUp: PageUp
            TreeNavigation --> TreeScrollDown: PageDown
        }

        state FocusREPL {
            [*] --> REPLInput
            REPLInput --> REPLExecute: Enter
            REPLExecute --> REPLOutput
            REPLOutput --> REPLInput
        }

        FocusSidebar --> Navigation: Esc
        FocusREPL --> Navigation: Esc
    }
Implementation

```rust use ratatui::{ backend::CrosstermBackend, crossterm::{ event::{self, Event, KeyCode}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }, layout::{Constraint, Direction, Layout}, style::{Color, Modifier, Style}, widgets::{Block, Borders, Paragraph}, Frame, Terminal, }; use std::io; use tui_tree_widget::{Tree, TreeItem, TreeState}; #[derive(Clone, Copy, PartialEq, Eq)] enum FocusState { Navigation, Repl, Sidebar, } type Identifier = String; struct Model { keyspaces: Vec>, tree_state: TreeState, repl_input: String, repl_output: Vec, cluster_info: ClusterInfo, focus_state: FocusState, hovered_module: Option, } struct ClusterInfo { nodes: u32, active_nodes: u32, datacenter: String, current_keyspace: String, } impl Model { fn new() -> Self { let keyspace_items = vec![ TreeItem::new_leaf("mykeyspace".to_string(), "My Keyspace"), TreeItem::new_leaf("another".to_string(), "Another Keyspace"), ]; let system_items = vec![ TreeItem::new_leaf("system_schema".to_string(), "System Schema"), TreeItem::new_leaf("system_auth".to_string(), "System Auth"), ]; let keyspaces = vec![ TreeItem::new( "user keyspaces".to_string(), "User Keyspaces", keyspace_items, ) .expect("unique identifier"), TreeItem::new( "system keyspaces".to_string(), "System Keyspaces", system_items, ) .expect("unique identifier"), ]; Model { keyspaces, tree_state: tui_tree_widget::TreeState::default(), repl_input: String::new(), repl_output: Vec::new(), cluster_info: ClusterInfo { nodes: 3, active_nodes: 3, datacenter: "none".to_string(), current_keyspace: "none".to_string(), }, focus_state: FocusState::Repl, hovered_module: None, } } } #[allow(dead_code)] enum Message { InputChanged(String), ExecuteCommand, SelectKeyspace(String), ChangeFocus(FocusState), TreeAction(TreeAction), Hover(Option), Quit, } #[allow(dead_code)] enum TreeAction { Toggle, Left, Right, Down, Up, SelectFirst, SelectLast, ScrollDown(usize), ScrollUp(usize), Deselect, } fn update(model: &mut Model, msg: Message) -> bool { match msg { Message::InputChanged(input) => { model.repl_input = input; true } Message::ExecuteCommand => { model .repl_output .push(format!("Executed: {}", model.repl_input)); model.repl_input.clear(); true } Message::SelectKeyspace(keyspace) => { model.cluster_info.current_keyspace = keyspace; true } Message::ChangeFocus(new_focus) => { model.focus_state = new_focus; true } Message::Hover(module) => { model.hovered_module = module; true } Message::TreeAction(action) => match action { TreeAction::Toggle => model.tree_state.toggle_selected(), TreeAction::Left => model.tree_state.key_left(), TreeAction::Right => model.tree_state.key_right(), TreeAction::Down => model.tree_state.key_down(), TreeAction::Up => model.tree_state.key_up(), TreeAction::SelectFirst => model.tree_state.select_first(), TreeAction::SelectLast => model.tree_state.select_last(), TreeAction::ScrollDown(amount) => model.tree_state.scroll_down(amount), TreeAction::ScrollUp(amount) => model.tree_state.scroll_up(amount), TreeAction::Deselect => model.tree_state.select(Vec::new()), }, Message::Quit => false, } } fn view(model: &mut Model, f: &mut Frame) { let chunks = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Length(3), Constraint::Min(0)].as_ref()) .split(f.area()); let header_chunks = Layout::default() .direction(Direction::Horizontal) .constraints([Constraint::Length(30), Constraint::Min(0)].as_ref()) .split(chunks[0]); let body_chunks = Layout::default() .direction(Direction::Horizontal) .constraints([Constraint::Length(30), Constraint::Min(0)].as_ref()) .split(chunks[1]); // Sidebar with tree widget let sidebar_block = Block::default() .borders(Borders::ALL) .title("Select your keyspace") .border_style(Style::default().fg(match model.focus_state { FocusState::Sidebar => Color::Green, FocusState::Navigation if model.hovered_module == Some(FocusState::Sidebar) => Color::Yellow, _ => Color::White, })); let tree_widget = Tree::new(&model.keyspaces) .expect("all item identifiers are unique") .block(sidebar_block) .highlight_style( Style::default() .fg(Color::Black) .bg(Color::LightGreen) .add_modifier(Modifier::BOLD), ); f.render_stateful_widget(tree_widget, body_chunks[0], &mut model.tree_state); // Header with cluster information let header = Paragraph::new(format!( "Nodes: {} Active Nodes: {} Datacenter: {} Current Keyspace: {}", model.cluster_info.nodes, model.cluster_info.active_nodes, model.cluster_info.datacenter, model.cluster_info.current_keyspace )) .block(Block::default().borders(Borders::ALL)); f.render_widget(header, header_chunks[1]); // REPL output let repl_block = Block::default() .borders(Borders::ALL) .title("ScyllaSH 0.0.1") .border_style(Style::default().fg(match model.focus_state { FocusState::Repl => Color::Green, FocusState::Navigation if model.hovered_module == Some(FocusState::Repl) => Color::Yellow, _ => Color::White, })); let repl_output = Paragraph::new(model.repl_output.join("\n")).block(repl_block); f.render_widget(repl_output, body_chunks[1]); } fn main() -> Result<(), Box> { execute!(io::stdout(), EnterAlternateScreen)?; enable_raw_mode()?; let mut terminal = Terminal::new(CrosstermBackend::new(io::stdout()))?; let mut model = Model::new(); // let mut line_editor = Reedline::create(); // let prompt = DefaultPrompt::default(); loop { terminal.draw(|f| view(&mut model, f))?; if let Event::Key(key) = event::read()? { let update = match model.focus_state { FocusState::Navigation => match key.code { KeyCode::Char('q') => break, KeyCode::Left => update(&mut model, Message::Hover(Some(FocusState::Sidebar))), KeyCode::Right => update(&mut model, Message::Hover(Some(FocusState::Repl))), KeyCode::Enter => match model.hovered_module { Some(module) => update(&mut model, Message::ChangeFocus(module)), None => false, }, _ => false, }, FocusState::Repl => match key.code { KeyCode::Esc => update(&mut model, Message::ChangeFocus(FocusState::Navigation)), KeyCode::Enter => { false // if let Signal::Success(line) = line_editor.read_line(&prompt)? { // update(&mut model, Message::InputChanged(line)) // | update(&mut model, Message::ExecuteCommand) // } else { // false // } } _ => false, // Handle other REPL input }, FocusState::Sidebar => match key.code { KeyCode::Esc => update(&mut model, Message::ChangeFocus(FocusState::Navigation)), KeyCode::Enter | KeyCode::Char(' ') => { update(&mut model, Message::TreeAction(TreeAction::Toggle)) } KeyCode::Left => update(&mut model, Message::TreeAction(TreeAction::Left)), KeyCode::Right => update(&mut model, Message::TreeAction(TreeAction::Right)), KeyCode::Down => update(&mut model, Message::TreeAction(TreeAction::Down)), KeyCode::Up => update(&mut model, Message::TreeAction(TreeAction::Up)), KeyCode::Home => update(&mut model, Message::TreeAction(TreeAction::SelectFirst)), KeyCode::End => update(&mut model, Message::TreeAction(TreeAction::SelectLast)), KeyCode::PageDown => update(&mut model, Message::TreeAction(TreeAction::ScrollDown(3))), KeyCode::PageUp => update(&mut model, Message::TreeAction(TreeAction::ScrollUp(3))), _ => false, }, }; if update { terminal.draw(|f| view(&mut model, f))?; } } } execute!(io::stdout(), LeaveAlternateScreen)?; disable_raw_mode()?; Ok(()) } ```