ditty/gui.go

476 lines
11 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
mainList *cview.List
queueList *cview.List
topstatusbuf *cview.TextView
bottomstatusbuf *cview.TextView
mainFiles []*libraryEntry
mainCursor = -1
mainDirectory string
mainAutoFocus string // Entry path to focus after loading display
mainBuffer bytes.Buffer
mainLock = new(sync.Mutex)
queueFiles []*libraryEntry
queueCursor = -1
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 {
/*cview.Styles.TitleColor = tcell.ColorDefault
cview.Styles.BorderColor = tcell.ColorDefault
cview.Styles.PrimaryTextColor = tcell.ColorDefault
cview.Styles.PrimitiveBackgroundColor = tcell.ColorDefault*/
app = cview.NewApplication()
if !disableMouse {
app.EnableMouse(true)
}
app.SetAfterResizeFunc(handleResize)
app.SetMouseCapture(handleMouse)
app.SetInputCapture(inputConfig.Capture)
app.SetBeforeFocusFunc(handleBeforeFocus)
grid := cview.NewGrid().SetRows(-2, -1, 1, 1).SetColumns(-1)
mainList = cview.NewList().
ShowSecondaryText(false).
SetScrollBarVisibility(cview.ScrollBarAlways).
SetHighlightFullLine(true).
SetSelectedTextColor(tcell.ColorBlack)
queueList = cview.NewList().
ShowSecondaryText(false).
SetScrollBarVisibility(cview.ScrollBarAlways).
SetHighlightFullLine(true).
SetSelectedTextColor(tcell.ColorBlack)
topstatusbuf = cview.NewTextView().SetWrap(false).SetWordWrap(false)
bottomstatusbuf = cview.NewTextView().SetWrap(false).SetWordWrap(false)
mainList.SetBorder(true).SetTitleAlign(cview.AlignLeft)
mainList.SetSelectedFunc(handleMainSelection)
queueList.SetBorder(true).SetTitleAlign(cview.AlignLeft).SetTitle(" Queue ")
queueList.SetSelectedFunc(handleQueueSelection)
queueList.SetSelectedAlwaysCentered(true)
grid.AddItem(mainList, 0, 0, 1, 1, 0, 0, false)
grid.AddItem(queueList, 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)
app.SetRoot(grid, true)
focusUpdated(true)
return nil
}
func handleBeforeFocus(p cview.Primitive) bool {
focusMain := p == mainList
focusQueue := p == queueList
if focusMain || focusQueue {
queueFocused = focusQueue
focusUpdated(false)
return true
}
return false
}
func browseFolder(browse string) {
var err error
browse, err = filepath.Abs(browse)
if err != nil {
return
}
if browse == mainDirectory {
mainAutoFocus = ""
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
}
mainList.SetTitle(" " + cview.Escape(runewidth.Truncate(mainDirectory, screenWidth-4, "...")) + "... ")
placeCursorAtTop := mainList.GetCurrentItem() == 0
mainFiles = scanFolder(browse)
if mainCursor == -1 {
if !placeCursorAtTop && len(mainFiles) > 0 {
mainCursor = 1
} else {
mainCursor = 0
}
}
mainDirectory = browse
if mainAutoFocus != "" {
autoSelectAbs, err := filepath.Abs(mainAutoFocus)
if err == nil && autoSelectAbs != mainDirectory {
autoSelect := -1
var entryPath string
for i, entry := range mainFiles {
if !entry.File.IsDir() {
continue
}
entryPath, err = filepath.Abs(path.Join(mainDirectory, entry.File.Name()))
if err == nil {
if entryPath == autoSelectAbs {
autoSelect = i
break
}
}
}
if autoSelect >= 0 {
mainCursor = autoSelect + 1
mainAutoFocus = ""
}
}
mainAutoFocus = ""
}
go app.QueueUpdateDraw(updateMain)
}
func browseParent() {
mainAutoFocus = mainDirectory
go browseFolder(path.Join(mainDirectory, ".."))
}
func updateMain() {
mainLock.Lock()
defer mainLock.Unlock()
mainBuffer.Reset()
var statusMessage string
if statusText != "" {
statusMessage = statusText
} else {
statusMessage = mainDirectory
}
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)
mainList.SetTitle(" " + cview.Escape(runewidth.Truncate(mainBuffer.String(), screenWidth-4, "...")) + " ")
mainBuffer.Reset()
mainOffset := 0
if mainCursor == -1 {
mainCursor = mainList.GetCurrentItem()
mainOffset = mainList.GetOffset()
}
mainList.Clear()
var printed int
var line string
//var length string
if mainDirectory == "/" {
line = "./"
} else {
line = "../"
}
line = cview.Escape(line)
mainList.AddItem(line, "", 0, nil)
printed++
for _, entry := range mainFiles {
//length = ""
if entry.File.IsDir() {
line = strings.TrimSpace(entry.File.Name()) + "/"
} else {
line = entry.String()
if entry.Metadata.Length > 0 {
//m := entry.Metadata.Length / time.Minute
//length = fmt.Sprintf(" %d:%02d", m, (entry.Metadata.Length%(m*time.Minute))/time.Second)
}
}
line = cview.Escape(line)
mainList.AddItem(line, "", 0, nil)
printed++
}
if mainCursor >= mainList.GetItemCount() {
mainList.SetCurrentItem(mainList.GetItemCount() - 1)
} else if mainCursor >= 0 {
mainList.SetCurrentItem(mainCursor)
}
mainList.SetOffset(mainOffset)
mainCursor = -1
}
func updateQueue() {
queueLock.Lock()
defer queueLock.Unlock()
queueBuffer.Reset()
if queueCursor == -1 {
queueCursor = queueList.GetCurrentItem()
}
queueList.Clear()
var printed int
var line string
//var length string
for _, entry := range queueFiles {
line = entry.String()
//lineWidth := runewidth.StringWidth(line)
line = cview.Escape(line)
queueList.AddItem(line, "", 0, nil)
/*m := entry.Metadata.Length / time.Minute
length = fmt.Sprintf(" %d:%02d", m, (entry.Metadata.Length%(m*time.Minute))/time.Second)*/
printed++
}
if queueCursor >= queueList.GetItemCount() {
queueList.SetCurrentItem(queueList.GetItemCount() - 1)
} else if queueCursor >= 0 {
queueList.SetCurrentItem(queueCursor)
}
queueCursor = -1
}
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()
}
func handleMainSelection(i int, s string, s2 string, r rune) {
go listSelect(i)
}
func handleQueueSelection(i int, s string, s2 string, r rune) {
go queueSelect(i)
}