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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +15 -2
  3. data/lib/charming/cli.rb +16 -4
  4. data/lib/charming/controller.rb +1 -1
  5. data/lib/charming/generators/app_file_generator.rb +29 -0
  6. data/lib/charming/generators/app_generator.rb +12 -25
  7. data/lib/charming/generators/base.rb +5 -0
  8. data/lib/charming/generators/layout_generator.rb +155 -0
  9. data/lib/charming/generators/screen_generator.rb +0 -29
  10. data/lib/charming/generators/templates/app/README.md.template +15 -1
  11. data/lib/charming/generators/templates/app/application.template +0 -6
  12. data/lib/charming/generators/templates/app/application_controller.template +0 -10
  13. data/lib/charming/generators/templates/app/layout.template +4 -93
  14. data/lib/charming/generators/templates/app/routes.template +3 -1
  15. data/lib/charming/generators/templates/layout/sidebar/application_layout.rb.template +110 -0
  16. data/lib/charming/image/protocol/kitty.rb +7 -0
  17. data/lib/charming/image/source.rb +10 -0
  18. data/lib/charming/presentation/components/autocomplete.rb +7 -0
  19. data/lib/charming/presentation/components/form/field.rb +5 -0
  20. data/lib/charming/presentation/components/form/input.rb +14 -4
  21. data/lib/charming/presentation/components/form/textarea.rb +18 -8
  22. data/lib/charming/presentation/components/form.rb +6 -0
  23. data/lib/charming/presentation/components/modal.rb +4 -4
  24. data/lib/charming/presentation/components/stopwatch.rb +1 -1
  25. data/lib/charming/presentation/components/text_area.rb +2 -2
  26. data/lib/charming/presentation/components/text_input.rb +2 -2
  27. data/lib/charming/presentation/components/timer.rb +1 -1
  28. data/lib/charming/presentation/components/toast.rb +4 -3
  29. data/lib/charming/presentation/components/viewport.rb +11 -1
  30. data/lib/charming/runtime.rb +13 -2
  31. data/lib/charming/version.rb +1 -1
  32. data/lib/charming/welcome/controller.rb +23 -0
  33. data/lib/charming/welcome/show_view.rb +113 -0
  34. data/lib/charming/welcome.rb +13 -0
  35. metadata +6 -7
  36. data/lib/charming/generators/templates/app/home_controller.template +0 -6
  37. data/lib/charming/generators/templates/app/home_state.template +0 -7
  38. data/lib/charming/generators/templates/app/spec_controller.template +0 -17
  39. data/lib/charming/generators/templates/app/spec_state.template +0 -17
  40. data/lib/charming/generators/templates/app/spec_view.template +0 -16
  41. 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
- result = text_input.handle_key(event)
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
- area = text_area
33
- result = area.handle_key(event)
34
- return nil unless result == :handled
32
+ forward_to_text_area(:handle_key, event)
33
+ end
35
34
 
36
- state[:values][name] = area.value
37
- field_state[:cursor] = area.cursor
38
- field_state[:offset] = area.offset
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 and help lines stacked
43
- # above the content.
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, help, content.
52
+ # Returns the array of non-nil lines: title, content, help footer.
53
53
  def lines
54
- [title_line, help_line, body_content].compact
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.
@@ -34,7 +34,7 @@ module Charming
34
34
 
35
35
  # Adds *seconds* (default 1) when running; a no-op otherwise. Returns self.
36
36
  def tick(seconds = 1)
37
- @elapsed += seconds.to_i if running?
37
+ @elapsed += seconds if running?
38
38
  self
39
39
  end
40
40
 
@@ -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 byte offset, top-visible row offset, and remembered
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 byte offset where line *row* begins in the value.
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 byte offset of the cursor within it.
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 byte length.
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
@@ -18,7 +18,7 @@ module Charming
18
18
 
19
19
  # Counts down by *seconds* (default 1), clamping at zero. Returns self.
20
20
  def tick(seconds = 1)
21
- @remaining = [@remaining - seconds.to_i, 0].max
21
+ @remaining = [@remaining - seconds, 0].max
22
22
  self
23
23
  end
24
24
 
@@ -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. Controllers manage its lifetime with the
7
- # `show_toast` / `dismiss_toast` helpers (which pair it with a timer); the component
8
- # itself just renders the styled box.
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
- def initialize(content:, width: nil, height: nil, offset: 0, column: 0, wrap: false, keymap: :vim)
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)
@@ -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 = @application.routes.resolve("/")
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 = @application.routes.resolve(response.path)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Charming
4
- VERSION = "0.2.2"
4
+ VERSION = "0.2.3"
5
5
  end
@@ -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.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,6 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module __APP_CLASS__
4
- class HomeController < ApplicationController
5
- __CONTROLLER_ACTIONS____CONTROLLER_HELPERS__ end
6
- end
@@ -1,7 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module __APP_CLASS__
4
- class HomeState < ApplicationState
5
- attribute :title, :string, default: "__APP_NAME__"
6
- end
7
- end
@@ -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