charming 0.1.0 → 0.1.2
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/README.md +38 -378
- data/lib/charming/application.rb +14 -3
- data/lib/charming/{application_model.rb → application_state.rb} +3 -3
- data/lib/charming/cli.rb +62 -3
- data/lib/charming/controller/class_methods.rb +115 -0
- data/lib/charming/controller/command_palette.rb +135 -0
- data/lib/charming/controller/component_dispatching.rb +81 -0
- data/lib/charming/controller/dispatching.rb +60 -0
- data/lib/charming/controller/focus_management.rb +30 -0
- data/lib/charming/controller/rendering.rb +127 -0
- data/lib/charming/controller/session_state.rb +41 -0
- data/lib/charming/controller/sidebar_navigation.rb +111 -0
- data/lib/charming/controller.rb +46 -448
- data/lib/charming/database_commands.rb +103 -0
- data/lib/charming/database_installer.rb +152 -0
- data/lib/charming/events/key_event.rb +15 -0
- data/lib/charming/events/mouse_event.rb +42 -0
- data/lib/charming/events/resize_event.rb +9 -0
- data/lib/charming/events/task_event.rb +19 -0
- data/lib/charming/events/timer_event.rb +9 -0
- data/lib/charming/focus.rb +58 -2
- data/lib/charming/generators/app_file_generator.rb +13 -0
- data/lib/charming/generators/app_generator.rb +147 -45
- data/lib/charming/generators/base.rb +26 -0
- data/lib/charming/generators/component_generator.rb +10 -10
- data/lib/charming/generators/controller_generator.rb +22 -14
- data/lib/charming/generators/model_generator.rb +128 -0
- data/lib/charming/generators/name.rb +10 -4
- data/lib/charming/generators/screen_generator.rb +84 -52
- data/lib/charming/generators/templates/app/Gemfile.template +5 -0
- data/lib/charming/generators/templates/app/README.md.template +9 -0
- data/lib/charming/generators/templates/app/Rakefile.template +3 -0
- data/lib/charming/generators/templates/app/application.template +13 -0
- data/lib/charming/generators/templates/app/application_controller.template +19 -0
- data/lib/charming/generators/templates/app/application_record.template +7 -0
- data/lib/charming/generators/templates/app/application_state.template +6 -0
- data/lib/charming/generators/templates/app/database_config.template +12 -0
- data/lib/charming/generators/templates/app/executable.template +7 -0
- data/lib/charming/generators/templates/app/gemspec.template +6 -0
- data/lib/charming/generators/templates/app/home_controller.template +6 -0
- data/lib/charming/generators/templates/app/home_state.template +7 -0
- data/lib/charming/generators/templates/app/keep.template +0 -0
- data/lib/charming/generators/templates/app/layout.template +113 -0
- data/lib/charming/generators/templates/app/root_file.template +20 -0
- data/lib/charming/generators/templates/app/routes.template +5 -0
- data/lib/charming/generators/templates/app/seeds.template +1 -0
- data/lib/charming/generators/templates/app/spec_controller.template +17 -0
- data/lib/charming/generators/templates/app/spec_helper.template +3 -0
- data/lib/charming/generators/templates/app/spec_state.template +17 -0
- data/lib/charming/generators/templates/app/spec_view.template +16 -0
- data/lib/charming/generators/templates/app/version.template +5 -0
- data/lib/charming/generators/templates/app/view.template +21 -0
- data/lib/charming/generators/templates/component/component.rb.template +9 -0
- data/lib/charming/generators/templates/controller/controller.rb.template +6 -0
- data/lib/charming/generators/templates/model/migration.rb.template +9 -0
- data/lib/charming/generators/templates/model/model.rb.template +6 -0
- data/lib/charming/generators/templates/model/spec.rb.template +9 -0
- data/lib/charming/generators/templates/screen/controller.rb.template +7 -0
- data/lib/charming/generators/templates/screen/spec_controller.rb.template +17 -0
- data/lib/charming/generators/templates/screen/spec_state.rb.template +17 -0
- data/lib/charming/generators/templates/screen/spec_view.rb.template +13 -0
- data/lib/charming/generators/templates/screen/state.rb.template +7 -0
- data/lib/charming/generators/templates/screen/view.rb.template +11 -0
- data/lib/charming/generators/templates/view/view.rb.template +11 -0
- data/lib/charming/generators/view_generator.rb +26 -13
- data/lib/charming/internal/renderer/differential.rb +17 -3
- data/lib/charming/internal/renderer/full_repaint.rb +6 -0
- data/lib/charming/internal/terminal/adapter.rb +29 -3
- data/lib/charming/internal/terminal/key_normalizer.rb +84 -0
- data/lib/charming/internal/terminal/memory_backend.rb +28 -1
- data/lib/charming/internal/terminal/mouse_parser.rb +81 -0
- data/lib/charming/internal/terminal/tty_backend.rb +62 -115
- data/lib/charming/presentation/component.rb +10 -0
- data/lib/charming/presentation/components/activity_indicator.rb +160 -0
- data/lib/charming/presentation/components/command_palette.rb +120 -0
- data/lib/charming/presentation/components/empty_state.rb +56 -0
- data/lib/charming/presentation/components/form/builder.rb +62 -0
- data/lib/charming/presentation/components/form/confirm.rb +69 -0
- data/lib/charming/presentation/components/form/field.rb +121 -0
- data/lib/charming/presentation/components/form/input.rb +71 -0
- data/lib/charming/presentation/components/form/note.rb +41 -0
- data/lib/charming/presentation/components/form/select.rb +112 -0
- data/lib/charming/presentation/components/form/textarea.rb +86 -0
- data/lib/charming/presentation/components/form.rb +156 -0
- data/lib/charming/presentation/components/keyboard_handler.rb +58 -0
- data/lib/charming/presentation/components/list.rb +132 -0
- data/lib/charming/presentation/components/markdown.rb +31 -0
- data/lib/charming/presentation/components/modal.rb +64 -0
- data/lib/charming/presentation/components/progressbar.rb +70 -0
- data/lib/charming/presentation/components/spinner.rb +49 -0
- data/lib/charming/presentation/components/table.rb +143 -0
- data/lib/charming/presentation/components/text_area.rb +267 -0
- data/lib/charming/presentation/components/text_input.rb +129 -0
- data/lib/charming/presentation/components/viewport.rb +272 -0
- data/lib/charming/presentation/layout/builder.rb +86 -0
- data/lib/charming/presentation/layout/overlay.rb +57 -0
- data/lib/charming/presentation/layout/pane.rb +145 -0
- data/lib/charming/presentation/layout/rect.rb +23 -0
- data/lib/charming/presentation/layout/screen_layout.rb +60 -0
- data/lib/charming/presentation/layout/split.rb +134 -0
- data/lib/charming/presentation/layout.rb +43 -0
- data/lib/charming/presentation/markdown/block_renderers.rb +120 -0
- data/lib/charming/presentation/markdown/inline_renderers.rb +68 -0
- data/lib/charming/presentation/markdown/render_context.rb +22 -0
- data/lib/charming/presentation/markdown/renderer.rb +113 -0
- data/lib/charming/presentation/markdown/syntax_highlighter.rb +79 -0
- data/lib/charming/presentation/markdown.rb +11 -0
- data/lib/charming/presentation/template_view.rb +34 -0
- data/lib/charming/presentation/templates/erb_handler.rb +15 -0
- data/lib/charming/presentation/templates.rb +68 -0
- data/lib/charming/presentation/ui/ansi_codes.rb +89 -0
- data/lib/charming/presentation/ui/ansi_slicer.rb +94 -0
- data/lib/charming/presentation/ui/border.rb +35 -0
- data/lib/charming/presentation/ui/border_painter.rb +58 -0
- data/lib/charming/presentation/ui/canvas.rb +82 -0
- data/lib/charming/presentation/ui/style.rb +213 -0
- data/lib/charming/presentation/ui/theme.rb +180 -0
- data/lib/charming/{ui → presentation/ui}/themes/phosphor.json +2 -2
- data/lib/charming/presentation/ui/width.rb +26 -0
- data/lib/charming/presentation/ui.rb +91 -0
- data/lib/charming/presentation/view.rb +135 -0
- data/lib/charming/runtime.rb +9 -7
- data/lib/charming/screen.rb +5 -1
- data/lib/charming/tasks/inline_executor.rb +37 -0
- data/lib/charming/tasks/task.rb +12 -0
- data/lib/charming/tasks/threaded_executor.rb +51 -0
- data/lib/charming/version.rb +1 -1
- data/lib/charming.rb +17 -0
- metadata +170 -36
- data/lib/charming/component.rb +0 -8
- data/lib/charming/components/activity_indicator.rb +0 -158
- data/lib/charming/components/command_palette.rb +0 -118
- data/lib/charming/components/keyboard_handler.rb +0 -22
- data/lib/charming/components/list.rb +0 -105
- data/lib/charming/components/modal.rb +0 -48
- data/lib/charming/components/progressbar.rb +0 -55
- data/lib/charming/components/spinner.rb +0 -37
- data/lib/charming/components/table.rb +0 -115
- data/lib/charming/components/text_input.rb +0 -103
- data/lib/charming/components/viewport.rb +0 -191
- data/lib/charming/generators/app_generator/app_spec_templates.rb +0 -86
- data/lib/charming/generators/app_generator/basic_templates.rb +0 -69
- data/lib/charming/generators/app_generator/component_templates.rb +0 -36
- data/lib/charming/generators/app_generator/controller_template.rb +0 -69
- data/lib/charming/generators/app_generator/layout_template.rb +0 -160
- data/lib/charming/generators/app_generator/model_templates.rb +0 -30
- data/lib/charming/generators/app_generator/screen_spec_templates.rb +0 -70
- data/lib/charming/generators/app_generator/view_template.rb +0 -90
- data/lib/charming/key_event.rb +0 -13
- data/lib/charming/mouse_event.rb +0 -40
- data/lib/charming/resize_event.rb +0 -7
- data/lib/charming/task.rb +0 -7
- data/lib/charming/task_event.rb +0 -17
- data/lib/charming/task_executor.rb +0 -62
- data/lib/charming/timer_event.rb +0 -7
- data/lib/charming/ui/border.rb +0 -33
- data/lib/charming/ui/style.rb +0 -244
- data/lib/charming/ui/theme.rb +0 -178
- data/lib/charming/ui/width.rb +0 -24
- data/lib/charming/ui.rb +0 -230
- data/lib/charming/view.rb +0 -116
- /data/lib/charming/{generators.rb → generators/error.rb} +0 -0
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Charming
|
|
4
|
-
module Components
|
|
5
|
-
class Modal < Component
|
|
6
|
-
def initialize(content:, title: nil, help: nil, width: 52, style: nil, theme: nil)
|
|
7
|
-
super(theme: theme)
|
|
8
|
-
@content = content
|
|
9
|
-
@title = title
|
|
10
|
-
@help = help
|
|
11
|
-
@width = width
|
|
12
|
-
@style = style
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def render
|
|
16
|
-
box(column(*lines, gap: 1), style: modal_style)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
private
|
|
20
|
-
|
|
21
|
-
attr_reader :content, :title, :help, :width
|
|
22
|
-
|
|
23
|
-
def lines
|
|
24
|
-
[title_line, help_line, render_content].compact
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def title_line
|
|
28
|
-
text(title, style: theme.title.align(:center).width(title_width)) if title
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def help_line
|
|
32
|
-
text(help, style: theme.muted) if help
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def render_content
|
|
36
|
-
content.respond_to?(:render) ? render_component(content) : content.to_s
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def modal_style
|
|
40
|
-
@style || theme.modal.width(width)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def title_width
|
|
44
|
-
[width - 8, 0].max
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Charming
|
|
4
|
-
module Components
|
|
5
|
-
class Progressbar < Component
|
|
6
|
-
attr_accessor :total, :current, :label, :complete, :incomplete, :bar_format
|
|
7
|
-
|
|
8
|
-
def initialize(total:, complete: "=", incomplete: " ", bar_format: :classic, label: nil)
|
|
9
|
-
super()
|
|
10
|
-
@total = [total.to_i, 0].max
|
|
11
|
-
@complete = complete.to_s
|
|
12
|
-
@incomplete = incomplete.to_s
|
|
13
|
-
@bar_format = bar_format.to_sym
|
|
14
|
-
@label = label
|
|
15
|
-
@current = 0
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def tick(count = 1)
|
|
19
|
-
update(@current + count)
|
|
20
|
-
self
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def update(value)
|
|
24
|
-
@current = value.to_i.clamp(0, @total)
|
|
25
|
-
self
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def complete!
|
|
29
|
-
@current = @total
|
|
30
|
-
self
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def render
|
|
34
|
-
width = [@total, 1].max
|
|
35
|
-
completed = completed_width(width)
|
|
36
|
-
incomplete = width - completed
|
|
37
|
-
incomplete -= 1 if @current.zero?
|
|
38
|
-
bar = (@complete * completed) + (@incomplete * incomplete)
|
|
39
|
-
result = "[" + bar + "]"
|
|
40
|
-
|
|
41
|
-
return result unless @label
|
|
42
|
-
|
|
43
|
-
"#{result} #{@label}"
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
private
|
|
47
|
-
|
|
48
|
-
def completed_width(width)
|
|
49
|
-
return 0 unless @total.positive?
|
|
50
|
-
|
|
51
|
-
((width * @current) / @total.to_f).round
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
end
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Charming
|
|
4
|
-
module Components
|
|
5
|
-
class Spinner < Component
|
|
6
|
-
DEFAULT_FRAMES = ["-", "\\", "|", "/"].freeze
|
|
7
|
-
|
|
8
|
-
attr_reader :frames, :index, :label
|
|
9
|
-
|
|
10
|
-
def initialize(frames: DEFAULT_FRAMES, index: 0, label: nil)
|
|
11
|
-
super()
|
|
12
|
-
raise ArgumentError, "frames cannot be empty" if frames.empty?
|
|
13
|
-
|
|
14
|
-
@frames = frames
|
|
15
|
-
@index = index
|
|
16
|
-
@label = label
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def tick
|
|
20
|
-
@index = (index + 1) % frames.length
|
|
21
|
-
self
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def render
|
|
25
|
-
return frame unless label
|
|
26
|
-
|
|
27
|
-
"#{frame} #{label}"
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
private
|
|
31
|
-
|
|
32
|
-
def frame
|
|
33
|
-
frames.fetch(index % frames.length)
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "tty-table"
|
|
4
|
-
|
|
5
|
-
module Charming
|
|
6
|
-
module Components
|
|
7
|
-
class Table < Component
|
|
8
|
-
include KeyboardHandler
|
|
9
|
-
|
|
10
|
-
KEY_ACTIONS = {
|
|
11
|
-
up: :move_up,
|
|
12
|
-
down: :move_down,
|
|
13
|
-
home: :move_home,
|
|
14
|
-
end: :move_end
|
|
15
|
-
}.freeze
|
|
16
|
-
|
|
17
|
-
HEADER_HEIGHT = 2
|
|
18
|
-
|
|
19
|
-
attr_reader :header, :rows, :selected_index
|
|
20
|
-
|
|
21
|
-
def initialize(header:, rows: [], selected_index: 0)
|
|
22
|
-
super()
|
|
23
|
-
@header = Array(header).map(&:to_s)
|
|
24
|
-
@rows = Array(rows)
|
|
25
|
-
@selected_index = clamp_index(selected_index)
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def handle_key(event)
|
|
29
|
-
return nil if rows.empty?
|
|
30
|
-
|
|
31
|
-
case Charming.key_of(event)
|
|
32
|
-
when :enter then [:selected, selected_row]
|
|
33
|
-
else super
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def handle_mouse(event)
|
|
38
|
-
return nil if rows.empty?
|
|
39
|
-
return nil unless event.respond_to?(:click?) && event.click?
|
|
40
|
-
|
|
41
|
-
clicked = event.y - HEADER_HEIGHT
|
|
42
|
-
return nil if clicked.negative? || clicked >= rows.length
|
|
43
|
-
|
|
44
|
-
@selected_index = clicked
|
|
45
|
-
:handled
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def selected_row
|
|
49
|
-
rows[selected_index]
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def render
|
|
53
|
-
return "(empty table)" if header.empty? && rows.empty?
|
|
54
|
-
|
|
55
|
-
normalized = rows.map { |row| normalize_row(row) }
|
|
56
|
-
lines = TTY::Table.new(header: header, rows: normalized)
|
|
57
|
-
.render(:unicode)
|
|
58
|
-
.lines(chomp: true)
|
|
59
|
-
|
|
60
|
-
compact_layout(lines)
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
private
|
|
64
|
-
|
|
65
|
-
def normalize_row(row)
|
|
66
|
-
cells = case row
|
|
67
|
-
when Hash then row.values
|
|
68
|
-
when String then [row]
|
|
69
|
-
else Array(row)
|
|
70
|
-
end
|
|
71
|
-
return cells if header.length <= 1 || cells.length <= header.length
|
|
72
|
-
|
|
73
|
-
kept = cells.first(header.length - 1)
|
|
74
|
-
merged = cells[(header.length - 1)..].join(" ")
|
|
75
|
-
kept + [merged]
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def compact_layout(lines)
|
|
79
|
-
return lines.join("\n") if lines.length < 4
|
|
80
|
-
|
|
81
|
-
top, header_line, _separator, *rest = lines
|
|
82
|
-
body = rest.first(rows.length)
|
|
83
|
-
bottom = rest[rows.length]
|
|
84
|
-
|
|
85
|
-
highlighted = body.each_with_index.map do |line, index|
|
|
86
|
-
(index == selected_index) ? "\e[7m#{line}\e[m" : line
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
[top, header_line, *highlighted, bottom].compact.join("\n")
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def move_up
|
|
93
|
-
@selected_index -= 1 if selected_index.positive?
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def move_down
|
|
97
|
-
@selected_index += 1 if selected_index < rows.length - 1
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def move_home
|
|
101
|
-
@selected_index = 0
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def move_end
|
|
105
|
-
@selected_index = rows.length - 1
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def clamp_index(value)
|
|
109
|
-
return 0 if rows.empty?
|
|
110
|
-
|
|
111
|
-
value.to_i.clamp(0, rows.length - 1)
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
end
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Charming
|
|
4
|
-
module Components
|
|
5
|
-
class TextInput < Component
|
|
6
|
-
include KeyboardHandler
|
|
7
|
-
|
|
8
|
-
# Maps editing keys (left/right/home/end/backspace/delete) to the instance
|
|
9
|
-
# methods they dispatch via KeyboardHandler. Each symbol key (e.g., :left)
|
|
10
|
-
# maps to a method (e.g., :move_left) that adjusts cursor position or text content.
|
|
11
|
-
KEY_ACTIONS = {
|
|
12
|
-
left: :move_left,
|
|
13
|
-
right: :move_right,
|
|
14
|
-
home: :move_home,
|
|
15
|
-
end: :move_end,
|
|
16
|
-
backspace: :delete_before_cursor,
|
|
17
|
-
delete: :delete_at_cursor
|
|
18
|
-
}.freeze
|
|
19
|
-
|
|
20
|
-
attr_reader :value, :cursor
|
|
21
|
-
|
|
22
|
-
def initialize(value: "", placeholder: "", width: nil, cursor: nil)
|
|
23
|
-
super()
|
|
24
|
-
@value = value.dup
|
|
25
|
-
@placeholder = placeholder
|
|
26
|
-
@width = width
|
|
27
|
-
@cursor = cursor || @value.length
|
|
28
|
-
clamp_position
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def handle_key(event)
|
|
32
|
-
return :handled if character_event?(event) && insert(event.char)
|
|
33
|
-
|
|
34
|
-
super
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def render
|
|
38
|
-
rendered = render_value
|
|
39
|
-
@width ? style.width(@width).render(rendered) : rendered
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
private
|
|
43
|
-
|
|
44
|
-
attr_reader :placeholder
|
|
45
|
-
|
|
46
|
-
def character_event?(event)
|
|
47
|
-
event.respond_to?(:char) && event.char && event.char.length == 1 && printable?(event.char)
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def printable?(char)
|
|
51
|
-
!char.match?(/[[:cntrl:]]/)
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def insert(char)
|
|
55
|
-
@value = value[0...cursor] + char + value[cursor..]
|
|
56
|
-
@cursor += char.length
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def move_left
|
|
60
|
-
@cursor -= 1 if cursor.positive?
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def move_right
|
|
64
|
-
@cursor += 1 if cursor < value.length
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def move_home
|
|
68
|
-
@cursor = 0
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def move_end
|
|
72
|
-
@cursor = value.length
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def delete_before_cursor
|
|
76
|
-
return if cursor.zero?
|
|
77
|
-
|
|
78
|
-
@value = value[0...(cursor - 1)] + value[cursor..]
|
|
79
|
-
@cursor -= 1
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def delete_at_cursor
|
|
83
|
-
return if cursor >= value.length
|
|
84
|
-
|
|
85
|
-
@value = value[0...cursor] + value[(cursor + 1)..]
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def render_value
|
|
89
|
-
return cursor_marker + placeholder if value.empty?
|
|
90
|
-
|
|
91
|
-
value[0...cursor] + cursor_marker + value[cursor..]
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def cursor_marker
|
|
95
|
-
"|"
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def clamp_position
|
|
99
|
-
@cursor = cursor.clamp(0, value.length)
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
end
|
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "unicode/display_width"
|
|
4
|
-
|
|
5
|
-
module Charming
|
|
6
|
-
module Components
|
|
7
|
-
class Viewport < Component
|
|
8
|
-
include KeyboardHandler
|
|
9
|
-
|
|
10
|
-
ANSI_PATTERN = /\e\[[0-9;]*m/
|
|
11
|
-
KEY_ACTIONS = {
|
|
12
|
-
up: :scroll_up,
|
|
13
|
-
down: :scroll_down,
|
|
14
|
-
page_up: :page_up,
|
|
15
|
-
page_down: :page_down,
|
|
16
|
-
home: :scroll_home,
|
|
17
|
-
end: :scroll_end,
|
|
18
|
-
left: :scroll_left,
|
|
19
|
-
right: :scroll_right
|
|
20
|
-
}.freeze
|
|
21
|
-
|
|
22
|
-
attr_reader :offset, :column
|
|
23
|
-
|
|
24
|
-
def initialize(content:, width: nil, height: nil, offset: 0, column: 0)
|
|
25
|
-
super()
|
|
26
|
-
@content = content
|
|
27
|
-
@width = width
|
|
28
|
-
@height = height
|
|
29
|
-
@offset = offset
|
|
30
|
-
@column = column
|
|
31
|
-
clamp_position
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def render
|
|
35
|
-
visible_lines.map { |line| render_line(line) }.join("\n")
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def handle_mouse(event)
|
|
39
|
-
return nil unless height
|
|
40
|
-
|
|
41
|
-
if event.scroll?
|
|
42
|
-
scroll_delta = (event.button_name == :scroll_up) ? -1 : 1
|
|
43
|
-
@offset += scroll_delta
|
|
44
|
-
clamp_position
|
|
45
|
-
return :handled
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
return nil unless event.click?
|
|
49
|
-
|
|
50
|
-
clicked_row = event.y
|
|
51
|
-
return nil if clicked_row < offset || clicked_row >= offset + viewport_height
|
|
52
|
-
|
|
53
|
-
@offset = clicked_row
|
|
54
|
-
clamp_position
|
|
55
|
-
:handled
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
private
|
|
59
|
-
|
|
60
|
-
attr_reader :content, :width, :height
|
|
61
|
-
|
|
62
|
-
def scroll_up
|
|
63
|
-
@offset -= 1
|
|
64
|
-
clamp_position
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def scroll_down
|
|
68
|
-
@offset += 1
|
|
69
|
-
clamp_position
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def page_up
|
|
73
|
-
@offset -= page_size
|
|
74
|
-
clamp_position
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def page_down
|
|
78
|
-
@offset += page_size
|
|
79
|
-
clamp_position
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def scroll_home
|
|
83
|
-
@offset = 0
|
|
84
|
-
@column = 0
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def scroll_end
|
|
88
|
-
@offset = max_offset
|
|
89
|
-
@column = max_column
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def scroll_left
|
|
93
|
-
@column -= 1
|
|
94
|
-
clamp_position
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def scroll_right
|
|
98
|
-
@column += 1
|
|
99
|
-
clamp_position
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def clamp_position
|
|
103
|
-
@offset = offset.clamp(0, max_offset)
|
|
104
|
-
@column = column.clamp(0, max_column)
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def visible_lines
|
|
108
|
-
lines = content_lines.slice(offset, viewport_height) || []
|
|
109
|
-
return lines unless height
|
|
110
|
-
|
|
111
|
-
lines + Array.new([height - lines.length, 0].max, "")
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def render_line(line)
|
|
115
|
-
return line unless width
|
|
116
|
-
|
|
117
|
-
pad_line(clip_line(line), width)
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def clip_line(line)
|
|
121
|
-
clipped = clip_tokens(line.to_s)
|
|
122
|
-
needs_reset?(clipped) ? "#{clipped}\e[0m" : clipped
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
def clip_tokens(line)
|
|
126
|
-
state = {cursor: 0, output: +""}
|
|
127
|
-
line.scan(/#{ANSI_PATTERN}|./mo) do |token|
|
|
128
|
-
ansi?(token) ? append_ansi(state, token) : append_character(state, token)
|
|
129
|
-
end
|
|
130
|
-
state.fetch(:output)
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
def append_ansi(state, token)
|
|
134
|
-
state.fetch(:output) << token
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def append_character(state, char)
|
|
138
|
-
char_width = Unicode::DisplayWidth.of(char)
|
|
139
|
-
cursor = state.fetch(:cursor)
|
|
140
|
-
state.fetch(:output) << char if visible?(cursor, char_width)
|
|
141
|
-
state[:cursor] = cursor + char_width
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
def visible?(cursor, char_width)
|
|
145
|
-
cursor >= column && cursor + char_width <= column + width
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
def needs_reset?(value)
|
|
149
|
-
value.match?(ANSI_PATTERN) && !value.end_with?("\e[0m")
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
def pad_line(line, target_width)
|
|
153
|
-
line + (" " * [target_width - UI::Width.measure(line), 0].max)
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
def content_lines
|
|
157
|
-
rendered_content.lines(chomp: true)
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
def rendered_content
|
|
161
|
-
content.respond_to?(:render) ? content.render.to_s : content.to_s
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
def viewport_height
|
|
165
|
-
height || content_lines.length
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
def page_size
|
|
169
|
-
[viewport_height, 1].max
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def max_offset
|
|
173
|
-
[content_lines.length - viewport_height, 0].max
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
def max_column
|
|
177
|
-
return 0 unless width
|
|
178
|
-
|
|
179
|
-
[content_width - width, 0].max
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
def content_width
|
|
183
|
-
content_lines.map { |line| UI::Width.measure(line) }.max || 0
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
def ansi?(token)
|
|
187
|
-
token.match?(ANSI_PATTERN)
|
|
188
|
-
end
|
|
189
|
-
end
|
|
190
|
-
end
|
|
191
|
-
end
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Charming
|
|
4
|
-
module Generators
|
|
5
|
-
class AppGenerator
|
|
6
|
-
module AppSpecTemplates
|
|
7
|
-
def spec_model
|
|
8
|
-
%(# frozen_string_literal: true
|
|
9
|
-
|
|
10
|
-
require "#{app_name.snake_name}"
|
|
11
|
-
|
|
12
|
-
RSpec.describe #{app_name.class_name}::HomeModel do
|
|
13
|
-
describe "#title" do
|
|
14
|
-
it "has the correct default string value" do
|
|
15
|
-
instance = described_class.new
|
|
16
|
-
expect(instance.title).to eq("#{app_name.class_name}")
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
it "accepts overridden title values" do
|
|
20
|
-
instance = described_class.new(title: "Alternative")
|
|
21
|
-
expect(instance.title).to eq("Alternative")
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
)
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def spec_controller
|
|
29
|
-
%(# frozen_string_literal: true
|
|
30
|
-
|
|
31
|
-
require "#{app_name.snake_name}"
|
|
32
|
-
|
|
33
|
-
RSpec.describe #{app_name.class_name}::HomeController do
|
|
34
|
-
let(:application) { #{app_name.class_name}::Application.new }
|
|
35
|
-
|
|
36
|
-
subject(:controller) { described_class.new(application: application) }
|
|
37
|
-
|
|
38
|
-
describe "#show" do
|
|
39
|
-
it "renders the view with the model" do
|
|
40
|
-
response = controller.dispatch(:show)
|
|
41
|
-
|
|
42
|
-
expect(response).to respond_to(:body)
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def spec_view
|
|
50
|
-
%(# frozen_string_literal: true
|
|
51
|
-
|
|
52
|
-
require "#{app_name.snake_name}"
|
|
53
|
-
|
|
54
|
-
RSpec.describe #{app_name.class_name}::HomeView do
|
|
55
|
-
describe "#render" do
|
|
56
|
-
it "renders the model title" do
|
|
57
|
-
view = described_class.new(
|
|
58
|
-
home: double(title: "#{app_name.class_name}")
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
expect(view.render).to include("#{app_name.class_name}")
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
)
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def spec_component
|
|
69
|
-
%(# frozen_string_literal: true
|
|
70
|
-
|
|
71
|
-
require "#{app_name.snake_name}"
|
|
72
|
-
|
|
73
|
-
RSpec.describe #{app_name.class_name}::AppFrameComponent do
|
|
74
|
-
describe "#render" do
|
|
75
|
-
it "returns a string" do
|
|
76
|
-
component = described_class.new(title: "#{app_name.class_name}")
|
|
77
|
-
expect(component.render).to be_a(String)
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
)
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
end
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Charming
|
|
4
|
-
module Generators
|
|
5
|
-
class AppGenerator
|
|
6
|
-
module BasicTemplates
|
|
7
|
-
def gemfile
|
|
8
|
-
%(# frozen_string_literal: true
|
|
9
|
-
|
|
10
|
-
source "https://rubygems.org"
|
|
11
|
-
|
|
12
|
-
gemspec
|
|
13
|
-
)
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def rakefile
|
|
17
|
-
%(# frozen_string_literal: true
|
|
18
|
-
|
|
19
|
-
require "bundler/gem_tasks"
|
|
20
|
-
)
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def readme
|
|
24
|
-
%(# #{name.class_name}
|
|
25
|
-
|
|
26
|
-
A Charming terminal user interface.
|
|
27
|
-
|
|
28
|
-
Run it with:
|
|
29
|
-
|
|
30
|
-
```sh
|
|
31
|
-
bundle exec #{name.snake_name}
|
|
32
|
-
```
|
|
33
|
-
)
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def gemspec
|
|
37
|
-
%(# frozen_string_literal: true
|
|
38
|
-
|
|
39
|
-
require_relative "lib/#{name.snake_name}/version"
|
|
40
|
-
|
|
41
|
-
Gem::Specification.new do |spec|
|
|
42
|
-
#{gemspec_attributes}
|
|
43
|
-
#{gemspec_dependencies}
|
|
44
|
-
end
|
|
45
|
-
)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def gemspec_attributes
|
|
49
|
-
%( spec.name = "#{name.snake_name}"
|
|
50
|
-
spec.version = #{name.class_name}::VERSION
|
|
51
|
-
spec.summary = "A Charming terminal user interface."
|
|
52
|
-
spec.authors = ["TODO: Your name"]
|
|
53
|
-
spec.email = ["TODO: Your email"]
|
|
54
|
-
spec.files = Dir.glob("{app,config,exe,lib}/**/*") + %w[README.md]
|
|
55
|
-
spec.bindir = "exe"
|
|
56
|
-
spec.executables = ["#{name.snake_name}"]
|
|
57
|
-
spec.require_paths = ["lib"]
|
|
58
|
-
spec.required_ruby_version = ">= 4.0.0"
|
|
59
|
-
spec.metadata["rubygems_mfa_required"] = "true")
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def gemspec_dependencies
|
|
63
|
-
%(
|
|
64
|
-
spec.add_dependency "charming")
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
end
|