Skip to content

OverlayManager

The OverlayManager is the single high-level facade for all layer management in Tako. Three categories of operations live here: basic overlays, component overlays, and dialog primitives (via the Dialog() sub-namespace).

The Problem It Solves

go
// ❌ Old way — 3 calls just to show an overlay
ctx.Stack().Push("search")
ctx.Router().Focus(1, "search")
ctx.Hooks().Set("app.overlay.search", func() any { return p.render() })

// And cleanup when closing
ctx.Stack().Pop()
ctx.Router().Focus(0, ctx.Stack().Top())

Five coordinated calls, all of which you had to write and keep in sync.

The Solution

go
// ✅ One call to show
ctx.Overlay().Show("search", func() any { return p.render() })

// One call to close
ctx.Overlay().Close()

// Dismiss everything
ctx.Overlay().CloseAll()

Accessing the OverlayManager

LocationHow to access
Host app (main.go)app.Overlay()
Plugin (OnInit, etc.)ctx.Overlay()
IoC containerapp.Make(new(contracts.OverlayManager))

API Reference

Basic Overlays

Show(layerID string, render func() any)

Pushes layerID onto the focus stack, registers render as the hook provider under "tako.overlay.<layerID>", and shifts keyboard focus to the new layer.

  • Idempotent: calling Show when layerID is already the top layer is a no-op.
  • Emits: "overlay:shown" on the EventBus with layerID as data.
go
ctx.Overlay().Show("help", func() any {
    return p.renderHelpView()  // return any — string, struct, etc.
})

Your layout reads the render output via the hook:

go
// In your Layout.View():
content := ctx.Hooks().Get("tako.overlay.help")

Close()

Pops the topmost overlay and reverts focus to the layer below.

  • Safe at base layer: no-op if only the base layer is active.
  • Emits: "overlay:closed" with the closed layer ID.

CloseAll()

Pops every overlay down to (but not including) the base layer. Emits "overlay:closed" for each.

Top() string

Returns the ID of the currently active overlay, or "" at the base layer.

IsActive() bool

Reports whether any overlay is currently shown. Sugar for Top() != "".


Component Overlays

Register(c Component)

Wires the component's render hook and keybindings without pushing to the stack. For pre-registration before a layer becomes active. Prefer ShowComponent for the common case.

ShowComponent(c Component)

Wires the component (hook + keybindings) and immediately pushes it onto the overlay stack.

go
ctx.Keys().Bind("ctrl+f", func() {
    if !ctx.Overlay().IsActive() {
        ctx.Overlay().ShowComponent(&SearchBox{})
    }
})

See 04.03 — Component Interface for how to define a Component.


Dialog Sub-Namespace

Dialog() DialogService

Returns the DialogService for this overlay manager — the sub-namespace for built-in interaction primitives.

Dialogs are overlays: they push onto the same stack and are dismissed the same way. Grouping them under .Dialog() makes that relationship explicit, while keeping OverlayManager's interface clean as new dialog types are added.

go
// Single call
app.Overlay().Dialog().Confirm("Delete?", func(yes bool) { ... })

// Cached for multiple calls
d := ctx.Overlay().Dialog()
d.Confirm("Step 1?", cb1)

See 04.04 — DialogService for the full dialog API.


Hook Namespace

Render hooks are stored under:

"tako.overlay.<layerID>"

Your layout should consume hooks under this namespace. The older "app.overlay.*" namespace from pre-DX code is unaffected — you can migrate incrementally:

go
// Layout supporting both namespaces during migration
top := r.Stack().Top()
content := ctx.Hooks().Get("tako.overlay." + top)
if content == nil {
    content = ctx.Hooks().Get("app.overlay." + top) // legacy fallback
}

Events Emitted

EventDataWhen
"overlay:shown"layerID (string)After a successful Show() or ShowComponent()
"overlay:closed"layerID (string)After each successful Close()

Complete Plugin Example

Before (manual coordination)

go
func (p *Plugin) OnInit(ctx *tako.Context) error {
    ctx.Hooks().Set("app.overlay.fzf", func() any {
        if !p.isActive { return nil }
        return p.renderFZF()
    })
    ctx.Keys().Bind("ctrl+f", func() {
        if p.isActive { return }
        p.isActive = true
        ctx.Stack().Push("fzf")
        ctx.Router().Focus(1, "fzf")
        ctx.Emit("fzf:opened", nil)
    })
    ctx.On("fzf:closed", func(e contracts.Event) {
        p.isActive = false
    })
    return nil
}

After (OverlayManager)

go
func (p *Plugin) OnInit(ctx *tako.Context) error {
    ctx.Keys().Bind("ctrl+f", func() {
        if ctx.Overlay().IsActive() { return }
        ctx.Overlay().Show("fzf", p.renderFZF)
        ctx.Emit("fzf:opened", nil)
    })
    return nil
}

The isActive field is gone — the overlay manager's state is the single source of truth.

Overlay Guards Still Work

OverlayManager.Show() calls Router.PushLayer(), which calls Stack.Push(). All guards registered with BeforePush still fire.


Up next: 04.03 — Component Interface — bundle a view and its keybindings into a single reusable struct.