Class: Charming::Controller
- Inherits:
-
Object
- Object
- Charming::Controller
- Defined in:
- lib/charming/controller.rb
Overview
Controller is the base class for all controller implementations in a Charming application. It provides the action dispatch pipeline, key/command/timer/task bindings, sidebar navigation, command palette management, and view rendering with layout composition.
Defined Under Namespace
Classes: TaskBinding, TimerBinding
Instance Attribute Summary collapse
-
#application ⇒ Object
readonly
Returns the value of attribute application.
-
#event ⇒ Object
readonly
Returns the value of attribute event.
-
#params ⇒ Object
readonly
Returns the value of attribute params.
-
#screen ⇒ Object
readonly
Returns the value of attribute screen.
Class Method Summary collapse
-
.command(label, action = nil, &block) ⇒ Object
Registers a command palette entry — visible in fuzzy search when Ctrl+K is pressed.
-
.command_bindings ⇒ Object
Returns inherited command bindings (command palette entries) from this controller and its ancestors.
-
.focus_ring(*slots) ⇒ Object
Registers a focus ring slot for this controller — slots participate in Tab/Shift+Tab traversal.
-
.focus_ring_slots ⇒ Object
Returns inherited focus ring slots merged from the class hierarchy.
-
.key(name, action, scope: :content) ⇒ Object
Registers a key binding (string or symbol key name → method symbol).
-
.key_binding_scopes ⇒ Object
Returns inherited key binding scopes merged from the class hierarchy.
-
.key_bindings ⇒ Object
Returns inherited key bindings merged from the class hierarchy.
-
.layout(layout_class = :__charming_layout_reader__) ⇒ Object
Sets the layout class to wrap this controller’s rendered output (e.g., for sidebar + main content).
-
.on_task(name, action:) ⇒ Object
Registers an async task handler that runs when a TaskEvent arrives from the task executor.
-
.task_bindings ⇒ Object
Returns inherited task bindings (async task handlers) from this controller and its ancestors.
-
.timer(name, every:, action:) ⇒ Object
Registers a periodic timer that fires at ‘every`-second intervals.
-
.timer_bindings ⇒ Object
Returns inherited timer bindings from this controller and its ancestors.
Instance Method Summary collapse
-
#close_command_palette ⇒ Object
Closes the command palette: removes its state from the session, pops its scope from the focus ring, and re-renders the default action.
-
#command_palette ⇒ Object
Returns a command palette component rebuilt from the current primitive session state, if open.
-
#command_palette_open? ⇒ Boolean
Returns whether the command palette is active in the current session.
-
#content_focused? ⇒ Boolean
Returns whether the main content area currently has focus (from focus ring or session state).
-
#dispatch(action) ⇒ Object
Dispatches a named action on this controller (e.g., :show).
-
#dispatch_key ⇒ Object
Key event dispatch pipeline for controllers: checks command palette first (if open), then global key bindings, then sidebar (if focused), then content-scoped key bindings, then tab traversal, then focused component handling.
-
#dispatch_mouse ⇒ Object
Mouse event dispatcher: checks command palette (if open), then sidebar (if focused), then falls through to component mouse dispatch.
-
#dispatch_task ⇒ Object
Task event dispatcher: looks up the event’s named handler in this controller’s task bindings and dispatches it.
-
#dispatch_timer ⇒ Object
Timer event dispatcher: looks up the event’s named action in this controller’s timer bindings and dispatches it.
-
#focus ⇒ Object
Returns or lazily initializes the Focus instance for this controller, which manages keyboard-driven focus traversal between components (sidebar, content, etc.).
-
#focus_content ⇒ Object
Shifts focus back to the main content area: moves the focus ring cursor or sets ‘session` to :content, and re-renders.
-
#focus_sidebar ⇒ Object
Shifts focus to the sidebar: moves the focus ring cursor or sets ‘session` to :sidebar, highlights the current route index, and re-renders.
-
#focused?(slot) ⇒ Boolean
Checks whether the given focus slot (e.g., :sidebar, :content) is currently focused.
-
#initialize(application:, event: nil, params: {}, screen: nil) ⇒ Controller
constructor
Initializes the controller with its parent application and an optional event (key/mouse/timer/task data).
-
#model(name, model_class, **attributes) ⇒ Object
Lazily instantiates a model class and caches it in the session under ‘:models`.
-
#navigate_to(path) ⇒ Object
Responds with a navigation redirect to the given URL path.
-
#open_command_palette ⇒ Object
Opens the command palette (fuzzy search UI): stores primitive palette state and pushes a palette scope onto the focus ring so input is captured inside it.
- #open_theme_palette ⇒ Object
-
#quit ⇒ Object
Exits the application — sets a quit response that terminates the event loop.
-
#render(body = "") ⇒ Object
Renders ‘body` wrapped in this controller’s layout (if one is defined) and stores the response.
-
#run_task(name, &block) ⇒ Object
Submits an async task to the application’s task executor (threaded or inline).
-
#session ⇒ Object
Returns the parent application’s session hash for per-request state storage (e.g., form data, flags).
-
#sidebar_focused? ⇒ Boolean
Returns whether the sidebar currently has focus (from focus ring or session state).
-
#sidebar_index ⇒ Object
Returns the currently highlighted sidebar item index, falling back to the current route’s position when no explicit sidebar selection has been made yet.
- #theme ⇒ Object
- #use_theme(name) ⇒ Object
Constructor Details
#initialize(application:, event: nil, params: {}, screen: nil) ⇒ Controller
Initializes the controller with its parent application and an optional event (key/mouse/timer/task data). Defaults to a 80x24 screen when no backend size is available.
107 108 109 110 111 112 113 |
# File 'lib/charming/controller.rb', line 107 def initialize(application:, event: nil, params: {}, screen: nil) @application = application @event = event @params = params @screen = screen || Screen.new(width: 80, height: 24) @response = nil end |
Instance Attribute Details
#application ⇒ Object (readonly)
Returns the value of attribute application.
103 104 105 |
# File 'lib/charming/controller.rb', line 103 def application @application end |
#event ⇒ Object (readonly)
Returns the value of attribute event.
103 104 105 |
# File 'lib/charming/controller.rb', line 103 def event @event end |
#params ⇒ Object (readonly)
Returns the value of attribute params.
103 104 105 |
# File 'lib/charming/controller.rb', line 103 def params @params end |
#screen ⇒ Object (readonly)
Returns the value of attribute screen.
103 104 105 |
# File 'lib/charming/controller.rb', line 103 def screen @screen end |
Class Method Details
.command(label, action = nil, &block) ⇒ Object
Registers a command palette entry — visible in fuzzy search when Ctrl+K is pressed. Accepts either a method symbol or an inline callable block.
23 24 25 |
# File 'lib/charming/controller.rb', line 23 def command(label, action = nil, &block) command_bindings << Components::CommandPalette::Command.new(label: label, value: block || action) end |
.command_bindings ⇒ Object
Returns inherited command bindings (command palette entries) from this controller and its ancestors.
70 71 72 |
# File 'lib/charming/controller.rb', line 70 def command_bindings @command_bindings ||= superclass.respond_to?(:command_bindings) ? superclass.command_bindings.dup : [] end |
.focus_ring(*slots) ⇒ Object
Registers a focus ring slot for this controller — slots participate in Tab/Shift+Tab traversal. Example: ‘focus_ring :sidebar, :content` makes sidebar and content tabbable.
60 61 62 |
# File 'lib/charming/controller.rb', line 60 def focus_ring(*slots) @focus_ring_slots = slots end |
.focus_ring_slots ⇒ Object
Returns inherited focus ring slots merged from the class hierarchy.
65 66 67 |
# File 'lib/charming/controller.rb', line 65 def focus_ring_slots @focus_ring_slots ||= superclass.respond_to?(:focus_ring_slots) ? superclass.focus_ring_slots.dup : [] end |
.key(name, action, scope: :content) ⇒ Object
Registers a key binding (string or symbol key name → method symbol). Content-scoped bindings run from the main content pane; global bindings run from any pane.
14 15 16 17 18 19 |
# File 'lib/charming/controller.rb', line 14 def key(name, action, scope: :content) normalized_scope = validate_key_scope(scope) key_name = name.to_sym key_bindings[key_name] = action key_binding_scopes[key_name] = normalized_scope end |
.key_binding_scopes ⇒ Object
Returns inherited key binding scopes merged from the class hierarchy. Each subclass gets a fresh copy of its parent’s scopes to match key binding inheritance.
54 55 56 |
# File 'lib/charming/controller.rb', line 54 def key_binding_scopes @key_binding_scopes ||= superclass.respond_to?(:key_binding_scopes) ? superclass.key_binding_scopes.dup : {} end |
.key_bindings ⇒ Object
Returns inherited key bindings merged from the class hierarchy. Each subclass gets a fresh copy of its parent’s key bindings to avoid cross-controller pollution.
48 49 50 |
# File 'lib/charming/controller.rb', line 48 def key_bindings @key_bindings ||= superclass.respond_to?(:key_bindings) ? superclass.key_bindings.dup : {} end |
.layout(layout_class = :__charming_layout_reader__) ⇒ Object
Sets the layout class to wrap this controller’s rendered output (e.g., for sidebar + main content). Accepts a special ‘:charming_layout_reader` sentinel to query — without setting — the current layout.
40 41 42 43 44 |
# File 'lib/charming/controller.rb', line 40 def layout(layout_class = :__charming_layout_reader__) return resolved_layout if layout_class == :__charming_layout_reader__ @layout = layout_class end |
.on_task(name, action:) ⇒ Object
Registers an async task handler that runs when a TaskEvent arrives from the task executor.
34 35 36 |
# File 'lib/charming/controller.rb', line 34 def on_task(name, action:) task_bindings[name.to_sym] = TaskBinding.new(name: name.to_sym, action: action) end |
.task_bindings ⇒ Object
Returns inherited task bindings (async task handlers) from this controller and its ancestors.
80 81 82 |
# File 'lib/charming/controller.rb', line 80 def task_bindings @task_bindings ||= superclass.respond_to?(:task_bindings) ? superclass.task_bindings.dup : {} end |
.timer(name, every:, action:) ⇒ Object
Registers a periodic timer that fires at ‘every`-second intervals. The named action is dispatched on the current route’s controller each time.
29 30 31 |
# File 'lib/charming/controller.rb', line 29 def timer(name, every:, action:) timer_bindings[name.to_sym] = TimerBinding.new(name: name.to_sym, interval: every, action: action) end |
.timer_bindings ⇒ Object
Returns inherited timer bindings from this controller and its ancestors.
75 76 77 |
# File 'lib/charming/controller.rb', line 75 def timer_bindings @timer_bindings ||= superclass.respond_to?(:timer_bindings) ? superclass.timer_bindings.dup : {} end |
Instance Method Details
#close_command_palette ⇒ Object
Closes the command palette: removes its state from the session, pops its scope from the focus ring, and re-renders the default action. Pops all nested scopes until only the palette remains.
220 221 222 223 224 |
# File 'lib/charming/controller.rb', line 220 def close_command_palette session.delete(:command_palette) pop_command_palette_scope render_default_action end |
#command_palette ⇒ Object
Returns a command palette component rebuilt from the current primitive session state, if open.
246 247 248 |
# File 'lib/charming/controller.rb', line 246 def command_palette build_command_palette_from_state(session[:command_palette]) if command_palette_open? end |
#command_palette_open? ⇒ Boolean
Returns whether the command palette is active in the current session.
241 242 243 |
# File 'lib/charming/controller.rb', line 241 def command_palette_open? session.key?(:command_palette) end |
#content_focused? ⇒ Boolean
Returns whether the main content area currently has focus (from focus ring or session state).
281 282 283 284 285 |
# File 'lib/charming/controller.rb', line 281 def content_focused? return focused?(:content) if focus_ring_slot?(:content) session[:focus] == :content end |
#dispatch(action) ⇒ Object
Dispatches a named action on this controller (e.g., :show). Calls the method via public_send, returning a default empty render if the action produces no response.
117 118 119 120 |
# File 'lib/charming/controller.rb', line 117 def dispatch(action) public_send(action) response || render("") end |
#dispatch_key ⇒ Object
Key event dispatch pipeline for controllers: checks command palette first (if open), then global key bindings, then sidebar (if focused), then content-scoped key bindings, then tab traversal, then focused component handling. Returns nil if no handler consumed the event.
125 126 127 128 129 130 131 132 133 134 135 |
# File 'lib/charming/controller.rb', line 125 def dispatch_key return dispatch_command_palette_key if command_palette_open? return dispatch(global_key_action) if global_key_action return if return dispatch(content_key_action) if content_key_action return response if dispatch_tab_traversal == :handled return response if dispatch_to_focused_component == :handled nil end |
#dispatch_mouse ⇒ Object
Mouse event dispatcher: checks command palette (if open), then sidebar (if focused), then falls through to component mouse dispatch. Always returns nil in the base controller —subclasses override as needed.
154 155 156 157 158 159 |
# File 'lib/charming/controller.rb', line 154 def dispatch_mouse return dispatch_command_palette_mouse if command_palette_open? return if dispatch_component_mouse end |
#dispatch_task ⇒ Object
Task event dispatcher: looks up the event’s named handler in this controller’s task bindings and dispatches it. Used by async tasks submitted via ‘run_task`.
146 147 148 149 |
# File 'lib/charming/controller.rb', line 146 def dispatch_task binding = self.class.task_bindings[event.name.to_sym] binding ? dispatch(binding.action) : nil end |
#dispatch_timer ⇒ Object
Timer event dispatcher: looks up the event’s named action in this controller’s timer bindings and dispatches it. Returns nil if no binding exists for this timer name.
139 140 141 142 |
# File 'lib/charming/controller.rb', line 139 def dispatch_timer binding = self.class.timer_bindings[event.name.to_sym] binding ? dispatch(binding.action) : nil end |
#focus ⇒ Object
Returns or lazily initializes the Focus instance for this controller, which manages keyboard-driven focus traversal between components (sidebar, content, etc.). Defines focus ring slots from class-level declarations on first access.
229 230 231 232 233 |
# File 'lib/charming/controller.rb', line 229 def focus @focus ||= Focus.for(session, self.class).tap do |f| f.define(self.class.focus_ring_slots) unless self.class.focus_ring_slots.empty? end end |
#focus_content ⇒ Object
Shifts focus back to the main content area: moves the focus ring cursor or sets ‘session` to :content, and re-renders. Used by Escape key from sidebar and other navigation transitions.
264 265 266 267 268 269 270 271 |
# File 'lib/charming/controller.rb', line 264 def focus_content if focus_ring_slot?(:content) focus.focus(:content) else session[:focus] = :content end render_default_action end |
#focus_sidebar ⇒ Object
Shifts focus to the sidebar: moves the focus ring cursor or sets ‘session` to :sidebar, highlights the current route index, and re-renders. Sidebar selection uses j/k keys.
252 253 254 255 256 257 258 259 260 |
# File 'lib/charming/controller.rb', line 252 def if focus_ring_slot?(:sidebar) focus.focus(:sidebar) else session[:focus] = :sidebar end session[:sidebar_index] ||= current_route_index render_default_action end |
#focused?(slot) ⇒ Boolean
Checks whether the given focus slot (e.g., :sidebar, :content) is currently focused.
236 237 238 |
# File 'lib/charming/controller.rb', line 236 def focused?(slot) focus.focused?(slot) end |
#model(name, model_class, **attributes) ⇒ Object
Lazily instantiates a model class and caches it in the session under ‘:models`. Subsequent calls with the same name return the cached instance. Used like: model(:user, UserModel)
199 200 201 202 |
# File 'lib/charming/controller.rb', line 199 def model(name, model_class, **attributes) session[:models] ||= {} session[:models][name.to_sym] ||= model_class.new(**attributes) end |
#navigate_to(path) ⇒ Object
Responds with a navigation redirect to the given URL path. Used for route transitions triggered from controllers (e.g., sidebar selection).
183 184 185 |
# File 'lib/charming/controller.rb', line 183 def navigate_to(path) @response = Response.navigate(path) end |
#open_command_palette ⇒ Object
Opens the command palette (fuzzy search UI): stores primitive palette state and pushes a palette scope onto the focus ring so input is captured inside it. Renders the default action afterward.
212 213 214 215 216 |
# File 'lib/charming/controller.rb', line 212 def open_command_palette session[:command_palette] = command_palette_state(:commands) focus.push_scope([:command_palette], origin: :command_palette) render_default_action end |
#open_theme_palette ⇒ Object
175 176 177 178 179 |
# File 'lib/charming/controller.rb', line 175 def open_theme_palette session[:command_palette] = command_palette_state(:themes) focus.push_scope([:command_palette], origin: :command_palette) render_default_action end |
#quit ⇒ Object
Exits the application — sets a quit response that terminates the event loop.
188 189 190 |
# File 'lib/charming/controller.rb', line 188 def quit @response = Response.quit end |
#render(body = "") ⇒ Object
Renders ‘body` wrapped in this controller’s layout (if one is defined) and stores the response. If no layout is set, renders body bare. Called by controllers after rendering a view.
163 164 165 |
# File 'lib/charming/controller.rb', line 163 def render(body = "") @response = Response.render(render_with_layout(body)) end |
#run_task(name, &block) ⇒ Object
Submits an async task to the application’s task executor (threaded or inline). The task runs in a background thread; results arrive as TaskEvents in ‘dispatch_task`.
206 207 208 |
# File 'lib/charming/controller.rb', line 206 def run_task(name, &block) application.task_executor.submit(name, &block) end |
#session ⇒ Object
Returns the parent application’s session hash for per-request state storage (e.g., form data, flags).
193 194 195 |
# File 'lib/charming/controller.rb', line 193 def session application.session end |
#sidebar_focused? ⇒ Boolean
Returns whether the sidebar currently has focus (from focus ring or session state).
274 275 276 277 278 |
# File 'lib/charming/controller.rb', line 274 def return focused?(:sidebar) if focus_ring_slot?(:sidebar) session[:focus] == :sidebar end |
#sidebar_index ⇒ Object
Returns the currently highlighted sidebar item index, falling back to the current route’s position when no explicit sidebar selection has been made yet.
289 290 291 |
# File 'lib/charming/controller.rb', line 289 def session[:sidebar_index] || current_route_index end |
#theme ⇒ Object
167 168 169 |
# File 'lib/charming/controller.rb', line 167 def theme application.theme end |
#use_theme(name) ⇒ Object
171 172 173 |
# File 'lib/charming/controller.rb', line 171 def use_theme(name) application.use_theme(name) end |