charming 0.2.2 → 0.2.3
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 +15 -2
- data/lib/charming/cli.rb +16 -4
- data/lib/charming/controller.rb +1 -1
- data/lib/charming/generators/app_file_generator.rb +29 -0
- data/lib/charming/generators/app_generator.rb +12 -25
- data/lib/charming/generators/base.rb +5 -0
- data/lib/charming/generators/layout_generator.rb +155 -0
- data/lib/charming/generators/screen_generator.rb +0 -29
- data/lib/charming/generators/templates/app/README.md.template +15 -1
- data/lib/charming/generators/templates/app/application.template +0 -6
- data/lib/charming/generators/templates/app/application_controller.template +0 -10
- data/lib/charming/generators/templates/app/layout.template +4 -93
- data/lib/charming/generators/templates/app/routes.template +3 -1
- data/lib/charming/generators/templates/layout/sidebar/application_layout.rb.template +110 -0
- data/lib/charming/image/protocol/kitty.rb +7 -0
- data/lib/charming/image/source.rb +10 -0
- data/lib/charming/presentation/components/autocomplete.rb +7 -0
- data/lib/charming/presentation/components/form/field.rb +5 -0
- data/lib/charming/presentation/components/form/input.rb +14 -4
- data/lib/charming/presentation/components/form/textarea.rb +18 -8
- data/lib/charming/presentation/components/form.rb +6 -0
- data/lib/charming/presentation/components/modal.rb +4 -4
- data/lib/charming/presentation/components/stopwatch.rb +1 -1
- data/lib/charming/presentation/components/text_area.rb +2 -2
- data/lib/charming/presentation/components/text_input.rb +2 -2
- data/lib/charming/presentation/components/timer.rb +1 -1
- data/lib/charming/presentation/components/toast.rb +4 -3
- data/lib/charming/presentation/components/viewport.rb +11 -1
- data/lib/charming/runtime.rb +13 -2
- data/lib/charming/version.rb +1 -1
- data/lib/charming/welcome/controller.rb +23 -0
- data/lib/charming/welcome/show_view.rb +113 -0
- data/lib/charming/welcome.rb +13 -0
- metadata +6 -7
- data/lib/charming/generators/templates/app/home_controller.template +0 -6
- data/lib/charming/generators/templates/app/home_state.template +0 -7
- data/lib/charming/generators/templates/app/spec_controller.template +0 -17
- data/lib/charming/generators/templates/app/spec_state.template +0 -17
- data/lib/charming/generators/templates/app/spec_view.template +0 -16
- data/lib/charming/generators/templates/app/view.template +0 -21
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module __APP_CLASS__
|
|
4
|
+
module Layouts
|
|
5
|
+
class ApplicationLayout < Charming::View
|
|
6
|
+
def render
|
|
7
|
+
screen_layout(background: theme.background) do
|
|
8
|
+
split(narrow? ? :vertical : :horizontal, gap: 1) do
|
|
9
|
+
pane(:sidebar, **sidebar_options, border: :rounded, padding: [1, 2], style: sidebar_style) do
|
|
10
|
+
column(app_title, navigation, shortcuts, gap: 1)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
pane(:content, grow: 1, border: :rounded, padding: [1, 2], style: content_style) do
|
|
14
|
+
yield_content
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
overlay command_palette_modal if command_palette_modal
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def palette_component
|
|
25
|
+
assigns.fetch(:palette, nil)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def narrow?
|
|
29
|
+
screen.narrow?(below: 72, min_height: 20)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def sidebar_options
|
|
33
|
+
narrow? ? {height: [screen.height / 3, 5].max} : {width: 22}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def sidebar_inner_width
|
|
37
|
+
narrow? ? [screen.width - 6, 20].max : 16
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def app_title
|
|
41
|
+
text "__APP_NAME__", style: theme.header_accent.align(:center).width(sidebar_inner_width)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def navigation
|
|
45
|
+
column(*nav_items)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def nav_items
|
|
49
|
+
controller.sidebar_routes.each_with_index.map do |route, index|
|
|
50
|
+
text nav_item_label(route, index), style: nav_item_style(route, index)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def nav_item_label(route, index)
|
|
55
|
+
cursor = (sidebar_focused? && index == sidebar_index) ? ">" : " "
|
|
56
|
+
active = current_route?(route) ? "\u{25cf}" : " "
|
|
57
|
+
"#{cursor} #{active} #{route.title}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def nav_item_style(route, index)
|
|
61
|
+
if sidebar_focused? && index == sidebar_index
|
|
62
|
+
theme.selected
|
|
63
|
+
elsif current_route?(route)
|
|
64
|
+
theme.title
|
|
65
|
+
else
|
|
66
|
+
theme.muted
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def shortcuts
|
|
71
|
+
text "tab focus\nctrl+p commands\nq quit", style: theme.muted
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def sidebar_style
|
|
75
|
+
focused_style = sidebar_focused? ? theme.title : theme.border
|
|
76
|
+
palette_component ? focused_style.faint : focused_style
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def content_style
|
|
80
|
+
focused_style = content_focused? ? theme.title : theme.border
|
|
81
|
+
palette_component ? focused_style.faint : focused_style
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def command_palette_modal
|
|
85
|
+
return unless palette_component
|
|
86
|
+
|
|
87
|
+
render_component Charming::Components::CommandPaletteModal.new(
|
|
88
|
+
content: palette_component,
|
|
89
|
+
theme: theme
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def sidebar_focused?
|
|
94
|
+
controller.sidebar_focused?
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def content_focused?
|
|
98
|
+
controller.content_focused?
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def sidebar_index
|
|
102
|
+
controller.sidebar_index
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def current_route?(route)
|
|
106
|
+
controller.current_route?(route)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -82,6 +82,13 @@ module Charming
|
|
|
82
82
|
end.join("\n")
|
|
83
83
|
end
|
|
84
84
|
|
|
85
|
+
# Builds the `a=d,d=I` escape that deletes *image_id*'s placements AND frees its data from
|
|
86
|
+
# terminal memory (lowercase `d=i` would keep the data resident). Long-lived apps that cycle
|
|
87
|
+
# many images must emit this when evicting, or the terminal's image storage grows unbounded.
|
|
88
|
+
def delete(image_id:)
|
|
89
|
+
"\e_Ga=d,d=I,i=#{image_id},q=2\e\\"
|
|
90
|
+
end
|
|
91
|
+
|
|
85
92
|
# The chunked `a=t` (transmit) escapes carrying the base64 PNG data directly (`f=100,t=d`).
|
|
86
93
|
# Control keys ride only the first chunk; `m=1` flags more-to-come, `m=0` the last.
|
|
87
94
|
def transmit_image(image_id, png_bytes)
|
|
@@ -53,6 +53,16 @@ module Charming
|
|
|
53
53
|
protocol.placeholder_block(image_id: image_id, rows: rows, cols: cols)
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
+
# Builds the out-of-band {Transmit} that frees the image from terminal memory, or nil when
|
|
57
|
+
# graphics are unsupported. Re-arms {transmitted?} so a later render retransmits. Callers
|
|
58
|
+
# evicting an image register this the same way as {transmit}.
|
|
59
|
+
def release
|
|
60
|
+
return unless protocol
|
|
61
|
+
|
|
62
|
+
@transmitted = false
|
|
63
|
+
Transmit.new(image_id: image_id, payload: protocol.delete(image_id: image_id))
|
|
64
|
+
end
|
|
65
|
+
|
|
56
66
|
# True once {mark_transmitted} has recorded that the image was sent to the terminal.
|
|
57
67
|
def transmitted?
|
|
58
68
|
@transmitted
|
|
@@ -67,6 +67,13 @@ module Charming
|
|
|
67
67
|
end
|
|
68
68
|
end
|
|
69
69
|
|
|
70
|
+
# Forwards pasted text to the inner input and re-filters the suggestions.
|
|
71
|
+
def handle_paste(event)
|
|
72
|
+
result = @input.handle_paste(event)
|
|
73
|
+
clamp_selection if result
|
|
74
|
+
result
|
|
75
|
+
end
|
|
76
|
+
|
|
70
77
|
# Renders the input row followed by the suggestion dropdown.
|
|
71
78
|
def render
|
|
72
79
|
[input_line, *suggestion_lines].join("\n")
|
|
@@ -41,6 +41,11 @@ module Charming
|
|
|
41
41
|
nil
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
+
# Default paste handler returns nil (paste ignored). Text fields override.
|
|
45
|
+
def handle_paste(_event)
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
|
|
44
49
|
# Renders the field as a control line prefixed with ">" (active) or " " (inactive),
|
|
45
50
|
# optionally followed by the help line and any error lines.
|
|
46
51
|
def render(active: false)
|
|
@@ -27,17 +27,27 @@ module Charming
|
|
|
27
27
|
# Forwards key events to the underlying TextInput, syncing the value and cursor
|
|
28
28
|
# back into the form state. Returns :handled when the event was consumed.
|
|
29
29
|
def handle_key(event)
|
|
30
|
+
forward_to_input(:handle_key, event)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Forwards pasted text to the underlying TextInput the same way.
|
|
34
|
+
def handle_paste(event)
|
|
35
|
+
forward_to_input(:handle_paste, event)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
# Sends *message* to a freshly-built TextInput and, when the widget consumed
|
|
41
|
+
# the event, persists the resulting value and cursor into the form state.
|
|
42
|
+
def forward_to_input(message, event)
|
|
30
43
|
text_input = input
|
|
31
|
-
|
|
32
|
-
return nil unless result == :handled
|
|
44
|
+
return nil unless text_input.public_send(message, event) == :handled
|
|
33
45
|
|
|
34
46
|
state[:values][name] = text_input.value
|
|
35
47
|
field_state[:cursor] = text_input.cursor
|
|
36
48
|
:handled
|
|
37
49
|
end
|
|
38
50
|
|
|
39
|
-
private
|
|
40
|
-
|
|
41
51
|
# The default value for a freshly-bound field is the *value* passed at construction.
|
|
42
52
|
def default_value
|
|
43
53
|
@initial_value
|
|
@@ -29,15 +29,12 @@ module Charming
|
|
|
29
29
|
# Forwards key events to the underlying TextArea, syncing the value, cursor, offset,
|
|
30
30
|
# and preferred column back into the form state. Returns :handled when consumed.
|
|
31
31
|
def handle_key(event)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return nil unless result == :handled
|
|
32
|
+
forward_to_text_area(:handle_key, event)
|
|
33
|
+
end
|
|
35
34
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
field_state[:preferred_column] = area.preferred_column
|
|
40
|
-
:handled
|
|
35
|
+
# Forwards pasted text (newlines preserved) to the underlying TextArea the same way.
|
|
36
|
+
def handle_paste(event)
|
|
37
|
+
forward_to_text_area(:handle_paste, event)
|
|
41
38
|
end
|
|
42
39
|
|
|
43
40
|
# Renders the field with its label on the first line, body lines indented, and
|
|
@@ -50,6 +47,19 @@ module Charming
|
|
|
50
47
|
|
|
51
48
|
private
|
|
52
49
|
|
|
50
|
+
# Sends *message* to a freshly-built TextArea and, when the widget consumed the
|
|
51
|
+
# event, persists the value, cursor, offset, and preferred column into form state.
|
|
52
|
+
def forward_to_text_area(message, event)
|
|
53
|
+
area = text_area
|
|
54
|
+
return nil unless area.public_send(message, event) == :handled
|
|
55
|
+
|
|
56
|
+
state[:values][name] = area.value
|
|
57
|
+
field_state[:cursor] = area.cursor
|
|
58
|
+
field_state[:offset] = area.offset
|
|
59
|
+
field_state[:preferred_column] = area.preferred_column
|
|
60
|
+
:handled
|
|
61
|
+
end
|
|
62
|
+
|
|
53
63
|
# The default value for a freshly-bound field is the *value* passed at construction.
|
|
54
64
|
def default_value
|
|
55
65
|
@initial_value
|
|
@@ -38,6 +38,12 @@ module Charming
|
|
|
38
38
|
advance_or_submit if key == :enter
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
+
# Forwards pasted text to the currently focused field. Text fields insert it
|
|
42
|
+
# at their cursor; other fields ignore it.
|
|
43
|
+
def handle_paste(event)
|
|
44
|
+
current_field&.handle_paste(event)
|
|
45
|
+
end
|
|
46
|
+
|
|
41
47
|
# Forms accept free-typed text (their input/textarea fields do), so printable
|
|
42
48
|
# characters route here before global/content key bindings.
|
|
43
49
|
def captures_text?
|
|
@@ -39,8 +39,8 @@ module Charming
|
|
|
39
39
|
result
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
-
# Renders the modal as a bordered, padded string with the title
|
|
43
|
-
#
|
|
42
|
+
# Renders the modal as a bordered, padded string with the title above the content
|
|
43
|
+
# and the help footer below it.
|
|
44
44
|
def render
|
|
45
45
|
box(column(*lines, gap: 1), style: modal_style)
|
|
46
46
|
end
|
|
@@ -49,9 +49,9 @@ module Charming
|
|
|
49
49
|
|
|
50
50
|
attr_reader :content, :title, :help, :width
|
|
51
51
|
|
|
52
|
-
# Returns the array of non-nil lines: title,
|
|
52
|
+
# Returns the array of non-nil lines: title, content, help footer.
|
|
53
53
|
def lines
|
|
54
|
-
[title_line,
|
|
54
|
+
[title_line, body_content, help_line].compact
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
# The body: windowed through a Viewport when scrollable, otherwise rendered directly.
|
|
@@ -8,7 +8,7 @@ module Charming
|
|
|
8
8
|
# for long buffers. Vertical movement preserves a "preferred column" so left/right
|
|
9
9
|
# navigation feels stable.
|
|
10
10
|
class TextArea < Component
|
|
11
|
-
# The current text value, cursor
|
|
11
|
+
# The current text value, cursor character offset, top-visible row offset, and remembered
|
|
12
12
|
# column for vertical navigation, respectively.
|
|
13
13
|
attr_reader :value, :cursor, :offset, :preferred_column
|
|
14
14
|
|
|
@@ -212,7 +212,7 @@ module Charming
|
|
|
212
212
|
[row, column]
|
|
213
213
|
end
|
|
214
214
|
|
|
215
|
-
# Returns the
|
|
215
|
+
# Returns the character offset where line *row* begins in the value.
|
|
216
216
|
def line_start(row)
|
|
217
217
|
lines.first(row).sum(&:length) + row
|
|
218
218
|
end
|
|
@@ -25,7 +25,7 @@ module Charming
|
|
|
25
25
|
delete: :delete_at_cursor
|
|
26
26
|
}.freeze
|
|
27
27
|
|
|
28
|
-
# The current input string and the
|
|
28
|
+
# The current input string and the character offset of the cursor within it.
|
|
29
29
|
attr_reader :value, :cursor
|
|
30
30
|
|
|
31
31
|
# *value* is the initial text. *placeholder* is shown when the value is empty.
|
|
@@ -94,7 +94,7 @@ module Charming
|
|
|
94
94
|
!char.match?(/[[:cntrl:]]/)
|
|
95
95
|
end
|
|
96
96
|
|
|
97
|
-
# Inserts *char* at the cursor and advances the cursor by its
|
|
97
|
+
# Inserts *char* at the cursor and advances the cursor by its character length.
|
|
98
98
|
def insert(char)
|
|
99
99
|
@value = value[0...cursor] + char + value[cursor..]
|
|
100
100
|
@cursor += char.length
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
module Charming
|
|
4
4
|
module Components
|
|
5
5
|
# Toast is a small auto-dismissing notification panel, usually composited as an
|
|
6
|
-
# overlay anchored to a screen corner.
|
|
7
|
-
#
|
|
8
|
-
#
|
|
6
|
+
# overlay anchored to a screen corner. The component just renders the styled box;
|
|
7
|
+
# apps manage its lifetime themselves — typically a session-backed hash with an
|
|
8
|
+
# expiry deadline plus a controller timer that clears it (see the journal example's
|
|
9
|
+
# ApplicationController#show_toast / #expire_toast).
|
|
9
10
|
#
|
|
10
11
|
# Toast.new(message: "Saved!", kind: :success)
|
|
11
12
|
#
|
|
@@ -25,7 +25,10 @@ module Charming
|
|
|
25
25
|
# *content* may be a string, an array of lines, or any object responding to `render`.
|
|
26
26
|
# *width* and *height* constrain the visible window; *offset* is the top-visible row
|
|
27
27
|
# and *column* is the left-visible column. *wrap* enables soft-wrapping of long lines.
|
|
28
|
-
|
|
28
|
+
# *follow* pins the viewport to the bottom of the content regardless of *offset*, so
|
|
29
|
+
# callers that rebuild each event stick to new content; pair with `at_bottom?` to
|
|
30
|
+
# decide whether to keep following after the user scrolls.
|
|
31
|
+
def initialize(content:, width: nil, height: nil, offset: 0, column: 0, wrap: false, keymap: :vim, follow: false)
|
|
29
32
|
super()
|
|
30
33
|
@content = content
|
|
31
34
|
@width = width
|
|
@@ -34,6 +37,7 @@ module Charming
|
|
|
34
37
|
@wrap = wrap
|
|
35
38
|
@keymap = keymap
|
|
36
39
|
position.clamp(bounds)
|
|
40
|
+
position.move_to(max_offset, bounds) if follow
|
|
37
41
|
end
|
|
38
42
|
|
|
39
43
|
# Renders the visible window of content as a multi-line string.
|
|
@@ -51,6 +55,12 @@ module Charming
|
|
|
51
55
|
@position.column
|
|
52
56
|
end
|
|
53
57
|
|
|
58
|
+
# True when the last content row is visible. Callers persisting follow mode
|
|
59
|
+
# store this after key dispatch to decide whether the next build follows.
|
|
60
|
+
def at_bottom?
|
|
61
|
+
offset >= max_offset
|
|
62
|
+
end
|
|
63
|
+
|
|
54
64
|
# Handles mouse events: scroll wheel adjusts the row offset, click moves the top
|
|
55
65
|
# visible row to the clicked position. Returns :handled on success.
|
|
56
66
|
def handle_mouse(event)
|
data/lib/charming/runtime.rb
CHANGED
|
@@ -16,7 +16,7 @@ module Charming
|
|
|
16
16
|
@task_queue = Thread::Queue.new
|
|
17
17
|
@task_executor = build_task_executor(task_executor)
|
|
18
18
|
@application.task_executor = @task_executor
|
|
19
|
-
@route =
|
|
19
|
+
@route = resolve_route("/")
|
|
20
20
|
@screen = backend_screen
|
|
21
21
|
@coalesce_input = @application.respond_to?(:coalesce_input?) && @application.coalesce_input?
|
|
22
22
|
@event_loop = build_event_loop
|
|
@@ -281,11 +281,22 @@ module Charming
|
|
|
281
281
|
def resolve_response(response)
|
|
282
282
|
return response unless response.navigate?
|
|
283
283
|
|
|
284
|
-
@route =
|
|
284
|
+
@route = resolve_route(response.path)
|
|
285
285
|
@event_loop.reset_timers(@route.controller_class.timer_bindings.values)
|
|
286
286
|
dispatch(@route.action)
|
|
287
287
|
end
|
|
288
288
|
|
|
289
|
+
# Resolves *path* from the app's router. An unrouted "/" falls back to the app's
|
|
290
|
+
# first route, or to the built-in welcome screen when no routes are defined yet
|
|
291
|
+
# (like Rails' welcome page); other unrouted paths still raise.
|
|
292
|
+
def resolve_route(path)
|
|
293
|
+
@application.routes.resolve(path)
|
|
294
|
+
rescue KeyError
|
|
295
|
+
raise unless path == "/"
|
|
296
|
+
|
|
297
|
+
@application.routes.all.first || Welcome.route
|
|
298
|
+
end
|
|
299
|
+
|
|
289
300
|
# Derives Screen dimensions (width, height) from the terminal backend.
|
|
290
301
|
def backend_screen
|
|
291
302
|
width, height = @backend.size
|
data/lib/charming/version.rb
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Charming
|
|
4
|
+
module Welcome
|
|
5
|
+
# Controller for the built-in welcome screen. Renders without an app layout and
|
|
6
|
+
# binds only `q` to quit.
|
|
7
|
+
class Controller < Charming::Controller
|
|
8
|
+
key "q", :quit, scope: :global
|
|
9
|
+
|
|
10
|
+
def show
|
|
11
|
+
render_view ShowView, app_name: app_display_name
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
# The app's namespace for the welcome heading, or "Charming" for anonymous apps.
|
|
17
|
+
def app_display_name
|
|
18
|
+
name = application.class.namespace
|
|
19
|
+
name.to_s.empty? ? "Charming" : name
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Charming
|
|
4
|
+
module Welcome
|
|
5
|
+
# The welcome screen body: a pastel Miami-deco skyline over the app name, tagline,
|
|
6
|
+
# Charming version, and project link, centered on the terminal.
|
|
7
|
+
class ShowView < Charming::View
|
|
8
|
+
SKYLINE_COLORS = {
|
|
9
|
+
"F" => "#f0437c", # flamingo
|
|
10
|
+
"B" => "#f9cfd8", # blush
|
|
11
|
+
"M" => "#bfe8dc", # mint
|
|
12
|
+
"C" => "#f3e6c9", # cream
|
|
13
|
+
"P" => "#fdfaf3", # plaster
|
|
14
|
+
"G" => "#1d4b44" # palm green
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
# Rows of [art, color mask] pairs; each mask letter colors the glyph above it.
|
|
18
|
+
SKYLINE = [
|
|
19
|
+
[" ▲ ",
|
|
20
|
+
" F "],
|
|
21
|
+
[" █ ",
|
|
22
|
+
" F "],
|
|
23
|
+
[" ▄▄▄ ▄█▄ ▄▄▄ ",
|
|
24
|
+
" BBB PFP MMM "],
|
|
25
|
+
[" ███ ▐███▌ ███ ",
|
|
26
|
+
" BBB PPPPP MMM "],
|
|
27
|
+
[" █▀▀▀█ ▐███▌ █▀▀▀█ ",
|
|
28
|
+
" BBBBB PPPPP MMMMM "],
|
|
29
|
+
[" ▄█▄ █████ ▐█████▌ █████ ▄█▄ ",
|
|
30
|
+
" GGG BBBBB PPPPPPP MMMMM GGG "],
|
|
31
|
+
[" █ ▄▄▄ ██▀▀▀██ ▐█████▌ ██▀▀▀██ ▄▄▄ █ ",
|
|
32
|
+
" G CCC BBBBBBB PPPPPPP MMMMMMM CCC G "],
|
|
33
|
+
[" █ ███ ███████ ▐███████▌ ███████ ███ █ ",
|
|
34
|
+
" G CCC BBBBBBB PPPPPPPPP MMMMMMM CCC G "],
|
|
35
|
+
[" █ ███ ███████ ▐███████▌ ███████ ███ █ ",
|
|
36
|
+
" G CCC BBBBBBB PPPPPPPPP MMMMMMM CCC G "],
|
|
37
|
+
["▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
|
|
38
|
+
"CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"]
|
|
39
|
+
].freeze
|
|
40
|
+
SKYLINE_WIDTH = SKYLINE.map { |art, _mask| art.length }.max
|
|
41
|
+
SKYLINE_HEIGHT = SKYLINE.length
|
|
42
|
+
|
|
43
|
+
def render
|
|
44
|
+
screen_layout(background: theme.background) do
|
|
45
|
+
pane(style: centered) do
|
|
46
|
+
welcome_content
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def centered
|
|
54
|
+
style.align(:center).align_vertical(:middle)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def welcome_content
|
|
58
|
+
column(*blocks, gap: 1, align: :center)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def blocks
|
|
62
|
+
[skyline, heading, tagline, footer, hint].compact
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def skyline
|
|
66
|
+
return unless skyline_fits?
|
|
67
|
+
|
|
68
|
+
column(*SKYLINE.map { |art, mask| paint_row(art, mask) })
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Leave room below the art for the heading, tagline, footer, and hint lines.
|
|
72
|
+
def skyline_fits?
|
|
73
|
+
layout_screen.width >= SKYLINE_WIDTH && layout_screen.height >= SKYLINE_HEIGHT + 8
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def paint_row(art, mask)
|
|
77
|
+
color_runs(art, mask).map { |run, key| paint(run, key) }.join
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Groups adjacent glyphs sharing a mask letter into [run, letter] pairs.
|
|
81
|
+
def color_runs(art, mask)
|
|
82
|
+
art.chars.zip(mask.chars)
|
|
83
|
+
.chunk { |_glyph, key| key || " " }
|
|
84
|
+
.map { |key, pairs| [pairs.map(&:first).join, key] }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def paint(run, key)
|
|
88
|
+
color = SKYLINE_COLORS[key]
|
|
89
|
+
color ? style.foreground(color).render(run) : run
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def heading
|
|
93
|
+
text app_name, style: theme.title
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def tagline
|
|
97
|
+
text "A Rails-inspired Ruby TUI framework", style: theme.text
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def footer
|
|
101
|
+
column(
|
|
102
|
+
text("Charming v#{Charming::VERSION}", style: theme.muted),
|
|
103
|
+
text("https://github.com/pandorocks/charming", style: theme.muted),
|
|
104
|
+
align: :center
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def hint
|
|
109
|
+
text "Define a root route in config/routes.rb to replace this screen.", style: theme.muted
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Charming
|
|
4
|
+
# Welcome is the built-in placeholder screen shown when an application defines no
|
|
5
|
+
# routes yet — the TUI equivalent of Rails' welcome page. It lives in the gem and is
|
|
6
|
+
# never copied into apps: defining any route in config/routes.rb replaces it.
|
|
7
|
+
module Welcome
|
|
8
|
+
# The fallback route the Runtime uses when the application has no routes.
|
|
9
|
+
def self.route
|
|
10
|
+
Router::Route.new(path: "/", controller_class: Controller, action: :show, title: "Welcome", params: {})
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
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.2.
|
|
4
|
+
version: 0.2.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- pando
|
|
@@ -223,6 +223,7 @@ files:
|
|
|
223
223
|
- lib/charming/generators/controller_generator.rb
|
|
224
224
|
- lib/charming/generators/database_installer.rb
|
|
225
225
|
- lib/charming/generators/error.rb
|
|
226
|
+
- lib/charming/generators/layout_generator.rb
|
|
226
227
|
- lib/charming/generators/migration_generator.rb
|
|
227
228
|
- lib/charming/generators/migration_timestamp.rb
|
|
228
229
|
- lib/charming/generators/model_generator.rb
|
|
@@ -239,21 +240,16 @@ files:
|
|
|
239
240
|
- lib/charming/generators/templates/app/dot_rspec.template
|
|
240
241
|
- lib/charming/generators/templates/app/executable.template
|
|
241
242
|
- lib/charming/generators/templates/app/gemspec.template
|
|
242
|
-
- lib/charming/generators/templates/app/home_controller.template
|
|
243
|
-
- lib/charming/generators/templates/app/home_state.template
|
|
244
243
|
- lib/charming/generators/templates/app/keep.template
|
|
245
244
|
- lib/charming/generators/templates/app/layout.template
|
|
246
245
|
- lib/charming/generators/templates/app/root_file.template
|
|
247
246
|
- lib/charming/generators/templates/app/routes.template
|
|
248
247
|
- lib/charming/generators/templates/app/seeds.template
|
|
249
|
-
- lib/charming/generators/templates/app/spec_controller.template
|
|
250
248
|
- lib/charming/generators/templates/app/spec_helper.template
|
|
251
|
-
- lib/charming/generators/templates/app/spec_state.template
|
|
252
|
-
- lib/charming/generators/templates/app/spec_view.template
|
|
253
249
|
- lib/charming/generators/templates/app/version.template
|
|
254
|
-
- lib/charming/generators/templates/app/view.template
|
|
255
250
|
- lib/charming/generators/templates/component/component.rb.template
|
|
256
251
|
- lib/charming/generators/templates/controller/controller.rb.template
|
|
252
|
+
- lib/charming/generators/templates/layout/sidebar/application_layout.rb.template
|
|
257
253
|
- lib/charming/generators/templates/model/migration.rb.template
|
|
258
254
|
- lib/charming/generators/templates/model/model.rb.template
|
|
259
255
|
- lib/charming/generators/templates/model/spec.rb.template
|
|
@@ -381,6 +377,9 @@ files:
|
|
|
381
377
|
- lib/charming/tasks/threaded_executor.rb
|
|
382
378
|
- lib/charming/test_helper.rb
|
|
383
379
|
- lib/charming/version.rb
|
|
380
|
+
- lib/charming/welcome.rb
|
|
381
|
+
- lib/charming/welcome/controller.rb
|
|
382
|
+
- lib/charming/welcome/show_view.rb
|
|
384
383
|
- sig/charming.rbs
|
|
385
384
|
homepage: https://github.com/pandorocks/charming
|
|
386
385
|
licenses:
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "__APP_SNAKE__"
|
|
4
|
-
|
|
5
|
-
RSpec.describe __APP_CLASS__::HomeController do
|
|
6
|
-
let(:application) { __APP_CLASS__::Application.new }
|
|
7
|
-
|
|
8
|
-
subject(:controller) { described_class.new(application: application) }
|
|
9
|
-
|
|
10
|
-
describe "#show" do
|
|
11
|
-
it "renders the view with the state" do
|
|
12
|
-
response = controller.dispatch(:show)
|
|
13
|
-
|
|
14
|
-
expect(response).to respond_to(:body)
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
end
|