-
-
Notifications
You must be signed in to change notification settings - Fork 1
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
Comments
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
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(())
} |
Feature
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.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.
So, what would you like to implement in a first moment?
The text was updated successfully, but these errors were encountered: