ditty/gui.go

420 lines
9.7 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 ")
setTextViewParameters(mainbuf)
setTextViewParameters(queuebuf)
setTextViewParameters(topstatusbuf)
setTextViewParameters(bottomstatusbuf)
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
}
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
}
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 setTextViewParameters(tv *cview.TextView) {
tv.SetTitleColor(tcell.ColorDefault)
tv.SetBorderColor(tcell.ColorDefault)
tv.SetTextColor(tcell.ColorDefault)
tv.SetBackgroundColor(tcell.ColorDefault)
}
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
p time.Duration
l time.Duration
v float64
paused bool
silent bool
progressFormatted string
durationFormatted string
)
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()
progressFormatted = formatDuration(p)
durationFormatted = formatDuration(l)
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())
} else {
v = startingVolumeLevel
silent = startingVolumeSilent
progressFormatted = "--:--"
durationFormatted = "--:--"
statusBuffer.Reset()
trackInfo := fmt.Sprintf("ditty %s", version)
topStatusMaxLength := screenWidth - 2
padding := (topStatusMaxLength - runewidth.StringWidth(trackInfo)) + 1
for i := 0; i < padding; i++ {
statusBuffer.WriteRune(' ')
}
statusBuffer.WriteString(trackInfo)
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", durationFormatted, 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(" " + progressFormatted + " " + 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()
}