Class: Charming::Application

Inherits:
Object
  • Object
show all
Defined in:
lib/charming/application.rb

Overview

Application is a lightweight, Rails-inspired application base for building terminal-based apps. It provides routing (via a DSL), session storage, and task execution for managing async operations.

Constant Summary collapse

LOGGER_READER =
Object.new.freeze
THEME_READER =
Object.new.freeze
COALESCE_READER =
Object.new.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeApplication

Initializes the session hash for per-request state storage, restoring a previously persisted session when persist_session is configured.



170
171
172
173
# File 'lib/charming/application.rb', line 170

def initialize
  @logger = self.class.logger
  @session = load_session
end

Instance Attribute Details

#loggerObject

Returns the value of attribute logger.



165
166
167
# File 'lib/charming/application.rb', line 165

def logger
  @logger
end

#sessionObject (readonly)

Returns the value of attribute session.



166
167
168
# File 'lib/charming/application.rb', line 166

def session
  @session
end

#task_executorObject

Returns the value of attribute task_executor.



165
166
167
# File 'lib/charming/application.rb', line 165

def task_executor
  @task_executor
end

Class Method Details

.coalesce_input(value = COALESCE_READER) ⇒ Object

When true, the runtime collapses bursts of identical key events — the flood a terminal emits while a key is held (OS auto-repeat) — into a single dispatch, so holding a key can't queue a backlog that keeps acting after release. Off by default: it also merges intentional fast repeats of the same key (e.g. tab tab), so enable it only for movement-style apps. Pass a boolean to set; call without args to read (inherited).



117
118
119
120
121
# File 'lib/charming/application.rb', line 117

def coalesce_input(value = COALESCE_READER)
  return configured_coalesce_input if value == COALESCE_READER

  @coalesce_input = value
end

.default_theme(name = THEME_READER) ⇒ Object

Returns the default theme name, or sets it when name is given. When unset, falls back to the first registered theme. Used by theme_for when no name is provided.



80
81
82
83
84
# File 'lib/charming/application.rb', line 80

def default_theme(name = THEME_READER)
  return @default_theme || themes.keys.first if name == THEME_READER

  @default_theme = name.to_sym
end

.logger(value = LOGGER_READER) ⇒ Object

Returns or sets the app logger. Defaults to a null-device logger so app and framework code can safely call logging methods without writing into the terminal UI.



32
33
34
35
36
# File 'lib/charming/application.rb', line 32

def logger(value = LOGGER_READER)
  return configured_logger if value == LOGGER_READER

  @logger = value
end

.mouse_motion(value = COALESCE_READER) ⇒ Object

The mouse motion reporting mode: :drag (default) reports movement only while a button is held; :all also reports buttonless hover movement. Pass a symbol to set; call without args to read (inherited).

Raises:

  • (ArgumentError)


126
127
128
129
130
131
# File 'lib/charming/application.rb', line 126

def mouse_motion(value = COALESCE_READER)
  return configured_mouse_motion if value == COALESCE_READER
  raise ArgumentError, "mouse_motion must be :drag or :all" unless %i[drag all].include?(value)

  @mouse_motion = value
end

.namespaceObject

Derives the module namespace from the class name — e.g., Admin::HomeController yields "Admin". Mirrors Rails' engine-style namespacing.



26
27
28
# File 'lib/charming/application.rb', line 26

def namespace
  ActiveSupport::Inflector.deconstantize(name.to_s)
end

.persist_session(to:) ⇒ Object

Opts into session persistence: the session hash is serialized as JSON to to when the app quits and reloaded on boot. Only JSON-safe values survive the round-trip (hash keys come back as symbols); non-serializable entries (state objects, procs) are skipped with a warning in the log.



99
100
101
# File 'lib/charming/application.rb', line 99

def persist_session(to:)
  @session_path = to
end

.root(path = THEME_READER) ⇒ Object

Returns the app's filesystem root, used to resolve relative theme and template paths. Pass path to set it; without arguments it returns the current value (or nil if unset).



40
41
42
43
44
# File 'lib/charming/application.rb', line 40

def root(path = THEME_READER)
  return @root if path == THEME_READER

  @root = File.expand_path(path)
end

.routes(&block) ⇒ Object

Registers or returns the app's Router. Accepts an optional block to define routes via DSL (screen, root). Lazily initializes a new Router per namespace.



18
19
20
21
22
# File 'lib/charming/application.rb', line 18

def routes(&block)
  @routes ||= Router.new(namespace: namespace)
  @routes.draw(&block) if block
  @routes
end

.session_pathObject

The configured session file path, walking the superclass chain. Nil when persistence is not enabled.



105
106
107
108
109
110
# File 'lib/charming/application.rb', line 105

def session_path
  return @session_path if instance_variable_defined?(:@session_path)
  return superclass.session_path if superclass.respond_to?(:session_path)

  nil
end

.theme(name, from: nil, built_in: nil, extends: nil, overrides: nil) ⇒ Object

Registers a named theme. Provide one of:

  • from: — path to a JSON theme file relative to the app root

  • built_in: — name of a bundled theme ("phosphor", "catppuccin-mocha", "catppuccin-latte", "gruvbox-dark", "nord", "tokyonight")

  • extends: — name of an already-registered theme to derive from, with overrides: (token name → style spec) merged on top:

    theme :dark, built_in: "tokyonight"
    theme :high_contrast, extends: :dark, overrides: {text: {foreground: "#ffffff"}}

Raises:

  • (ArgumentError)


55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/charming/application.rb', line 55

def theme(name, from: nil, built_in: nil, extends: nil, overrides: nil)
  sources = [from, built_in, extends].compact
  raise ArgumentError, "theme expects from:, built_in:, or extends:" if sources.empty?
  raise ArgumentError, "theme expects only one of from:, built_in:, or extends:" if sources.length > 1
  raise ArgumentError, "overrides: requires extends:" if overrides && !extends

  themes[name.to_sym] = if extends
    parent = themes.fetch(extends.to_sym) do
      raise ArgumentError, "unknown parent theme: #{extends.inspect} (register it before extending)"
    end
    parent.merge(overrides || {})
  elsif built_in
    UI::Theme.load_builtin(built_in)
  else
    UI::Theme.load_file(resolve_theme_path(from))
  end
end

.theme_for(name = nil) ⇒ Object

Resolves a theme by name (or the default theme when name is nil). Returns the default built-in theme if no name is given and no default is registered.



88
89
90
91
92
93
# File 'lib/charming/application.rb', line 88

def theme_for(name = nil)
  theme_name = name || default_theme
  return UI::Theme.default unless theme_name

  themes.fetch(theme_name.to_sym)
end

.themesObject

Hash of all registered themes keyed by symbol, including those inherited from superclasses.



74
75
76
# File 'lib/charming/application.rb', line 74

def themes
  @themes ||= superclass.respond_to?(:themes) ? superclass.themes.dup : {}
end

Instance Method Details

#coalesce_input?Boolean

Whether the runtime should coalesce held-key auto-repeat for this app (see the class-level coalesce_input DSL). Read by the Runtime at startup.

Returns:

  • (Boolean)


204
205
206
# File 'lib/charming/application.rb', line 204

def coalesce_input?
  self.class.coalesce_input == true
end

#mouse_motionObject

The mouse motion reporting mode for this app (see the class-level mouse_motion DSL). Read by the Runtime at startup.



210
211
212
# File 'lib/charming/application.rb', line 210

def mouse_motion
  self.class.mouse_motion
end

#routesObject

Delegates to the class-level Router, providing instance access to route definitions.



189
190
191
# File 'lib/charming/application.rb', line 189

def routes
  self.class.routes
end

#save_sessionObject

Serializes the session to the configured persist_session path. Entries that don't survive a JSON round-trip (state objects, procs, focus scopes) are skipped. No-op when persistence isn't configured. Called by the Runtime on exit.



178
179
180
181
182
183
184
185
186
# File 'lib/charming/application.rb', line 178

def save_session
  path = self.class.session_path
  return unless path

  FileUtils.mkdir_p(File.dirname(path))
  File.write(path, JSON.generate(serializable_session))
rescue => e
  logger.warn("session not saved: #{e.class}: #{e.message}")
end

#themeObject



193
194
195
# File 'lib/charming/application.rb', line 193

def theme
  self.class.theme_for(session[:theme])
end

#use_theme(name) ⇒ Object



197
198
199
200
# File 'lib/charming/application.rb', line 197

def use_theme(name)
  self.class.theme_for(name)
  session[:theme] = name.to_sym
end