charming 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +2 -0
- data/lib/charming/application.rb +91 -14
- data/lib/charming/application_state.rb +23 -0
- data/lib/charming/cli.rb +2 -2
- data/lib/charming/controller/action_hooks.rb +5 -1
- data/lib/charming/controller/class_methods.rb +66 -15
- data/lib/charming/controller/component_dispatch.rb +163 -0
- data/lib/charming/controller/dispatching.rb +1 -2
- data/lib/charming/controller/focus_management.rb +67 -1
- data/lib/charming/controller/key_dispatch.rb +7 -7
- data/lib/charming/controller/rendering.rb +22 -6
- data/lib/charming/controller/session_state.rb +63 -22
- data/lib/charming/controller.rb +247 -59
- data/lib/charming/cross_thread_access.rb +9 -0
- data/lib/charming/double_render_error.rb +8 -0
- data/lib/charming/generators/layout_generator.rb +4 -1
- data/lib/charming/generators/migration_generator.rb +1 -1
- data/lib/charming/generators/model_generator.rb +2 -2
- data/lib/charming/generators/name.rb +1 -1
- data/lib/charming/generators/screen_generator.rb +2 -2
- data/lib/charming/generators/view_generator.rb +1 -1
- data/lib/charming/internal/deep_freeze.rb +23 -0
- data/lib/charming/internal/env_inquirer.rb +22 -0
- data/lib/charming/internal/inflections.rb +93 -0
- data/lib/charming/internal/session_guard.rb +27 -0
- data/lib/charming/internal/terminal/cursor.rb +29 -0
- data/lib/charming/internal/terminal/size.rb +47 -0
- data/lib/charming/internal/terminal/tty_backend.rb +10 -8
- data/lib/charming/presentation/components/autocomplete.rb +14 -6
- data/lib/charming/presentation/components/command_palette.rb +11 -9
- data/lib/charming/presentation/components/filepicker.rb +4 -4
- data/lib/charming/presentation/components/form/confirm.rb +2 -1
- data/lib/charming/presentation/components/form/field.rb +1 -1
- data/lib/charming/presentation/components/form/input.rb +3 -3
- data/lib/charming/presentation/components/form/multiselect.rb +4 -5
- data/lib/charming/presentation/components/form/select.rb +3 -3
- data/lib/charming/presentation/components/form/textarea.rb +3 -3
- data/lib/charming/presentation/components/form.rb +6 -6
- data/lib/charming/presentation/components/help_overlay.rb +2 -2
- data/lib/charming/presentation/components/keyboard_handler.rb +3 -3
- data/lib/charming/presentation/components/list.rb +14 -5
- data/lib/charming/presentation/components/modal.rb +3 -2
- data/lib/charming/presentation/components/multi_select_list.rb +14 -7
- data/lib/charming/presentation/components/result.rb +61 -0
- data/lib/charming/presentation/components/tab_bar.rb +14 -6
- data/lib/charming/presentation/components/table.rb +64 -32
- data/lib/charming/presentation/components/text_area.rb +7 -7
- data/lib/charming/presentation/components/text_input.rb +7 -7
- data/lib/charming/presentation/components/tree.rb +15 -6
- data/lib/charming/presentation/components/viewport.rb +3 -3
- data/lib/charming/presentation/layout/pane.rb +6 -2
- data/lib/charming/presentation/layout/screen_layout.rb +7 -0
- data/lib/charming/presentation/view.rb +35 -29
- data/lib/charming/render_artifacts.rb +24 -0
- data/lib/charming/response.rb +19 -8
- data/lib/charming/router.rb +50 -68
- data/lib/charming/runtime.rb +42 -26
- data/lib/charming/{controller/command_palette.rb → shell/palette.rb} +43 -11
- data/lib/charming/{controller/sidebar_navigation.rb → shell/sidebar.rb} +11 -11
- data/lib/charming/tasks/context.rb +35 -0
- data/lib/charming/test_helper.rb +42 -22
- data/lib/charming/unhandled_component_event.rb +9 -0
- data/lib/charming/unknown_slot.rb +9 -0
- data/lib/charming/version.rb +1 -1
- data/lib/charming/welcome.rb +1 -1
- data/lib/charming.rb +14 -6
- metadata +21 -70
- data/lib/charming/controller/component_dispatching.rb +0 -125
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Charming
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
module SidebarNavigation
|
|
4
|
+
# Shell::Sidebar is the opt-in app-shell sidebar: route listing, sidebar/content focus
|
|
5
|
+
# split, j/k/enter navigation, and mouse clicks on route rows. Generated apps include it
|
|
6
|
+
# in ApplicationController when generated with the sidebar layout
|
|
7
|
+
# (`include Charming::Shell::Sidebar`). Controllers that want Tab-driven sidebar
|
|
8
|
+
# navigation declare `focus_ring :sidebar, :content`.
|
|
9
|
+
module Shell
|
|
10
|
+
module Sidebar
|
|
12
11
|
# Moves focus to the sidebar slot and remembers the highlighted route.
|
|
13
12
|
def focus_sidebar
|
|
14
13
|
focus.focus(:sidebar)
|
|
@@ -37,7 +36,8 @@ module Charming
|
|
|
37
36
|
end
|
|
38
37
|
|
|
39
38
|
# Returns the index of the currently selected route in `sidebar_routes`, defaulting to the
|
|
40
|
-
# active route when the session index is unset.
|
|
39
|
+
# active route when the session index is unset. Stored in the session: the sidebar cursor
|
|
40
|
+
# is app-global — it should keep its position as the user moves between screens.
|
|
41
41
|
def sidebar_index
|
|
42
42
|
session[:sidebar_index] || current_route_index
|
|
43
43
|
end
|
|
@@ -52,7 +52,7 @@ module Charming
|
|
|
52
52
|
def current_route?(candidate)
|
|
53
53
|
return candidate.controller_class == self.class && candidate.action == :show unless route
|
|
54
54
|
|
|
55
|
-
candidate.
|
|
55
|
+
candidate.name == route.name &&
|
|
56
56
|
candidate.controller_class == route.controller_class &&
|
|
57
57
|
candidate.action == route.action
|
|
58
58
|
end
|
|
@@ -139,7 +139,7 @@ module Charming
|
|
|
139
139
|
route = sidebar_routes[sidebar_index]
|
|
140
140
|
slot = content_slot
|
|
141
141
|
focus.focus(slot) if slot
|
|
142
|
-
route ?
|
|
142
|
+
route ? navigate(route.name) : render_default_action
|
|
143
143
|
end
|
|
144
144
|
end
|
|
145
145
|
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Charming
|
|
4
|
+
module Tasks
|
|
5
|
+
# Context is the object a task block receives as its argument. It carries the
|
|
6
|
+
# task's inputs (the `with:` hash given to `run_task`, deep-frozen at submit time)
|
|
7
|
+
# and the progress reporter:
|
|
8
|
+
#
|
|
9
|
+
# run_task(:search, with: {query: query.value}) do |ctx|
|
|
10
|
+
# results = Api.search(ctx[:query])
|
|
11
|
+
# ctx.report(1, of: 1)
|
|
12
|
+
# results
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# Task blocks receive data in via `with:` and return data out as the block value.
|
|
16
|
+
# They touch nothing else: the `on_task` handler on the loop thread is the only
|
|
17
|
+
# place task results become state.
|
|
18
|
+
class Context
|
|
19
|
+
def initialize(data, progress)
|
|
20
|
+
@data = data
|
|
21
|
+
@progress = progress
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Returns the input value for *key* from the `with:` hash.
|
|
25
|
+
def [](key)
|
|
26
|
+
@data[key]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Reports progress to the matching `on_task_progress` handler on the loop thread.
|
|
30
|
+
def report(current, of: nil, message: nil)
|
|
31
|
+
@progress.report(current, of: of, message: message)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/charming/test_helper.rb
CHANGED
|
@@ -18,27 +18,28 @@ module Charming
|
|
|
18
18
|
# end
|
|
19
19
|
#
|
|
20
20
|
# it "quits on q" do
|
|
21
|
-
# expect(press(
|
|
21
|
+
# expect(press(ctrl, "q")).to be_quit
|
|
22
22
|
# end
|
|
23
23
|
# end
|
|
24
24
|
#
|
|
25
25
|
# Helpers:
|
|
26
|
-
# - `build_controller(klass, app:, screen:, route:)` — controller instance wired to an app
|
|
26
|
+
# - `build_controller(klass, app:, screen:, route:, params:)` — controller instance wired to an app
|
|
27
27
|
# - `key_event("ctrl+p")` — build a KeyEvent from a human-readable string
|
|
28
|
-
# - `press(
|
|
29
|
-
# - `press_sequence(
|
|
28
|
+
# - `press(controller, "down")` — dispatch a key press at the instance, returns the Response
|
|
29
|
+
# - `press_sequence(controller, ["down", "down", "enter"])` — dispatch several presses at one instance
|
|
30
30
|
#
|
|
31
31
|
# RSpec matchers (when RSpec is loaded):
|
|
32
32
|
# - `expect(response).to render_text("...")` / `render_match(/.../)`
|
|
33
33
|
# - `expect(response).to be_quit` / `be_navigate` (predicate matchers on Response)
|
|
34
|
-
# - `expect(response).to navigate_to(
|
|
34
|
+
# - `expect(response).to navigate_to(:projects)`
|
|
35
35
|
module TestHelper
|
|
36
|
-
# Builds a controller instance with sensible test defaults: a fresh Application
|
|
37
|
-
# an 80x24 screen,
|
|
38
|
-
|
|
36
|
+
# Builds a controller instance with sensible test defaults: a fresh Application and
|
|
37
|
+
# an 80x24 screen. Runs the screen_entered lifecycle hook, mirroring the Runtime's
|
|
38
|
+
# persistent-controller lifecycle.
|
|
39
|
+
def build_controller(controller_class, app: nil, screen: nil, route: nil, params: {})
|
|
39
40
|
app ||= Charming::Application.new
|
|
40
41
|
screen ||= Charming::Screen.new(width: 80, height: 24)
|
|
41
|
-
controller_class.new(application: app,
|
|
42
|
+
controller_class.new(application: app, params: params, screen: screen, route: route).tap(&:screen_entered)
|
|
42
43
|
end
|
|
43
44
|
|
|
44
45
|
# Builds a KeyEvent from a human-readable string like "q", "down", "ctrl+p",
|
|
@@ -57,18 +58,16 @@ module Charming
|
|
|
57
58
|
)
|
|
58
59
|
end
|
|
59
60
|
|
|
60
|
-
# Dispatches a single key press
|
|
61
|
-
#
|
|
62
|
-
def press(
|
|
63
|
-
controller
|
|
64
|
-
controller.dispatch_key
|
|
61
|
+
# Dispatches a single key press at *controller* (an instance from build_controller)
|
|
62
|
+
# and returns the Response. Accepts a human-readable string or a ready KeyEvent.
|
|
63
|
+
def press(controller, key)
|
|
64
|
+
controller.dispatch_key(key.is_a?(String) ? key_event(key) : key)
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
-
# Dispatches each key in *keys* in order
|
|
68
|
-
#
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
keys.map { |key| press(controller_class, key, app: app, screen: screen, route: route) }.last
|
|
67
|
+
# Dispatches each key in *keys* in order at the same controller instance (mirroring
|
|
68
|
+
# the runtime's persistent-controller model). Returns the last Response.
|
|
69
|
+
def press_sequence(controller, keys)
|
|
70
|
+
keys.map { |key| press(controller, key) }.last
|
|
72
71
|
end
|
|
73
72
|
|
|
74
73
|
# Builds a MemoryBackend pre-seeded with KeyEvents parsed from *keys*, ready to be
|
|
@@ -77,6 +76,24 @@ module Charming
|
|
|
77
76
|
events = keys.map { |key| key.is_a?(String) ? key_event(key) : key }
|
|
78
77
|
Charming::Internal::Terminal::MemoryBackend.new(events: events, width: width, height: height)
|
|
79
78
|
end
|
|
79
|
+
|
|
80
|
+
# Renders a view class standalone — no controller needed — and returns a hash with
|
|
81
|
+
# the painted :frame plus the layout's registration data (:focus_slots,
|
|
82
|
+
# :mouse_targets). Rendering is pure: registrations only commit when a response
|
|
83
|
+
# paints, so unit-testing a view means asserting on these artifacts.
|
|
84
|
+
#
|
|
85
|
+
# result = render_view(Home::ShowView, screen: Charming::Screen.new(width: 40, height: 10))
|
|
86
|
+
# expect(result[:focus_slots]).to eq([:entries])
|
|
87
|
+
def render_view(view_class, **assigns)
|
|
88
|
+
view = view_class.new(**assigns)
|
|
89
|
+
frame = view.render
|
|
90
|
+
artifacts = view.render_artifacts
|
|
91
|
+
{
|
|
92
|
+
frame: frame,
|
|
93
|
+
focus_slots: artifacts.last&.focus_slots || [],
|
|
94
|
+
mouse_targets: artifacts.flat_map(&:mouse_targets)
|
|
95
|
+
}
|
|
96
|
+
end
|
|
80
97
|
end
|
|
81
98
|
end
|
|
82
99
|
|
|
@@ -108,13 +125,16 @@ if defined?(RSpec)
|
|
|
108
125
|
end
|
|
109
126
|
end
|
|
110
127
|
|
|
111
|
-
RSpec::Matchers.define :navigate_to do |
|
|
128
|
+
RSpec::Matchers.define :navigate_to do |expected_name, **expected_params|
|
|
112
129
|
match do |response|
|
|
113
|
-
response.respond_to?(:navigate?) && response.navigate?
|
|
130
|
+
next false unless response.respond_to?(:navigate?) && response.navigate?
|
|
131
|
+
next false unless response.name == expected_name.to_sym
|
|
132
|
+
|
|
133
|
+
expected_params.empty? || response.params == expected_params
|
|
114
134
|
end
|
|
115
135
|
|
|
116
136
|
failure_message do |response|
|
|
117
|
-
"expected a navigation response to #{
|
|
137
|
+
"expected a navigation response to #{expected_name.inspect}, got: #{response.inspect}"
|
|
118
138
|
end
|
|
119
139
|
end
|
|
120
140
|
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Charming
|
|
4
|
+
# Raised when a component emits a result (`[:submitted, value]`, `[:selected, value]`,
|
|
5
|
+
# or `:cancelled`) and the controller has neither an `on_submit`/`on_select`/`on_cancel`
|
|
6
|
+
# registration nor a legacy `<slot>_<event>` hook for it. Raised in development and test;
|
|
7
|
+
# production logs a warning and falls back to the default render instead.
|
|
8
|
+
class UnhandledComponentEvent < Error; end
|
|
9
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Charming
|
|
4
|
+
# Raised when a rendered layout names a focusable pane, or an `on_*` registration
|
|
5
|
+
# references a slot, that nothing declares — not a `slot` declaration, not a
|
|
6
|
+
# `focus_ring` entry, not a same-named method. Raised in development and test;
|
|
7
|
+
# production logs a warning. The message names the fix and lists declared slots.
|
|
8
|
+
class UnknownSlot < Error; end
|
|
9
|
+
end
|
data/lib/charming/version.rb
CHANGED
data/lib/charming/welcome.rb
CHANGED
|
@@ -7,7 +7,7 @@ module Charming
|
|
|
7
7
|
module Welcome
|
|
8
8
|
# The fallback route the Runtime uses when the application has no routes.
|
|
9
9
|
def self.route
|
|
10
|
-
Router::Route.new(
|
|
10
|
+
Router::Route.new(name: :root, controller_class: Controller, action: :show, title: "Welcome", params: {})
|
|
11
11
|
end
|
|
12
12
|
end
|
|
13
13
|
end
|
data/lib/charming.rb
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "active_support/inflector"
|
|
4
|
-
require "active_support/string_inquirer"
|
|
5
3
|
require "logger"
|
|
6
4
|
require "zeitwerk"
|
|
7
5
|
|
|
@@ -26,15 +24,15 @@ module Charming
|
|
|
26
24
|
# Base error class for all Charming-specific exceptions (used by templates, generators, runtime, etc.).
|
|
27
25
|
class Error < StandardError; end
|
|
28
26
|
|
|
29
|
-
# The current environment, read from CHARMING_ENV (default "development"). Returns
|
|
30
|
-
#
|
|
27
|
+
# The current environment, read from CHARMING_ENV (default "development"). Returns an
|
|
28
|
+
# EnvInquirer so callers can write `Charming.env.test?` / `Charming.env.production?`.
|
|
31
29
|
def self.env
|
|
32
|
-
@env ||=
|
|
30
|
+
@env ||= Internal::EnvInquirer.new(ENV["CHARMING_ENV"] || "development")
|
|
33
31
|
end
|
|
34
32
|
|
|
35
33
|
# Overrides the environment (used by tests and the CLI).
|
|
36
34
|
def self.env=(value)
|
|
37
|
-
@env = value.nil? ? nil :
|
|
35
|
+
@env = value.nil? ? nil : Internal::EnvInquirer.new(value.to_s)
|
|
38
36
|
end
|
|
39
37
|
|
|
40
38
|
# Entry point for running a Charming application. Instantiates a Runtime for *application* and starts
|
|
@@ -84,6 +82,16 @@ module Charming
|
|
|
84
82
|
|
|
85
83
|
:"#{(ordered_modifiers + [key]).join("+")}"
|
|
86
84
|
end
|
|
85
|
+
|
|
86
|
+
# Emits a deprecation warning once per process per *category*, prefixed
|
|
87
|
+
# "[charming deprecation]". Warnings go to stderr.
|
|
88
|
+
def self.deprecate(message, category:)
|
|
89
|
+
@deprecation_emitted ||= {}
|
|
90
|
+
return if @deprecation_emitted[category]
|
|
91
|
+
|
|
92
|
+
@deprecation_emitted[category] = true
|
|
93
|
+
warn("[charming deprecation] #{message}")
|
|
94
|
+
end
|
|
87
95
|
end
|
|
88
96
|
|
|
89
97
|
Charming::Templates.register ".tui.erb", Charming::Templates::ErbHandler
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: charming
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- pando
|
|
@@ -29,26 +29,6 @@ dependencies:
|
|
|
29
29
|
- - ">="
|
|
30
30
|
- !ruby/object:Gem::Version
|
|
31
31
|
version: 8.1.2
|
|
32
|
-
- !ruby/object:Gem::Dependency
|
|
33
|
-
name: activesupport
|
|
34
|
-
requirement: !ruby/object:Gem::Requirement
|
|
35
|
-
requirements:
|
|
36
|
-
- - "~>"
|
|
37
|
-
- !ruby/object:Gem::Version
|
|
38
|
-
version: '8.1'
|
|
39
|
-
- - ">="
|
|
40
|
-
- !ruby/object:Gem::Version
|
|
41
|
-
version: 8.1.2
|
|
42
|
-
type: :runtime
|
|
43
|
-
prerelease: false
|
|
44
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
45
|
-
requirements:
|
|
46
|
-
- - "~>"
|
|
47
|
-
- !ruby/object:Gem::Version
|
|
48
|
-
version: '8.1'
|
|
49
|
-
- - ">="
|
|
50
|
-
- !ruby/object:Gem::Version
|
|
51
|
-
version: 8.1.2
|
|
52
32
|
- !ruby/object:Gem::Dependency
|
|
53
33
|
name: commonmarker
|
|
54
34
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -91,48 +71,6 @@ dependencies:
|
|
|
91
71
|
- - "~>"
|
|
92
72
|
- !ruby/object:Gem::Version
|
|
93
73
|
version: '5.0'
|
|
94
|
-
- !ruby/object:Gem::Dependency
|
|
95
|
-
name: tty-cursor
|
|
96
|
-
requirement: !ruby/object:Gem::Requirement
|
|
97
|
-
requirements:
|
|
98
|
-
- - "~>"
|
|
99
|
-
- !ruby/object:Gem::Version
|
|
100
|
-
version: '0.7'
|
|
101
|
-
type: :runtime
|
|
102
|
-
prerelease: false
|
|
103
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
104
|
-
requirements:
|
|
105
|
-
- - "~>"
|
|
106
|
-
- !ruby/object:Gem::Version
|
|
107
|
-
version: '0.7'
|
|
108
|
-
- !ruby/object:Gem::Dependency
|
|
109
|
-
name: zeitwerk
|
|
110
|
-
requirement: !ruby/object:Gem::Requirement
|
|
111
|
-
requirements:
|
|
112
|
-
- - "~>"
|
|
113
|
-
- !ruby/object:Gem::Version
|
|
114
|
-
version: '2.6'
|
|
115
|
-
type: :runtime
|
|
116
|
-
prerelease: false
|
|
117
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
118
|
-
requirements:
|
|
119
|
-
- - "~>"
|
|
120
|
-
- !ruby/object:Gem::Version
|
|
121
|
-
version: '2.6'
|
|
122
|
-
- !ruby/object:Gem::Dependency
|
|
123
|
-
name: tty-table
|
|
124
|
-
requirement: !ruby/object:Gem::Requirement
|
|
125
|
-
requirements:
|
|
126
|
-
- - "~>"
|
|
127
|
-
- !ruby/object:Gem::Version
|
|
128
|
-
version: '0.12'
|
|
129
|
-
type: :runtime
|
|
130
|
-
prerelease: false
|
|
131
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
132
|
-
requirements:
|
|
133
|
-
- - "~>"
|
|
134
|
-
- !ruby/object:Gem::Version
|
|
135
|
-
version: '0.12'
|
|
136
74
|
- !ruby/object:Gem::Dependency
|
|
137
75
|
name: tty-reader
|
|
138
76
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -148,21 +86,21 @@ dependencies:
|
|
|
148
86
|
- !ruby/object:Gem::Version
|
|
149
87
|
version: '0.9'
|
|
150
88
|
- !ruby/object:Gem::Dependency
|
|
151
|
-
name:
|
|
89
|
+
name: unicode-display_width
|
|
152
90
|
requirement: !ruby/object:Gem::Requirement
|
|
153
91
|
requirements:
|
|
154
92
|
- - "~>"
|
|
155
93
|
- !ruby/object:Gem::Version
|
|
156
|
-
version: '
|
|
94
|
+
version: '2.6'
|
|
157
95
|
type: :runtime
|
|
158
96
|
prerelease: false
|
|
159
97
|
version_requirements: !ruby/object:Gem::Requirement
|
|
160
98
|
requirements:
|
|
161
99
|
- - "~>"
|
|
162
100
|
- !ruby/object:Gem::Version
|
|
163
|
-
version: '
|
|
101
|
+
version: '2.6'
|
|
164
102
|
- !ruby/object:Gem::Dependency
|
|
165
|
-
name:
|
|
103
|
+
name: zeitwerk
|
|
166
104
|
requirement: !ruby/object:Gem::Requirement
|
|
167
105
|
requirements:
|
|
168
106
|
- - "~>"
|
|
@@ -196,18 +134,18 @@ files:
|
|
|
196
134
|
- lib/charming/controller.rb
|
|
197
135
|
- lib/charming/controller/action_hooks.rb
|
|
198
136
|
- lib/charming/controller/class_methods.rb
|
|
199
|
-
- lib/charming/controller/
|
|
200
|
-
- lib/charming/controller/component_dispatching.rb
|
|
137
|
+
- lib/charming/controller/component_dispatch.rb
|
|
201
138
|
- lib/charming/controller/dispatching.rb
|
|
202
139
|
- lib/charming/controller/focus.rb
|
|
203
140
|
- lib/charming/controller/focus_management.rb
|
|
204
141
|
- lib/charming/controller/key_dispatch.rb
|
|
205
142
|
- lib/charming/controller/rendering.rb
|
|
206
143
|
- lib/charming/controller/session_state.rb
|
|
207
|
-
- lib/charming/controller/sidebar_navigation.rb
|
|
208
144
|
- lib/charming/controller/terminal.rb
|
|
209
145
|
- lib/charming/controller/timers.rb
|
|
146
|
+
- lib/charming/cross_thread_access.rb
|
|
210
147
|
- lib/charming/database/commands.rb
|
|
148
|
+
- lib/charming/double_render_error.rb
|
|
211
149
|
- lib/charming/escape.rb
|
|
212
150
|
- lib/charming/events/focus_event.rb
|
|
213
151
|
- lib/charming/events/key_event.rb
|
|
@@ -268,14 +206,20 @@ files:
|
|
|
268
206
|
- lib/charming/image/source.rb
|
|
269
207
|
- lib/charming/image/terminal.rb
|
|
270
208
|
- lib/charming/image/transmit.rb
|
|
209
|
+
- lib/charming/internal/deep_freeze.rb
|
|
210
|
+
- lib/charming/internal/env_inquirer.rb
|
|
271
211
|
- lib/charming/internal/event_loop.rb
|
|
212
|
+
- lib/charming/internal/inflections.rb
|
|
272
213
|
- lib/charming/internal/renderer/differential.rb
|
|
273
214
|
- lib/charming/internal/renderer/full_repaint.rb
|
|
215
|
+
- lib/charming/internal/session_guard.rb
|
|
274
216
|
- lib/charming/internal/terminal/adapter.rb
|
|
217
|
+
- lib/charming/internal/terminal/cursor.rb
|
|
275
218
|
- lib/charming/internal/terminal/key_normalizer.rb
|
|
276
219
|
- lib/charming/internal/terminal/memory_backend.rb
|
|
277
220
|
- lib/charming/internal/terminal/modified_key_parser.rb
|
|
278
221
|
- lib/charming/internal/terminal/mouse_parser.rb
|
|
222
|
+
- lib/charming/internal/terminal/size.rb
|
|
279
223
|
- lib/charming/internal/terminal/tty_backend.rb
|
|
280
224
|
- lib/charming/internal/timer_control.rb
|
|
281
225
|
- lib/charming/presentation/component.rb
|
|
@@ -309,6 +253,7 @@ files:
|
|
|
309
253
|
- lib/charming/presentation/components/multi_select_list.rb
|
|
310
254
|
- lib/charming/presentation/components/paginator.rb
|
|
311
255
|
- lib/charming/presentation/components/progressbar.rb
|
|
256
|
+
- lib/charming/presentation/components/result.rb
|
|
312
257
|
- lib/charming/presentation/components/sparkline.rb
|
|
313
258
|
- lib/charming/presentation/components/spinner.rb
|
|
314
259
|
- lib/charming/presentation/components/status_bar.rb
|
|
@@ -369,17 +314,23 @@ files:
|
|
|
369
314
|
- lib/charming/presentation/ui/width.rb
|
|
370
315
|
- lib/charming/presentation/view.rb
|
|
371
316
|
- lib/charming/projectile.rb
|
|
317
|
+
- lib/charming/render_artifacts.rb
|
|
372
318
|
- lib/charming/response.rb
|
|
373
319
|
- lib/charming/router.rb
|
|
374
320
|
- lib/charming/runtime.rb
|
|
375
321
|
- lib/charming/screen.rb
|
|
322
|
+
- lib/charming/shell/palette.rb
|
|
323
|
+
- lib/charming/shell/sidebar.rb
|
|
376
324
|
- lib/charming/spring.rb
|
|
377
325
|
- lib/charming/tasks/cancelled.rb
|
|
326
|
+
- lib/charming/tasks/context.rb
|
|
378
327
|
- lib/charming/tasks/inline_executor.rb
|
|
379
328
|
- lib/charming/tasks/progress.rb
|
|
380
329
|
- lib/charming/tasks/task.rb
|
|
381
330
|
- lib/charming/tasks/threaded_executor.rb
|
|
382
331
|
- lib/charming/test_helper.rb
|
|
332
|
+
- lib/charming/unhandled_component_event.rb
|
|
333
|
+
- lib/charming/unknown_slot.rb
|
|
383
334
|
- lib/charming/version.rb
|
|
384
335
|
- lib/charming/welcome.rb
|
|
385
336
|
- lib/charming/welcome/controller.rb
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Charming
|
|
4
|
-
class Controller
|
|
5
|
-
# Component-dispatch helpers mixed into Controller. Forwards key events to the currently
|
|
6
|
-
# focused component (the slot returned by `focus.current`) and translates component return
|
|
7
|
-
# values into controller hook calls (e.g., `slot_submitted`, `slot_selected`, `slot_cancelled`).
|
|
8
|
-
module ComponentDispatching
|
|
9
|
-
private
|
|
10
|
-
|
|
11
|
-
# Sends the current key event to the focused component (if it responds to `handle_key`).
|
|
12
|
-
# Returns `:handled` after dispatching, or nil when no component is focused.
|
|
13
|
-
def dispatch_to_focused_component
|
|
14
|
-
slot = focus.current
|
|
15
|
-
return nil unless slot && respond_to?(slot, true)
|
|
16
|
-
|
|
17
|
-
component = send(slot)
|
|
18
|
-
return nil unless component.respond_to?(:handle_key)
|
|
19
|
-
|
|
20
|
-
result = component.handle_key(event)
|
|
21
|
-
return nil if result.nil?
|
|
22
|
-
|
|
23
|
-
dispatch_component_result(slot, result)
|
|
24
|
-
:handled
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
# Translates a component `handle_key` *result* into a controller hook call. `:cancelled`
|
|
28
|
-
# triggers `<slot>_cancelled`, `[:submitted, value]` triggers `<slot>_submitted(value)`,
|
|
29
|
-
# `[:selected, value]` triggers `<slot>_selected(value)`. Falls back to a default render
|
|
30
|
-
# when no matching hook exists.
|
|
31
|
-
def dispatch_component_result(slot, result)
|
|
32
|
-
action, arguments = component_result_action(slot, result)
|
|
33
|
-
action ? send(action, *arguments) : render_default_action
|
|
34
|
-
render_default_action unless response
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# Resolves which controller hook (if any) corresponds to the *result* from a component.
|
|
38
|
-
def component_result_action(slot, result)
|
|
39
|
-
case result
|
|
40
|
-
when :cancelled
|
|
41
|
-
component_action(slot, :cancelled)
|
|
42
|
-
when Array
|
|
43
|
-
component_array_action(slot, result)
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# Handles array-shaped component results, currently `[:submitted, value]` and `[:selected, value]`.
|
|
48
|
-
def component_array_action(slot, result)
|
|
49
|
-
event_name, value = result
|
|
50
|
-
return component_action(slot, :submitted, value) if event_name == :submitted
|
|
51
|
-
return component_action(slot, :selected, value) if event_name == :selected
|
|
52
|
-
|
|
53
|
-
nil
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
# Returns `[action, arguments]` for the `<slot>_<suffix>` controller hook if defined, or nil.
|
|
57
|
-
def component_action(slot, suffix, *arguments)
|
|
58
|
-
action = :"#{slot}_#{suffix}"
|
|
59
|
-
return unless respond_to?(action, true)
|
|
60
|
-
|
|
61
|
-
[action, arguments]
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
# Handles Tab/Shift+Tab by cycling through the focus ring. Returns :handled after rendering.
|
|
65
|
-
def dispatch_tab_traversal
|
|
66
|
-
return nil unless key_name == :tab
|
|
67
|
-
return nil if focus.ring.empty?
|
|
68
|
-
|
|
69
|
-
focus.cycle(event.shift ? -1 : +1)
|
|
70
|
-
render_default_action
|
|
71
|
-
:handled
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
# Hit-tests the current mouse event against named layout panes from the latest render.
|
|
75
|
-
# Clicks move focus to matching slots; components in clicked panes receive local coordinates.
|
|
76
|
-
def dispatch_component_mouse
|
|
77
|
-
target = mouse_target_for_event
|
|
78
|
-
return nil unless target
|
|
79
|
-
|
|
80
|
-
slot = target.fetch(:name)
|
|
81
|
-
previous_focus = focus.current
|
|
82
|
-
focus.focus(slot) if focusable_click?(slot)
|
|
83
|
-
|
|
84
|
-
result = dispatch_mouse_to_target_component(slot, target)
|
|
85
|
-
return response if result.nil? && previous_focus == focus.current
|
|
86
|
-
|
|
87
|
-
result ? dispatch_component_result(slot, result) : render_default_action
|
|
88
|
-
response
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def mouse_target_for_event
|
|
92
|
-
mouse_targets.rfind { |target| target.fetch(:rect).cover?(event.x, event.y) }
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def focusable_click?(slot)
|
|
96
|
-
event.respond_to?(:click?) && event.click? && focus.ring.include?(slot)
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def dispatch_mouse_to_target_component(slot, target)
|
|
100
|
-
return nil unless respond_to?(slot, true)
|
|
101
|
-
|
|
102
|
-
component = send(slot)
|
|
103
|
-
return nil unless component.respond_to?(:handle_mouse)
|
|
104
|
-
|
|
105
|
-
local_event = local_mouse_event(target.fetch(:inner_rect))
|
|
106
|
-
return nil unless local_event
|
|
107
|
-
|
|
108
|
-
component.handle_mouse(local_event)
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def local_mouse_event(rect)
|
|
112
|
-
return nil unless rect.cover?(event.x, event.y)
|
|
113
|
-
|
|
114
|
-
Events::MouseEvent.new(
|
|
115
|
-
button: event.button,
|
|
116
|
-
x: event.x - rect.x,
|
|
117
|
-
y: event.y - rect.y,
|
|
118
|
-
ctrl: event.ctrl,
|
|
119
|
-
alt: event.alt,
|
|
120
|
-
shift: event.shift
|
|
121
|
-
)
|
|
122
|
-
end
|
|
123
|
-
end
|
|
124
|
-
end
|
|
125
|
-
end
|