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: from simple keybindings to commands like in vim #17

Merged
merged 38 commits into from
Feb 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
0878cbb
feat: render search matches in current nodes list
jaehyun1ee-furiosa Feb 15, 2023
adfb375
feat: show match progress
jaehyun1ee-furiosa Feb 15, 2023
1f18e78
feat: visualize match progress in footer
jaehyun1ee-furiosa Feb 16, 2023
e2abbc0
feat: temporarily disable filter mode
jaehyun1ee-furiosa Feb 16, 2023
42d1f21
feat: add command mode
jaehyun1ee-furiosa Feb 16, 2023
11a6e5f
feat: clap parser for dot-viewer command filter
jaehyun1ee-furiosa Feb 16, 2023
6feb76a
feat: aucompltete filter prefix on tab
jaehyun1ee-furiosa Feb 16, 2023
4cb4ef4
feat: define commands as enum
jaehyun1ee-furiosa Feb 16, 2023
2f2d2a9
refactor: integrate navigate mode into view
jaehyun1ee-furiosa Feb 16, 2023
22c8ffb
refactor: flatten mode
jaehyun1ee-furiosa Feb 16, 2023
45a3af5
feat: goto matched id while input
jaehyun1ee-furiosa Feb 16, 2023
b6e1183
feat: use matches for search only
jaehyun1ee-furiosa Feb 16, 2023
209f293
feat: integrate keybindings into command
jaehyun1ee-furiosa Feb 16, 2023
0772b4d
feat: autocomplete cmd
jaehyun1ee-furiosa Feb 17, 2023
1858ab7
feat: filter on search matches
jaehyun1ee-furiosa Feb 17, 2023
62abc0d
feat: show filter keyword in view title
jaehyun1ee-furiosa Feb 17, 2023
dd8f8ff
feat: update help popup contents
jaehyun1ee-furiosa Feb 17, 2023
610ad87
refactor: better help message
jaehyun1ee-furiosa Feb 17, 2023
f1dad60
fmt
jaehyun1ee-furiosa Feb 17, 2023
e95ca8d
feat: create a new view on neighbors command
jaehyun1ee-furiosa Feb 17, 2023
8891075
feat: optionally accept filename for export and xdot commands
jaehyun1ee-furiosa Feb 17, 2023
22512be
Update README.md
jaehyun1ee-furiosa Feb 17, 2023
27f16a1
fix: fallback and print all attrs on html parse failure
jaehyun1ee-furiosa Feb 17, 2023
a372127
feat: vim keybinding for goto first and last
jaehyun1ee-furiosa Feb 17, 2023
328c542
update: submodule dot-graph
jaehyun1ee-furiosa Feb 20, 2023
b4f9869
fix: export filename
jaehyun1ee-furiosa Feb 20, 2023
160a87d
fmt
jaehyun1ee-furiosa Feb 20, 2023
df08507
feat: traverse help popup
jaehyun1ee-furiosa Feb 20, 2023
0a6af95
refactor: misc
jaehyun1ee-furiosa Feb 20, 2023
5b61c04
refactor: rename successstate to success
jaehyun1ee-furiosa Feb 21, 2023
4f9c224
refactor: reorganize function orders
jaehyun1ee-furiosa Feb 21, 2023
468b71e
fix: ignore goto errors while updating search matches
jaehyun1ee-furiosa Feb 21, 2023
f4b4a5c
fix: go to normal mode after selecting a subgraph
jaehyun1ee-furiosa Feb 21, 2023
d7c8c48
refactor: merge tab error into viewer error
jaehyun1ee-furiosa Feb 21, 2023
fe1ef41
Update README.md
jaehyun1ee-furiosa Feb 21, 2023
1f42289
refactor: remove unnecessary param
jaehyun1ee-furiosa Feb 21, 2023
ff77711
fix: allow moving cursors with left, right
jaehyun1ee-furiosa Feb 21, 2023
e876fab
update: submodule dot-graph
jaehyun1ee-furiosa Feb 21, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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