Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: tree view #2

Open
DanielHe4rt opened this issue Aug 22, 2024 · 1 comment
Open

feat: tree view #2

DanielHe4rt opened this issue Aug 22, 2024 · 1 comment
Assignees

Comments

@DanielHe4rt
Copy link
Contributor

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 Daniel-Boll self-assigned this Aug 22, 2024
@Daniel-Boll
Copy link
Collaborator

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
Loading

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
    }
Loading
Implementation

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<TreeItem<'static, Identifier>>,
  tree_state: TreeState<Identifier>,
  repl_input: String,
  repl_output: Vec<String>,
  cluster_info: ClusterInfo,
  focus_state: FocusState,
  hovered_module: Option<FocusState>,
}

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<FocusState>),
  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<dyn std::error::Error>> {
  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(())
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

When branches are created from issues, their pull requests are automatically linked.

2 participants