Audio player
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

512 lines
11 KiB

2 years ago
package main
import (
"bytes"
2 years ago
"fmt"
"math"
"os"
"path"
2 years ago
"path/filepath"
"strings"
"sync"
2 years ago
"time"
"code.rocketnine.space/tslocum/cview"
"github.com/faiface/beep"
2 years ago
"github.com/faiface/beep/speaker"
"github.com/gdamore/tcell/v2"
"github.com/mattn/go-runewidth"
2 years ago
)
const (
defaultLayout = "main,queue,playing"
defaultVolume = 100
)
2 years ago
var (
app *cview.Application
mainList *cview.List
queueList *cview.List
2 years ago
topstatusbuf *cview.TextView
bottomstatusbuf *cview.TextView
mainFiles []*libraryEntry
mainCursor = -1
mainDirectory string
mainAutoFocus string // Entry path to focus after loading display
2 years ago
mainBuffer bytes.Buffer
mainLock = new(sync.Mutex)
queueFiles []*libraryEntry
queueCursor = -1 // Focused entry
queuePlaying int // Playing entry
queueBuffer bytes.Buffer
queueLock = new(sync.Mutex)
queueFocused bool
2 years ago
seekStart, seekEnd int
volumeStart, volumeEnd int
screenWidth, screenHeight int
statusText string
statusBuffer bytes.Buffer
statusLock = new(sync.Mutex)
2 years ago
)
func initTUI() error {
/*cview.Styles.TitleColor = tcell.ColorDefault
cview.Styles.BorderColor = tcell.ColorDefault
cview.Styles.PrimaryTextColor = tcell.ColorDefault
cview.Styles.PrimitiveBackgroundColor = tcell.ColorDefault*/
2 years ago
app = cview.NewApplication()
if !disableMouse {
app.EnableMouse(true)
}
2 years ago
app.SetAfterResizeFunc(handleResize)
app.SetMouseCapture(handleMouse)
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)
mainList.SetSelectedFunc(handleMainSelection)
2 years ago
queueList.SetBorder(true)
queueList.SetTitleAlign(cview.AlignLeft)
queueList.SetTitle(" Queue ")
queueList.SetSelectedFunc(handleQueueSelection)
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...)
2 years ago
app.SetRoot(grid, true)
focusUpdated(true)
2 years ago
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
}
2 years ago
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
}
2 years ago
mainList.SetTitle(" " + cview.Escape(runewidth.Truncate(mainDirectory, screenWidth-4, "...")) + "... ")
placeCursorAtTop := mainList.GetCurrentItemIndex() == 0
mainFiles = scanFolder(browse)
if mainCursor == -1 {
if !placeCursorAtTop && len(mainFiles) > 0 {
mainCursor = 1
} else {
mainCursor = 0
}
}
2 years ago
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.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 {
mainCursor = autoSelect + 1
mainAutoFocus = ""
}
}
mainAutoFocus = ""
}
go app.QueueUpdateDraw(updateMain)
2 years ago
}
func browseParent() {
mainAutoFocus = mainDirectory
go browseFolder(path.Join(mainDirectory, ".."))
}
2 years ago
func updateMain() {
mainLock.Lock()
defer mainLock.Unlock()
mainBuffer.Reset()
var statusMessage string
2 years ago
if statusText != "" {
statusMessage = statusText
2 years ago
} else {
statusMessage = mainDirectory
}
2 years ago
truncated := false
widthRequirement := 4
for {
if runewidth.StringWidth(statusMessage) <= screenWidth-widthRequirement || !strings.ContainsRune(statusMessage, os.PathSeparator) {
break
}
2 years ago
statusMessage = statusMessage[strings.IndexRune(statusMessage, '/')+1:]
2 years ago
truncated = true
widthRequirement = 8
}
if truncated {
mainBuffer.WriteString(".../")
2 years ago
}
mainBuffer.WriteString(statusMessage)
2 years ago
mainList.SetTitle(" " + cview.Escape(runewidth.Truncate(mainBuffer.String(), screenWidth-4, "...")) + " ")
mainBuffer.Reset()
2 years ago
mainOffset := 0
if mainCursor == -1 {
mainCursor = mainList.GetCurrentItemIndex()
mainOffset, _ = mainList.GetOffset()
}
mainList.Clear()
var printed int
var line string
//var length string
if mainDirectory == "/" {
line = "./"
} else {
line = "../"
2 years ago
}
line = cview.Escape(line)
mainList.AddItem(cview.NewListItem(line))
printed++
for _, entry := range mainFiles {
//length = ""
if entry.IsDir || entry.Mode&os.ModeSymlink != 0 {
line = strings.TrimSpace(entry.Name) + "/"
2 years ago
} 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)
}
2 years ago
}
line = cview.Escape(line)
mainList.AddItem(cview.NewListItem(line))
printed++
}
if mainCursor >= mainList.GetItemCount() {
mainList.SetCurrentItem(mainList.GetItemCount() - 1)
} else if mainCursor >= 0 {
mainList.SetCurrentItem(mainCursor)
2 years ago
}
mainList.SetOffset(mainOffset, 0)
mainCursor = -1
2 years ago
}
func updateQueue() {
queueLock.Lock()
defer queueLock.Unlock()
queueBuffer.Reset()
if queueCursor == -1 {
queueCursor = queueList.GetCurrentItemIndex()
}
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(cview.NewListItem(line))
/*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()
2 years ago
}
func updateStatus() {
app.QueueUpdateDraw(_updateStatus, topstatusbuf, bottomstatusbuf)
}
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 {
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
2 years ago
speaker.Unlock()
progressFormatted = formatDuration(p)
durationFormatted = formatDuration(l)
statusBuffer.Reset()
2 years ago
if paused {
statusBuffer.WriteString("Paused ")
}
statusBuffer.WriteString(fmt.Sprintf(" %dHz %s", sampleRate.N(time.Second), fileFormat(playingFileName)))
2 years ago
topStatusExtra := statusBuffer.String()
statusBuffer.Reset()
topStatusMaxLength := screenWidth - 2
printExtra := topStatusMaxLength >= (len(topStatusExtra)*2)+1
if printExtra {
topStatusMaxLength -= len(topStatusExtra)
2 years ago
}
statusBuffer.WriteRune(' ')
var trackInfo string
if playingFileInfo != "" {
trackInfo = runewidth.Truncate(playingFileInfo, topStatusMaxLength, "...")
} else {
trackInfo = runewidth.Truncate(playingFileName, topStatusMaxLength, "...")
2 years ago
}
statusBuffer.WriteString(trackInfo)
if printExtra {
padding := topStatusMaxLength - runewidth.StringWidth(trackInfo)
for i := 0; i < padding; i++ {
statusBuffer.WriteRune(' ')
}
2 years ago
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())
2 years ago
}
statusBuffer.Reset()
if silent {
statusBuffer.WriteString("Mut ")
2 years ago
for i := -7.5; i < 0.0; i += 0.5 {
statusBuffer.WriteRune(' ')
2 years ago
}
} else {
statusBuffer.WriteString("Vol ")
2 years ago
for i := -7.5; i < v-0.5; i += 0.5 {
statusBuffer.WriteRune(tcell.RuneHLine)
2 years ago
}
statusBuffer.WriteRune('▷')
2 years ago
for i := v; i < 0; i += 0.5 {
statusBuffer.WriteRune(' ')
2 years ago
}
}
bottomStatus := fmt.Sprintf("%s %s", durationFormatted, statusBuffer.String())
statusBuffer.Reset()
2 years ago
var progressIndicator string
2 years ago
if paused {
progressIndicator = "||"
2 years ago
} else {
progressIndicator = "▷"
2 years ago
}
padding := screenWidth - runewidth.StringWidth(bottomStatus) - len(formatDuration(p)) - runewidth.StringWidth(progressIndicator) - 3
position := int(float64(padding) * (float64(p) / float64(l)))
2 years ago
if position > padding-1 {
position = padding - 1
}
if paused && position > 0 {
position--
}
for i := 0; i < padding; i++ {
if i == position {
statusBuffer.WriteString(progressIndicator)
2 years ago
} else {
statusBuffer.WriteRune(tcell.RuneHLine)
2 years ago
}
}
seekStart = len(formatDuration(p)) + 2
2 years ago
seekEnd = seekStart + padding - 1
volumeStart = seekEnd + len(formatDuration(l)) + 4
volumeEnd = screenWidth - 2
bottomstatusbuf.SetText(" " + progressFormatted + " " + statusBuffer.String() + " " + bottomStatus)
statusBuffer.Reset()
2 years ago
}
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
2 years ago
updateMain()
updateQueue()
updateStatus()
}
func handleMainSelection(i int, item *cview.ListItem) {
go listSelect(i)
}
func handleQueueSelection(i int, item *cview.ListItem) {
go queueSelect(i)
}