jkawamoto/loci

View on GitHub
command/display.go

Summary

Maintainability
A
1 hr
Test Coverage
//
// command/display.go
//
// Copyright (c) 2016-2017 Junpei Kawamoto
//
// This software is released under the MIT License.
//
// http://opensource.org/licenses/mit-license.php
//

package command

import (
    "bufio"
    "context"
    "fmt"
    "io"
    "sort"
    "strings"
    "sync"

    "github.com/jroimartin/gocui"
    "github.com/ttacon/chalk"
)

const (
    headerHeight = 6
)

// Header represents a header space in a display.
type Header struct {
    body   []string
    Logger io.Writer
}

func newHeader(update DisplayUpdateFunc) (header *Header) {

    header = new(Header)
    reader, writer := io.Pipe()
    go func() {
        defer reader.Close()

        scanner := bufio.NewScanner(reader)
        for scanner.Scan() {

            header.body = append(header.body, scanner.Text())
            update(func(view *gocui.View) {
                _, h := view.Size()
                for i, line := range header.body {
                    if len(header.body)-i > h {
                        continue
                    }
                    fmt.Fprintln(view, line)
                }
            })

        }
    }()

    header.Logger = writer
    return

}

// Section represents a section in a display. Each section has a header text and
// several strings as the body.
type Section struct {
    Header string
    Body   []string
    update DisplayUpdateFunc
}

func newSection(header string, update DisplayUpdateFunc) *Section {

    return &Section{
        Header: header,
        update: update,
    }

}

// Writer returns io.WriteCloser to write messages into the section. Users have
// to close the returned writer.
func (s *Section) Writer() io.WriteCloser {

    reader, writer := io.Pipe()
    go func() {
        defer reader.Close()

        scanner := bufio.NewScanner(reader)
        for scanner.Scan() {
            s.Body = append(s.Body, scanner.Text())
            s.update(func(view *gocui.View) {
                _, h := view.Size()
                for i, line := range s.Body {
                    if len(s.Body)-i > h {
                        continue
                    }
                    fmt.Fprintln(view, line)
                }
            })
        }

    }()

    return writer

}

// String returns a string representing this section.
func (s *Section) String() string {

    return fmt.Sprintf(
        "%v\n%v",
        chalk.Cyan.Color(s.Header),
        strings.Join(s.Body, "\n"))

}

// Display represents a display which consists of several sections.
type Display struct {
    MaxSection int
    Title      string
    Header     *Header
    mutex      sync.Mutex
    sections   []*Section
    closed     bool
    done       chan error
    gui        *gocui.Gui
}

// DisplayUpdateHandler defines a handler function to update section body.
type DisplayUpdateHandler func(view *gocui.View)

// DisplayUpdateFunc is a function which a section calls to update the section
// body.
type DisplayUpdateFunc func(handler DisplayUpdateHandler)

// NewDisplay creates a new display.
func NewDisplay(ctx context.Context, title string, maxSection int) (display *Display, nctx context.Context, err error) {

    g, err := gocui.NewGui(gocui.OutputNormal)
    if err != nil {
        return
    }

    display = &Display{
        MaxSection: maxSection,
        Title:      title,
        gui:        g,
        done:       make(chan error),
        Header: newHeader(func(handler DisplayUpdateHandler) {
            g.Update(func(g *gocui.Gui) (err error) {
                v, err := g.View("header")
                if err != nil {
                    return
                }
                v.Clear()
                handler(v)
                return
            })
        }),
    }
    g.SetManager(display)

    err = g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
        return gocui.ErrQuit
    })
    if err != nil {
        g.Close()
        return
    }

    nctx, cancel := context.WithCancel(ctx)
    go func() {
        err := g.MainLoop()
        if err == gocui.ErrQuit {
            err = nil
        }
        cancel()
        display.done <- err
    }()

    return

}

// Layout is called every time the GUI is redrawn, it must contain the
// base views and its initializations.
func (d *Display) Layout(g *gocui.Gui) error {
    if d.closed {
        return fmt.Errorf("Display has been closed already")
    }

    d.mutex.Lock()
    defer d.mutex.Unlock()

    width, height := g.Size()
    if v, err := g.SetView("root", 0, 0, width-1, height-1); err != nil {
        if err != gocui.ErrUnknownView {
            return err
        }
        v.Title = d.Title
        v.Frame = true
    }

    if v, err := g.SetView("header", 0, 0, width-2, headerHeight+1); err != nil {
        if err != gocui.ErrUnknownView {
            return err
        }
        v.Frame = false
        v.Autoscroll = true
    }

    sectionHeight := (height - headerHeight - 2) / d.MaxSection
    for i, s := range d.sections {

        v, err := g.SetView(s.Header, 1, i*sectionHeight+headerHeight+1, width-2, (i+1)*sectionHeight+headerHeight)
        if err != nil {
            if err != gocui.ErrUnknownView {
                return err
            }
            v.Title = s.Header
            v.Autoscroll = true
        }

    }

    return nil

}

// Close closes this display.
func (d *Display) Close() (err error) {
    d.mutex.Lock()
    defer d.mutex.Unlock()

    if !d.closed {
        d.gui.Update(func(g *gocui.Gui) error {
            return gocui.ErrQuit
        })
        err = <-d.done
        d.gui.Close()
        d.closed = true
    }
    return

}

// AddSection adds a new section to this display.
func (d *Display) AddSection(header string) *Section {
    d.mutex.Lock()
    defer d.mutex.Unlock()

    s := newSection(header, func(handler DisplayUpdateHandler) {
        d.gui.Update(func(g *gocui.Gui) (err error) {
            v, err := g.View(header)
            if err != nil {
                return
            }
            v.Clear()
            handler(v)
            return
        })
    })

    d.sections = append(d.sections, s)
    sort.Slice(d.sections, func(i int, j int) bool {
        return d.sections[i].Header < d.sections[j].Header
    })

    d.gui.Update(d.Layout)
    return s
}

// DeleteSection deletes the given section from this display.
func (d *Display) DeleteSection(sec *Section) {
    d.mutex.Lock()
    defer d.mutex.Unlock()

    old := d.sections
    d.sections = make([]*Section, 0, len(d.sections)-1)
    for _, s := range old {
        if s != sec {
            d.sections = append(d.sections, s)
        }
    }

    d.gui.Update(func(g *gocui.Gui) error {
        return g.DeleteView(sec.Header)
    })

}