ratatat 1.0.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 +7 -0
- data/README.md +201 -0
- data/examples/log_tailer.rb +460 -0
- data/lib/ratatat/ansi_backend.rb +175 -0
- data/lib/ratatat/app.rb +342 -0
- data/lib/ratatat/binding.rb +74 -0
- data/lib/ratatat/buffer.rb +238 -0
- data/lib/ratatat/cell.rb +166 -0
- data/lib/ratatat/color.rb +191 -0
- data/lib/ratatat/css_parser.rb +192 -0
- data/lib/ratatat/dom_query.rb +124 -0
- data/lib/ratatat/driver.rb +200 -0
- data/lib/ratatat/input.rb +208 -0
- data/lib/ratatat/message.rb +147 -0
- data/lib/ratatat/reactive.rb +79 -0
- data/lib/ratatat/styles.rb +293 -0
- data/lib/ratatat/terminal.rb +168 -0
- data/lib/ratatat/version.rb +3 -0
- data/lib/ratatat/widget.rb +337 -0
- data/lib/ratatat/widgets/button.rb +43 -0
- data/lib/ratatat/widgets/checkbox.rb +68 -0
- data/lib/ratatat/widgets/container.rb +50 -0
- data/lib/ratatat/widgets/data_table.rb +123 -0
- data/lib/ratatat/widgets/grid.rb +40 -0
- data/lib/ratatat/widgets/horizontal.rb +52 -0
- data/lib/ratatat/widgets/log.rb +97 -0
- data/lib/ratatat/widgets/modal.rb +161 -0
- data/lib/ratatat/widgets/progress_bar.rb +80 -0
- data/lib/ratatat/widgets/radio_set.rb +91 -0
- data/lib/ratatat/widgets/scrollable_container.rb +88 -0
- data/lib/ratatat/widgets/select.rb +100 -0
- data/lib/ratatat/widgets/sparkline.rb +61 -0
- data/lib/ratatat/widgets/spinner.rb +93 -0
- data/lib/ratatat/widgets/static.rb +23 -0
- data/lib/ratatat/widgets/tabbed_content.rb +114 -0
- data/lib/ratatat/widgets/text_area.rb +183 -0
- data/lib/ratatat/widgets/text_input.rb +143 -0
- data/lib/ratatat/widgets/toast.rb +55 -0
- data/lib/ratatat/widgets/tooltip.rb +66 -0
- data/lib/ratatat/widgets/tree.rb +172 -0
- data/lib/ratatat/widgets/vertical.rb +52 -0
- data/lib/ratatat.rb +51 -0
- metadata +142 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Ratatat
|
|
5
|
+
# Animated loading spinner widget
|
|
6
|
+
class Spinner < Widget
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
9
|
+
STYLES = T.let({
|
|
10
|
+
dots: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
|
|
11
|
+
line: ["-", "\\", "|", "/"],
|
|
12
|
+
blocks: ["▏", "▎", "▍", "▌", "▋", "▊", "▉", "█"],
|
|
13
|
+
arrows: ["←", "↖", "↑", "↗", "→", "↘", "↓", "↙"],
|
|
14
|
+
bounce: ["⠁", "⠂", "⠄", "⠂"],
|
|
15
|
+
pulse: ["◐", "◓", "◑", "◒"]
|
|
16
|
+
}.freeze, T::Hash[Symbol, T::Array[String]])
|
|
17
|
+
|
|
18
|
+
DEFAULT_FRAMES = T.let(STYLES[:dots] || [], T::Array[String])
|
|
19
|
+
|
|
20
|
+
sig { returns(T::Array[String]) }
|
|
21
|
+
attr_reader :frames
|
|
22
|
+
|
|
23
|
+
sig { returns(Float) }
|
|
24
|
+
attr_reader :speed
|
|
25
|
+
|
|
26
|
+
sig { returns(T.nilable(String)) }
|
|
27
|
+
attr_reader :text
|
|
28
|
+
|
|
29
|
+
reactive :frame_index, default: 0, repaint: true
|
|
30
|
+
|
|
31
|
+
sig { params(frames: T.nilable(T::Array[String]), style: T.nilable(Symbol), speed: Float, text: T.nilable(String), id: T.nilable(String), classes: T::Array[String]).void }
|
|
32
|
+
def initialize(frames: nil, style: nil, speed: 0.1, text: nil, id: nil, classes: [])
|
|
33
|
+
super(id: id, classes: classes)
|
|
34
|
+
@frames = T.let(
|
|
35
|
+
frames || (style ? (STYLES[style] || DEFAULT_FRAMES) : DEFAULT_FRAMES),
|
|
36
|
+
T::Array[String]
|
|
37
|
+
)
|
|
38
|
+
@speed = speed
|
|
39
|
+
@text = text
|
|
40
|
+
@frame_index = 0
|
|
41
|
+
@spinning = T.let(false, T::Boolean)
|
|
42
|
+
@timer_id = T.let(nil, T.nilable(Integer))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
sig { returns(String) }
|
|
46
|
+
def current_frame
|
|
47
|
+
@frames[@frame_index] || @frames.first || " "
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
sig { void }
|
|
51
|
+
def advance
|
|
52
|
+
@frame_index = (@frame_index + 1) % @frames.length
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
sig { returns(T::Boolean) }
|
|
56
|
+
def spinning?
|
|
57
|
+
@spinning
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
sig { void }
|
|
61
|
+
def start
|
|
62
|
+
return if @spinning
|
|
63
|
+
|
|
64
|
+
@spinning = true
|
|
65
|
+
app_instance = app
|
|
66
|
+
return unless app_instance
|
|
67
|
+
|
|
68
|
+
@timer_id = app_instance.set_interval(@speed) { advance }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
sig { void }
|
|
72
|
+
def stop
|
|
73
|
+
return unless @spinning
|
|
74
|
+
|
|
75
|
+
@spinning = false
|
|
76
|
+
app_instance = app
|
|
77
|
+
return unless app_instance || @timer_id.nil?
|
|
78
|
+
|
|
79
|
+
app_instance&.cancel_timer(T.must(@timer_id)) if @timer_id
|
|
80
|
+
@timer_id = nil
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
sig { params(buffer: Buffer, x: Integer, y: Integer, width: Integer, height: Integer).void }
|
|
84
|
+
def render(buffer, x:, y:, width:, height:)
|
|
85
|
+
output = if @text
|
|
86
|
+
"#{current_frame} #{@text}"
|
|
87
|
+
else
|
|
88
|
+
current_frame
|
|
89
|
+
end
|
|
90
|
+
buffer.put_string(x, y, output[0, width] || "")
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Ratatat
|
|
5
|
+
# Simple text display widget
|
|
6
|
+
class Static < Widget
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
9
|
+
reactive :text, default: "", repaint: true
|
|
10
|
+
|
|
11
|
+
sig { params(text: String, id: T.nilable(String), classes: T::Array[String], styles: T.nilable(T::Hash[Symbol, T.untyped])).void }
|
|
12
|
+
def initialize(text = "", id: nil, classes: [], styles: nil)
|
|
13
|
+
super(id: id, classes: classes, styles: styles)
|
|
14
|
+
@text = text
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
sig { params(buffer: Buffer, x: Integer, y: Integer, width: Integer, height: Integer).void }
|
|
18
|
+
def render(buffer, x:, y:, width:, height:)
|
|
19
|
+
display_text = text[0, width] || ""
|
|
20
|
+
buffer.put_string(x, y, display_text)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Ratatat
|
|
5
|
+
# Tabbed content container with switchable panes
|
|
6
|
+
class TabbedContent < Widget
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
9
|
+
CAN_FOCUS = true
|
|
10
|
+
|
|
11
|
+
class TabChanged < Message
|
|
12
|
+
extend T::Sig
|
|
13
|
+
|
|
14
|
+
sig { returns(Integer) }
|
|
15
|
+
attr_reader :index
|
|
16
|
+
|
|
17
|
+
sig { returns(String) }
|
|
18
|
+
attr_reader :label
|
|
19
|
+
|
|
20
|
+
sig { params(sender: Widget, index: Integer, label: String).void }
|
|
21
|
+
def initialize(sender:, index:, label:)
|
|
22
|
+
super(sender: sender)
|
|
23
|
+
@index = index
|
|
24
|
+
@label = label
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
sig { returns(T::Array[String]) }
|
|
29
|
+
attr_reader :labels
|
|
30
|
+
|
|
31
|
+
reactive :active_tab, default: 0, repaint: true
|
|
32
|
+
|
|
33
|
+
sig { params(id: T.nilable(String), classes: T::Array[String]).void }
|
|
34
|
+
def initialize(id: nil, classes: [])
|
|
35
|
+
super(id: id, classes: classes)
|
|
36
|
+
@labels = T.let([], T::Array[String])
|
|
37
|
+
@panes = T.let([], T::Array[Widget])
|
|
38
|
+
@active_tab = 0
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
sig { params(label: String, content: Widget).void }
|
|
42
|
+
def add_tab(label, content)
|
|
43
|
+
@labels << label
|
|
44
|
+
@panes << content
|
|
45
|
+
mount(content)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
sig { returns(Integer) }
|
|
49
|
+
def tab_count
|
|
50
|
+
@labels.length
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
sig { returns(T.nilable(String)) }
|
|
54
|
+
def active_label
|
|
55
|
+
@labels[@active_tab]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
sig { returns(T.nilable(Widget)) }
|
|
59
|
+
def active_pane
|
|
60
|
+
@panes[@active_tab]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
sig { params(message: Key).void }
|
|
64
|
+
def on_key(message)
|
|
65
|
+
case message.key
|
|
66
|
+
when "left", "shift+tab"
|
|
67
|
+
switch_tab(-1)
|
|
68
|
+
message.stop
|
|
69
|
+
when "right", "tab"
|
|
70
|
+
switch_tab(1)
|
|
71
|
+
message.stop
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
sig { params(buffer: Buffer, x: Integer, y: Integer, width: Integer, height: Integer).void }
|
|
76
|
+
def render(buffer, x:, y:, width:, height:)
|
|
77
|
+
return if height < 2
|
|
78
|
+
|
|
79
|
+
# Render tab bar
|
|
80
|
+
render_tab_bar(buffer, x, y, width)
|
|
81
|
+
|
|
82
|
+
# Render active pane content
|
|
83
|
+
pane = active_pane
|
|
84
|
+
pane&.render(buffer, x: x, y: y + 1, width: width, height: height - 1) if pane.respond_to?(:render)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
sig { params(delta: Integer).void }
|
|
90
|
+
def switch_tab(delta)
|
|
91
|
+
return if @labels.empty?
|
|
92
|
+
|
|
93
|
+
old_tab = @active_tab
|
|
94
|
+
@active_tab = (@active_tab + delta) % @labels.length
|
|
95
|
+
return if old_tab == @active_tab
|
|
96
|
+
|
|
97
|
+
parent&.dispatch(TabChanged.new(sender: self, index: @active_tab, label: active_label || ""))
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
sig { params(buffer: Buffer, x: Integer, y: Integer, width: Integer).void }
|
|
101
|
+
def render_tab_bar(buffer, x, y, width)
|
|
102
|
+
pos = x
|
|
103
|
+
@labels.each_with_index do |label, i|
|
|
104
|
+
break if pos >= x + width
|
|
105
|
+
|
|
106
|
+
prefix = i == @active_tab ? "[" : " "
|
|
107
|
+
suffix = i == @active_tab ? "]" : " "
|
|
108
|
+
text = "#{prefix}#{label}#{suffix}"
|
|
109
|
+
buffer.put_string(pos, y, text[0, width - (pos - x)])
|
|
110
|
+
pos += text.length + 1
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Ratatat
|
|
5
|
+
# Multi-line text input widget
|
|
6
|
+
class TextArea < Widget
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
9
|
+
CAN_FOCUS = true
|
|
10
|
+
|
|
11
|
+
class Changed < Message
|
|
12
|
+
extend T::Sig
|
|
13
|
+
|
|
14
|
+
sig { returns(String) }
|
|
15
|
+
attr_reader :value
|
|
16
|
+
|
|
17
|
+
sig { params(sender: Widget, value: String).void }
|
|
18
|
+
def initialize(sender:, value:)
|
|
19
|
+
super(sender: sender)
|
|
20
|
+
@value = value
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
reactive :value, default: "", repaint: true
|
|
25
|
+
|
|
26
|
+
sig { returns(Integer) }
|
|
27
|
+
attr_reader :cursor_row, :cursor_col
|
|
28
|
+
|
|
29
|
+
sig { params(value: String, id: T.nilable(String), classes: T::Array[String]).void }
|
|
30
|
+
def initialize(value: "", id: nil, classes: [])
|
|
31
|
+
super(id: id, classes: classes)
|
|
32
|
+
@value = value
|
|
33
|
+
@cursor_row = T.let(0, Integer)
|
|
34
|
+
@cursor_col = T.let(value.split("\n").first&.length || 0, Integer)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
sig { returns(T::Array[String]) }
|
|
38
|
+
def lines
|
|
39
|
+
@value.split("\n", -1)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
sig { params(message: Key).void }
|
|
43
|
+
def on_key(message)
|
|
44
|
+
case message.key
|
|
45
|
+
when "up"
|
|
46
|
+
move_cursor_vertical(-1)
|
|
47
|
+
message.stop
|
|
48
|
+
when "down"
|
|
49
|
+
move_cursor_vertical(1)
|
|
50
|
+
message.stop
|
|
51
|
+
when "left"
|
|
52
|
+
move_cursor_horizontal(-1)
|
|
53
|
+
message.stop
|
|
54
|
+
when "right"
|
|
55
|
+
move_cursor_horizontal(1)
|
|
56
|
+
message.stop
|
|
57
|
+
when "home"
|
|
58
|
+
@cursor_col = 0
|
|
59
|
+
message.stop
|
|
60
|
+
when "end"
|
|
61
|
+
@cursor_col = current_line.length
|
|
62
|
+
message.stop
|
|
63
|
+
when "enter"
|
|
64
|
+
insert_newline
|
|
65
|
+
message.stop
|
|
66
|
+
when "backspace"
|
|
67
|
+
delete_backward
|
|
68
|
+
message.stop
|
|
69
|
+
when "delete"
|
|
70
|
+
delete_forward
|
|
71
|
+
message.stop
|
|
72
|
+
else
|
|
73
|
+
insert_char(message.key) if message.key.length == 1
|
|
74
|
+
message.stop
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
sig { params(buffer: Buffer, x: Integer, y: Integer, width: Integer, height: Integer).void }
|
|
79
|
+
def render(buffer, x:, y:, width:, height:)
|
|
80
|
+
lines.each_with_index do |line, i|
|
|
81
|
+
break if i >= height
|
|
82
|
+
|
|
83
|
+
buffer.put_string(x, y + i, line[0, width] || "")
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
sig { returns(String) }
|
|
90
|
+
def current_line
|
|
91
|
+
lines[@cursor_row] || ""
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
sig { params(delta: Integer).void }
|
|
95
|
+
def move_cursor_vertical(delta)
|
|
96
|
+
new_row = (@cursor_row + delta).clamp(0, [lines.length - 1, 0].max)
|
|
97
|
+
@cursor_row = new_row
|
|
98
|
+
@cursor_col = [@cursor_col, current_line.length].min
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
sig { params(delta: Integer).void }
|
|
102
|
+
def move_cursor_horizontal(delta)
|
|
103
|
+
@cursor_col = (@cursor_col + delta).clamp(0, current_line.length)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
sig { params(char: String).void }
|
|
107
|
+
def insert_char(char)
|
|
108
|
+
current_lines = lines
|
|
109
|
+
line = current_lines[@cursor_row] || ""
|
|
110
|
+
new_line = line.dup
|
|
111
|
+
new_line.insert(@cursor_col, char)
|
|
112
|
+
current_lines[@cursor_row] = new_line
|
|
113
|
+
@cursor_col += 1
|
|
114
|
+
self.value = current_lines.join("\n")
|
|
115
|
+
emit_changed
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
sig { void }
|
|
119
|
+
def insert_newline
|
|
120
|
+
current_lines = lines
|
|
121
|
+
line = current_lines[@cursor_row] || ""
|
|
122
|
+
before = line[0, @cursor_col] || ""
|
|
123
|
+
after = line[@cursor_col..] || ""
|
|
124
|
+
current_lines[@cursor_row] = before
|
|
125
|
+
current_lines.insert(@cursor_row + 1, after)
|
|
126
|
+
@cursor_row += 1
|
|
127
|
+
@cursor_col = 0
|
|
128
|
+
self.value = current_lines.join("\n")
|
|
129
|
+
emit_changed
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
sig { void }
|
|
133
|
+
def delete_backward
|
|
134
|
+
if @cursor_col > 0
|
|
135
|
+
current_lines = lines
|
|
136
|
+
line = current_lines[@cursor_row] || ""
|
|
137
|
+
new_line = line.dup
|
|
138
|
+
new_line.slice!(@cursor_col - 1)
|
|
139
|
+
current_lines[@cursor_row] = new_line
|
|
140
|
+
@cursor_col -= 1
|
|
141
|
+
self.value = current_lines.join("\n")
|
|
142
|
+
emit_changed
|
|
143
|
+
elsif @cursor_row > 0
|
|
144
|
+
# Merge with previous line
|
|
145
|
+
current_lines = lines
|
|
146
|
+
prev_line = current_lines[@cursor_row - 1] || ""
|
|
147
|
+
curr_line = current_lines[@cursor_row] || ""
|
|
148
|
+
@cursor_col = prev_line.length
|
|
149
|
+
current_lines[@cursor_row - 1] = prev_line + curr_line
|
|
150
|
+
current_lines.delete_at(@cursor_row)
|
|
151
|
+
@cursor_row -= 1
|
|
152
|
+
self.value = current_lines.join("\n")
|
|
153
|
+
emit_changed
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
sig { void }
|
|
158
|
+
def delete_forward
|
|
159
|
+
current_lines = lines
|
|
160
|
+
line = current_lines[@cursor_row] || ""
|
|
161
|
+
|
|
162
|
+
if @cursor_col < line.length
|
|
163
|
+
new_line = line.dup
|
|
164
|
+
new_line.slice!(@cursor_col)
|
|
165
|
+
current_lines[@cursor_row] = new_line
|
|
166
|
+
self.value = current_lines.join("\n")
|
|
167
|
+
emit_changed
|
|
168
|
+
elsif @cursor_row < current_lines.length - 1
|
|
169
|
+
# Merge with next line
|
|
170
|
+
next_line = current_lines[@cursor_row + 1] || ""
|
|
171
|
+
current_lines[@cursor_row] = line + next_line
|
|
172
|
+
current_lines.delete_at(@cursor_row + 1)
|
|
173
|
+
self.value = current_lines.join("\n")
|
|
174
|
+
emit_changed
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
sig { void }
|
|
179
|
+
def emit_changed
|
|
180
|
+
parent&.dispatch(Changed.new(sender: self, value: @value))
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Ratatat
|
|
5
|
+
# Single-line text input widget
|
|
6
|
+
class TextInput < Widget
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
9
|
+
CAN_FOCUS = true
|
|
10
|
+
|
|
11
|
+
# Emitted when value changes
|
|
12
|
+
class Changed < Message
|
|
13
|
+
extend T::Sig
|
|
14
|
+
|
|
15
|
+
sig { returns(String) }
|
|
16
|
+
attr_reader :value
|
|
17
|
+
|
|
18
|
+
sig { params(sender: Widget, value: String).void }
|
|
19
|
+
def initialize(sender:, value:)
|
|
20
|
+
super(sender: sender)
|
|
21
|
+
@value = value
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Emitted when Enter is pressed
|
|
26
|
+
class Submitted < Message
|
|
27
|
+
extend T::Sig
|
|
28
|
+
|
|
29
|
+
sig { returns(String) }
|
|
30
|
+
attr_reader :value
|
|
31
|
+
|
|
32
|
+
sig { params(sender: Widget, value: String).void }
|
|
33
|
+
def initialize(sender:, value:)
|
|
34
|
+
super(sender: sender)
|
|
35
|
+
@value = value
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
reactive :value, default: "", repaint: true
|
|
40
|
+
reactive :placeholder, default: "", repaint: true
|
|
41
|
+
|
|
42
|
+
sig { returns(Integer) }
|
|
43
|
+
attr_reader :cursor
|
|
44
|
+
|
|
45
|
+
sig do
|
|
46
|
+
params(
|
|
47
|
+
value: String,
|
|
48
|
+
placeholder: String,
|
|
49
|
+
id: T.nilable(String),
|
|
50
|
+
classes: T::Array[String]
|
|
51
|
+
).void
|
|
52
|
+
end
|
|
53
|
+
def initialize(value: "", placeholder: "", id: nil, classes: [])
|
|
54
|
+
super(id: id, classes: classes)
|
|
55
|
+
@value = value
|
|
56
|
+
@placeholder = placeholder
|
|
57
|
+
@cursor = T.let(value.length, Integer)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
sig { params(message: Key).void }
|
|
61
|
+
def on_key(message)
|
|
62
|
+
case message.key
|
|
63
|
+
when "backspace"
|
|
64
|
+
delete_backward
|
|
65
|
+
message.stop
|
|
66
|
+
when "delete"
|
|
67
|
+
delete_forward
|
|
68
|
+
message.stop
|
|
69
|
+
when "left"
|
|
70
|
+
move_cursor(-1)
|
|
71
|
+
message.stop
|
|
72
|
+
when "right"
|
|
73
|
+
move_cursor(1)
|
|
74
|
+
message.stop
|
|
75
|
+
when "home"
|
|
76
|
+
@cursor = 0
|
|
77
|
+
message.stop
|
|
78
|
+
when "end"
|
|
79
|
+
@cursor = value.length
|
|
80
|
+
message.stop
|
|
81
|
+
when "enter"
|
|
82
|
+
submit
|
|
83
|
+
message.stop
|
|
84
|
+
else
|
|
85
|
+
insert_char(message.key) if message.key.length == 1
|
|
86
|
+
message.stop
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
sig { params(buffer: Buffer, x: Integer, y: Integer, width: Integer, height: Integer).void }
|
|
91
|
+
def render(buffer, x:, y:, width:, height:)
|
|
92
|
+
display = value.empty? ? placeholder : value
|
|
93
|
+
buffer.put_string(x, y, display[0, width] || "")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
sig { params(char: String).void }
|
|
99
|
+
def insert_char(char)
|
|
100
|
+
new_value = value.dup
|
|
101
|
+
new_value.insert(@cursor, char)
|
|
102
|
+
@cursor += 1
|
|
103
|
+
self.value = new_value
|
|
104
|
+
emit_changed
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
sig { void }
|
|
108
|
+
def delete_backward
|
|
109
|
+
return if @cursor == 0
|
|
110
|
+
|
|
111
|
+
new_value = value.dup
|
|
112
|
+
new_value.slice!(@cursor - 1)
|
|
113
|
+
@cursor -= 1
|
|
114
|
+
self.value = new_value
|
|
115
|
+
emit_changed
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
sig { void }
|
|
119
|
+
def delete_forward
|
|
120
|
+
return if @cursor >= value.length
|
|
121
|
+
|
|
122
|
+
new_value = value.dup
|
|
123
|
+
new_value.slice!(@cursor)
|
|
124
|
+
self.value = new_value
|
|
125
|
+
emit_changed
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
sig { params(delta: Integer).void }
|
|
129
|
+
def move_cursor(delta)
|
|
130
|
+
@cursor = (@cursor + delta).clamp(0, value.length)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
sig { void }
|
|
134
|
+
def emit_changed
|
|
135
|
+
parent&.dispatch(Changed.new(sender: self, value: value))
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
sig { void }
|
|
139
|
+
def submit
|
|
140
|
+
parent&.dispatch(Submitted.new(sender: self, value: value))
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Ratatat
|
|
5
|
+
# Toast notification widget
|
|
6
|
+
class Toast < Widget
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
9
|
+
ICONS = T.let({
|
|
10
|
+
info: "ℹ",
|
|
11
|
+
success: "✓",
|
|
12
|
+
warning: "⚠",
|
|
13
|
+
error: "✗"
|
|
14
|
+
}.freeze, T::Hash[Symbol, String])
|
|
15
|
+
|
|
16
|
+
sig { returns(String) }
|
|
17
|
+
attr_reader :message
|
|
18
|
+
|
|
19
|
+
sig { returns(Symbol) }
|
|
20
|
+
attr_reader :severity
|
|
21
|
+
|
|
22
|
+
sig { returns(Float) }
|
|
23
|
+
attr_reader :duration
|
|
24
|
+
|
|
25
|
+
reactive :visible, default: true, repaint: true
|
|
26
|
+
|
|
27
|
+
sig { params(message: String, severity: Symbol, duration: Float, id: T.nilable(String), classes: T::Array[String]).void }
|
|
28
|
+
def initialize(message:, severity: :info, duration: 5.0, id: nil, classes: [])
|
|
29
|
+
super(id: id, classes: classes)
|
|
30
|
+
@message = message
|
|
31
|
+
@severity = severity
|
|
32
|
+
@duration = duration
|
|
33
|
+
@visible = true
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
sig { void }
|
|
37
|
+
def show
|
|
38
|
+
@visible = true
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
sig { void }
|
|
42
|
+
def hide
|
|
43
|
+
@visible = false
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
sig { params(buffer: Buffer, x: Integer, y: Integer, width: Integer, height: Integer).void }
|
|
47
|
+
def render(buffer, x:, y:, width:, height:)
|
|
48
|
+
return unless @visible
|
|
49
|
+
|
|
50
|
+
icon = ICONS[@severity] || ICONS[:info]
|
|
51
|
+
text = "#{icon} #{@message}"
|
|
52
|
+
buffer.put_string(x, y, text[0, width] || "")
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Ratatat
|
|
5
|
+
# Floating tooltip widget
|
|
6
|
+
class Tooltip < Widget
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
9
|
+
sig { returns(String) }
|
|
10
|
+
attr_reader :text
|
|
11
|
+
|
|
12
|
+
sig { returns(Integer) }
|
|
13
|
+
attr_reader :anchor_x, :anchor_y
|
|
14
|
+
|
|
15
|
+
reactive :visible, default: false, repaint: true
|
|
16
|
+
|
|
17
|
+
sig { params(text: String, anchor_x: Integer, anchor_y: Integer, id: T.nilable(String), classes: T::Array[String]).void }
|
|
18
|
+
def initialize(text:, anchor_x: 0, anchor_y: 0, id: nil, classes: [])
|
|
19
|
+
super(id: id, classes: classes)
|
|
20
|
+
@text = text
|
|
21
|
+
@anchor_x = anchor_x
|
|
22
|
+
@anchor_y = anchor_y
|
|
23
|
+
@visible = false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
sig { void }
|
|
27
|
+
def show
|
|
28
|
+
@visible = true
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
sig { void }
|
|
32
|
+
def hide
|
|
33
|
+
@visible = false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
sig { params(new_x: Integer, new_y: Integer).void }
|
|
37
|
+
def move_to(new_x, new_y)
|
|
38
|
+
@anchor_x = new_x
|
|
39
|
+
@anchor_y = new_y
|
|
40
|
+
refresh
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
sig { params(buffer: Buffer, x: Integer, y: Integer, width: Integer, height: Integer).void }
|
|
44
|
+
def render(buffer, x:, y:, width:, height:)
|
|
45
|
+
return unless @visible
|
|
46
|
+
return if height < 3
|
|
47
|
+
|
|
48
|
+
text_width = @text.length
|
|
49
|
+
box_width = [text_width + 4, width].min
|
|
50
|
+
box_height = 3
|
|
51
|
+
|
|
52
|
+
# Draw top border
|
|
53
|
+
top = "┌#{"─" * (box_width - 2)}┐"
|
|
54
|
+
buffer.put_string(x, y, top[0, width])
|
|
55
|
+
|
|
56
|
+
# Draw middle with text
|
|
57
|
+
padding = (box_width - 2 - text_width) / 2
|
|
58
|
+
middle = "│#{" " * padding}#{@text}#{" " * (box_width - 3 - padding - text_width)}│"
|
|
59
|
+
buffer.put_string(x, y + 1, middle[0, width])
|
|
60
|
+
|
|
61
|
+
# Draw bottom border
|
|
62
|
+
bottom = "└#{"─" * (box_width - 2)}┘"
|
|
63
|
+
buffer.put_string(x, y + 2, bottom[0, width])
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|