Skip to content
Subha Sundar Das edited this page Jul 18, 2026 · 4 revisions

Window System

A lightweight, thread-safe window manager for retui terminal applications. It adds floating windows, modal dialogs, Z-ordering, and focus management on top of retui's component model — without retui needing to know anything about windows directly.

Table of Contents


Concepts

What is a Window?

A Window is a floating UI surface — think of it as a resizable, movable box that sits on top of your app's main screen. It has a title, a position, a size, and content to render. A window can be:

  • Non-modal — floats above the background but doesn't block interaction with it (like a status panel or a docked palette).
  • Modal — blocks all interaction with everything behind it until it's closed (like a confirmation dialog or an edit form).

Windows start out hidden. Nothing shows up on screen until you call .Show().

The WindowManager

Every window is tracked by a single global WindowManager. You never create one yourself — the package manages a globalManager instance for you, and most operations are exposed as both:

  • Methods on *WindowManager (if you fetch it via window.GetManager()), and
  • Free package-level functions that operate on the global instance (e.g. window.CloseAll(), window.GetFocused()).

The manager is responsible for:

  • Keeping a registry of all windows (map[string]*Window)
  • Maintaining Z-order — which window is drawn on top
  • Maintaining a separate modal stack — which modal is currently active
  • Tracking focus — which window receives keyboard input
  • Triggering re-renders when window state changes

Modal vs. Non-Modal

  Non-Modal Modal
Blocks background interaction ❌ No ✅ Yes
Appears in modalStack ❌ No ✅ Yes
Escape has default "close" behavior ❌ No (you wire it yourself) ✅ Yes (built-in fallback)
Tab cycles between it and other modals N/A ✅ Yes, if multiple modals stacked
Typical use case Status bar, docked panel, tooltip Edit form, confirmation dialog, alert

Gotchas & Best Practices

  • Always return true/false deliberately from OnKeyPress. Returning true when you didn't actually handle the key will swallow it silently for everyone else; returning false when you did handle it can let the key "leak" into the background screen.
  • Close() is final. Once closed, a window is unregistered and cannot be reshown. If you need a reusable dialog, keep it around and use Hide() / Show() instead of Close() / NewWindow().
  • Only one modal blocks input at a time — the topmost one. Lower modals in the stack are inert until the ones above them are closed.
  • Escape has default behavior; Tab does not (much). Tab only auto-cycles focus among open modals — it does nothing special for non-modal windows or when nothing is open.
  • Don't forget SetScreenSize. If you never call it, Center() falls back to DefaultScreenWidth/DefaultScreenHeight (140×40), which may not match your actual terminal.
  • All state is protected by mutexes, so it's safe to call window/manager methods from goroutines (e.g. background workers closing a progress dialog when a task finishes). Rendering re-triggers are dispatched via go wm.triggerRender(), so they won't block the caller.

FAQ

Q: I pressed Escape and it closed my modal and my whole screen. What happened? A: Almost certainly your OnKeyPress handler for Escape did something (like calling Close()) but returned false instead of true, so the key kept propagating past the window layer into your background screen's own key handling. Return true whenever you act on a key.

Q: How do I make a window that can't be closed by Escape? A: Add an OnKeyPress handler that intercepts KeyEscape and returns true without calling Close():

w.OnKeyPress(func(key retui.Key) bool {
    if key.Code == retui.KeyEscape {
        return true // swallow it — do nothing
    }
    return false
})

Q: Can I have two modals open with neither blocking the other? A: No — modal means "blocks everything behind it," including other modals lower in the stack. If you need two independent floating panels, use non-modal windows instead.

Q: How do I know which window currently has keyboard focus? A: window.GetFocused() returns its ID; pass that to window.GetWindowByID(id) to get the *Window.

# Window System

A lightweight, thread-safe window manager for [retui](https://github.com/subhasundardass/retui) terminal applications. It adds floating windows, modal dialogs, Z-ordering, and focus management on top of retui's component model — without retui needing to know anything about windows directly.

Table of Contents


Concepts

What is a Window?

A Window is a floating UI surface — think of it as a resizable, movable box that sits on top of your app's main screen. It has a title, a position, a size, and content to render. A window can be:

  • Non-modal — floats above the background but doesn't block interaction with it (like a status panel or a docked palette).
  • Modal — blocks all interaction with everything behind it until it's closed (like a confirmation dialog or an edit form).

Windows start out hidden. Nothing shows up on screen until you call .Show().

The WindowManager

Every window is tracked by a single global WindowManager. You never create one yourself — the package manages a globalManager instance for you, and most operations are exposed as both:

  • Methods on *WindowManager (if you fetch it via window.GetManager()), and
  • Free package-level functions that operate on the global instance (e.g. window.CloseAll(), window.GetFocused()).

The manager is responsible for:

  • Keeping a registry of all windows (map[string]*Window)
  • Maintaining Z-order — which window is drawn on top
  • Maintaining a separate modal stack — which modal is currently active
  • Tracking focus — which window receives keyboard input
  • Triggering re-renders when window state changes

Modal vs. Non-Modal

Non-Modal Modal
Blocks background interaction ❌ No ✅ Yes
Appears in modalStack ❌ No ✅ Yes
Escape has default "close" behavior ❌ No (you wire it yourself) ✅ Yes (built-in fallback)
Tab cycles between it and other modals N/A ✅ Yes, if multiple modals stacked
Typical use case Status bar, docked panel, tooltip Edit form, confirmation dialog, alert

Set modal behavior with .SetModal(true) before calling .Show().

Z-Order and Focus

  • The manager keeps an ordered stack of window IDs — stack[0] is the bottom-most window, the last element is the top-most (frontmost, drawn last, receives clicks/focus first).
  • Non-modal windows are always reordered beneath modal windows (reorderStackLocked), so a modal can never be visually buried under a regular window.
  • Only one window is focused at a time — the one that receives key events. If any modal is open, focus is pinned to the topmost modal, no matter what SetFocus or a background click tries to do. This is what makes modals "block" interaction.
  • Focus() brings a window to the front of the Z-order and gives it focus in one call.

Key Event Propagation

This is the part that trips people up the most, so it gets its own section.

A single keypress travels through several layers before it reaches your code:

Terminal input
     │
     ▼
retui's App.Run() event loop
     │
     ▼
WindowKeyDispatch(key) bool   ← window package's dispatcher
     │
     ▼
Window.HandleKey(key) bool    ← per-window dispatch
     │
     ▼
your OnKeyPress(key) bool     ← your callback

Every layer returns a bool: "did I consume this key?"

  • If your OnKeyPress callback returns true, that true bubbles all the way back up.
  • If it reaches all the way back to App.Run() as true, the key is considered fully handled — retui will not pass it on to your root/background screen.
  • If your callback returns false (or you never registered one), the key is not consumed by that window, and — for most keys — nothing else happens automatically. The one exception is Escape while a modal is open: the dispatcher has a built-in fallback that closes the topmost modal by default, even if you never wrote an OnKeyPress handler for it.

⚠️ Always return true from OnKeyPress when you've acted on a key. If you close a window, print something, or otherwise "do something" in response to a key, and forget to return true, that key will keep propagating and may be picked up by your background screen too (a classic symptom: "pressing Escape closes my modal and my whole app/screen").


Getting Started

Add the import:

import "github.com/subhasundardass/retui/window"

Somewhere during startup, tell the window system your screen dimensions (so Center() and default positioning work correctly) and hook up rendering:

window.SetScreenSize(140, 40)
window.SetRenderTrigger(func() {
    // however your app schedules a re-render, e.g.:
    retui.Exit() // NOT literally this — call your app's redraw/rerender hook
})

In most retui apps you don't need to do this manually — retui.RootRenderWrap is already wired up by the window package's init() to composite window overlays on top of your root element automatically.


Common Recipes

A Simple Info Window

func showInfoWindow(message string) {
    w := window.NewWindow().
        SetTitle("Info").
        SetSize(50, 10).
        Center().
        SetRenderFn(func() retui.Element {
            return retui.Text(message)
        })

    w.Show()
}
  • Not modal — the user can still interact with the background.
  • .Center() uses the manager's tracked screen size to position it.

A Modal Edit Dialog (F2 to Edit)

This is the pattern from a ledger-group list: press F2 on a selected row to open an edit dialog.

func openEditLedgerGroup(group *LedgerGroup) {
    render := func() retui.Element {
        return buildEditForm(group) // your form-rendering logic
    }

    w := window.NewWindow().
        SetTitle("Edit Ledger Group").
        SetModal(true).
        Center().
        SetSize(80, 40).
        SetRenderFn(render)

    w.OnKeyPress(func(key retui.Key) bool {
        switch key.Code {
        case retui.KeyEscape:
            w.Close()
            return true // consumed — do not let this propagate further
        case retui.KeyEnter:
            saveLedgerGroup(group)
            w.Close()
            return true
        }
        return false // let anything else fall through (e.g. text input, Tab)
    })

    w.Show()
}

Wire it up on your list screen:

listWindow.OnKeyPress(func(key retui.Key) bool {
    if key.Code == retui.KeyF2 {
        openEditLedgerGroup(selectedGroup())
        return true
    }
    return false
})

Handling Escape Correctly

You get Escape-closes-modal behavior for free — you don't have to write anything:

w := window.NewWindow().SetModal(true) /* ... */
w.Show()
// Pressing Escape now closes this window automatically,
// even with zero OnKeyPress wiring.

Only add your own Escape handling if you need to do something other than a plain close — e.g. show a "discard changes?" confirmation instead:

w.OnKeyPress(func(key retui.Key) bool {
    if key.Code == retui.KeyEscape {
        if formIsDirty {
            showDiscardConfirmation(w)
        } else {
            w.Close()
        }
        return true // always return true — this tells the dispatcher
                     // "I've handled Escape myself, don't run your own close fallback"
    }
    return false
})

If you return false for Escape (or omit the case), the built-in fallback takes over and closes the window anyway — your custom logic is skipped for that keypress. Always return true once you've decided to act on Escape yourself.

Centering and Positioning

w := window.NewWindow().
    SetSize(60, 20).
    Center()               // uses current global screen size

// Or position it explicitly:
w.MoveTo(10, 5)

// Or center on just one axis:
w.CenterHorizontally(140)
w.CenterVertically(40)

// Nudge it:
w.MoveBy(2, -1)

Stacked Modals

You can open a modal from within another modal (e.g. an edit dialog opens a "pick from list" sub-dialog). The manager keeps a separate modalStack for this:

  • Tab cycles focus between currently open modals, when more than one is stacked.
  • Escape always closes the topmost modal only — lower modals stay open and become focused/topmost afterward.
  • A non-modal window can never steal focus while any modal is open.
w1 := window.NewWindow().SetModal(true).SetTitle("Edit Item")
w1.Show()

// later, from inside w1's render or key handler:
w2 := window.NewWindow().SetModal(true).SetTitle("Pick Category")
w2.Show() // becomes the new topmost modal; w1 is still open underneath

API Reference

Package Functions

Convenience wrappers around the global manager — use these unless you specifically need a *WindowManager reference.

Function Description
SetDefaultScreenSize(width, height int) Sets the fallback screen size new windows use for centering before the manager has an explicit size.
SetScreenSize(width, height int) Sets the manager's tracked screen size (used by Center()) and updates the package defaults.
GetScreenSize() (int, int) Returns the current tracked screen width/height.
SetRenderTrigger(trigger func()) Registers the callback used to request a re-render whenever window state changes.
GetManager() *WindowManager Returns the global WindowManager instance.
GetFocusedID() string / GetFocused() string Returns the ID of the currently focused window (a modal, if any is open).
GetWindowByID(id string) *Window Looks up a window by ID.
Count() int Total number of registered windows (visible or not).
CountVisible() int Number of windows currently visible.
GetVisible() []*Window All visible windows, bottom-to-top Z-order.
IsAnyModalOpen() bool True if at least one modal window is visible.
IsWindowBlocked(id string) bool True if the given window is blocked by an active modal (i.e. it isn't the topmost modal, or a modal is open and this isn't it).
GetActiveModal() string ID of the topmost modal, or "".
CloseAll() Force-closes and removes every window immediately.
ResetGlobalManager() Replaces the global manager with a fresh instance. Intended for tests.
CloseFocusedWindow() Closes the currently focused window; if none is focused, closes the topmost window in Z-order instead.

Window type

Constructor

func NewWindow() *Window

Creates a new window with sensible defaults (40x15, centered on the default screen size, not modal, hidden). Chain configuration methods, then call .Show().

Configuration (fluent — each returns *Window)

Method Description
SetTitle(title string) *Window Sets the window's title.
SetSize(width, height int) *Window Sets fixed dimensions.
SetPosition(x, y int) *Window Sets the top-left screen coordinate directly.
SetModal(modal bool) *Window Marks the window modal (blocks background) or not.
SetRenderFn(fn func() retui.Element) *Window Sets the function used to render the window's content each frame. Takes priority over StaticContent/SetContent.
OnKeyPress(fn func(key retui.Key) bool) *Window Registers the key handler. Must return true if the key was consumed, or false to let it fall through.

Lifecycle

Method Description
Show() *Window Makes the window visible and registers it with the manager. Triggers a re-render.
Hide() *Window Hides the window but keeps it registered — can be shown again later.
Close() Hides and unregisters the window permanently. It cannot be shown again after this — create a new one instead.
ToggleVisibility() bool Flips visible/hidden state; returns the new state.

Focus

Method Description
Focus() Brings this window to the front and gives it keyboard focus.
IsFocused() bool Whether this window currently has focus.
IsActive() bool Alias for IsFocused().

Queries

Method Description
IsVisible() bool Whether the window is currently shown.
IsModal() bool Whether the window is modal.
GetTitle() string Current title.
GetSize() [2]int [width, height].
GetPosition() [2]int [x, y].
GetBounds() [4]int [x, y, width, height].

Positioning helpers

Method Description
Center() *Window Centers using the manager's current screen size; clamps so the window never overflows the screen.
CenterHorizontally(screenWidth int) *Window Centers on the X axis only, using the given width.
CenterVertically(screenHeight int) *Window Centers on the Y axis only, using the given height.
MoveTo(x, y int) *Window Absolute move.
MoveBy(dx, dy int) *Window Relative move.
ResizeTo(width, height int) *Window Changes dimensions.

Content

Method Description
SetContent(content retui.Element) *Window Sets static content directly (used if no RenderFn is set). Triggers a re-render.
GetContent() retui.Element Returns the current static content.
Render() retui.Element Called internally by the overlay renderer each frame — prefers RenderFn if set, otherwise StaticContent. You normally don't call this yourself.

Keyboard

Method Description
HandleKey(key retui.Key) bool Invoked by the dispatch system when this window has focus. Calls your OnKeyPress callback (if any) and returns whether the key was consumed.

Misc

Method Description
String() string Debug-friendly summary of the window's state.
Clone() *Window Returns a copy with a new ID, offset position by (+5, +5), hidden and unfocused. Content and size are copied as-is.

WindowManager type

You'll rarely need this directly — get it via window.GetManager() if you do.

Method Description
NewWindowManager() *WindowManager Constructs a manager (used internally to create the global instance; you generally don't call this yourself).
SetScreenSize(width, height int) / GetScreenSize() (int, int) Tracked screen dimensions.
SetRenderTrigger(trigger func()) Registers the re-render callback.
AddWindow(w *Window) Registers a window and inserts it into the Z-order / modal stack as appropriate. Called by Show().
RemoveWindow(id string) Unregisters a window, removes it from all stacks, and reassigns focus if it was focused. Called by Close().
GetWindow(id string) *Window Look up by ID.
BringToFront(id string) Moves a window to the top of the Z-order and focuses it. No-ops if a different modal is currently active.
GetZOrder() []string Copy of the current bottom-to-top stack of window IDs.
GetFocused() string ID of the focused window — the topmost modal if one is open, otherwise the tracked focused field.
SetFocus(id string) Explicitly focuses a window. No-ops if a different modal is active.
IsFocused(id string) bool Whether the given ID currently has focus.
FocusNext() Cycles focus — between modals if any are open (Tab-among-modals), otherwise through the full Z-order stack.
GetAll() []*Window Every registered window, visible or not.
GetVisible() []*Window Visible windows, bottom-to-top.
Count() int / CountVisible() int Counts.
IsAnyModalOpen() bool True if any visible window is modal.
GetTopVisibleModal() *Window The topmost visible modal, or nil.
HasWindow(id string) bool Existence check.
IsWindowBlocked(id string) bool Whether a modal is open and it isn't this window.
GetActiveModal() string Topmost modal ID, or "".

Gotchas & Best Practices

  • Always return true/false deliberately from OnKeyPress. Returning true when you didn't actually handle the key will swallow it silently for everyone else; returning false when you did handle it can let the key "leak" into the background screen.
  • Close() is final. Once closed, a window is unregistered and cannot be reshown. If you need a reusable dialog, keep it around and use Hide() / Show() instead of Close() / NewWindow().
  • Only one modal blocks input at a time — the topmost one. Lower modals in the stack are inert until the ones above them are closed.
  • Escape has default behavior; Tab does not (much). Tab only auto-cycles focus among open modals — it does nothing special for non-modal windows or when nothing is open.
  • Don't forget SetScreenSize. If you never call it, Center() falls back to DefaultScreenWidth/DefaultScreenHeight (140×40), which may not match your actual terminal.
  • All state is protected by mutexes, so it's safe to call window/manager methods from goroutines (e.g. background workers closing a progress dialog when a task finishes). Rendering re-triggers are dispatched via go wm.triggerRender(), so they won't block the caller.

FAQ

Q: I pressed Escape and it closed my modal and my whole screen. What happened? A: Almost certainly your OnKeyPress handler for Escape did something (like calling Close()) but returned false instead of true, so the key kept propagating past the window layer into your background screen's own key handling. Return true whenever you act on a key.

Q: How do I make a window that can't be closed by Escape? A: Add an OnKeyPress handler that intercepts KeyEscape and returns true without calling Close():

w.OnKeyPress(func(key retui.Key) bool {
    if key.Code == retui.KeyEscape {
        return true // swallow it — do nothing
    }
    return false
})

Q: Can I have two modals open with neither blocking the other? A: No — modal means "blocks everything behind it," including other modals lower in the stack. If you need two independent floating panels, use non-modal windows instead.

Q: How do I know which window currently has keyboard focus? A: window.GetFocused() returns its ID; pass that to window.GetWindowByID(id) to get the *Window.

Clone this wiki locally