A keybindings/keybindings.go => keybindings/keybindings.go +151 -0
@@ 0,0 1,151 @@
+package keybindings
+
+import (
+ "fmt"
+ "log"
+ "strings"
+ "unicode"
+ "unicode/utf8"
+
+ "git.sr.ht/~ghost08/libphoton/states"
+)
+
+type Callback func() error
+
+type KeyEvent struct {
+ Key rune
+ Modifiers Modifiers
+}
+
+func (s KeyEvent) String() string {
+ return s.Modifiers.String() + string(s.Key)
+}
+
+type KeyEvents []KeyEvent
+
+func (ss KeyEvents) String() string {
+ var idents string
+ for _, s := range ss {
+ idents += s.String()
+ }
+ return idents
+}
+
+func parseState(s string) (string, KeyEvent, error) {
+ ns, mod := modPrefix(s)
+ if len(ns) == 0 {
+ return "", KeyEvent{}, fmt.Errorf("not valid keybinding string: %s", s)
+ }
+ r, size := utf8.DecodeRuneInString(ns)
+ r = unicode.ToLower(r)
+ return ns[size:], KeyEvent{Key: r, Modifiers: mod}, nil
+}
+
+func modPrefix(s string) (string, Modifiers) {
+ switch {
+ case strings.HasPrefix(s, "<ctrl>"):
+ return s[6:], ModCtrl
+ case strings.HasPrefix(s, "<command>"):
+ return s[9:], ModCommand
+ case strings.HasPrefix(s, "<shift>"):
+ return s[7:], ModShift
+ case strings.HasPrefix(s, "<alt>"):
+ return s[5:], ModAlt
+ case strings.HasPrefix(s, "<super>"):
+ return s[7:], ModSuper
+ default:
+ return s, 0
+ }
+}
+
+func parseStates(s string) (KeyEvents, error) {
+ var ss KeyEvents
+ for {
+ if len(s) == 0 {
+ break
+ }
+ var err error
+ var state KeyEvent
+ s, state, err = parseState(s)
+ if err != nil {
+ return nil, err
+ }
+ ss = append(ss, state)
+ }
+ return ss, nil
+}
+
+type Registry struct {
+ currentLayout CurrentLayout
+ reg map[states.StateEnum]map[string]Callback
+ currentState KeyEvents
+ repeat int
+}
+
+//Function type that returns the layout that is currently used
+type CurrentLayout func() states.StateEnum
+
+func New(cl CurrentLayout) *Registry {
+ return &Registry{
+ reg: make(map[states.StateEnum]map[string]Callback),
+ currentLayout: cl,
+ }
+}
+
+func (kbr *Registry) Add(layout states.StateEnum, keyString string, callback Callback) {
+ if _, ok := kbr.reg[layout]; !ok {
+ kbr.reg[layout] = make(map[string]Callback)
+ }
+ ss, err := parseStates(keyString)
+ if err != nil {
+ log.Printf("ERROR: parsing keybinding string (%s): %s", keyString, err)
+ return
+ }
+ kbr.reg[layout][ss.String()] = callback
+}
+
+func (kbr *Registry) Run(e KeyEvent) {
+ cl := kbr.currentLayout()
+ reg, ok := kbr.reg[cl]
+ if !ok {
+ return
+ }
+ if e.Modifiers == 0 && unicode.IsDigit(e.Key) && len(kbr.currentState) == 0 {
+ kbr.repeat = kbr.repeat*10 + (int(e.Key) - '0')
+ }
+ kbr.currentState = append(kbr.currentState, e)
+ ident := kbr.currentState.String()
+ var hasPrefix bool
+ var callback Callback
+ for k, c := range reg {
+ if !strings.HasPrefix(k, ident) {
+ continue
+ }
+ hasPrefix = true
+ if len(ident) == len(k) {
+ callback = c
+ break
+ }
+ }
+ if !hasPrefix {
+ kbr.currentState = nil
+ return
+ }
+ if callback == nil {
+ return
+ }
+ kbr.currentState = nil
+ if kbr.repeat == 0 {
+ if err := callback(); err != nil {
+ log.Println("ERROR:", err)
+ }
+ return
+ }
+ for i := 0; i < kbr.repeat; i++ {
+ if err := callback(); err != nil {
+ log.Println("ERROR:", err)
+ break
+ }
+ }
+ kbr.repeat = 0
+}
A keybindings/keybindings_lua.go => keybindings/keybindings_lua.go +31 -0
@@ 0,0 1,31 @@
+package keybindings
+
+import (
+ lua "github.com/yuin/gopher-lua"
+ "git.sr.ht/~ghost08/photon/internal/states"
+)
+
+func Loader(kb *Registry) lua.LGFunction {
+ return func(L *lua.LState) int {
+ var exports = map[string]lua.LGFunction{
+ "add": keybindingsAdd(kb),
+ }
+ mod := L.SetFuncs(L.NewTable(), exports)
+ L.Push(mod)
+
+ return 1
+ }
+}
+
+func keybindingsAdd(kb *Registry) lua.LGFunction {
+ return func(L *lua.LState) int {
+ state := states.StateEnum(L.ToNumber(1))
+ keyString := L.ToString(2)
+ fn := L.ToFunction(3)
+ kb.Add(state, keyString, func() error {
+ L.Push(fn)
+ return L.PCall(0, lua.MultRet, nil)
+ })
+ return 0
+ }
+}
A keybindings/modifiers.go => keybindings/modifiers.go +48 -0
@@ 0,0 1,48 @@
+package keybindings
+
+import "strings"
+
+// Modifiers
+type Modifiers uint32
+
+const (
+ // ModCtrl is the ctrl modifier key.
+ ModCtrl Modifiers = 1 << iota
+ // ModCommand is the command modifier key
+ // found on Apple keyboards.
+ ModCommand
+ // ModShift is the shift modifier key.
+ ModShift
+ // ModAlt is the alt modifier key, or the option
+ // key on Apple keyboards.
+ ModAlt
+ // ModSuper is the "logo" modifier key, often
+ // represented by a Windows logo.
+ ModSuper
+)
+
+// Contain reports whether m contains all modifiers
+// in m2.
+func (m Modifiers) Contain(m2 Modifiers) bool {
+ return m&m2 == m2
+}
+
+func (m Modifiers) String() string {
+ var strs []string
+ if m.Contain(ModCtrl) {
+ strs = append(strs, "ModCtrl")
+ }
+ if m.Contain(ModCommand) {
+ strs = append(strs, "ModCommand")
+ }
+ if m.Contain(ModShift) {
+ strs = append(strs, "ModShift")
+ }
+ if m.Contain(ModAlt) {
+ strs = append(strs, "ModAlt")
+ }
+ if m.Contain(ModSuper) {
+ strs = append(strs, "ModSuper")
+ }
+ return strings.Join(strs, "|")
+}
M libphoton.go => libphoton.go +3 -1
@@ 12,6 12,7 @@ import (
"git.sr.ht/~ghost08/libphoton/events"
"git.sr.ht/~ghost08/libphoton/inputs"
+ "git.sr.ht/~ghost08/libphoton/keybindings"
"git.sr.ht/~ghost08/libphoton/media"
"github.com/mmcdole/gofeed"
)
@@ 21,6 22,7 @@ type Photon struct {
imgDownloader *ImgDownloader
mediaExtractor *media.Extractor
httpClient *http.Client
+ KeyBindings *keybindings.Registry
downloadPath string
redraw func()
@@ 31,7 33,7 @@ type Photon struct {
OpenedArticle *Article
}
-func New(redraw func(), paths []string, options ...Option) (*Photon, error) {
+func New(redraw func(), state states.Func, paths []string, options ...Option) (*Photon, error) {
p := &Photon{}
feedInputs, err := p.loadFeeds(paths)
if err != nil {
A plugins.go => plugins.go +130 -0
@@ 0,0 1,130 @@
+package libphoton
+
+import (
+ "fmt"
+ "os"
+ "os/user"
+ "path/filepath"
+ "strings"
+
+ "git.sr.ht/~ghost08/libphoton/events"
+ "git.sr.ht/~ghost08/libphoton/inputs"
+ "git.sr.ht/~ghost08/libphoton/keybindings"
+ "git.sr.ht/~ghost08/libphoton/media"
+ lua "github.com/yuin/gopher-lua"
+)
+
+var luaState *lua.LState
+
+func (p *Photon) LoadPlugins() error {
+ plugins, err := findPlugins()
+ if err != nil {
+ return fmt.Errorf("finding plugins: %w", err)
+ }
+ if len(plugins) == 0 {
+ return nil
+ }
+ p.initLuaState()
+ for _, pluginPath := range plugins {
+ if err := luaState.DoFile(pluginPath); err != nil {
+ return fmt.Errorf("loading plugin: %s\n%s", pluginPath, err)
+ }
+ }
+ return nil
+}
+
+func findPlugins() ([]string, error) {
+ usr, err := user.Current()
+ if err != nil {
+ return nil, err
+ }
+ pluginsDir := filepath.Join(usr.HomeDir, ".config", "photon", "plugins")
+ if _, err := os.Stat(pluginsDir); os.IsNotExist(err) {
+ return nil, nil
+ }
+ des, err := os.ReadDir(pluginsDir)
+ if err != nil {
+ return nil, err
+ }
+ var plugins []string
+ for _, de := range des {
+ if de.IsDir() || !strings.HasSuffix(de.Name(), ".lua") {
+ continue
+ }
+ plugins = append(plugins, filepath.Join(pluginsDir, de.Name()))
+ }
+ return plugins, nil
+}
+
+func (p *Photon) initLuaState() {
+ luaState = lua.NewState()
+ media.Loader(luaState)
+ cardsLoader(luaState)
+ luaState.PreloadModule("photon", photonLoader)
+ luaState.PreloadModule("photon.events", events.Loader)
+ luaState.PreloadModule("photon.feedInputs", inputs.Loader(&p.feedInputs))
+ luaState.PreloadModule("photon.keybindings", keybindings.Loader(p.keyBindings))
+}
+
+func photonLoader(L *lua.LState) int {
+ var exports = map[string]lua.LGFunction{
+ "state": photonState,
+ }
+ mod := L.SetFuncs(L.NewTable(), exports)
+ registerTypeSelectedCard(L)
+ L.SetField(mod, "Normal", lua.LNumber(states.Normal))
+ L.SetField(mod, "Article", lua.LNumber(states.Article))
+ L.SetField(mod, "Search", lua.LNumber(states.Search))
+ L.SetField(mod, "cards", newCards(&cards, L))
+ L.SetField(mod, "visibleCards", newCards(&visibleCards, L))
+ L.SetField(mod, "selectedCard", newSelectedCard(L))
+ L.Push(mod)
+
+ return 1
+}
+
+func photonState(L *lua.LState) int {
+ L.Push(lua.LNumber(State()))
+ return 1
+}
+
+const luaSelectedCardTypeName = "photon.selectedCardType"
+
+func registerTypeSelectedCard(L *lua.LState) {
+ var selectedCardMethods = map[string]lua.LGFunction{
+ "moveRight": func(L *lua.LState) int { selectedCard.MoveRight(); return 0 },
+ "moveLeft": func(L *lua.LState) int { selectedCard.MoveLeft(); return 0 },
+ "moveUp": func(L *lua.LState) int { selectedCard.MoveUp(); return 0 },
+ "moveDown": func(L *lua.LState) int { selectedCard.MoveDown(); return 0 },
+ "posX": func(L *lua.LState) int {
+ if L.GetTop() == 2 {
+ selectedCard.Pos.X = L.CheckInt(2)
+ return 0
+ }
+ L.Push(lua.LNumber(selectedCard.Pos.X))
+ return 1
+ },
+ "posY": func(L *lua.LState) int {
+ if L.GetTop() == 2 {
+ selectedCard.Pos.Y = L.CheckInt(2)
+ return 0
+ }
+ L.Push(lua.LNumber(selectedCard.Pos.X))
+ return 1
+ },
+ "card": func(L *lua.LState) int {
+ L.Push(newCard(selectedCard.Card, L))
+ return 1
+ },
+ }
+ newClass := L.SetFuncs(L.NewTable(), selectedCardMethods)
+ mt := L.NewTypeMetatable(luaSelectedCardTypeName)
+ L.SetField(mt, "__index", newClass)
+}
+
+func newSelectedCard(L *lua.LState) *lua.LUserData {
+ ud := L.NewUserData()
+ ud.Value = selectedCard
+ L.SetMetatable(ud, L.GetTypeMetatable(luaSelectedCardTypeName))
+ return ud
+}
A plugins/autoplay.lua => plugins/autoplay.lua +25 -0
@@ 0,0 1,25 @@
+--this plugins plays the media on the selected cars on <ctrl>p
+--when the player ended, moves the selected card to the right
+--so user can type 10<ctrl>p and play the next 10 items
+photon = require("photon")
+events = require("photon.events")
+keybindings = require("photon.keybindings")
+
+run = 0
+
+keybindings.add(photon.NormalState, "<ctrl>p", function()
+ if run == 0 then
+ photon.selectedCard.runMedia()
+ end
+ run = run + 1
+end)
+
+events.subscribe(events.RunMediaEnd, function(e)
+ if run > 1 then
+ photon.selectedCard.moveRight()
+ photon.selectedCard.runMedia()
+ run = run - 1
+ elseif run == 1 then
+ run = 0
+ end
+end)
A plugins/cards.lua => plugins/cards.lua +7 -0
@@ 0,0 1,7 @@
+photon = require("photon")
+events = require("photon.events")
+
+events.subscribe(events.FeedsDownloaded, function(e)
+ print(photon.selectedCard:moveRight())
+ print(photon.selectedCard:card():link())
+end)
A plugins/save_media_links.lua => plugins/save_media_links.lua +14 -0
@@ 0,0 1,14 @@
+--this plugin gets the media of the selectedCard and saves the links to a text file
+keybindings = require("photon.keybindings")
+
+keybindings.add(photon.NormalState, "dl", function()
+ local media, err = photon.selectedCard.getMedia()
+ if err ~= nil then
+ error(err)
+ end
+ local f = io.open("/tmp/links.txt", "w")
+ for _, link in ipairs(media.links) do
+ f:write(link)
+ end
+ f:close()
+end)
A plugins/translate-url.lua => plugins/translate-url.lua +52 -0
@@ 0,0 1,52 @@
+--translate-url is a extension that translates known input urls to rss urls
+--now it works on: youtube channels, subreddits, odysee channels
+events = require("photon.events")
+feedInputs = require("photon.feedInputs")
+
+events.subscribe(events.Init, function(e)
+ for i = 1, feedInputs.len(), 1 do
+ feed = feedInputs.get(i)
+ translator = match(feed)
+ if translator ~= nil then
+ feedInputs.set(i, translator(feed))
+ end
+ end
+end)
+
+function match(feed)
+ if feed:match("https://www.youtube.com/channel/.*") ~= nil then
+ return ytchannelTranslator
+ end
+ if feed:match("https://www.youtube.com/user/.*") ~= nil then
+ return ytuserTranslator
+ end
+ if feed:match("https://www.reddit.com/r/.*") ~= nil then
+ return redditTranslator
+ end
+ if feed:match("https://odysee.com/.*") ~= nil then
+ return odyseeTranslator
+ end
+ return nil
+end
+
+function ytchannelTranslator(feed)
+ channelID = feed:match("https://www.youtube.com/channel/(.*)")
+ return "https://www.youtube.com/feeds/videos.xml?channel_id=" .. channelID
+end
+
+function ytuserTranslator(feed)
+ username = feed:match("https://www.youtube.com/user/(.*)")
+ return "https://www.youtube.com/feeds/videos.xml?user=" .. username
+end
+
+function redditTranslator(feed)
+ if feed:sub(#feed-3) == ".rss" then
+ return feed
+ end
+ return feed .. ".rss"
+end
+
+function odyseeTranslator(feed)
+ channelID = feed:match("https://odysee.com/(.*)")
+ return "https://lbryfeed.melroy.org/channel/odysee/" .. channelID
+end
A states/states.go => states/states.go +9 -0
@@ 0,0 1,9 @@
+package states
+
+type StateEnum int
+
+const (
+ Normal StateEnum = iota
+ Article
+ Search
+)