You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

214 lines
4.1 KiB

package main
import (
"fmt"
"io/ioutil"
"os"
"path"
"github.com/MichaelMure/go-term-markdown"
"github.com/awesome-gocui/gocui"
"github.com/mattn/go-isatty"
"github.com/pkg/errors"
)
const padding = 4
func main() {
if len(os.Args) >= 2 && (os.Args[1] == "version" || os.Args[1] == "--version") {
printVersion()
return
}
var content []byte
switch len(os.Args) {
case 1:
if isatty.IsTerminal(os.Stdin.Fd()) {
exitError(fmt.Errorf("usage: %s <file.md>", os.Args[0]))
}
data, err := ioutil.ReadAll(os.Stdin)
if err != nil {
exitError(errors.Wrap(err, "error while reading STDIN"))
}
content = data
case 2:
data, err := ioutil.ReadFile(os.Args[1])
if err != nil {
exitError(errors.Wrap(err, "error while reading file"))
}
err = os.Chdir(path.Dir(os.Args[1]))
if err != nil {
exitError(err)
}
content = data
default:
exitError(fmt.Errorf("only one file is supported"))
}
g, err := gocui.NewGui(gocui.OutputNormal, false)
if err != nil {
exitError(errors.Wrap(err, "error starting the interactive UI"))
}
defer g.Close()
ui, err := newUi(g)
if err != nil {
exitError(err)
}
ui.setContent(content)
if err := g.MainLoop(); err != nil && err != gocui.ErrQuit {
exitError(err)
}
}
func exitError(err error) {
_, _ = fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
const renderView = "render"
type ui struct {
keybindings []keybinding
raw string
// current width of the view
width int
XOffset int
YOffset int
// number of lines in the rendered markdown
lines int
}
func newUi(g *gocui.Gui) (*ui, error) {
result := &ui{
width: -1,
}
g.SetManagerFunc(result.layout)
result.keybindings = []keybinding{
{"", gocui.KeyCtrlC, gocui.ModNone, result.quit},
{renderView, 'q', gocui.ModNone, result.quit},
{renderView, 'k', gocui.ModNone, result.up},
{renderView, gocui.KeyCtrlP, gocui.ModNone, result.up},
{renderView, gocui.KeyArrowUp, gocui.ModNone, result.up},
{renderView, 'j', gocui.ModNone, result.down},
{renderView, gocui.KeyCtrlN, gocui.ModNone, result.down},
{renderView, gocui.KeyArrowDown, gocui.ModNone, result.down},
{renderView, gocui.KeyPgup, gocui.ModNone, result.pageUp},
{renderView, gocui.KeyPgdn, gocui.ModNone, result.pageDown},
{renderView, gocui.KeySpace, gocui.ModNone, result.pageDown},
}
for _, kb := range result.keybindings {
err := kb.Register(g)
if err != nil {
return nil, err
}
}
return result, nil
}
func (ui *ui) setContent(content []byte) {
ui.raw = string(content)
ui.width = -1
}
func (ui *ui) layout(g *gocui.Gui) error {
maxX, maxY := g.Size()
v, err := g.SetView(renderView, ui.XOffset, -ui.YOffset, maxX, maxY, 0)
if err != nil {
if !gocui.IsUnknownView(err) {
return err
}
v.Frame = false
v.Wrap = false
}
if len(ui.raw) > 0 && ui.width != maxX {
ui.width = maxX
v.Clear()
_, _ = v.Write(ui.render(g))
}
_, err = g.SetCurrentView(renderView)
if err != nil {
return err
}
return nil
}
func (ui *ui) render(g *gocui.Gui) []byte {
maxX, _ := g.Size()
opts := []markdown.Options{
// needed when going through gocui
markdown.WithImageDithering(markdown.DitheringWithBlocks),
}
rendered := markdown.Render(ui.raw, maxX-1-padding, padding, opts...)
ui.lines = 0
for _, b := range rendered {
if b == '\n' {
ui.lines++
}
}
return rendered
}
func (ui *ui) quit(g *gocui.Gui, v *gocui.View) error {
return gocui.ErrQuit
}
func (ui *ui) up(g *gocui.Gui, v *gocui.View) error {
ui.YOffset -= 1
ui.YOffset = max(ui.YOffset, 0)
return nil
}
func (ui *ui) down(g *gocui.Gui, v *gocui.View) error {
_, maxY := g.Size()
ui.YOffset += 1
ui.YOffset = min(ui.YOffset, ui.lines-maxY+1)
ui.YOffset = max(ui.YOffset, 0)
return nil
}
func (ui *ui) pageUp(g *gocui.Gui, v *gocui.View) error {
_, maxY := g.Size()
ui.YOffset -= maxY / 2
ui.YOffset = max(ui.YOffset, 0)
return nil
}
func (ui *ui) pageDown(g *gocui.Gui, v *gocui.View) error {
_, maxY := g.Size()
ui.YOffset += maxY / 2
ui.YOffset = min(ui.YOffset, ui.lines-maxY+1)
ui.YOffset = max(ui.YOffset, 0)
return nil
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func max(a, b int) int {
if a > b {
return a
}
return b
}