Class: Charming::Controller

Inherits:
Object
  • Object
show all
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

Class Method Summary collapse

Instance Method Summary collapse

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

#applicationObject (readonly)

Returns the value of attribute application.



103
104
105
# File 'lib/charming/controller.rb', line 103

def application
  @application
end

#eventObject (readonly)

Returns the value of attribute event.



103
104
105
# File 'lib/charming/controller.rb', line 103

def event
  @event
end

#paramsObject (readonly)

Returns the value of attribute params.



103
104
105
# File 'lib/charming/controller.rb', line 103

def params
  @params
end

#screenObject (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_bindingsObject

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_slotsObject

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_scopesObject

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_bindingsObject

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_bindingsObject

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_bindingsObject

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_paletteObject

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_paletteObject

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.

Returns:

  • (Boolean)


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).

Returns:

  • (Boolean)


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_keyObject

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 dispatch_sidebar_key if sidebar_focused?

  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_mouseObject

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 dispatch_sidebar_mouse if sidebar_focused?

  dispatch_component_mouse
end

#dispatch_taskObject

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_timerObject

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

#focusObject

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_contentObject

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_sidebarObject

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 focus_sidebar
  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.

Returns:

  • (Boolean)


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

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_paletteObject

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_paletteObject



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

#quitObject

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

#sessionObject

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

Returns whether the sidebar currently has focus (from focus ring or session state).

Returns:

  • (Boolean)


274
275
276
277
278
# File 'lib/charming/controller.rb', line 274

def sidebar_focused?
  return focused?(:sidebar) if focus_ring_slot?(:sidebar)

  session[:focus] == :sidebar
end

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 sidebar_index
  session[:sidebar_index] || current_route_index
end

#themeObject



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