From 462b2290c82f9956d43273592aebfc3f6696bfce Mon Sep 17 00:00:00 2001 From: Caleb Foust Date: Fri, 10 Nov 2023 06:41:39 -0500 Subject: [PATCH] feat: add support for parameters --- pkg/cy/api/cmd.go | 14 ++++++--- pkg/cy/cy-boot.janet | 2 +- pkg/cy/defaults.go | 19 ++++++++++++ pkg/cy/janet.go | 58 +++++++++++++++++++++++++++++++++++- pkg/cy/module.go | 15 +++++++++- pkg/cy/params/constants.go | 6 ++++ pkg/janet/translate.go | 14 ++++++++- pkg/mux/screen/tree/group.go | 2 ++ pkg/mux/screen/tree/node.go | 13 ++++++-- pkg/mux/screen/tree/tree.go | 15 +++++++++- pkg/params/module.go | 53 ++++++++++++++++++++++++++++++++ 11 files changed, 199 insertions(+), 12 deletions(-) create mode 100644 pkg/cy/defaults.go create mode 100644 pkg/cy/params/constants.go create mode 100644 pkg/params/module.go diff --git a/pkg/cy/api/cmd.go b/pkg/cy/api/cmd.go index 646ef00b..cca5e43c 100644 --- a/pkg/cy/api/cmd.go +++ b/pkg/cy/api/cmd.go @@ -5,6 +5,7 @@ import ( "github.com/cfoust/cy/pkg/bind" "github.com/cfoust/cy/pkg/cy/cmd" + "github.com/cfoust/cy/pkg/cy/params" "github.com/cfoust/cy/pkg/janet" "github.com/cfoust/cy/pkg/mux/screen/replayable" "github.com/cfoust/cy/pkg/mux/screen/tree" @@ -21,16 +22,15 @@ type CmdParams struct { type Cmd struct { Lifetime util.Lifetime Tree *tree.Tree - DataDir string replayBinds *bind.BindScope } func (c *Cmd) New( groupId tree.NodeID, path string, - params *janet.Named[CmdParams], + cmdParams *janet.Named[CmdParams], ) (tree.NodeID, error) { - values := params.WithDefault(CmdParams{ + values := cmdParams.WithDefault(CmdParams{ Command: "/bin/bash", }) @@ -39,6 +39,12 @@ func (c *Cmd) New( return 0, fmt.Errorf("node not found: %d", groupId) } + param, _ := group.Params().Get(params.ParamDataDirectory) + dataDir, ok := param.(string) + if !ok { + return 0, fmt.Errorf("param %s was not a string", params.ParamDataDirectory) + } + replayable, err := cmd.New( c.Lifetime.Ctx(), stream.CmdOptions{ @@ -46,7 +52,7 @@ func (c *Cmd) New( Args: values.Args, Directory: path, }, - c.DataDir, + dataDir, c.replayBinds, ) if err != nil { diff --git a/pkg/cy/cy-boot.janet b/pkg/cy/cy-boot.janet index 1cc8a3d3..a6764786 100644 --- a/pkg/cy/cy-boot.janet +++ b/pkg/cy/cy-boot.janet @@ -132,7 +132,7 @@ cy/open-log "open an existing log file" (-?>> - (path/glob "/Users/cfoust/.local/share/cy/*.borg") + (path/glob (path/join [(cy/get :data-dir) "*.borg"])) (map |(tuple $ [:replay [$]] $)) (fzf/find) (replay/open (tree/root)) diff --git a/pkg/cy/defaults.go b/pkg/cy/defaults.go new file mode 100644 index 00000000..0f3ed2b7 --- /dev/null +++ b/pkg/cy/defaults.go @@ -0,0 +1,19 @@ +package cy + +import ( + "github.com/cfoust/cy/pkg/cy/params" +) + +func (c *Cy) setDefaults(options Options) error { + defaults := map[string]interface{}{ + params.ParamDataDirectory: options.DataDir, + } + + for key, value := range defaults { + err := c.defaults.Set(key, value) + if err != nil { + return err + } + } + return nil +} diff --git a/pkg/cy/janet.go b/pkg/cy/janet.go index fcb4e8a3..e7a9e982 100644 --- a/pkg/cy/janet.go +++ b/pkg/cy/janet.go @@ -87,7 +87,6 @@ func (c *Cy) initJanet(ctx context.Context, dataDir string) (*janet.VM, error) { modules := map[string]interface{}{ "cmd": &api.Cmd{ Lifetime: util.NewLifetime(c.Ctx()), - DataDir: dataDir, Tree: c.tree, }, "group": &api.GroupModule{Tree: c.tree}, @@ -120,6 +119,63 @@ func (c *Cy) initJanet(ctx context.Context, dataDir string) (*janet.VM, error) { client.Detach("detached") }, + "cy/get": func(user interface{}, key *janet.Value) (interface{}, error) { + defer key.Free() + + client, ok := user.(*Client) + if !ok { + return nil, fmt.Errorf("missing client context") + } + + node := client.Node() + if node == nil { + return nil, fmt.Errorf("client was not attached") + } + + var keyword janet.Keyword + err := key.Unmarshal(&keyword) + if err != nil { + return nil, err + } + + value, ok := node.Params().Get(string(keyword)) + return value, nil + }, + "cy/set": func(user interface{}, key *janet.Value, value *janet.Value) error { + defer key.Free() + + client, ok := user.(*Client) + if !ok { + return fmt.Errorf("missing client context") + } + + node := client.Node() + if node == nil { + return fmt.Errorf("client was not attached") + } + + var keyword janet.Keyword + err := key.Unmarshal(&keyword) + if err != nil { + return err + } + + var str string + err = value.Unmarshal(&str) + if err == nil { + node.Params().Set(string(keyword), str) + return nil + } + + var _int int + err = value.Unmarshal(&_int) + if err == nil { + node.Params().Set(string(keyword), _int) + return nil + } + + return fmt.Errorf("parameter type not supported") + }, "cy/replay": func(user interface{}) { client, ok := user.(*Client) if !ok { diff --git a/pkg/cy/module.go b/pkg/cy/module.go index 4c1d1b5a..8b8d1e41 100644 --- a/pkg/cy/module.go +++ b/pkg/cy/module.go @@ -15,6 +15,7 @@ import ( "github.com/cfoust/cy/pkg/mux/screen/toasts" "github.com/cfoust/cy/pkg/mux/screen/tree" "github.com/cfoust/cy/pkg/mux/stream" + "github.com/cfoust/cy/pkg/params" "github.com/cfoust/cy/pkg/util" "github.com/rs/zerolog" @@ -32,10 +33,16 @@ type Options struct { type Cy struct { util.Lifetime deadlock.RWMutex + janet *janet.VM muxServer *server.Server + // The top-level fallback for all parameter queries. This is distinct + // from the *Parameters at the root node of the tree, which the user + // can actually change. + defaults *params.Parameters + // The tree of groups and panes tree *tree.Tree @@ -101,14 +108,20 @@ func (c *Cy) Shutdown() error { func Start(ctx context.Context, options Options) (*Cy, error) { replayBinds := bind.NewBindScope() - t := tree.NewTree() + defaults := params.New() + t := tree.NewTree(tree.WithParams(defaults.NewChild())) cy := Cy{ Lifetime: util.NewLifetime(ctx), tree: t, muxServer: server.New(), replayBinds: replayBinds, + defaults: defaults, } cy.toast = NewToastLogger(cy.sendToast) + err := cy.setDefaults(options) + if err != nil { + return nil, err + } subscriber := t.Subscribe(cy.Ctx()) go cy.pollNodeEvents(cy.Ctx(), subscriber.Recv()) diff --git a/pkg/cy/params/constants.go b/pkg/cy/params/constants.go new file mode 100644 index 00000000..573801e9 --- /dev/null +++ b/pkg/cy/params/constants.go @@ -0,0 +1,6 @@ +package params + +const ( + // The directory in which .borg files will be saved. + ParamDataDirectory = "data-dir" +) diff --git a/pkg/janet/translate.go b/pkg/janet/translate.go index f166fe39..339a8daf 100644 --- a/pkg/janet/translate.go +++ b/pkg/janet/translate.go @@ -84,6 +84,11 @@ func isValidType(type_ reflect.Type) bool { } } +// IsValidType returns true if the given value can be translated to a Janet value. +func IsValidType(value interface{}) bool { + return isValidType(reflect.TypeOf(value)) +} + // Marshal a Go value into a Janet value. func (v *VM) marshal(item interface{}) (result C.Janet, err error) { result = C.janet_wrap_nil() @@ -293,9 +298,16 @@ func (v *VM) unmarshal(source C.Janet, dest interface{}) error { } strPtr := strings.Clone(C.GoString(C.cast_janet_string(C.janet_unwrap_keyword(source)))) - if strPtr != string(keyword) { + + // if the keyword already contains a value, act as if + // we're comparing against a constant + keywordValue := string(keyword) + if len(keywordValue) == 0 { + value.SetString(strings.Clone(strPtr)) + } else if strPtr != keywordValue { return fmt.Errorf("keyword :%s does not match :%s", strPtr, keyword) } + return nil } diff --git a/pkg/mux/screen/tree/group.go b/pkg/mux/screen/tree/group.go index e28f104d..80e26b53 100644 --- a/pkg/mux/screen/tree/group.go +++ b/pkg/mux/screen/tree/group.go @@ -53,6 +53,7 @@ func (g *Group) NewPane(ctx context.Context, screen mux.Screen) *Pane { metadata := g.tree.newMetadata() pane := newPane(ctx, metadata.Id(), screen) pane.metaData = metadata + metadata.params = g.params.NewChild() g.addNode(pane) go func() { @@ -78,6 +79,7 @@ func (g *Group) NewGroup() *Group { metaData: g.tree.newMetadata(), tree: g.tree, } + g.params = g.params.NewChild() g.addNode(group) return group } diff --git a/pkg/mux/screen/tree/node.go b/pkg/mux/screen/tree/node.go index d116a041..0892894c 100644 --- a/pkg/mux/screen/tree/node.go +++ b/pkg/mux/screen/tree/node.go @@ -5,6 +5,7 @@ import ( "unicode" "github.com/cfoust/cy/pkg/bind" + "github.com/cfoust/cy/pkg/params" "github.com/sasha-s/go-deadlock" ) @@ -13,9 +14,10 @@ type NodeID = int32 type metaData struct { deadlock.RWMutex - id NodeID - name string - binds *bind.BindScope + id NodeID + name string + binds *bind.BindScope + params *params.Parameters } func (m *metaData) Id() int32 { @@ -28,6 +30,10 @@ func (m *metaData) Name() string { return m.name } +func (m *metaData) Params() *params.Parameters { + return m.params +} + func (m *metaData) SetName(name string) { m.Lock() defer m.Unlock() @@ -47,6 +53,7 @@ func (m *metaData) Binds() *bind.BindScope { type Node interface { Id() NodeID Name() string + Params() *params.Parameters SetName(string) Binds() *bind.BindScope } diff --git a/pkg/mux/screen/tree/tree.go b/pkg/mux/screen/tree/tree.go index a1f81529..b646e395 100644 --- a/pkg/mux/screen/tree/tree.go +++ b/pkg/mux/screen/tree/tree.go @@ -6,6 +6,7 @@ import ( "github.com/cfoust/cy/pkg/bind" "github.com/cfoust/cy/pkg/mux" + "github.com/cfoust/cy/pkg/params" "github.com/sasha-s/go-deadlock" ) @@ -140,7 +141,15 @@ func (t *Tree) GroupById(id NodeID) (*Group, bool) { return group, true } -func NewTree() *Tree { +type TreeOption func(*Tree) + +func WithParams(p *params.Parameters) TreeOption { + return func(t *Tree) { + t.root.params = p + } +} + +func NewTree(options ...TreeOption) *Tree { tree := &Tree{ UpdatePublisher: mux.NewPublisher(), nodes: make(map[NodeID]Node), @@ -153,5 +162,9 @@ func NewTree() *Tree { tree.storeNode(tree.root) + for _, option := range options { + option(tree) + } + return tree } diff --git a/pkg/params/module.go b/pkg/params/module.go new file mode 100644 index 00000000..385f7a16 --- /dev/null +++ b/pkg/params/module.go @@ -0,0 +1,53 @@ +package params + +import ( + "fmt" + + "github.com/cfoust/cy/pkg/janet" + + "github.com/sasha-s/go-deadlock" +) + +type Parameters struct { + deadlock.RWMutex + parent *Parameters + table map[string]interface{} +} + +func (p *Parameters) Set(key string, value interface{}) error { + if !janet.IsValidType(value) { + return fmt.Errorf("all parameters must be representable in Janet") + } + + p.Lock() + p.table[key] = value + p.Unlock() + return nil +} + +func (p *Parameters) Get(key string) (value interface{}, ok bool) { + var current *Parameters = p + for current != nil { + current.RLock() + value, ok = current.table[key] + current.RUnlock() + if ok { + return value, ok + } + current = current.parent + } + + return nil, false +} + +func (p *Parameters) NewChild() *Parameters { + child := New() + child.parent = p + return child +} + +func New() *Parameters { + return &Parameters{ + table: make(map[string]interface{}), + } +}