charming 0.1.1 → 0.1.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 (132) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/lib/charming/application.rb +14 -3
  4. data/lib/charming/cli.rb +23 -0
  5. data/lib/charming/controller/class_methods.rb +115 -0
  6. data/lib/charming/controller/command_palette.rb +135 -0
  7. data/lib/charming/controller/component_dispatching.rb +81 -0
  8. data/lib/charming/controller/dispatching.rb +60 -0
  9. data/lib/charming/controller/focus_management.rb +30 -0
  10. data/lib/charming/controller/rendering.rb +127 -0
  11. data/lib/charming/controller/session_state.rb +41 -0
  12. data/lib/charming/controller/sidebar_navigation.rb +111 -0
  13. data/lib/charming/controller.rb +35 -559
  14. data/lib/charming/database_commands.rb +16 -0
  15. data/lib/charming/database_installer.rb +27 -0
  16. data/lib/charming/focus.rb +58 -2
  17. data/lib/charming/generators/app_file_generator.rb +13 -0
  18. data/lib/charming/generators/app_generator.rb +123 -47
  19. data/lib/charming/generators/base.rb +26 -0
  20. data/lib/charming/generators/component_generator.rb +10 -10
  21. data/lib/charming/generators/controller_generator.rb +22 -11
  22. data/lib/charming/generators/model_generator.rb +38 -29
  23. data/lib/charming/generators/name.rb +10 -0
  24. data/lib/charming/generators/screen_generator.rb +78 -32
  25. data/lib/charming/generators/templates/app/Gemfile.template +5 -0
  26. data/lib/charming/generators/templates/app/README.md.template +9 -0
  27. data/lib/charming/generators/templates/app/Rakefile.template +3 -0
  28. data/lib/charming/generators/templates/app/application.template +13 -0
  29. data/lib/charming/generators/templates/app/application_controller.template +19 -0
  30. data/lib/charming/generators/templates/app/application_record.template +7 -0
  31. data/lib/charming/generators/templates/app/application_state.template +6 -0
  32. data/lib/charming/generators/templates/app/database_config.template +12 -0
  33. data/lib/charming/generators/templates/app/executable.template +7 -0
  34. data/lib/charming/generators/templates/app/gemspec.template +6 -0
  35. data/lib/charming/generators/templates/app/home_controller.template +6 -0
  36. data/lib/charming/generators/templates/app/home_state.template +7 -0
  37. data/lib/charming/generators/templates/app/keep.template +0 -0
  38. data/lib/charming/generators/templates/app/layout.template +110 -0
  39. data/lib/charming/generators/templates/app/root_file.template +20 -0
  40. data/lib/charming/generators/templates/app/routes.template +5 -0
  41. data/lib/charming/generators/templates/app/seeds.template +1 -0
  42. data/lib/charming/generators/templates/app/spec_controller.template +17 -0
  43. data/lib/charming/generators/templates/app/spec_helper.template +3 -0
  44. data/lib/charming/generators/templates/app/spec_state.template +17 -0
  45. data/lib/charming/generators/templates/app/spec_view.template +16 -0
  46. data/lib/charming/generators/templates/app/version.template +5 -0
  47. data/lib/charming/generators/templates/app/view.template +21 -0
  48. data/lib/charming/generators/templates/component/component.rb.template +9 -0
  49. data/lib/charming/generators/templates/controller/controller.rb.template +6 -0
  50. data/lib/charming/generators/templates/model/migration.rb.template +9 -0
  51. data/lib/charming/generators/templates/model/model.rb.template +6 -0
  52. data/lib/charming/generators/templates/model/spec.rb.template +9 -0
  53. data/lib/charming/generators/templates/screen/controller.rb.template +7 -0
  54. data/lib/charming/generators/templates/screen/spec_controller.rb.template +17 -0
  55. data/lib/charming/generators/templates/screen/spec_state.rb.template +17 -0
  56. data/lib/charming/generators/templates/screen/spec_view.rb.template +13 -0
  57. data/lib/charming/generators/templates/screen/state.rb.template +7 -0
  58. data/lib/charming/generators/templates/screen/view.rb.template +11 -0
  59. data/lib/charming/generators/templates/view/view.rb.template +11 -0
  60. data/lib/charming/generators/view_generator.rb +19 -3
  61. data/lib/charming/internal/renderer/differential.rb +25 -2
  62. data/lib/charming/internal/renderer/full_repaint.rb +6 -0
  63. data/lib/charming/internal/terminal/adapter.rb +29 -3
  64. data/lib/charming/internal/terminal/key_normalizer.rb +84 -0
  65. data/lib/charming/internal/terminal/memory_backend.rb +28 -1
  66. data/lib/charming/internal/terminal/mouse_parser.rb +81 -0
  67. data/lib/charming/internal/terminal/tty_backend.rb +65 -115
  68. data/lib/charming/presentation/component.rb +3 -5
  69. data/lib/charming/presentation/components/activity_indicator.rb +173 -134
  70. data/lib/charming/presentation/components/command_palette.rb +94 -96
  71. data/lib/charming/presentation/components/command_palette_modal.rb +33 -0
  72. data/lib/charming/presentation/components/empty_state.rb +47 -36
  73. data/lib/charming/presentation/components/form/builder.rb +52 -40
  74. data/lib/charming/presentation/components/form/confirm.rb +50 -39
  75. data/lib/charming/presentation/components/form/field.rb +94 -71
  76. data/lib/charming/presentation/components/form/input.rb +61 -49
  77. data/lib/charming/presentation/components/form/note.rb +27 -20
  78. data/lib/charming/presentation/components/form/select.rb +84 -63
  79. data/lib/charming/presentation/components/form/textarea.rb +67 -53
  80. data/lib/charming/presentation/components/form.rb +120 -93
  81. data/lib/charming/presentation/components/keyboard_handler.rb +41 -43
  82. data/lib/charming/presentation/components/list.rb +123 -97
  83. data/lib/charming/presentation/components/markdown.rb +21 -17
  84. data/lib/charming/presentation/components/modal.rb +55 -43
  85. data/lib/charming/presentation/components/progressbar.rb +61 -50
  86. data/lib/charming/presentation/components/spinner.rb +35 -27
  87. data/lib/charming/presentation/components/table.rb +108 -85
  88. data/lib/charming/presentation/components/text_area.rb +219 -173
  89. data/lib/charming/presentation/components/text_input.rb +120 -98
  90. data/lib/charming/presentation/components/viewport.rb +218 -168
  91. data/lib/charming/presentation/layout/builder.rb +84 -0
  92. data/lib/charming/presentation/layout/overlay.rb +55 -0
  93. data/lib/charming/presentation/layout/pane.rb +149 -0
  94. data/lib/charming/presentation/layout/rect.rb +21 -0
  95. data/lib/charming/presentation/layout/screen_layout.rb +58 -0
  96. data/lib/charming/presentation/layout/split.rb +132 -0
  97. data/lib/charming/presentation/layout.rb +28 -30
  98. data/lib/charming/presentation/markdown/block_renderers.rb +118 -0
  99. data/lib/charming/presentation/markdown/inline_renderers.rb +66 -0
  100. data/lib/charming/presentation/markdown/render_context.rb +20 -0
  101. data/lib/charming/presentation/markdown/renderer.rb +82 -174
  102. data/lib/charming/presentation/markdown/syntax_highlighter.rb +58 -44
  103. data/lib/charming/presentation/markdown.rb +4 -3
  104. data/lib/charming/presentation/template_view.rb +22 -17
  105. data/lib/charming/presentation/templates/erb_handler.rb +4 -6
  106. data/lib/charming/presentation/templates.rb +47 -32
  107. data/lib/charming/presentation/ui/ansi_codes.rb +87 -0
  108. data/lib/charming/presentation/ui/ansi_slicer.rb +92 -0
  109. data/lib/charming/presentation/ui/border.rb +24 -26
  110. data/lib/charming/presentation/ui/border_painter.rb +56 -0
  111. data/lib/charming/presentation/ui/canvas.rb +80 -0
  112. data/lib/charming/presentation/ui/style.rb +170 -205
  113. data/lib/charming/presentation/ui/theme.rb +133 -135
  114. data/lib/charming/presentation/ui/width.rb +12 -14
  115. data/lib/charming/presentation/ui.rb +70 -213
  116. data/lib/charming/presentation/view.rb +107 -92
  117. data/lib/charming/runtime.rb +25 -10
  118. data/lib/charming/tasks/inline_executor.rb +9 -0
  119. data/lib/charming/tasks/task.rb +3 -0
  120. data/lib/charming/tasks/threaded_executor.rb +12 -0
  121. data/lib/charming/version.rb +1 -1
  122. data/lib/charming.rb +16 -2
  123. metadata +60 -10
  124. data/lib/charming/generators/app_generator/app_spec_templates.rb +0 -90
  125. data/lib/charming/generators/app_generator/basic_templates.rb +0 -81
  126. data/lib/charming/generators/app_generator/component_templates.rb +0 -36
  127. data/lib/charming/generators/app_generator/controller_template.rb +0 -60
  128. data/lib/charming/generators/app_generator/database_templates.rb +0 -45
  129. data/lib/charming/generators/app_generator/layout_template.rb +0 -66
  130. data/lib/charming/generators/app_generator/screen_spec_templates.rb +0 -69
  131. data/lib/charming/generators/app_generator/state_templates.rb +0 -30
  132. data/lib/charming/generators/app_generator/view_template.rb +0 -84
@@ -1,218 +1,264 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Charming
4
- module Presentation
5
- module Components
6
- class TextArea < Component
7
- attr_reader :value, :cursor, :offset, :preferred_column
8
-
9
- def initialize(value: "", placeholder: "", width: nil, height: nil, cursor: nil, offset: 0, preferred_column: nil)
10
- super()
11
- @value = value.dup
12
- @placeholder = placeholder
13
- @width = width
14
- @height = height
15
- @cursor = cursor || @value.length
16
- @offset = offset
17
- @preferred_column = preferred_column
18
- clamp_position
19
- ensure_cursor_visible
20
- end
4
+ module Components
5
+ # TextArea is a multi-line text editor component. Supports character insertion (with
6
+ # newline insertion via Shift+Enter or Ctrl+J), cursor movement (left/right/up/down,
7
+ # home/end, page up/down), deletion (backspace/delete), and scrolling for long buffers.
8
+ # Vertical movement preserves a "preferred column" so left/right navigation feels stable.
9
+ class TextArea < Component
10
+ # The current text value, cursor byte offset, top-visible row offset, and remembered
11
+ # column for vertical navigation, respectively.
12
+ attr_reader :value, :cursor, :offset, :preferred_column
13
+
14
+ # *value* is the initial text. *placeholder* is shown when the value is empty. *width* and
15
+ # *height* constrain the rendered output. *cursor* defaults to the end of the value.
16
+ # *offset* is the top-visible row. *preferred_column* is the column to resume at on
17
+ # vertical movement (defaults to the current column on first use).
18
+ def initialize(value: "", placeholder: "", width: nil, height: nil, cursor: nil, offset: 0, preferred_column: nil)
19
+ super()
20
+ @value = value.dup
21
+ @placeholder = placeholder
22
+ @width = width
23
+ @height = height
24
+ @cursor = cursor || @value.length
25
+ @offset = offset
26
+ @preferred_column = preferred_column
27
+ clamp_position
28
+ ensure_cursor_visible
29
+ end
21
30
 
22
- def handle_key(event)
23
- key = Charming.key_of(event)
24
- return :handled if newline_event?(event) && insert("\n")
25
- return :handled if character_event?(event) && insert(event.char)
26
-
27
- case key
28
- when :left then move_left
29
- when :right then move_right
30
- when :up then move_up
31
- when :down then move_down
32
- when :home then move_home
33
- when :end then move_end
34
- when :backspace then delete_before_cursor
35
- when :delete then delete_at_cursor
36
- when :page_up then page_up
37
- when :page_down then page_down
38
- else return nil
39
- end
40
-
41
- :handled
42
- end
31
+ # Routes key events to the appropriate cursor/text mutation. Returns :handled when the
32
+ # event was consumed, nil otherwise.
33
+ def handle_key(event)
34
+ key = Charming.key_of(event)
35
+ return :handled if newline_event?(event) && insert("\n")
36
+ return :handled if character_event?(event) && insert(event.char)
37
+
38
+ case key
39
+ when :left then move_left
40
+ when :right then move_right
41
+ when :up then move_up
42
+ when :down then move_down
43
+ when :home then move_home
44
+ when :end then move_end
45
+ when :backspace then delete_before_cursor
46
+ when :delete then delete_at_cursor
47
+ when :page_up then page_up
48
+ when :page_down then page_down
49
+ else return nil
50
+ end
51
+
52
+ :handled
53
+ end
43
54
 
44
- def render
45
- visible_lines.map { |line| render_line(line) }.join("\n")
46
- end
55
+ # Renders the visible portion of the text buffer (scrolled to `offset`), with each
56
+ # visible line either clipped to `width` or padded to it.
57
+ def render
58
+ visible_lines.map { |line| render_line(line) }.join("\n")
59
+ end
47
60
 
48
- private
61
+ private
49
62
 
50
- attr_reader :placeholder, :width, :height
63
+ attr_reader :placeholder, :width, :height
51
64
 
52
- def newline_event?(event)
53
- key = Charming.key_of(event)
54
- return true if key == :enter && event.respond_to?(:shift) && event.shift
55
- return true if key == :j && event.respond_to?(:ctrl) && event.ctrl
65
+ # True when the event represents an explicit newline request: Shift+Enter or Ctrl+J.
66
+ def newline_event?(event)
67
+ key = Charming.key_of(event)
68
+ return true if key == :enter && event.respond_to?(:shift) && event.shift
69
+ return true if key == :j && event.respond_to?(:ctrl) && event.ctrl
56
70
 
57
- false
58
- end
71
+ false
72
+ end
59
73
 
60
- def character_event?(event)
61
- event.respond_to?(:char) && event.char && event.char.length == 1 && printable?(event.char)
62
- end
74
+ # True when *event* carries a single printable character.
75
+ def character_event?(event)
76
+ event.respond_to?(:char) && event.char && event.char.length == 1 && printable?(event.char)
77
+ end
63
78
 
64
- def printable?(char)
65
- !char.match?(/[[:cntrl:]]/)
66
- end
79
+ # True when *char* is not a control character.
80
+ def printable?(char)
81
+ !char.match?(/[[:cntrl:]]/)
82
+ end
67
83
 
68
- def insert(text)
69
- @value = value[0...cursor].to_s + text + value[cursor..].to_s
70
- @cursor += text.length
71
- reset_preferred_column
72
- ensure_cursor_visible
73
- end
84
+ # Inserts *text* at the cursor, advances the cursor by its length, resets the preferred
85
+ # column, and ensures the cursor remains visible.
86
+ def insert(text)
87
+ @value = value[0...cursor].to_s + text + value[cursor..].to_s
88
+ @cursor += text.length
89
+ reset_preferred_column
90
+ ensure_cursor_visible
91
+ end
74
92
 
75
- def move_left
76
- @cursor -= 1 if cursor.positive?
77
- reset_preferred_column
78
- ensure_cursor_visible
79
- end
93
+ # Moves the cursor one character left.
94
+ def move_left
95
+ @cursor -= 1 if cursor.positive?
96
+ reset_preferred_column
97
+ ensure_cursor_visible
98
+ end
80
99
 
81
- def move_right
82
- @cursor += 1 if cursor < value.length
83
- reset_preferred_column
84
- ensure_cursor_visible
85
- end
100
+ # Moves the cursor one character right.
101
+ def move_right
102
+ @cursor += 1 if cursor < value.length
103
+ reset_preferred_column
104
+ ensure_cursor_visible
105
+ end
86
106
 
87
- def move_up
88
- move_vertical(-1)
89
- end
107
+ # Moves the cursor up one line while preserving the preferred column.
108
+ def move_up
109
+ move_vertical(-1)
110
+ end
90
111
 
91
- def move_down
92
- move_vertical(+1)
93
- end
112
+ # Moves the cursor down one line while preserving the preferred column.
113
+ def move_down
114
+ move_vertical(+1)
115
+ end
94
116
 
95
- def move_home
96
- row, = cursor_position
97
- @cursor = line_start(row)
98
- reset_preferred_column
99
- ensure_cursor_visible
100
- end
117
+ # Moves the cursor to the start of the current line.
118
+ def move_home
119
+ row, = cursor_position
120
+ @cursor = line_start(row)
121
+ reset_preferred_column
122
+ ensure_cursor_visible
123
+ end
101
124
 
102
- def move_end
103
- row, = cursor_position
104
- @cursor = line_start(row) + line_length(row)
105
- reset_preferred_column
106
- ensure_cursor_visible
107
- end
125
+ # Moves the cursor to the end of the current line.
126
+ def move_end
127
+ row, = cursor_position
128
+ @cursor = line_start(row) + line_length(row)
129
+ reset_preferred_column
130
+ ensure_cursor_visible
131
+ end
108
132
 
109
- def delete_before_cursor
110
- return if cursor.zero?
133
+ # Deletes the character before the cursor (backspace behavior).
134
+ def delete_before_cursor
135
+ return if cursor.zero?
111
136
 
112
- @value = value[0...(cursor - 1)].to_s + value[cursor..].to_s
113
- @cursor -= 1
114
- reset_preferred_column
115
- ensure_cursor_visible
116
- end
137
+ @value = value[0...(cursor - 1)].to_s + value[cursor..].to_s
138
+ @cursor -= 1
139
+ reset_preferred_column
140
+ ensure_cursor_visible
141
+ end
117
142
 
118
- def delete_at_cursor
119
- return if cursor >= value.length
143
+ # Deletes the character at the cursor (delete-key behavior).
144
+ def delete_at_cursor
145
+ return if cursor >= value.length
120
146
 
121
- @value = value[0...cursor].to_s + value[(cursor + 1)..].to_s
122
- reset_preferred_column
123
- ensure_cursor_visible
124
- end
147
+ @value = value[0...cursor].to_s + value[(cursor + 1)..].to_s
148
+ reset_preferred_column
149
+ ensure_cursor_visible
150
+ end
125
151
 
126
- def page_up
127
- @offset -= viewport_height
128
- clamp_offset
129
- end
152
+ # Scrolls the buffer up by one viewport height.
153
+ def page_up
154
+ @offset -= viewport_height
155
+ clamp_offset
156
+ end
130
157
 
131
- def page_down
132
- @offset += viewport_height
133
- clamp_offset
134
- end
158
+ # Scrolls the buffer down by one viewport height.
159
+ def page_down
160
+ @offset += viewport_height
161
+ clamp_offset
162
+ end
135
163
 
136
- def move_vertical(delta)
137
- row, column = cursor_position
138
- target_row = (row + delta).clamp(0, lines.length - 1)
139
- @preferred_column ||= column
140
- @cursor = line_start(target_row) + [@preferred_column, line_length(target_row)].min
141
- ensure_cursor_visible
142
- end
164
+ # Moves the cursor vertically by *delta* rows. Stays within the line count and uses
165
+ # `preferred_column` so up/down movement feels stable on short lines.
166
+ def move_vertical(delta)
167
+ row, column = cursor_position
168
+ target_row = (row + delta).clamp(0, lines.length - 1)
169
+ @preferred_column ||= column
170
+ @cursor = line_start(target_row) + [@preferred_column, line_length(target_row)].min
171
+ ensure_cursor_visible
172
+ end
143
173
 
144
- def reset_preferred_column
145
- @preferred_column = cursor_position.last
146
- end
174
+ # Sets the preferred column to the current column (called when horizontal movement happens).
175
+ def reset_preferred_column
176
+ @preferred_column = cursor_position.last
177
+ end
147
178
 
148
- def cursor_position
149
- before = value[0...cursor].to_s
150
- row = before.count("\n")
151
- last_newline = before.rindex("\n")
152
- column = last_newline ? before.length - last_newline - 1 : before.length
153
- [row, column]
154
- end
179
+ # Returns the cursor's current position as `[row, column]`, where row is the zero-based
180
+ # line index and column is the character offset within that line.
181
+ def cursor_position
182
+ before = value[0...cursor].to_s
183
+ row = before.count("\n")
184
+ last_newline = before.rindex("\n")
185
+ column = last_newline ? before.length - last_newline - 1 : before.length
186
+ [row, column]
187
+ end
155
188
 
156
- def line_start(row)
157
- lines.first(row).sum(&:length) + row
158
- end
189
+ # Returns the byte offset where line *row* begins in the value.
190
+ def line_start(row)
191
+ lines.first(row).sum(&:length) + row
192
+ end
159
193
 
160
- def line_length(row)
161
- lines.fetch(row, "").length
162
- end
194
+ # Returns the character length of the line at *row* (empty string when row is past the end).
195
+ def line_length(row)
196
+ lines.fetch(row, "").length
197
+ end
163
198
 
164
- def lines
165
- value.empty? ? [""] : value.split("\n", -1)
166
- end
199
+ # Splits the value into an array of lines (preserving trailing empty lines).
200
+ def lines
201
+ value.empty? ? [""] : value.split("\n", -1)
202
+ end
167
203
 
168
- def rendered_lines
169
- return [cursor_marker + placeholder] if value.empty?
204
+ # Returns the rendered lines (with cursor marker inserted) before viewport slicing.
205
+ def rendered_lines
206
+ return [cursor_marker + placeholder] if value.empty?
170
207
 
171
- (value[0...cursor].to_s + cursor_marker + value[cursor..].to_s).split("\n", -1)
172
- end
208
+ (value[0...cursor].to_s + cursor_marker + value[cursor..].to_s).split("\n", -1)
209
+ end
173
210
 
174
- def visible_lines
175
- ensure_cursor_visible
176
- rendered = rendered_lines.slice(offset, viewport_height) || []
177
- return rendered unless height
211
+ # Returns the lines that should be visible in the current viewport, padded to *height*
212
+ # with empty strings when the buffer is shorter.
213
+ def visible_lines
214
+ ensure_cursor_visible
215
+ rendered = rendered_lines.slice(offset, viewport_height) || []
216
+ return rendered unless height
178
217
 
179
- rendered + Array.new([height - rendered.length, 0].max, "")
180
- end
218
+ rendered + Array.new([height - rendered.length, 0].max, "")
219
+ end
181
220
 
182
- def render_line(line)
183
- return line unless width
221
+ # Renders a single line, clipping to *width* and padding with spaces.
222
+ def render_line(line)
223
+ return line unless width
184
224
 
185
- clipped = UI.visible_slice(line, 0, width)
186
- clipped + (" " * [width - UI::Width.measure(clipped), 0].max)
187
- end
225
+ clipped = UI.visible_slice(line, 0, width)
226
+ clipped + (" " * [width - UI::Width.measure(clipped), 0].max)
227
+ end
188
228
 
189
- def ensure_cursor_visible
190
- row, = cursor_position
191
- @offset = row if row < offset
192
- @offset = row - viewport_height + 1 if row >= offset + viewport_height
193
- clamp_offset
194
- end
229
+ # Adjusts the top-visible offset so the cursor row is in view. Scrolling is performed
230
+ # one row at a time when needed.
231
+ def ensure_cursor_visible
232
+ row, = cursor_position
233
+ @offset = row if row < offset
234
+ @offset = row - viewport_height + 1 if row >= offset + viewport_height
235
+ clamp_offset
236
+ end
195
237
 
196
- def clamp_position
197
- @cursor = cursor.clamp(0, value.length)
198
- clamp_offset
199
- end
238
+ # Clamps the cursor and offset to valid bounds.
239
+ def clamp_position
240
+ @cursor = cursor.clamp(0, value.length)
241
+ clamp_offset
242
+ end
200
243
 
201
- def clamp_offset
202
- @offset = offset.clamp(0, max_offset)
203
- end
244
+ # Clamps the offset to the valid range `[0, max_offset]`.
245
+ def clamp_offset
246
+ @offset = offset.clamp(0, max_offset)
247
+ end
204
248
 
205
- def max_offset
206
- [lines.length - viewport_height, 0].max
207
- end
249
+ # Returns the maximum allowed offset (so the bottom of the buffer is reachable).
250
+ def max_offset
251
+ [lines.length - viewport_height, 0].max
252
+ end
208
253
 
209
- def viewport_height
210
- height || lines.length
211
- end
254
+ # Returns the visible row count (the configured *height* or the buffer's line count).
255
+ def viewport_height
256
+ height || lines.length
257
+ end
212
258
 
213
- def cursor_marker
214
- "|"
215
- end
259
+ # The literal character used to mark the cursor position in `rendered_lines`.
260
+ def cursor_marker
261
+ "|"
216
262
  end
217
263
  end
218
264
  end