ditty/gui.go

510 lines
11 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"
2021-04-09 15:07:07 +00:00
"code.rocketnine.space/tslocum/cview"
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-09-08 22:06:17 +00:00
"github.com/gdamore/tcell/v2"
2020-01-09 23:51:37 +00:00
"github.com/mattn/go-runewidth"
2020-01-08 23:38:41 +00:00
)
2021-03-03 08:32:28 +00:00
const (
defaultLayout = "main,queue,playing"
defaultVolume = 100
)
2020-01-08 23:38:41 +00:00
var (
app *cview.Application
2020-04-24 22:39:02 +00:00
mainList *cview.List
queueList *cview.List
2020-01-08 23:38:41 +00:00
topstatusbuf *cview.TextView
bottomstatusbuf *cview.TextView
mainFiles []*LibraryEntry
2020-04-24 22:39:02 +00:00
mainCursor = -1
mainDirectory string
mainAutoFocus 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 = -1 // Focused entry
queuePlaying int // Playing entry
2020-01-28 17:26:44 +00:00
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 {
2020-04-24 22:39:02 +00:00
/*cview.Styles.TitleColor = tcell.ColorDefault
cview.Styles.BorderColor = tcell.ColorDefault
cview.Styles.PrimaryTextColor = tcell.ColorDefault
cview.Styles.PrimitiveBackgroundColor = tcell.ColorDefault*/
2020-01-08 23:38:41 +00:00
app = cview.NewApplication()
2020-01-29 16:54:05 +00:00
if !disableMouse {
2020-04-24 22:39:02 +00:00
app.EnableMouse(true)
2020-01-29 16:54:05 +00:00
}
2020-01-08 23:38:41 +00:00
app.SetAfterResizeFunc(handleResize)
app.SetMouseCapture(handleMouse)
2020-04-24 22:39:02 +00:00
app.SetInputCapture(inputConfig.Capture)
app.SetBeforeFocusFunc(handleBeforeFocus)
grid := cview.NewGrid()
grid.SetColumns(-1)
mainList = cview.NewList()
mainList.ShowSecondaryText(false)
mainList.SetScrollBarVisibility(cview.ScrollBarAlways)
mainList.SetHighlightFullLine(true)
mainList.SetSelectedTextColor(tcell.ColorBlack)
queueList = cview.NewList()
queueList.ShowSecondaryText(false)
queueList.SetScrollBarVisibility(cview.ScrollBarAlways)
queueList.SetHighlightFullLine(true)
queueList.SetSelectedTextColor(tcell.ColorBlack)
topstatusbuf = cview.NewTextView()
topstatusbuf.SetWrap(false)
topstatusbuf.SetWordWrap(false)
bottomstatusbuf = cview.NewTextView()
bottomstatusbuf.SetWrap(false)
bottomstatusbuf.SetWordWrap(false)
mainList.SetBorder(true)
mainList.SetTitleAlign(cview.AlignLeft)
2020-04-24 22:39:02 +00:00
mainList.SetSelectedFunc(handleMainSelection)
2020-01-08 23:38:41 +00:00
queueList.SetBorder(true)
queueList.SetTitleAlign(cview.AlignLeft)
queueList.SetTitle(" Queue ")
2020-05-01 23:35:50 +00:00
queueList.SetSelectedFunc(handleQueueSelection)
2020-05-20 22:40:35 +00:00
queueList.SetSelectedAlwaysCentered(true)
var i int
var rowHeights []int
ls := strings.Split(config.Layout, ",")
for _, l := range ls {
l = strings.ToLower(strings.TrimSpace(l))
switch l {
case "main":
grid.AddItem(mainList, i, 0, 1, 1, 0, 0, false)
rowHeights = append(rowHeights, -2)
case "queue":
grid.AddItem(queueList, i, 0, 1, 1, 0, 0, false)
rowHeights = append(rowHeights, -1)
case "playing":
grid.AddItem(topstatusbuf, i, 0, 1, 1, 0, 0, false)
grid.AddItem(bottomstatusbuf, i+1, 0, 1, 1, 0, 0, false)
rowHeights = append(rowHeights, 1, 1)
i++ // Use two rows
}
i++
}
grid.SetRows(rowHeights...)
2020-01-08 23:38:41 +00:00
2020-04-25 13:55:09 +00:00
app.SetRoot(grid, true)
2020-05-01 23:35:50 +00:00
focusUpdated(true)
2020-01-08 23:38:41 +00:00
return nil
}
2020-04-24 22:39:02 +00:00
func handleBeforeFocus(p cview.Primitive) bool {
2020-05-01 23:35:50 +00:00
focusMain := p == mainList
focusQueue := p == queueList
if focusMain || focusQueue {
queueFocused = focusQueue
focusUpdated(false)
return true
}
return false
2020-04-24 22:39:02 +00:00
}
2020-01-08 23:38:41 +00:00
func browseFolder(browse string) {
var err error
browse, err = filepath.Abs(browse)
if err != nil {
return
}
2020-04-24 22:39:02 +00:00
if browse == mainDirectory {
mainAutoFocus = ""
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-04-24 22:39:02 +00:00
mainList.SetTitle(" " + cview.Escape(runewidth.Truncate(mainDirectory, screenWidth-4, "...")) + "... ")
placeCursorAtTop := mainList.GetCurrentItemIndex() == 0
2020-04-24 22:39:02 +00:00
mainFiles = scanFolder(browse)
if mainCursor == -1 {
if !placeCursorAtTop && len(mainFiles) > 0 {
mainCursor = 1
} else {
mainCursor = 0
}
2020-01-22 22:54:55 +00:00
}
2020-01-08 23:38:41 +00:00
2020-04-24 22:39:02 +00:00
mainDirectory = browse
if mainAutoFocus != "" {
autoSelectAbs, err := filepath.Abs(mainAutoFocus)
if err == nil && autoSelectAbs != mainDirectory {
autoSelect := -1
var entryPath string
2020-04-24 22:39:02 +00:00
for i, entry := range mainFiles {
if !entry.IsDir && entry.Mode&os.ModeSymlink == 0 {
continue
}
entryPath, err = filepath.Abs(path.Join(mainDirectory, entry.Name))
if err == nil {
if entryPath == autoSelectAbs {
autoSelect = i
break
}
}
}
if autoSelect >= 0 {
2020-04-24 22:39:02 +00:00
mainCursor = autoSelect + 1
mainAutoFocus = ""
}
}
2020-04-24 22:39:02 +00:00
mainAutoFocus = ""
}
2020-01-09 23:51:37 +00:00
go app.QueueUpdateDraw(updateMain)
2020-01-08 23:38:41 +00:00
}
2020-01-22 01:10:20 +00:00
func browseParent() {
2020-04-24 22:39:02 +00:00
mainAutoFocus = mainDirectory
go browseFolder(path.Join(mainDirectory, ".."))
2020-01-22 01:10:20 +00:00
}
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-04-24 22:39:02 +00:00
statusMessage = mainDirectory
2020-01-09 17:48:00 +00:00
}
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-04-24 22:39:02 +00:00
mainList.SetTitle(" " + cview.Escape(runewidth.Truncate(mainBuffer.String(), screenWidth-4, "...")) + " ")
2020-01-09 17:48:00 +00:00
mainBuffer.Reset()
2020-01-08 23:38:41 +00:00
2020-05-01 23:35:50 +00:00
mainOffset := 0
2020-04-24 22:39:02 +00:00
if mainCursor == -1 {
mainCursor = mainList.GetCurrentItemIndex()
mainOffset, _ = mainList.GetOffset()
2020-04-24 22:39:02 +00:00
}
mainList.Clear()
2020-01-29 15:29:38 +00:00
2020-01-09 17:48:00 +00:00
var printed int
var line string
2020-04-24 22:39:02 +00:00
//var length string
2020-01-28 17:26:44 +00:00
2020-04-24 22:39:02 +00:00
if mainDirectory == "/" {
line = "./"
} else {
line = "../"
2020-01-08 23:38:41 +00:00
}
2020-04-24 22:39:02 +00:00
line = cview.Escape(line)
2020-01-09 01:50:52 +00:00
mainList.AddItem(cview.NewListItem(line))
2020-01-09 01:50:52 +00:00
2020-04-24 22:39:02 +00:00
printed++
2020-01-28 17:26:44 +00:00
2020-04-24 22:39:02 +00:00
for _, entry := range mainFiles {
//length = ""
if entry.IsDir || entry.Mode&os.ModeSymlink != 0 {
line = strings.TrimSpace(entry.Name) + "/"
2020-01-08 23:38:41 +00:00
} else {
line = entry.String()
2020-04-24 22:39:02 +00:00
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)
}
2020-01-08 23:38:41 +00:00
}
2020-01-29 15:29:38 +00:00
line = cview.Escape(line)
2020-01-28 17:26:44 +00:00
mainList.AddItem(cview.NewListItem(line))
2020-01-28 17:26:44 +00:00
2020-01-29 15:29:38 +00:00
printed++
}
2020-01-28 17:26:44 +00:00
2020-04-24 22:39:02 +00:00
if mainCursor >= mainList.GetItemCount() {
mainList.SetCurrentItem(mainList.GetItemCount() - 1)
} else if mainCursor >= 0 {
mainList.SetCurrentItem(mainCursor)
2020-01-08 23:38:41 +00:00
}
2020-05-20 22:40:35 +00:00
mainList.SetOffset(mainOffset, 0)
2020-04-24 22:39:02 +00:00
mainCursor = -1
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()
2020-04-24 22:39:02 +00:00
if queueCursor == -1 {
queueCursor = queueList.GetCurrentItemIndex()
2020-04-24 22:39:02 +00:00
}
queueList.Clear()
2020-01-29 15:29:38 +00:00
2020-01-28 17:26:44 +00:00
var printed int
var line string
2020-04-24 22:39:02 +00:00
//var length string
for _, entry := range queueFiles {
2020-01-28 22:53:38 +00:00
line = entry.String()
2020-04-24 22:39:02 +00:00
//lineWidth := runewidth.StringWidth(line)
2020-01-29 15:29:38 +00:00
line = cview.Escape(line)
2020-01-28 22:53:38 +00:00
queueList.AddItem(cview.NewListItem(line))
2020-01-28 17:26:44 +00:00
2020-04-24 22:39:02 +00:00
/*m := entry.Metadata.Length / time.Minute
length = fmt.Sprintf(" %d:%02d", m, (entry.Metadata.Length%(m*time.Minute))/time.Second)*/
2020-01-28 17:26:44 +00:00
printed++
2020-01-29 15:29:38 +00:00
}
2020-04-24 22:39:02 +00:00
if queueCursor >= queueList.GetItemCount() {
queueList.SetCurrentItem(queueList.GetItemCount() - 1)
} else if queueCursor >= 0 {
queueList.SetCurrentItem(queueCursor)
2020-01-28 17:26:44 +00:00
}
2020-04-24 22:39:02 +00:00
queueCursor = -1
2020-01-28 17:26:44 +00:00
}
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()
}
2020-04-24 22:39:02 +00:00
func handleMainSelection(i int, item *cview.ListItem) {
2020-04-24 22:39:02 +00:00
go listSelect(i)
}
2020-05-01 23:35:50 +00:00
func handleQueueSelection(i int, item *cview.ListItem) {
2020-05-01 23:35:50 +00:00
go queueSelect(i)
}