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,84 +1,82 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Charming
4
- module Presentation
5
- module Components
6
- class Form
7
- # Textarea is a multi-line Form field backed by a TextArea widget. The cursor offset,
8
- # top-visible row, and preferred vertical column are all persisted in the form's
9
- # per-field state so the field behaves consistently when refocused mid-edit.
10
- class Textarea < Field
11
- # *value* is the initial text. *placeholder* is shown when the value is empty.
12
- # *width* and *height* constrain the rendered area. All other options are forwarded
13
- # to Field (label, required, validate, help, theme).
14
- def initialize(name, value: "", placeholder: "", width: nil, height: nil, **options)
15
- super(name, **options)
16
- @initial_value = value
17
- @placeholder = placeholder
18
- @width = width
19
- @height = height
20
- end
4
+ module Components
5
+ class Form
6
+ # Textarea is a multi-line Form field backed by a TextArea widget. The cursor offset,
7
+ # top-visible row, and preferred vertical column are all persisted in the form's
8
+ # per-field state so the field behaves consistently when refocused mid-edit.
9
+ class Textarea < Field
10
+ # *value* is the initial text. *placeholder* is shown when the value is empty.
11
+ # *width* and *height* constrain the rendered area. All other options are forwarded
12
+ # to Field (label, required, validate, help, theme).
13
+ def initialize(name, value: "", placeholder: "", width: nil, height: nil, **options)
14
+ super(name, **options)
15
+ @initial_value = value
16
+ @placeholder = placeholder
17
+ @width = width
18
+ @height = height
19
+ end
21
20
 
22
- # Binds the field, seeds the initial value, and initializes the cursor/offset state.
23
- def bind(state)
24
- super
25
- state[:values][name] = @initial_value if state[:values][name].nil?
26
- field_state[:cursor] = state[:values][name].to_s.length unless field_state.key?(:cursor)
27
- field_state[:offset] ||= 0
28
- end
21
+ # Binds the field, seeds the initial value, and initializes the cursor/offset state.
22
+ def bind(state)
23
+ super
24
+ state[:values][name] = @initial_value if state[:values][name].nil?
25
+ field_state[:cursor] = state[:values][name].to_s.length unless field_state.key?(:cursor)
26
+ field_state[:offset] ||= 0
27
+ end
29
28
 
30
- # Forwards key events to the underlying TextArea, syncing the value, cursor, offset,
31
- # and preferred column back into the form state. Returns :handled when consumed.
32
- def handle_key(event)
33
- area = text_area
34
- result = area.handle_key(event)
35
- return nil unless result == :handled
29
+ # Forwards key events to the underlying TextArea, syncing the value, cursor, offset,
30
+ # and preferred column back into the form state. Returns :handled when consumed.
31
+ def handle_key(event)
32
+ area = text_area
33
+ result = area.handle_key(event)
34
+ return nil unless result == :handled
36
35
 
37
- state[:values][name] = area.value
38
- field_state[:cursor] = area.cursor
39
- field_state[:offset] = area.offset
40
- field_state[:preferred_column] = area.preferred_column
41
- :handled
42
- end
36
+ state[:values][name] = area.value
37
+ field_state[:cursor] = area.cursor
38
+ field_state[:offset] = area.offset
39
+ field_state[:preferred_column] = area.preferred_column
40
+ :handled
41
+ end
43
42
 
44
- # Renders the field with its label on the first line, body lines indented, and
45
- # optional help/error lines below.
46
- def render(active: false)
47
- label_line = "#{active ? ">" : " "} #{label}:"
48
- label_line = theme.selected.render(label_line) if active
49
- [label_line, *body_lines, help_line, *error_lines].compact.join("\n")
50
- end
43
+ # Renders the field with its label on the first line, body lines indented, and
44
+ # optional help/error lines below.
45
+ def render(active: false)
46
+ label_line = "#{active ? ">" : " "} #{label}:"
47
+ label_line = theme.selected.render(label_line) if active
48
+ [label_line, *body_lines, help_line, *error_lines].compact.join("\n")
49
+ end
51
50
 
52
- private
51
+ private
53
52
 
54
- # The default value for a freshly-bound field is the *value* passed at construction.
55
- def default_value
56
- @initial_value
57
- end
53
+ # The default value for a freshly-bound field is the *value* passed at construction.
54
+ def default_value
55
+ @initial_value
56
+ end
58
57
 
59
- # Renders the multi-line body, indenting each line by two spaces.
60
- def body_lines
61
- text_area.render.lines(chomp: true).map { |line| " #{line}" }
62
- end
58
+ # Renders the multi-line body, indenting each line by two spaces.
59
+ def body_lines
60
+ text_area.render.lines(chomp: true).map { |line| " #{line}" }
61
+ end
63
62
 
64
- # Builds a fresh TextArea each render, seeded from the current form-state value and
65
- # the persisted cursor/offset/preferred_column.
66
- def text_area
67
- TextArea.new(
68
- value: value.to_s,
69
- placeholder: @placeholder,
70
- width: @width,
71
- height: @height,
72
- cursor: field_state[:cursor],
73
- offset: field_state[:offset],
74
- preferred_column: field_state[:preferred_column]
75
- )
76
- end
63
+ # Builds a fresh TextArea each render, seeded from the current form-state value and
64
+ # the persisted cursor/offset/preferred_column.
65
+ def text_area
66
+ TextArea.new(
67
+ value: value.to_s,
68
+ placeholder: @placeholder,
69
+ width: @width,
70
+ height: @height,
71
+ cursor: field_state[:cursor],
72
+ offset: field_state[:offset],
73
+ preferred_column: field_state[:preferred_column]
74
+ )
75
+ end
77
76
 
78
- # Returns the per-field state hash for this field.
79
- def field_state
80
- state[:fields][name]
81
- end
77
+ # Returns the per-field state hash for this field.
78
+ def field_state
79
+ state[:fields][name]
82
80
  end
83
81
  end
84
82
  end
@@ -1,155 +1,153 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Charming
4
- module Presentation
5
- module Components
6
- # Form is a multi-field form component with built-in focus traversal, validation, and
7
- # submit/cancel handling. Fields are produced by `Form::Builder` (see `controller.form`)
8
- # and bound to a per-form mutable state hash. Tab/Shift+Tab cycles focus through
9
- # focusable fields, Enter advances to the next field (or submits on the last), Escape
10
- # cancels, and Ctrl+S submits from any field.
11
- class Form < Component
12
- # The list of field objects and the mutable state hash the form is bound to.
13
- attr_reader :fields, :state
14
-
15
- # *fields* is the array of form field objects. *state* is a hash for storing field
16
- # values/errors and the current focus index; usually `session[:forms][form_name]`.
17
- def initialize(fields:, state: nil, theme: nil)
18
- super(theme: theme)
19
- @fields = fields
20
- @state = normalize_state(state || {})
21
- bind_fields
22
- clamp_focus
23
- end
4
+ module Components
5
+ # Form is a multi-field form component with built-in focus traversal, validation, and
6
+ # submit/cancel handling. Fields are produced by `Form::Builder` (see `controller.form`)
7
+ # and bound to a per-form mutable state hash. Tab/Shift+Tab cycles focus through
8
+ # focusable fields, Enter advances to the next field (or submits on the last), Escape
9
+ # cancels, and Ctrl+S submits from any field.
10
+ class Form < Component
11
+ # The list of field objects and the mutable state hash the form is bound to.
12
+ attr_reader :fields, :state
13
+
14
+ # *fields* is the array of form field objects. *state* is a hash for storing field
15
+ # values/errors and the current focus index; usually `session[:forms][form_name]`.
16
+ def initialize(fields:, state: nil, theme: nil)
17
+ super(theme: theme)
18
+ @fields = fields
19
+ @state = normalize_state(state || {})
20
+ bind_fields
21
+ clamp_focus
22
+ end
24
23
 
25
- # Handles key events: Escape cancels, Ctrl+S submits, Tab cycles focus, Enter advances
26
- # or submits, and unhandled keys are passed to the focused field.
27
- def handle_key(event)
28
- key = Charming.key_of(event)
29
- return :cancelled if key == :escape
30
- return submit if submit_shortcut?(event)
31
- return move_focus(tab_direction(event)) if key == :tab
24
+ # Handles key events: Escape cancels, Ctrl+S submits, Tab cycles focus, Enter advances
25
+ # or submits, and unhandled keys are passed to the focused field.
26
+ def handle_key(event)
27
+ key = Charming.key_of(event)
28
+ return :cancelled if key == :escape
29
+ return submit if submit_shortcut?(event)
30
+ return move_focus(tab_direction(event)) if key == :tab
32
31
 
33
- result = handle_current_field(event)
34
- return result if result
32
+ result = handle_current_field(event)
33
+ return result if result
35
34
 
36
- advance_or_submit if key == :enter
37
- end
35
+ advance_or_submit if key == :enter
36
+ end
38
37
 
39
- # Returns a hash of `{field_name => value}` for the current field values.
40
- def values
41
- state[:values]
42
- end
38
+ # Returns a hash of `{field_name => value}` for the current field values.
39
+ def values
40
+ state[:values]
41
+ end
43
42
 
44
- # Renders each field on its own line, marking the active field with `active: true`.
45
- def render
46
- fields.each_with_index.map do |field, index|
47
- field.render(active: index == state[:focus_index])
48
- end.join("\n")
49
- end
43
+ # Renders each field on its own line, marking the active field with `active: true`.
44
+ def render
45
+ fields.each_with_index.map do |field, index|
46
+ field.render(active: index == state[:focus_index])
47
+ end.join("\n")
48
+ end
50
49
 
51
- private
50
+ private
52
51
 
53
- # Ensures the state hash has all the required sub-keys: :values, :fields, :errors, and
54
- # a sensible :focus_index default.
55
- def normalize_state(value)
56
- value[:values] ||= {}
57
- value[:fields] ||= {}
58
- value[:errors] ||= {}
59
- value[:focus_index] ||= first_focusable_index || 0
60
- value
61
- end
52
+ # Ensures the state hash has all the required sub-keys: :values, :fields, :errors, and
53
+ # a sensible :focus_index default.
54
+ def normalize_state(value)
55
+ value[:values] ||= {}
56
+ value[:fields] ||= {}
57
+ value[:errors] ||= {}
58
+ value[:focus_index] ||= first_focusable_index || 0
59
+ value
60
+ end
62
61
 
63
- # Binds each field to the state hash so field updates write back into `state[:values]`.
64
- def bind_fields
65
- fields.each { |field| field.bind(state) }
66
- end
62
+ # Binds each field to the state hash so field updates write back into `state[:values]`.
63
+ def bind_fields
64
+ fields.each { |field| field.bind(state) }
65
+ end
67
66
 
68
- # Forwards *event* to the currently focused field and returns its result.
69
- def handle_current_field(event)
70
- current_field&.handle_key(event)
71
- end
67
+ # Forwards *event* to the currently focused field and returns its result.
68
+ def handle_current_field(event)
69
+ current_field&.handle_key(event)
70
+ end
72
71
 
73
- # Returns -1 for Shift+Tab (backward), +1 for plain Tab (forward).
74
- def tab_direction(event)
75
- return -1 if event.respond_to?(:shift) && event.shift
72
+ # Returns -1 for Shift+Tab (backward), +1 for plain Tab (forward).
73
+ def tab_direction(event)
74
+ return -1 if event.respond_to?(:shift) && event.shift
76
75
 
77
- +1
78
- end
76
+ +1
77
+ end
79
78
 
80
- # True when the event is the submit shortcut (Ctrl+S).
81
- def submit_shortcut?(event)
82
- Charming.key_of(event) == :s && event.respond_to?(:ctrl) && event.ctrl
83
- end
79
+ # True when the event is the submit shortcut (Ctrl+S).
80
+ def submit_shortcut?(event)
81
+ Charming.key_of(event) == :s && event.respond_to?(:ctrl) && event.ctrl
82
+ end
84
83
 
85
- # On Enter: submit when the last focusable field is active, otherwise advance focus.
86
- def advance_or_submit
87
- return submit if last_focusable?
84
+ # On Enter: submit when the last focusable field is active, otherwise advance focus.
85
+ def advance_or_submit
86
+ return submit if last_focusable?
88
87
 
89
- move_focus(+1)
90
- end
88
+ move_focus(+1)
89
+ end
91
90
 
92
- # Validates all fields, focuses the first invalid one, and returns [:submitted, values]
93
- # when there are no errors.
94
- def submit
95
- state[:errors] = validation_errors
96
- focus_first_error unless state[:errors].empty?
97
- return :handled unless state[:errors].empty?
91
+ # Validates all fields, focuses the first invalid one, and returns [:submitted, values]
92
+ # when there are no errors.
93
+ def submit
94
+ state[:errors] = validation_errors
95
+ focus_first_error unless state[:errors].empty?
96
+ return :handled unless state[:errors].empty?
98
97
 
99
- [:submitted, values.dup]
100
- end
98
+ [:submitted, values.dup]
99
+ end
101
100
 
102
- # Runs each field's validator and collects per-field error messages.
103
- def validation_errors
104
- fields.each_with_object({}) do |field, errors|
105
- messages = field.validate
106
- errors[field.name] = messages unless messages.empty?
107
- end
101
+ # Runs each field's validator and collects per-field error messages.
102
+ def validation_errors
103
+ fields.each_with_object({}) do |field, errors|
104
+ messages = field.validate
105
+ errors[field.name] = messages unless messages.empty?
108
106
  end
107
+ end
109
108
 
110
- # Moves focus to the first focusable field with errors, when any.
111
- def focus_first_error
112
- invalid = fields.index { |field| field.focusable? && state[:errors].key?(field.name) }
113
- state[:focus_index] = invalid if invalid
114
- end
109
+ # Moves focus to the first focusable field with errors, when any.
110
+ def focus_first_error
111
+ invalid = fields.index { |field| field.focusable? && state[:errors].key?(field.name) }
112
+ state[:focus_index] = invalid if invalid
113
+ end
115
114
 
116
- # Returns the field at the current focus index, or nil when out of range.
117
- def current_field
118
- fields[state[:focus_index]]
119
- end
115
+ # Returns the field at the current focus index, or nil when out of range.
116
+ def current_field
117
+ fields[state[:focus_index]]
118
+ end
120
119
 
121
- # Moves focus by *direction* (forward or backward) through the focusable fields.
122
- def move_focus(direction)
123
- indices = focusable_indices
124
- return nil if indices.empty?
120
+ # Moves focus by *direction* (forward or backward) through the focusable fields.
121
+ def move_focus(direction)
122
+ indices = focusable_indices
123
+ return nil if indices.empty?
125
124
 
126
- current = indices.index(state[:focus_index]) || 0
127
- state[:focus_index] = indices[(current + direction) % indices.length]
128
- :handled
129
- end
125
+ current = indices.index(state[:focus_index]) || 0
126
+ state[:focus_index] = indices[(current + direction) % indices.length]
127
+ :handled
128
+ end
130
129
 
131
- # True when the current focus index is the last focusable field.
132
- def last_focusable?
133
- focusable_indices.last == state[:focus_index]
134
- end
130
+ # True when the current focus index is the last focusable field.
131
+ def last_focusable?
132
+ focusable_indices.last == state[:focus_index]
133
+ end
135
134
 
136
- # Indices of focusable fields, memoized.
137
- def focusable_indices
138
- @focusable_indices ||= fields.each_index.select { |index| fields[index].focusable? }
139
- end
135
+ # Indices of focusable fields, memoized.
136
+ def focusable_indices
137
+ @focusable_indices ||= fields.each_index.select { |index| fields[index].focusable? }
138
+ end
140
139
 
141
- # The first index of a focusable field, or nil when no fields are focusable.
142
- def first_focusable_index
143
- fields.each_index.find { |index| fields[index].focusable? }
144
- end
140
+ # The first index of a focusable field, or nil when no fields are focusable.
141
+ def first_focusable_index
142
+ fields.each_index.find { |index| fields[index].focusable? }
143
+ end
145
144
 
146
- # On initialization, ensures :focus_index points at a focusable field.
147
- def clamp_focus
148
- return if focusable_indices.empty?
149
- return if focusable_indices.include?(state[:focus_index])
145
+ # On initialization, ensures :focus_index points at a focusable field.
146
+ def clamp_focus
147
+ return if focusable_indices.empty?
148
+ return if focusable_indices.include?(state[:focus_index])
150
149
 
151
- state[:focus_index] = focusable_indices.first
152
- end
150
+ state[:focus_index] = focusable_indices.first
153
151
  end
154
152
  end
155
153
  end
@@ -1,56 +1,54 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Charming
4
- module Presentation
5
- module Components
6
- # KeyboardHandler is a mixin module that provides keyboard event dispatch by mapping symbolic key names
7
- # to private method calls. Implementors must define a constant +KEY_ACTIONS+ as a hash where each key is
8
- # a symbol (e.g., :up, :down, :enter) and each value is the target method name (e.g., :move_up). Call
9
- # +handle_key(event)+ with any event object; it uses Charming.key_of to resolve the raw event to a symbol,
10
- # looks up the corresponding action in KEY_ACTIONS, sends that method on self, and returns :handled if an
11
- # action was found. Returns nil (via :handled being truthy or not) when no matching key exists.
12
- module KeyboardHandler
13
- VIM_KEYMAP = {
14
- up: :k,
15
- down: :j,
16
- left: :h,
17
- right: :l
18
- }.freeze
19
-
20
- def handle_key(event)
21
- key = Charming.key_of(event)
22
- action = key_actions[key]
23
- return unless action
24
-
25
- send(action)
26
- :handled
27
- end
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
+ VIM_KEYMAP = {
13
+ up: :k,
14
+ down: :j,
15
+ left: :h,
16
+ right: :l
17
+ }.freeze
18
+
19
+ def handle_key(event)
20
+ key = Charming.key_of(event)
21
+ action = key_actions[key]
22
+ return unless action
23
+
24
+ send(action)
25
+ :handled
26
+ end
28
27
 
29
- private
28
+ private
30
29
 
31
- def key_actions
32
- base_key_actions.merge(normalized_keymap)
33
- end
30
+ def key_actions
31
+ base_key_actions.merge(normalized_keymap)
32
+ end
34
33
 
35
- def base_key_actions
36
- self.class.const_get(:KEY_ACTIONS)
37
- end
34
+ def base_key_actions
35
+ self.class.const_get(:KEY_ACTIONS)
36
+ end
38
37
 
39
- def normalized_keymap
40
- resolved_keymap.each_with_object({}) do |(action_key, keys), actions|
41
- action = base_key_actions[action_key.to_sym]
42
- next unless action
38
+ def normalized_keymap
39
+ resolved_keymap.each_with_object({}) do |(action_key, keys), actions|
40
+ action = base_key_actions[action_key.to_sym]
41
+ next unless action
43
42
 
44
- Array(keys).each { |key| actions[key.to_sym] = action }
45
- end
43
+ Array(keys).each { |key| actions[key.to_sym] = action }
46
44
  end
45
+ end
47
46
 
48
- def resolved_keymap
49
- case @keymap
50
- when :vim then VIM_KEYMAP
51
- when nil then {}
52
- else @keymap
53
- end
47
+ def resolved_keymap
48
+ case @keymap
49
+ when :vim then VIM_KEYMAP
50
+ when nil then {}
51
+ else @keymap
54
52
  end
55
53
  end
56
54
  end