512 lines
12 KiB
Go
512 lines
12 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 = 1 // Position cursor on first entry
|
|
mainBufferDirectory string
|
|
mainBufferOrigin int
|
|
mainBufHeight int
|
|
mainBufferAutoFocus string // Entry path to focus after loading display
|
|
|
|
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
|
|
|
|
seekStart, seekEnd int
|
|
volumeStart, volumeEnd int
|
|
|
|
screenWidth, screenHeight int
|
|
|
|
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()
|
|
})
|
|
|
|
queuebuf.SetDrawFunc(func(screen tcell.Screen, x, y, width, height int) (i int, i2 int, i3 int, i4 int) {
|
|
queueHeight = height
|
|
return queuebuf.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 {
|
|
writeListItemPrefix(&mainBuffer, !queueFocused, mainBufferCursor, 0)
|
|
|
|
if mainBufferDirectory == "/" {
|
|
line = "./"
|
|
} else {
|
|
line = "../"
|
|
}
|
|
mainBuffer.WriteString(line)
|
|
|
|
if queueFocused {
|
|
writeListItemSuffix(&mainBuffer, !queueFocused, mainBufferCursor, 0)
|
|
}
|
|
for i := len(line); i < screenWidth-2; i++ {
|
|
mainBuffer.WriteRune(' ')
|
|
}
|
|
if !queueFocused {
|
|
writeListItemSuffix(&mainBuffer, !queueFocused, mainBufferCursor, 0)
|
|
}
|
|
|
|
printed++
|
|
}
|
|
for i, entry := range mainBufferFiles {
|
|
if i < mainBufferOrigin-1 || i-mainBufferOrigin-1 > mainBufHeight-1 {
|
|
continue
|
|
}
|
|
|
|
if printed > 0 {
|
|
mainBuffer.WriteRune('\n')
|
|
}
|
|
|
|
writeListItemPrefix(&mainBuffer, !queueFocused, mainBufferCursor-1, i)
|
|
|
|
if entry.File.IsDir() {
|
|
line = entry.File.Name() + "/"
|
|
} else {
|
|
line = entry.String()
|
|
}
|
|
mainBuffer.WriteString(line)
|
|
|
|
if queueFocused {
|
|
writeListItemSuffix(&mainBuffer, !queueFocused, mainBufferCursor-1, i)
|
|
}
|
|
|
|
for i := runewidth.StringWidth(line); i < screenWidth-2; i++ {
|
|
mainBuffer.WriteRune(' ')
|
|
}
|
|
|
|
if !queueFocused {
|
|
writeListItemSuffix(&mainBuffer, !queueFocused, mainBufferCursor-1, i)
|
|
}
|
|
|
|
printed++
|
|
if printed == mainBufHeight-2 {
|
|
break
|
|
}
|
|
}
|
|
|
|
mainbuf.SetText(mainBuffer.String())
|
|
}
|
|
|
|
func updateQueue() {
|
|
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)
|
|
|
|
line = entry.String()
|
|
|
|
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()
|
|
}
|
|
|
|
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 v%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()
|
|
}
|