Skip to content

Commit

Permalink
dot-viewer in TUI (#7)
Browse files Browse the repository at this point in the history
* fix: handle goto from empty adjacent node list

* Create README.md

* feat: backtab to navigate blocks in reverse

* refactor: merge app field update functions

* feat: navigate blocks with right, left arrows

* Update README.md

* feat: tab autocomplete gt node name

* fix: command mode to search mode

* refactor: merged graph related app fields into struct lists

* fix: search goto based on current list index

* fix: show error message on search failure

* feat: show matching nodes as user types in search keyword

* feat: show filtered node lists on enter after search

* refactor: rename struct Lists to Ctxt

* feat: render tabs in layout

* feat: open new tab on search keyword enter

* feat: navigate tabs with tab and backtab

* refactor: abstract tab in utils

* feat: allow tab close

* feat: change search match layout

* fix: re-enter nav mode on esc

* feat: goto searched node on enter

* feat: delete tabs from layout

* feat: fuzz match node ids

* feat: highlight fuzz matching characters

* Update README.md

* Update README.md

* fix: update adjacent nodes when traversing node list

* feat: delete autocomplete feature

* feat: add tabs again to app

* feat: add filter to focus to allow filtering feature

* fix: faster filtered node list update

* refactor: unify mode and focus

* feat: create a subgraph on filter

* feat: show search progress

* feat: help message for filter

* Update README.md

* Update README.md

* feat: parse subgraphs in dot-graph

* feat: parse and print edge ports in dot-graph

* feat: define viewer error

* refactor: render viewer run result in layout

* refactor: relocate function

* fix: prevent terminal crash by running viewer in thread

* feat: simple export with e keybinding

* fix: show edge attributes in dot-graph

* feat: export neigbors of current node with digits keybindings

* Update README.md

* feat: keep current node centered after goto

* feat: keep current.dot to keep tracking neighbors

* Update README.md

* rustfmt

* fix: subgraph filtering logic in dot-graph

* fmt: dot-graph

* fix: change submodule url

* fix: change submodule branch

* fix: faster filter application

* feat: launch xdot with x keybinding

* Update README.md

* fix: update help message

* fix: layout change for smaller screen

* fmt

* refactor: reorganize functions

* Update README.md

* feat: pretty print node metadata

* refactor: refactor dot-graph

* update: submodule dot-graph

* refactor: reorganize structs

* refactor: reorganize errors with thiserror

* feat: simple regex matcher on raw dot

* refactor: categorize prefix and regex match into search

* fix: rename prefix to fuzzy

* refactor: merge filter and search list of struct viewer

* feat: show node metadata in input mode

* feat: utilize cache on filter and regex match

* fmt

* Update README.md

* feat: faster match with parallel rayon

* update: submodule dot-graph with rayon

* fmt

* feat: vim traversal keybindings

* Update README.md

* feat: autocomplete for filter mode

* feat: autocomplete for fuzzy and regex search mode

* refactor: merge matching logic

* refactor: change function pointer signature

* fix: handle empty autocomplete key

* Update README.md

* Update README.md

* fix: remove match cache

* refactor: abstract input into struct

* feat: allow moving cursor for input

* feat: shorter dot-viewer launch command

* fmt

* Update README.md

* update: submodule dot-graph

* Update README.md

* feat: allow autocomplete on empty string

* feat: show number of matching nodes

* Update README.md

* fix: redirect stdout and stderr for xdot process

* update: submodule dot-graph

* feat: add popup mode to modes

* feat: render interactive popup

* feat: show subgraph tree with s keybinding

* feat: filter by subgraph

* update: submodule dot-graph

* fmt

* Update README.md

* Update README.md

* feat: allow quit in popup

* fix: handle error when no subgraph is selected

* refactor: reorganize modes

* refactor: handle errors from dot-graph

* fmt

* fix: remove cargo lock

* fmt with updated formatter

* refactor: use then_else for bool

* refactor: change in to_dot parameter in dot-graph

* refactor: change in function name in dot-graph

* refactor: static to module function

* refactor: rename functions with id postfix

* refactor: rename mode related variables

* fix: initialize tab title with graph id

* fix: print dot-graph error message

* refactor: misc

* refactor: rename viewer to view

* doc: app

* doc: view

* doc: mode

* refactor: misc

* refactor: change module names

* add: simple state diagram

* fix: view title on subgraph

* refactor: better use of dot-graph api in tree util

* fmt

* feat: show subgraph statistics in popup

* update: submodule dot-graph

* fix: simpler cleanup on dot-viewer panic

* update: submodule dot-graph

* fix: subgraph ordering

* fix: remove print

* feat: print panic backtrace to terminal

* feat: conditional panic hook in build or release

* fix: handle error on app initialization

* feat: sort subgraphs by id

* update: submodule dot-graph

* refactor: no call to dot-graph api in ui

* refactor: shorten thiserror prints

* refactor: rename node to item

* refactor: better use of dot-graph api

* refactor: reorganize default for input

* refactor: change visibility from public to crate public

* refactor: type alaias for dot-viewer result

* refactor: reorganize crate structure

* fmt

* fix: ignore logs

* update: submodule dot-graph

* feat: simple logger

* refactor: separate subtree drawer into module

* feat: add help in popup mode

* feat: render keybinding table with ? keybinding

* feat: simplify help message in input form

* fmt

* Update README.md

* refactor: rename alias to dotviewerresult

* refactor: remove redundant result

* refactor: remove unnecessary string in result

* refactor: pub(crate) struct field visibility to pub

* refactor: define custom success state type

* refactor: change visibility of success state

* refactor: better handling of dot-viewer panic

* refactor: group imports

* refactor: implicit format args

* refactor: change return type of current_id

* refactor: viewer utils

* refactor: integrate mode switch into goto, filter, and subgraph

* refactor: break down lengthy map_or

* feat: graphviz linker for mac m1

* feat: render search matches in current nodes list

* feat: show match progress

* feat: visualize match progress in footer

* feat: temporarily disable filter mode

* feat: add command mode

* feat: clap parser for dot-viewer command filter

* feat: aucompltete filter prefix on tab

* feat: define commands as enum

* refactor: integrate navigate mode into view

* refactor: flatten mode

* feat: goto matched id while input

* feat: use matches for search only

* feat: integrate keybindings into command

* feat: autocomplete cmd

* feat: filter on search matches

* feat: show filter keyword in view title

* feat: update help popup contents

* refactor: better help message

* fmt

* feat: create a new view on neighbors command

* feat: optionally accept filename for export and xdot commands

* Update README.md

* fix: fallback and print all attrs on html parse failure

* feat: vim keybinding for goto first and last

* update: submodule dot-graph

* fix: export filename

* fmt

* feat: traverse help popup

* refactor: misc

* refactor: rename successstate to success

* refactor: reorganize function orders

* fix: ignore goto errors while updating search matches

* fix: go to normal mode after selecting a subgraph

* refactor: merge tab error into viewer error

* Update README.md

* refactor: remove unnecessary param

* fix: allow moving cursors with left, right

* update: submodule dot-graph

* refactor: remove unnecessary pub(crate)

* feat: enforce orderings in nexts, prevs list

* Update README.md

* refactor: help messages into const

* refactor: remove svg

* fix: different colors for success and error

* fix: switch mode after export and xdot

* refactor: impl fromiterator for trie

* refactor: reuse openoptions

* refactor: misc viewer utils

* refactor: misc ui utils

* refactor: parenthesis for better readability

* refactor: use self in impls

* update: submodule dot-graph branch from dev to main

* Update README.md

* update: package description

* fix: author

---------

Co-authored-by: Jaehyun Lee <121415381+jaehyun1ee@users.noreply.github.com>
  • Loading branch information
jaehyun1ee-furiosa and jaehyun1ee-furiosa authored Feb 22, 2023
1 parent 9a77fd1 commit 8de9671
Show file tree
Hide file tree
Showing 33 changed files with 2,298 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[target.aarch-apple-darwin]
rustflags = ["-C", "link-args=-L/opt/homebrew/lib"]
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Cargo.lock
/logs
/target
.DS_STORE
*.dot
*.log
4 changes: 4 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[submodule "dot-graph"]
path = dot-graph
url = https://github.com/furiosa-ai/dot-graph.git
branch = main
29 changes: 29 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
[package]
name = "dot-viewer"
version = "0.1.0"
authors = ["FuriosaAI, Inc."]
description = "A viewer/debugger for large DAGs in Vim-like TUI"
readme = "README.md"
keywords = ["graph", "dag", "dot", "visualize", "tui"]
license = "MIT"
repository = "https://github.com/furiosa-ai/dot-viewer"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
dot-graph = { path = "dot-graph" }
tui = "0.19.0"
tui-tree-widget = "0.11.0"
crossterm = "0.25"
clap = { version = "4.1.1", features = ["derive"] }
trie-rs = "0.1.1"
fuzzy-matcher = "0.3.7"
html_parser = "0.6.3"
thiserror = "1.0.38"
regex = "1.7.1"
rayon = "1.6.1"
better-panic = "0.3.0"
log = "0.4.17"
simplelog = "0.12.0"
chrono = "0.4.23"
158 changes: 158 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# dot-viewer

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

# 1. Getting Started

## a. Prerequisites

### i. Graphviz

`dot-viewer` parses a dot format file using C bindings to [Graphviz (v7.0.6)](https://gitlab.com/graphviz/graphviz/-/tree/7.0.6/lib).

The system environment should be able to find and include the following header files.

```C
#include <gvc.h>
#include <cgraph.h>
```

#### Option 1. Installing Graphviz from Package Manager

Coming from Linux,
```console
$ sudo apt install graphviz-dev
```

And coming from vanilla Ubuntu, you may want to install these too.
```console
$ sudo apt install build-essentials cmake
$ sudo apt install clang
```

Coming from Mac,
```console
$ brew install graphviz
```

And coming from Apple Silicon Mac, and [add an environment variable](https://apple.stackexchange.com/questions/414622/installing-a-c-c-library-with-homebrew-on-m1-macs),
```shell
export CPATH=/opt/homebrew/include
```

#### Option 2. Building Graphviz from Source

Or, try building from the source code following the [guide](https://graphviz.org/download/source/).

### ii. xdot.py

`dot-viewer` renders a subgraph with `xdot.py`, an interactive dot visualizer.

It is required that [xdot is executable in command-line](https://github.com/jrfonseca/xdot.py) beforehand such that the following works.
```console
$ xdot *.dot
```

## b. Installation

### i. Initialize

First initialize and update the submodule `dot-graph`.

```console
$ git submodule init
$ git submodule update
```

### ii. Run

Then run crate.

```console
$ cargo run --release [path-to-dot-file]
```

This will open a TUI screen on the terminal.

# 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
- applying filter on search matches
- neighboring `n` nodes of the currently selected node

## Keybindings

### General

Key | Command | Actions
--- | --- | ---
`q` | | quit `dot-viewer`
&nbsp; | `:help<CR>` | show help
`esc` | &nbsp; | go back to the main screen

### Mode Switches

Key | From | To
--- | --- | ---
`esc` | All | Normal
`/` | Normal | Fuzzy Search
`r` | Normal | Regex Search
`:` | Normal | Command

### Normal

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
`gg` | move to the topmost node in focused list
`G` | move to the bottom node in focused list
`tab`/`backtab` | move between tabs

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

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

### Command

Key | Command | Actions
--- | --- | ---
&nbsp; | `filter` | apply filter on current matches, opening a new tab(view)
&nbsp; | `neighbors [depth]` | get up to `depth` neighbors of the current node in a new tab(view)
&nbsp; | `export [(opt) filename]` | export the current tab(view) to dot
&nbsp; | `xdot [(opt) filename]` | launch `xdot` with the filename or `exports/current.dot` by default
&nbsp; | `subgraph` | open a popup showing subgraph tree
`tab` | &nbsp; | autocomplete command
`enter` | &nbsp; | 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
1 change: 1 addition & 0 deletions dot-graph
Submodule dot-graph added at f463a2
4 changes: 4 additions & 0 deletions rustfmt.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
edition = "2021"

# https://github.com/rust-lang/rustfmt/blob/master/Configurations.md#use_small_heuristics
use_small_heuristics = "Max"
29 changes: 29 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
mod terminal;
mod ui;
mod viewer;

use std::error::Error;
use std::fs;

use chrono::prelude::*;
use clap::Parser;
use simplelog::{Config, LevelFilter, WriteLogger};

use terminal::launch;

#[derive(Parser, Default, Debug)]
struct Cli {
path: String,
}

fn main() -> Result<(), Box<dyn Error>> {
let args = Cli::parse();

fs::create_dir_all("./logs")?;
let file = fs::File::create(format!("logs/log_{}.log", Local::now()))?;
WriteLogger::init(LevelFilter::Info, Config::default(), file)?;

launch(args.path)?;

Ok(())
}
81 changes: 81 additions & 0 deletions src/terminal.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
use crate::{ui, viewer::App};

use std::io::Stdout;
use std::{error::Error, io};

use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use log::error;
use tui::{
backend::{Backend, CrosstermBackend},
Terminal,
};

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

// create and run app
let app = App::new(&path).map_err(|_| {
let _ = cleanup();

Box::<dyn Error>::from("user should provide path to a valid dot file")
})?;
let _ = run(&mut terminal, app);

// restore terminal
cleanup()?;

Ok(())
}

fn setup() -> Result<Terminal<CrosstermBackend<Stdout>>, Box<dyn Error>> {
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
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)
}

fn setup_panic_hook() {
let panic_handler = better_panic::Settings::auto().create_panic_handler();
std::panic::set_hook(Box::new(move |info| {
let _ = cleanup();

error!("dot-viewer {}", info);

panic_handler(info);
}));
}

fn run<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
loop {
terminal.draw(|f| ui::draw_app(f, &mut app))?;

if let Event::Key(key) = event::read()? {
app.key(key);
}

if app.quit {
break;
}
}

Ok(())
}

fn cleanup() -> Result<(), Box<dyn Error>> {
let mut stdout = io::stdout();
execute!(stdout, LeaveAlternateScreen, DisableMouseCapture)?;
disable_raw_mode()?;

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

use tui::{
backend::Backend,
layout::{Alignment, Constraint, Direction, Layout, Rect},
widgets::{Block, BorderType, Borders},
Frame,
};

pub(crate) fn draw_app<B: Backend>(f: &mut Frame<B>, app: &mut App) {
let size = f.size();

let block = Block::default()
.borders(Borders::ALL)
.title("Dot-Viewer (v0.1.0)")
.title_alignment(Alignment::Center)
.border_type(BorderType::Rounded);

f.render_widget(block, size);

match &app.mode {
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, 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], app);
draw_input(f, chunks[1], app);
}
Loading

0 comments on commit 8de9671

Please sign in to comment.