162 lines
3.8 KiB
Go
162 lines
3.8 KiB
Go
package ui
|
|
|
|
import (
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gdamore/tcell/v2"
|
|
"github.com/rivo/tview"
|
|
)
|
|
|
|
type level int
|
|
|
|
const (
|
|
levelInfo level = iota
|
|
levelSuccess
|
|
levelWarn
|
|
levelError
|
|
)
|
|
|
|
func (l level) String() string {
|
|
return [...]string{"INFO", "OK", "WARN", "ERROR"}[l]
|
|
}
|
|
|
|
func (l level) color() tcell.Color {
|
|
return [...]tcell.Color{tcell.ColorWhite, tcell.ColorGreen, tcell.ColorYellow, tcell.ColorRed}[l]
|
|
}
|
|
|
|
// how long a toast stays on screen, and how long the end of that time fades.
|
|
func (l level) lifetime() time.Duration {
|
|
return [...]time.Duration{3 * time.Second, 3 * time.Second, 5 * time.Second, 8 * time.Second}[l]
|
|
}
|
|
|
|
const (
|
|
toastFade = time.Second
|
|
maxToasts = 4
|
|
)
|
|
|
|
type toast struct {
|
|
text string
|
|
level level
|
|
expires time.Time
|
|
}
|
|
|
|
// progress toasts ("Searching…") are replaced by the next message.
|
|
func (t toast) progress() bool { return strings.HasSuffix(t.text, "…") }
|
|
|
|
var (
|
|
tagRe = regexp.MustCompile(`\[([^\[\]]*)\[\]|\[[^\[\]]*\]`)
|
|
levelPrefix = []struct {
|
|
tag string
|
|
level level
|
|
}{{"[red]", levelError}, {"[green]", levelSuccess}, {"[yellow]", levelWarn}}
|
|
)
|
|
|
|
// plainText removes tview color tags and undoes tview.Escape.
|
|
func plainText(s string) string {
|
|
return tagRe.ReplaceAllStringFunc(s, func(m string) string {
|
|
if strings.HasSuffix(m, "[]") {
|
|
return m[:len(m)-2] + "]"
|
|
}
|
|
return ""
|
|
})
|
|
}
|
|
|
|
// SetMessage reports something to the user: as a short-lived popup and, unless
|
|
// it is only progress ("Adding…"), as an entry in the log. A leading [red],
|
|
// [yellow] or [green] tag selects the level; an empty text dismisses progress
|
|
// popups. UI goroutine only; use post from other goroutines.
|
|
func (a *App) SetMessage(text string) {
|
|
if text == "" {
|
|
a.dropProgress()
|
|
return
|
|
}
|
|
lvl := levelInfo
|
|
for _, p := range levelPrefix {
|
|
if strings.HasPrefix(text, p.tag) {
|
|
lvl, text = p.level, strings.TrimPrefix(text, p.tag)
|
|
break
|
|
}
|
|
}
|
|
a.Notify(lvl, plainText(text))
|
|
}
|
|
|
|
// Notify shows a popup and logs the message. UI goroutine only.
|
|
func (a *App) Notify(l level, text string) {
|
|
t := toast{text: text, level: l, expires: time.Now().Add(l.lifetime())}
|
|
a.dropProgress()
|
|
if !t.progress() {
|
|
a.logs.add(l, text)
|
|
}
|
|
a.toasts = append(a.toasts, t)
|
|
if len(a.toasts) > maxToasts {
|
|
a.toasts = a.toasts[len(a.toasts)-maxToasts:]
|
|
}
|
|
// Redraw when the fade starts and when the toast is gone.
|
|
for _, d := range []time.Duration{l.lifetime() - toastFade, l.lifetime() + 50*time.Millisecond} {
|
|
time.AfterFunc(d, func() { a.post(func() {}) })
|
|
}
|
|
}
|
|
|
|
func (a *App) dropProgress() {
|
|
kept := a.toasts[:0]
|
|
for _, t := range a.toasts {
|
|
if !t.progress() {
|
|
kept = append(kept, t)
|
|
}
|
|
}
|
|
a.toasts = kept
|
|
}
|
|
|
|
// toastRoot draws the popups above the whole interface.
|
|
type toastRoot struct {
|
|
*tview.Flex
|
|
app *App
|
|
}
|
|
|
|
// narrowWidth is the terminal width below which the six tabs and the full
|
|
// service names no longer fit on one line.
|
|
const narrowWidth = 92
|
|
|
|
func (r *toastRoot) Draw(screen tcell.Screen) {
|
|
w, _ := screen.Size()
|
|
r.app.setNarrow(w < narrowWidth)
|
|
r.Flex.Draw(screen)
|
|
a := r.app
|
|
now := time.Now()
|
|
kept := a.toasts[:0]
|
|
for _, t := range a.toasts {
|
|
if now.Before(t.expires) {
|
|
kept = append(kept, t)
|
|
}
|
|
}
|
|
a.toasts = kept
|
|
|
|
w, h := screen.Size()
|
|
y := h - 3 // above the status bar
|
|
for i := len(a.toasts) - 1; i >= 0 && y >= 1; i-- {
|
|
t := a.toasts[i]
|
|
text := " " + t.text + " "
|
|
width := min(tview.TaggedStringWidth(text), w-2)
|
|
x := w - width - 1
|
|
fg := t.level.color()
|
|
if t.expires.Sub(now) < toastFade {
|
|
fg = tcell.ColorGray // fade out
|
|
}
|
|
style := tcell.StyleDefault.Background(tcell.NewRGBColor(30, 30, 40)).Foreground(fg)
|
|
for cx := x; cx < x+width; cx++ {
|
|
screen.SetContent(cx, y, ' ', nil, style)
|
|
}
|
|
col := x
|
|
for _, ch := range text {
|
|
if col >= x+width {
|
|
break
|
|
}
|
|
screen.SetContent(col, y, ch, nil, style)
|
|
col += tview.TaggedStringWidth(string(ch))
|
|
}
|
|
y--
|
|
}
|
|
}
|