ditty/gui.go

513 lines
12 KiB
Go
Raw Normal View History

2020-01-08 23:38:41 +00:00
package main
import (
2020-01-09 17:48:00 +00:00
"bytes"
2020-01-08 23:38:41 +00:00
"fmt"
"math"
"os"
"path"
2020-01-08 23:38:41 +00:00
"path/filepath"
"strings"
2020-01-09 17:48:00 +00:00
"sync"
2020-01-08 23:38:41 +00:00
"time"
2020-01-09 23:51:37 +00:00
"github.com/faiface/beep"
2020-01-08 23:38:41 +00:00
"github.com/faiface/beep/speaker"
2020-01-09 23:51:37 +00:00
"github.com/gdamore/tcell"
"github.com/mattn/go-runewidth"
2020-01-22 23:37:05 +00:00
"gitlab.com/tslocum/cview"
2020-01-08 23:38:41 +00:00
)
var (
app *cview.Application
mainbuf *cview.TextView
queuebuf *cview.TextView
topstatusbuf *cview.TextView
bottomstatusbuf *cview.TextView
2020-01-22 22:54:55 +00:00
mainBufferFiles []*libraryEntry
2020-01-28 17:26:44 +00:00
mainBufferCursor = 1 // Position cursor on first entry
2020-01-08 23:38:41 +00:00
mainBufferDirectory string
2020-01-09 01:50:52 +00:00
mainBufferOrigin int
2020-01-28 17:26:44 +00:00
mainBufHeight int
mainBufferAutoFocus string // Entry path to focus after loading display
2020-01-08 23:38:41 +00:00
2020-01-28 17:26:44 +00:00
mainBuffer bytes.Buffer
mainLock = new(sync.Mutex)
queueFiles []*libraryEntry
queueCursor int
queueDirectory string
queueOrigin int
queueHeight int
queueBuffer bytes.Buffer
queueLock = new(sync.Mutex)
queueFocused bool
2020-01-08 23:38:41 +00:00
seekStart, seekEnd int
volumeStart, volumeEnd int
screenWidth, screenHeight int
2020-01-09 17:48:00 +00:00
statusText string
statusBuffer bytes.Buffer
statusLock = new(sync.Mutex)
2020-01-08 23:38:41 +00:00
)
func initTUI() error {
app = cview.NewApplication()
app.EnableMouse()
2020-01-22 01:10:20 +00:00
app.SetInputCapture(inputConfig.Capture)
2020-01-08 23:38:41 +00:00
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 ")
setTextViewParameters(mainbuf)
setTextViewParameters(queuebuf)
setTextViewParameters(topstatusbuf)
setTextViewParameters(bottomstatusbuf)
2020-01-08 23:38:41 +00:00
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()
})
2020-01-28 17:26:44 +00:00
queuebuf.SetDrawFunc(func(screen tcell.Screen, x, y, width, height int) (i int, i2 int, i3 int, i4 int) {
queueHeight = height
return queuebuf.GetInnerRect()
})
2020-01-08 23:38:41 +00:00
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
}
2020-01-23 16:55:39 +00:00
if !strings.HasPrefix(browse, restrictLibrary) {
statusText = "failed to browse folder: permission denied"
go func() {
time.Sleep(5 * time.Second)
statusText = ""
go app.QueueUpdateDraw(updateMain)
}()
go app.QueueUpdateDraw(updateMain)
return
}
2020-01-08 23:38:41 +00:00
2020-01-22 22:54:55 +00:00
placeCursorAtTop := mainBufferCursor == 0
2020-01-08 23:38:41 +00:00
mainBufferFiles = scanFolder(browse)
2020-01-22 22:54:55 +00:00
if !placeCursorAtTop && len(mainBufferFiles) > 0 {
mainBufferCursor = 1
} else {
mainBufferCursor = 0
}
2020-01-09 23:51:37 +00:00
mainBufferOrigin = 0
2020-01-08 23:38:41 +00:00
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 = ""
}
2020-01-09 23:51:37 +00:00
go app.QueueUpdateDraw(updateMain)
2020-01-08 23:38:41 +00:00
}
func setTextViewParameters(tv *cview.TextView) {
tv.SetTitleColor(tcell.ColorDefault)
tv.SetBorderColor(tcell.ColorDefault)
tv.SetTextColor(tcell.ColorDefault)
tv.SetBackgroundColor(tcell.ColorDefault)
}
2020-01-22 01:10:20 +00:00
func browseParent() {
mainBufferAutoFocus = mainBufferDirectory
go browseFolder(path.Join(mainBufferDirectory, ".."))
}
2020-01-08 23:38:41 +00:00
func updateMain() {
2020-01-09 17:48:00 +00:00
mainLock.Lock()
defer mainLock.Unlock()
mainBuffer.Reset()
var statusMessage string
2020-01-08 23:38:41 +00:00
if statusText != "" {
2020-01-09 17:48:00 +00:00
statusMessage = statusText
2020-01-08 23:38:41 +00:00
} else {
2020-01-09 17:48:00 +00:00
statusMessage = mainBufferDirectory
}
2020-01-08 23:38:41 +00:00
2020-01-09 17:48:00 +00:00
truncated := false
widthRequirement := 4
for {
if runewidth.StringWidth(statusMessage) <= screenWidth-widthRequirement || !strings.ContainsRune(statusMessage, os.PathSeparator) {
break
}
2020-01-08 23:38:41 +00:00
2020-01-09 17:48:00 +00:00
statusMessage = statusMessage[strings.IndexRune(statusMessage, '/')+1:]
2020-01-08 23:38:41 +00:00
2020-01-09 17:48:00 +00:00
truncated = true
widthRequirement = 8
}
if truncated {
mainBuffer.WriteString(".../")
2020-01-08 23:38:41 +00:00
}
2020-01-09 17:48:00 +00:00
mainBuffer.WriteString(statusMessage)
2020-01-08 23:38:41 +00:00
2020-01-09 17:48:00 +00:00
mainbuf.SetTitle(" " + runewidth.Truncate(mainBuffer.String(), screenWidth-4, "...") + " ")
mainBuffer.Reset()
2020-01-08 23:38:41 +00:00
2020-01-09 17:48:00 +00:00
var printed int
var line string
2020-01-09 01:50:52 +00:00
if mainBufferOrigin == 0 {
2020-01-28 17:26:44 +00:00
writeListItemPrefix(&mainBuffer, !queueFocused, mainBufferCursor, 0)
2020-01-09 01:50:52 +00:00
if mainBufferDirectory == "/" {
line = "./"
} else {
line = "../"
}
2020-01-09 17:48:00 +00:00
mainBuffer.WriteString(line)
2020-01-28 17:26:44 +00:00
if queueFocused {
writeListItemSuffix(&mainBuffer, !queueFocused, mainBufferCursor, 0)
}
2020-01-09 01:50:52 +00:00
for i := len(line); i < screenWidth-2; i++ {
2020-01-09 17:48:00 +00:00
mainBuffer.WriteRune(' ')
2020-01-09 01:50:52 +00:00
}
2020-01-28 17:26:44 +00:00
if !queueFocused {
writeListItemSuffix(&mainBuffer, !queueFocused, mainBufferCursor, 0)
2020-01-09 01:50:52 +00:00
}
2020-01-28 17:26:44 +00:00
2020-01-09 01:50:52 +00:00
printed++
2020-01-08 23:38:41 +00:00
}
for i, entry := range mainBufferFiles {
2020-01-09 01:50:52 +00:00
if i < mainBufferOrigin-1 || i-mainBufferOrigin-1 > mainBufHeight-1 {
continue
}
if printed > 0 {
2020-01-09 17:48:00 +00:00
mainBuffer.WriteRune('\n')
2020-01-09 01:50:52 +00:00
}
2020-01-28 17:26:44 +00:00
writeListItemPrefix(&mainBuffer, !queueFocused, mainBufferCursor-1, i)
2020-01-08 23:38:41 +00:00
if entry.File.IsDir() {
line = entry.File.Name() + "/"
} else {
line = entry.String()
}
2020-01-09 17:48:00 +00:00
mainBuffer.WriteString(line)
2020-01-28 17:26:44 +00:00
if queueFocused {
writeListItemSuffix(&mainBuffer, !queueFocused, mainBufferCursor-1, i)
}
2020-01-08 23:38:41 +00:00
for i := runewidth.StringWidth(line); i < screenWidth-2; i++ {
2020-01-09 17:48:00 +00:00
mainBuffer.WriteRune(' ')
2020-01-08 23:38:41 +00:00
}
2020-01-28 17:26:44 +00:00
if !queueFocused {
writeListItemSuffix(&mainBuffer, !queueFocused, mainBufferCursor-1, i)
2020-01-08 23:38:41 +00:00
}
printed++
2020-01-09 01:50:52 +00:00
if printed == mainBufHeight-2 {
2020-01-08 23:38:41 +00:00
break
}
}
2020-01-09 17:48:00 +00:00
mainbuf.SetText(mainBuffer.String())
2020-01-08 23:38:41 +00:00
}
func updateQueue() {
2020-01-28 17:26:44 +00:00
queueLock.Lock()
defer queueLock.Unlock()
queueBuffer.Reset()
var printed int
var line string
for i, entry := range queueFiles {
if i < queueOrigin || i-queueOrigin > queueHeight-1 {
continue
}
if printed > 0 {
queueBuffer.WriteRune('\n')
}
writeListItemPrefix(&queueBuffer, queueFocused, queueCursor, i)
2020-01-28 22:53:38 +00:00
line = entry.String()
2020-01-28 17:26:44 +00:00
queueBuffer.WriteString(line)
if !queueFocused {
writeListItemSuffix(&queueBuffer, queueFocused, queueCursor, i)
}
for i := runewidth.StringWidth(line); i < screenWidth-2; i++ {
queueBuffer.WriteRune(' ')
}
if queueFocused {
writeListItemSuffix(&queueBuffer, queueFocused, queueCursor, i)
}
printed++
if printed == queueHeight-2 {
break
}
}
queuebuf.SetText(queueBuffer.String())
}
func writeListItemPrefix(buffer *bytes.Buffer, focused bool, cursor int, i int) {
if focused {
if i == cursor {
buffer.WriteString("[::r]")
}
} else {
if i == cursor {
buffer.WriteString("[::bu]")
}
}
}
func writeListItemSuffix(buffer *bytes.Buffer, focused bool, cursor int, i int) {
if focused {
if i == cursor {
buffer.WriteString("[-:-:-]")
}
} else {
if i == cursor {
buffer.WriteString("[-:-:-]")
}
}
}
func updateLists() {
updateMain()
updateQueue()
2020-01-08 23:38:41 +00:00
}
func updateStatus() {
2020-01-09 17:48:00 +00:00
statusLock.Lock()
defer statusLock.Unlock()
var (
sampleRate beep.SampleRate
p time.Duration
l time.Duration
v float64
paused bool
silent bool
progressFormatted string
durationFormatted string
)
2020-01-09 17:48:00 +00:00
if playingStreamer != nil && volume != nil && ctrl != nil {
2020-01-09 23:51:37 +00:00
audioLock.Lock()
2020-01-09 17:48:00 +00:00
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
2020-01-08 23:38:41 +00:00
speaker.Unlock()
2020-01-09 23:51:37 +00:00
audioLock.Unlock()
2020-01-08 23:38:41 +00:00
progressFormatted = formatDuration(p)
durationFormatted = formatDuration(l)
2020-01-09 17:48:00 +00:00
statusBuffer.Reset()
2020-01-08 23:38:41 +00:00
2020-01-09 17:48:00 +00:00
if paused {
statusBuffer.WriteString("Paused ")
}
statusBuffer.WriteString(fmt.Sprintf(" %dHz %s", sampleRate.N(time.Second), fileFormat(playingFileName)))
2020-01-08 23:38:41 +00:00
2020-01-09 17:48:00 +00:00
topStatusExtra := statusBuffer.String()
statusBuffer.Reset()
topStatusMaxLength := screenWidth - 2
printExtra := topStatusMaxLength >= (len(topStatusExtra)*2)+1
if printExtra {
topStatusMaxLength -= len(topStatusExtra)
2020-01-08 23:38:41 +00:00
}
2020-01-09 17:48:00 +00:00
statusBuffer.WriteRune(' ')
var trackInfo string
if playingFileInfo != "" {
trackInfo = runewidth.Truncate(playingFileInfo, topStatusMaxLength, "...")
} else {
trackInfo = runewidth.Truncate(playingFileName, topStatusMaxLength, "...")
2020-01-08 23:38:41 +00:00
}
2020-01-09 17:48:00 +00:00
statusBuffer.WriteString(trackInfo)
if printExtra {
padding := topStatusMaxLength - runewidth.StringWidth(trackInfo)
for i := 0; i < padding; i++ {
statusBuffer.WriteRune(' ')
}
2020-01-08 23:38:41 +00:00
2020-01-09 17:48:00 +00:00
statusBuffer.WriteString(topStatusExtra)
}
topstatusbuf.SetText(statusBuffer.String())
} else {
v = startingVolumeLevel
silent = startingVolumeSilent
progressFormatted = "--:--"
durationFormatted = "--:--"
statusBuffer.Reset()
2020-01-28 22:53:38 +00:00
trackInfo := fmt.Sprintf("ditty v%s", version)
topStatusMaxLength := screenWidth - 2
padding := (topStatusMaxLength - runewidth.StringWidth(trackInfo)) + 1
for i := 0; i < padding; i++ {
statusBuffer.WriteRune(' ')
}
statusBuffer.WriteString(trackInfo)
2020-01-09 17:48:00 +00:00
topstatusbuf.SetText(statusBuffer.String())
2020-01-08 23:38:41 +00:00
}
2020-01-09 17:48:00 +00:00
statusBuffer.Reset()
if silent {
statusBuffer.WriteString("Mut ")
2020-01-08 23:38:41 +00:00
for i := -7.5; i < 0.0; i += 0.5 {
2020-01-22 01:10:20 +00:00
statusBuffer.WriteRune(' ')
2020-01-08 23:38:41 +00:00
}
} else {
2020-01-09 17:48:00 +00:00
statusBuffer.WriteString("Vol ")
2020-01-08 23:38:41 +00:00
for i := -7.5; i < v-0.5; i += 0.5 {
2020-01-09 17:48:00 +00:00
statusBuffer.WriteRune(tcell.RuneHLine)
2020-01-08 23:38:41 +00:00
}
statusBuffer.WriteRune('▷')
2020-01-08 23:38:41 +00:00
for i := v; i < 0; i += 0.5 {
statusBuffer.WriteRune(' ')
2020-01-08 23:38:41 +00:00
}
}
bottomStatus := fmt.Sprintf("%s %s", durationFormatted, statusBuffer.String())
2020-01-09 17:48:00 +00:00
statusBuffer.Reset()
2020-01-08 23:38:41 +00:00
2020-01-09 17:48:00 +00:00
var progressIndicator string
2020-01-08 23:38:41 +00:00
if paused {
2020-01-09 17:48:00 +00:00
progressIndicator = "||"
2020-01-08 23:38:41 +00:00
} else {
progressIndicator = "▷"
2020-01-08 23:38:41 +00:00
}
2020-01-09 17:48:00 +00:00
padding := screenWidth - runewidth.StringWidth(bottomStatus) - len(formatDuration(p)) - runewidth.StringWidth(progressIndicator) - 3
position := int(float64(padding) * (float64(p) / float64(l)))
2020-01-08 23:38:41 +00:00
if position > padding-1 {
position = padding - 1
}
if paused && position > 0 {
position--
}
for i := 0; i < padding; i++ {
if i == position {
2020-01-09 17:48:00 +00:00
statusBuffer.WriteString(progressIndicator)
2020-01-08 23:38:41 +00:00
} else {
2020-01-09 17:48:00 +00:00
statusBuffer.WriteRune(tcell.RuneHLine)
2020-01-08 23:38:41 +00:00
}
}
2020-01-09 17:48:00 +00:00
seekStart = len(formatDuration(p)) + 2
2020-01-08 23:38:41 +00:00
seekEnd = seekStart + padding - 1
volumeStart = seekEnd + len(formatDuration(l)) + 4
volumeEnd = screenWidth - 2
bottomstatusbuf.SetText(" " + progressFormatted + " " + statusBuffer.String() + " " + bottomStatus)
2020-01-09 17:48:00 +00:00
statusBuffer.Reset()
2020-01-08 23:38:41 +00:00
}
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)
}
2020-01-22 01:10:20 +00:00
func handleResize(width int, height int) {
screenWidth, screenHeight = width, height
2020-01-08 23:38:41 +00:00
updateMain()
updateQueue()
updateStatus()
}