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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +60 -0
- data/README.md +51 -14
- data/examples/README.md +28 -0
- data/examples/breakout.rb +225 -0
- data/examples/counter.rb +11 -12
- data/examples/csv_viewer.rb +28 -50
- data/examples/downloads.rb +128 -0
- data/examples/file_browser.rb +64 -55
- data/examples/form.rb +80 -102
- data/examples/life.rb +1 -1
- data/examples/log_viewer.rb +118 -0
- data/examples/todo.rb +43 -64
- data/examples/widgets.rb +4 -4
- data/lib/tui_tui/box_chrome.rb +0 -9
- data/lib/tui_tui/box_chrome_resolver.rb +21 -0
- data/lib/tui_tui/canvas.rb +6 -55
- data/lib/tui_tui/canvas_compositor.rb +12 -10
- data/lib/tui_tui/canvas_renderer.rb +59 -0
- data/lib/tui_tui/cell.rb +4 -3
- data/lib/tui_tui/command.rb +21 -0
- data/lib/tui_tui/event_stream.rb +42 -7
- data/lib/tui_tui/key_reader.rb +1 -1
- data/lib/tui_tui/modal/command_palette.rb +193 -0
- data/lib/tui_tui/modal/confirm.rb +74 -0
- data/lib/tui_tui/modal/help.rb +44 -0
- data/lib/tui_tui/modal/pager.rb +106 -0
- data/lib/tui_tui/modal/prompt.rb +66 -0
- data/lib/tui_tui/modal/select.rb +103 -0
- data/lib/tui_tui/modal.rb +4 -0
- data/lib/tui_tui/modal_host.rb +2 -2
- data/lib/tui_tui/rect.rb +69 -0
- data/lib/tui_tui/runtime.rb +46 -23
- data/lib/tui_tui/screen.rb +7 -3
- data/lib/tui_tui/test_runtime.rb +71 -0
- data/lib/tui_tui/text_input.rb +168 -0
- data/lib/tui_tui/text_sanitizer.rb +3 -10
- data/lib/tui_tui/theme.rb +25 -27
- data/lib/tui_tui/version.rb +1 -1
- data/lib/tui_tui/widget/list.rb +59 -0
- data/lib/tui_tui/widget/progress.rb +33 -0
- data/lib/tui_tui/widget/scroll_list.rb +59 -0
- data/lib/tui_tui/widget/scrollbar.rb +43 -0
- data/lib/tui_tui/widget/spinner.rb +37 -0
- data/lib/tui_tui/widget/status_bar.rb +25 -0
- data/lib/tui_tui/widget/table.rb +81 -0
- data/lib/tui_tui/widget/tabs.rb +67 -0
- data/lib/tui_tui/widget/text_view.rb +47 -0
- data/lib/tui_tui/widget/toast.rb +83 -0
- data/lib/tui_tui.rb +23 -15
- metadata +28 -15
- data/lib/tui_tui/command_palette.rb +0 -190
- data/lib/tui_tui/confirm.rb +0 -74
- data/lib/tui_tui/help.rb +0 -44
- data/lib/tui_tui/list.rb +0 -57
- data/lib/tui_tui/pager.rb +0 -106
- data/lib/tui_tui/prompt.rb +0 -108
- data/lib/tui_tui/scroll_list.rb +0 -57
- data/lib/tui_tui/scrollbar.rb +0 -41
- data/lib/tui_tui/select.rb +0 -103
- data/lib/tui_tui/status_bar.rb +0 -23
- data/lib/tui_tui/text_view.rb +0 -45
- data/lib/tui_tui/toast.rb +0 -83
- /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/
|
|
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`,
|
|
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.
|
|
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/
|
|
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:
|
|
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
|
data/lib/tui_tui/confirm.rb
DELETED
|
@@ -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
|