charming 0.1.2 → 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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/lib/charming/application.rb +3 -3
  3. data/lib/charming/controller/class_methods.rb +2 -2
  4. data/lib/charming/controller/command_palette.rb +2 -2
  5. data/lib/charming/controller/rendering.rb +2 -2
  6. data/lib/charming/controller/session_state.rb +1 -1
  7. data/lib/charming/generators/component_generator.rb +1 -1
  8. data/lib/charming/generators/templates/app/application.template +1 -1
  9. data/lib/charming/generators/templates/app/layout.template +3 -6
  10. data/lib/charming/generators/templates/app/view.template +1 -1
  11. data/lib/charming/generators/templates/component/component.rb.template +1 -1
  12. data/lib/charming/generators/templates/screen/view.rb.template +1 -1
  13. data/lib/charming/generators/templates/view/view.rb.template +1 -1
  14. data/lib/charming/internal/renderer/differential.rb +13 -5
  15. data/lib/charming/internal/terminal/tty_backend.rb +22 -2
  16. data/lib/charming/presentation/component.rb +3 -5
  17. data/lib/charming/presentation/components/activity_indicator.rb +173 -134
  18. data/lib/charming/presentation/components/command_palette.rb +94 -96
  19. data/lib/charming/presentation/components/command_palette_modal.rb +33 -0
  20. data/lib/charming/presentation/components/empty_state.rb +47 -49
  21. data/lib/charming/presentation/components/form/builder.rb +52 -54
  22. data/lib/charming/presentation/components/form/confirm.rb +49 -51
  23. data/lib/charming/presentation/components/form/field.rb +94 -96
  24. data/lib/charming/presentation/components/form/input.rb +53 -55
  25. data/lib/charming/presentation/components/form/note.rb +27 -29
  26. data/lib/charming/presentation/components/form/select.rb +84 -86
  27. data/lib/charming/presentation/components/form/textarea.rb +67 -69
  28. data/lib/charming/presentation/components/form.rb +120 -122
  29. data/lib/charming/presentation/components/keyboard_handler.rb +41 -43
  30. data/lib/charming/presentation/components/list.rb +123 -125
  31. data/lib/charming/presentation/components/markdown.rb +21 -23
  32. data/lib/charming/presentation/components/modal.rb +46 -48
  33. data/lib/charming/presentation/components/progressbar.rb +51 -53
  34. data/lib/charming/presentation/components/spinner.rb +40 -42
  35. data/lib/charming/presentation/components/table.rb +109 -111
  36. data/lib/charming/presentation/components/text_area.rb +219 -221
  37. data/lib/charming/presentation/components/text_input.rb +120 -122
  38. data/lib/charming/presentation/components/viewport.rb +218 -220
  39. data/lib/charming/presentation/layout/builder.rb +64 -66
  40. data/lib/charming/presentation/layout/overlay.rb +48 -50
  41. data/lib/charming/presentation/layout/pane.rb +122 -118
  42. data/lib/charming/presentation/layout/rect.rb +14 -16
  43. data/lib/charming/presentation/layout/screen_layout.rb +40 -42
  44. data/lib/charming/presentation/layout/split.rb +101 -103
  45. data/lib/charming/presentation/layout.rb +28 -30
  46. data/lib/charming/presentation/markdown/block_renderers.rb +94 -96
  47. data/lib/charming/presentation/markdown/inline_renderers.rb +52 -54
  48. data/lib/charming/presentation/markdown/render_context.rb +12 -14
  49. data/lib/charming/presentation/markdown/renderer.rb +84 -86
  50. data/lib/charming/presentation/markdown/syntax_highlighter.rb +57 -59
  51. data/lib/charming/presentation/markdown.rb +4 -6
  52. data/lib/charming/presentation/template_view.rb +22 -24
  53. data/lib/charming/presentation/templates/erb_handler.rb +4 -6
  54. data/lib/charming/presentation/templates.rb +47 -49
  55. data/lib/charming/presentation/ui/ansi_codes.rb +66 -68
  56. data/lib/charming/presentation/ui/ansi_slicer.rb +67 -69
  57. data/lib/charming/presentation/ui/border.rb +24 -26
  58. data/lib/charming/presentation/ui/border_painter.rb +37 -39
  59. data/lib/charming/presentation/ui/canvas.rb +59 -61
  60. data/lib/charming/presentation/ui/style.rb +173 -175
  61. data/lib/charming/presentation/ui/theme.rb +133 -135
  62. data/lib/charming/presentation/ui/width.rb +12 -14
  63. data/lib/charming/presentation/ui.rb +69 -71
  64. data/lib/charming/presentation/view.rb +103 -105
  65. data/lib/charming/runtime.rb +23 -10
  66. data/lib/charming/version.rb +1 -1
  67. data/lib/charming.rb +3 -2
  68. metadata +2 -1
@@ -1,118 +1,116 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Charming
4
- module Presentation
5
- module Components
6
- # CommandPalette renders a fuzzy-searchable command picker UI. It wraps a TextInput for search
7
- # input and a List for result display, dispatching key events between them. Users type to filter
8
- # the registered commands by label match, navigate with up/down/home/end keys (delegated to List),
9
- # confirm a selection with Enter (returns [:selected, command]), or cancel with Escape (returns :cancelled).
10
- # State is serializable as a hash of value/cursor/selected_index for session persistence.
11
- class CommandPalette < Component
12
- Command = Data.define(:label, :value)
13
-
14
- # A single command palette entry: a human-readable +label+ and a callable or
15
- # method symbol +value+ that gets executed when the user selects it.
16
- attr_reader :commands, :input
17
-
18
- # Initializes the dropdown widget with a list of Command entries and search
19
- # parameters for building the underlying TextInput (placeholder text, cursor
20
- # position, value) and List (display height, initial selection). Returns void;
21
- # the state is later serializable via +state+ for session persistence.
22
- def initialize(commands:, placeholder: "Search commands", height: nil, value: "", cursor: nil, selected_index: 0, theme: nil)
23
- super(theme: theme)
24
- @commands = commands
25
- @height = height
26
- @input = TextInput.new(value: value, placeholder: placeholder, cursor: cursor)
27
- @list = build_list(selected_index: selected_index)
28
- end
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
29
28
 
30
- # Returns the currently displayed Command entry in the List at the time of calling.
31
- # Returns nil if no entry is highlighted (i.e., user has opened the palette but not
32
- # moved the selection). Useful for retrieving the result after key handling.
33
- def selected_command
34
- list.selected_item
35
- end
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
36
35
 
37
- # Collects the current state of the TextInput and List into a serializable hash
38
- # suitable for round-trip storage in session. Returns {value:, cursor:, selected_index:}.
39
- def state
40
- {
41
- value: input.value,
42
- cursor: input.cursor,
43
- selected_index: list.selected_index
44
- }
45
- end
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
46
45
 
47
- # Handles key events by routing them to the appropriate sub-component: Escape kills the
48
- # palette returning :cancelled; up/down/home/end keys go to the List selection handler
49
- # via handle_list_key; all other keys (including typed characters) are passed to the TextInput
50
- # which manages cursor position and input filtering. If a list key match fails, falls through
51
- # to the TextInput handler. Returns nil/nil if no handler consumed the event, or :cancelled when
52
- # Escape is pressed.
53
- def handle_key(event)
54
- key = Charming.key_of(event)
55
- return :cancelled if key == :escape
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
56
55
 
57
- return handle_list_key(event) if list_key?(key)
56
+ return handle_list_key(event) if list_key?(key)
58
57
 
59
- handle_input_key(event)
60
- end
58
+ handle_input_key(event)
59
+ end
61
60
 
62
- # Renders the command palette as a vertically-stacked text representation: the search TextInput
63
- # row on line 1, and then the filtered List results (or "No commands found") on subsequent lines.
64
- # Returns a multiline string suitable for terminal rendering.
65
- def render
66
- [input.render, render_results].join("\n")
67
- end
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
68
67
 
69
- private
68
+ private
70
69
 
71
- attr_reader :height, :list
70
+ attr_reader :height, :list
72
71
 
73
- # Delegates key handling entirely to the internal List widget, which manages up/down/home/end selection.
74
- # Returns whatever the List's handle_key returns (typically nil or the symbol from the subclass).
75
- def handle_list_key(event)
76
- list.handle_key(event)
77
- end
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
78
77
 
79
- # Passes the key event to the TextInput for cursor position and search text management.
80
- # If the input returns :handled, rebuilds the List so that filtering is re-evaluated against
81
- # the new input value. Returns nil/nil if no handler consumed the event.
82
- def handle_input_key(event)
83
- result = input.handle_key(event)
84
- @list = build_list if result == :handled
85
- result
86
- end
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
87
86
 
88
- # Checks whether the given key is a List-navigation key (up/down/home/end). Returns true for those keys
89
- # so they can be dispatched via +handle_list_key+ rather than falling through to TextInput.
90
- def list_key?(key)
91
- %i[up down home end enter].include?(key)
92
- end
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
93
92
 
94
- # Renders the filtered results section below the search input. If no commands match the current filter text,
95
- # returns "No commands found"; otherwise renders the List widget's styled display string. Returns a single-line string.
96
- def render_results
97
- return "No commands found" if filtered_commands.empty?
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?
98
97
 
99
- list.render
100
- end
98
+ list.render
99
+ end
101
100
 
102
- # Builds a new List from the currently filtered commands at the given selected_index height and label extractor.
103
- # The +selected_index+ parameter defaults to the last known value in +list+ to preserve scroll position across rebuilds.
104
- def build_list(selected_index: list&.selected_index || 0)
105
- List.new(items: filtered_commands, selected_index: selected_index, height: height, label: :label.to_proc, theme: theme)
106
- end
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
107
106
 
108
- # Returns the full commands array when input value is empty; otherwise a subset whose labels match case-insensitively
109
- # against the current TextInput value. Used to drive the fuzzy search behavior. Returns an Array of Command entries.
110
- def filtered_commands
111
- return commands if input.value.empty?
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?
112
111
 
113
- commands.select do |command|
114
- command.label.downcase.include?(input.value.downcase)
115
- end
112
+ commands.select do |command|
113
+ command.label.downcase.include?(input.value.downcase)
116
114
  end
117
115
  end
118
116
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Charming
4
+ module Components
5
+ # CommandPaletteModal wraps command palette content in the framework's standard modal chrome.
6
+ class CommandPaletteModal < Component
7
+ DEFAULT_TITLE = "Command palette"
8
+ DEFAULT_HELP = "Type to filter. Enter selects. Escape closes."
9
+ DEFAULT_WIDTH = 52
10
+
11
+ def initialize(content:, title: DEFAULT_TITLE, help: DEFAULT_HELP, width: DEFAULT_WIDTH, style: nil, theme: nil)
12
+ super(theme: theme)
13
+ @content = content
14
+ @title = title
15
+ @help = help
16
+ @width = width
17
+ @style = style
18
+ end
19
+
20
+ def render
21
+ render_component Modal.new(content: content, title: title, help: help, width: width, style: modal_style, theme: theme)
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :content, :title, :help, :width
27
+
28
+ def modal_style
29
+ @style
30
+ end
31
+ end
32
+ end
33
+ end
@@ -1,55 +1,53 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Charming
4
- module Presentation
5
- module Components
6
- # EmptyState is a placeholder component for screens with no content. Renders one of three
7
- # states: a default "nothing to show" message, a "loading…" message, or an error message
8
- # with optional help text.
9
- class EmptyState < Component
10
- # *message* is shown in the default state. *loading* switches to the loading message
11
- # (overrides *message*). *loading_message* is the string rendered in the loading state.
12
- # *error* and *error_message* switch to the error state (the string form takes precedence).
13
- # *help* is an optional muted line shown below the error message.
14
- def initialize(message: "Nothing to show.", loading: false, loading_message: "Loading...", error: nil, error_message: nil, help: nil, theme: nil)
15
- super(theme: theme)
16
- @message = message
17
- @loading = loading
18
- @loading_message = loading_message
19
- @error = error
20
- @error_message = error_message
21
- @help = help
22
- end
23
-
24
- # Renders the appropriate state as styled text: loading → loading message, error →
25
- # error message + help, otherwise the default message.
26
- def render
27
- return loading_state if @loading
28
- return error_state if error?
29
-
30
- text @message, style: theme.muted
31
- end
32
-
33
- private
34
-
35
- # Renders the loading state as a muted line.
36
- def loading_state
37
- text @loading_message, style: theme.muted
38
- end
39
-
40
- # Renders the error state: the error message styled with the theme's warn style,
41
- # optionally followed by a muted help line.
42
- def error_state
43
- lines = [text(@error_message || @error.to_s, style: theme.warn)]
44
- lines << text(@help, style: theme.muted) if @help.to_s.strip != ""
45
-
46
- column(*lines)
47
- end
48
-
49
- # True when either the *error* or *error_message* string is non-blank.
50
- def error?
51
- @error.to_s.strip != "" || @error_message.to_s.strip != ""
52
- end
4
+ module Components
5
+ # EmptyState is a placeholder component for screens with no content. Renders one of three
6
+ # states: a default "nothing to show" message, a "loading…" message, or an error message
7
+ # with optional help text.
8
+ class EmptyState < Component
9
+ # *message* is shown in the default state. *loading* switches to the loading message
10
+ # (overrides *message*). *loading_message* is the string rendered in the loading state.
11
+ # *error* and *error_message* switch to the error state (the string form takes precedence).
12
+ # *help* is an optional muted line shown below the error message.
13
+ def initialize(message: "Nothing to show.", loading: false, loading_message: "Loading...", error: nil, error_message: nil, help: nil, theme: nil)
14
+ super(theme: theme)
15
+ @message = message
16
+ @loading = loading
17
+ @loading_message = loading_message
18
+ @error = error
19
+ @error_message = error_message
20
+ @help = help
21
+ end
22
+
23
+ # Renders the appropriate state as styled text: loading → loading message, error →
24
+ # error message + help, otherwise the default message.
25
+ def render
26
+ return loading_state if @loading
27
+ return error_state if error?
28
+
29
+ text @message, style: theme.muted
30
+ end
31
+
32
+ private
33
+
34
+ # Renders the loading state as a muted line.
35
+ def loading_state
36
+ text @loading_message, style: theme.muted
37
+ end
38
+
39
+ # Renders the error state: the error message styled with the theme's warn style,
40
+ # optionally followed by a muted help line.
41
+ def error_state
42
+ lines = [text(@error_message || @error.to_s, style: theme.warn)]
43
+ lines << text(@help, style: theme.muted) if @help.to_s.strip != ""
44
+
45
+ column(*lines)
46
+ end
47
+
48
+ # True when either the *error* or *error_message* string is non-blank.
49
+ def error?
50
+ @error.to_s.strip != "" || @error_message.to_s.strip != ""
53
51
  end
54
52
  end
55
53
  end
@@ -1,60 +1,58 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Charming
4
- module Presentation
5
- module Components
6
- class Form
7
- # Builder collects form field declarations inside a `form(:name) { ... }` block and
8
- # assembles them into a Form component when `build` is called. Each declaration method
9
- # appends a Field subclass instance to the builder's *fields* list.
10
- class Builder
11
- # The accumulated field list and the theme applied to each declared field.
12
- attr_reader :fields, :theme
13
-
14
- # Initializes an empty builder. *theme* is forwarded to every declared field unless
15
- # the field declaration explicitly overrides it.
16
- def initialize(theme: nil)
17
- @theme = theme
18
- @fields = []
19
- end
20
-
21
- # Appends a single-line Input field. *options* are passed through to Input.
22
- def input(name, **options)
23
- fields << Input.new(name, **field_options(options))
24
- end
25
-
26
- # Appends a multi-line Textarea field.
27
- def textarea(name, **options)
28
- fields << Textarea.new(name, **field_options(options))
29
- end
30
-
31
- # Appends a Select field with the given *options* array.
32
- def select(name, **options)
33
- fields << Select.new(name, **field_options(options))
34
- end
35
-
36
- # Appends a Confirm (boolean) field.
37
- def confirm(name, **options)
38
- fields << Confirm.new(name, **field_options(options))
39
- end
40
-
41
- # Appends a static Note (non-focusable).
42
- def note(text, **options)
43
- fields << Note.new(text, **field_options(options))
44
- end
45
-
46
- # Assembles the collected fields into a Form component, bound to *state* and using
47
- # the *theme* argument (falling back to the builder's theme).
48
- def build(state:, theme: nil)
49
- Components::Form.new(fields: fields, state: state, theme: theme || self.theme)
50
- end
51
-
52
- private
53
-
54
- # Merges the builder's theme into the per-field *options* so each field receives it.
55
- def field_options(options)
56
- {theme: theme}.merge(options)
57
- end
4
+ module Components
5
+ class Form
6
+ # Builder collects form field declarations inside a `form(:name) { ... }` block and
7
+ # assembles them into a Form component when `build` is called. Each declaration method
8
+ # appends a Field subclass instance to the builder's *fields* list.
9
+ class Builder
10
+ # The accumulated field list and the theme applied to each declared field.
11
+ attr_reader :fields, :theme
12
+
13
+ # Initializes an empty builder. *theme* is forwarded to every declared field unless
14
+ # the field declaration explicitly overrides it.
15
+ def initialize(theme: nil)
16
+ @theme = theme
17
+ @fields = []
18
+ end
19
+
20
+ # Appends a single-line Input field. *options* are passed through to Input.
21
+ def input(name, **options)
22
+ fields << Input.new(name, **field_options(options))
23
+ end
24
+
25
+ # Appends a multi-line Textarea field.
26
+ def textarea(name, **options)
27
+ fields << Textarea.new(name, **field_options(options))
28
+ end
29
+
30
+ # Appends a Select field with the given *options* array.
31
+ def select(name, **options)
32
+ fields << Select.new(name, **field_options(options))
33
+ end
34
+
35
+ # Appends a Confirm (boolean) field.
36
+ def confirm(name, **options)
37
+ fields << Confirm.new(name, **field_options(options))
38
+ end
39
+
40
+ # Appends a static Note (non-focusable).
41
+ def note(text, **options)
42
+ fields << Note.new(text, **field_options(options))
43
+ end
44
+
45
+ # Assembles the collected fields into a Form component, bound to *state* and using
46
+ # the *theme* argument (falling back to the builder's theme).
47
+ def build(state:, theme: nil)
48
+ Components::Form.new(fields: fields, state: state, theme: theme || self.theme)
49
+ end
50
+
51
+ private
52
+
53
+ # Merges the builder's theme into the per-field *options* so each field receives it.
54
+ def field_options(options)
55
+ {theme: theme}.merge(options)
58
56
  end
59
57
  end
60
58
  end
@@ -1,67 +1,65 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Charming
4
- module Presentation
5
- module Components
6
- class Form
7
- # Confirm is a boolean Form field that renders a checkbox-style control. Space toggles
8
- # the value; y/Right sets it to true; n/Left sets it to false. Required confirms must
9
- # be accepted (value == true) to pass validation.
10
- class Confirm < Field
11
- # *value* is the initial boolean state (default: false). All other options are
12
- # forwarded to Field.
13
- def initialize(name, value: false, **options)
14
- super(name, **options)
15
- @initial_value = value
16
- end
4
+ module Components
5
+ class Form
6
+ # Confirm is a boolean Form field that renders a checkbox-style control. Space toggles
7
+ # the value; y/Right sets it to true; n/Left sets it to false. Required confirms must
8
+ # be accepted (value == true) to pass validation.
9
+ class Confirm < Field
10
+ # *value* is the initial boolean state (default: false). All other options are
11
+ # forwarded to Field.
12
+ def initialize(name, value: false, **options)
13
+ super(name, **options)
14
+ @initial_value = value
15
+ end
17
16
 
18
- # Handles the standard confirm keys: space toggles, y/right sets to true, n/left
19
- # sets to false, and a space character (when the event exposes `char`) also toggles.
20
- def handle_key(event)
21
- case Charming.key_of(event)
22
- when :space
23
- toggle
24
- when :y, :right
25
- state[:values][name] = true
26
- when :n, :left
27
- state[:values][name] = false
28
- else
29
- return nil unless event.respond_to?(:char) && event.char == " "
17
+ # Handles the standard confirm keys: space toggles, y/right sets to true, n/left
18
+ # sets to false, and a space character (when the event exposes `char`) also toggles.
19
+ def handle_key(event)
20
+ case Charming.key_of(event)
21
+ when :space
22
+ toggle
23
+ when :y, :right
24
+ state[:values][name] = true
25
+ when :n, :left
26
+ state[:values][name] = false
27
+ else
28
+ return nil unless event.respond_to?(:char) && event.char == " "
30
29
 
31
- toggle
32
- end
33
- :handled
30
+ toggle
34
31
  end
32
+ :handled
33
+ end
35
34
 
36
- # Returns ["must be accepted"] when required and the value is not true, otherwise
37
- # the result of the base Field validation.
38
- def validate
39
- return ["must be accepted"] if required? && value != true
35
+ # Returns ["must be accepted"] when required and the value is not true, otherwise
36
+ # the result of the base Field validation.
37
+ def validate
38
+ return ["must be accepted"] if required? && value != true
40
39
 
41
- super
42
- end
40
+ super
41
+ end
43
42
 
44
- private
43
+ private
45
44
 
46
- # The default value for a freshly-bound field is the *value* passed at construction.
47
- def default_value
48
- @initial_value
49
- end
45
+ # The default value for a freshly-bound field is the *value* passed at construction.
46
+ def default_value
47
+ @initial_value
48
+ end
50
49
 
51
- # Renders "[x] Label" or "[ ] Label" depending on the current value.
52
- def render_control
53
- "#{checked_marker} #{label}"
54
- end
50
+ # Renders "[x] Label" or "[ ] Label" depending on the current value.
51
+ def render_control
52
+ "#{checked_marker} #{label}"
53
+ end
55
54
 
56
- # Returns the checkbox marker string.
57
- def checked_marker
58
- value ? "[x]" : "[ ]"
59
- end
55
+ # Returns the checkbox marker string.
56
+ def checked_marker
57
+ value ? "[x]" : "[ ]"
58
+ end
60
59
 
61
- # Flips the current value (true ↔ false).
62
- def toggle
63
- state[:values][name] = !value
64
- end
60
+ # Flips the current value (true ↔ false).
61
+ def toggle
62
+ state[:values][name] = !value
65
63
  end
66
64
  end
67
65
  end