Allow keybindings to be configured
This commit is contained in:
parent
6a88324acb
commit
f6d6abde23
13 changed files with 325 additions and 145 deletions
|
@ -1,3 +1,7 @@
|
|||
0.1.3:
|
||||
- Allow keybindings to be configured
|
||||
- Add --fd option
|
||||
|
||||
0.1.2:
|
||||
- Allow audio buffer size to be configured
|
||||
- Add keybinding backspace to focus the current folder after returning to the parent folder
|
||||
|
|
|
@ -1,15 +1,56 @@
|
|||
This document covers the [ditty](https://git.sr.ht/~tslocum/ditty) configuration options.
|
||||
This document covers the [ditty](https://git.sr.ht/~tslocum/ditty)
|
||||
configuration options and their defaults.
|
||||
|
||||
# Options
|
||||
|
||||
* **--buffer-size** Audio buffer size (default 500ms)
|
||||
* **--fd** Write audio in WAV format to the specified file descriptor. This allows ditty to be used over ssh:
|
||||
* `ssh ditty.rocketnine.space -t 'ditty --fd=2' 2> >(aplay --quiet)`
|
||||
|
||||
# Default keybindings
|
||||
|
||||
* Browse: J/K, Down/Up and PgDown/PgUp
|
||||
* Previous: P
|
||||
* Next: N
|
||||
* Select: Enter
|
||||
* Pause: Space
|
||||
* Volume: -/+/M
|
||||
* Parent folder, focus current: Backspace
|
||||
* **Select** Enter
|
||||
* **Pause** Space
|
||||
* **Refresh** R
|
||||
* **Browse parent folder and focus last** Backspace
|
||||
* **Browse items** J/K, Down/Up and PageDown/PageUp
|
||||
* **Previous track** P
|
||||
* **Next track** N
|
||||
* **Volume** -/+/M
|
||||
* **Exit** Escape
|
||||
|
||||
# config.yaml
|
||||
# Default ~/.config/ditty/config.yaml
|
||||
|
||||
TODO
|
||||
```yaml
|
||||
input:
|
||||
select:
|
||||
- 'Enter'
|
||||
pause:
|
||||
- 'Space'
|
||||
refresh:
|
||||
- 'r'
|
||||
browse-parent:
|
||||
- 'Backspace'
|
||||
volume-mute:
|
||||
- 'm'
|
||||
volume-down:
|
||||
- '-'
|
||||
volume-up:
|
||||
- '+'
|
||||
previous-item:
|
||||
- 'Up'
|
||||
- 'k'
|
||||
next-item:
|
||||
- 'Down'
|
||||
- 'j'
|
||||
previous-page:
|
||||
- 'PageUp'
|
||||
next-page:
|
||||
- 'PageDown'
|
||||
previous-track:
|
||||
- 'p'
|
||||
next-track:
|
||||
- 'n'
|
||||
exit:
|
||||
- 'Escape'
|
||||
```
|
||||
|
|
|
@ -20,9 +20,13 @@ Choose one of the following methods:
|
|||
### Compile
|
||||
|
||||
```
|
||||
GO111MODULE=on go get git.sr.ht/~tslocum/ditty
|
||||
go get git.sr.ht/~tslocum/ditty
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
ditty is powered by [beep](https://github.com/faiface/beep).
|
||||
|
||||
## Documentation
|
||||
|
||||
See [CONFIGURATION.md](https://man.sr.ht/~tslocum/ditty/CONFIGURATION.md) for default keybindings.
|
||||
|
|
76
audio.go
76
audio.go
|
@ -20,7 +20,7 @@ import (
|
|||
"github.com/faiface/beep/wav"
|
||||
)
|
||||
|
||||
const DefaultBufferSize = 500 * time.Millisecond
|
||||
const defaultBufferSize = 500 * time.Millisecond
|
||||
|
||||
var (
|
||||
playingFileName string
|
||||
|
@ -40,14 +40,14 @@ var (
|
|||
audioLock = new(sync.Mutex)
|
||||
)
|
||||
|
||||
type AudioFile struct {
|
||||
type audioFile struct {
|
||||
File *os.File
|
||||
Streamer beep.StreamSeekCloser
|
||||
Format beep.Format
|
||||
Metadata *Metadata
|
||||
Metadata *metadata
|
||||
}
|
||||
|
||||
func openFile(filePath string, metadata *Metadata) (*AudioFile, error) {
|
||||
func openFile(filePath string, metadata *metadata) (*audioFile, error) {
|
||||
f, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -81,11 +81,11 @@ func openFile(filePath string, metadata *Metadata) (*AudioFile, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
a := AudioFile{File: f, Streamer: streamer, Format: format, Metadata: metadata}
|
||||
a := audioFile{File: f, Streamer: streamer, Format: format, Metadata: metadata}
|
||||
return &a, nil
|
||||
}
|
||||
|
||||
func play(audioFile *AudioFile) {
|
||||
func play(audioFile *audioFile) {
|
||||
audioLock.Lock()
|
||||
defer audioLock.Unlock()
|
||||
|
||||
|
@ -121,36 +121,48 @@ func play(audioFile *AudioFile) {
|
|||
speaker.Clear()
|
||||
}
|
||||
|
||||
var (
|
||||
vol float64
|
||||
silent bool
|
||||
)
|
||||
streamer := beep.Seq(audioFile.Streamer, beep.Callback(func() {
|
||||
if playingFileID != thisFileID {
|
||||
return
|
||||
}
|
||||
|
||||
go nextTrack()
|
||||
}))
|
||||
|
||||
speaker.Lock()
|
||||
if volume != nil {
|
||||
vol = volume.Volume
|
||||
silent = volume.Silent
|
||||
volume.Streamer = streamer
|
||||
ctrl.Paused = false
|
||||
} else {
|
||||
volume = &effects.Volume{
|
||||
Streamer: streamer,
|
||||
Base: volumeBase,
|
||||
Volume: 0.0,
|
||||
Silent: false,
|
||||
}
|
||||
|
||||
ctrl = &beep.Ctrl{
|
||||
Streamer: volume,
|
||||
Paused: false,
|
||||
}
|
||||
}
|
||||
speaker.Unlock()
|
||||
|
||||
volume = &effects.Volume{
|
||||
Streamer: beep.Seq(audioFile.Streamer, beep.Callback(func() {
|
||||
if playingFileID != thisFileID {
|
||||
return
|
||||
}
|
||||
if streamFdInt >= 0 {
|
||||
if streamFd == nil {
|
||||
streamFd = os.NewFile(uintptr(streamFdInt), "out")
|
||||
|
||||
go nextTrack()
|
||||
})),
|
||||
Base: volumeBase,
|
||||
Volume: vol,
|
||||
Silent: silent,
|
||||
go func() {
|
||||
for {
|
||||
_ = wav.Encode(streamFd, ctrl, playingFormat)
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
}
|
||||
}()
|
||||
}
|
||||
} else {
|
||||
speaker.Play(ctrl)
|
||||
}
|
||||
|
||||
ctrl = &beep.Ctrl{
|
||||
Streamer: volume,
|
||||
Paused: false,
|
||||
}
|
||||
|
||||
speaker.Play(ctrl)
|
||||
go app.QueueUpdateDraw(func() {
|
||||
updateMain()
|
||||
updateQueue()
|
||||
|
@ -276,6 +288,14 @@ func setVolume(vol float64) {
|
|||
go app.QueueUpdateDraw(updateStatus)
|
||||
}
|
||||
|
||||
func decreaseVolume() {
|
||||
adjustVolume(-0.5)
|
||||
}
|
||||
|
||||
func increaseVolume() {
|
||||
adjustVolume(0.5)
|
||||
}
|
||||
|
||||
func volumeUpdated() {
|
||||
if volume.Volume <= -7.5 {
|
||||
volume.Volume = -7.5
|
||||
|
|
48
config.go
Normal file
48
config.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
type appConfig struct {
|
||||
Input map[string][]string
|
||||
}
|
||||
|
||||
var config = &appConfig{}
|
||||
|
||||
func defaultConfigPath() string {
|
||||
homedir, err := os.UserHomeDir()
|
||||
if err == nil && homedir != "" {
|
||||
return path.Join(homedir, ".config", "ditty", "config.yaml")
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func readConfig(configPath string) error {
|
||||
if configPath == "" {
|
||||
configPath = defaultConfigPath()
|
||||
if configPath == "" {
|
||||
return nil
|
||||
} else if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
configData, err := ioutil.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read file: %s", err)
|
||||
}
|
||||
|
||||
err = yaml.Unmarshal(configData, config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse file: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
3
go.mod
3
go.mod
|
@ -3,7 +3,7 @@ module git.sr.ht/~tslocum/ditty
|
|||
go 1.13
|
||||
|
||||
require (
|
||||
git.sr.ht/~tslocum/cbind v0.0.0-20200122005705-c4f326764399
|
||||
git.sr.ht/~tslocum/cbind v0.0.0-20200122224255-ab6e3ebbd35c
|
||||
git.sr.ht/~tslocum/cview v1.4.1-0.20200117063451-51704b98449e
|
||||
github.com/dhowden/tag v0.0.0-20191122115059-7e5c04feccd8
|
||||
github.com/faiface/beep v1.0.2
|
||||
|
@ -17,4 +17,5 @@ require (
|
|||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a // indirect
|
||||
golang.org/x/image v0.0.0-20200119044424-58c23975cae1 // indirect
|
||||
golang.org/x/mobile v0.0.0-20200121160505-1d4ecbb920e2 // indirect
|
||||
gopkg.in/yaml.v2 v2.2.7
|
||||
)
|
||||
|
|
12
go.sum
12
go.sum
|
@ -1,6 +1,6 @@
|
|||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
git.sr.ht/~tslocum/cbind v0.0.0-20200122005705-c4f326764399 h1:r7VQ34sqmzSQzuWS7nrHqaR92ARCN62s032M7cJ9Heo=
|
||||
git.sr.ht/~tslocum/cbind v0.0.0-20200122005705-c4f326764399/go.mod h1:NE2mliwcbn3v+eT4rCiQsQYEUftKzGXMfii83F385rA=
|
||||
git.sr.ht/~tslocum/cbind v0.0.0-20200122224255-ab6e3ebbd35c h1:6OaMHWvMXT/jygdKXDbeqEmPRQWRkM2udYypJt1jQrA=
|
||||
git.sr.ht/~tslocum/cbind v0.0.0-20200122224255-ab6e3ebbd35c/go.mod h1:pwSeemOTUIOImiuZhlY2dWXpvFqI7woKg0nJ2a50XPA=
|
||||
git.sr.ht/~tslocum/cview v1.4.1-0.20200117063451-51704b98449e h1:gKRY1WnpfiBLT7ypBPz9njkkqBXrjMfru1QfnhFDnG8=
|
||||
git.sr.ht/~tslocum/cview v1.4.1-0.20200117063451-51704b98449e/go.mod h1:TLTjvAd3pw6MqV6SaBMpxOdOdODW4O2gtQJ3B3H6PoU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
|
@ -104,8 +104,8 @@ golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200103143344-a1369afcdac7 h1:/W9OPMnnpmFXHYkcp2rQsbFUbRlRzfECQjmAFiOyHE8=
|
||||
golang.org/x/sys v0.0.0-20200103143344-a1369afcdac7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200121082415-34d275377bf9 h1:N19i1HjUnR7TF7rMt8O4p3dLvqvmYyzB6ifMFmrbY50=
|
||||
golang.org/x/sys v0.0.0-20200121082415-34d275377bf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82 h1:ywK/j/KkyTHcdyYSZNXGjMwgmDSfjglYZ3vStQ/gSCU=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
|
@ -116,3 +116,7 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
|
|||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0/go.mod h1:OdE7CF6DbADk7lN8LIKRzRJTTZXIjtWgA5THM5lhBAw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
|
||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
|
11
gui.go
11
gui.go
|
@ -25,7 +25,7 @@ var (
|
|||
topstatusbuf *cview.TextView
|
||||
bottomstatusbuf *cview.TextView
|
||||
|
||||
mainBufferFiles []*LibraryEntry
|
||||
mainBufferFiles []*libraryEntry
|
||||
mainBufferCursor int
|
||||
mainBufferDirectory string
|
||||
mainBufferOrigin int
|
||||
|
@ -50,7 +50,6 @@ func initTUI() error {
|
|||
|
||||
app.EnableMouse()
|
||||
|
||||
setDefaultKeyBinds()
|
||||
app.SetInputCapture(inputConfig.Capture)
|
||||
|
||||
app.SetAfterResizeFunc(handleResize)
|
||||
|
@ -94,9 +93,13 @@ func browseFolder(browse string) {
|
|||
return
|
||||
}
|
||||
|
||||
placeCursorAtTop := mainBufferCursor == 0
|
||||
mainBufferFiles = scanFolder(browse)
|
||||
|
||||
mainBufferCursor = 0
|
||||
if !placeCursorAtTop && len(mainBufferFiles) > 0 {
|
||||
mainBufferCursor = 1
|
||||
} else {
|
||||
mainBufferCursor = 0
|
||||
}
|
||||
mainBufferOrigin = 0
|
||||
|
||||
mainBufferDirectory = browse
|
||||
|
|
171
gui_key.go
171
gui_key.go
|
@ -1,93 +1,100 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.sr.ht/~tslocum/cbind"
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
||||
const (
|
||||
actionSelect = "select"
|
||||
actionPause = "pause"
|
||||
actionRefresh = "refresh"
|
||||
actionBrowseParent = "browse-parent"
|
||||
actionVolumeMute = "volume-mute"
|
||||
actionVolumeDown = "volume-down"
|
||||
actionVolumeUp = "volume-up"
|
||||
actionPreviousItem = "previous-item"
|
||||
actionNextItem = "next-item"
|
||||
actionPreviousPage = "previous-page"
|
||||
actionNextPage = "next-page"
|
||||
actionPreviousTrack = "previous-track"
|
||||
actionNextTrack = "next-track"
|
||||
actionExit = "exit"
|
||||
)
|
||||
|
||||
var actionHandlers = map[string]func(){
|
||||
actionSelect: listSelect,
|
||||
actionPause: pause,
|
||||
actionRefresh: listRefresh,
|
||||
actionBrowseParent: browseParent,
|
||||
actionVolumeMute: toggleMute,
|
||||
actionVolumeDown: decreaseVolume,
|
||||
actionVolumeUp: increaseVolume,
|
||||
actionPreviousItem: listPrevious,
|
||||
actionNextItem: listNext,
|
||||
actionPreviousPage: listPreviousPage,
|
||||
actionNextPage: listNextPage,
|
||||
actionPreviousTrack: skipPrevious,
|
||||
actionNextTrack: skipNext,
|
||||
actionExit: exit,
|
||||
}
|
||||
|
||||
var inputConfig = cbind.NewConfiguration()
|
||||
|
||||
func setDefaultKeyBinds() {
|
||||
inputConfig.SetKey(tcell.ModNone, tcell.KeyUp, func(ev *tcell.EventKey) *tcell.EventKey {
|
||||
listPrevious()
|
||||
func wrapEventHandler(f func()) func(_ *tcell.EventKey) *tcell.EventKey {
|
||||
return func(_ *tcell.EventKey) *tcell.EventKey {
|
||||
f()
|
||||
return nil
|
||||
})
|
||||
|
||||
inputConfig.SetKey(tcell.ModNone, tcell.KeyDown, func(ev *tcell.EventKey) *tcell.EventKey {
|
||||
listNext()
|
||||
return nil
|
||||
})
|
||||
inputConfig.SetRune(tcell.ModNone, 'k', func(ev *tcell.EventKey) *tcell.EventKey {
|
||||
listPrevious()
|
||||
return nil
|
||||
})
|
||||
|
||||
inputConfig.SetRune(tcell.ModNone, 'j', func(ev *tcell.EventKey) *tcell.EventKey {
|
||||
listNext()
|
||||
return nil
|
||||
})
|
||||
|
||||
inputConfig.SetKey(tcell.ModNone, tcell.KeyEnter, func(ev *tcell.EventKey) *tcell.EventKey {
|
||||
go listSelect()
|
||||
return nil
|
||||
})
|
||||
|
||||
inputConfig.SetRune(tcell.ModNone, ' ', func(ev *tcell.EventKey) *tcell.EventKey {
|
||||
pause()
|
||||
return nil
|
||||
})
|
||||
|
||||
inputConfig.SetRune(tcell.ModNone, 'p', func(ev *tcell.EventKey) *tcell.EventKey {
|
||||
skipPrevious()
|
||||
return nil
|
||||
})
|
||||
|
||||
inputConfig.SetRune(tcell.ModNone, 'n', func(ev *tcell.EventKey) *tcell.EventKey {
|
||||
skipNext()
|
||||
return nil
|
||||
})
|
||||
|
||||
inputConfig.SetRune(tcell.ModNone, '-', func(ev *tcell.EventKey) *tcell.EventKey {
|
||||
adjustVolume(-0.5)
|
||||
return nil
|
||||
})
|
||||
|
||||
inputConfig.SetRune(tcell.ModNone, '+', func(ev *tcell.EventKey) *tcell.EventKey {
|
||||
adjustVolume(0.5)
|
||||
return nil
|
||||
})
|
||||
|
||||
inputConfig.SetRune(tcell.ModNone, 'm', func(ev *tcell.EventKey) *tcell.EventKey {
|
||||
toggleMute()
|
||||
return nil
|
||||
})
|
||||
|
||||
inputConfig.SetKey(tcell.ModNone, tcell.KeyEscape, func(ev *tcell.EventKey) *tcell.EventKey {
|
||||
done <- true
|
||||
return nil
|
||||
})
|
||||
|
||||
inputConfig.SetKey(tcell.ModNone, tcell.KeyBackspace, func(ev *tcell.EventKey) *tcell.EventKey {
|
||||
browseParent()
|
||||
return nil
|
||||
})
|
||||
|
||||
inputConfig.SetKey(tcell.ModNone, tcell.KeyBackspace2, func(ev *tcell.EventKey) *tcell.EventKey {
|
||||
browseParent()
|
||||
return nil
|
||||
})
|
||||
|
||||
inputConfig.SetKey(tcell.ModNone, tcell.KeyPgUp, func(ev *tcell.EventKey) *tcell.EventKey {
|
||||
listPreviousPage()
|
||||
return nil
|
||||
})
|
||||
|
||||
inputConfig.SetKey(tcell.ModNone, tcell.KeyPgDn, func(ev *tcell.EventKey) *tcell.EventKey {
|
||||
listNextPage()
|
||||
return nil
|
||||
})
|
||||
|
||||
// TODO:
|
||||
// Queue non-recursively - q
|
||||
// Queue recursively - Q
|
||||
}
|
||||
}
|
||||
|
||||
func setKeyBinds() error {
|
||||
if len(config.Input) == 0 {
|
||||
setDefaultKeyBinds()
|
||||
}
|
||||
|
||||
for a, keys := range config.Input {
|
||||
a = strings.ToLower(a)
|
||||
handler := actionHandlers[a]
|
||||
if handler == nil {
|
||||
return fmt.Errorf("failed to set keybind for %s: unknown action", a)
|
||||
}
|
||||
|
||||
for _, k := range keys {
|
||||
mod, key, ch, err := cbind.Decode(k)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set keybind %s for %s: %s", k, a, err)
|
||||
}
|
||||
|
||||
if key == tcell.KeyRune {
|
||||
inputConfig.SetRune(mod, ch, wrapEventHandler(handler))
|
||||
} else {
|
||||
inputConfig.SetKey(mod, key, wrapEventHandler(handler))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setDefaultKeyBinds() {
|
||||
config.Input = map[string][]string{
|
||||
actionSelect: {"Enter"},
|
||||
actionPause: {"Space"},
|
||||
actionRefresh: {"r"},
|
||||
actionBrowseParent: {"Backspace"},
|
||||
actionVolumeMute: {"m"},
|
||||
actionVolumeDown: {"-"},
|
||||
actionVolumeUp: {"+"},
|
||||
actionPreviousItem: {"Up", "k"},
|
||||
actionNextItem: {"Down", "j"},
|
||||
actionPreviousPage: {"PageUp"},
|
||||
actionNextPage: {"PageDown"},
|
||||
actionPreviousTrack: {"p"},
|
||||
actionNextTrack: {"n"},
|
||||
actionExit: {"Escape"},
|
||||
}
|
||||
}
|
||||
|
|
11
gui_list.go
11
gui_list.go
|
@ -59,11 +59,11 @@ func listSelect() {
|
|||
go app.QueueUpdateDraw(updateStatus)
|
||||
}
|
||||
|
||||
func selectedEntry() *LibraryEntry {
|
||||
func selectedEntry() *libraryEntry {
|
||||
return mainBufferFiles[mainBufferCursor-1]
|
||||
}
|
||||
|
||||
func offsetEntry(offset int) *LibraryEntry {
|
||||
func offsetEntry(offset int) *libraryEntry {
|
||||
return mainBufferFiles[(mainBufferCursor-1)+offset]
|
||||
}
|
||||
|
||||
|
@ -102,3 +102,10 @@ func listNextPage() {
|
|||
|
||||
go app.QueueUpdateDraw(updateMain)
|
||||
}
|
||||
|
||||
func listRefresh() {
|
||||
// TODO Remember cursor position
|
||||
d := mainBufferDirectory
|
||||
mainBufferDirectory = ""
|
||||
browseFolder(d)
|
||||
}
|
||||
|
|
20
gui_mouse.go
20
gui_mouse.go
|
@ -37,7 +37,18 @@ func handleMouse(event *cview.EventMouse) *cview.EventMouse {
|
|||
|
||||
audioLock.Lock()
|
||||
speaker.Lock()
|
||||
seekTo := int(float64(playingStreamer.Len()) * (float64(mouseX-seekStart) / float64(seekEnd-seekStart)))
|
||||
|
||||
if playingStreamer == nil {
|
||||
speaker.Unlock()
|
||||
audioLock.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
pos := float64(mouseX-seekStart) / float64(seekEnd-seekStart)
|
||||
if pos > 1 {
|
||||
pos = 1
|
||||
}
|
||||
seekTo := int(float64(playingStreamer.Len()-1) * pos)
|
||||
_ = playingStreamer.Seek(seekTo) // Ignore seek errors
|
||||
speaker.Unlock()
|
||||
audioLock.Unlock()
|
||||
|
@ -52,6 +63,13 @@ func handleMouse(event *cview.EventMouse) *cview.EventMouse {
|
|||
if mouseX-volumeStart <= 3 {
|
||||
audioLock.Lock()
|
||||
speaker.Lock()
|
||||
|
||||
if volume == nil {
|
||||
speaker.Unlock()
|
||||
audioLock.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
volume.Silent = !volume.Silent
|
||||
speaker.Unlock()
|
||||
audioLock.Unlock()
|
||||
|
|
20
library.go
20
library.go
|
@ -11,15 +11,15 @@ import (
|
|||
"github.com/dhowden/tag"
|
||||
)
|
||||
|
||||
type Metadata struct {
|
||||
type metadata struct {
|
||||
Title string
|
||||
Artist string
|
||||
Album string
|
||||
Track int
|
||||
}
|
||||
|
||||
func readMetadata(f *os.File) *Metadata {
|
||||
var metadata Metadata
|
||||
func readMetadata(f *os.File) *metadata {
|
||||
var metadata metadata
|
||||
|
||||
m, err := tag.ReadFrom(f)
|
||||
if err != nil || m.Title() == "" {
|
||||
|
@ -34,12 +34,12 @@ func readMetadata(f *os.File) *Metadata {
|
|||
return &metadata
|
||||
}
|
||||
|
||||
type LibraryEntry struct {
|
||||
type libraryEntry struct {
|
||||
File os.FileInfo
|
||||
Metadata *Metadata
|
||||
Metadata *metadata
|
||||
}
|
||||
|
||||
func (e *LibraryEntry) String() string {
|
||||
func (e *libraryEntry) String() string {
|
||||
if e.Metadata.Title != "" {
|
||||
if e.Metadata.Artist != "" {
|
||||
return e.Metadata.Artist + " - " + e.Metadata.Title
|
||||
|
@ -51,16 +51,16 @@ func (e *LibraryEntry) String() string {
|
|||
return e.File.Name()
|
||||
}
|
||||
|
||||
func scanFolder(scanPath string) []*LibraryEntry {
|
||||
func scanFolder(scanPath string) []*libraryEntry {
|
||||
files, err := ioutil.ReadDir(scanPath)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to scan %s: %s", scanPath, err)
|
||||
}
|
||||
|
||||
var entries []*LibraryEntry
|
||||
var entries []*libraryEntry
|
||||
for _, fileInfo := range files {
|
||||
if fileInfo.IsDir() {
|
||||
entries = append(entries, &LibraryEntry{File: fileInfo, Metadata: &Metadata{Title: fileInfo.Name()}})
|
||||
entries = append(entries, &libraryEntry{File: fileInfo, Metadata: &metadata{Title: fileInfo.Name()}})
|
||||
continue
|
||||
} else if !supportedFormat(fileInfo.Name()) {
|
||||
continue
|
||||
|
@ -73,7 +73,7 @@ func scanFolder(scanPath string) []*LibraryEntry {
|
|||
metadata := readMetadata(f)
|
||||
f.Close()
|
||||
|
||||
entries = append(entries, &LibraryEntry{File: fileInfo, Metadata: metadata})
|
||||
entries = append(entries, &libraryEntry{File: fileInfo, Metadata: metadata})
|
||||
}
|
||||
|
||||
sort.Slice(entries, func(i, j int) bool {
|
||||
|
|
27
main.go
27
main.go
|
@ -27,19 +27,28 @@ Copyright (c) 2020 Trevor Slocum <trevor@rocketnine.space>
|
|||
)
|
||||
|
||||
var (
|
||||
configPath string
|
||||
printVersionInfo bool
|
||||
bufferSize time.Duration
|
||||
debugAddress string
|
||||
cpuProfile string
|
||||
streamFdInt int
|
||||
streamFd *os.File
|
||||
|
||||
done = make(chan bool)
|
||||
)
|
||||
|
||||
func exit() {
|
||||
done <- true
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
|
||||
flag.StringVar(&configPath, "config", "", "path to configuration file")
|
||||
flag.BoolVar(&printVersionInfo, "version", false, "print version information and exit")
|
||||
flag.DurationVar(&bufferSize, "buffer-size", DefaultBufferSize, "audio buffer size")
|
||||
flag.IntVar(&streamFdInt, "fd", -1, "stream audio to file descriptor")
|
||||
flag.DurationVar(&bufferSize, "buffer-size", defaultBufferSize, "audio buffer size")
|
||||
flag.StringVar(&debugAddress, "debug-address", "", "address to serve debug info")
|
||||
flag.StringVar(&cpuProfile, "cpu-profile", "", "path to save CPU profiling")
|
||||
flag.Parse()
|
||||
|
@ -68,7 +77,17 @@ func main() {
|
|||
defer pprof.StopCPUProfile()
|
||||
}
|
||||
|
||||
err := initTUI()
|
||||
err := readConfig(configPath)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to read configuration file: %s", err)
|
||||
}
|
||||
|
||||
err = setKeyBinds()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to set keybinds: %s", err)
|
||||
}
|
||||
|
||||
err = initTUI()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to initialize terminal user interface: %s", err)
|
||||
}
|
||||
|
@ -136,6 +155,10 @@ func main() {
|
|||
for {
|
||||
select {
|
||||
case <-done:
|
||||
if streamFd != nil {
|
||||
streamFd.Close()
|
||||
}
|
||||
|
||||
return
|
||||
case <-t.C:
|
||||
app.QueueUpdateDraw(updateStatus)
|
||||
|
|
Loading…
Reference in a new issue