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.
- checksums.yaml +4 -4
- data/lib/charming/application.rb +3 -3
- data/lib/charming/controller/class_methods.rb +2 -2
- data/lib/charming/controller/command_palette.rb +2 -2
- data/lib/charming/controller/rendering.rb +2 -2
- data/lib/charming/controller/session_state.rb +1 -1
- data/lib/charming/generators/component_generator.rb +1 -1
- data/lib/charming/generators/templates/app/application.template +1 -1
- data/lib/charming/generators/templates/app/layout.template +3 -6
- data/lib/charming/generators/templates/app/view.template +1 -1
- data/lib/charming/generators/templates/component/component.rb.template +1 -1
- data/lib/charming/generators/templates/screen/view.rb.template +1 -1
- data/lib/charming/generators/templates/view/view.rb.template +1 -1
- data/lib/charming/internal/renderer/differential.rb +13 -5
- data/lib/charming/internal/terminal/tty_backend.rb +22 -2
- data/lib/charming/presentation/component.rb +3 -5
- data/lib/charming/presentation/components/activity_indicator.rb +173 -134
- data/lib/charming/presentation/components/command_palette.rb +94 -96
- data/lib/charming/presentation/components/command_palette_modal.rb +33 -0
- data/lib/charming/presentation/components/empty_state.rb +47 -49
- data/lib/charming/presentation/components/form/builder.rb +52 -54
- data/lib/charming/presentation/components/form/confirm.rb +49 -51
- data/lib/charming/presentation/components/form/field.rb +94 -96
- data/lib/charming/presentation/components/form/input.rb +53 -55
- data/lib/charming/presentation/components/form/note.rb +27 -29
- data/lib/charming/presentation/components/form/select.rb +84 -86
- data/lib/charming/presentation/components/form/textarea.rb +67 -69
- data/lib/charming/presentation/components/form.rb +120 -122
- data/lib/charming/presentation/components/keyboard_handler.rb +41 -43
- data/lib/charming/presentation/components/list.rb +123 -125
- data/lib/charming/presentation/components/markdown.rb +21 -23
- data/lib/charming/presentation/components/modal.rb +46 -48
- data/lib/charming/presentation/components/progressbar.rb +51 -53
- data/lib/charming/presentation/components/spinner.rb +40 -42
- data/lib/charming/presentation/components/table.rb +109 -111
- data/lib/charming/presentation/components/text_area.rb +219 -221
- data/lib/charming/presentation/components/text_input.rb +120 -122
- data/lib/charming/presentation/components/viewport.rb +218 -220
- data/lib/charming/presentation/layout/builder.rb +64 -66
- data/lib/charming/presentation/layout/overlay.rb +48 -50
- data/lib/charming/presentation/layout/pane.rb +122 -118
- data/lib/charming/presentation/layout/rect.rb +14 -16
- data/lib/charming/presentation/layout/screen_layout.rb +40 -42
- data/lib/charming/presentation/layout/split.rb +101 -103
- data/lib/charming/presentation/layout.rb +28 -30
- data/lib/charming/presentation/markdown/block_renderers.rb +94 -96
- data/lib/charming/presentation/markdown/inline_renderers.rb +52 -54
- data/lib/charming/presentation/markdown/render_context.rb +12 -14
- data/lib/charming/presentation/markdown/renderer.rb +84 -86
- data/lib/charming/presentation/markdown/syntax_highlighter.rb +57 -59
- data/lib/charming/presentation/markdown.rb +4 -6
- data/lib/charming/presentation/template_view.rb +22 -24
- data/lib/charming/presentation/templates/erb_handler.rb +4 -6
- data/lib/charming/presentation/templates.rb +47 -49
- data/lib/charming/presentation/ui/ansi_codes.rb +66 -68
- data/lib/charming/presentation/ui/ansi_slicer.rb +67 -69
- data/lib/charming/presentation/ui/border.rb +24 -26
- data/lib/charming/presentation/ui/border_painter.rb +37 -39
- data/lib/charming/presentation/ui/canvas.rb +59 -61
- data/lib/charming/presentation/ui/style.rb +173 -175
- data/lib/charming/presentation/ui/theme.rb +133 -135
- data/lib/charming/presentation/ui/width.rb +12 -14
- data/lib/charming/presentation/ui.rb +69 -71
- data/lib/charming/presentation/view.rb +103 -105
- data/lib/charming/runtime.rb +23 -10
- data/lib/charming/version.rb +1 -1
- data/lib/charming.rb +3 -2
- metadata +2 -1
|
@@ -1,118 +1,116 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Charming
|
|
4
|
-
module
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
56
|
+
return handle_list_key(event) if list_key?(key)
|
|
58
57
|
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
handle_input_key(event)
|
|
59
|
+
end
|
|
61
60
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
68
|
+
private
|
|
70
69
|
|
|
71
|
-
|
|
70
|
+
attr_reader :height, :list
|
|
72
71
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
100
|
-
|
|
98
|
+
list.render
|
|
99
|
+
end
|
|
101
100
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
114
|
-
|
|
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
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
32
|
-
end
|
|
33
|
-
:handled
|
|
30
|
+
toggle
|
|
34
31
|
end
|
|
32
|
+
:handled
|
|
33
|
+
end
|
|
35
34
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
42
|
-
|
|
40
|
+
super
|
|
41
|
+
end
|
|
43
42
|
|
|
44
|
-
|
|
43
|
+
private
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
# Renders "[x] Label" or "[ ] Label" depending on the current value.
|
|
51
|
+
def render_control
|
|
52
|
+
"#{checked_marker} #{label}"
|
|
53
|
+
end
|
|
55
54
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
# Returns the checkbox marker string.
|
|
56
|
+
def checked_marker
|
|
57
|
+
value ? "[x]" : "[ ]"
|
|
58
|
+
end
|
|
60
59
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|