charming 0.1.0 → 0.1.1

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 (99) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +38 -378
  3. data/lib/charming/application.rb +3 -3
  4. data/lib/charming/{application_model.rb → application_state.rb} +3 -3
  5. data/lib/charming/cli.rb +39 -3
  6. data/lib/charming/controller.rb +146 -24
  7. data/lib/charming/database_commands.rb +87 -0
  8. data/lib/charming/database_installer.rb +125 -0
  9. data/lib/charming/events/key_event.rb +15 -0
  10. data/lib/charming/events/mouse_event.rb +42 -0
  11. data/lib/charming/events/resize_event.rb +9 -0
  12. data/lib/charming/events/task_event.rb +19 -0
  13. data/lib/charming/events/timer_event.rb +9 -0
  14. data/lib/charming/generators/app_generator/app_spec_templates.rb +12 -8
  15. data/lib/charming/generators/app_generator/basic_templates.rb +14 -2
  16. data/lib/charming/generators/app_generator/component_templates.rb +1 -1
  17. data/lib/charming/generators/app_generator/controller_template.rb +3 -12
  18. data/lib/charming/generators/app_generator/database_templates.rb +45 -0
  19. data/lib/charming/generators/app_generator/layout_template.rb +51 -145
  20. data/lib/charming/generators/app_generator/screen_spec_templates.rb +7 -8
  21. data/lib/charming/generators/app_generator/{model_templates.rb → state_templates.rb} +5 -5
  22. data/lib/charming/generators/app_generator/view_template.rb +12 -18
  23. data/lib/charming/generators/app_generator.rb +37 -11
  24. data/lib/charming/generators/component_generator.rb +1 -1
  25. data/lib/charming/generators/controller_generator.rb +1 -4
  26. data/lib/charming/generators/model_generator.rb +119 -0
  27. data/lib/charming/generators/name.rb +0 -4
  28. data/lib/charming/generators/screen_generator.rb +14 -28
  29. data/lib/charming/generators/view_generator.rb +11 -14
  30. data/lib/charming/internal/renderer/differential.rb +2 -3
  31. data/lib/charming/internal/terminal/tty_backend.rb +25 -8
  32. data/lib/charming/presentation/component.rb +10 -0
  33. data/lib/charming/presentation/components/activity_indicator.rb +160 -0
  34. data/lib/charming/presentation/components/command_palette.rb +120 -0
  35. data/lib/charming/presentation/components/empty_state.rb +43 -0
  36. data/lib/charming/presentation/components/form/builder.rb +48 -0
  37. data/lib/charming/presentation/components/form/confirm.rb +56 -0
  38. data/lib/charming/presentation/components/form/field.rb +96 -0
  39. data/lib/charming/presentation/components/form/input.rb +57 -0
  40. data/lib/charming/presentation/components/form/note.rb +32 -0
  41. data/lib/charming/presentation/components/form/select.rb +89 -0
  42. data/lib/charming/presentation/components/form/textarea.rb +70 -0
  43. data/lib/charming/presentation/components/form.rb +127 -0
  44. data/lib/charming/presentation/components/keyboard_handler.rb +58 -0
  45. data/lib/charming/presentation/components/list.rb +104 -0
  46. data/lib/charming/presentation/components/markdown.rb +25 -0
  47. data/lib/charming/presentation/components/modal.rb +50 -0
  48. data/lib/charming/presentation/components/progressbar.rb +57 -0
  49. data/lib/charming/presentation/components/spinner.rb +39 -0
  50. data/lib/charming/presentation/components/table.rb +118 -0
  51. data/lib/charming/presentation/components/text_area.rb +219 -0
  52. data/lib/charming/presentation/components/text_input.rb +105 -0
  53. data/lib/charming/presentation/components/viewport.rb +220 -0
  54. data/lib/charming/presentation/layout.rb +43 -0
  55. data/lib/charming/presentation/markdown/renderer.rb +203 -0
  56. data/lib/charming/presentation/markdown/syntax_highlighter.rb +63 -0
  57. data/lib/charming/presentation/markdown.rb +8 -0
  58. data/lib/charming/presentation/template_view.rb +27 -0
  59. data/lib/charming/presentation/templates/erb_handler.rb +15 -0
  60. data/lib/charming/presentation/templates.rb +51 -0
  61. data/lib/charming/presentation/ui/border.rb +35 -0
  62. data/lib/charming/presentation/ui/style.rb +246 -0
  63. data/lib/charming/presentation/ui/theme.rb +180 -0
  64. data/lib/charming/{ui → presentation/ui}/themes/phosphor.json +2 -2
  65. data/lib/charming/presentation/ui/width.rb +26 -0
  66. data/lib/charming/presentation/ui.rb +232 -0
  67. data/lib/charming/presentation/view.rb +118 -0
  68. data/lib/charming/runtime.rb +7 -7
  69. data/lib/charming/screen.rb +5 -1
  70. data/lib/charming/tasks/inline_executor.rb +28 -0
  71. data/lib/charming/tasks/task.rb +9 -0
  72. data/lib/charming/{task_executor.rb → tasks/threaded_executor.rb} +4 -27
  73. data/lib/charming/version.rb +1 -1
  74. data/lib/charming.rb +4 -0
  75. metadata +114 -29
  76. data/lib/charming/component.rb +0 -8
  77. data/lib/charming/components/activity_indicator.rb +0 -158
  78. data/lib/charming/components/command_palette.rb +0 -118
  79. data/lib/charming/components/keyboard_handler.rb +0 -22
  80. data/lib/charming/components/list.rb +0 -105
  81. data/lib/charming/components/modal.rb +0 -48
  82. data/lib/charming/components/progressbar.rb +0 -55
  83. data/lib/charming/components/spinner.rb +0 -37
  84. data/lib/charming/components/table.rb +0 -115
  85. data/lib/charming/components/text_input.rb +0 -103
  86. data/lib/charming/components/viewport.rb +0 -191
  87. data/lib/charming/key_event.rb +0 -13
  88. data/lib/charming/mouse_event.rb +0 -40
  89. data/lib/charming/resize_event.rb +0 -7
  90. data/lib/charming/task.rb +0 -7
  91. data/lib/charming/task_event.rb +0 -17
  92. data/lib/charming/timer_event.rb +0 -7
  93. data/lib/charming/ui/border.rb +0 -33
  94. data/lib/charming/ui/style.rb +0 -244
  95. data/lib/charming/ui/theme.rb +0 -178
  96. data/lib/charming/ui/width.rb +0 -24
  97. data/lib/charming/ui.rb +0 -230
  98. data/lib/charming/view.rb +0 -116
  99. /data/lib/charming/{generators.rb → generators/error.rb} +0 -0
@@ -1,118 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Charming
4
- module Components
5
- # CommandPalette renders a fuzzy-searchable command picker UI. It wraps a TextInput for search
6
- # input and a List for result display, dispatching key events between them. Users type to filter
7
- # the registered commands by label match, navigate with up/down/home/end keys (delegated to List),
8
- # confirm a selection with Enter (returns [:selected, command]), or cancel with Escape (returns :cancelled).
9
- # State is serializable as a hash of value/cursor/selected_index for session persistence.
10
- class CommandPalette < Component
11
- Command = Data.define(:label, :value)
12
-
13
- # A single command palette entry: a human-readable +label+ and a callable or
14
- # method symbol +value+ that gets executed when the user selects it.
15
- attr_reader :commands, :input
16
-
17
- # Initializes the dropdown widget with a list of Command entries and search
18
- # parameters for building the underlying TextInput (placeholder text, cursor
19
- # position, value) and List (display height, initial selection). Returns void;
20
- # the state is later serializable via +state+ for session persistence.
21
- def initialize(commands:, placeholder: "Search commands", height: nil, value: "", cursor: nil, selected_index: 0, theme: nil)
22
- super(theme: theme)
23
- @commands = commands
24
- @height = height
25
- @input = TextInput.new(value: value, placeholder: placeholder, cursor: cursor)
26
- @list = build_list(selected_index: selected_index)
27
- end
28
-
29
- # Returns the currently displayed Command entry in the List at the time of calling.
30
- # Returns nil if no entry is highlighted (i.e., user has opened the palette but not
31
- # moved the selection). Useful for retrieving the result after key handling.
32
- def selected_command
33
- list.selected_item
34
- end
35
-
36
- # Collects the current state of the TextInput and List into a serializable hash
37
- # suitable for round-trip storage in session. Returns {value:, cursor:, selected_index:}.
38
- def state
39
- {
40
- value: input.value,
41
- cursor: input.cursor,
42
- selected_index: list.selected_index
43
- }
44
- end
45
-
46
- # Handles key events by routing them to the appropriate sub-component: Escape kills the
47
- # palette returning :cancelled; up/down/home/end keys go to the List selection handler
48
- # via handle_list_key; all other keys (including typed characters) are passed to the TextInput
49
- # which manages cursor position and input filtering. If a list key match fails, falls through
50
- # to the TextInput handler. Returns nil/nil if no handler consumed the event, or :cancelled when
51
- # Escape is pressed.
52
- def handle_key(event)
53
- key = Charming.key_of(event)
54
- return :cancelled if key == :escape
55
-
56
- return handle_list_key(event) if list_key?(key)
57
-
58
- handle_input_key(event)
59
- end
60
-
61
- # Renders the command palette as a vertically-stacked text representation: the search TextInput
62
- # row on line 1, and then the filtered List results (or "No commands found") on subsequent lines.
63
- # Returns a multiline string suitable for terminal rendering.
64
- def render
65
- [input.render, render_results].join("\n")
66
- end
67
-
68
- private
69
-
70
- attr_reader :height, :list
71
-
72
- # Delegates key handling entirely to the internal List widget, which manages up/down/home/end selection.
73
- # Returns whatever the List's handle_key returns (typically nil or the symbol from the subclass).
74
- def handle_list_key(event)
75
- list.handle_key(event)
76
- end
77
-
78
- # Passes the key event to the TextInput for cursor position and search text management.
79
- # If the input returns :handled, rebuilds the List so that filtering is re-evaluated against
80
- # the new input value. Returns nil/nil if no handler consumed the event.
81
- def handle_input_key(event)
82
- result = input.handle_key(event)
83
- @list = build_list if result == :handled
84
- result
85
- end
86
-
87
- # Checks whether the given key is a List-navigation key (up/down/home/end). Returns true for those keys
88
- # so they can be dispatched via +handle_list_key+ rather than falling through to TextInput.
89
- def list_key?(key)
90
- %i[up down home end enter].include?(key)
91
- end
92
-
93
- # Renders the filtered results section below the search input. If no commands match the current filter text,
94
- # returns "No commands found"; otherwise renders the List widget's styled display string. Returns a single-line string.
95
- def render_results
96
- return "No commands found" if filtered_commands.empty?
97
-
98
- list.render
99
- end
100
-
101
- # Builds a new List from the currently filtered commands at the given selected_index height and label extractor.
102
- # The +selected_index+ parameter defaults to the last known value in +list+ to preserve scroll position across rebuilds.
103
- def build_list(selected_index: list&.selected_index || 0)
104
- List.new(items: filtered_commands, selected_index: selected_index, height: height, label: :label.to_proc, theme: theme)
105
- end
106
-
107
- # Returns the full commands array when input value is empty; otherwise a subset whose labels match case-insensitively
108
- # against the current TextInput value. Used to drive the fuzzy search behavior. Returns an Array of Command entries.
109
- def filtered_commands
110
- return commands if input.value.empty?
111
-
112
- commands.select do |command|
113
- command.label.downcase.include?(input.value.downcase)
114
- end
115
- end
116
- end
117
- end
118
- end
@@ -1,22 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Charming
4
- module Components
5
- # KeyboardHandler is a mixin module that provides keyboard event dispatch by mapping symbolic key names
6
- # to private method calls. Implementors must define a constant +KEY_ACTIONS+ as a hash where each key is
7
- # a symbol (e.g., :up, :down, :enter) and each value is the target method name (e.g., :move_up). Call
8
- # +handle_key(event)+ with any event object; it uses Charming.key_of to resolve the raw event to a symbol,
9
- # looks up the corresponding action in KEY_ACTIONS, sends that method on self, and returns :handled if an
10
- # action was found. Returns nil (via :handled being truthy or not) when no matching key exists.
11
- module KeyboardHandler
12
- def handle_key(event)
13
- key = Charming.key_of(event)
14
- action = self.class.const_get(:KEY_ACTIONS)[key]
15
- return unless action
16
-
17
- send(action)
18
- :handled
19
- end
20
- end
21
- end
22
- end
@@ -1,105 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Charming
4
- module Components
5
- class List < Component
6
- include KeyboardHandler
7
-
8
- # Maps navigation key symbols to instance methods consumed by the KeyboardHandler
9
- # mixin: :up moves selection up, :down moves down, :home jumps to first item,
10
- # :end jumps to last. See Viewport#KEY_ACTIONS and Table#KEY_ACTIONS for identical pattern.
11
- KEY_ACTIONS = {
12
- up: :move_up,
13
- down: :move_down,
14
- home: :move_home,
15
- end: :move_end
16
- }.freeze
17
-
18
- attr_reader :items, :selected_index
19
-
20
- def initialize(items:, selected_index: 0, height: nil, label: nil, theme: nil)
21
- super(theme: theme)
22
- @items = items
23
- @selected_index = selected_index
24
- @height = height
25
- @label = label || :to_s.to_proc
26
- clamp_position
27
- end
28
-
29
- def handle_key(event)
30
- return [:selected, selected_item] if Charming.key_of(event) == :enter && selected_item
31
-
32
- super
33
- end
34
-
35
- def handle_mouse(event)
36
- return nil unless @height
37
- return nil unless event.respond_to?(:click?) && event.click?
38
-
39
- clicked = event.y
40
- return nil if clicked.negative? || clicked >= visible_items.length
41
-
42
- @selected_index = viewport_start + clicked
43
- clamp_position
44
- :handled
45
- end
46
-
47
- def selected_item
48
- items[selected_index]
49
- end
50
-
51
- def render
52
- visible_items.each_with_index.map do |item, index|
53
- render_item(item, viewport_start + index)
54
- end.join("\n")
55
- end
56
-
57
- private
58
-
59
- def move_up
60
- @selected_index -= 1 if selected_index.positive?
61
- end
62
-
63
- def move_down
64
- @selected_index += 1 if selected_index < items.length - 1
65
- end
66
-
67
- def move_home
68
- @selected_index = 0
69
- end
70
-
71
- def move_end
72
- @selected_index = items.length - 1 unless items.empty?
73
- end
74
-
75
- def visible_items
76
- items[viewport_start, viewport_height] || []
77
- end
78
-
79
- def viewport_start
80
- return 0 unless @height
81
-
82
- (selected_index - @height + 1).clamp(0, max_viewport_start)
83
- end
84
-
85
- def viewport_height
86
- @height || items.length
87
- end
88
-
89
- def max_viewport_start
90
- [items.length - @height, 0].max
91
- end
92
-
93
- def render_item(item, index)
94
- prefix = (index == selected_index) ? "> " : " "
95
- rendered = "#{prefix}#{@label.call(item)}"
96
- (index == selected_index) ? theme.selected.render(rendered) : rendered
97
- end
98
-
99
- def clamp_position
100
- @selected_index = 0 if items.empty?
101
- @selected_index = selected_index.clamp(0, items.length - 1) unless items.empty?
102
- end
103
- end
104
- end
105
- end
@@ -1,48 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Charming
4
- module Components
5
- class Modal < Component
6
- def initialize(content:, title: nil, help: nil, width: 52, style: nil, theme: nil)
7
- super(theme: theme)
8
- @content = content
9
- @title = title
10
- @help = help
11
- @width = width
12
- @style = style
13
- end
14
-
15
- def render
16
- box(column(*lines, gap: 1), style: modal_style)
17
- end
18
-
19
- private
20
-
21
- attr_reader :content, :title, :help, :width
22
-
23
- def lines
24
- [title_line, help_line, render_content].compact
25
- end
26
-
27
- def title_line
28
- text(title, style: theme.title.align(:center).width(title_width)) if title
29
- end
30
-
31
- def help_line
32
- text(help, style: theme.muted) if help
33
- end
34
-
35
- def render_content
36
- content.respond_to?(:render) ? render_component(content) : content.to_s
37
- end
38
-
39
- def modal_style
40
- @style || theme.modal.width(width)
41
- end
42
-
43
- def title_width
44
- [width - 8, 0].max
45
- end
46
- end
47
- end
48
- end
@@ -1,55 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Charming
4
- module Components
5
- class Progressbar < Component
6
- attr_accessor :total, :current, :label, :complete, :incomplete, :bar_format
7
-
8
- def initialize(total:, complete: "=", incomplete: " ", bar_format: :classic, label: nil)
9
- super()
10
- @total = [total.to_i, 0].max
11
- @complete = complete.to_s
12
- @incomplete = incomplete.to_s
13
- @bar_format = bar_format.to_sym
14
- @label = label
15
- @current = 0
16
- end
17
-
18
- def tick(count = 1)
19
- update(@current + count)
20
- self
21
- end
22
-
23
- def update(value)
24
- @current = value.to_i.clamp(0, @total)
25
- self
26
- end
27
-
28
- def complete!
29
- @current = @total
30
- self
31
- end
32
-
33
- def render
34
- width = [@total, 1].max
35
- completed = completed_width(width)
36
- incomplete = width - completed
37
- incomplete -= 1 if @current.zero?
38
- bar = (@complete * completed) + (@incomplete * incomplete)
39
- result = "[" + bar + "]"
40
-
41
- return result unless @label
42
-
43
- "#{result} #{@label}"
44
- end
45
-
46
- private
47
-
48
- def completed_width(width)
49
- return 0 unless @total.positive?
50
-
51
- ((width * @current) / @total.to_f).round
52
- end
53
- end
54
- end
55
- end
@@ -1,37 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Charming
4
- module Components
5
- class Spinner < Component
6
- DEFAULT_FRAMES = ["-", "\\", "|", "/"].freeze
7
-
8
- attr_reader :frames, :index, :label
9
-
10
- def initialize(frames: DEFAULT_FRAMES, index: 0, label: nil)
11
- super()
12
- raise ArgumentError, "frames cannot be empty" if frames.empty?
13
-
14
- @frames = frames
15
- @index = index
16
- @label = label
17
- end
18
-
19
- def tick
20
- @index = (index + 1) % frames.length
21
- self
22
- end
23
-
24
- def render
25
- return frame unless label
26
-
27
- "#{frame} #{label}"
28
- end
29
-
30
- private
31
-
32
- def frame
33
- frames.fetch(index % frames.length)
34
- end
35
- end
36
- end
37
- end
@@ -1,115 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "tty-table"
4
-
5
- module Charming
6
- module Components
7
- class Table < Component
8
- include KeyboardHandler
9
-
10
- KEY_ACTIONS = {
11
- up: :move_up,
12
- down: :move_down,
13
- home: :move_home,
14
- end: :move_end
15
- }.freeze
16
-
17
- HEADER_HEIGHT = 2
18
-
19
- attr_reader :header, :rows, :selected_index
20
-
21
- def initialize(header:, rows: [], selected_index: 0)
22
- super()
23
- @header = Array(header).map(&:to_s)
24
- @rows = Array(rows)
25
- @selected_index = clamp_index(selected_index)
26
- end
27
-
28
- def handle_key(event)
29
- return nil if rows.empty?
30
-
31
- case Charming.key_of(event)
32
- when :enter then [:selected, selected_row]
33
- else super
34
- end
35
- end
36
-
37
- def handle_mouse(event)
38
- return nil if rows.empty?
39
- return nil unless event.respond_to?(:click?) && event.click?
40
-
41
- clicked = event.y - HEADER_HEIGHT
42
- return nil if clicked.negative? || clicked >= rows.length
43
-
44
- @selected_index = clicked
45
- :handled
46
- end
47
-
48
- def selected_row
49
- rows[selected_index]
50
- end
51
-
52
- def render
53
- return "(empty table)" if header.empty? && rows.empty?
54
-
55
- normalized = rows.map { |row| normalize_row(row) }
56
- lines = TTY::Table.new(header: header, rows: normalized)
57
- .render(:unicode)
58
- .lines(chomp: true)
59
-
60
- compact_layout(lines)
61
- end
62
-
63
- private
64
-
65
- def normalize_row(row)
66
- cells = case row
67
- when Hash then row.values
68
- when String then [row]
69
- else Array(row)
70
- end
71
- return cells if header.length <= 1 || cells.length <= header.length
72
-
73
- kept = cells.first(header.length - 1)
74
- merged = cells[(header.length - 1)..].join(" ")
75
- kept + [merged]
76
- end
77
-
78
- def compact_layout(lines)
79
- return lines.join("\n") if lines.length < 4
80
-
81
- top, header_line, _separator, *rest = lines
82
- body = rest.first(rows.length)
83
- bottom = rest[rows.length]
84
-
85
- highlighted = body.each_with_index.map do |line, index|
86
- (index == selected_index) ? "\e[7m#{line}\e[m" : line
87
- end
88
-
89
- [top, header_line, *highlighted, bottom].compact.join("\n")
90
- end
91
-
92
- def move_up
93
- @selected_index -= 1 if selected_index.positive?
94
- end
95
-
96
- def move_down
97
- @selected_index += 1 if selected_index < rows.length - 1
98
- end
99
-
100
- def move_home
101
- @selected_index = 0
102
- end
103
-
104
- def move_end
105
- @selected_index = rows.length - 1
106
- end
107
-
108
- def clamp_index(value)
109
- return 0 if rows.empty?
110
-
111
- value.to_i.clamp(0, rows.length - 1)
112
- end
113
- end
114
- end
115
- end
@@ -1,103 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Charming
4
- module Components
5
- class TextInput < Component
6
- include KeyboardHandler
7
-
8
- # Maps editing keys (left/right/home/end/backspace/delete) to the instance
9
- # methods they dispatch via KeyboardHandler. Each symbol key (e.g., :left)
10
- # maps to a method (e.g., :move_left) that adjusts cursor position or text content.
11
- KEY_ACTIONS = {
12
- left: :move_left,
13
- right: :move_right,
14
- home: :move_home,
15
- end: :move_end,
16
- backspace: :delete_before_cursor,
17
- delete: :delete_at_cursor
18
- }.freeze
19
-
20
- attr_reader :value, :cursor
21
-
22
- def initialize(value: "", placeholder: "", width: nil, cursor: nil)
23
- super()
24
- @value = value.dup
25
- @placeholder = placeholder
26
- @width = width
27
- @cursor = cursor || @value.length
28
- clamp_position
29
- end
30
-
31
- def handle_key(event)
32
- return :handled if character_event?(event) && insert(event.char)
33
-
34
- super
35
- end
36
-
37
- def render
38
- rendered = render_value
39
- @width ? style.width(@width).render(rendered) : rendered
40
- end
41
-
42
- private
43
-
44
- attr_reader :placeholder
45
-
46
- def character_event?(event)
47
- event.respond_to?(:char) && event.char && event.char.length == 1 && printable?(event.char)
48
- end
49
-
50
- def printable?(char)
51
- !char.match?(/[[:cntrl:]]/)
52
- end
53
-
54
- def insert(char)
55
- @value = value[0...cursor] + char + value[cursor..]
56
- @cursor += char.length
57
- end
58
-
59
- def move_left
60
- @cursor -= 1 if cursor.positive?
61
- end
62
-
63
- def move_right
64
- @cursor += 1 if cursor < value.length
65
- end
66
-
67
- def move_home
68
- @cursor = 0
69
- end
70
-
71
- def move_end
72
- @cursor = value.length
73
- end
74
-
75
- def delete_before_cursor
76
- return if cursor.zero?
77
-
78
- @value = value[0...(cursor - 1)] + value[cursor..]
79
- @cursor -= 1
80
- end
81
-
82
- def delete_at_cursor
83
- return if cursor >= value.length
84
-
85
- @value = value[0...cursor] + value[(cursor + 1)..]
86
- end
87
-
88
- def render_value
89
- return cursor_marker + placeholder if value.empty?
90
-
91
- value[0...cursor] + cursor_marker + value[cursor..]
92
- end
93
-
94
- def cursor_marker
95
- "|"
96
- end
97
-
98
- def clamp_position
99
- @cursor = cursor.clamp(0, value.length)
100
- end
101
- end
102
- end
103
- end