2020-05-11 20:13:16 +00:00
|
|
|
package ui
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
2020-05-14 19:06:13 +00:00
|
|
|
"fmt"
|
|
|
|
"os"
|
|
|
|
"strings"
|
2020-05-11 20:13:16 +00:00
|
|
|
|
2020-05-13 16:53:58 +00:00
|
|
|
"github.com/charmbracelet/boba"
|
2020-05-14 02:08:17 +00:00
|
|
|
"github.com/charmbracelet/boba/pager"
|
2020-05-13 16:53:58 +00:00
|
|
|
"github.com/charmbracelet/boba/spinner"
|
2020-05-11 20:13:16 +00:00
|
|
|
"github.com/charmbracelet/charm"
|
|
|
|
"github.com/charmbracelet/charm/ui/common"
|
|
|
|
"github.com/charmbracelet/charm/ui/keygen"
|
2020-05-13 23:02:39 +00:00
|
|
|
"github.com/muesli/reflow/indent"
|
2020-05-14 19:06:13 +00:00
|
|
|
te "github.com/muesli/termenv"
|
|
|
|
"golang.org/x/crypto/ssh/terminal"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
statusBarHeight = 1
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
statusBarBg = common.NewColorPair("#242424", "#F4F0F4")
|
|
|
|
statusBarFg = common.NewColorPair("#5A5A5A", "")
|
2020-05-11 20:13:16 +00:00
|
|
|
)
|
|
|
|
|
2020-05-13 16:53:58 +00:00
|
|
|
// NewProgram returns a new Boba program
|
|
|
|
func NewProgram() *boba.Program {
|
|
|
|
return boba.NewProgram(initialize, update, view)
|
2020-05-11 20:13:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// MESSAGES
|
|
|
|
|
|
|
|
type fatalErrMsg error
|
|
|
|
type errMsg error
|
|
|
|
type newCharmClientMsg *charm.Client
|
|
|
|
type sshAuthErrMsg struct{}
|
|
|
|
|
2020-05-14 19:06:13 +00:00
|
|
|
type terminalSizeMsg struct {
|
|
|
|
width int
|
|
|
|
height int
|
|
|
|
err error
|
|
|
|
}
|
|
|
|
|
|
|
|
func (t terminalSizeMsg) Size() (int, int) { return t.width, t.height }
|
|
|
|
func (t terminalSizeMsg) Error() error { return t.err }
|
|
|
|
|
2020-05-11 20:13:16 +00:00
|
|
|
// MODEL
|
|
|
|
|
|
|
|
type state int
|
|
|
|
|
|
|
|
const (
|
|
|
|
stateInitCharmClient state = iota
|
|
|
|
stateKeygenRunning
|
|
|
|
stateKeygenFinished
|
2020-05-13 16:53:58 +00:00
|
|
|
stateShowStash
|
2020-05-14 02:08:17 +00:00
|
|
|
stateShowDocument
|
2020-05-11 20:13:16 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type model struct {
|
2020-05-14 19:06:13 +00:00
|
|
|
cc *charm.Client
|
|
|
|
user *charm.User
|
|
|
|
spinner spinner.Model
|
|
|
|
keygen keygen.Model
|
|
|
|
state state
|
|
|
|
err error
|
|
|
|
stash stashModel
|
|
|
|
pager pager.Model
|
|
|
|
terminalWidth int
|
|
|
|
terminalHeight int
|
|
|
|
docNote string
|
2020-05-14 02:08:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (m *model) unloadDocument() {
|
|
|
|
m.pager = pager.Model{}
|
|
|
|
m.state = stateShowStash
|
|
|
|
m.stash.state = stashStateLoaded
|
2020-05-11 20:13:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// INIT
|
|
|
|
|
2020-05-13 16:53:58 +00:00
|
|
|
func initialize() (boba.Model, boba.Cmd) {
|
2020-05-11 20:13:16 +00:00
|
|
|
s := spinner.NewModel()
|
|
|
|
s.Type = spinner.Dot
|
|
|
|
s.ForegroundColor = common.SpinnerColor
|
|
|
|
|
2020-05-14 19:06:13 +00:00
|
|
|
w, h, err := terminal.GetSize(int(os.Stdout.Fd()))
|
|
|
|
|
2020-05-11 20:13:16 +00:00
|
|
|
return model{
|
2020-05-14 19:06:13 +00:00
|
|
|
spinner: s,
|
|
|
|
state: stateInitCharmClient,
|
|
|
|
err: err,
|
|
|
|
terminalWidth: w,
|
|
|
|
terminalHeight: h,
|
2020-05-13 16:53:58 +00:00
|
|
|
}, boba.Batch(
|
|
|
|
newCharmClient,
|
|
|
|
spinner.Tick(s),
|
2020-05-14 19:06:13 +00:00
|
|
|
boba.GetTerminalSize(func(w, h int, err error) boba.TerminalSizeMsg {
|
|
|
|
return terminalSizeMsg{width: w, height: h, err: err}
|
|
|
|
}),
|
2020-05-13 16:53:58 +00:00
|
|
|
)
|
2020-05-11 20:13:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// UPDATE
|
|
|
|
|
2020-05-13 16:53:58 +00:00
|
|
|
func update(msg boba.Msg, mdl boba.Model) (boba.Model, boba.Cmd) {
|
2020-05-11 20:13:16 +00:00
|
|
|
m, ok := mdl.(model)
|
|
|
|
if !ok {
|
2020-05-13 16:53:58 +00:00
|
|
|
return model{err: errors.New("could not perform assertion on model in update")}, boba.Quit
|
2020-05-11 20:13:16 +00:00
|
|
|
}
|
|
|
|
|
2020-05-13 20:00:27 +00:00
|
|
|
var cmd boba.Cmd
|
|
|
|
|
2020-05-11 20:13:16 +00:00
|
|
|
switch msg := msg.(type) {
|
|
|
|
|
2020-05-13 16:53:58 +00:00
|
|
|
case boba.KeyMsg:
|
2020-05-11 20:13:16 +00:00
|
|
|
switch msg.String() {
|
2020-05-14 02:08:17 +00:00
|
|
|
|
2020-05-13 23:15:39 +00:00
|
|
|
case "q":
|
|
|
|
fallthrough
|
|
|
|
case "esc":
|
2020-05-14 02:08:17 +00:00
|
|
|
if m.state == stateShowDocument {
|
|
|
|
m.unloadDocument()
|
|
|
|
return m, nil
|
|
|
|
}
|
|
|
|
return m, boba.Quit
|
|
|
|
|
2020-05-11 20:13:16 +00:00
|
|
|
case "ctrl+c":
|
2020-05-13 16:53:58 +00:00
|
|
|
return m, boba.Quit
|
2020-05-11 20:13:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
case fatalErrMsg:
|
|
|
|
m.err = msg
|
2020-05-13 16:53:58 +00:00
|
|
|
return m, boba.Quit
|
2020-05-11 20:13:16 +00:00
|
|
|
|
|
|
|
case errMsg:
|
|
|
|
m.err = msg
|
|
|
|
return m, nil
|
|
|
|
|
2020-05-14 19:06:13 +00:00
|
|
|
case terminalSizeMsg:
|
|
|
|
if msg.Error() != nil {
|
|
|
|
m.err = msg.Error()
|
|
|
|
return m, nil
|
|
|
|
}
|
|
|
|
w, h := msg.Size()
|
|
|
|
m.terminalWidth = w
|
|
|
|
m.terminalHeight = h
|
|
|
|
return m, nil
|
|
|
|
|
2020-05-11 20:13:16 +00:00
|
|
|
case sshAuthErrMsg:
|
|
|
|
// If we haven't run the keygen yet, do that
|
|
|
|
if m.state != stateKeygenFinished {
|
|
|
|
m.state = stateKeygenRunning
|
|
|
|
m.keygen = keygen.NewModel()
|
|
|
|
return m, keygen.GenerateKeys
|
|
|
|
}
|
|
|
|
|
2020-05-13 20:00:27 +00:00
|
|
|
// The keygen didn't work and we can't auth
|
2020-05-11 20:13:16 +00:00
|
|
|
m.err = errors.New("SSH authentication failed")
|
2020-05-13 16:53:58 +00:00
|
|
|
return m, boba.Quit
|
2020-05-11 20:13:16 +00:00
|
|
|
|
|
|
|
case spinner.TickMsg:
|
2020-05-13 20:00:27 +00:00
|
|
|
switch m.state {
|
|
|
|
case stateInitCharmClient:
|
2020-05-13 16:53:58 +00:00
|
|
|
m.spinner, cmd = spinner.Update(msg, m.spinner)
|
|
|
|
}
|
2020-05-13 20:00:27 +00:00
|
|
|
return m, cmd
|
|
|
|
|
|
|
|
case stashSpinnerTickMsg:
|
2020-05-14 19:06:13 +00:00
|
|
|
if m.state == stateShowStash {
|
|
|
|
m.stash, cmd = stashUpdate(msg, m.stash)
|
|
|
|
}
|
2020-05-13 20:00:27 +00:00
|
|
|
return m, cmd
|
2020-05-11 20:13:16 +00:00
|
|
|
|
|
|
|
case keygen.DoneMsg:
|
|
|
|
m.state = stateKeygenFinished
|
|
|
|
return m, newCharmClient
|
|
|
|
|
|
|
|
case newCharmClientMsg:
|
|
|
|
m.cc = msg
|
2020-05-13 16:53:58 +00:00
|
|
|
m.state = stateShowStash
|
2020-05-13 20:00:27 +00:00
|
|
|
m.stash, cmd = stashInit(m.cc)
|
|
|
|
return m, cmd
|
2020-05-14 02:08:17 +00:00
|
|
|
|
|
|
|
case gotStashedItemMsg:
|
2020-05-14 19:06:13 +00:00
|
|
|
// TODO: there's a (unlikely) potential race condition that could
|
|
|
|
// happen here where we start the pager before we have received the
|
|
|
|
// terminal size. We could solve this in a few ways, including by
|
|
|
|
// getting the terminal size imperatively and synchronously
|
2020-05-14 02:08:17 +00:00
|
|
|
m.state = stateShowDocument
|
2020-05-14 19:06:13 +00:00
|
|
|
m.pager = pager.NewModel(
|
|
|
|
m.terminalWidth,
|
|
|
|
m.terminalHeight-statusBarHeight,
|
|
|
|
)
|
|
|
|
m.docNote = msg.Note
|
2020-05-14 02:08:17 +00:00
|
|
|
m.pager.Content(msg.Body)
|
2020-05-14 19:06:13 +00:00
|
|
|
return m, nil
|
2020-05-13 20:00:27 +00:00
|
|
|
}
|
2020-05-11 20:13:16 +00:00
|
|
|
|
2020-05-13 20:00:27 +00:00
|
|
|
switch m.state {
|
|
|
|
|
|
|
|
case stateKeygenRunning:
|
|
|
|
mdl, cmd := keygen.Update(msg, boba.Model(m.keygen))
|
|
|
|
keygenModel, ok := mdl.(keygen.Model)
|
|
|
|
if !ok {
|
|
|
|
m.err = errors.New("could not perform assertion on keygen model in main update")
|
|
|
|
return m, boba.Quit
|
2020-05-11 20:13:16 +00:00
|
|
|
}
|
2020-05-13 20:00:27 +00:00
|
|
|
m.keygen = keygenModel
|
|
|
|
return m, cmd
|
|
|
|
|
|
|
|
case stateShowStash:
|
|
|
|
m.stash, cmd = stashUpdate(msg, m.stash)
|
|
|
|
return m, cmd
|
2020-05-14 02:08:17 +00:00
|
|
|
|
|
|
|
case stateShowDocument:
|
|
|
|
newPagerModel, cmd := pager.Update(msg, boba.Model(m.pager))
|
|
|
|
newPagerModel_, ok := newPagerModel.(pager.Model)
|
|
|
|
if !ok {
|
|
|
|
m.err = errors.New("could not assert boba.Model to pager.Model in main update")
|
|
|
|
return m, nil
|
|
|
|
}
|
|
|
|
m.pager = newPagerModel_
|
|
|
|
return m, cmd
|
2020-05-11 20:13:16 +00:00
|
|
|
}
|
2020-05-13 16:53:58 +00:00
|
|
|
|
|
|
|
return m, nil
|
2020-05-11 20:13:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// VIEW
|
|
|
|
|
2020-05-13 16:53:58 +00:00
|
|
|
func view(mdl boba.Model) string {
|
2020-05-11 20:13:16 +00:00
|
|
|
m, ok := mdl.(model)
|
|
|
|
if !ok {
|
|
|
|
return "could not perform assertion on model in view"
|
|
|
|
}
|
|
|
|
|
|
|
|
if m.err != nil {
|
2020-05-13 20:00:27 +00:00
|
|
|
return m.err.Error() + "\n"
|
2020-05-11 20:13:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var s string
|
|
|
|
switch m.state {
|
|
|
|
case stateInitCharmClient:
|
|
|
|
s += spinner.View(m.spinner) + " Initializing..."
|
|
|
|
case stateKeygenRunning:
|
|
|
|
s += keygen.View(m.keygen)
|
|
|
|
case stateKeygenFinished:
|
|
|
|
s += spinner.View(m.spinner) + " Re-initializing..."
|
2020-05-13 16:53:58 +00:00
|
|
|
case stateShowStash:
|
2020-05-13 20:00:27 +00:00
|
|
|
s += stashView(m.stash)
|
2020-05-14 02:08:17 +00:00
|
|
|
case stateShowDocument:
|
2020-05-14 19:06:13 +00:00
|
|
|
//return fmt.Sprintf("\n%s\n%s", statusBarView(m), pager.View(m.pager))
|
|
|
|
return fmt.Sprintf("%s\n%s", pager.View(m.pager), statusBarView(m))
|
2020-05-11 20:13:16 +00:00
|
|
|
}
|
2020-05-14 02:08:17 +00:00
|
|
|
if m.state != stateShowStash && m.state != stateShowDocument {
|
2020-05-13 23:02:39 +00:00
|
|
|
s = "\n" + indent.String(s, 2)
|
|
|
|
}
|
2020-05-14 02:08:17 +00:00
|
|
|
return s
|
2020-05-11 20:13:16 +00:00
|
|
|
}
|
|
|
|
|
2020-05-14 19:06:13 +00:00
|
|
|
func statusBarView(m model) string {
|
|
|
|
// Logo
|
|
|
|
logoText := " Glow "
|
|
|
|
logo := te.String(logoText).
|
|
|
|
Bold().
|
|
|
|
Foreground(common.YellowGreen.Color()).
|
|
|
|
Background(common.Fuschia.Color()).
|
|
|
|
String()
|
|
|
|
|
|
|
|
// Note
|
|
|
|
noteText := m.docNote
|
|
|
|
if len(noteText) == 0 {
|
|
|
|
noteText = "(No title)"
|
|
|
|
}
|
|
|
|
noteText = " " + noteText
|
|
|
|
note := te.String(noteText).
|
|
|
|
Foreground(statusBarFg.Color()).
|
|
|
|
Background(statusBarBg.Color()).String()
|
|
|
|
|
|
|
|
// Scroll percent
|
|
|
|
percentText := fmt.Sprintf(" %3.f%% ", m.pager.ScrollPercent()*100)
|
|
|
|
percent := te.String(percentText).
|
|
|
|
Foreground(statusBarFg.Color()).
|
|
|
|
Background(statusBarBg.Color()).
|
|
|
|
String()
|
|
|
|
|
|
|
|
emptySpace := te.String(" ").Background(statusBarBg.Color()).String()
|
|
|
|
|
|
|
|
return logo + note + strings.Repeat(
|
|
|
|
emptySpace,
|
|
|
|
m.terminalWidth-len(logoText)-len(noteText)-len(percentText),
|
|
|
|
) + percent
|
|
|
|
}
|
|
|
|
|
2020-05-11 20:13:16 +00:00
|
|
|
// COMMANDS
|
|
|
|
|
2020-05-13 16:53:58 +00:00
|
|
|
func newCharmClient() boba.Msg {
|
2020-05-11 20:13:16 +00:00
|
|
|
cfg, err := charm.ConfigFromEnv()
|
|
|
|
if err != nil {
|
|
|
|
return fatalErrMsg(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
cc, err := charm.NewClient(cfg)
|
|
|
|
if err == charm.ErrMissingSSHAuth {
|
|
|
|
return sshAuthErrMsg{}
|
|
|
|
} else if err != nil {
|
|
|
|
return fatalErrMsg(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return newCharmClientMsg(cc)
|
|
|
|
}
|
2020-05-14 00:21:37 +00:00
|
|
|
|
|
|
|
// ETC
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|