r2ui 0.1.0 → 0.2.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 +64 -0
- data/README.md +79 -0
- data/lib/r2ui/app.rb +311 -34
- data/lib/r2ui/canvas.rb +135 -14
- data/lib/r2ui/cli/builder.rb +72 -0
- data/lib/r2ui/cli/context.rb +45 -0
- data/lib/r2ui/cli/definition.rb +174 -0
- data/lib/r2ui/cli/ext/ask.rb +133 -0
- data/lib/r2ui/cli/ext/box.rb +112 -0
- data/lib/r2ui/cli/ext/choose.rb +162 -0
- data/lib/r2ui/cli/ext/choose_many.rb +241 -0
- data/lib/r2ui/cli/ext/color_option.rb +64 -0
- data/lib/r2ui/cli/ext/completion.rb +520 -0
- data/lib/r2ui/cli/ext/config.rb +179 -0
- data/lib/r2ui/cli/ext/confirm.rb +110 -0
- data/lib/r2ui/cli/ext/dashboard.rb +105 -0
- data/lib/r2ui/cli/ext/diff.rb +195 -0
- data/lib/r2ui/cli/ext/editor.rb +94 -0
- data/lib/r2ui/cli/ext/env.rb +108 -0
- data/lib/r2ui/cli/ext/examples.rb +62 -0
- data/lib/r2ui/cli/ext/filter.rb +269 -0
- data/lib/r2ui/cli/ext/format.rb +104 -0
- data/lib/r2ui/cli/ext/list.rb +117 -0
- data/lib/r2ui/cli/ext/log.rb +89 -0
- data/lib/r2ui/cli/ext/pager.rb +97 -0
- data/lib/r2ui/cli/ext/pairs.rb +62 -0
- data/lib/r2ui/cli/ext/parallel.rb +274 -0
- data/lib/r2ui/cli/ext/password.rb +158 -0
- data/lib/r2ui/cli/ext/progress.rb +330 -0
- data/lib/r2ui/cli/ext/reference.rb +129 -0
- data/lib/r2ui/cli/ext/spinner.rb +165 -0
- data/lib/r2ui/cli/ext/summary.rb +63 -0
- data/lib/r2ui/cli/ext/table.rb +141 -0
- data/lib/r2ui/cli/ext/tasks.rb +216 -0
- data/lib/r2ui/cli/ext/trace.rb +64 -0
- data/lib/r2ui/cli/ext/tree.rb +114 -0
- data/lib/r2ui/cli/ext/version.rb +67 -0
- data/lib/r2ui/cli/extension.rb +129 -0
- data/lib/r2ui/cli/help.rb +115 -0
- data/lib/r2ui/cli/helpers.rb +20 -0
- data/lib/r2ui/cli/live.rb +150 -0
- data/lib/r2ui/cli/parser.rb +165 -0
- data/lib/r2ui/cli/prompt.rb +133 -0
- data/lib/r2ui/cli/runner.rb +86 -0
- data/lib/r2ui/cli/shell.rb +185 -0
- data/lib/r2ui/cli/testing.rb +49 -0
- data/lib/r2ui/cli.rb +129 -0
- data/lib/r2ui/compat/bubbletea/LICENSE-bubbletea.txt +21 -0
- data/lib/r2ui/compat/bubbletea/ansi.rb +424 -0
- data/lib/r2ui/compat/bubbletea/commands.rb +143 -0
- data/lib/r2ui/compat/bubbletea/input.rb +269 -0
- data/lib/r2ui/compat/bubbletea/input_reader.rb +66 -0
- data/lib/r2ui/compat/bubbletea/messages.rb +251 -0
- data/lib/r2ui/compat/bubbletea/model.rb +65 -0
- data/lib/r2ui/compat/bubbletea/native.rb +254 -0
- data/lib/r2ui/compat/bubbletea/renderer.rb +192 -0
- data/lib/r2ui/compat/bubbletea/runner.rb +425 -0
- data/lib/r2ui/compat/bubbletea/terminal.rb +230 -0
- data/lib/r2ui/compat/bubbletea/version.rb +8 -0
- data/lib/r2ui/compat/bubbletea/width_table.rb +228 -0
- data/lib/r2ui/compat/bubbletea.rb +16 -0
- data/lib/r2ui/compat/lipgloss/border.rb +17 -0
- data/lib/r2ui/compat/lipgloss/color.rb +78 -0
- data/lib/r2ui/compat/lipgloss/color_blend.rb +166 -0
- data/lib/r2ui/compat/lipgloss/colorful.rb +585 -0
- data/lib/r2ui/compat/lipgloss/layout.rb +461 -0
- data/lib/r2ui/compat/lipgloss/list.rb +85 -0
- data/lib/r2ui/compat/lipgloss/position.rb +30 -0
- data/lib/r2ui/compat/lipgloss/style/border.rb +134 -0
- data/lib/r2ui/compat/lipgloss/style.rb +1062 -0
- data/lib/r2ui/compat/lipgloss/table/render.rb +518 -0
- data/lib/r2ui/compat/lipgloss/table.rb +185 -0
- data/lib/r2ui/compat/lipgloss/termenv.rb +768 -0
- data/lib/r2ui/compat/lipgloss/text/unicode_tables.rb +428 -0
- data/lib/r2ui/compat/lipgloss/text.rb +1555 -0
- data/lib/r2ui/compat/lipgloss/tree/go_tree.rb +243 -0
- data/lib/r2ui/compat/lipgloss/tree.rb +105 -0
- data/lib/r2ui/compat/lipgloss/version.rb +15 -0
- data/lib/r2ui/compat/lipgloss.rb +64 -0
- data/lib/r2ui/compat/load_path/bubbletea.rb +4 -0
- data/lib/r2ui/compat/load_path/lipgloss.rb +4 -0
- data/lib/r2ui/component.rb +67 -0
- data/lib/r2ui/context.rb +87 -0
- data/lib/r2ui/drop_in.rb +16 -0
- data/lib/r2ui/dsl/column.rb +11 -3
- data/lib/r2ui/dsl/dashboard.rb +45 -9
- data/lib/r2ui/dsl/resource.rb +12 -0
- data/lib/r2ui/ext/after.rb +54 -0
- data/lib/r2ui/ext/async.rb +81 -0
- data/lib/r2ui/ext/batch_sequence.rb +54 -0
- data/lib/r2ui/ext/countdown.rb +52 -0
- data/lib/r2ui/ext/data_table.rb +71 -0
- data/lib/r2ui/ext/every.rb +66 -0
- data/lib/r2ui/ext/exec.rb +116 -0
- data/lib/r2ui/ext/file_picker.rb +78 -0
- data/lib/r2ui/ext/focus_report.rb +69 -0
- data/lib/r2ui/ext/help.rb +62 -0
- data/lib/r2ui/ext/keys.rb +64 -0
- data/lib/r2ui/ext/list.rb +145 -0
- data/lib/r2ui/ext/messages.rb +87 -0
- data/lib/r2ui/ext/mouse.rb +134 -0
- data/lib/r2ui/ext/paginator.rb +86 -0
- data/lib/r2ui/ext/paste.rb +86 -0
- data/lib/r2ui/ext/println.rb +21 -0
- data/lib/r2ui/ext/progress.rb +85 -0
- data/lib/r2ui/ext/refresh.rb +56 -0
- data/lib/r2ui/ext/resize.rb +62 -0
- data/lib/r2ui/ext/screen.rb +47 -0
- data/lib/r2ui/ext/screen_mode.rb +63 -0
- data/lib/r2ui/ext/spinner.rb +59 -0
- data/lib/r2ui/ext/state.rb +45 -0
- data/lib/r2ui/ext/stopwatch.rb +67 -0
- data/lib/r2ui/ext/suspend.rb +56 -0
- data/lib/r2ui/ext/text_area.rb +73 -0
- data/lib/r2ui/ext/text_input.rb +83 -0
- data/lib/r2ui/ext/theme.rb +112 -0
- data/lib/r2ui/ext/view.rb +37 -0
- data/lib/r2ui/ext/viewport.rb +92 -0
- data/lib/r2ui/ext/window_title.rb +51 -0
- data/lib/r2ui/extension.rb +199 -0
- data/lib/r2ui/history.rb +4 -1
- data/lib/r2ui/keys.rb +35 -8
- data/lib/r2ui/motion.rb +169 -0
- data/lib/r2ui/panel_state.rb +46 -2
- data/lib/r2ui/renderer.rb +87 -22
- data/lib/r2ui/version.rb +1 -1
- data/lib/r2ui/widgets/box.rb +6 -3
- data/lib/r2ui/widgets/glyphs.rb +149 -0
- data/lib/r2ui/widgets/table.rb +109 -25
- data/lib/r2ui.rb +12 -1
- metadata +119 -4
- data/lib/r2ui/terminal.rb +0 -53
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# A port of lipgloss v1.1.0 tree/ (tree.go, children.go, enumerator.go, renderer.go) and the
|
|
4
|
+
# enumerators of list/ (list/enumerator.go). Lipgloss::Tree and Lipgloss::List wrap a GoTree::Node,
|
|
5
|
+
# the way the gem's Ruby objects wrap a handle to a Go *tree.Tree / *list.List: every method mutates
|
|
6
|
+
# that shared node, and the returned Ruby object is a fresh wrapper around the same node.
|
|
7
|
+
#
|
|
8
|
+
# Only what the gem's Ruby API can reach is ported: no hidden nodes, no style or enumerator funcs
|
|
9
|
+
# that look at the children (the Ruby API only sets constant styles and the built-in enumerators).
|
|
10
|
+
module R2UI
|
|
11
|
+
module Compat
|
|
12
|
+
module Gloss
|
|
13
|
+
module GoTree
|
|
14
|
+
UTF_8 = Encoding::UTF_8
|
|
15
|
+
|
|
16
|
+
# tree.Leaf
|
|
17
|
+
class Leaf
|
|
18
|
+
attr_accessor :value
|
|
19
|
+
|
|
20
|
+
def initialize(value)
|
|
21
|
+
@value = value
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# tree.Tree
|
|
26
|
+
class Node
|
|
27
|
+
attr_accessor :value, :children, :offset, :r
|
|
28
|
+
|
|
29
|
+
def initialize
|
|
30
|
+
@value = +""
|
|
31
|
+
@children = []
|
|
32
|
+
@offset = [0, 0]
|
|
33
|
+
@r = nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Tree.Root(string)
|
|
37
|
+
def root(value)
|
|
38
|
+
@value = value
|
|
39
|
+
self
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Tree.Children(): the children inside the offset window.
|
|
43
|
+
def visible_children
|
|
44
|
+
(@offset[0]...(@children.length - @offset[1])).map { |i| @children[i] }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Tree.Child for one *Tree, Node or string.
|
|
48
|
+
def child(item)
|
|
49
|
+
case item
|
|
50
|
+
when Node
|
|
51
|
+
new_item, rm = ensure_parent(@children, item)
|
|
52
|
+
@children.delete_at(rm) if rm >= 0
|
|
53
|
+
@children << new_item
|
|
54
|
+
when Leaf
|
|
55
|
+
@children << item
|
|
56
|
+
when String
|
|
57
|
+
@children << Leaf.new(item)
|
|
58
|
+
end
|
|
59
|
+
self
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Tree.Offset
|
|
63
|
+
def set_offset(start, stop)
|
|
64
|
+
start, stop = stop, start if start > stop
|
|
65
|
+
start = 0 if start.negative?
|
|
66
|
+
stop = @children.length if stop.negative? || stop > @children.length
|
|
67
|
+
@offset = [start, stop]
|
|
68
|
+
self
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def ensure_renderer
|
|
72
|
+
@r ||= Renderer.new
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Tree.String()
|
|
76
|
+
def to_s
|
|
77
|
+
ensure_renderer.render(self, true, "")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
# tree.go ensureParent: a tree without a root takes over the previous leaf's value (or merges
|
|
83
|
+
# into the previous subtree).
|
|
84
|
+
def ensure_parent(nodes, item)
|
|
85
|
+
return [item, -1] if item.value != "" || nodes.empty?
|
|
86
|
+
|
|
87
|
+
j = nodes.length - 1
|
|
88
|
+
parent = nodes[j]
|
|
89
|
+
case parent
|
|
90
|
+
when Node
|
|
91
|
+
i = 0
|
|
92
|
+
while i < item.visible_children.length
|
|
93
|
+
parent.child(item.children[i])
|
|
94
|
+
i += 1
|
|
95
|
+
end
|
|
96
|
+
[parent, j]
|
|
97
|
+
else
|
|
98
|
+
item.value = parent.value
|
|
99
|
+
[item, j]
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
DEFAULT_ENUMERATOR = ->(children, index) { children.length - 1 == index ? "└──" : "├──" }
|
|
105
|
+
ROUNDED_ENUMERATOR = ->(children, index) { children.length - 1 == index ? "╰──" : "├──" }
|
|
106
|
+
DEFAULT_INDENTER = ->(children, index) { children.length - 1 == index ? " " : "│ " }
|
|
107
|
+
|
|
108
|
+
# list/enumerator.go
|
|
109
|
+
module ListEnumerators
|
|
110
|
+
ABC_LEN = 26
|
|
111
|
+
ROMAN = %w[M CM D CD C XC L XL X IX V IV I].freeze
|
|
112
|
+
ARABIC = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1].freeze
|
|
113
|
+
|
|
114
|
+
BULLET = ->(_items, _i) { "•" }
|
|
115
|
+
ASTERISK = ->(_items, _i) { "*" }
|
|
116
|
+
DASH = ->(_items, _i) { "-" }
|
|
117
|
+
ARABIC_ENUM = ->(_items, i) { "#{i + 1}." }
|
|
118
|
+
|
|
119
|
+
ALPHABET = lambda do |_items, i|
|
|
120
|
+
a = "A".ord
|
|
121
|
+
if i >= ABC_LEN * ABC_LEN + ABC_LEN
|
|
122
|
+
[a + i / ABC_LEN / ABC_LEN - 1, a + (i / ABC_LEN) % ABC_LEN - 1, a + i % ABC_LEN].pack("U*") + "."
|
|
123
|
+
elsif i >= ABC_LEN
|
|
124
|
+
[a + i / ABC_LEN - 1, a + i % ABC_LEN].pack("U*") + "."
|
|
125
|
+
else
|
|
126
|
+
[a + i % ABC_LEN].pack("U*") + "."
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
ROMAN_ENUM = lambda do |_items, i|
|
|
131
|
+
result = +""
|
|
132
|
+
ARABIC.each_with_index do |value, v|
|
|
133
|
+
while i >= value - 1
|
|
134
|
+
i -= value
|
|
135
|
+
result << ROMAN[v]
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
result << "."
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
INDENTER = ->(_items, _i) { " " }
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# list.New(): a tree with the bullet enumerator and a one-space indenter.
|
|
145
|
+
def self.new_list
|
|
146
|
+
node = Node.new
|
|
147
|
+
r = node.ensure_renderer
|
|
148
|
+
r.enumerator = ListEnumerators::BULLET
|
|
149
|
+
r.indenter = ListEnumerators::INDENTER
|
|
150
|
+
node
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# tree/renderer.go
|
|
154
|
+
class Renderer
|
|
155
|
+
attr_accessor :enumerator_style, :item_style, :root_style, :enumerator, :indenter
|
|
156
|
+
|
|
157
|
+
def initialize
|
|
158
|
+
@enumerator_style = ::Lipgloss::Style.new.padding_right(1)
|
|
159
|
+
@item_style = ::Lipgloss::Style.new
|
|
160
|
+
@root_style = ::Lipgloss::Style.new
|
|
161
|
+
@enumerator = DEFAULT_ENUMERATOR
|
|
162
|
+
@indenter = DEFAULT_INDENTER
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def render(node, root, prefix)
|
|
166
|
+
strs = []
|
|
167
|
+
max_len = 0
|
|
168
|
+
children = node.is_a?(Node) ? node.visible_children : []
|
|
169
|
+
|
|
170
|
+
name = node.value
|
|
171
|
+
strs << style_render(@root_style, name) if name != "" && root
|
|
172
|
+
|
|
173
|
+
children.each_index do |i|
|
|
174
|
+
p = style_render(@enumerator_style, @enumerator.call(children, i))
|
|
175
|
+
w = layout.width(p)
|
|
176
|
+
max_len = w if w > max_len
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
children.each_with_index do |child, i|
|
|
180
|
+
indent = @indenter.call(children, i)
|
|
181
|
+
node_prefix = style_render(@enumerator_style, @enumerator.call(children, i))
|
|
182
|
+
l = max_len - layout.width(node_prefix)
|
|
183
|
+
node_prefix = (" " * l) + node_prefix if l.positive?
|
|
184
|
+
|
|
185
|
+
item = style_render(@item_style, child.value)
|
|
186
|
+
multiline_prefix = prefix
|
|
187
|
+
|
|
188
|
+
while layout.height(item) > layout.height(node_prefix)
|
|
189
|
+
node_prefix = layout.join_vertical(0.0, [node_prefix, style_render(@enumerator_style, indent)])
|
|
190
|
+
end
|
|
191
|
+
while layout.height(node_prefix) > layout.height(multiline_prefix)
|
|
192
|
+
multiline_prefix = layout.join_vertical(0.0, [multiline_prefix, prefix])
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
strs << layout.join_horizontal(0.0, [multiline_prefix, node_prefix, item])
|
|
196
|
+
|
|
197
|
+
renderer = (child.is_a?(Node) && child.r) || self
|
|
198
|
+
s = renderer.render(child, false, prefix + style_render(@enumerator_style, indent))
|
|
199
|
+
strs << s unless s.empty?
|
|
200
|
+
end
|
|
201
|
+
strs.join("\n")
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
private
|
|
205
|
+
|
|
206
|
+
def layout = Layout
|
|
207
|
+
|
|
208
|
+
def style_render(style, str)
|
|
209
|
+
style.send(:go_render, str)
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# --- Ruby-side argument handling shared by Lipgloss::List and Lipgloss::Tree --------------
|
|
214
|
+
|
|
215
|
+
# Check_Type(value, T_STRING) + StringValueCStr, copied the way Go copies a C string.
|
|
216
|
+
def self.c_string_arg(value)
|
|
217
|
+
Layout.check_string(value)
|
|
218
|
+
String.new(Layout.c_string(value), encoding: UTF_8)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Array#to_json unmarshalled into []string (nil when the unmarshal fails).
|
|
222
|
+
def self.json_string_array(array)
|
|
223
|
+
Layout.check_array(array)
|
|
224
|
+
strs = Layout.json_strings(array)
|
|
225
|
+
return nil if strs.equal?(Layout::JSON_FAIL)
|
|
226
|
+
|
|
227
|
+
strs.map { |s| String.new(s, encoding: UTF_8) }
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# TypedData_Get_Struct(style, ..., &style_type)
|
|
231
|
+
def self.style_arg(style)
|
|
232
|
+
return style if style.is_a?(::Lipgloss::Style)
|
|
233
|
+
|
|
234
|
+
raise TypeError, "wrong argument type #{Layout.type_name(style)} (expected Lipgloss::Style)"
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def self.render(node)
|
|
238
|
+
Layout.c_result(node.to_s)
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "tree/go_tree"
|
|
4
|
+
|
|
5
|
+
module Lipgloss
|
|
6
|
+
# Lipgloss::Tree as the gem's C extension (tree.c) defines it over Go lipgloss tree.Tree. Every method
|
|
7
|
+
# mutates the underlying tree and returns a new Lipgloss::Tree wrapping that same tree.
|
|
8
|
+
class Tree
|
|
9
|
+
def self.root(root)
|
|
10
|
+
value = R2UI::Compat::Gloss::GoTree.c_string_arg(root)
|
|
11
|
+
tree = allocate
|
|
12
|
+
tree.instance_variable_set(:@tree, R2UI::Compat::Gloss::GoTree::Node.new.root(value))
|
|
13
|
+
tree
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize(*args)
|
|
17
|
+
node = go_tree
|
|
18
|
+
node.root(R2UI::Compat::Gloss::GoTree.c_string_arg(args[0])) if args.length == 1
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def root=(root)
|
|
22
|
+
node = go_tree
|
|
23
|
+
node.root(R2UI::Compat::Gloss::GoTree.c_string_arg(root))
|
|
24
|
+
wrap(node)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def child(*children)
|
|
28
|
+
node = go_tree
|
|
29
|
+
raise ArgumentError, "wrong number of arguments (given 0, expected 1+)" if children.empty?
|
|
30
|
+
|
|
31
|
+
children.each do |child|
|
|
32
|
+
if child.is_a?(Lipgloss::Tree)
|
|
33
|
+
node.child(child.send(:go_tree))
|
|
34
|
+
else
|
|
35
|
+
node.child(R2UI::Compat::Gloss::GoTree.c_string_arg(child))
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
wrap(node)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def children(children)
|
|
42
|
+
node = go_tree
|
|
43
|
+
strs = R2UI::Compat::Gloss::GoTree.json_string_array(children)
|
|
44
|
+
strs&.each { |s| node.child(s) }
|
|
45
|
+
wrap(node)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def enumerator(enum_symbol)
|
|
49
|
+
node = go_tree
|
|
50
|
+
node.ensure_renderer.enumerator =
|
|
51
|
+
if enum_symbol.equal?(:rounded)
|
|
52
|
+
R2UI::Compat::Gloss::GoTree::ROUNDED_ENUMERATOR
|
|
53
|
+
else
|
|
54
|
+
R2UI::Compat::Gloss::GoTree::DEFAULT_ENUMERATOR
|
|
55
|
+
end
|
|
56
|
+
wrap(node)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def enumerator_style(style)
|
|
60
|
+
node = go_tree
|
|
61
|
+
node.ensure_renderer.enumerator_style = R2UI::Compat::Gloss::GoTree.style_arg(style)
|
|
62
|
+
wrap(node)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def item_style(style)
|
|
66
|
+
node = go_tree
|
|
67
|
+
node.ensure_renderer.item_style = R2UI::Compat::Gloss::GoTree.style_arg(style)
|
|
68
|
+
wrap(node)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def root_style(style)
|
|
72
|
+
node = go_tree
|
|
73
|
+
node.ensure_renderer.root_style = R2UI::Compat::Gloss::GoTree.style_arg(style)
|
|
74
|
+
wrap(node)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def offset(start, stop)
|
|
78
|
+
node = go_tree
|
|
79
|
+
layout = R2UI::Compat::Gloss::Layout
|
|
80
|
+
node.set_offset(layout.num2int(start), layout.num2int(stop))
|
|
81
|
+
wrap(node)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def render
|
|
85
|
+
R2UI::Compat::Gloss::GoTree.render(go_tree)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def to_s
|
|
89
|
+
render
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
# The gem allocates the Go tree in the alloc function, so even an uninitialized object has one.
|
|
95
|
+
def go_tree
|
|
96
|
+
@tree ||= R2UI::Compat::Gloss::GoTree::Node.new
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def wrap(node)
|
|
100
|
+
tree = self.class.allocate
|
|
101
|
+
tree.instance_variable_set(:@tree, node)
|
|
102
|
+
tree
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lipgloss
|
|
4
|
+
VERSION = "0.2.2"
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
module R2UI
|
|
8
|
+
module Compat
|
|
9
|
+
# Internals of the pure-Ruby lipgloss (see lib/r2ui/compat/lipgloss.rb).
|
|
10
|
+
module Gloss
|
|
11
|
+
# The Go lipgloss version the real gem wraps; returned by Lipgloss.upstream_version.
|
|
12
|
+
UPSTREAM_VERSION = "v1.1.0"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Pure-Ruby Lipgloss: a drop-in for the lipgloss gem 0.2.2 (marcoroth/lipgloss-ruby, MIT), which wraps
|
|
4
|
+
# Go lipgloss v1.1.0 through a C extension. This port follows the Go sources (lipgloss v1.1.0,
|
|
5
|
+
# charmbracelet/x/ansi v0.8.0, muesli/termenv v0.16.0, lucasb-eyer/go-colorful v1.2.0, rivo/uniseg v0.4.7)
|
|
6
|
+
# so output is byte-identical to the real gem for the same calls and color profile.
|
|
7
|
+
#
|
|
8
|
+
# Public API lives under `Lipgloss`, exactly as the gem exposes it. Internals (text width and wrapping,
|
|
9
|
+
# terminal color handling, color math) live under `R2UI::Compat::Gloss` so they never leak into the
|
|
10
|
+
# gem's namespace.
|
|
11
|
+
|
|
12
|
+
require_relative "lipgloss/version"
|
|
13
|
+
require_relative "lipgloss/text"
|
|
14
|
+
require_relative "lipgloss/colorful"
|
|
15
|
+
require_relative "lipgloss/termenv"
|
|
16
|
+
require_relative "lipgloss/position"
|
|
17
|
+
require_relative "lipgloss/border"
|
|
18
|
+
require_relative "lipgloss/color"
|
|
19
|
+
require_relative "lipgloss/color_blend"
|
|
20
|
+
require_relative "lipgloss/style"
|
|
21
|
+
require_relative "lipgloss/layout"
|
|
22
|
+
require_relative "lipgloss/table"
|
|
23
|
+
require_relative "lipgloss/list"
|
|
24
|
+
require_relative "lipgloss/tree"
|
|
25
|
+
|
|
26
|
+
module Lipgloss
|
|
27
|
+
TOP = Position::TOP
|
|
28
|
+
BOTTOM = Position::BOTTOM
|
|
29
|
+
LEFT = Position::LEFT
|
|
30
|
+
RIGHT = Position::RIGHT
|
|
31
|
+
CENTER = Position::CENTER
|
|
32
|
+
|
|
33
|
+
NORMAL_BORDER = Border::NORMAL
|
|
34
|
+
ROUNDED_BORDER = Border::ROUNDED
|
|
35
|
+
THICK_BORDER = Border::THICK
|
|
36
|
+
DOUBLE_BORDER = Border::DOUBLE
|
|
37
|
+
HIDDEN_BORDER = Border::HIDDEN
|
|
38
|
+
BLOCK_BORDER = Border::BLOCK
|
|
39
|
+
ASCII_BORDER = Border::ASCII
|
|
40
|
+
|
|
41
|
+
NO_TAB_CONVERSION = -1
|
|
42
|
+
|
|
43
|
+
class << self
|
|
44
|
+
def join_horizontal(position, *strings)
|
|
45
|
+
_join_horizontal(Position.resolve(position), strings)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def join_vertical(position, *strings)
|
|
49
|
+
_join_vertical(Position.resolve(position), strings)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def place(width, height, horizontal, vertical, string, **opts)
|
|
53
|
+
_place(width, height, Position.resolve(horizontal), Position.resolve(vertical), string, **opts)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def place_horizontal(width, position, string)
|
|
57
|
+
_place_horizontal(width, Position.resolve(position), string)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def place_vertical(height, position, string)
|
|
61
|
+
_place_vertical(height, Position.resolve(position), string)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module R2UI
|
|
4
|
+
# Hosts Bubbletea-style models (bubbles components, or your own: `update(msg)` → [model, cmd],
|
|
5
|
+
# `view` → String) as panel items. An extension declares one with
|
|
6
|
+
#
|
|
7
|
+
# component(SpinnerItem) { |item| Bubbles::Spinner.new } # build block, runs on a Context
|
|
8
|
+
# component(InputItem, focusable: true) { |item| Bubbles::TextInput.new }
|
|
9
|
+
#
|
|
10
|
+
# and the app then, for every panel item of that class:
|
|
11
|
+
# - builds the model once, in App#init, and runs its `init` (if it has one);
|
|
12
|
+
# - sends it every non-key message (ticks, window size, mouse, your own messages) and batches the
|
|
13
|
+
# commands it returns;
|
|
14
|
+
# - if `focusable`, sends it the key messages while its panel has focus (the first focusable item
|
|
15
|
+
# in the panel), except tab / shift+tab (panel focus) and ctrl+c (quit); calls its `focus`
|
|
16
|
+
# (or `focus!`) / `blur` as its panel gains and loses focus;
|
|
17
|
+
# - draws `model.view` in the panel, unless the extension registers its own `panel_item` drawer.
|
|
18
|
+
module Component
|
|
19
|
+
# One hosted model. `item` is the DSL object the panel holds; `model` changes as update returns.
|
|
20
|
+
Instance = Struct.new(:panel, :item, :spec, :model, :focused, keyword_init: true) do
|
|
21
|
+
def name = item.respond_to?(:name) ? item.name : nil
|
|
22
|
+
def focusable? = spec.options.fetch(:focusable, false)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
Spec = Data.define(:matcher, :options, :build)
|
|
26
|
+
|
|
27
|
+
# Keys a focused component never sees.
|
|
28
|
+
RESERVED = %i[tab back_tab interrupt].freeze
|
|
29
|
+
|
|
30
|
+
module_function
|
|
31
|
+
|
|
32
|
+
# Bubbles is an optional dependency: component extensions call this when first used (not at
|
|
33
|
+
# load time). It opts into r2ui/drop_in, so bubbles' `require "bubbletea"`/`"lipgloss"` load
|
|
34
|
+
# r2ui's engine instead of the real gems.
|
|
35
|
+
def require_bubbles!
|
|
36
|
+
return if defined?(::Bubbles)
|
|
37
|
+
|
|
38
|
+
begin
|
|
39
|
+
require_relative "drop_in"
|
|
40
|
+
rescue LoadError => e
|
|
41
|
+
raise Error, "bubbles components run on r2ui's Bubbletea, but #{e.message}"
|
|
42
|
+
end
|
|
43
|
+
begin
|
|
44
|
+
require "bubbles"
|
|
45
|
+
rescue LoadError
|
|
46
|
+
raise Error, "this needs the bubbles gem: add `gem \"bubbles\"` to your Gemfile"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# [model, command] from an init/update result (a pair, a bare command, or nil).
|
|
51
|
+
def split(model, result)
|
|
52
|
+
return result if result.is_a?(Array) && result.size == 2 && !Context.command?(result.first)
|
|
53
|
+
|
|
54
|
+
[model, Context.command?(result) ? result : nil]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def focus(instance)
|
|
58
|
+
model = instance.model
|
|
59
|
+
method = %i[focus focus!].find { |m| model.respond_to?(m) }
|
|
60
|
+
method && model.public_send(method)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def blur(instance)
|
|
64
|
+
instance.model.blur if instance.model.respond_to?(:blur)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
data/lib/r2ui/context.rb
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module R2UI
|
|
4
|
+
# What a hook or user block runs on (instance_exec). Holds the running app and the commands the
|
|
5
|
+
# block enqueues; the app batches them into the Bubbletea command its init/update returns.
|
|
6
|
+
#
|
|
7
|
+
# Core helpers are below; extensions add more with `helpers do ... end`. Command helpers enqueue
|
|
8
|
+
# with `command(...)` and return the command, so a block can also just return one.
|
|
9
|
+
class Context
|
|
10
|
+
attr_reader :app, :message
|
|
11
|
+
# Set while drawing a panel item.
|
|
12
|
+
attr_accessor :panel, :width, :height
|
|
13
|
+
|
|
14
|
+
def initialize(app, message = nil)
|
|
15
|
+
@app = app
|
|
16
|
+
@message = message
|
|
17
|
+
@commands = []
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.command?(value) = value.is_a?(Bubbletea::Command) || value.is_a?(Proc)
|
|
21
|
+
|
|
22
|
+
# nil, one command, or a batch of several.
|
|
23
|
+
def self.combine(commands)
|
|
24
|
+
commands = commands.compact
|
|
25
|
+
commands.size <= 1 ? commands.first : Bubbletea.batch(*commands)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# The app's user state: a Hash any block may read and write.
|
|
29
|
+
def state = app.state
|
|
30
|
+
|
|
31
|
+
# Private runtime state for one extension (timers it scheduled, component models, ...).
|
|
32
|
+
def store(name) = app.store(name)
|
|
33
|
+
|
|
34
|
+
# The app's animation state (R2UI::Motion), for panel items that tween, pulse or age values.
|
|
35
|
+
def motion = app.motion
|
|
36
|
+
|
|
37
|
+
def dashboard = app.dashboard
|
|
38
|
+
|
|
39
|
+
# The focused panel (a DSL::Panel).
|
|
40
|
+
def focus = app.focus
|
|
41
|
+
|
|
42
|
+
# Rows under the selection in the focused table panel ([] if none).
|
|
43
|
+
def selected_rows = app.selected_rows
|
|
44
|
+
|
|
45
|
+
# The hosted model of the component item named `name` (see R2UI::Component), or nil.
|
|
46
|
+
def component(name) = app.component(name)
|
|
47
|
+
|
|
48
|
+
# Moves key focus to the named component, or away from all with nil (App#focus_component).
|
|
49
|
+
def focus_component(name)
|
|
50
|
+
app.focus_component(name, self)
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Shows text on the status bar for a few seconds.
|
|
55
|
+
def flash(text) = app.flash(text)
|
|
56
|
+
|
|
57
|
+
def quit = command(Bubbletea.quit)
|
|
58
|
+
|
|
59
|
+
# Enqueues a Bubbletea command (or a Proc, run in the background). Returns it.
|
|
60
|
+
def command(cmd)
|
|
61
|
+
@commands << cmd if cmd && @commands.none? { |c| c.equal?(cmd) }
|
|
62
|
+
cmd
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Inside an `on` handler: decline the message, so lower-priority handlers and the core see it.
|
|
66
|
+
def pass = throw(:r2ui_pass)
|
|
67
|
+
|
|
68
|
+
# Runs `block` here; a returned command is enqueued too. Returns the block's value.
|
|
69
|
+
def call(block, *args)
|
|
70
|
+
result = instance_exec(*args, &block)
|
|
71
|
+
(result.is_a?(Array) ? result : [result]).each { |r| command(r) if Context.command?(r) }
|
|
72
|
+
result
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Runs an `on` handler: true if it consumed the message (didn't `pass`).
|
|
76
|
+
def handle(block, message)
|
|
77
|
+
catch(:r2ui_pass) do
|
|
78
|
+
call(block, message)
|
|
79
|
+
return true
|
|
80
|
+
end
|
|
81
|
+
false
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# The enqueued commands as one Bubbletea command (nil if none).
|
|
85
|
+
def commands = Context.combine(@commands)
|
|
86
|
+
end
|
|
87
|
+
end
|
data/lib/r2ui/drop_in.rb
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Makes `require "bubbletea"` and `require "lipgloss"` load r2ui's pure-Ruby versions, so code
|
|
4
|
+
# written for the Charm Ruby gems (and pure-Ruby gems on top of them, like bubbles) runs unchanged:
|
|
5
|
+
#
|
|
6
|
+
# require "r2ui/drop_in"
|
|
7
|
+
# require "bubbles" # now runs on r2ui
|
|
8
|
+
#
|
|
9
|
+
# Load it before anything requires the real gems.
|
|
10
|
+
|
|
11
|
+
loaded = $LOADED_FEATURES.grep(%r{/gems/(bubbletea|lipgloss)-[^/]+/})
|
|
12
|
+
unless loaded.empty?
|
|
13
|
+
raise LoadError, "r2ui/drop_in must be required before the real bubbletea/lipgloss gems (already loaded: #{loaded.first})"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
$LOAD_PATH.unshift(File.expand_path("compat/load_path", __dir__))
|
data/lib/r2ui/dsl/column.rb
CHANGED
|
@@ -3,11 +3,19 @@
|
|
|
3
3
|
module R2UI
|
|
4
4
|
module DSL
|
|
5
5
|
# One attribute of a resource: how to read it, show it and roll it up.
|
|
6
|
-
|
|
6
|
+
#
|
|
7
|
+
# Drawing options for the index table: `style` is a callable (value, line) -> style or nil (a
|
|
8
|
+
# palette Symbol or an SGR String; nil keeps the default colouring). `sparkline` is true (block
|
|
9
|
+
# bars) or :braille; `spark_width` its width in cells, `spark_max` its scale (else the window
|
|
10
|
+
# max), `spark_style` a style or a callable (values, line) -> style. When columns don't fit, the
|
|
11
|
+
# lowest `priority` is dropped first (ties: the last declared).
|
|
12
|
+
Column = Data.define(:key, :label, :format, :sparkline, :aggregate, :width, :align, :sort, :reader,
|
|
13
|
+
:style, :spark_width, :spark_max, :spark_style, :priority) do
|
|
7
14
|
def self.build(key, label: nil, format: :text, sparkline: false, aggregate: nil, width: nil, align: nil,
|
|
8
|
-
sort: nil, &reader)
|
|
15
|
+
sort: nil, style: nil, spark_width: nil, spark_max: nil, spark_style: nil, priority: 0, &reader)
|
|
9
16
|
new(
|
|
10
|
-
key:, format:, sparkline:, width:, sort:, reader:,
|
|
17
|
+
key:, format:, sparkline:, width:, sort:, reader:, style:, spark_width:, spark_max:, spark_style:,
|
|
18
|
+
priority:,
|
|
11
19
|
label: label || key.to_s.tr("_", " ").capitalize,
|
|
12
20
|
aggregate: aggregate || Format.default_aggregate(format),
|
|
13
21
|
align: align || Format.default_align(format)
|