Merge remote-tracking branch 'upstream/master'

This commit is contained in:
tramhao 2021-02-21 15:16:20 +08:00
commit 1362c9925b
12 changed files with 394 additions and 125 deletions

View File

@ -101,8 +101,8 @@ Each panel has it's own additional keybinding. To view the available keybinding
### Scripting
Gomu uses [anko](github.com/mattn/anko) as its scripting language. You can read
more about scripting at our [wiki](github.com/issadarkthing/gomu/wiki)
Gomu uses [anko](https://github.com/mattn/anko) as its scripting language. You can read
more about scripting at our [wiki](https://github.com/issadarkthing/gomu/wiki)
``` go

View File

@ -17,7 +17,7 @@ type Anko struct {
env *env.Env
}
func NewAnko() Anko {
func NewAnko() *Anko {
env := core.Import(env.NewEnv())
importToX(env)
@ -45,7 +45,7 @@ func NewAnko() Anko {
panic(err)
}
return Anko{env}
return &Anko{env}
}
// Define defines new symbol and value to the Anko env.

View File

@ -1,10 +1,6 @@
package main
import (
"fmt"
"time"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
"github.com/ztrue/tracerr"
)
@ -66,98 +62,7 @@ func (c Command) defineCommands() {
})
c.define("youtube_search", func() {
popupId := "youtube-search-input-popup"
input := newInputPopup(popupId, " Youtube Search ", "search: ", "")
// quick hack to change the autocomplete text color
tview.Styles.PrimitiveBackgroundColor = tcell.ColorBlack
input.SetAutocompleteFunc(func(currentText string) (entries []string) {
if currentText == "" {
return []string{}
}
suggestions, err := getSuggestions(currentText)
if err != nil {
logError(err)
}
return suggestions
})
input.SetDoneFunc(func(key tcell.Key) {
switch key {
case tcell.KeyEnter:
search := input.GetText()
defaultTimedPopup(" Youtube Search ", "Searching for "+search)
gomu.pages.RemovePage(popupId)
gomu.popups.pop()
go func() {
results, err := getSearchResult(search)
if err != nil {
logError(err)
defaultTimedPopup(" Error ", err.Error())
return
}
titles := []string{}
urls := make(map[string]string)
for _, result := range results {
duration, err := time.ParseDuration(fmt.Sprintf("%ds", result.LengthSeconds))
if err != nil {
logError(err)
return
}
durationText := fmt.Sprintf("[ %s ] ", fmtDuration(duration))
title := durationText + result.Title
urls[title] = `https://www.youtube.com/watch?v=` + result.VideoId
titles = append(titles, title)
}
searchPopup("Youtube Videos", titles, func(title string) {
audioFile := gomu.playlist.getCurrentFile()
var dir *tview.TreeNode
if audioFile.isAudioFile {
dir = audioFile.parent
} else {
dir = audioFile.node
}
go func() {
url := urls[title]
if err := ytdl(url, dir); err != nil {
logError(err)
}
gomu.playlist.refresh()
}()
gomu.app.SetFocus(gomu.prevPanel.(tview.Primitive))
})
gomu.app.Draw()
}()
case tcell.KeyEscape:
gomu.pages.RemovePage(popupId)
gomu.popups.pop()
gomu.app.SetFocus(gomu.prevPanel.(tview.Primitive))
default:
input.Autocomplete()
}
})
ytSearchPopup()
})
c.define("download_audio", func() {

25
command_test.go Normal file
View File

@ -0,0 +1,25 @@
package main
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetFn(t *testing.T) {
c := newCommand()
c.define("sample", func() {})
f, err := c.getFn("sample")
if err != nil {
t.Error(err)
}
assert.NotNil(t, f)
f, err = c.getFn("x")
assert.Error(t, err)
}

View File

@ -1,9 +1,10 @@
package main
import (
"github.com/issadarkthing/gomu/anko"
"github.com/issadarkthing/gomu/hook"
"github.com/rivo/tview"
"github.com/ztrue/tracerr"
"github.com/issadarkthing/gomu/anko"
)
var VERSION = "N/A"
@ -24,7 +25,8 @@ type Gomu struct {
prevPanel Panel
panels []Panel
args Args
anko anko.Anko
anko *anko.Anko
hook *hook.EventHook
}
// Creates new instance of gomu with default values
@ -33,6 +35,7 @@ func newGomu() *Gomu {
gomu := &Gomu{
command: newCommand(),
anko: anko.NewAnko(),
hook: hook.NewEventHook(),
}
return gomu

35
hook/hook.go Normal file
View File

@ -0,0 +1,35 @@
package hook
type EventHook struct {
events map[string][]func()
}
// NewNewEventHook returns new instance of EventHook
func NewEventHook() *EventHook {
return &EventHook{make(map[string][]func())}
}
// AddHook accepts a function which will be executed when the event is emitted.
func (e *EventHook) AddHook(eventName string, handler func()) {
hooks, ok := e.events[eventName]
if !ok {
e.events[eventName] = []func(){handler}
return
}
e.events[eventName] = append(hooks, handler)
}
// RunHooks executes all hooks installed for an event.
func (e *EventHook) RunHooks(eventName string) {
hooks, ok := e.events[eventName]
if !ok {
return
}
for _, hook := range hooks {
hook()
}
}

57
hook/hook_test.go Normal file
View File

@ -0,0 +1,57 @@
package hook
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestAddHook(t *testing.T) {
h := NewEventHook()
h.AddHook("a", nil)
h.AddHook("a", nil)
h.AddHook("a", nil)
h.AddHook("a", nil)
assert.Equal(t, 1, len(h.events), "should only contain 1 event")
hooks := h.events["a"]
assert.Equal(t, 4, len(hooks), "should contain 4 hooks")
h.AddHook("b", nil)
h.AddHook("c", nil)
assert.Equal(t, 3, len(h.events), "should contain 3 events")
}
func TestRunHooks(t *testing.T) {
h := NewEventHook()
x := 0
for i := 0; i < 100; i ++ {
h.AddHook("sample", func() {
x++
})
}
h.AddHook("noop", func() {
x++
})
h.AddHook("noop", func() {
x++
})
assert.Equal(t, x, 0, "should not execute any hook")
h.RunHooks("x")
assert.Equal(t, x, 0, "should not execute any hook")
h.RunHooks("sample")
assert.Equal(t, x, 100, "should only execute event 'sample'")
}

View File

@ -121,7 +121,10 @@ func (p *Player) run(currSong *AudioFile) error {
// sets the volume of previous player
volume.Volume += p.volume
p._volume = volume
// starts playing the audio
speaker.Play(p._volume)
gomu.hook.RunHooks("new_song")
p.isRunning = true
@ -157,6 +160,7 @@ next:
// when there are no songs to be played, set currentSong as nil
p.currentSong = nil
gomu.playingBar.setDefault()
gomu.app.Draw()
break next
}
@ -190,6 +194,7 @@ next:
}
func (p *Player) pause() {
gomu.hook.RunHooks("pause")
speaker.Lock()
p.ctrl.Paused = true
p.isRunning = false
@ -197,6 +202,7 @@ func (p *Player) pause() {
}
func (p *Player) play() {
gomu.hook.RunHooks("play")
speaker.Lock()
p.ctrl.Paused = false
p.isRunning = true
@ -235,7 +241,9 @@ func (p *Player) togglePause() {
// skips current song
func (p *Player) skip() {
if gomu.queue.GetItemCount() < 1 {
gomu.hook.RunHooks("skip")
if p.currentSong == nil {
return
}

View File

@ -38,14 +38,6 @@ func newPlayingBar() *PlayingBar {
progress: make(chan int),
}
textView.SetChangedFunc(func() {
gomu.app.Draw()
if !gomu.player.isRunning {
p.setDefault()
}
})
return p
}
@ -84,6 +76,7 @@ func (p *PlayingBar) run() error {
progressBar,
fmtDuration(end),
))
gomu.app.Draw()
}

139
popup.go
View File

@ -8,6 +8,7 @@ import (
"path/filepath"
"regexp"
"strings"
"sync"
"time"
"unicode/utf8"
@ -53,6 +54,8 @@ func (s *Stack) pop() tview.Primitive {
}
last := s.popups[len(s.popups)-1]
s.popups[len(s.popups)-1] = nil // avoid memory leak
res := s.popups[:len(s.popups)-1]
s.popups = res
@ -611,16 +614,20 @@ func replPopup() {
switch event.Key() {
case tcell.KeyUp:
input.SetText(history[upCount])
if upCount < len(history)-1 {
if upCount < len(history) {
input.SetText(history[upCount])
upCount++
}
case tcell.KeyDown:
if upCount > 0 {
upCount--
if upCount == len(history) {
upCount -= 2
} else {
upCount -= 1
}
input.SetText(history[upCount])
} else if upCount == 0 {
input.SetText("")
@ -642,15 +649,20 @@ func replPopup() {
input.SetText("")
defer func() {
if err := recover(); err != nil {
fmt.Fprintf(textview, "%s%s\n%v\n\n", prompt, text, err)
}
}()
res, err := gomu.anko.Execute(text)
if err != nil {
fmt.Fprintf(textview, "%s%s\n%v\n\n", prompt, text, err)
return nil
}
if res != nil {
} else {
fmt.Fprintf(textview, "%s%s\n%v\n\n", prompt, text, res)
}
}
return event
@ -671,6 +683,121 @@ func replPopup() {
gomu.popups.push(flex)
}
func ytSearchPopup() {
popupId := "youtube-search-input-popup"
input := newInputPopup(popupId, " Youtube Search ", "search: ", "")
var mutex sync.Mutex
prefixMap := make(map[string][]string)
// quick hack to change the autocomplete text color
tview.Styles.PrimitiveBackgroundColor = tcell.ColorBlack
input.SetAutocompleteFunc(func(currentText string) []string {
// Ignore empty text.
prefix := strings.TrimSpace(strings.ToLower(currentText))
if prefix == "" {
return nil
}
mutex.Lock()
defer mutex.Unlock()
entries, ok := prefixMap[prefix]
if ok {
return entries
}
go func() {
suggestions, err := getSuggestions(currentText)
if err != nil {
logError(err)
return
}
mutex.Lock()
prefixMap[prefix] = suggestions
mutex.Unlock()
input.Autocomplete()
gomu.app.Draw()
}()
return nil
})
input.SetDoneFunc(func(key tcell.Key) {
switch key {
case tcell.KeyEnter:
search := input.GetText()
defaultTimedPopup(" Youtube Search ", "Searching for "+search)
gomu.pages.RemovePage(popupId)
gomu.popups.pop()
go func() {
results, err := getSearchResult(search)
if err != nil {
logError(err)
defaultTimedPopup(" Error ", err.Error())
return
}
titles := []string{}
urls := make(map[string]string)
for _, result := range results {
duration, err := time.ParseDuration(fmt.Sprintf("%ds", result.LengthSeconds))
if err != nil {
logError(err)
return
}
durationText := fmt.Sprintf("[ %s ] ", fmtDuration(duration))
title := durationText + result.Title
urls[title] = `https://www.youtube.com/watch?v=` + result.VideoId
titles = append(titles, title)
}
searchPopup("Youtube Videos", titles, func(title string) {
audioFile := gomu.playlist.getCurrentFile()
var dir *tview.TreeNode
if audioFile.isAudioFile {
dir = audioFile.parent
} else {
dir = audioFile.node
}
go func() {
url := urls[title]
if err := ytdl(url, dir); err != nil {
logError(err)
}
gomu.playlist.refresh()
}()
gomu.app.SetFocus(gomu.prevPanel.(tview.Primitive))
})
gomu.app.Draw()
}()
case tcell.KeyEscape:
gomu.pages.RemovePage(popupId)
gomu.popups.pop()
gomu.app.SetFocus(gomu.prevPanel.(tview.Primitive))
}
})
}
func tagPopup(node *AudioFile) bool {
var tag *id3v2.Tag
var err error

View File

@ -13,6 +13,8 @@ import (
"syscall"
"github.com/gdamore/tcell/v2"
"github.com/issadarkthing/gomu/anko"
"github.com/issadarkthing/gomu/hook"
"github.com/rivo/tview"
"github.com/ztrue/tracerr"
)
@ -62,9 +64,33 @@ func defineBuiltins() {
gomu.anko.Define("shell", shell)
}
// execInit executes helper modules and default config that should only be
func setupHooks(hook *hook.EventHook, anko *anko.Anko) {
events := []string{
"enter",
"new_song",
"skip",
"play",
"pause",
"exit",
}
for _, event := range events {
name := event
hook.AddHook(name, func() {
src := fmt.Sprintf(`Event.run_hooks("%s")`, name)
_, err := anko.Execute(src)
if err != nil {
err = tracerr.Errorf("error execute hook: %w", err)
logError(err)
}
})
}
}
// loadModules executes helper modules and default config that should only be
// executed once
func execInit() error {
func loadModules(env *anko.Anko) error {
const listModule = `
module List {
@ -94,6 +120,35 @@ module List {
return acc
}
}
`
const eventModule = `
module Event {
events = {}
func add_hook(name, f) {
hooks = events[name]
if hooks == nil {
events[name] = [f]
return
}
hooks += f
events[name] = hooks
}
func run_hooks(name) {
hooks = events[name]
if hooks == nil {
return
}
for hook in hooks {
hook()
}
}
}
`
const keybindModule = `
@ -115,8 +170,7 @@ module Keybinds {
}
}
`
_, err := gomu.anko.Execute(listModule + keybindModule)
_, err := env.Execute(eventModule + listModule + keybindModule)
if err != nil {
return tracerr.Wrap(err)
}
@ -231,7 +285,8 @@ func start(application *tview.Application, args Args) {
gomu = newGomu()
gomu.command.defineCommands()
defineBuiltins()
err := execInit()
err := loadModules(gomu.anko)
if err != nil {
die(err)
}
@ -241,6 +296,9 @@ func start(application *tview.Application, args Args) {
die(err)
}
setupHooks(gomu.hook, gomu.anko)
gomu.hook.RunHooks("enter")
gomu.args = args
gomu.colors = newColor()
@ -357,15 +415,26 @@ func start(application *tview.Application, args Args) {
})
// fix transparent background issue
application.SetBeforeDrawFunc(func(screen tcell.Screen) bool {
gomu.app.SetBeforeDrawFunc(func(screen tcell.Screen) bool {
screen.Clear()
return false
})
init := false
gomu.app.SetAfterDrawFunc(func(_ tcell.Screen) {
if !init && !gomu.player.isRunning {
gomu.playingBar.setDefault()
init = true
}
})
go populateAudioLength(gomu.playlist.GetRoot())
gomu.app.SetRoot(gomu.pages, true).SetFocus(gomu.playlist)
// main loop
if err := application.SetRoot(gomu.pages, true).SetFocus(gomu.playlist).Run(); err != nil {
logError(err)
if err := gomu.app.Run(); err != nil {
die(err)
}
gomu.hook.RunHooks("exit")
}

47
start_test.go Normal file
View File

@ -0,0 +1,47 @@
package main
import (
"testing"
"github.com/issadarkthing/gomu/anko"
"github.com/issadarkthing/gomu/hook"
"github.com/stretchr/testify/assert"
)
func TestSetupHooks(t *testing.T) {
gomu := newGomu()
gomu.anko = anko.NewAnko()
gomu.hook = hook.NewEventHook()
err := loadModules(gomu.anko)
if err != nil {
t.Error(err)
}
setupHooks(gomu.hook, gomu.anko)
const src = `
i = 0
Event.add_hook("skip", func() {
i++
})
`
_, err = gomu.anko.Execute(src)
if err != nil {
t.Error(err)
}
gomu.hook.RunHooks("enter")
for i := 0; i < 12; i++ {
gomu.hook.RunHooks("skip")
}
got := gomu.anko.GetInt("i")
assert.Equal(t, 12, got)
}