charming 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/lib/charming/application.rb +14 -3
  4. data/lib/charming/cli.rb +23 -0
  5. data/lib/charming/controller/class_methods.rb +115 -0
  6. data/lib/charming/controller/command_palette.rb +135 -0
  7. data/lib/charming/controller/component_dispatching.rb +81 -0
  8. data/lib/charming/controller/dispatching.rb +60 -0
  9. data/lib/charming/controller/focus_management.rb +30 -0
  10. data/lib/charming/controller/rendering.rb +127 -0
  11. data/lib/charming/controller/session_state.rb +41 -0
  12. data/lib/charming/controller/sidebar_navigation.rb +111 -0
  13. data/lib/charming/controller.rb +35 -559
  14. data/lib/charming/database_commands.rb +16 -0
  15. data/lib/charming/database_installer.rb +27 -0
  16. data/lib/charming/focus.rb +58 -2
  17. data/lib/charming/generators/app_file_generator.rb +13 -0
  18. data/lib/charming/generators/app_generator.rb +123 -47
  19. data/lib/charming/generators/base.rb +26 -0
  20. data/lib/charming/generators/component_generator.rb +10 -10
  21. data/lib/charming/generators/controller_generator.rb +22 -11
  22. data/lib/charming/generators/model_generator.rb +38 -29
  23. data/lib/charming/generators/name.rb +10 -0
  24. data/lib/charming/generators/screen_generator.rb +78 -32
  25. data/lib/charming/generators/templates/app/Gemfile.template +5 -0
  26. data/lib/charming/generators/templates/app/README.md.template +9 -0
  27. data/lib/charming/generators/templates/app/Rakefile.template +3 -0
  28. data/lib/charming/generators/templates/app/application.template +13 -0
  29. data/lib/charming/generators/templates/app/application_controller.template +19 -0
  30. data/lib/charming/generators/templates/app/application_record.template +7 -0
  31. data/lib/charming/generators/templates/app/application_state.template +6 -0
  32. data/lib/charming/generators/templates/app/database_config.template +12 -0
  33. data/lib/charming/generators/templates/app/executable.template +7 -0
  34. data/lib/charming/generators/templates/app/gemspec.template +6 -0
  35. data/lib/charming/generators/templates/app/home_controller.template +6 -0
  36. data/lib/charming/generators/templates/app/home_state.template +7 -0
  37. data/lib/charming/generators/templates/app/keep.template +0 -0
  38. data/lib/charming/generators/templates/app/layout.template +110 -0
  39. data/lib/charming/generators/templates/app/root_file.template +20 -0
  40. data/lib/charming/generators/templates/app/routes.template +5 -0
  41. data/lib/charming/generators/templates/app/seeds.template +1 -0
  42. data/lib/charming/generators/templates/app/spec_controller.template +17 -0
  43. data/lib/charming/generators/templates/app/spec_helper.template +3 -0
  44. data/lib/charming/generators/templates/app/spec_state.template +17 -0
  45. data/lib/charming/generators/templates/app/spec_view.template +16 -0
  46. data/lib/charming/generators/templates/app/version.template +5 -0
  47. data/lib/charming/generators/templates/app/view.template +21 -0
  48. data/lib/charming/generators/templates/component/component.rb.template +9 -0
  49. data/lib/charming/generators/templates/controller/controller.rb.template +6 -0
  50. data/lib/charming/generators/templates/model/migration.rb.template +9 -0
  51. data/lib/charming/generators/templates/model/model.rb.template +6 -0
  52. data/lib/charming/generators/templates/model/spec.rb.template +9 -0
  53. data/lib/charming/generators/templates/screen/controller.rb.template +7 -0
  54. data/lib/charming/generators/templates/screen/spec_controller.rb.template +17 -0
  55. data/lib/charming/generators/templates/screen/spec_state.rb.template +17 -0
  56. data/lib/charming/generators/templates/screen/spec_view.rb.template +13 -0
  57. data/lib/charming/generators/templates/screen/state.rb.template +7 -0
  58. data/lib/charming/generators/templates/screen/view.rb.template +11 -0
  59. data/lib/charming/generators/templates/view/view.rb.template +11 -0
  60. data/lib/charming/generators/view_generator.rb +19 -3
  61. data/lib/charming/internal/renderer/differential.rb +25 -2
  62. data/lib/charming/internal/renderer/full_repaint.rb +6 -0
  63. data/lib/charming/internal/terminal/adapter.rb +29 -3
  64. data/lib/charming/internal/terminal/key_normalizer.rb +84 -0
  65. data/lib/charming/internal/terminal/memory_backend.rb +28 -1
  66. data/lib/charming/internal/terminal/mouse_parser.rb +81 -0
  67. data/lib/charming/internal/terminal/tty_backend.rb +65 -115
  68. data/lib/charming/presentation/component.rb +3 -5
  69. data/lib/charming/presentation/components/activity_indicator.rb +173 -134
  70. data/lib/charming/presentation/components/command_palette.rb +94 -96
  71. data/lib/charming/presentation/components/command_palette_modal.rb +33 -0
  72. data/lib/charming/presentation/components/empty_state.rb +47 -36
  73. data/lib/charming/presentation/components/form/builder.rb +52 -40
  74. data/lib/charming/presentation/components/form/confirm.rb +50 -39
  75. data/lib/charming/presentation/components/form/field.rb +94 -71
  76. data/lib/charming/presentation/components/form/input.rb +61 -49
  77. data/lib/charming/presentation/components/form/note.rb +27 -20
  78. data/lib/charming/presentation/components/form/select.rb +84 -63
  79. data/lib/charming/presentation/components/form/textarea.rb +67 -53
  80. data/lib/charming/presentation/components/form.rb +120 -93
  81. data/lib/charming/presentation/components/keyboard_handler.rb +41 -43
  82. data/lib/charming/presentation/components/list.rb +123 -97
  83. data/lib/charming/presentation/components/markdown.rb +21 -17
  84. data/lib/charming/presentation/components/modal.rb +55 -43
  85. data/lib/charming/presentation/components/progressbar.rb +61 -50
  86. data/lib/charming/presentation/components/spinner.rb +35 -27
  87. data/lib/charming/presentation/components/table.rb +108 -85
  88. data/lib/charming/presentation/components/text_area.rb +219 -173
  89. data/lib/charming/presentation/components/text_input.rb +120 -98
  90. data/lib/charming/presentation/components/viewport.rb +218 -168
  91. data/lib/charming/presentation/layout/builder.rb +84 -0
  92. data/lib/charming/presentation/layout/overlay.rb +55 -0
  93. data/lib/charming/presentation/layout/pane.rb +149 -0
  94. data/lib/charming/presentation/layout/rect.rb +21 -0
  95. data/lib/charming/presentation/layout/screen_layout.rb +58 -0
  96. data/lib/charming/presentation/layout/split.rb +132 -0
  97. data/lib/charming/presentation/layout.rb +28 -30
  98. data/lib/charming/presentation/markdown/block_renderers.rb +118 -0
  99. data/lib/charming/presentation/markdown/inline_renderers.rb +66 -0
  100. data/lib/charming/presentation/markdown/render_context.rb +20 -0
  101. data/lib/charming/presentation/markdown/renderer.rb +82 -174
  102. data/lib/charming/presentation/markdown/syntax_highlighter.rb +58 -44
  103. data/lib/charming/presentation/markdown.rb +4 -3
  104. data/lib/charming/presentation/template_view.rb +22 -17
  105. data/lib/charming/presentation/templates/erb_handler.rb +4 -6
  106. data/lib/charming/presentation/templates.rb +47 -32
  107. data/lib/charming/presentation/ui/ansi_codes.rb +87 -0
  108. data/lib/charming/presentation/ui/ansi_slicer.rb +92 -0
  109. data/lib/charming/presentation/ui/border.rb +24 -26
  110. data/lib/charming/presentation/ui/border_painter.rb +56 -0
  111. data/lib/charming/presentation/ui/canvas.rb +80 -0
  112. data/lib/charming/presentation/ui/style.rb +170 -205
  113. data/lib/charming/presentation/ui/theme.rb +133 -135
  114. data/lib/charming/presentation/ui/width.rb +12 -14
  115. data/lib/charming/presentation/ui.rb +70 -213
  116. data/lib/charming/presentation/view.rb +107 -92
  117. data/lib/charming/runtime.rb +25 -10
  118. data/lib/charming/tasks/inline_executor.rb +9 -0
  119. data/lib/charming/tasks/task.rb +3 -0
  120. data/lib/charming/tasks/threaded_executor.rb +12 -0
  121. data/lib/charming/version.rb +1 -1
  122. data/lib/charming.rb +16 -2
  123. metadata +60 -10
  124. data/lib/charming/generators/app_generator/app_spec_templates.rb +0 -90
  125. data/lib/charming/generators/app_generator/basic_templates.rb +0 -81
  126. data/lib/charming/generators/app_generator/component_templates.rb +0 -36
  127. data/lib/charming/generators/app_generator/controller_template.rb +0 -60
  128. data/lib/charming/generators/app_generator/database_templates.rb +0 -45
  129. data/lib/charming/generators/app_generator/layout_template.rb +0 -66
  130. data/lib/charming/generators/app_generator/screen_spec_templates.rb +0 -69
  131. data/lib/charming/generators/app_generator/state_templates.rb +0 -30
  132. data/lib/charming/generators/app_generator/view_template.rb +0 -84
@@ -1,38 +1,46 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Charming
4
- module Presentation
5
- module Components
6
- class Spinner < Component
7
- DEFAULT_FRAMES = ["-", "\\", "|", "/"].freeze
8
-
9
- attr_reader :frames, :index, :label
10
-
11
- def initialize(frames: DEFAULT_FRAMES, index: 0, label: nil)
12
- super()
13
- raise ArgumentError, "frames cannot be empty" if frames.empty?
14
-
15
- @frames = frames
16
- @index = index
17
- @label = label
18
- end
4
+ module Components
5
+ # Spinner is a simple rotating-frame indicator. The component cycles through a list of
6
+ # frames on each `tick`; pair it with a controller timer to drive animation. An optional
7
+ # *label* is appended after the current frame on each render.
8
+ class Spinner < Component
9
+ # The default frame set: a 4-frame ASCII spinner.
10
+ DEFAULT_FRAMES = ["-", "\\", "|", "/"].freeze
11
+
12
+ # The current frame list, frame index, and optional label string.
13
+ attr_reader :frames, :index, :label
14
+
15
+ # *frames* defaults to DEFAULT_FRAMES but may be replaced with any array of frame strings.
16
+ # *index* is the starting frame index. *label* is an optional suffix shown after the frame.
17
+ def initialize(frames: DEFAULT_FRAMES, index: 0, label: nil)
18
+ super()
19
+ raise ArgumentError, "frames cannot be empty" if frames.empty?
20
+
21
+ @frames = frames
22
+ @index = index
23
+ @label = label
24
+ end
19
25
 
20
- def tick
21
- @index = (index + 1) % frames.length
22
- self
23
- end
26
+ # Advances the frame index by one position, wrapping around. Returns self for chaining.
27
+ def tick
28
+ @index = (index + 1) % frames.length
29
+ self
30
+ end
24
31
 
25
- def render
26
- return frame unless label
32
+ # Renders the current frame, optionally followed by the label and a space.
33
+ def render
34
+ return frame unless label
27
35
 
28
- "#{frame} #{label}"
29
- end
36
+ "#{frame} #{label}"
37
+ end
30
38
 
31
- private
39
+ private
32
40
 
33
- def frame
34
- frames.fetch(index % frames.length)
35
- end
41
+ # Returns the current frame string (with index modulo frame count to be safe).
42
+ def frame
43
+ frames.fetch(index % frames.length)
36
44
  end
37
45
  end
38
46
  end
@@ -3,115 +3,138 @@
3
3
  require "tty-table"
4
4
 
5
5
  module Charming
6
- module Presentation
7
- module Components
8
- class Table < Component
9
- include KeyboardHandler
10
-
11
- KEY_ACTIONS = {
12
- up: :move_up,
13
- down: :move_down,
14
- home: :move_home,
15
- end: :move_end
16
- }.freeze
17
-
18
- HEADER_HEIGHT = 2
19
-
20
- attr_reader :header, :rows, :selected_index
21
-
22
- def initialize(header:, rows: [], selected_index: 0, keymap: :vim)
23
- super()
24
- @header = Array(header).map(&:to_s)
25
- @rows = Array(rows)
26
- @selected_index = clamp_index(selected_index)
27
- @keymap = keymap
28
- end
6
+ module Components
7
+ # Table renders tabular data with a header row, a selected row highlight, and keyboard
8
+ # navigation. Mouse clicks within the body area also select rows. The table is rendered
9
+ # via tty-table and the selected row is overlaid with reverse-video ANSI styling.
10
+ class Table < Component
11
+ include KeyboardHandler
12
+
13
+ # Maps navigation keys to the instance methods that move the selection. Shared with
14
+ # List and Viewport via KeyboardHandler.
15
+ KEY_ACTIONS = {
16
+ up: :move_up,
17
+ down: :move_down,
18
+ home: :move_home,
19
+ end: :move_end
20
+ }.freeze
21
+
22
+ # Number of terminal rows occupied by the table's top border and header line. Used by
23
+ # the mouse handler to translate absolute row coordinates to body rows.
24
+ HEADER_HEIGHT = 2
25
+
26
+ # The header row, the body rows, and the currently selected row index, respectively.
27
+ attr_reader :header, :rows, :selected_index
28
+
29
+ # *header* is an array of column labels. *rows* is the array of body rows (each either a
30
+ # String, an Array, or a Hash of column-value pairs). *selected_index* defaults to 0.
31
+ # *keymap* selects the keybinding style (`:vim` enables h/j/k/l → left/down/up/right).
32
+ def initialize(header:, rows: [], selected_index: 0, keymap: :vim)
33
+ super()
34
+ @header = Array(header).map(&:to_s)
35
+ @rows = Array(rows)
36
+ @selected_index = clamp_index(selected_index)
37
+ @keymap = keymap
38
+ end
29
39
 
30
- def handle_key(event)
31
- return nil if rows.empty?
40
+ # Handles key events. Returns `[:selected, row]` on Enter; otherwise delegates to the
41
+ # KeyboardHandler for navigation keys.
42
+ def handle_key(event)
43
+ return nil if rows.empty?
32
44
 
33
- case Charming.key_of(event)
34
- when :enter then [:selected, selected_row]
35
- else super
36
- end
45
+ case Charming.key_of(event)
46
+ when :enter then [:selected, selected_row]
47
+ else super
37
48
  end
49
+ end
38
50
 
39
- def handle_mouse(event)
40
- return nil if rows.empty?
41
- return nil unless event.respond_to?(:click?) && event.click?
42
-
43
- clicked = event.y - HEADER_HEIGHT
44
- return nil if clicked.negative? || clicked >= rows.length
51
+ # Handles mouse events: a click within the body area selects the clicked row.
52
+ # Returns :handled on a successful click.
53
+ def handle_mouse(event)
54
+ return nil if rows.empty?
55
+ return nil unless event.respond_to?(:click?) && event.click?
45
56
 
46
- @selected_index = clicked
47
- :handled
48
- end
57
+ clicked = event.y - HEADER_HEIGHT
58
+ return nil if clicked.negative? || clicked >= rows.length
49
59
 
50
- def selected_row
51
- rows[selected_index]
52
- end
60
+ @selected_index = clicked
61
+ :handled
62
+ end
53
63
 
54
- def render
55
- return "(empty table)" if header.empty? && rows.empty?
64
+ # Returns the currently selected row, or nil when the table is empty.
65
+ def selected_row
66
+ rows[selected_index]
67
+ end
56
68
 
57
- normalized = rows.map { |row| normalize_row(row) }
58
- lines = TTY::Table.new(header: header, rows: normalized)
59
- .render(:unicode)
60
- .lines(chomp: true)
69
+ # Renders the table to a string. Returns a placeholder when both header and rows are empty.
70
+ def render
71
+ return "(empty table)" if header.empty? && rows.empty?
61
72
 
62
- compact_layout(lines)
63
- end
73
+ normalized = rows.map { |row| normalize_row(row) }
74
+ lines = TTY::Table.new(header: header, rows: normalized)
75
+ .render(:unicode)
76
+ .lines(chomp: true)
64
77
 
65
- private
78
+ compact_layout(lines)
79
+ end
66
80
 
67
- def normalize_row(row)
68
- cells = case row
69
- when Hash then row.values
70
- when String then [row]
71
- else Array(row)
72
- end
73
- return cells if header.length <= 1 || cells.length <= header.length
81
+ private
74
82
 
75
- kept = cells.first(header.length - 1)
76
- merged = cells[(header.length - 1)..].join(" ")
77
- kept + [merged]
83
+ # Coerces a *row* (Hash / String / Array) into a flat cell array matching the header.
84
+ # Excess cells are merged into the last column with a space separator.
85
+ def normalize_row(row)
86
+ cells = case row
87
+ when Hash then row.values
88
+ when String then [row]
89
+ else Array(row)
78
90
  end
91
+ return cells if header.length <= 1 || cells.length <= header.length
79
92
 
80
- def compact_layout(lines)
81
- return lines.join("\n") if lines.length < 4
93
+ kept = cells.first(header.length - 1)
94
+ merged = cells[(header.length - 1)..].join(" ")
95
+ kept + [merged]
96
+ end
82
97
 
83
- top, header_line, _separator, *rest = lines
84
- body = rest.first(rows.length)
85
- bottom = rest[rows.length]
98
+ # Applies the selected-row highlight and trims unused body rows below the actual row count.
99
+ def compact_layout(lines)
100
+ return lines.join("\n") if lines.length < 4
86
101
 
87
- highlighted = body.each_with_index.map do |line, index|
88
- (index == selected_index) ? "\e[7m#{line}\e[m" : line
89
- end
102
+ top, header_line, _separator, *rest = lines
103
+ body = rest.first(rows.length)
104
+ bottom = rest[rows.length]
90
105
 
91
- [top, header_line, *highlighted, bottom].compact.join("\n")
106
+ highlighted = body.each_with_index.map do |line, index|
107
+ (index == selected_index) ? "\e[7m#{line}\e[m" : line
92
108
  end
93
109
 
94
- def move_up
95
- @selected_index -= 1 if selected_index.positive?
96
- end
110
+ [top, header_line, *highlighted, bottom].compact.join("\n")
111
+ end
97
112
 
98
- def move_down
99
- @selected_index += 1 if selected_index < rows.length - 1
100
- end
113
+ # Moves the selection up one row.
114
+ def move_up
115
+ @selected_index -= 1 if selected_index.positive?
116
+ end
101
117
 
102
- def move_home
103
- @selected_index = 0
104
- end
118
+ # Moves the selection down one row.
119
+ def move_down
120
+ @selected_index += 1 if selected_index < rows.length - 1
121
+ end
105
122
 
106
- def move_end
107
- @selected_index = rows.length - 1
108
- end
123
+ # Moves the selection to the first row.
124
+ def move_home
125
+ @selected_index = 0
126
+ end
109
127
 
110
- def clamp_index(value)
111
- return 0 if rows.empty?
128
+ # Moves the selection to the last row.
129
+ def move_end
130
+ @selected_index = rows.length - 1
131
+ end
112
132
 
113
- value.to_i.clamp(0, rows.length - 1)
114
- end
133
+ # Clamps *value* to the valid row range, defaulting to 0 when the table is empty.
134
+ def clamp_index(value)
135
+ return 0 if rows.empty?
136
+
137
+ value.to_i.clamp(0, rows.length - 1)
115
138
  end
116
139
  end
117
140
  end