ditty/gui.go

333 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)
}