From c188b018608889d2ea8be370d3bc5f2817628038 Mon Sep 17 00:00:00 2001 From: Trevor Slocum Date: Thu, 9 Jan 2020 09:48:00 -0800 Subject: [PATCH] Optimize rendering --- CONFIGURATION.md | 15 +++- README.md | 4 +- audio.go | 16 ++-- gui.go | 202 ++++++++++++++++++++++++++--------------------- gui_key.go | 15 ++++ gui_list.go | 4 +- gui_mouse.go | 5 +- main.go | 18 ++++- 8 files changed, 174 insertions(+), 105 deletions(-) diff --git a/CONFIGURATION.md b/CONFIGURATION.md index 15c666a..cf94e19 100644 --- a/CONFIGURATION.md +++ b/CONFIGURATION.md @@ -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 diff --git a/README.md b/README.md index 55d55e4..e90d8c0 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/audio.go b/audio.go index dc41092..404db25 100644 --- a/audio.go +++ b/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 } diff --git a/gui.go b/gui.go index 47ae6b4..f165500 100644 --- a/gui.go +++ b/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() { diff --git a/gui_key.go b/gui_key.go index 2542824..6138172 100644 --- a/gui_key.go +++ b/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': diff --git a/gui_list.go b/gui_list.go index 1d1bb80..120e2b9 100644 --- a/gui_list.go +++ b/gui_list.go @@ -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 { diff --git a/gui_mouse.go b/gui_mouse.go index 8a0f036..210d03a 100644 --- a/gui_mouse.go +++ b/gui_mouse.go @@ -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 diff --git a/main.go b/main.go index 9a10bf2..14dcb45 100644 --- a/main.go +++ b/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 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)