-
Notifications
You must be signed in to change notification settings - Fork 4
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.
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().
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 viawindow.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
| 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 |
-
Always return
true/falsedeliberately fromOnKeyPress. Returningtruewhen you didn't actually handle the key will swallow it silently for everyone else; returningfalsewhen 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 useHide()/Show()instead ofClose()/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 toDefaultScreenWidth/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.
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.
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.
- [Concepts](#concepts)
- [What is a Window?](#what-is-a-window)
- [The WindowManager](#the-windowmanager)
- [Modal vs. Non-Modal](#modal-vs-non-modal)
- [Z-Order and Focus](#z-order-and-focus)
- [Key Event Propagation](#key-event-propagation)
- [Getting Started](#getting-started)
- [Common Recipes](#common-recipes)
- [A Simple Info Window](#a-simple-info-window)
- [A Modal Edit Dialog (F2 to Edit)](#a-modal-edit-dialog-f2-to-edit)
- [Handling Escape Correctly](#handling-escape-correctly)
- [Centering and Positioning](#centering-and-positioning)
- [Stacked Modals](#stacked-modals)
- [API Reference](#api-reference)
- [Gotchas & Best Practices](#gotchas--best-practices)
- [FAQ](#faq)
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().
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 viawindow.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
| 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().
- The manager keeps an ordered
stackof 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
SetFocusor 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.
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
OnKeyPresscallback returnstrue, thattruebubbles all the way back up. - If it reaches all the way back to
App.Run()astrue, 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 anOnKeyPresshandler for it.
⚠️ Always returntruefromOnKeyPresswhen 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 returntrue, 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").
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.RootRenderWrapis already wired up by thewindowpackage'sinit()to composite window overlays on top of your root element automatically.
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.
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
})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
falsefor Escape (or omit the case), the built-in fallback takes over and closes the window anyway — your custom logic is skipped for that keypress. Alwaysreturn trueonce you've decided to act on Escape yourself.
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)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 underneathConvenience 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. |
func NewWindow() *WindowCreates a new window with sensible defaults (40x15, centered on the default screen size, not modal, hidden). Chain configuration methods, then call .Show().
| 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. |
| 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. |
| 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(). |
| 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]. |
| 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. |
| 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. |
| 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. |
| 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. |
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 "". |
-
Always return
true/falsedeliberately fromOnKeyPress. Returningtruewhen you didn't actually handle the key will swallow it silently for everyone else; returningfalsewhen 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 useHide()/Show()instead ofClose()/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 toDefaultScreenWidth/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.
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.