~ghost08/libphoton

63773974c154585b780fce725ed7c2d0ff68ce7f — Vladimír Magyar 2 years ago fda3f05
Add keybindings and plugins
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
)