termgui 0.0.5 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +14 -0
  3. data/LICENSE +19 -0
  4. data/README.md +321 -0
  5. data/TODO.md +259 -0
  6. data/src/action.rb +58 -0
  7. data/src/box.rb +90 -0
  8. data/src/color.rb +174 -0
  9. data/src/cursor.rb +69 -0
  10. data/src/editor/editor_base.rb +152 -0
  11. data/src/editor/editor_base_handlers.rb +116 -0
  12. data/src/element.rb +61 -0
  13. data/src/element_bounds.rb +111 -0
  14. data/src/element_box.rb +64 -0
  15. data/src/element_render.rb +102 -0
  16. data/src/element_style.rb +51 -0
  17. data/src/emitter.rb +102 -0
  18. data/src/emitter_state.rb +19 -0
  19. data/src/enterable.rb +93 -0
  20. data/src/event.rb +92 -0
  21. data/src/focus.rb +102 -0
  22. data/src/geometry.rb +53 -0
  23. data/src/image.rb +60 -0
  24. data/src/input.rb +85 -0
  25. data/src/input_grab.rb +17 -0
  26. data/src/input_time.rb +97 -0
  27. data/src/key.rb +114 -0
  28. data/src/log.rb +24 -0
  29. data/src/node.rb +117 -0
  30. data/src/node_attributes.rb +27 -0
  31. data/src/node_visit.rb +52 -0
  32. data/src/renderer.rb +119 -0
  33. data/src/renderer_cursor.rb +18 -0
  34. data/src/renderer_draw.rb +28 -0
  35. data/src/renderer_image.rb +31 -0
  36. data/src/renderer_print.rb +40 -0
  37. data/src/screen.rb +96 -0
  38. data/src/screen_element.rb +59 -0
  39. data/src/screen_input.rb +43 -0
  40. data/src/screen_renderer.rb +53 -0
  41. data/src/style.rb +149 -0
  42. data/src/tco/colouring.rb +248 -0
  43. data/src/tco/config.rb +57 -0
  44. data/src/tco/palette.rb +603 -0
  45. data/src/tco/tco_termgui.rb +30 -0
  46. data/src/termgui.rb +29 -0
  47. data/src/util.rb +110 -0
  48. data/src/util/css.rb +98 -0
  49. data/src/util/css_query.rb +23 -0
  50. data/src/util/easing.rb +364 -0
  51. data/src/util/hash_object.rb +131 -0
  52. data/src/util/imagemagick.rb +27 -0
  53. data/src/util/justify.rb +20 -0
  54. data/src/util/unicode-categories.rb +572 -0
  55. data/src/util/wrap.rb +102 -0
  56. data/src/widget/button.rb +33 -0
  57. data/src/widget/checkbox.rb +47 -0
  58. data/src/widget/col.rb +30 -0
  59. data/src/widget/image.rb +106 -0
  60. data/src/widget/inline.rb +40 -0
  61. data/src/widget/input_number.rb +73 -0
  62. data/src/widget/inputbox.rb +85 -0
  63. data/src/widget/label.rb +33 -0
  64. data/src/widget/modal.rb +69 -0
  65. data/src/widget/row.rb +26 -0
  66. data/src/widget/selectbox.rb +100 -0
  67. data/src/widget/textarea.rb +54 -0
  68. data/src/xml/xml.rb +80 -0
  69. data/test/action_test.rb +34 -0
  70. data/test/box_test.rb +15 -0
  71. data/test/css_test.rb +39 -0
  72. data/test/editor/editor_base_test.rb +201 -0
  73. data/test/element_bounds_test.rb +77 -0
  74. data/test/element_box_test.rb +8 -0
  75. data/test/element_render_test.rb +124 -0
  76. data/test/element_style_test.rb +85 -0
  77. data/test/element_test.rb +10 -0
  78. data/test/emitter_test.rb +108 -0
  79. data/test/event_test.rb +19 -0
  80. data/test/focus_test.rb +37 -0
  81. data/test/geometry_test.rb +12 -0
  82. data/test/input_test.rb +47 -0
  83. data/test/key_test.rb +14 -0
  84. data/test/log_test.rb +21 -0
  85. data/test/node_test.rb +105 -0
  86. data/test/performance/performance1.rb +48 -0
  87. data/test/renderer_test.rb +74 -0
  88. data/test/renderer_test_rect.rb +4 -0
  89. data/test/screen_test.rb +58 -0
  90. data/test/style_test.rb +18 -0
  91. data/test/termgui_test.rb +10 -0
  92. data/test/test_all.rb +30 -0
  93. data/test/util_hash_object_test.rb +93 -0
  94. data/test/util_test.rb +26 -0
  95. data/test/widget/checkbox_test.rb +99 -0
  96. data/test/widget/col_test.rb +87 -0
  97. data/test/widget/inline_test.rb +40 -0
  98. data/test/widget/label_test.rb +94 -0
  99. data/test/widget/row_test.rb +40 -0
  100. data/test/wrap_test.rb +11 -0
  101. data/test/xml_test.rb +77 -0
  102. metadata +101 -1
@@ -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
@@ -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
@@ -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
@@ -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