Controllers & Responses
Controllers are where your app’s behavior lives: they bind keys, run actions, kick off async tasks, and decide what happens next. Every action returns a response object — render, navigate, or quit — that the runtime then carries out. One controller instance handles every event for its screen, so instance variables are legal screen-lifetime state. Data that must outlive the screen goes in ApplicationState objects. For a guided introduction, see Controllers & Views.
Charming::Controller
Inherit from Charming::Controller or your app’s ApplicationController.
Class APIs
Key bindings and commands — how users trigger actions:
key name, action, scope: :contentbinds a content-pane key to an action.key name, action, scope: :globalbinds an app-level shortcut.command label, action = nil, &blockadds a command palette item (available when the controller includesCharming::Shell::Palette).
Component event declarations — how interactive components report results:
on_submit slot, actionroutes a component’s submitted value to an action.on_select slot, actionroutes a[:selected, value]component result to an action.on_cancel slot, actionroutes a:cancelledcomponent result to an action (no value argument).
The old <slot>_submitted naming convention still works but emits a one-time deprecation warning. A component result with no handler raises Charming::UnhandledComponentEvent in development and test; production logs a warning and falls back to the default render.
Timers and animation — periodic dispatch while the route is active:
timer name, every:, action:, autostart: truedispatches a periodic timer while the route is active (every:must be positive;autostart: falsewaits forstart_timer).animate name, fps:, action:declares a stopped animation timer ticking at fps frames per second (start it withstart_timer).
Async task callbacks:
on_task name, action:handles async task completion.on_task_progress name, action:handlesprogress.reportcalls from a running task.
Action hooks and error handling:
before_action method, only: nil, except: nilruns a hook before matching actions.after_action method, only: nil, except: nilruns a hook after matching actions.around_action method, only: nil, except: nilwraps matching actions (the hook mustyield).rescue_from ExceptionClass, with: :handlerhandles action exceptions (most-specific class wins).
Layout and focus:
layout layout_classwraps rendered output in a class-based layout view.layout "layouts/application"wraps rendered output in an ERB template layout fallback.layout falsedisables inherited layout wrapping.focus_ring *slotsdefines tab-traversable focus slots (the first slot starts focused).
Instance APIs
Dispatch — how the runtime invokes actions (app code rarely calls these directly):
dispatch(action, event: nil)calls an action (through its hooks) and returns a response.dispatch_key(event),dispatch_timer(event),dispatch_task(event),dispatch_task_progress(event),dispatch_mouse(event), anddispatch_paste(event)dispatch event-specific handlers.Controller.new(event:)is deprecated — the event arrives at dispatch time.
Screen lifecycle — override to manage per-screen resources:
screen_enteredruns after construction, before the first dispatch.screen_exitedruns before the instance is discarded on navigation or quit.
Rendering and navigation — what actions return:
render(body = "", **assigns)produces a render response.render "literal"renders a literal string.render :show, **assignsrenders a conventional Ruby view class, falling back toapp/views/<controller>/show.tui.erbor.txt.erb.render view_objectrenders a class-based view or component object.render_template(name, **assigns)renders an explicit template path underapp/views.navigate(name, **params)produces a navigation response. Params pass through as Ruby values.quitproduces a quit response.
State and session — where data survives between events:
sessionaccesses the application session.state(name, state_class, **attributes)stores or returns a session-backed state object.component_state(name, **defaults)is deprecated — use memoized instance variables (for example@query ||= Components::TextInput.new(...)) for widget state.loggerreturns the application logger.
Tasks and timers at runtime:
run_task(name, timeout: nil) { ... }submits async work; blocks accepting an argument receive aTasks::Progressreporter.cancel_task(name)cancels an in-flight task (raisesTasks::Cancelledinside it).start_timer(name),stop_timer(name), andtimer_running?(name)control declared timers at runtime (unknown names raiseArgumentError; both mutators are idempotent).
Request context — what the current event looks like:
paramsexposes the current screen’s navigate-time params.eventexposes the current key, timer, task, progress, resize, mouse, or paste event.screenexposes terminal dimensions.themereturns the current theme.use_theme(name)switches themes.
Palettes (from include Charming::Shell::Palette):
open_command_palette,close_command_palette, andcommand_palettemanage the command palette.open_theme_paletteopens the theme picker.command_palette_open?returns whether a command or theme palette is open.
Focus:
focusreturns the controller’s focus object (focus.push_scope,focus.pop_scope,focus.cycle,focus.focus(slot),focus.current,focus.ring,focus.overlay?).focused?(slot)asks whether a slot is currently focused.focus_sidebar,focus_content,sidebar_focused?, andcontent_focused?(frominclude Charming::Shell::Sidebar) support generated layouts (focus_contenttargets:contentor the first non-sidebar slot in the ring).sidebar_routes(fromCharming::Shell::Sidebar) returns the routes listed in the sidebar — override to filter.
One controller instance handles every event for its screen. Use instance variables for screen-lifetime state, state(:name, StateClass) objects for app-lifetime state, and persist_session for restart-lifetime state. Hooks do not run on component key dispatch — build focus-slot components from session/params, not hook-set instance variables. Data-bound components need both halves: memoize the component so its selection survives, and refresh its data on render (list.items = rows, table.rows = rows).
Charming::ApplicationState
Typed, session-backed state objects — the durable home for data that outlives a single screen. Inherit from Charming::ApplicationState:
class CounterState < Charming::ApplicationState
attribute :count, :integer, default: 0
end
It includes ActiveModel model and attributes support, so typed attributes and validations are available.
Common attribute types include :string, :integer, :float, :boolean, :date, :datetime, and :time.
Responses
Every controller action returns a response object telling the runtime what to do next. Controllers return response objects through helper methods:
render(...)creates a render response.navigate(name, **params)creates a navigation response.quitcreates a quit response.
Response factories:
Charming::Response.render(body)Charming::Response.navigate(name, **params)Charming::Response.quit
Response predicates:
response.navigate?response.quit?
Response attributes:
kindbodynameparams
The runtime follows navigation responses, renders render responses, and exits on quit responses.