tui_tui 0.2.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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +80 -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 +106 -56
  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/box_prober.rb +3 -4
  18. data/lib/tui_tui/canvas.rb +6 -55
  19. data/lib/tui_tui/canvas_compositor.rb +12 -10
  20. data/lib/tui_tui/canvas_renderer.rb +59 -0
  21. data/lib/tui_tui/cell.rb +4 -3
  22. data/lib/tui_tui/clock.rb +11 -0
  23. data/lib/tui_tui/command.rb +21 -0
  24. data/lib/tui_tui/event_stream.rb +42 -7
  25. data/lib/tui_tui/key_code.rb +3 -0
  26. data/lib/tui_tui/key_reader.rb +1 -1
  27. data/lib/tui_tui/line.rb +11 -0
  28. data/lib/tui_tui/modal/command_palette.rb +193 -0
  29. data/lib/tui_tui/modal/confirm.rb +74 -0
  30. data/lib/tui_tui/modal/help.rb +44 -0
  31. data/lib/tui_tui/modal/pager.rb +106 -0
  32. data/lib/tui_tui/modal/prompt.rb +66 -0
  33. data/lib/tui_tui/modal/select.rb +103 -0
  34. data/lib/tui_tui/modal.rb +4 -0
  35. data/lib/tui_tui/modal_host.rb +2 -2
  36. data/lib/tui_tui/rect.rb +69 -0
  37. data/lib/tui_tui/runtime.rb +52 -20
  38. data/lib/tui_tui/screen.rb +15 -3
  39. data/lib/tui_tui/terminal_session.rb +16 -1
  40. data/lib/tui_tui/test_runtime.rb +71 -0
  41. data/lib/tui_tui/text_input.rb +168 -0
  42. data/lib/tui_tui/text_sanitizer.rb +3 -2
  43. data/lib/tui_tui/theme.rb +25 -27
  44. data/lib/tui_tui/version.rb +1 -1
  45. data/lib/tui_tui/widget/list.rb +59 -0
  46. data/lib/tui_tui/widget/progress.rb +33 -0
  47. data/lib/tui_tui/widget/scroll_list.rb +59 -0
  48. data/lib/tui_tui/widget/scrollbar.rb +43 -0
  49. data/lib/tui_tui/widget/spinner.rb +37 -0
  50. data/lib/tui_tui/widget/status_bar.rb +25 -0
  51. data/lib/tui_tui/widget/table.rb +81 -0
  52. data/lib/tui_tui/widget/tabs.rb +67 -0
  53. data/lib/tui_tui/widget/text_view.rb +47 -0
  54. data/lib/tui_tui/widget/toast.rb +83 -0
  55. data/lib/tui_tui.rb +24 -14
  56. metadata +28 -13
  57. data/lib/tui_tui/confirm.rb +0 -74
  58. data/lib/tui_tui/help.rb +0 -44
  59. data/lib/tui_tui/list.rb +0 -59
  60. data/lib/tui_tui/pager.rb +0 -94
  61. data/lib/tui_tui/prompt.rb +0 -111
  62. data/lib/tui_tui/scroll_list.rb +0 -57
  63. data/lib/tui_tui/scrollbar.rb +0 -41
  64. data/lib/tui_tui/select.rb +0 -104
  65. data/lib/tui_tui/status_bar.rb +0 -23
  66. data/lib/tui_tui/text_view.rb +0 -56
  67. data/lib/tui_tui/toast.rb +0 -82
  68. /data/lib/tui_tui/{event.rb → events.rb} +0 -0
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../display_text"
4
+ require_relative "../theme"
5
+
6
+ module TuiTui
7
+ module Widget
8
+ # A one-row tab bar: labels rendered as ` label ` segments separated by a
9
+ # single space. The active tab uses theme.selection, the rest theme.muted.
10
+ # Stateless like StatusBar / Scrollbar — the active index lives in the app;
11
+ # `index_at` maps a mouse event back to the tab under it.
12
+ #
13
+ # Labels that overflow rect.cols are clipped at the right edge; there is no
14
+ # scrolling. Advanced handling for bars with more tabs than fit (scrolling,
15
+ # collapsing, overflow menus) is out of scope.
16
+ module Tabs
17
+ module_function
18
+
19
+ # Horizontal padding inside a tab, on each side of the label.
20
+ PAD = 1
21
+ # Columns between adjacent tabs.
22
+ GAP = 1
23
+
24
+ def draw(canvas, rect, labels, active:, theme: Theme::DEFAULT)
25
+ canvas.fill(rect, nil)
26
+ segments(labels).each_with_index do |(offset, _width, padded), index|
27
+ remaining = rect.cols - offset
28
+ break if remaining <= 0
29
+
30
+ style = index == active ? theme.selection : theme.muted
31
+ canvas.text(rect.row, rect.col + offset, padded.truncate(remaining, marker: ""), style)
32
+ end
33
+
34
+ canvas
35
+ end
36
+
37
+ # The tab index under a MouseEvent, or nil: outside the rect, past the
38
+ # visible (clipped) columns, or beyond the last tab. The single separator
39
+ # space between tabs belongs to no tab and also yields nil. Shares the
40
+ # segment layout with `draw`, so hits always match what was rendered.
41
+ def index_at(rect, event, labels)
42
+ return nil unless rect.hit?(event)
43
+
44
+ offset = event.col - rect.col
45
+ segments(labels).each_with_index do |(start, width, _padded), index|
46
+ return index if offset >= start && offset < start + width
47
+ end
48
+
49
+ nil
50
+ end
51
+
52
+ # The layout both draw and index_at share: for each label, its starting
53
+ # column offset (0-based, relative to rect.col), its rendered width, and
54
+ # the padded ` label ` DisplayText.
55
+ def segments(labels)
56
+ offset = 0
57
+ labels.map do |label|
58
+ padded = DisplayText.new(" " * PAD + label.to_s + " " * PAD)
59
+ width = padded.width
60
+ segment = [offset, width, padded]
61
+ offset += width + GAP
62
+ segment
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -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
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "tui_tui/version"
4
4
 
5
+ require_relative "tui_tui/clock"
5
6
  require_relative "tui_tui/width"
6
7
  require_relative "tui_tui/text_sanitizer"
7
8
  require_relative "tui_tui/display_text"
@@ -11,6 +12,7 @@ require_relative "tui_tui/ansi"
11
12
  require_relative "tui_tui/color_depth"
12
13
  require_relative "tui_tui/box_chrome"
13
14
  require_relative "tui_tui/box_prober"
15
+ require_relative "tui_tui/box_chrome_resolver"
14
16
  require_relative "tui_tui/palette"
15
17
  require_relative "tui_tui/style"
16
18
  require_relative "tui_tui/theme"
@@ -19,28 +21,36 @@ require_relative "tui_tui/rect"
19
21
  require_relative "tui_tui/cell"
20
22
  require_relative "tui_tui/canvas"
21
23
  require_relative "tui_tui/render_context"
24
+ require_relative "tui_tui/canvas_renderer"
22
25
  require_relative "tui_tui/canvas_compositor"
23
- require_relative "tui_tui/event"
26
+ require_relative "tui_tui/events"
27
+ require_relative "tui_tui/command"
24
28
  require_relative "tui_tui/key_code"
29
+ require_relative "tui_tui/text_input"
25
30
  require_relative "tui_tui/key_reader"
26
31
  require_relative "tui_tui/key_intent"
27
32
  require_relative "tui_tui/event_stream"
28
33
  require_relative "tui_tui/terminal_session"
29
- require_relative "tui_tui/scroll_list"
30
- require_relative "tui_tui/list"
31
- require_relative "tui_tui/text_view"
32
- require_relative "tui_tui/scrollbar"
33
- require_relative "tui_tui/status_bar"
34
- 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"
35
44
  require_relative "tui_tui/focus_ring"
36
45
  require_relative "tui_tui/fuzzy"
37
46
  require_relative "tui_tui/modal"
38
47
  require_relative "tui_tui/modal_host"
39
- require_relative "tui_tui/confirm"
40
- require_relative "tui_tui/select"
41
- require_relative "tui_tui/help"
42
- require_relative "tui_tui/prompt"
43
- 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"
44
54
  require_relative "tui_tui/screen"
45
55
  require_relative "tui_tui/runtime"
46
56
 
@@ -49,7 +59,7 @@ require_relative "tui_tui/runtime"
49
59
  # pieces — width, color, style, layout — need nothing.
50
60
  #
51
61
  # The framework is application-agnostic. Build an app object responding to
52
- # `view(size) -> Canvas` and `update(event) -> app | :quit`, then drive it with
53
- # `TuiTui::Runtime`.
62
+ # `view(size) -> Canvas` and `update(event) -> app | [app, *commands] | :quit`,
63
+ # then drive it with `TuiTui::Runtime`.
54
64
  module TuiTui
55
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.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - takahashim
@@ -23,58 +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
48
+ - lib/tui_tui/clock.rb
42
49
  - lib/tui_tui/color_depth.rb
43
- - lib/tui_tui/confirm.rb
50
+ - lib/tui_tui/command.rb
44
51
  - lib/tui_tui/display_text.rb
45
- - lib/tui_tui/event.rb
46
52
  - lib/tui_tui/event_stream.rb
53
+ - lib/tui_tui/events.rb
47
54
  - lib/tui_tui/focus_ring.rb
48
55
  - lib/tui_tui/fuzzy.rb
49
- - lib/tui_tui/help.rb
50
56
  - lib/tui_tui/key_code.rb
51
57
  - lib/tui_tui/key_intent.rb
52
58
  - lib/tui_tui/key_reader.rb
53
59
  - lib/tui_tui/line.rb
54
- - lib/tui_tui/list.rb
55
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
56
67
  - lib/tui_tui/modal_host.rb
57
- - lib/tui_tui/pager.rb
58
68
  - lib/tui_tui/palette.rb
59
- - lib/tui_tui/prompt.rb
60
69
  - lib/tui_tui/rect.rb
61
70
  - lib/tui_tui/render_context.rb
62
71
  - lib/tui_tui/runtime.rb
63
72
  - lib/tui_tui/screen.rb
64
- - lib/tui_tui/scroll_list.rb
65
- - lib/tui_tui/scrollbar.rb
66
- - lib/tui_tui/select.rb
67
73
  - lib/tui_tui/size.rb
68
74
  - lib/tui_tui/span.rb
69
- - lib/tui_tui/status_bar.rb
70
75
  - lib/tui_tui/style.rb
71
76
  - lib/tui_tui/terminal_session.rb
72
77
  - lib/tui_tui/terminal_size.rb
78
+ - lib/tui_tui/test_runtime.rb
79
+ - lib/tui_tui/text_input.rb
73
80
  - lib/tui_tui/text_sanitizer.rb
74
- - lib/tui_tui/text_view.rb
75
81
  - lib/tui_tui/theme.rb
76
- - lib/tui_tui/toast.rb
77
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
78
93
  - lib/tui_tui/width.rb
79
94
  homepage: https://github.com/takahashim/tui_tui
80
95
  licenses:
@@ -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,59 +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, as_line(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
-
57
- def as_line(content) = content.is_a?(Line) ? content : Line.new(Array(content))
58
- end
59
- end
data/lib/tui_tui/pager.rb DELETED
@@ -1,94 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "display_text"
4
- require_relative "style"
5
- require_relative "rect"
6
- require_relative "modal"
7
- require_relative "key_intent"
8
-
9
- module TuiTui
10
- # Scrollable read-only text modal.
11
- class Pager < Modal
12
- MARGIN = 2
13
- WHEEL = 3
14
-
15
- def initialize(title, lines, start: 0, close_keys: [], theme: Theme::DEFAULT)
16
- @title = title
17
- @lines = lines.map { |line| DisplayText.new(line) }
18
- @top = start
19
- @page = 1
20
- @close_keys = close_keys
21
- @theme = theme
22
- end
23
-
24
- def handle(key)
25
- return :close if @close_keys.include?(key)
26
-
27
- case KeyIntent.for(key)
28
- when :up
29
- scroll(-1)
30
- when :down
31
- scroll(1)
32
- when :top
33
- scroll(-@lines.size)
34
- when :bottom
35
- scroll(@lines.size)
36
- when :cancel
37
- :close
38
- else
39
- paginate(key)
40
- end
41
- end
42
-
43
- def handle_mouse(event)
44
- scroll(event.button == :wheel_up ? -WHEEL : WHEEL) if event.action == :wheel
45
- end
46
-
47
- def draw(canvas, size)
48
- width = [size.cols - (MARGIN * 2), 20].max
49
- height = [size.rows - (MARGIN * 2), 5].max
50
- rect = Rect.centered(size, cols: width, rows: height)
51
- canvas.frame(rect, style: theme.frame)
52
-
53
- inner = width - 4
54
- body = [height - 4, 1].max
55
- @page = body
56
- clamp(body)
57
-
58
- canvas.text(rect.row + 1, rect.col + 2, DisplayText.new(title_line(body)).truncate(inner), theme.title)
59
- body.times do |offset|
60
- line = @lines[@top + offset]
61
- next if line.nil?
62
-
63
- canvas.text(rect.row + 3 + offset, rect.col + 2, line.truncate(inner), theme.muted)
64
- end
65
-
66
- canvas
67
- end
68
-
69
- private
70
-
71
- def paginate(key)
72
- case key
73
- when " ", :pgdn
74
- scroll(@page)
75
- when "b", :pgup
76
- scroll(-@page)
77
- end
78
- end
79
-
80
- def scroll(delta)
81
- @top += delta
82
- nil
83
- end
84
-
85
- def clamp(body)
86
- @top = @top.clamp(0, [@lines.size - body, 0].max)
87
- end
88
-
89
- def title_line(body)
90
- last = [@top + body, @lines.size].min
91
- "#{@title} (#{@top + 1}-#{last}/#{@lines.size})"
92
- end
93
- end
94
- end