ditty/gui.go

370 lines
8.5 KiB
Go

package main
import (
"bytes"
"fmt"
"math"
"os"
"path"
"path/filepath"
"strings"
"sync"
"time"
"github.com/faiface/beep"
"github.com/faiface/beep/speaker"
"github.com/gdamore/tcell"
"github.com/mattn/go-runewidth"
"gitlab.com/tslocum/cview"
)
var (
app *cview.Application
mainbuf *cview.TextView
queuebuf *cview.TextView
topstatusbuf *cview.TextView
bottomstatusbuf *cview.TextView
mainBufferFiles []*libraryEntry
mainBufferCursor int
mainBufferDirectory string
mainBufferOrigin int
mainBufferAutoFocus string // Entry path to focus after loading display
seekStart, seekEnd int
volumeStart, volumeEnd int
screenWidth, screenHeight int
mainBufHeight int
mainBuffer bytes.Buffer
mainLock = new(sync.Mutex)
statusText string
statusBuffer bytes.Buffer
statusLock = new(sync.Mutex)
)
func initTUI() error {
app = cview.NewApplication()
app.EnableMouse()
app.SetInputCapture(inputConfig.Capture)
app.SetAfterResizeFunc(handleResize)
app.SetMouseCapture(handleMouse)
grid := cview.NewGrid().SetRows(-2, -1, 1, 1).SetColumns(-1)
mainbuf = cview.NewTextView().SetDynamicColors(true).SetWrap(true).SetWordWrap(false)
queuebuf = cview.NewTextView().SetDynamicColors(true).SetWrap(true).SetWordWrap(false)
topstatusbuf = cview.NewTextView().SetWrap(false).SetWordWrap(false)
bottomstatusbuf = cview.NewTextView().SetWrap(false).SetWordWrap(false)
mainbuf.SetBorder(true).SetTitleAlign(cview.AlignLeft)
queuebuf.SetBorder(true).SetTitleAlign(cview.AlignLeft).SetTitle(" Queue ")
grid.AddItem(mainbuf, 0, 0, 1, 1, 0, 0, false)
grid.AddItem(queuebuf, 1, 0, 1, 1, 0, 0, false)
grid.AddItem(topstatusbuf, 2, 0, 1, 1, 0, 0, false)
grid.AddItem(bottomstatusbuf, 3, 0, 1, 1, 0, 0, false)
mainbuf.SetDrawFunc(func(screen tcell.Screen, x, y, width, height int) (i int, i2 int, i3 int, i4 int) {
mainBufHeight = height
return mainbuf.GetInnerRect()
})
app.SetRoot(grid, true)
return nil
}
func browseFolder(browse string) {
var err error
browse, err = filepath.Abs(browse)
if err != nil {
return
}
if browse == mainBufferDirectory {
mainBufferAutoFocus = ""
return
}
placeCursorAtTop := mainBufferCursor == 0
mainBufferFiles = scanFolder(browse)
if !placeCursorAtTop && len(mainBufferFiles) > 0 {
mainBufferCursor = 1
} else {
mainBufferCursor = 0
}
mainBufferOrigin = 0
mainBufferDirectory = browse
if mainBufferAutoFocus != "" {
autoSelectAbs, err := filepath.Abs(mainBufferAutoFocus)
if err == nil && autoSelectAbs != mainBufferDirectory {
autoSelect := -1
var entryPath string
for i, entry := range mainBufferFiles {
if !entry.File.IsDir() {
continue
}
entryPath, err = filepath.Abs(path.Join(mainBufferDirectory, entry.File.Name()))
if err == nil {
if entryPath == autoSelectAbs {
autoSelect = i
break
}
}
}
if autoSelect >= 0 {
mainBufferCursor = autoSelect
mainBufferNewOrigin := (mainBufferCursor - (mainBufHeight - 4)) + ((mainBufHeight - 2) / 2)
if mainBufferNewOrigin <= 0 {
mainBufferNewOrigin = 0
} else if mainBufferNewOrigin > len(mainBufferFiles)-(mainBufHeight-3) {
mainBufferNewOrigin = len(mainBufferFiles) - (mainBufHeight - 3)
}
mainBufferOrigin = mainBufferNewOrigin
mainBufferAutoFocus = ""
go listNext()
return
}
}
mainBufferAutoFocus = ""
}
go app.QueueUpdateDraw(updateMain)
}
func browseParent() {
mainBufferAutoFocus = mainBufferDirectory
go browseFolder(path.Join(mainBufferDirectory, ".."))
}
func updateMain() {
mainLock.Lock()
defer mainLock.Unlock()
mainBuffer.Reset()
var statusMessage string
if statusText != "" {
statusMessage = statusText
} else {
statusMessage = mainBufferDirectory
}
truncated := false
widthRequirement := 4
for {
if runewidth.StringWidth(statusMessage) <= screenWidth-widthRequirement || !strings.ContainsRune(statusMessage, os.PathSeparator) {
break
}
statusMessage = statusMessage[strings.IndexRune(statusMessage, '/')+1:]
truncated = true
widthRequirement = 8
}
if truncated {
mainBuffer.WriteString(".../")
}
mainBuffer.WriteString(statusMessage)
mainbuf.SetTitle(" " + runewidth.Truncate(mainBuffer.String(), screenWidth-4, "...") + " ")
mainBuffer.Reset()
var printed int
var line string
if mainBufferOrigin == 0 {
if mainBufferCursor == 0 {
mainBuffer.WriteString("[::r]")
}
if mainBufferDirectory == "/" {
line = "./"
} else {
line = "../"
}
mainBuffer.WriteString(line)
for i := len(line); i < screenWidth-2; i++ {
mainBuffer.WriteRune(' ')
}
if mainBufferCursor == 0 {
mainBuffer.WriteString("[-]")
}
printed++
}
for i, entry := range mainBufferFiles {
if i < mainBufferOrigin-1 || i-mainBufferOrigin-1 > mainBufHeight-1 {
continue
}
if printed > 0 {
mainBuffer.WriteRune('\n')
}
if i == mainBufferCursor-1 {
mainBuffer.WriteString("[::r]")
}
if entry.File.IsDir() {
line = entry.File.Name() + "/"
} else {
line = entry.String()
}
mainBuffer.WriteString(line)
for i := runewidth.StringWidth(line); i < screenWidth-2; i++ {
mainBuffer.WriteRune(' ')
}
if i == mainBufferCursor-1 {
mainBuffer.WriteString("[-]")
}
printed++
if printed == mainBufHeight-2 {
break
}
}
mainbuf.SetText(mainBuffer.String())
}
func updateQueue() {
// TODO
}
func updateStatus() {
statusLock.Lock()
defer statusLock.Unlock()
var sampleRate beep.SampleRate
var p time.Duration
var l time.Duration
var v float64
var paused bool
var silent bool
if playingStreamer != nil && volume != nil && ctrl != nil {
audioLock.Lock()
speaker.Lock()
silent = volume.Silent
paused = ctrl.Paused
sampleRate = playingFormat.SampleRate
p = playingFormat.SampleRate.D(playingStreamer.Position()).Truncate(time.Second)
l = playingFormat.SampleRate.D(playingStreamer.Len()).Truncate(time.Second)
v = volume.Volume
speaker.Unlock()
audioLock.Unlock()
statusBuffer.Reset()
if paused {
statusBuffer.WriteString("Paused ")
}
statusBuffer.WriteString(fmt.Sprintf(" %dHz %s", sampleRate.N(time.Second), fileFormat(playingFileName)))
topStatusExtra := statusBuffer.String()
statusBuffer.Reset()
topStatusMaxLength := screenWidth - 2
printExtra := topStatusMaxLength >= (len(topStatusExtra)*2)+1
if printExtra {
topStatusMaxLength -= len(topStatusExtra)
}
statusBuffer.WriteRune(' ')
var trackInfo string
if playingFileInfo != "" {
trackInfo = runewidth.Truncate(playingFileInfo, topStatusMaxLength, "...")
} else {
trackInfo = runewidth.Truncate(playingFileName, topStatusMaxLength, "...")
}
statusBuffer.WriteString(trackInfo)
if printExtra {
padding := topStatusMaxLength - runewidth.StringWidth(trackInfo)
for i := 0; i < padding; i++ {
statusBuffer.WriteRune(' ')
}
statusBuffer.WriteString(topStatusExtra)
}
topstatusbuf.SetText(statusBuffer.String())
}
statusBuffer.Reset()
if silent {
statusBuffer.WriteString("Mut ")
for i := -7.5; i < 0.0; i += 0.5 {
statusBuffer.WriteRune(' ')
}
} else {
statusBuffer.WriteString("Vol ")
for i := -7.5; i < v-0.5; i += 0.5 {
statusBuffer.WriteRune(tcell.RuneHLine)
}
statusBuffer.WriteRune('▷')
for i := v; i < 0; i += 0.5 {
statusBuffer.WriteRune(' ')
}
}
bottomStatus := fmt.Sprintf("%s %s", formatDuration(l), statusBuffer.String())
statusBuffer.Reset()
var progressIndicator string
if paused {
progressIndicator = "||"
} else {
progressIndicator = "▷"
}
padding := screenWidth - runewidth.StringWidth(bottomStatus) - len(formatDuration(p)) - runewidth.StringWidth(progressIndicator) - 3
position := int(float64(padding) * (float64(p) / float64(l)))
if position > padding-1 {
position = padding - 1
}
if paused && position > 0 {
position--
}
for i := 0; i < padding; i++ {
if i == position {
statusBuffer.WriteString(progressIndicator)
} else {
statusBuffer.WriteRune(tcell.RuneHLine)
}
}
seekStart = len(formatDuration(p)) + 2
seekEnd = seekStart + padding - 1
volumeStart = seekEnd + len(formatDuration(l)) + 4
volumeEnd = screenWidth - 2
bottomstatusbuf.SetText(" " + formatDuration(p) + " " + statusBuffer.String() + " " + bottomStatus)
statusBuffer.Reset()
}
func formatDuration(d time.Duration) string {
minutes := int(math.Floor(float64(d) / float64(time.Minute)))
seconds := int((d % time.Minute) / time.Second)
return fmt.Sprintf("%02d:%02d", minutes, seconds)
}
func handleResize(width int, height int) {
screenWidth, screenHeight = width, height
updateMain()
updateQueue()
updateStatus()
}