From 5ec64016f9a8692bc95ee6c8f500c6ed57038103 Mon Sep 17 00:00:00 2001 From: BrookJeynes Date: Fri, 18 Oct 2024 06:28:32 +1000 Subject: [PATCH 1/2] feat: Add color map layout node --- pkg/cy/api/layout_test.janet | 19 +++++ pkg/cy/boot/layout.janet | 13 +++- pkg/cy/stories.go | 49 +++++++++++++ pkg/layout/colormap/module.go | 132 ++++++++++++++++++++++++++++++++++ pkg/layout/engine/nodes.go | 40 +++++++++++ pkg/layout/janet.go | 52 ++++++++++++-- pkg/layout/module.go | 35 +++++++-- pkg/layout/prop/module.go | 1 + 8 files changed, 330 insertions(+), 11 deletions(-) create mode 100644 pkg/layout/colormap/module.go diff --git a/pkg/cy/api/layout_test.janet b/pkg/cy/api/layout_test.janet index ccabad67..9c8f9ec8 100644 --- a/pkg/cy/api/layout_test.janet +++ b/pkg/cy/api/layout_test.janet @@ -75,6 +75,25 @@ (expect-error (layout/set layout))) +(test "color-map" + (def layout + (layout/new + (color-map + @{} + (attach)))) + + (layout/set layout) + (assert (deep= (layout/get) layout))) + +(test "invalid color-map" + (def layout + (layout/new + (color-map + "" + (attach)))) + + (expect-error (layout/set layout))) + (test "tab actions" (action/new-tab) (action/new-tab) diff --git a/pkg/cy/boot/layout.janet b/pkg/cy/boot/layout.janet index 83e3aae1..5be285bf 100644 --- a/pkg/cy/boot/layout.janet +++ b/pkg/cy/boot/layout.janet @@ -46,7 +46,8 @@ For example: (or (layout/type? :margins node) (layout/type? :bar node) - (layout/type? :borders node)) @[[:node]] + (layout/type? :borders node) + (layout/type? :color-map node)) @[[:node]] @[])) (defn @@ -168,6 +169,14 @@ For example: :text text :bottom bottom}) +(defn + layout/color-map + ```Convenience function for creating a new :color-map node.``` + [map node] + {:type :color-map + :map map + :node node}) + (defmacro layout/new ```Macro for quickly creating layouts. layout/new replaces shorthand versions of node creation functions with their longform versions and also includes a few abbreviations that do not exist elsewhere in the API. @@ -184,6 +193,7 @@ Supported short forms: * tab: A :tab inside of a :tabs node. * active-tab: A :tab with :active=true inside of a :tabs node. * bar: A :bar node. +* color-map: A :color-map node. See [the layouts chapter](/layouts.md#api) for more information. ``` @@ -209,6 +219,7 @@ See [the layouts chapter](/layouts.md#api) for more information. (def tab ,layout/tab) (def active-tab ,active-tab) (def bar ,layout/bar) + (def color-map ,layout/color-map) ,body)) (defn diff --git a/pkg/cy/stories.go b/pkg/cy/stories.go index 62b43096..eac5f5fb 100644 --- a/pkg/cy/stories.go +++ b/pkg/cy/stories.go @@ -790,6 +790,55 @@ func init() { return screen, err }, stories.Config{}) + stories.Register("layout/colormap", func(ctx context.Context) ( + mux.Screen, + error, + ) { + _, client, screen, err := createStory(ctx) + err = client.execute(` +(def cmd1 (shell/new)) +(layout/set (layout/new + (color-map + (fn [layout] + (def node (layout/attach-id layout)) + (if + (nil? node) @{"0" "#ff0000"} + @{"0" "#f00000"})) + (attach :id cmd1)))) + +(def cmd1 (shell/new)) +(def cmd2 (shell/new)) +(defn + theme [layout] + (def node (layout/attach-id layout)) + (if + (nil? node) @{"0" "#ff0000"} + @{"0" "#0000ff"})) + +(layout/set (layout/new + (split + (color-map + theme + (attach :id cmd1)) + (color-map + theme + (pane :id cmd2))))) + `) + return screen, err + }, stories.Config{ + Input: []interface{}{ + stories.Wait(stories.Some), + stories.Type("ctrl+a", "L"), + stories.Wait(stories.Some), + stories.Type("ctrl+a", "H"), + stories.Wait(stories.Some), + stories.Type("ctrl+a", "L"), + stories.Wait(stories.Some), + stories.Type("ctrl+a", "H"), + stories.Wait(stories.Some), + }, + }) + stories.Register("layout/bar/bad", func(ctx context.Context) ( mux.Screen, error, diff --git a/pkg/layout/colormap/module.go b/pkg/layout/colormap/module.go new file mode 100644 index 00000000..5a3985f0 --- /dev/null +++ b/pkg/layout/colormap/module.go @@ -0,0 +1,132 @@ +package colormap + +import ( + "context" + + "github.com/cfoust/cy/pkg/geom" + "github.com/cfoust/cy/pkg/geom/tty" + L "github.com/cfoust/cy/pkg/layout" + "github.com/cfoust/cy/pkg/layout/prop" + "github.com/cfoust/cy/pkg/mux" + "github.com/cfoust/cy/pkg/taro" + + "github.com/sasha-s/go-deadlock" +) + +type ColorMap struct { + *L.Computable + deadlock.RWMutex + *mux.UpdatePublisher + render *taro.Renderer + screen mux.Screen + size geom.Size + config L.ColorMapType +} + +var _ mux.Screen = (*ColorMap)(nil) +var _ L.Reusable = (*ColorMap)(nil) + +func (l *ColorMap) Apply(node L.NodeType) (bool, error) { + config, ok := node.(L.ColorMapType) + if !ok { + return false, nil + } + + l.Lock() + defer l.Unlock() + + l.config = config + + layout := L.New(config.Node) + for _, prop := range []prop.Presettable{ + config.Map, + } { + prop.SetLogger(l.Logger) + prop.Preset( + l.Ctx(), + l.Context.Context(), + &layout, + ) + prop.ClearCache() + } + + return true, nil +} + +func (l *ColorMap) Kill() { + l.RLock() + screen := l.screen + l.RUnlock() + + screen.Kill() +} + +func (l *ColorMap) State() *tty.State { + l.Lock() + defer l.Unlock() + + var ( + size = l.size + config = l.config + ) + + innerState := l.screen.State() + state := tty.New(size) + tty.Copy(geom.Vec2{}, state, innerState) + + if value, ok := config.Map.GetPreset(); ok { + value.Apply(state.Image) + } + + return state +} + +func (l *ColorMap) Send(msg mux.Msg) { + l.RLock() + l.RUnlock() + l.screen.Send(msg) +} + +func (l *ColorMap) Size() geom.Size { + l.RLock() + defer l.RUnlock() + return l.size +} + +func (l *ColorMap) poll(ctx context.Context) { + updates := l.screen.Subscribe(ctx) + + for { + select { + case <-ctx.Done(): + return + case event := <-updates.Recv(): + if _, ok := event.(L.NodeChangeEvent); ok { + continue + } + l.Publish(event) + } + } +} + +func (l *ColorMap) Resize(size geom.Size) error { + l.Lock() + l.size = size + l.Unlock() + return l.screen.Resize(size) +} + +func New(ctx context.Context, screen mux.Screen) *ColorMap { + c := L.NewComputable(ctx) + colormap := &ColorMap{ + Computable: c, + UpdatePublisher: mux.NewPublisher(), + size: geom.DEFAULT_SIZE, + screen: screen, + render: taro.NewRenderer(), + } + + go colormap.poll(colormap.Ctx()) + + return colormap +} diff --git a/pkg/layout/engine/nodes.go b/pkg/layout/engine/nodes.go index 754216c3..b969de6f 100644 --- a/pkg/layout/engine/nodes.go +++ b/pkg/layout/engine/nodes.go @@ -7,6 +7,7 @@ import ( L "github.com/cfoust/cy/pkg/layout" "github.com/cfoust/cy/pkg/layout/bar" "github.com/cfoust/cy/pkg/layout/borders" + "github.com/cfoust/cy/pkg/layout/colormap" "github.com/cfoust/cy/pkg/layout/margins" "github.com/cfoust/cy/pkg/layout/pane" "github.com/cfoust/cy/pkg/layout/split" @@ -42,6 +43,8 @@ func (l *LayoutEngine) createNode( err = l.createTabs(node, config) case L.BarType: err = l.createBar(node, config) + case L.ColorMapType: + err = l.createColorMap(node, config) default: err = fmt.Errorf("unimplemented screen") } @@ -155,6 +158,13 @@ func (l *LayoutEngine) updateNode( Node: current.Children[0], }, ) + case L.ColorMapType: + updates = append(updates, + updateNode{ + Config: node.Node, + Node: current.Children[0], + }, + ) } // If any of the node's children cannot be reused, we need to remake @@ -314,6 +324,28 @@ func (l *LayoutEngine) createBar( return nil } +func (l *LayoutEngine) createColorMap( + node *screenNode, + config L.ColorMapType, +) error { + innerNode, err := l.createNode( + node.Ctx(), + config.Node, + ) + if err != nil { + return err + } + + colormap := colormap.New( + node.Ctx(), + innerNode.Screen, + ) + + node.Screen = colormap + node.Children = []*screenNode{innerNode} + return nil +} + // applyNodeChange replaces the configuration of the target node with // newConfig. This is only used to allow nodes to change their own // configurations in response to user input (for now, just mouse events.) @@ -380,6 +412,14 @@ func applyNodeChange( newConfig, ) return currentConfig + case L.ColorMapType: + currentConfig.Node = applyNodeChange( + current.Children[0], + target, + currentConfig.Node, + newConfig, + ) + return currentConfig } return currentConfig diff --git a/pkg/layout/janet.go b/pkg/layout/janet.go index 1f524461..759f25b7 100644 --- a/pkg/layout/janet.go +++ b/pkg/layout/janet.go @@ -10,12 +10,13 @@ import ( ) var ( - KEYWORD_PANE = janet.Keyword("pane") - KEYWORD_SPLIT = janet.Keyword("split") - KEYWORD_MARGINS = janet.Keyword("margins") - KEYWORD_BORDERS = janet.Keyword("borders") - KEYWORD_TABS = janet.Keyword("tabs") - KEYWORD_BAR = janet.Keyword("bar") + KEYWORD_PANE = janet.Keyword("pane") + KEYWORD_SPLIT = janet.Keyword("split") + KEYWORD_MARGINS = janet.Keyword("margins") + KEYWORD_BORDERS = janet.Keyword("borders") + KEYWORD_TABS = janet.Keyword("tabs") + KEYWORD_BAR = janet.Keyword("bar") + KEYWORD_COLORMAP = janet.Keyword("color-map") defaultBorder = prop.NewStatic(&style.DefaultBorder) ) @@ -273,6 +274,35 @@ func unmarshalNode(value *janet.Value) (NodeType, error) { type_.Bottom = *args.Bottom } + return type_, nil + case KEYWORD_COLORMAP: + type colormapArgs struct { + Map *prop.ColorMap + Node *janet.Value + } + + args := colormapArgs{} + err = value.Unmarshal(&args) + if err != nil { + return nil, err + } + + node, err := unmarshalNode(args.Node) + if err != nil { + return nil, err + } + + if args.Map == nil { + return nil, fmt.Errorf( + ":color-map node missing :map", + ) + } + + type_ := ColorMapType{ + Map: args.Map, + Node: node, + } + return type_, nil } @@ -416,6 +446,16 @@ func marshalNode(node NodeType) interface{} { Bottom: node.Bottom, Node: marshalNode(node.Node), } + case ColorMapType: + return struct { + Type janet.Keyword + Map *prop.ColorMap + Node interface{} + }{ + Type: KEYWORD_COLORMAP, + Map: node.Map, + Node: marshalNode(node.Node), + } } return nil } diff --git a/pkg/layout/module.go b/pkg/layout/module.go index 0bb7818d..58f4ab4e 100644 --- a/pkg/layout/module.go +++ b/pkg/layout/module.go @@ -47,6 +47,11 @@ type BorderType struct { Node NodeType } +type ColorMapType struct { + Map *prop.ColorMap + Node NodeType +} + type Tab struct { Active bool Name string @@ -114,20 +119,18 @@ func getPaneType(tree NodeType) (panes []PaneType) { case SplitType: panes = append(panes, getPaneType(node.A)...) panes = append(panes, getPaneType(node.B)...) - return case MarginsType: panes = append(panes, getPaneType(node.Node)...) - return case BorderType: panes = append(panes, getPaneType(node.Node)...) - return case BarType: panes = append(panes, getPaneType(node.Node)...) - return case TabsType: for _, tab := range node.Tabs { panes = append(panes, getPaneType(tab.Node)...) } + case ColorMapType: + panes = append(panes, getPaneType(node.Node)...) } return } @@ -151,6 +154,8 @@ func getNumLeaves(node NodeType) int { result += getNumLeaves(tab.Node) } return result + case ColorMapType: + return getNumLeaves(node.Node) } return 0 } @@ -189,6 +194,10 @@ func Copy(node NodeType) NodeType { } copied.Tabs = newTabs return copied + case ColorMapType: + copied := node + copied.Node = Copy(node.Node) + return node } return node @@ -220,6 +229,9 @@ func AttachFirst(node NodeType) NodeType { node.Tabs[i].Node = AttachFirst(tab.Node) } return node + case ColorMapType: + node.Node = AttachFirst(node.Node) + return node } return node @@ -283,6 +295,9 @@ func RemoveAttached(node NodeType) NodeType { newNode := node newNode.Tabs = newTabs return newNode + case ColorMapType: + node.Node = RemoveAttached(node.Node) + return node } return node @@ -309,6 +324,8 @@ func IsAttached(tree NodeType) bool { } } return false + case ColorMapType: + return IsAttached(node.Node) } return false } @@ -337,6 +354,9 @@ func Detach(node NodeType) NodeType { node.Tabs[i].Node = Detach(tab.Node) } return node + case ColorMapType: + node.Node = Detach(node.Node) + return node } return node @@ -373,6 +393,9 @@ func attach(node NodeType, id tree.NodeID) NodeType { node.Tabs[i].Node = attach(tab.Node, id) } return node + case ColorMapType: + node.Node = attach(node.Node, id) + return node } return node @@ -412,6 +435,8 @@ func getAttached(node NodeType) *tree.NodeID { } } return nil + case ColorMapType: + return getAttached(node.Node) } return nil @@ -480,6 +505,8 @@ func validateNodes(node NodeType) error { } return nil + case ColorMapType: + return validateNodes(node.Node) } return nil diff --git a/pkg/layout/prop/module.go b/pkg/layout/prop/module.go index 0a0f3746..6d221734 100644 --- a/pkg/layout/prop/module.go +++ b/pkg/layout/prop/module.go @@ -204,3 +204,4 @@ func NewStatic[T any](value T) *Prop[T] { type String = Prop[string] type Color = Prop[*style.Color] type Border = Prop[*style.Border] +type ColorMap = Prop[*style.ColorMap] From 570811f74c5cebdd280985c4a9325e4d5bbcc3b0 Mon Sep 17 00:00:00 2001 From: BrookJeynes Date: Fri, 18 Oct 2024 06:37:35 +1000 Subject: [PATCH 2/2] fixed formatting --- pkg/cy/stories.go | 2 +- pkg/layout/module.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cy/stories.go b/pkg/cy/stories.go index eac5f5fb..77d79c3d 100644 --- a/pkg/cy/stories.go +++ b/pkg/cy/stories.go @@ -837,7 +837,7 @@ func init() { stories.Type("ctrl+a", "H"), stories.Wait(stories.Some), }, - }) + }) stories.Register("layout/bar/bad", func(ctx context.Context) ( mux.Screen, diff --git a/pkg/layout/module.go b/pkg/layout/module.go index 58f4ab4e..2c2da846 100644 --- a/pkg/layout/module.go +++ b/pkg/layout/module.go @@ -129,7 +129,7 @@ func getPaneType(tree NodeType) (panes []PaneType) { for _, tab := range node.Tabs { panes = append(panes, getPaneType(tab.Node)...) } - case ColorMapType: + case ColorMapType: panes = append(panes, getPaneType(node.Node)...) } return