From 7b7005c0681021083c25c9ff29233257761ab068 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sun, 30 Apr 2023 12:50:23 -0400 Subject: [PATCH] Re-implemented removed functionality in Nasin We also have a plugin system now :3 --- nasin/application.go | 79 ++++++++++++++++++++++++++++ nasin/doc.go | 3 ++ nasin/plugin.go | 119 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 201 insertions(+) create mode 100644 nasin/application.go create mode 100644 nasin/doc.go create mode 100644 nasin/plugin.go diff --git a/nasin/application.go b/nasin/application.go new file mode 100644 index 0000000..b1a7111 --- /dev/null +++ b/nasin/application.go @@ -0,0 +1,79 @@ +package nasin + +import "image" +import "errors" +import "git.tebibyte.media/sashakoshka/tomo" + +// Application is +type Application interface { + Name () string + Init () error +} + +// Run runs the given application. This function will block until the +// application exits. +func Run (application Application) { + loadPlugins() + + backend, err := instantiateBackend() + if err != nil { + println("nasin: cannot start application:", err.Error()) + return + } + tomo.SetBackend(backend) + + if application == nil { panic("nasin: nil application") } + err = application.Init() + if err != nil { + println("nasin: backend exited with error:", err.Error()) + return + } + + err = backend.Run() + if err != nil { + println("nasin: backend exited with error:", err.Error()) + return + } + return +} + +// Stop stops the event loop +func Stop () { + assertBackend() + tomo.GetBackend().Stop() +} + +// Do executes the specified callback within the main thread as soon as +// possible. +func Do (callback func()) { + assertBackend() + tomo.GetBackend().Do(callback) +} + +// NewWindow creates a new window within the specified bounding rectangle. The +// position on screen may be overridden by the backend or operating system. +func NewWindow (bounds image.Rectangle) (tomo.MainWindow, error) { + assertBackend() + return tomo.GetBackend().NewWindow(bounds) +} + +func assertBackend () { + if tomo.GetBackend() == nil { + panic("nasin: no running tomo backend") + } +} + +func instantiateBackend () (backend tomo.Backend, err error) { + // find a suitable backend + for _, factory := range factories { + backend, err = factory() + if err == nil && backend != nil { return } + } + + // if none were found, but there was no error produced, produce an error + if err == nil { + return nil, errors.New("no available tomo backends") + } + + return +} diff --git a/nasin/doc.go b/nasin/doc.go new file mode 100644 index 0000000..caceae0 --- /dev/null +++ b/nasin/doc.go @@ -0,0 +1,3 @@ +// Package nasin provides a high-level framework for Tomo applications. Nasin +// also automatically handles themes, backend instantiation, and plugin support. +package nasin diff --git a/nasin/plugin.go b/nasin/plugin.go new file mode 100644 index 0000000..d75f2ad --- /dev/null +++ b/nasin/plugin.go @@ -0,0 +1,119 @@ +package nasin + +import "os" +import "fmt" +// TODO: possibly fork the official plugin module and add support for other +// operating systems? perhaps enhance the Lookup function with +// the generic extract function we have here for extra type safety goodness. +import "plugin" +import "strings" +import "path/filepath" +import "git.tebibyte.media/sashakoshka/tomo" + +type expectsFunc func () (int, int, int) +type nameFunc func () string +type descriptionFunc func () string +type backendFactory func () (tomo.Backend, error) +type themeFactory func () tomo.Theme + +var factories []backendFactory +var theme tomo.Theme + +func loadPlugins () { + // TODO: do not hardcode all these paths here, have separate files that + // build on different platforms that set these paths. + + pathVariable := os.Getenv("NASIN_PLUGIN_PATH") + paths := strings.Split(pathVariable, ":") + paths = append ( + paths, + "/usr/lib/nasin/plugins", + "/usr/local/lib/nasin/plugins") + homeDir, err := os.UserHomeDir() + if err != nil { + paths = append ( + paths, + filepath.Join(homeDir, ".local/lib/nasin/plugins")) + } + + for _, dir := range paths { + loadPluginsFrom(dir) + } +} + +func loadPluginsFrom (dir string) { + entries, err := os.ReadDir(dir) + // its no big deal if one of the dirs doesn't exist + if err != nil { return } + + for _, entry := range entries { + if entry.IsDir() { continue } + if filepath.Ext(entry.Name()) != ".so" { continue } + pluginPath := filepath.Join(dir, entry.Name()) + loadPlugin(pluginPath) + } +} + +func loadPlugin (path string) { + die := func (reason string) { + println ( + "nasin: could not load plugin at ", + path + ":", reason) + } + + plugin, err := plugin.Open(path) + if err != nil { + die(err.Error()) + return + } + + // check for and obtain basic plugin functions + expects, ok := extract[expectsFunc](plugin, "Expects") + if !ok { die("does not implement Expects() (int, int, int)"); return } + name, ok := extract[nameFunc](plugin, "Name") + if !ok { die("does not implement Name() string"); return } + _, ok = extract[descriptionFunc](plugin, "Description") + if !ok { die("does not implement Description() string"); return } + + // check for version compatibility + // TODO: have exported version type in tomo base package, and have a + // function within that that gives the current tomo/nasin version. call + // that here. + major, minor, patch := expects() + currentVersion := version { 0, 0, 0 } + pluginVersion := version { major, minor, patch } + if !pluginVersion.CompatibleABI(currentVersion) { + die ( + "plugin (" + pluginVersion.String() + + ") incompatible with nasin/tomo version (" + + currentVersion.String() + ")") + return + } + + // if it's a backend plugin... + newBackend, ok := extract[backendFactory](plugin, "NewBackend") + if ok { factories = append(factories, newBackend) } + + // if it's a theme plugin... + newTheme, ok := extract[themeFactory](plugin, "NewTheme") + if ok { theme = newTheme() } + + println("nasin: loaded plugin", name()) +} + +func extract[T any] (plugin *plugin.Plugin, name string) (value T, ok bool) { + symbol, err := plugin.Lookup(name) + if err != nil { return } + value, ok = symbol.(T) + return +} + +type version [3]int + +func (version version) CompatibleABI (other version) bool { + return version[0] == other[0] && version[1] == other[1] +} + +func (version version) String () string { + return fmt.Sprint(version[0], ".", version[1], ".", version[2]) +}