Skip to content

Commit

Permalink
Merge pull request #17 from jaehyun1ee-furiosa/dev-vim-rebased
Browse files Browse the repository at this point in the history
feat: from simple keybindings to commands like in vim
  • Loading branch information
jaehyun1ee-furiosa authored Feb 21, 2023
2 parents fa43a97 + e876fab commit 00c244d
Show file tree
Hide file tree
Showing 21 changed files with 901 additions and 621 deletions.
109 changes: 62 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,8 @@
# dot-viewer
dot debugger in TUI

# 1. Motivations
`dot-viewer` is a dot-format graph debugger in TUI, inspired by Vim.

커다란 graph를 전부 다 visualize 할 수 없음

- graphviz, xdot, netron와 같은 graph visualizer들이 있지만, node 개수가 수천 개가 넘어가면 rendering 시간이 오래 걸림
- rendering에 성공한다 하더라도, graph 크기가 너무 커서 직관적으로 보기 어려움

# 2. Overview

커다란 graph를 효율적으로 다루고 디버깅할 수 있는 dot-viewer 툴 구현

- dot format의 전체 graph를 input으로 받아서,
- TUI로 전체 graph를 탐색하고,
- (visualize 하기에 충분히 작은) 특정 subgraph만 선택하여 dot format으로 export

# 3. Getting Started
# 1. Getting Started

## a. Prerequisites

Expand Down Expand Up @@ -61,6 +47,14 @@ It is required that [xdot is executable in command-line](https://github.com/jrfo
$ xdot *.dot
```

### iii. Others

Coming from Linux, the followings are necessary for `bindgen` to make bindings to Graphviz.
```console
$ sudo apt install build-essentials cmake
$ sudo apt install clang
```

## b. Installation

### i. Initialize
Expand All @@ -82,61 +76,82 @@ $ cargo run --release [path-to-dot-file]

This will open a TUI screen on the terminal.

# 4. Feature Demo
# 2. Features

With `dot-viewer`, users may

**traverse the graph in TUI** using,
- goto next/prev node of the currently selected node
- fuzzy search on node name
- regex search on node name and attributes


**make and export subgraphs** using,
- subgraph tree selection
- prefix filtering on node names
- applying filter on search matches
- neighboring `n` nodes of the currently selected node

## Keybindings

### General

Key | Actions
--- | ---
`q` | quit
`?` | show help
`esc ` | go back to the main screen
`c` | close the current tab (except for the root tab)
Key | Command | Actions
--- | --- | ---
`q` | | quit `dot-viewer`
. | `:help<CR>` | show help
`esc` | . | go back to the main screen

### Navigation
### Mode Switches

Key | Mode | Actions
Key | From | To
--- | --- | ---
`tab`/`backtab` | `Nav` | move between tabs
`up` | `Nav`/`Search` | traverse the focused node list
`down` | `Nav`/`Search` | traverse the focused node list
`right` | `Nav` | move focus between lists (highlighted in yellow borders)
`left` | `Nav` | move focus between lists (highlighted in yellow borders)
`esc` | All | Normal
`/` | Normal | Fuzzy Search
`r` | Normal | Regex Search
`:` | Normal | Command

### Mode Switch
### Normal

Key | Mode | Actions
--- | --- | ---
`/` | `Nav` | switch to fuzzy `Search` mode (`/[node-id-pattern]`)
`r` | `Nav` | switch to regex `Search` mode (`r[node-attr-regex]`)
`f` | `Nav` | switch to prefix `Filter` mode (`f[node-id-prefix]`)
`s` | `Nav` | switch to subgraph `Popup` mode
`enter` | `Nav`/`Search` | go to the selected node
--- | `Filter` | apply entered prefix (opens a new tab)
--- | `Subgraph` | extract selected subgraph (opens a new tab)
Key | Actions
--- | ---
`c` | close the current tab(view)
`h/l` | move focus between current, prevs, nexts list
`j/k` | traverse in focused list
`n/N` | move between matched nodes
`tab`/`backtab` | move between tabs

### Search
Key | Actions
--- | ---
`tab` | autocomplete search keyword
`enter` | apply search

### Exporting
e.g., in fuzzy search mode, `/g1_s14_t100` and in regex search mode, `r\(H: ., D: .\)`

Key | Mode | Actions
### Command

Key | Command | Actions
--- | --- | ---
`e` | `Nav` | export the current tab to dot
`0-9` | `Nav` | export the subgraph containing neighbors of the current node with given depth
`x` | `Nav` | launch xdot, showing `./exports/current.dot`
. | `filter` | apply filter on current matches, opening a new tab(view)
. | `neighbors [depth]` | get up to `depth` neighbors of the current node in a new tab(view)
. | `export [(opt) filename]` | export the current tab(view) to dot
. | `xdot [(opt) filename]` | launch `xdot` with the filename or `exports/current.dot` by default
. | `subgraph` | open a popup showing subgraph tree
`enter` | . | execute command

All exported files are saved in `exports` directory in the project root.

Most recently exported file is copied in `exports/current.dot`.

### Subgraph Popup

Key | Actions
--- | ---
`h/j/k/l` | traverse the tree
`enter` | change root to the selected subgraph, opening a new tab(view)

### Help Popup

Key | Actions
--- | ---
`h/j/k/l` | traverse help messages
2 changes: 1 addition & 1 deletion dot-graph
3 changes: 3 additions & 0 deletions src/terminal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use tui::{
};

pub fn launch(path: String) -> Result<(), Box<dyn Error>> {
// setup terminal
let mut terminal = setup()?;

// create and run app
Expand All @@ -25,6 +26,7 @@ pub fn launch(path: String) -> Result<(), Box<dyn Error>> {
})?;
let _ = run(&mut terminal, app);

// restore terminal
cleanup()?;

Ok(())
Expand All @@ -37,6 +39,7 @@ fn setup() -> Result<Terminal<CrosstermBackend<Stdout>>, Box<dyn Error>> {
let backend = CrosstermBackend::new(stdout);
let terminal = Terminal::new(backend)?;

// setup panic hook to ensure that the terminal is restored even on panics
setup_panic_hook();

Ok(terminal)
Expand Down
14 changes: 7 additions & 7 deletions src/ui/app.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::ui::{input::draw_input, popup::draw_popup, tabs::draw_tabs};
use crate::viewer::{App, MainMode, Mode};
use crate::viewer::{App, Mode};

use tui::{
backend::Backend,
Expand All @@ -11,26 +11,26 @@ use tui::{
pub(crate) fn draw_app<B: Backend>(f: &mut Frame<B>, app: &mut App) {
let size = f.size();

// surrounding block
let block = Block::default()
.borders(Borders::ALL)
.title("Dot Viewer (Dev)")
.title_alignment(Alignment::Center)
.border_type(BorderType::Rounded);

f.render_widget(block, size);

match &app.mode {
Mode::Main(mmode) => draw_main(f, size, &mmode.clone(), app),
Mode::Popup(pmode) => draw_popup(f, size, &pmode.clone(), app),
Mode::Normal | Mode::Command | Mode::Search(_) => draw_main(f, size, app),
Mode::Popup(_) => draw_popup(f, size, app),
}
}

fn draw_main<B: Backend>(f: &mut Frame<B>, size: Rect, mode: &MainMode, app: &mut App) {
fn draw_main<B: Backend>(f: &mut Frame<B>, size: Rect, app: &mut App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Percentage(90), Constraint::Percentage(10)].as_ref())
.split(size);

draw_tabs(f, chunks[0], mode, app);
draw_input(f, chunks[1], mode, app);
draw_tabs(f, chunks[0], app);
draw_input(f, chunks[1], app);
}
74 changes: 24 additions & 50 deletions src/ui/input.rs
Original file line number Diff line number Diff line change
@@ -1,69 +1,42 @@
use crate::ui::surrounding_block;
use crate::viewer::{App, InputMode, MainMode, SearchMode};
use crate::viewer::{App, Mode, SearchMode};

use tui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Span, Spans, Text},
widgets::Paragraph,
Frame,
};

// input block
pub(super) fn draw_input<B: Backend>(
f: &mut Frame<B>,
chunk: Rect,
mmode: &MainMode,
app: &mut App,
) {
// surrounding block
let title = match mmode {
MainMode::Navigate(_) => "Navigate",
MainMode::Input(imode) => match imode {
InputMode::Search(smode) => match smode {
SearchMode::Fuzzy => "Fuzzy Search",
SearchMode::Regex => "Regex Search",
},
InputMode::Filter => "Filter",
pub(super) fn draw_input<B: Backend>(f: &mut Frame<B>, chunk: Rect, app: &mut App) {
let title = match &app.mode {
Mode::Normal => "Normal",
Mode::Command => "Command",
Mode::Search(smode) => match smode {
SearchMode::Fuzzy => "Fuzzy Search",
SearchMode::Regex => "Regex Search",
},
_ => unreachable!(),
};

let block = surrounding_block(title.to_string(), matches!(mmode, MainMode::Input(_)));
let block = surrounding_block(
title.to_string(),
matches!(app.mode, Mode::Command) || matches!(app.mode, Mode::Search(_)),
);

f.render_widget(block, chunk);

// inner blocks
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(1)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(chunk);
draw_help(f, chunks[0]);
match mmode {
MainMode::Navigate(_) => draw_error(f, chunks[1], app),
MainMode::Input(_) => draw_form(f, chunks[1], mmode, app),
};
}

// help block
fn draw_help<B: Backend>(f: &mut Frame<B>, chunk: Rect) {
let text = vec![
Span::raw("Press "),
Span::styled("?", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
Span::raw(" for help. "),
Span::raw("Press "),
Span::styled("q", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
Span::raw(" to exit"),
];
let mut text = Text::from(Spans::from(text));
text.patch_style(Style::default().add_modifier(Modifier::RAPID_BLINK));

let help = Paragraph::new(text);
f.render_widget(help, chunk);
draw_error(f, chunks[0], app);
draw_form(f, chunks[1], app);
}

// error block
fn draw_error<B: Backend>(f: &mut Frame<B>, chunk: Rect, app: &mut App) {
let msg = match &app.result {
Ok(succ) => succ.to_string(),
Expand All @@ -77,17 +50,18 @@ fn draw_error<B: Backend>(f: &mut Frame<B>, chunk: Rect, app: &mut App) {
}
}

// input block
fn draw_form<B: Backend>(f: &mut Frame<B>, chunk: Rect, mmode: &MainMode, app: &mut App) {
let input = Paragraph::new(app.input.key.clone()).style(match mmode {
MainMode::Navigate(_) => Style::default(),
MainMode::Input(_) => Style::default().fg(Color::Yellow),
fn draw_form<B: Backend>(f: &mut Frame<B>, chunk: Rect, app: &mut App) {
let input = Paragraph::new(app.input.key.clone()).style(match &app.mode {
Mode::Normal => Style::default(),
Mode::Command | Mode::Search(_) => Style::default().fg(Color::Yellow),
_ => unreachable!(),
});
f.render_widget(input, chunk);

// cursor
match mmode {
MainMode::Navigate(_) => {}
MainMode::Input(_) => f.set_cursor(chunk.x + app.input.cursor as u16, chunk.y),
match &app.mode {
Mode::Normal => {}
Mode::Command | Mode::Search(_) => f.set_cursor(chunk.x + app.input.cursor as u16, chunk.y),
_ => unreachable!(),
}
}
36 changes: 19 additions & 17 deletions src/ui/popup.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::ui::{centered_rect, surrounding_block};
use crate::viewer::{App, PopupMode};
use crate::viewer::{App, Mode, PopupMode};

use tui::{
backend::Backend,
Expand All @@ -10,22 +10,20 @@ use tui::{
};
use tui_tree_widget::Tree as TUITree;

pub(super) fn draw_popup<B: Backend>(
f: &mut Frame<B>,
size: Rect,
pmode: &PopupMode,
app: &mut App,
) {
let popup = centered_rect(70, 70, size);
pub(super) fn draw_popup<B: Backend>(f: &mut Frame<B>, size: Rect, app: &mut App) {
let popup = centered_rect(90, 90, size);

match pmode {
PopupMode::Tree => draw_tree(f, popup, app),
PopupMode::Help => draw_help(f, popup, app),
match &app.mode {
Mode::Popup(pmode) => match pmode {
PopupMode::Tree => draw_tree(f, popup, app),
PopupMode::Help => draw_help(f, popup, app),
},
_ => unreachable!(),
};
}

fn draw_tree<B: Backend>(f: &mut Frame<B>, chunk: Rect, app: &mut App) {
let block = surrounding_block("Filter by Subgraph".to_string(), false);
let block = surrounding_block("Select a subgraph".to_string(), false);

let view = app.tabs.selected();
let subtree = &mut view.subtree;
Expand Down Expand Up @@ -53,13 +51,17 @@ fn draw_help<B: Backend>(f: &mut Frame<B>, chunk: Rect, app: &mut App) {
Row::new(row).height(1).bottom_margin(1)
});

let table =
Table::new(rows).header(header).block(Block::default().borders(Borders::ALL)).widths(&[
let table = Table::new(rows)
.header(header)
.block(Block::default().borders(Borders::ALL))
.highlight_style(Style::default().add_modifier(Modifier::REVERSED))
.highlight_symbol("> ")
.widths(&[
Constraint::Percentage(15),
Constraint::Percentage(10),
Constraint::Percentage(10),
Constraint::Percentage(65),
Constraint::Percentage(15),
Constraint::Percentage(60),
]);

f.render_widget(table, chunk);
f.render_stateful_widget(table, chunk, &mut app.help.state);
}
Loading

0 comments on commit 00c244d

Please sign in to comment.