Optimize rendering
This commit is contained in:
parent
5a61dd98ed
commit
c188b01860
8 changed files with 174 additions and 105 deletions
|
@ -1,5 +1,14 @@
|
|||
This document covers the [ditty](https://git.sr.ht/~tslocum/ditty) command-line options.
|
||||
This document covers the [ditty](https://git.sr.ht/~tslocum/ditty) configuration options.
|
||||
|
||||
# TODO
|
||||
# Default keybindings
|
||||
|
||||
WIP
|
||||
* Browse: J/K, Down/Up and PgDown/PgUp
|
||||
* Previous: P
|
||||
* Next: N
|
||||
* Select: Enter
|
||||
* Pause: Space
|
||||
* Volume: -/+
|
||||
|
||||
# config.yaml
|
||||
|
||||
TODO
|
||||
|
|
|
@ -23,9 +23,9 @@ Choose one of the following methods:
|
|||
GO111MODULE=on go get git.sr.ht/~tslocum/ditty
|
||||
```
|
||||
|
||||
## Configure
|
||||
## Documentation
|
||||
|
||||
See [CONFIGURATION.md](https://man.sr.ht/~tslocum/ditty/CONFIGURATION.md)
|
||||
See [CONFIGURATION.md](https://man.sr.ht/~tslocum/ditty/CONFIGURATION.md) for default keybindings.
|
||||
|
||||
## Support
|
||||
|
||||
|
|
16
audio.go
16
audio.go
|
@ -45,16 +45,18 @@ type AudioFile struct {
|
|||
Metadata *Metadata
|
||||
}
|
||||
|
||||
func openFile(filePath string) (*AudioFile, error) {
|
||||
func openFile(filePath string, metadata *Metadata) (*AudioFile, error) {
|
||||
f, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metadata := readMetadata(f)
|
||||
_, err = f.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
if metadata == nil {
|
||||
metadata = readMetadata(f)
|
||||
_, err = f.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
|
@ -106,6 +108,7 @@ func play(audioFile *AudioFile) {
|
|||
}
|
||||
|
||||
if audioFile.Format.SampleRate != playingSampleRate {
|
||||
speaker.Clear()
|
||||
err := speaker.Init(audioFile.Format.SampleRate, audioFile.Format.SampleRate.N(time.Second/2))
|
||||
if err != nil {
|
||||
log.Fatalf("failed to initialize audio device: %s", err)
|
||||
|
@ -154,7 +157,8 @@ func nextTrack() {
|
|||
if mainBufferCursor-1 < len(mainBufferFiles)-1 {
|
||||
mainBufferCursor++
|
||||
|
||||
audioFile, err := openFile(path.Join(mainBufferDirectory, selectedEntry().File.Name()))
|
||||
entry := selectedEntry()
|
||||
audioFile, err := openFile(path.Join(mainBufferDirectory, entry.File.Name()), entry.Metadata)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
202
gui.go
202
gui.go
|
@ -1,12 +1,14 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mattn/go-runewidth"
|
||||
|
@ -37,7 +39,12 @@ var (
|
|||
screenWidth, screenHeight int
|
||||
mainBufHeight int
|
||||
|
||||
statusText string
|
||||
mainBuffer bytes.Buffer
|
||||
mainLock = new(sync.Mutex)
|
||||
|
||||
statusText string
|
||||
statusBuffer bytes.Buffer
|
||||
statusLock = new(sync.Mutex)
|
||||
)
|
||||
|
||||
func initTUI() error {
|
||||
|
@ -97,50 +104,54 @@ func browseFolder(browse string) {
|
|||
}
|
||||
|
||||
func updateMain() {
|
||||
var titleText string
|
||||
mainLock.Lock()
|
||||
defer mainLock.Unlock()
|
||||
|
||||
mainBuffer.Reset()
|
||||
var statusMessage string
|
||||
if statusText != "" {
|
||||
titleText = statusText
|
||||
statusMessage = 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, "...")
|
||||
statusMessage = mainBufferDirectory
|
||||
}
|
||||
mainbuf.SetTitle(" " + titleText + " ")
|
||||
|
||||
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)
|
||||
|
||||
mainbuf.SetTitle(" " + runewidth.Truncate(mainBuffer.String(), screenWidth-4, "...") + " ")
|
||||
mainBuffer.Reset()
|
||||
|
||||
var printed int
|
||||
|
||||
var newBufferText string
|
||||
var line string
|
||||
if mainBufferOrigin == 0 {
|
||||
if mainBufferCursor == 0 {
|
||||
newBufferText += "[::r]"
|
||||
mainBuffer.WriteString("[::r]")
|
||||
}
|
||||
var line string
|
||||
if mainBufferDirectory == "/" {
|
||||
line = "./"
|
||||
} else {
|
||||
line = "../"
|
||||
}
|
||||
newBufferText += line
|
||||
mainBuffer.WriteString(line)
|
||||
for i := len(line); i < screenWidth-2; i++ {
|
||||
newBufferText += " "
|
||||
mainBuffer.WriteRune(' ')
|
||||
}
|
||||
if mainBufferCursor == 0 {
|
||||
newBufferText += "[-]"
|
||||
mainBuffer.WriteString("[-]")
|
||||
}
|
||||
printed++
|
||||
}
|
||||
|
@ -150,24 +161,23 @@ func updateMain() {
|
|||
}
|
||||
|
||||
if printed > 0 {
|
||||
newBufferText += "\n"
|
||||
mainBuffer.WriteRune('\n')
|
||||
}
|
||||
|
||||
if i == mainBufferCursor-1 {
|
||||
newBufferText += "[::r]"
|
||||
mainBuffer.WriteString("[::r]")
|
||||
}
|
||||
var line string
|
||||
if entry.File.IsDir() {
|
||||
line = entry.File.Name() + "/"
|
||||
} else {
|
||||
line = entry.String()
|
||||
}
|
||||
newBufferText += line
|
||||
mainBuffer.WriteString(line)
|
||||
for i := runewidth.StringWidth(line); i < screenWidth-2; i++ {
|
||||
newBufferText += " "
|
||||
mainBuffer.WriteRune(' ')
|
||||
}
|
||||
if i == mainBufferCursor-1 {
|
||||
newBufferText += "[-]"
|
||||
mainBuffer.WriteString("[-]")
|
||||
}
|
||||
|
||||
printed++
|
||||
|
@ -176,7 +186,7 @@ func updateMain() {
|
|||
}
|
||||
}
|
||||
|
||||
mainbuf.SetText(newBufferText)
|
||||
mainbuf.SetText(mainBuffer.String())
|
||||
}
|
||||
|
||||
func updateQueue() {
|
||||
|
@ -184,83 +194,95 @@ func updateQueue() {
|
|||
}
|
||||
|
||||
func updateStatus() {
|
||||
statusLock.Lock()
|
||||
defer statusLock.Unlock()
|
||||
|
||||
var sampleRate beep.SampleRate
|
||||
var d time.Duration
|
||||
var p time.Duration
|
||||
var l time.Duration
|
||||
var v float64
|
||||
var topStatusExtra string
|
||||
|
||||
speaker.Lock()
|
||||
if playingStreamer == nil {
|
||||
topstatusbuf.SetText("")
|
||||
bottomstatusbuf.SetText("")
|
||||
var paused bool
|
||||
var silent bool
|
||||
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
|
||||
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
|
||||
}
|
||||
statusBuffer.Reset()
|
||||
|
||||
speaker.Unlock()
|
||||
if paused {
|
||||
statusBuffer.WriteString("Paused ")
|
||||
}
|
||||
statusBuffer.WriteString(fmt.Sprintf(" %dHz %s", sampleRate.N(time.Second), fileFormat(playingFileName)))
|
||||
|
||||
topStatus := " "
|
||||
if playingFileInfo != "" {
|
||||
topStatus += playingFileInfo
|
||||
} else {
|
||||
topStatus += playingFileName
|
||||
}
|
||||
topStatusMaxFileLength := screenWidth - len(topStatusExtra) - 1
|
||||
if topStatusMaxFileLength >= 7 {
|
||||
if len(topStatus) > topStatusMaxFileLength {
|
||||
topStatus = topStatus[:topStatusMaxFileLength]
|
||||
topStatusExtra := statusBuffer.String()
|
||||
statusBuffer.Reset()
|
||||
|
||||
topStatusMaxLength := screenWidth - 2
|
||||
|
||||
printExtra := topStatusMaxLength >= (len(topStatusExtra)*2)+1
|
||||
if printExtra {
|
||||
topStatusMaxLength -= len(topStatusExtra)
|
||||
}
|
||||
|
||||
padding := screenWidth - runewidth.StringWidth(topStatus) - len(topStatusExtra) - 1
|
||||
for i := 0; i < padding; i++ {
|
||||
topStatus += " "
|
||||
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)
|
||||
}
|
||||
|
||||
topStatus += topStatusExtra
|
||||
topstatusbuf.SetText(statusBuffer.String())
|
||||
}
|
||||
topstatusbuf.SetText(topStatus)
|
||||
|
||||
var vol string
|
||||
if volume.Silent {
|
||||
vol = "Mut "
|
||||
statusBuffer.Reset()
|
||||
|
||||
if silent {
|
||||
statusBuffer.WriteString("Mut ")
|
||||
|
||||
for i := -7.5; i < 0.0; i += 0.5 {
|
||||
vol += string(tcell.RuneHLine)
|
||||
statusBuffer.WriteRune(tcell.RuneHLine)
|
||||
}
|
||||
} else {
|
||||
vol = "Vol "
|
||||
statusBuffer.WriteString("Vol ")
|
||||
|
||||
for i := -7.5; i < v-0.5; i += 0.5 {
|
||||
vol += string(tcell.RuneHLine)
|
||||
statusBuffer.WriteRune(tcell.RuneHLine)
|
||||
}
|
||||
vol += string(tcell.RuneBlock)
|
||||
statusBuffer.WriteRune(tcell.RuneBlock)
|
||||
for i := v; i < 0; i += 0.5 {
|
||||
vol += string(tcell.RuneHLine)
|
||||
statusBuffer.WriteRune(tcell.RuneHLine)
|
||||
}
|
||||
}
|
||||
|
||||
bottomStatus := fmt.Sprintf("%s %s", formatDuration(l), vol)
|
||||
bottomStatus := fmt.Sprintf("%s %s", formatDuration(l), statusBuffer.String())
|
||||
statusBuffer.Reset()
|
||||
|
||||
var durationIndicator string
|
||||
var progressIndicator string
|
||||
if paused {
|
||||
durationIndicator = "||"
|
||||
progressIndicator = "||"
|
||||
} else {
|
||||
durationIndicator = string(tcell.RuneBlock)
|
||||
progressIndicator = string(tcell.RuneBlock)
|
||||
}
|
||||
|
||||
padding := screenWidth - runewidth.StringWidth(bottomStatus) - len(formatDuration(d)) - runewidth.StringWidth(durationIndicator) - 3
|
||||
position := int(float64(padding) * (float64(d) / float64(l)))
|
||||
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
|
||||
}
|
||||
|
@ -268,22 +290,22 @@ func updateStatus() {
|
|||
position--
|
||||
}
|
||||
|
||||
var durationBar string
|
||||
for i := 0; i < padding; i++ {
|
||||
if i == position {
|
||||
durationBar += durationIndicator
|
||||
statusBuffer.WriteString(progressIndicator)
|
||||
} else {
|
||||
durationBar += string(tcell.RuneHLine)
|
||||
statusBuffer.WriteRune(tcell.RuneHLine)
|
||||
}
|
||||
}
|
||||
|
||||
seekStart = len(formatDuration(d)) + 2
|
||||
seekStart = len(formatDuration(p)) + 2
|
||||
seekEnd = seekStart + padding - 1
|
||||
|
||||
volumeStart = seekEnd + len(formatDuration(l)) + 4
|
||||
volumeEnd = screenWidth - 2
|
||||
|
||||
bottomstatusbuf.SetText(" " + formatDuration(d) + " " + durationBar + " " + bottomStatus)
|
||||
bottomstatusbuf.SetText(" " + formatDuration(p) + " " + statusBuffer.String() + " " + bottomStatus)
|
||||
statusBuffer.Reset()
|
||||
}
|
||||
|
||||
func formatDuration(d time.Duration) string {
|
||||
|
@ -315,7 +337,7 @@ func selectTrack() {
|
|||
return
|
||||
}
|
||||
|
||||
audioFile, err := openFile(path.Join(mainBufferDirectory, entry.File.Name()))
|
||||
audioFile, err := openFile(path.Join(mainBufferDirectory, entry.File.Name()), entry.Metadata)
|
||||
if err != nil {
|
||||
statusText = err.Error()
|
||||
go func() {
|
||||
|
|
15
gui_key.go
15
gui_key.go
|
@ -11,6 +11,10 @@ func handleKeyPress(event *tcell.EventKey) *tcell.EventKey {
|
|||
audioLock.Lock()
|
||||
defer audioLock.Unlock()
|
||||
|
||||
if volume == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
speaker.Lock()
|
||||
volume.Volume -= 0.5
|
||||
if volume.Volume <= -7.5 {
|
||||
|
@ -18,12 +22,17 @@ func handleKeyPress(event *tcell.EventKey) *tcell.EventKey {
|
|||
volume.Silent = true
|
||||
}
|
||||
speaker.Unlock()
|
||||
|
||||
updateStatus()
|
||||
return nil
|
||||
case '+':
|
||||
audioLock.Lock()
|
||||
defer audioLock.Unlock()
|
||||
|
||||
if ctrl == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
speaker.Lock()
|
||||
volume.Volume += 0.5
|
||||
if volume.Volume > 0 {
|
||||
|
@ -31,15 +40,21 @@ func handleKeyPress(event *tcell.EventKey) *tcell.EventKey {
|
|||
}
|
||||
volume.Silent = false
|
||||
speaker.Unlock()
|
||||
|
||||
updateStatus()
|
||||
return nil
|
||||
case ' ':
|
||||
audioLock.Lock()
|
||||
defer audioLock.Unlock()
|
||||
|
||||
if ctrl == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
speaker.Lock()
|
||||
ctrl.Paused = !ctrl.Paused
|
||||
speaker.Unlock()
|
||||
|
||||
updateStatus()
|
||||
return nil
|
||||
case 'j':
|
||||
|
|
|
@ -7,7 +7,7 @@ func listPrevious() {
|
|||
if mainBufferCursor > 0 {
|
||||
mainBufferCursor--
|
||||
}
|
||||
updateMain()
|
||||
app.QueueUpdateDraw(updateMain)
|
||||
}
|
||||
|
||||
func listNext() {
|
||||
|
@ -17,7 +17,7 @@ func listNext() {
|
|||
mainBufferOrigin++
|
||||
}
|
||||
}
|
||||
updateMain()
|
||||
app.QueueUpdateDraw(updateMain)
|
||||
}
|
||||
|
||||
func selectedEntry() *LibraryEntry {
|
||||
|
|
|
@ -17,6 +17,7 @@ func handleMouse(event *cview.EventMouse) *cview.EventMouse {
|
|||
// TODO Delay playing while cursor is moved
|
||||
if mouseY-1 < len(mainBufferFiles)+1 {
|
||||
mainBufferCursor = mainBufferOrigin + (mouseY - 1)
|
||||
app.QueueUpdateDraw(updateMain)
|
||||
go selectTrack()
|
||||
}
|
||||
return nil
|
||||
|
@ -38,6 +39,7 @@ func handleMouse(event *cview.EventMouse) *cview.EventMouse {
|
|||
seekTo := int(float64(playingStreamer.Len()) * (float64(mouseX-seekStart) / float64(seekEnd-seekStart)))
|
||||
_ = playingStreamer.Seek(seekTo) // Ignore seek errors
|
||||
speaker.Unlock()
|
||||
|
||||
app.QueueUpdateDraw(updateStatus)
|
||||
return nil
|
||||
} else if mouseX >= volumeStart && mouseX <= volumeEnd+1 {
|
||||
|
@ -49,6 +51,7 @@ func handleMouse(event *cview.EventMouse) *cview.EventMouse {
|
|||
speaker.Lock()
|
||||
volume.Silent = !volume.Silent
|
||||
speaker.Unlock()
|
||||
|
||||
app.QueueUpdateDraw(updateStatus)
|
||||
} else {
|
||||
speaker.Lock()
|
||||
|
@ -63,8 +66,8 @@ func handleMouse(event *cview.EventMouse) *cview.EventMouse {
|
|||
}
|
||||
|
||||
volume.Silent = setVolume <= -7.5
|
||||
|
||||
speaker.Unlock()
|
||||
|
||||
app.QueueUpdateDraw(updateStatus)
|
||||
}
|
||||
return nil
|
||||
|
|
18
main.go
18
main.go
|
@ -9,6 +9,7 @@ import (
|
|||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime/pprof"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
@ -28,6 +29,7 @@ Copyright (c) 2020 Trevor Slocum <trevor@rocketnine.space>
|
|||
var (
|
||||
printVersionInfo bool
|
||||
debugAddress string
|
||||
cpuProfile string
|
||||
|
||||
done = make(chan bool)
|
||||
)
|
||||
|
@ -37,6 +39,7 @@ func main() {
|
|||
|
||||
flag.BoolVar(&printVersionInfo, "version", false, "print version information and exit")
|
||||
flag.StringVar(&debugAddress, "debug-address", "", "address to serve debug info")
|
||||
flag.StringVar(&cpuProfile, "cpu-profile", "", "path to save CPU profiling")
|
||||
flag.Parse()
|
||||
|
||||
if printVersionInfo {
|
||||
|
@ -50,6 +53,19 @@ func main() {
|
|||
}()
|
||||
}
|
||||
|
||||
if cpuProfile != "" {
|
||||
f, err := os.Create(cpuProfile)
|
||||
if err != nil {
|
||||
log.Fatal("could not create CPU profile: ", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if err := pprof.StartCPUProfile(f); err != nil {
|
||||
log.Fatal("could not start CPU profile: ", err)
|
||||
}
|
||||
defer pprof.StopCPUProfile()
|
||||
}
|
||||
|
||||
err := initTUI()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to initialize terminal user interface: %s", err)
|
||||
|
@ -99,7 +115,7 @@ func main() {
|
|||
} else {
|
||||
browseFolder(filepath.Dir(startPath))
|
||||
|
||||
audioFile, err := openFile(strings.Join(flag.Args(), " "))
|
||||
audioFile, err := openFile(strings.Join(flag.Args(), " "), nil)
|
||||
if err != nil {
|
||||
statusText = err.Error()
|
||||
app.QueueUpdateDraw(updateMain)
|
||||
|
|
Loading…
Reference in a new issue