332 lines
7.1 KiB
Go
332 lines
7.1 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/mattn/go-runewidth"
|
|
|
|
"github.com/gdamore/tcell"
|
|
|
|
"github.com/faiface/beep"
|
|
|
|
"git.sr.ht/~tslocum/cview"
|
|
"github.com/faiface/beep/speaker"
|
|
)
|
|
|
|
var (
|
|
app *cview.Application
|
|
mainbuf *cview.TextView
|
|
queuebuf *cview.TextView
|
|
topstatusbuf *cview.TextView
|
|
bottomstatusbuf *cview.TextView
|
|
|
|
mainBufferFiles []*LibraryEntry
|
|
mainBufferCursor int
|
|
mainBufferDirectory string
|
|
mainBufferOrigin int
|
|
|
|
seekStart, seekEnd int
|
|
volumeStart, volumeEnd int
|
|
|
|
screenWidth, screenHeight int
|
|
mainBufHeight int
|
|
|
|
statusText string
|
|
)
|
|
|
|
func initTUI() error {
|
|
app = cview.NewApplication()
|
|
|
|
app.EnableMouse()
|
|
|
|
app.SetInputCapture(handleKeyPress)
|
|
|
|
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 ")
|
|
|
|
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
|
|
}
|
|
|
|
mainBufferFiles = scanFolder(browse)
|
|
|
|
if len(mainBufferFiles) > 0 {
|
|
mainBufferCursor = 1
|
|
} else {
|
|
mainBufferCursor = 0
|
|
}
|
|
|
|
mainBufferDirectory = browse
|
|
app.QueueUpdateDraw(updateMain)
|
|
}
|
|
|
|
func updateMain() {
|
|
var titleText string
|
|
if statusText != "" {
|
|
titleText = statusText
|
|
} else {
|
|
titleText = mainBufferDirectory
|
|
|
|
truncated := false
|
|
widthRequirement := 4
|
|
for {
|
|
if runewidth.StringWidth(titleText) <= screenWidth-widthRequirement || !strings.ContainsRune(titleText, os.PathSeparator) {
|
|
break
|
|
}
|
|
|
|
titleText = titleText[strings.IndexRune(titleText, '/')+1:]
|
|
|
|
truncated = true
|
|
widthRequirement = 8
|
|
}
|
|
if truncated {
|
|
titleText = ".../" + titleText
|
|
}
|
|
titleText = runewidth.Truncate(titleText, screenWidth-4, "...")
|
|
}
|
|
mainbuf.SetTitle(" " + titleText + " ")
|
|
|
|
var printed int
|
|
|
|
var newBufferText string
|
|
if mainBufferOrigin == 0 {
|
|
if mainBufferCursor == 0 {
|
|
newBufferText += "[::r]"
|
|
}
|
|
var line string
|
|
if mainBufferDirectory == "/" {
|
|
line = "./"
|
|
} else {
|
|
line = "../"
|
|
}
|
|
newBufferText += line
|
|
for i := len(line); i < screenWidth-2; i++ {
|
|
newBufferText += " "
|
|
}
|
|
if mainBufferCursor == 0 {
|
|
newBufferText += "[-]"
|
|
}
|
|
printed++
|
|
}
|
|
for i, entry := range mainBufferFiles {
|
|
if i < mainBufferOrigin-1 || i-mainBufferOrigin-1 > mainBufHeight-1 {
|
|
continue
|
|
}
|
|
|
|
if printed > 0 {
|
|
newBufferText += "\n"
|
|
}
|
|
|
|
if i == mainBufferCursor-1 {
|
|
newBufferText += "[::r]"
|
|
}
|
|
var line string
|
|
if entry.File.IsDir() {
|
|
line = entry.File.Name() + "/"
|
|
} else {
|
|
line = entry.String()
|
|
}
|
|
newBufferText += line
|
|
for i := runewidth.StringWidth(line); i < screenWidth-2; i++ {
|
|
newBufferText += " "
|
|
}
|
|
if i == mainBufferCursor-1 {
|
|
newBufferText += "[-]"
|
|
}
|
|
|
|
printed++
|
|
if printed == mainBufHeight-2 {
|
|
break
|
|
}
|
|
}
|
|
|
|
mainbuf.SetText(newBufferText)
|
|
}
|
|
|
|
func updateQueue() {
|
|
// TODO
|
|
}
|
|
|
|
func updateStatus() {
|
|
var sampleRate beep.SampleRate
|
|
var d time.Duration
|
|
var l time.Duration
|
|
var v float64
|
|
var topStatusExtra string
|
|
|
|
speaker.Lock()
|
|
if playingStreamer == nil {
|
|
topstatusbuf.SetText("")
|
|
bottomstatusbuf.SetText("")
|
|
speaker.Unlock()
|
|
return
|
|
}
|
|
|
|
sampleRate = playingFormat.SampleRate
|
|
d = playingFormat.SampleRate.D(playingStreamer.Position()).Truncate(time.Second)
|
|
l = playingFormat.SampleRate.D(playingStreamer.Len()).Truncate(time.Second)
|
|
v = volume.Volume
|
|
paused := ctrl.Paused
|
|
topStatusExtra = fmt.Sprintf("%dHz %s", sampleRate.N(time.Second), fileFormat(playingFileName))
|
|
if paused {
|
|
topStatusExtra = "Paused " + topStatusExtra
|
|
}
|
|
|
|
speaker.Unlock()
|
|
|
|
topStatus := " "
|
|
if playingFileInfo != "" {
|
|
topStatus += playingFileInfo
|
|
} else {
|
|
topStatus += playingFileName
|
|
}
|
|
topStatusMaxFileLength := screenWidth - len(topStatusExtra) - 1
|
|
if topStatusMaxFileLength >= 7 {
|
|
if len(topStatus) > topStatusMaxFileLength {
|
|
topStatus = topStatus[:topStatusMaxFileLength]
|
|
}
|
|
|
|
padding := screenWidth - runewidth.StringWidth(topStatus) - len(topStatusExtra) - 1
|
|
for i := 0; i < padding; i++ {
|
|
topStatus += " "
|
|
}
|
|
|
|
topStatus += topStatusExtra
|
|
}
|
|
topstatusbuf.SetText(topStatus)
|
|
|
|
var vol string
|
|
if volume.Silent {
|
|
vol = "Mut "
|
|
|
|
for i := -7.5; i < 0.0; i += 0.5 {
|
|
vol += string(tcell.RuneHLine)
|
|
}
|
|
} else {
|
|
vol = "Vol "
|
|
|
|
for i := -7.5; i < v-0.5; i += 0.5 {
|
|
vol += string(tcell.RuneHLine)
|
|
}
|
|
vol += string(tcell.RuneBlock)
|
|
for i := v; i < 0; i += 0.5 {
|
|
vol += string(tcell.RuneHLine)
|
|
}
|
|
}
|
|
|
|
bottomStatus := fmt.Sprintf("%s %s", formatDuration(l), vol)
|
|
|
|
var durationIndicator string
|
|
if paused {
|
|
durationIndicator = "||"
|
|
} else {
|
|
durationIndicator = string(tcell.RuneBlock)
|
|
}
|
|
|
|
padding := screenWidth - runewidth.StringWidth(bottomStatus) - len(formatDuration(d)) - runewidth.StringWidth(durationIndicator) - 3
|
|
position := int(float64(padding) * (float64(d) / float64(l)))
|
|
if position > padding-1 {
|
|
position = padding - 1
|
|
}
|
|
if paused && position > 0 {
|
|
position--
|
|
}
|
|
|
|
var durationBar string
|
|
for i := 0; i < padding; i++ {
|
|
if i == position {
|
|
durationBar += durationIndicator
|
|
} else {
|
|
durationBar += string(tcell.RuneHLine)
|
|
}
|
|
}
|
|
|
|
seekStart = len(formatDuration(d)) + 2
|
|
seekEnd = seekStart + padding - 1
|
|
|
|
volumeStart = seekEnd + len(formatDuration(l)) + 4
|
|
volumeEnd = screenWidth - 2
|
|
|
|
bottomstatusbuf.SetText(" " + formatDuration(d) + " " + durationBar + " " + bottomStatus)
|
|
}
|
|
|
|
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(screen tcell.Screen) {
|
|
screenWidth, screenHeight = screen.Size()
|
|
|
|
updateMain()
|
|
updateQueue()
|
|
updateStatus()
|
|
}
|
|
|
|
func selectTrack() {
|
|
if mainBufferCursor == 0 {
|
|
browseFolder(path.Join(mainBufferDirectory, ".."))
|
|
return
|
|
}
|
|
nextStreamer = nil
|
|
nextFormat = beep.Format{}
|
|
|
|
entry := selectedEntry()
|
|
if entry.File.IsDir() {
|
|
browseFolder(path.Join(mainBufferDirectory, path.Base(entry.File.Name())))
|
|
return
|
|
}
|
|
|
|
audioFile, err := openFile(path.Join(mainBufferDirectory, entry.File.Name()))
|
|
if err != nil {
|
|
statusText = err.Error()
|
|
go func() {
|
|
time.Sleep(5 * time.Second)
|
|
statusText = ""
|
|
app.QueueUpdateDraw(updateMain)
|
|
}()
|
|
app.QueueUpdateDraw(updateMain)
|
|
return
|
|
}
|
|
go play(audioFile)
|
|
|
|
app.QueueUpdateDraw(updateStatus)
|
|
}
|