tui_tui 0.3.0 → 0.4.0

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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +60 -0
  3. data/README.md +51 -14
  4. data/examples/README.md +28 -0
  5. data/examples/breakout.rb +225 -0
  6. data/examples/counter.rb +11 -12
  7. data/examples/csv_viewer.rb +28 -50
  8. data/examples/downloads.rb +128 -0
  9. data/examples/file_browser.rb +64 -55
  10. data/examples/form.rb +80 -102
  11. data/examples/life.rb +1 -1
  12. data/examples/log_viewer.rb +118 -0
  13. data/examples/todo.rb +43 -64
  14. data/examples/widgets.rb +4 -4
  15. data/lib/tui_tui/box_chrome.rb +0 -9
  16. data/lib/tui_tui/box_chrome_resolver.rb +21 -0
  17. data/lib/tui_tui/canvas.rb +6 -55
  18. data/lib/tui_tui/canvas_compositor.rb +12 -10
  19. data/lib/tui_tui/canvas_renderer.rb +59 -0
  20. data/lib/tui_tui/cell.rb +4 -3
  21. data/lib/tui_tui/command.rb +21 -0
  22. data/lib/tui_tui/event_stream.rb +42 -7
  23. data/lib/tui_tui/key_reader.rb +1 -1
  24. data/lib/tui_tui/modal/command_palette.rb +193 -0
  25. data/lib/tui_tui/modal/confirm.rb +74 -0
  26. data/lib/tui_tui/modal/help.rb +44 -0
  27. data/lib/tui_tui/modal/pager.rb +106 -0
  28. data/lib/tui_tui/modal/prompt.rb +66 -0
  29. data/lib/tui_tui/modal/select.rb +103 -0
  30. data/lib/tui_tui/modal.rb +4 -0
  31. data/lib/tui_tui/modal_host.rb +2 -2
  32. data/lib/tui_tui/rect.rb +69 -0
  33. data/lib/tui_tui/runtime.rb +46 -23
  34. data/lib/tui_tui/screen.rb +7 -3
  35. data/lib/tui_tui/test_runtime.rb +71 -0
  36. data/lib/tui_tui/text_input.rb +168 -0
  37. data/lib/tui_tui/text_sanitizer.rb +3 -10
  38. data/lib/tui_tui/theme.rb +25 -27
  39. data/lib/tui_tui/version.rb +1 -1
  40. data/lib/tui_tui/widget/list.rb +59 -0
  41. data/lib/tui_tui/widget/progress.rb +33 -0
  42. data/lib/tui_tui/widget/scroll_list.rb +59 -0
  43. data/lib/tui_tui/widget/scrollbar.rb +43 -0
  44. data/lib/tui_tui/widget/spinner.rb +37 -0
  45. data/lib/tui_tui/widget/status_bar.rb +25 -0
  46. data/lib/tui_tui/widget/table.rb +81 -0
  47. data/lib/tui_tui/widget/tabs.rb +67 -0
  48. data/lib/tui_tui/widget/text_view.rb +47 -0
  49. data/lib/tui_tui/widget/toast.rb +83 -0
  50. data/lib/tui_tui.rb +23 -15
  51. metadata +28 -15
  52. data/lib/tui_tui/command_palette.rb +0 -190
  53. data/lib/tui_tui/confirm.rb +0 -74
  54. data/lib/tui_tui/help.rb +0 -44
  55. data/lib/tui_tui/list.rb +0 -57
  56. data/lib/tui_tui/pager.rb +0 -106
  57. data/lib/tui_tui/prompt.rb +0 -108
  58. data/lib/tui_tui/scroll_list.rb +0 -57
  59. data/lib/tui_tui/scrollbar.rb +0 -41
  60. data/lib/tui_tui/select.rb +0 -103
  61. data/lib/tui_tui/status_bar.rb +0 -23
  62. data/lib/tui_tui/text_view.rb +0 -45
  63. data/lib/tui_tui/toast.rb +0 -83
  64. /data/lib/tui_tui/{event.rb → events.rb} +0 -0
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../line"
4
+ require_relative "../span"
5
+ require_relative "scrollbar"
6
+
7
+ module TuiTui
8
+ module Widget
9
+ # A scrolled read-only text window.
10
+ # Lines may be Strings, Lines, or Span arrays, supplied eagerly or lazily.
11
+ module TextView
12
+ module_function
13
+
14
+ def draw(canvas, rect, lines = nil, top: 0, style: nil, scrollbar: nil, total: nil, auto: false)
15
+ # With auto:, reserve the gutter only when the content overflows the rect.
16
+ # When the total is unknown (lazy block, no total:), the bar is kept.
17
+ count = total || lines&.length
18
+ show_bar = scrollbar && !(auto && count && count <= rect.rows)
19
+ body, gutter = show_bar ? rect.split_gutter : [rect, nil]
20
+ body.rows.times do |offset|
21
+ index = top + offset
22
+ content = lines ? lines[index] : yield(index)
23
+ next if content.nil?
24
+
25
+ canvas.line(body.row + offset, body.col, Line.coerce(content, style).truncate(body.cols))
26
+ end
27
+
28
+ draw_scrollbar(canvas, gutter, top, total || lines&.length, body.rows, scrollbar) if gutter
29
+ canvas
30
+ end
31
+
32
+ def draw_scrollbar(canvas, gutter, top, total, visible, theme)
33
+ return unless total
34
+
35
+ Scrollbar.draw(
36
+ canvas,
37
+ gutter,
38
+ top: top,
39
+ visible: visible,
40
+ total: total,
41
+ track_style: theme.scroll_track,
42
+ thumb_style: theme.scroll_thumb
43
+ )
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../display_text"
4
+ require_relative "../style"
5
+ require_relative "../clock"
6
+
7
+ module TuiTui
8
+ module Widget
9
+ # A transient notification overlay.
10
+ # The clock is injectable so expiry is testable without sleeping.
11
+ class Toast
12
+ DEFAULT_SECONDS = 2.0
13
+ DEFAULT_STYLE = Style.new(attrs: [:reverse])
14
+ DEFAULT_POSITION = :bottom_center
15
+ POSITIONS = {
16
+ top_left: [:top, :left],
17
+ top_center: [:top, :center],
18
+ top_right: [:top, :right],
19
+ middle_left: [:middle, :left],
20
+ middle_center: [:middle, :center],
21
+ middle_right: [:middle, :right],
22
+ bottom_left: [:bottom, :left],
23
+ bottom_center: [:bottom, :center],
24
+ bottom_right: [:bottom, :right],
25
+ center: [:middle, :center]
26
+ }.freeze
27
+ def initialize(message, seconds: DEFAULT_SECONDS, position: DEFAULT_POSITION, clock: Clock)
28
+ @message = DisplayText.new(message)
29
+ @position = position
30
+ @clock = clock
31
+ @expires_at = clock.monotonic + seconds
32
+ validate_position!
33
+ end
34
+
35
+ def expired? = @clock.monotonic >= @expires_at
36
+
37
+ def draw(canvas, size, style: DEFAULT_STYLE, position: @position)
38
+ return canvas if expired?
39
+
40
+ label = DisplayText.new(" #{@message} ").truncate(size.cols)
41
+ vertical, horizontal = position_parts(position)
42
+ row = row_for(size, vertical)
43
+ col = col_for(size, label.width, horizontal)
44
+ canvas.text(row, col, label, style)
45
+ canvas
46
+ end
47
+
48
+ private
49
+
50
+ def validate_position!
51
+ position_parts(@position)
52
+ end
53
+
54
+ def position_parts(position)
55
+ POSITIONS.fetch(position) do
56
+ raise ArgumentError, "unknown toast position: #{position.inspect}"
57
+ end
58
+ end
59
+
60
+ def row_for(size, vertical)
61
+ case vertical
62
+ when :top
63
+ 1
64
+ when :middle
65
+ ((size.rows + 1) / 2).clamp(1, size.rows)
66
+ when :bottom
67
+ [size.rows - 2, 1].max
68
+ end
69
+ end
70
+
71
+ def col_for(size, width, horizontal)
72
+ case horizontal
73
+ when :left
74
+ 1
75
+ when :center
76
+ [((size.cols - width) / 2) + 1, 1].max
77
+ when :right
78
+ [size.cols - width + 1, 1].max
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
data/lib/tui_tui.rb CHANGED
@@ -12,6 +12,7 @@ require_relative "tui_tui/ansi"
12
12
  require_relative "tui_tui/color_depth"
13
13
  require_relative "tui_tui/box_chrome"
14
14
  require_relative "tui_tui/box_prober"
15
+ require_relative "tui_tui/box_chrome_resolver"
15
16
  require_relative "tui_tui/palette"
16
17
  require_relative "tui_tui/style"
17
18
  require_relative "tui_tui/theme"
@@ -20,29 +21,36 @@ require_relative "tui_tui/rect"
20
21
  require_relative "tui_tui/cell"
21
22
  require_relative "tui_tui/canvas"
22
23
  require_relative "tui_tui/render_context"
24
+ require_relative "tui_tui/canvas_renderer"
23
25
  require_relative "tui_tui/canvas_compositor"
24
- require_relative "tui_tui/event"
26
+ require_relative "tui_tui/events"
27
+ require_relative "tui_tui/command"
25
28
  require_relative "tui_tui/key_code"
29
+ require_relative "tui_tui/text_input"
26
30
  require_relative "tui_tui/key_reader"
27
31
  require_relative "tui_tui/key_intent"
28
32
  require_relative "tui_tui/event_stream"
29
33
  require_relative "tui_tui/terminal_session"
30
- require_relative "tui_tui/scroll_list"
31
- require_relative "tui_tui/list"
32
- require_relative "tui_tui/text_view"
33
- require_relative "tui_tui/scrollbar"
34
- require_relative "tui_tui/status_bar"
35
- require_relative "tui_tui/toast"
34
+ require_relative "tui_tui/widget/scroll_list"
35
+ require_relative "tui_tui/widget/list"
36
+ require_relative "tui_tui/widget/text_view"
37
+ require_relative "tui_tui/widget/scrollbar"
38
+ require_relative "tui_tui/widget/status_bar"
39
+ require_relative "tui_tui/widget/toast"
40
+ require_relative "tui_tui/widget/table"
41
+ require_relative "tui_tui/widget/tabs"
42
+ require_relative "tui_tui/widget/spinner"
43
+ require_relative "tui_tui/widget/progress"
36
44
  require_relative "tui_tui/focus_ring"
37
45
  require_relative "tui_tui/fuzzy"
38
46
  require_relative "tui_tui/modal"
39
47
  require_relative "tui_tui/modal_host"
40
- require_relative "tui_tui/confirm"
41
- require_relative "tui_tui/select"
42
- require_relative "tui_tui/command_palette"
43
- require_relative "tui_tui/help"
44
- require_relative "tui_tui/prompt"
45
- require_relative "tui_tui/pager"
48
+ require_relative "tui_tui/modal/confirm"
49
+ require_relative "tui_tui/modal/select"
50
+ require_relative "tui_tui/modal/command_palette"
51
+ require_relative "tui_tui/modal/help"
52
+ require_relative "tui_tui/modal/prompt"
53
+ require_relative "tui_tui/modal/pager"
46
54
  require_relative "tui_tui/screen"
47
55
  require_relative "tui_tui/runtime"
48
56
 
@@ -51,7 +59,7 @@ require_relative "tui_tui/runtime"
51
59
  # pieces — width, color, style, layout — need nothing.
52
60
  #
53
61
  # The framework is application-agnostic. Build an app object responding to
54
- # `view(size) -> Canvas` and `update(event) -> app | :quit`, then drive it with
55
- # `TuiTui::Runtime`.
62
+ # `view(size) -> Canvas` and `update(event) -> app | [app, *commands] | :quit`,
63
+ # then drive it with `TuiTui::Runtime`.
56
64
  module TuiTui
57
65
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tui_tui
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - takahashim
@@ -23,60 +23,73 @@ files:
23
23
  - LICENSE.txt
24
24
  - README.md
25
25
  - Rakefile
26
+ - examples/README.md
27
+ - examples/breakout.rb
26
28
  - examples/clock.rb
27
29
  - examples/counter.rb
28
30
  - examples/csv_viewer.rb
31
+ - examples/downloads.rb
29
32
  - examples/file_browser.rb
30
33
  - examples/form.rb
31
34
  - examples/life.rb
35
+ - examples/log_viewer.rb
32
36
  - examples/paint.rb
33
37
  - examples/todo.rb
34
38
  - examples/widgets.rb
35
39
  - lib/tui_tui.rb
36
40
  - lib/tui_tui/ansi.rb
37
41
  - lib/tui_tui/box_chrome.rb
42
+ - lib/tui_tui/box_chrome_resolver.rb
38
43
  - lib/tui_tui/box_prober.rb
39
44
  - lib/tui_tui/canvas.rb
40
45
  - lib/tui_tui/canvas_compositor.rb
46
+ - lib/tui_tui/canvas_renderer.rb
41
47
  - lib/tui_tui/cell.rb
42
48
  - lib/tui_tui/clock.rb
43
49
  - lib/tui_tui/color_depth.rb
44
- - lib/tui_tui/command_palette.rb
45
- - lib/tui_tui/confirm.rb
50
+ - lib/tui_tui/command.rb
46
51
  - lib/tui_tui/display_text.rb
47
- - lib/tui_tui/event.rb
48
52
  - lib/tui_tui/event_stream.rb
53
+ - lib/tui_tui/events.rb
49
54
  - lib/tui_tui/focus_ring.rb
50
55
  - lib/tui_tui/fuzzy.rb
51
- - lib/tui_tui/help.rb
52
56
  - lib/tui_tui/key_code.rb
53
57
  - lib/tui_tui/key_intent.rb
54
58
  - lib/tui_tui/key_reader.rb
55
59
  - lib/tui_tui/line.rb
56
- - lib/tui_tui/list.rb
57
60
  - lib/tui_tui/modal.rb
61
+ - lib/tui_tui/modal/command_palette.rb
62
+ - lib/tui_tui/modal/confirm.rb
63
+ - lib/tui_tui/modal/help.rb
64
+ - lib/tui_tui/modal/pager.rb
65
+ - lib/tui_tui/modal/prompt.rb
66
+ - lib/tui_tui/modal/select.rb
58
67
  - lib/tui_tui/modal_host.rb
59
- - lib/tui_tui/pager.rb
60
68
  - lib/tui_tui/palette.rb
61
- - lib/tui_tui/prompt.rb
62
69
  - lib/tui_tui/rect.rb
63
70
  - lib/tui_tui/render_context.rb
64
71
  - lib/tui_tui/runtime.rb
65
72
  - lib/tui_tui/screen.rb
66
- - lib/tui_tui/scroll_list.rb
67
- - lib/tui_tui/scrollbar.rb
68
- - lib/tui_tui/select.rb
69
73
  - lib/tui_tui/size.rb
70
74
  - lib/tui_tui/span.rb
71
- - lib/tui_tui/status_bar.rb
72
75
  - lib/tui_tui/style.rb
73
76
  - lib/tui_tui/terminal_session.rb
74
77
  - lib/tui_tui/terminal_size.rb
78
+ - lib/tui_tui/test_runtime.rb
79
+ - lib/tui_tui/text_input.rb
75
80
  - lib/tui_tui/text_sanitizer.rb
76
- - lib/tui_tui/text_view.rb
77
81
  - lib/tui_tui/theme.rb
78
- - lib/tui_tui/toast.rb
79
82
  - lib/tui_tui/version.rb
83
+ - lib/tui_tui/widget/list.rb
84
+ - lib/tui_tui/widget/progress.rb
85
+ - lib/tui_tui/widget/scroll_list.rb
86
+ - lib/tui_tui/widget/scrollbar.rb
87
+ - lib/tui_tui/widget/spinner.rb
88
+ - lib/tui_tui/widget/status_bar.rb
89
+ - lib/tui_tui/widget/table.rb
90
+ - lib/tui_tui/widget/tabs.rb
91
+ - lib/tui_tui/widget/text_view.rb
92
+ - lib/tui_tui/widget/toast.rb
80
93
  - lib/tui_tui/width.rb
81
94
  homepage: https://github.com/takahashim/tui_tui
82
95
  licenses:
@@ -100,7 +113,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
100
113
  - !ruby/object:Gem::Version
101
114
  version: '0'
102
115
  requirements: []
103
- rubygems_version: 3.6.9
116
+ rubygems_version: 4.0.10
104
117
  specification_version: 4
105
118
  summary: A tiny, dependency-free TUI runtime for modern terminals.
106
119
  test_files: []
@@ -1,190 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "display_text"
4
- require_relative "text_sanitizer"
5
- require_relative "style"
6
- require_relative "scroll_list"
7
- require_relative "list"
8
- require_relative "line"
9
- require_relative "span"
10
- require_relative "rect"
11
- require_relative "modal"
12
- require_relative "fuzzy"
13
- require_relative "key_code"
14
-
15
- module TuiTui
16
- # Fuzzy-filtered command palette modal (think Ctrl-P): type to narrow a list of
17
- # commands, arrows or Ctrl-N/Ctrl-P to move, Enter to pick, Esc to cancel.
18
- #
19
- # Items are arbitrary objects; pass a block to derive each one's display label
20
- # (defaults to #to_s). Resolves to the chosen item on Enter and :cancel on
21
- # escape; stays open (nil) while the query has no matches.
22
- #
23
- # host.open(CommandPalette.new(commands) { |c| c.title }) { |cmd| cmd.run; self }
24
- class CommandPalette < Modal
25
- MAX_ROWS = 10
26
- MIN_INNER = 28
27
- WHEEL = 3
28
-
29
- def initialize(items, prompt: "> ", placeholder: "Type to search…", theme: Theme::DEFAULT, &label)
30
- @items = items.to_a
31
- @label = label || :to_s.to_proc
32
- @prompt = DisplayText.new(prompt)
33
- @placeholder = DisplayText.new(placeholder)
34
- @theme = theme
35
- @graphemes = []
36
- @list = ScrollList.new(0)
37
- refilter
38
- end
39
-
40
- def query = @graphemes.join
41
-
42
- # The original item under the cursor, or nil when nothing matches.
43
- def selection = @filtered[@list.cursor]&.first
44
-
45
- def handle(key)
46
- case key
47
- when "\r"
48
- selection
49
- when :escape, KeyCode::CTRL_C
50
- :cancel
51
- when :up, KeyCode::CTRL_P
52
- move(-1)
53
- when :down, KeyCode::CTRL_N
54
- move(1)
55
- when :home
56
- move_to(0)
57
- when :end
58
- move_to(@list.last)
59
- when KeyCode::BACKSPACE, :backspace
60
- edit { @graphemes.pop }
61
- when String
62
- edit { @graphemes.concat(key.grapheme_clusters) if TextSanitizer.printable?(key) }
63
- end
64
- end
65
-
66
- # Wheel scrolls the highlight; a click on a row picks it (returns the item),
67
- # otherwise nil to stay open.
68
- def handle_mouse(event)
69
- case event.action
70
- when :wheel
71
- move(event.button == :wheel_up ? -WHEEL : WHEEL)
72
- when :press
73
- click(event)
74
- end
75
- end
76
-
77
- def draw(canvas, size)
78
- rows = visible_rows(size)
79
- inner = [MIN_INNER, *@filtered.map { |_item, label, _pos| label.width }].max
80
- rect, col = panel(canvas, inner: inner, body_rows: rows + 2)
81
-
82
- draw_query(canvas, rect.row + 1, col, inner)
83
- draw_items(canvas, rect.row + 3, col, inner, rows)
84
- canvas
85
- end
86
-
87
- private
88
-
89
- def move(delta)
90
- @list.move(delta)
91
- nil
92
- end
93
-
94
- def move_to(index)
95
- @list.go_to(index)
96
- nil
97
- end
98
-
99
- # Apply a query edit, then refilter. Returns nil so the modal stays open.
100
- def edit
101
- yield
102
- refilter
103
- nil
104
- end
105
-
106
- # Recompute the visible list: fuzzy-ranked (best first, with matched positions
107
- # for highlighting) while querying, otherwise the items in their given order.
108
- # Each entry is [item, DisplayText(label), positions]; the cursor resets so a
109
- # narrowed query always lands on the top match.
110
- def refilter
111
- @filtered =
112
- if @graphemes.empty?
113
- @items.map { |item| [item, label_text(item), []] }
114
- else
115
- Fuzzy.new(query).rank(@items) { |item| @label.call(item).to_s }
116
- .map { |item, found| [item, label_text(item), found.positions] }
117
- end
118
- @list.count = @filtered.size
119
- @list.go_to(0)
120
- end
121
-
122
- def label_text(item) = DisplayText.new(@label.call(item).to_s)
123
-
124
- def visible_rows(size)
125
- room = [size.rows - 4, 1].max
126
- [[@filtered.size, 1].max, MAX_ROWS, room].min
127
- end
128
-
129
- def draw_query(canvas, row, col, inner)
130
- canvas.text(row, col, @prompt, theme.accent)
131
- text_col = col + @prompt.width
132
- budget = inner - @prompt.width
133
- if @graphemes.empty?
134
- canvas.text(row, text_col, @placeholder.truncate(budget), theme.muted)
135
- else
136
- canvas.text(row, text_col, DisplayText.new(query).truncate(budget), theme.text)
137
- end
138
- end
139
-
140
- def draw_items(canvas, row, col, inner, rows)
141
- @items_rect = Rect.new(row: row, col: col, rows: rows, cols: inner)
142
- if @filtered.empty?
143
- canvas.text(row, col, DisplayText.new("No matches").truncate(inner), theme.muted)
144
- return
145
- end
146
-
147
- List.new(@list).draw(canvas, @items_rect, highlight: theme.selection) do |index, focused|
148
- _item, label, positions = @filtered[index]
149
- base = focused ? theme.selection : theme.text
150
- # Keep the focused row a single style; highlight matches with accent only
151
- # on unfocused rows so the selection bar stays legible.
152
- match = focused ? base : theme.accent
153
- styled_line(label.to_s, positions, base, match)
154
- end
155
- end
156
-
157
- # The label as a Line with matched graphemes in `match` and the rest in `base`;
158
- # runs of the same style coalesce into one Span (grapheme indices line up with
159
- # Fuzzy#positions).
160
- def styled_line(label, positions, base, match)
161
- return Line[Span[label, base]] if positions.empty?
162
-
163
- spans = []
164
- run = +""
165
- run_style = nil
166
- label.grapheme_clusters.each_with_index do |grapheme, i|
167
- style = positions.include?(i) ? match : base
168
- if style != run_style && !run.empty?
169
- spans << Span[run, run_style]
170
- run = +""
171
- end
172
- run_style = style
173
- run << grapheme
174
- end
175
- spans << Span[run, run_style] unless run.empty?
176
- Line.new(spans)
177
- end
178
-
179
- # The item under a click, picked, or nil if the click missed the list.
180
- def click(event)
181
- return nil unless @items_rect
182
-
183
- index = List.new(@list).index_at(@items_rect, event)
184
- return nil if index.nil?
185
-
186
- @list.go_to(index)
187
- selection
188
- end
189
- end
190
- end
@@ -1,74 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "display_text"
4
- require_relative "style"
5
- require_relative "modal"
6
- require_relative "key_code"
7
-
8
- module TuiTui
9
- # OK / Cancel confirmation modal.
10
- class Confirm < Modal
11
- GAP = 2
12
-
13
- attr_reader :focus
14
-
15
- def initialize(message, ok: "OK", cancel: "Cancel", default: :cancel, theme: Theme::DEFAULT)
16
- @message = DisplayText.new(message)
17
- @ok = button_text(ok)
18
- @cancel = button_text(cancel)
19
- @focus = default
20
- @theme = theme
21
- end
22
-
23
- def handle(key)
24
- case key
25
- when :left, :right, "\t", "h", "l"
26
- toggle
27
- when "\r", " "
28
- @focus
29
- when "y", "Y"
30
- :ok
31
- when "n", "N", :escape, KeyCode::CTRL_C
32
- :cancel
33
- end
34
- end
35
-
36
- def handle_mouse(event)
37
- return nil unless event.action == :press && @buttons_row == event.row
38
-
39
- return :ok if hit?(event.col, @ok_at, @ok.width)
40
- return :cancel if hit?(event.col, @cancel_at, @cancel.width)
41
-
42
- nil
43
- end
44
-
45
- def draw(canvas, size)
46
- inner = [@message.width, buttons_width].max
47
- rect, col = panel(canvas, inner: inner, body_rows: 3)
48
- canvas.text(rect.row + 1, col, @message.center(inner), theme.text)
49
- draw_buttons(canvas, rect.row + 3, col, inner)
50
- canvas
51
- end
52
-
53
- private
54
-
55
- def hit?(col, start, width) = col.between?(start, start + width - 1)
56
-
57
- def toggle
58
- @focus = @focus == :ok ? :cancel : :ok
59
- nil
60
- end
61
-
62
- def draw_buttons(canvas, row, col, inner)
63
- start = col + [(inner - buttons_width) / 2, 0].max
64
- @buttons_row = row
65
- @ok_at = start
66
- @cancel_at = start + @ok.width + GAP
67
- canvas.text(row, @ok_at, @ok, @focus == :ok ? theme.selection : theme.text)
68
- canvas.text(row, @cancel_at, @cancel, @focus == :cancel ? theme.selection : theme.text)
69
- end
70
-
71
- def buttons_width = @ok.width + GAP + @cancel.width
72
- def button_text(label) = DisplayText.new("[ #{label} ]")
73
- end
74
- end
data/lib/tui_tui/help.rb DELETED
@@ -1,44 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "display_text"
4
- require_relative "style"
5
- require_relative "modal"
6
-
7
- module TuiTui
8
- # Key-binding cheat sheet modal.
9
- class Help < Modal
10
- COLGAP = 2
11
-
12
- def initialize(title, entries, theme: Theme::DEFAULT)
13
- @title = DisplayText.new(title)
14
- @entries = entries.map { |keys, desc| [DisplayText.new(keys), DisplayText.new(desc)] }
15
- @theme = theme
16
- end
17
-
18
- def handle(_key) = :close
19
-
20
- # Any click dismisses the sheet, like any key does.
21
- def handle_mouse(event) = event.action == :press ? :close : nil
22
-
23
- def draw(canvas, size)
24
- key_w = @entries.map { |keys, _| keys.width }.max || 0
25
- body_w = @entries.map { |keys, desc| keys.width + COLGAP + desc.width }.max || 0
26
- inner = [@title.width, body_w].max
27
-
28
- rect, col = panel(canvas, inner: inner, body_rows: @entries.size + 2)
29
-
30
- canvas.text(rect.row + 1, col, @title.truncate(inner), theme.title)
31
- draw_entries(canvas, rect.row + 3, col, key_w)
32
- canvas
33
- end
34
-
35
- private
36
-
37
- def draw_entries(canvas, row, col, key_w)
38
- @entries.each_with_index do |(keys, desc), index|
39
- canvas.text(row + index, col, keys, theme.accent)
40
- canvas.text(row + index, col + key_w + COLGAP, desc, theme.muted)
41
- end
42
- end
43
- end
44
- end
data/lib/tui_tui/list.rb DELETED
@@ -1,57 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "rect"
4
- require_relative "line"
5
- require_relative "scrollbar"
6
-
7
- module TuiTui
8
- # Drawing companion for ScrollList.
9
- # Row content comes from the caller, keeping the list domain-agnostic.
10
- class List
11
- def initialize(scroll)
12
- @scroll = scroll
13
- end
14
-
15
- def draw(canvas, rect, highlight: nil, scrollbar: nil, auto: false)
16
- # With auto:, reserve the gutter only when the content overflows the rect.
17
- show_bar = scrollbar && !(auto && @scroll.count <= rect.rows)
18
- body, gutter = show_bar ? rect.split_gutter : [rect, nil]
19
- @scroll.ensure_visible(body.rows)
20
- @scroll.each_visible(body.rows) do |index, offset|
21
- row = body.row + offset
22
- selected = index == @scroll.cursor
23
- canvas.fill(Rect.new(row: row, col: body.col, rows: 1, cols: body.cols), highlight) if highlight && selected
24
- canvas.line(row, body.col, Line.coerce(yield(index, selected)).truncate(body.cols))
25
- end
26
-
27
- draw_scrollbar(canvas, gutter, scrollbar) if gutter
28
- canvas
29
- end
30
-
31
- # Map a MouseEvent to the list index under it, or nil. Pass the same `rect`
32
- # and `scrollbar:` used for `draw` so the gutter column is excluded and the
33
- # scroll offset matches what was rendered. Returns nil for clicks outside the
34
- # body or below the last item.
35
- def index_at(rect, event, scrollbar: nil)
36
- body = scrollbar ? rect.split_gutter.first : rect
37
- return nil unless body.hit?(event)
38
-
39
- index = @scroll.top + (event.row - body.row)
40
- index < @scroll.count ? index : nil
41
- end
42
-
43
- private
44
-
45
- def draw_scrollbar(canvas, gutter, theme)
46
- Scrollbar.draw(
47
- canvas,
48
- gutter,
49
- top: @scroll.top,
50
- visible: gutter.rows,
51
- total: @scroll.count,
52
- track_style: theme.scroll_track,
53
- thumb_style: theme.scroll_thumb
54
- )
55
- end
56
- end
57
- end