termgui 0.0.5 → 0.0.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +14 -0
- data/LICENSE +19 -0
- data/README.md +321 -0
- data/TODO.md +259 -0
- data/src/action.rb +58 -0
- data/src/box.rb +90 -0
- data/src/color.rb +174 -0
- data/src/cursor.rb +69 -0
- data/src/editor/editor_base.rb +152 -0
- data/src/editor/editor_base_handlers.rb +116 -0
- data/src/element.rb +61 -0
- data/src/element_bounds.rb +111 -0
- data/src/element_box.rb +64 -0
- data/src/element_render.rb +102 -0
- data/src/element_style.rb +51 -0
- data/src/emitter.rb +102 -0
- data/src/emitter_state.rb +19 -0
- data/src/enterable.rb +93 -0
- data/src/event.rb +92 -0
- data/src/focus.rb +102 -0
- data/src/geometry.rb +53 -0
- data/src/image.rb +60 -0
- data/src/input.rb +85 -0
- data/src/input_grab.rb +17 -0
- data/src/input_time.rb +97 -0
- data/src/key.rb +114 -0
- data/src/log.rb +24 -0
- data/src/node.rb +117 -0
- data/src/node_attributes.rb +27 -0
- data/src/node_visit.rb +52 -0
- data/src/renderer.rb +119 -0
- data/src/renderer_cursor.rb +18 -0
- data/src/renderer_draw.rb +28 -0
- data/src/renderer_image.rb +31 -0
- data/src/renderer_print.rb +40 -0
- data/src/screen.rb +96 -0
- data/src/screen_element.rb +59 -0
- data/src/screen_input.rb +43 -0
- data/src/screen_renderer.rb +53 -0
- data/src/style.rb +149 -0
- data/src/tco/colouring.rb +248 -0
- data/src/tco/config.rb +57 -0
- data/src/tco/palette.rb +603 -0
- data/src/tco/tco_termgui.rb +30 -0
- data/src/termgui.rb +29 -0
- data/src/util.rb +110 -0
- data/src/util/css.rb +98 -0
- data/src/util/css_query.rb +23 -0
- data/src/util/easing.rb +364 -0
- data/src/util/hash_object.rb +131 -0
- data/src/util/imagemagick.rb +27 -0
- data/src/util/justify.rb +20 -0
- data/src/util/unicode-categories.rb +572 -0
- data/src/util/wrap.rb +102 -0
- data/src/widget/button.rb +33 -0
- data/src/widget/checkbox.rb +47 -0
- data/src/widget/col.rb +30 -0
- data/src/widget/image.rb +106 -0
- data/src/widget/inline.rb +40 -0
- data/src/widget/input_number.rb +73 -0
- data/src/widget/inputbox.rb +85 -0
- data/src/widget/label.rb +33 -0
- data/src/widget/modal.rb +69 -0
- data/src/widget/row.rb +26 -0
- data/src/widget/selectbox.rb +100 -0
- data/src/widget/textarea.rb +54 -0
- data/src/xml/xml.rb +80 -0
- data/test/action_test.rb +34 -0
- data/test/box_test.rb +15 -0
- data/test/css_test.rb +39 -0
- data/test/editor/editor_base_test.rb +201 -0
- data/test/element_bounds_test.rb +77 -0
- data/test/element_box_test.rb +8 -0
- data/test/element_render_test.rb +124 -0
- data/test/element_style_test.rb +85 -0
- data/test/element_test.rb +10 -0
- data/test/emitter_test.rb +108 -0
- data/test/event_test.rb +19 -0
- data/test/focus_test.rb +37 -0
- data/test/geometry_test.rb +12 -0
- data/test/input_test.rb +47 -0
- data/test/key_test.rb +14 -0
- data/test/log_test.rb +21 -0
- data/test/node_test.rb +105 -0
- data/test/performance/performance1.rb +48 -0
- data/test/renderer_test.rb +74 -0
- data/test/renderer_test_rect.rb +4 -0
- data/test/screen_test.rb +58 -0
- data/test/style_test.rb +18 -0
- data/test/termgui_test.rb +10 -0
- data/test/test_all.rb +30 -0
- data/test/util_hash_object_test.rb +93 -0
- data/test/util_test.rb +26 -0
- data/test/widget/checkbox_test.rb +99 -0
- data/test/widget/col_test.rb +87 -0
- data/test/widget/inline_test.rb +40 -0
- data/test/widget/label_test.rb +94 -0
- data/test/widget/row_test.rb +40 -0
- data/test/wrap_test.rb +11 -0
- data/test/xml_test.rb +77 -0
- metadata +101 -1
data/src/node.rb
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
require_relative 'util'
|
2
|
+
require_relative 'node_attributes'
|
3
|
+
require_relative 'node_visit'
|
4
|
+
require_relative 'emitter'
|
5
|
+
|
6
|
+
module TermGui
|
7
|
+
# analog to HTML DOM Node class
|
8
|
+
# Ways of declaring node hierarchies by using parent and children props, or append_child or append_to methods. For declarative complex structures probably you want to use children
|
9
|
+
# `main = Row.new(parent: screen, height: 0.5, children: [Button.new(text: 'clickme', Col.new(width: 0.8, x: 0.2, children: [Label.new(text: 'hello')]))])`
|
10
|
+
# or
|
11
|
+
# `main = screen.append_child(Row.new(height: 0.5, children: [Button.new(text: 'clickme', Col.new(width: 0.8, x: 0.2, children: [Label.new(text: 'hello')]))]))`
|
12
|
+
# or
|
13
|
+
# `main = Row.new(height: 0.5, children: [Button.new(text: 'clickme', Col.new(width: 0.8, x: 0.2, children: [Label.new(text: 'hello')]))]).append_to(screen)`
|
14
|
+
class Node < Emitter
|
15
|
+
include NodeVisit
|
16
|
+
|
17
|
+
attr_reader :children, :text, :parent, :name
|
18
|
+
attr_writer :parent, :text
|
19
|
+
|
20
|
+
def initialize(**args)
|
21
|
+
@name = args[:name] || 'node'
|
22
|
+
@attributes = Attributes.new args[:attributes] || {}
|
23
|
+
@children = args[:children] || []
|
24
|
+
children.each { |child| child.parent = self }
|
25
|
+
@text = args[:text] || ''
|
26
|
+
args[:parent]&.append_children(self)
|
27
|
+
install(%i[after_render before_render])
|
28
|
+
end
|
29
|
+
|
30
|
+
# returns child so we can write: `button = screen.append_child Row.new(text: 'click me')`
|
31
|
+
def append_children(*children)
|
32
|
+
children.each do |child|
|
33
|
+
@children.push(child)
|
34
|
+
child.parent = self
|
35
|
+
end
|
36
|
+
children
|
37
|
+
end
|
38
|
+
|
39
|
+
def append_child(child)
|
40
|
+
(append_children child)[0]
|
41
|
+
end
|
42
|
+
|
43
|
+
def insert_children(index = 0, *children)
|
44
|
+
@children.insert(index, *children)
|
45
|
+
children
|
46
|
+
end
|
47
|
+
|
48
|
+
def prepend_child(child)
|
49
|
+
insert_child(0, child)
|
50
|
+
end
|
51
|
+
|
52
|
+
def insert_child(index, child)
|
53
|
+
(insert_children index, child)[0]
|
54
|
+
end
|
55
|
+
|
56
|
+
def remove_children(*children)
|
57
|
+
children.each do |child|
|
58
|
+
@children.delete(child)
|
59
|
+
child.parent = self
|
60
|
+
end
|
61
|
+
children
|
62
|
+
end
|
63
|
+
|
64
|
+
def remove_child(child)
|
65
|
+
(remove_children child)[0]
|
66
|
+
end
|
67
|
+
|
68
|
+
def append_to(parent)
|
69
|
+
parent.append_child(self)
|
70
|
+
self
|
71
|
+
end
|
72
|
+
|
73
|
+
def remove
|
74
|
+
parent&.remove_child(self)
|
75
|
+
self.parent = nil
|
76
|
+
end
|
77
|
+
|
78
|
+
def empty
|
79
|
+
children.each do |child|
|
80
|
+
child.parent = nil
|
81
|
+
end
|
82
|
+
@children = []
|
83
|
+
self
|
84
|
+
end
|
85
|
+
|
86
|
+
def attributes(attrs = nil)
|
87
|
+
attrs&.each_key { |key| set_attribute(key.to_s, attrs[key]) }
|
88
|
+
@attributes
|
89
|
+
end
|
90
|
+
|
91
|
+
def attributes=(attrs = nil)
|
92
|
+
attributes(attrs)
|
93
|
+
end
|
94
|
+
|
95
|
+
def set_attribute(name, value)
|
96
|
+
@attributes.set_attribute(name, value)
|
97
|
+
self
|
98
|
+
end
|
99
|
+
|
100
|
+
def get_attribute(name)
|
101
|
+
@attributes.get_attribute(name)
|
102
|
+
end
|
103
|
+
|
104
|
+
def to_s
|
105
|
+
"Node(name: #{name}, children: [#{children.map(&:to_s).join(', ')}])"
|
106
|
+
end
|
107
|
+
|
108
|
+
def pretty_print(d = 0)
|
109
|
+
"#{(' ' * d)}<#{name} #{(attributes.pairs.map { |p| "#{p[:name]}=#{pretty_print_attribute p[:value]}" }).join(' ')}>\n#{' ' * (d + 1)}#{text ? " #{text}\n#{' ' * d}" : ''}#{children.map { |c| c.pretty_print d + 1 }.join("\n" + (' ' * d))}\n#{(' ' * (d + 1))}</#{name}>"
|
110
|
+
end
|
111
|
+
|
112
|
+
def pretty_print_attribute(a)
|
113
|
+
a.respond_to?(:pretty_print) ? a.pretty_print : a.to_s
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
Node = TermGui::Node
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# Manages Node's attributes
|
2
|
+
class Attributes
|
3
|
+
def initialize(attrs = {})
|
4
|
+
@attrs = attrs
|
5
|
+
end
|
6
|
+
|
7
|
+
def names
|
8
|
+
@attrs.keys
|
9
|
+
end
|
10
|
+
|
11
|
+
def pairs
|
12
|
+
@attrs.keys.map { |n| { name: n, value: @attrs[n] } }
|
13
|
+
end
|
14
|
+
|
15
|
+
def set_attribute(name, value)
|
16
|
+
@attrs[name.to_sym] = value
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
def get_attribute(name)
|
21
|
+
@attrs[name.to_sym]
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_s
|
25
|
+
@attrs.to_s
|
26
|
+
end
|
27
|
+
end
|
data/src/node_visit.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
# adds node recirsive visit support and node query operations
|
2
|
+
module NodeVisit
|
3
|
+
def query_by_attribute(attr, value)
|
4
|
+
result = []
|
5
|
+
visit_node(self, proc { |n|
|
6
|
+
result.push n if n.attributes.get_attribute(attr) == value
|
7
|
+
false
|
8
|
+
})
|
9
|
+
result
|
10
|
+
end
|
11
|
+
|
12
|
+
def query_by_name(name)
|
13
|
+
result = []
|
14
|
+
visit_node(self, proc { |n|
|
15
|
+
result.push n if n.name == name
|
16
|
+
false
|
17
|
+
})
|
18
|
+
result
|
19
|
+
end
|
20
|
+
|
21
|
+
def query_one_by_attribute(attr, value)
|
22
|
+
result = nil
|
23
|
+
p = proc do |n|
|
24
|
+
if n.attributes.get_attribute(attr) == value
|
25
|
+
result = n
|
26
|
+
true
|
27
|
+
else
|
28
|
+
false
|
29
|
+
end
|
30
|
+
end
|
31
|
+
visit_node(self, p)
|
32
|
+
result
|
33
|
+
end
|
34
|
+
|
35
|
+
def visit(visitor, children_first = true)
|
36
|
+
visit_node(self, visitor, children_first)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# visit given node children bottom-up. If visitor returns truthy then visiting finishes
|
41
|
+
def visit_node(node, visitor, children_first = true)
|
42
|
+
result = nil
|
43
|
+
unless children_first
|
44
|
+
result = visitor.call node
|
45
|
+
return result if result
|
46
|
+
end
|
47
|
+
result = some(node.children, proc { |child| visit_node child, visitor, children_first })
|
48
|
+
return result if result
|
49
|
+
|
50
|
+
result = visitor.call node if children_first
|
51
|
+
result
|
52
|
+
end
|
data/src/renderer.rb
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
require_relative 'style'
|
2
|
+
require_relative 'key'
|
3
|
+
require_relative 'renderer_print'
|
4
|
+
require_relative 'renderer_cursor'
|
5
|
+
require_relative 'renderer_image'
|
6
|
+
require_relative 'renderer_draw'
|
7
|
+
|
8
|
+
module TermGui
|
9
|
+
# Responsible of (TODO: we should split Renderer into several delegate classes
|
10
|
+
# * build charsequences to render text on a position. these are directly write to $stdout by screen
|
11
|
+
# * maintain bitmap-like buffer of current screen state
|
12
|
+
# * manages current applied style
|
13
|
+
# TODO: add line, empty-rect and more drawing primitives
|
14
|
+
class Renderer
|
15
|
+
include RendererPrint
|
16
|
+
include RendererCursor
|
17
|
+
include RendererImage
|
18
|
+
include RendererDraw
|
19
|
+
|
20
|
+
attr_reader :width, :height, :buffer, :style
|
21
|
+
attr_writer :style, :no_buffer
|
22
|
+
|
23
|
+
def initialize(width = 80, height = 20, no_buffer = false)
|
24
|
+
@width = width
|
25
|
+
@height = height
|
26
|
+
@style = Style.new
|
27
|
+
@no_buffer = no_buffer
|
28
|
+
self.fast_colouring = true
|
29
|
+
unless no_buffer
|
30
|
+
@buffer = (0...@height).to_a.map do
|
31
|
+
(0...@width).to_a.map do
|
32
|
+
Pixel.new
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# all writing must be done using me
|
39
|
+
def write(x, y, s, style = nil)
|
40
|
+
if y < @height && y >= 0
|
41
|
+
# TODO: x could be negative now, trunc ch to respect screen bounds - if not negative values are printed at the right
|
42
|
+
if x < 0
|
43
|
+
s = s[[x * -1, s.length].min..s.length]
|
44
|
+
x = 0
|
45
|
+
end
|
46
|
+
unless @no_buffer
|
47
|
+
(x...[x + s.length, @width].min).to_a.each do |i|
|
48
|
+
@buffer[y][i].ch = s[i - x]
|
49
|
+
@buffer[y][i].style = (style || @style).clone
|
50
|
+
end
|
51
|
+
end
|
52
|
+
# apply the style after writing to the buffer so it don't contains escape secuences just the chars
|
53
|
+
s = style == nil ? s : style.print(s)
|
54
|
+
"#{move x, y + 1}#{s}" # TODO: investigate why y + 1
|
55
|
+
else
|
56
|
+
''
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def move(x, y)
|
61
|
+
Renderer.move x, y
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.move(x, y)
|
65
|
+
"#{CSI}#{y};#{x}H"
|
66
|
+
end
|
67
|
+
|
68
|
+
def text(x: 0, y: 0, text: ' ', style: nil)
|
69
|
+
write(x, y, text, style)
|
70
|
+
end
|
71
|
+
|
72
|
+
def rect(x: 0, y: 0, width: 5, height: 3, ch: Pixel.EMPTY_CH, style: nil)
|
73
|
+
s = []
|
74
|
+
ch = Pixel.EMPTY_CH if ch == nil
|
75
|
+
height.times do |y_|
|
76
|
+
s .push write(x, y + y_, ch * width, style).to_s
|
77
|
+
end
|
78
|
+
s.join('')
|
79
|
+
end
|
80
|
+
|
81
|
+
def clear
|
82
|
+
@style = Style.new
|
83
|
+
unless @no_buffer
|
84
|
+
@buffer.each_index do |y|
|
85
|
+
@buffer[y].each do |p|
|
86
|
+
p.ch = Pixel.EMPTY_CH
|
87
|
+
p.style.reset
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
"#{CSI}0m#{CSI}2J"
|
92
|
+
end
|
93
|
+
|
94
|
+
def style_assign(style)
|
95
|
+
@style.assign(style)
|
96
|
+
end
|
97
|
+
|
98
|
+
def fast_colouring=(value)
|
99
|
+
Style.fast_colouring(value)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Represents a pixel in renderer's buffer
|
104
|
+
class Pixel
|
105
|
+
attr_accessor :ch, :style
|
106
|
+
|
107
|
+
def self.EMPTY_CH
|
108
|
+
' '
|
109
|
+
end
|
110
|
+
|
111
|
+
def initialize(ch = Pixel.EMPTY_CH, style = Style.new)
|
112
|
+
@ch = ch
|
113
|
+
@style = style
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
Renderer = TermGui::Renderer
|
119
|
+
Pixel = TermGui::Pixel
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# takes care of cursor related ansi escape sequences
|
2
|
+
module RendererCursor
|
3
|
+
def cursor_save
|
4
|
+
"#{CSI}s"
|
5
|
+
end
|
6
|
+
|
7
|
+
def cursor_restore
|
8
|
+
"#{CSI}u"
|
9
|
+
end
|
10
|
+
|
11
|
+
def cursor_show
|
12
|
+
"#{CSI}?25h"
|
13
|
+
end
|
14
|
+
|
15
|
+
def cursor_hide
|
16
|
+
"#{CSI}?25l"
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'chunky_png'
|
2
|
+
|
3
|
+
# takes care of drawing shapes. Based on Image (uses chunky_png Canvas)
|
4
|
+
# TODO: we should use TermGui::Image and not ChunkyPNG's so we add value there too
|
5
|
+
module RendererDraw
|
6
|
+
def circle(x: nil, y: nil, radius: nil, stroke_ch: ' ', stroke: nil, fill: nil, fill_ch: stroke_ch)
|
7
|
+
canvas = draw_canvas
|
8
|
+
canvas.circle(x, y, radius, ChunkyPNG::Color::BLACK, ChunkyPNG::Color::WHITE)
|
9
|
+
radius = []
|
10
|
+
canvas.height.times do |y2|
|
11
|
+
canvas.width.times do |x2|
|
12
|
+
if canvas[x2, y2] == ChunkyPNG::Color::BLACK && stroke
|
13
|
+
radius.push text(x: x2, y: y2, text: stroke_ch, style: stroke)
|
14
|
+
elsif canvas[x2, y2] == ChunkyPNG::Color::WHITE && fill
|
15
|
+
radius.push text(x: x2, y: y2, text: fill_ch, style: fill)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
radius.join
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def draw_canvas
|
25
|
+
# TODO: reuse the canvas
|
26
|
+
ChunkyPNG::Canvas.new(width, height, ChunkyPNG::Color::TRANSPARENT)
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require_relative 'image'
|
2
|
+
|
3
|
+
# takes care rendering given Images
|
4
|
+
module RendererImage
|
5
|
+
# renders given Image or file path at given x, y coords. Currently only PNG format supported.
|
6
|
+
# by default bg attribute is used and space is printed for each pixel but this could be configured using fg, bg, and ch
|
7
|
+
# ch can be an array of chars in which case a random one is taken for each pixel
|
8
|
+
def image(x: 0, y: 0, image: nil, ch: ' ', style: Style.new, fg: false, bg: true, h: height - y, w: width - x,
|
9
|
+
transparent_color: nil) # if a [r,g,b] color is given, then alpha channel will be considered to mix colors accordingly
|
10
|
+
output = []
|
11
|
+
image = image.is_a?(String) ? TermGui::Image.new(image) : image
|
12
|
+
(y..[y + image.height - 1, height - 1].min).to_a.each do |y2|
|
13
|
+
(x..[x + image.width - 1, width - 1].min).to_a.each do |x2|
|
14
|
+
pixel = image.rgb(x2 - x, y2 - y, transparent_color)
|
15
|
+
style.bg = pixel if bg
|
16
|
+
style.fg = pixel if fg
|
17
|
+
output .push text(x: x2, y: y2, text: ch.is_a?(Array) ? ch.sample : ch, style: style) if x2 < w && y2 < h
|
18
|
+
end
|
19
|
+
end
|
20
|
+
output.join('')
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def mix_colors(fg, bg, alpha = 0.5)
|
26
|
+
r = (fg[0] * alpha + bg[0] * (1 - alpha)).to_i
|
27
|
+
g = (fg[1] * alpha + bg[1] * (1 - alpha)).to_i
|
28
|
+
b = (fg[2] * alpha + bg[2] * (1 - alpha)).to_i
|
29
|
+
[r, g, b]
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require_relative 'style'
|
2
|
+
require_relative 'key'
|
3
|
+
|
4
|
+
# takes care of printing renderer buffer in different ways
|
5
|
+
module RendererPrint
|
6
|
+
# prints current buffer as string
|
7
|
+
def print
|
8
|
+
s = []
|
9
|
+
@buffer.each_index do |y|
|
10
|
+
@buffer[y].each do |p|
|
11
|
+
s .push p.ch
|
12
|
+
end
|
13
|
+
s .push '\n'
|
14
|
+
end
|
15
|
+
s.join('')
|
16
|
+
end
|
17
|
+
|
18
|
+
def print_rows
|
19
|
+
rows = []
|
20
|
+
@buffer.each_index do |y|
|
21
|
+
line = []
|
22
|
+
@buffer[y].each do |p|
|
23
|
+
line .push p.ch
|
24
|
+
end
|
25
|
+
rows.push(line.join(''))
|
26
|
+
end
|
27
|
+
rows
|
28
|
+
end
|
29
|
+
|
30
|
+
# prints to stdout a representation in ruby string concatenated syntax so its easy for devs copy&paste for test asserts
|
31
|
+
def print_dev_stdout
|
32
|
+
print.split('\\n').each { |line| puts "'#{line}\\n' + " }
|
33
|
+
end
|
34
|
+
|
35
|
+
def print_dev
|
36
|
+
s = "'' + \n"
|
37
|
+
print.split('\\n').each { |line| s = "#{s}#{line}\\n' + \n" }
|
38
|
+
s + "''"
|
39
|
+
end
|
40
|
+
end
|
data/src/screen.rb
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
require_relative 'element'
|
2
|
+
require_relative 'renderer'
|
3
|
+
require_relative 'input'
|
4
|
+
require_relative 'event'
|
5
|
+
require_relative 'focus'
|
6
|
+
require_relative 'action'
|
7
|
+
require_relative 'util'
|
8
|
+
require_relative 'screen_element'
|
9
|
+
require_relative 'screen_input'
|
10
|
+
require_relative 'screen_renderer'
|
11
|
+
|
12
|
+
module TermGui
|
13
|
+
# Main user API entry point
|
14
|
+
# Manages instances of Input, Event, Renderer (by default disabling its buffer)
|
15
|
+
# Is a Node so new elements can be append_child
|
16
|
+
# Once `start`is called it will block execution and start an event loop
|
17
|
+
# on each interval user input is read and event listeners are called
|
18
|
+
class Screen < Node
|
19
|
+
include ScreenElement
|
20
|
+
include ScreenInput
|
21
|
+
include ScreenRenderer
|
22
|
+
attr_reader :width, :height, :input_stream, :output_stream, :renderer, :input, :event, :focus, :action
|
23
|
+
attr_accessor :silent
|
24
|
+
|
25
|
+
def initialize(
|
26
|
+
children: [], text: '', attributes: {}, exit_keys: %w[q C-c], no_exit_keys: false,
|
27
|
+
width: nil, height: nil, silent: false
|
28
|
+
)
|
29
|
+
super(name: 'screen', children: children, text: text, attributes: attributes, parent: nil)
|
30
|
+
install(%i[destroy after_destroy start after_start])
|
31
|
+
@width = width == nil ? terminal_width : width
|
32
|
+
@height = height == nil ? terminal_height : height
|
33
|
+
@input_stream = $stdin
|
34
|
+
@silent = silent
|
35
|
+
@exit_keys = exit_keys
|
36
|
+
@output_stream = $stdout
|
37
|
+
@renderer = Renderer.new(@width, @height)
|
38
|
+
@input = Input.new
|
39
|
+
@event = EventManager.new @input
|
40
|
+
@focus = FocusManager.new(root: self, event: @event)
|
41
|
+
@action = ActionManager.new(focus: @focus, event: @event)
|
42
|
+
@renderer.no_buffer = true
|
43
|
+
install_exit_keys unless no_exit_keys
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.new_for_testing(**args)
|
47
|
+
instance = new(args.merge(no_exit_keys: true, silent: true))
|
48
|
+
instance.renderer.no_buffer = false
|
49
|
+
instance
|
50
|
+
end
|
51
|
+
|
52
|
+
def terminal_width
|
53
|
+
$stdout.winsize[1]
|
54
|
+
rescue StandardError
|
55
|
+
80
|
56
|
+
end
|
57
|
+
|
58
|
+
def terminal_height
|
59
|
+
$stdout.winsize[0]
|
60
|
+
rescue StandardError
|
61
|
+
24
|
62
|
+
end
|
63
|
+
|
64
|
+
# start listening for user input. This starts an user input event loop
|
65
|
+
# that ends when screen.destroy is called
|
66
|
+
def start(clean: false)
|
67
|
+
emit :start
|
68
|
+
unless clean
|
69
|
+
clear
|
70
|
+
cursor_hide # TODO: move this to a CursorManager :start listener
|
71
|
+
render
|
72
|
+
end
|
73
|
+
emit :after_start
|
74
|
+
yield if block_given?
|
75
|
+
@input.start
|
76
|
+
end
|
77
|
+
|
78
|
+
def destroy
|
79
|
+
emit :destroy
|
80
|
+
@input.stop
|
81
|
+
cursor_show # TODO: move this to a CursorManager :destroy listener
|
82
|
+
emit :after_destroy
|
83
|
+
end
|
84
|
+
|
85
|
+
# writes directly to @output_stream. Shouldn't be used directly since these changes won't be tracked by the buffer.
|
86
|
+
def write(s)
|
87
|
+
@output_stream.write s unless @silent
|
88
|
+
s
|
89
|
+
end
|
90
|
+
|
91
|
+
def alert
|
92
|
+
puts "\a"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
Screen = TermGui::Screen
|