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,59 @@
1
+ require_relative 'geometry'
2
+
3
+ # so screen emulates an Element. TODO: make Screen < Element and get rid of width and height and all of this...
4
+ module ScreenElement
5
+ # complies with Element#render and also is capable of rendering given elements
6
+ def render(element = nil)
7
+ if element == self || element.nil?
8
+ children.each { |child| child.render self }
9
+ elsif !element.nil?
10
+ element.render self
11
+ end
12
+ end
13
+
14
+ def abs_x
15
+ 0
16
+ end
17
+
18
+ def abs_y
19
+ 0
20
+ end
21
+
22
+ def abs_width
23
+ @width
24
+ end
25
+
26
+ def style
27
+ @style ||= Style.new
28
+ @style
29
+ end
30
+
31
+ def style=(s)
32
+ @style = s
33
+ end
34
+
35
+ def final_style
36
+ style
37
+ end
38
+
39
+ def offset
40
+ v = get_attribute('offset')
41
+ unless v
42
+ v = Offset.new
43
+ set_attribute('offset', v)
44
+ end
45
+ v
46
+ end
47
+
48
+ def offset=(value)
49
+ set_attribute('offset', value)
50
+ end
51
+
52
+ def abs_height
53
+ @height
54
+ end
55
+
56
+ def root_screen
57
+ self
58
+ end
59
+ end
@@ -0,0 +1,43 @@
1
+ # adds Input related methods to screen
2
+ module ScreenInput
3
+ attr_accessor :exit_keys
4
+
5
+ # Analog to HTML DOM / Node.js setTimeout() using input event loop
6
+ # @param {Number} seconds
7
+ def set_timeout(seconds = @input.interval, listener = nil, &block)
8
+ the_listener = listener == nil ? block : listener
9
+ throw 'No listener provided' if the_listener == nil
10
+ @input.set_timeout(seconds, the_listener)
11
+ end
12
+
13
+ def clear_timeout(listener)
14
+ @input.clear_timeout(listener)
15
+ end
16
+
17
+ # Analog to HTML DOM / Node.js setInterval() using input event loop
18
+ # @param {Number} seconds
19
+ def set_interval(seconds = @input.interval, listener = nil, &block)
20
+ the_listener = listener == nil ? block : listener
21
+ throw 'No listener provided' if the_listener == nil
22
+ @input.set_interval(seconds, the_listener)
23
+ end
24
+
25
+ def clear_interval(listener)
26
+ @input.clear_interval(listener)
27
+ end
28
+
29
+ def install_exit_keys
30
+ return if @exit_keys_listener
31
+
32
+ @exit_keys_listener = @input.subscribe('key') do |e|
33
+ destroy if @exit_keys.include?(e.key)
34
+ end
35
+ end
36
+
37
+ def uninstall_exit_keys
38
+ return unless @exit_keys_listener
39
+
40
+ @input.off('key', @exit_keys_listener)
41
+ @exit_keys_listener = nil
42
+ end
43
+ end
@@ -0,0 +1,53 @@
1
+ # adds rendering related methods to screen
2
+ module ScreenRenderer
3
+ # renders given text at given position
4
+ def text(x: 0, y: 0, text: ' ', style: nil)
5
+ write @renderer.text(x: x, y: y, text: text, style: style)
6
+ end
7
+
8
+ def rect(x: 0, y: 0, width: self.width, height: self.height, ch: Pixel.EMPTY_CH, style: nil)
9
+ write @renderer.rect(x: x, y: y, width: width, height: height, ch: ch, style: style)
10
+ end
11
+
12
+ def image(x: 0, y: 0, image: nil, ch: Pixel.EMPTY_CH, style: Style.new, fg: false, bg: true, transparent_color: nil, h: height - y, w: width - x)
13
+ write @renderer.image(x: x, y: y, image: image, ch: ch || Pixel.EMPTY_CH, style: style, fg: fg, bg: bg, transparent_color: transparent_color, h: h, w: w)
14
+ end
15
+
16
+ def circle(x: nil, y: nil, radius: nil, stroke_ch: ' ', stroke: nil, fill: nil, fill_ch: stroke_ch)
17
+ write @renderer.circle(x: x, y: y, radius: radius, stroke_ch: stroke_ch, stroke: stroke, fill: fill, fill_ch: fill_ch)
18
+ end
19
+
20
+ def clear
21
+ @renderer.style = Style.new
22
+ write "#{@renderer.clear}#{@renderer.style.print}"
23
+ end
24
+
25
+ def style=(style)
26
+ @renderer.style = style
27
+ write @renderer.style.print
28
+ end
29
+
30
+ def box(x, y, width, height, border_style = :classic, style = nil, content = ' ')
31
+ self.style = style if style
32
+ box = draw_box(width: width, height: height, style: border_style, content: content)
33
+ (box.map.with_index do |line, index|
34
+ text(x: x, y: y + index, text: line, style: style)
35
+ end).join('')
36
+ end
37
+
38
+ def print
39
+ @renderer.print
40
+ end
41
+
42
+ def cursor_move(x, y)
43
+ write @renderer.move(x, y)
44
+ end
45
+
46
+ def cursor_show
47
+ write @renderer.cursor_show
48
+ end
49
+
50
+ def cursor_hide
51
+ write @renderer.cursor_hide
52
+ end
53
+ end
@@ -0,0 +1,149 @@
1
+ require_relative 'util'
2
+ require_relative 'util/hash_object'
3
+ require_relative 'tco/tco_termgui'
4
+
5
+ module TermGui
6
+ # refers to properties directly implemented using ansi escape codes
7
+ # responsible of printing escape ansi codes for style
8
+ # Styles are data objects, supporting hash to instantiate, assign, equals, print
9
+ class BaseStyle
10
+ include HashObject
11
+
12
+ attr_accessor :fg, :bg, :underline, :bold, :blink, :inverse, :fraktur, :framed
13
+
14
+ def initialize(fg: nil, bg: nil, bold: nil, blink: nil, inverse: nil, underline: nil, framed: nil, fraktur: nil, bright: nil, wrap: nil, border: nil, padding: nil, style: nil)
15
+ # TODO: for some reason **args is not working here that's why we have all subclasses props
16
+ @fg = fg
17
+ @bg = bg
18
+ @bold = bold || bright
19
+ @blink = blink
20
+ @inverse = inverse
21
+ @underline = underline
22
+ @fraktur = fraktur
23
+ @framed = framed
24
+ end
25
+
26
+ # Prints the style as escape sequences.
27
+ # This method shouln't be overriden by subclasses since it only makes sense for basic properties defined here.
28
+ def print(s = nil)
29
+ if s == nil
30
+ TermGui.open_style(self)
31
+ else
32
+ TermGui.print(s, self)
33
+ end
34
+ end
35
+
36
+ def self.fast_colouring(value)
37
+ TermGui.fast_colouring(value)
38
+ end
39
+
40
+ def reset
41
+ @bg = @fg = @wrap = @border = nil
42
+ end
43
+
44
+ # returns true if self has the same properties of given hash or Style and each property value is equals (comparission using ==)
45
+ def equals(style)
46
+ object_equal(self, BaseStyle.from_hash(style))
47
+ end
48
+
49
+ # if a hash is given returns a new Style instance with given properties. If an Style instance if given, returns it.
50
+ # It also ensures focus, action and enter properties are defined cloning self if not
51
+ def self.from_hash(obj)
52
+ if obj == nil
53
+ return nil
54
+ elsif obj.instance_of?(Hash)
55
+ r = merge_hash_into_object obj, new
56
+ else
57
+ r = obj
58
+ end
59
+
60
+ if r.is_a? Style
61
+ r.focus = r.focus || r.clone
62
+ r.border = Border.from_hash(r.border) if r.border
63
+ r.action = r.action || r.clone
64
+ r.enter = r.enter || r.clone
65
+ end
66
+ r
67
+ end
68
+
69
+ def pretty_print(delete_nil = true, delete_empty = true)
70
+ h = to_hash
71
+ h.keys.each do |k|
72
+ h.delete k if delete_nil && h[k] == nil
73
+ if delete_empty && (h[k].respond_to?(:to_hash) || h[k].is_a?(Hash)) && object_variables_to_hash(h[k]).keys.reject { |k| h[k] == nil }.empty?
74
+ h.delete k
75
+ end
76
+ end
77
+ "{#{h.keys.map { |k| "#{k}: #{pretty_print_value(h[k])}" }.join(', ')}}" .split(/, [^\s]+: \{\}/).join('')
78
+ end
79
+
80
+ def pretty_print_value(v)
81
+ v.is_a?(String) ? v : v.respond_to?(:pretty_print) ? v.pretty_print : v.to_s
82
+ end
83
+
84
+ def self.from_json(s)
85
+ r = from_hash(json_parse(s))
86
+ if r.is_a? Style
87
+ r.border = from_hash(r.border || new)
88
+ r.focus = from_hash(r.focus || new)
89
+ r.enter = from_hash(r.enter || new)
90
+ r.action = from_hash(r.action || new)
91
+ end
92
+ r
93
+ end
94
+
95
+ def bright
96
+ @bold
97
+ end
98
+
99
+ def bright=(value)
100
+ @bold = value
101
+ end
102
+ end
103
+
104
+ # Element style. This is the class of `element.style` - `get_attribute('style')``
105
+ class Style < BaseStyle
106
+ attr_accessor :border, :wrap, :padding, :focus, :enter, :action
107
+
108
+ def initialize(**args)
109
+ super
110
+ @wrap = args[:wrap]
111
+ # TODO: move this border checking & init to hash_object
112
+ if args[:border].nil?
113
+ @border = nil
114
+ elsif args[:border].instance_of? Border
115
+ @border = args[:border]
116
+ elsif args[:border].instance_of? Hash
117
+ @border = Border.new
118
+ @border.assign(args[:border])
119
+ end
120
+ padding = args[:padding]
121
+ focus = args[:focus] || clone
122
+ enter = args[:enter] || clone
123
+ action = args[:action] || clone
124
+
125
+ @padding = padding
126
+ @focus = focus
127
+ @enter = enter
128
+ @action = action
129
+ end
130
+ end
131
+
132
+ # style for the border
133
+ class Border < BaseStyle
134
+ attr_reader :style
135
+
136
+ def initialize(**args)
137
+ super
138
+ @style = args[:style]&.to_s || 'single'
139
+ end
140
+
141
+ def style=(style)
142
+ @style = style&.to_s
143
+ end
144
+ end
145
+ end
146
+
147
+ BaseStyle = TermGui::BaseStyle
148
+ Border = TermGui::Border
149
+ Style = TermGui::Style
@@ -0,0 +1,248 @@
1
+ # rubocop:disable Layout/EndAlignment
2
+
3
+ # adapted from tco
4
+ # tco - terminal colouring application and library
5
+ # Copyright (c) 2013, 2014 Radek Pazdera
6
+
7
+ require_relative 'palette'
8
+
9
+ module Tco
10
+ class Colouring
11
+ ANSI_FG_BASE = 30
12
+ ANSI_BG_BASE = 40
13
+
14
+ attr_reader :palette
15
+
16
+ def initialize(configuration)
17
+ @palette = Palette.new configuration.options['palette']
18
+ @output_type = configuration.options['output']
19
+ @disabled = configuration.options['disabled']
20
+
21
+ configuration.colour_values.each do |id, value|
22
+ @palette.set_colour_value(parse_colour_id(id), parse_rgb_value(value))
23
+ end
24
+
25
+ @names = {}
26
+ configuration.names.each do |name, colour_def|
27
+ @names[name] = resolve_colour_def colour_def
28
+ end
29
+
30
+ @styles = {}
31
+ configuration.styles.each do |name, style|
32
+ @styles[name] = Style.new(resolve_colour_def(style[:fg]),
33
+ resolve_colour_def(style[:bg]),
34
+ style[:bright], style[:underline])
35
+ end
36
+ end
37
+
38
+ # Decorate a string according to the style passed. The input string
39
+ # is processed line-by-line (the escape sequences are added to each
40
+ # line). This is due to some problems I've been having with some
41
+ # terminal emulators not handling multi-line coloured sequences well.
42
+ def decorate(string, style)
43
+ # (fg, bg, bright, underline)
44
+ # fg = style.fg
45
+ # bg = style.bg
46
+ # bright = style.bright
47
+ # underline = style.underline
48
+ return string if !STDOUT.isatty || @output_type == :raw || @disabled
49
+
50
+ fg = get_colour_instance style.fg
51
+ bg = get_colour_instance style.bg
52
+
53
+ output = []
54
+ lines = string.lines.map(&:chomp)
55
+ lines = [''] if lines.length.zero?
56
+ lines.each do |line|
57
+ unless line.length < 0
58
+ line = case @palette.type
59
+ when 'ansi' then colour_ansi line, fg, bg
60
+ when 'extended' then colour_extended line, fg, bg
61
+ else raise "Unknown palette '#{@palette.type}'."
62
+ end
63
+
64
+ line = e(1) + line if style.bright
65
+ line = e(4) + line if style.underline
66
+ line = e(5) + line if style.blink
67
+ line = e(7) + line if style.inverse
68
+ line = e(20) + line if style.fraktur
69
+ line = e(51) + line if style.framed
70
+
71
+ if (style.bright || style.underline || style.blink || style.inverse || style.fraktur || style.framed) && (fg == nil) && (bg == nil)
72
+ line << e(0)
73
+ end
74
+ end
75
+
76
+ output.push line
77
+ end
78
+
79
+ output << '' if string =~ /\n$/
80
+ output.join "\n"
81
+ end
82
+
83
+ def get_style(name)
84
+ raise "Style '#{name}' not found." unless @styles.key? name
85
+
86
+ @styles[name]
87
+ end
88
+
89
+ def set_output(output_type)
90
+ raise "Output '#{output_type}' not supported." unless %i[term raw].include? output_type
91
+
92
+ @output_type = output_type
93
+ end
94
+
95
+ def get_best_font_colour(background)
96
+ black = Tco::Colour.new([0, 0, 0])
97
+ white = Tco::Colour.new([255, 255, 255])
98
+
99
+ if background.is_a?(Colour) &&
100
+ (background - black).abs < (background - white).abs
101
+ return white
102
+ end
103
+
104
+ black
105
+ end
106
+
107
+ def get_colour_instance(value)
108
+ if value.is_a?(String)
109
+ resolve_colour_def value
110
+ elsif value.is_a?(Symbol)
111
+ resolve_colour_def value.to_s
112
+ elsif value.is_a?(Array)
113
+ Colour.new value
114
+ elsif value.is_a?(Colour) || value.is_a?(Unknown)
115
+ value
116
+ elsif value == nil
117
+ nil
118
+ else
119
+ raise "Colour value type '#{value.class}' not supported."
120
+ end
121
+ end
122
+
123
+ private
124
+
125
+ def e(seq)
126
+ if @output_type == :raw
127
+ "\\033[#{seq}m"
128
+ else
129
+ "\033[#{seq}m"
130
+ end
131
+ end
132
+
133
+ def colour_ansi(string, fg = nil, bg = nil)
134
+ unless fg == nil
135
+ colour_id = if fg.is_a? Unknown
136
+ fg.id
137
+ else
138
+ @palette.match_colour(fg)
139
+ end
140
+ string = e(colour_id + 30) + string
141
+ end
142
+
143
+ unless bg == nil
144
+ colour_id = if bg.is_a? Unknown
145
+ bg.id
146
+ else
147
+ @palette.match_colour(bg)
148
+ end
149
+ string = e(colour_id + 40) + string
150
+ end
151
+
152
+ string << e(0) unless (fg == nil) && (bg == nil)
153
+
154
+ string
155
+ end
156
+
157
+ def colour_extended(string, fg = nil, bg = nil)
158
+ unless fg == nil
159
+ colour_id = if fg.is_a? Unknown
160
+ fg.id
161
+ else
162
+ @palette.match_colour(fg)
163
+ end
164
+ string = e("38;5;#{colour_id}") + string
165
+ end
166
+
167
+ unless bg == nil
168
+ colour_id = if bg.is_a? Unknown
169
+ bg.id
170
+ else
171
+ @palette.match_colour(bg)
172
+ end
173
+ string = e("48;5;#{colour_id}") + string
174
+ end
175
+
176
+ string << e(0) unless (fg == nil) && (bg == nil)
177
+
178
+ string
179
+ end
180
+
181
+ def parse_colour_id(id_in_string)
182
+ id = String.new(id_in_string)
183
+ if id[0] == '@'
184
+ id[0] = ''
185
+ return id.to_i
186
+ end
187
+
188
+ raise "Invalid colour id #{id_in_string}."
189
+ end
190
+
191
+ def parse_rgb_value(rgb_value_in_string)
192
+ error_msg = "Invalid RGB value '#{rgb_value_in_string}'."
193
+ rgb_value = String.new rgb_value_in_string
194
+ if rgb_value[0] == '#'
195
+ rgb_value[0] = ''
196
+ elsif rgb_value[0..1] == '0x'
197
+ rgb_value[0..1] = ''
198
+ else
199
+ raise error_msg
200
+ end
201
+
202
+ raise error_msg unless rgb_value =~ /^[0-9a-fA-F]+$/
203
+
204
+ case rgb_value.length
205
+ when 3
206
+ rgb_value.scan(/./).map { |c| c.to_i 16 }
207
+ when 6
208
+ rgb_value.scan(/../).map { |c| c.to_i 16 }
209
+ else
210
+ raise error_msg
211
+ end
212
+ end
213
+
214
+ def resolve_colour_name(name)
215
+ raise "Name '#{name}' not found." unless @names.key? name
216
+
217
+ @names[name]
218
+ end
219
+
220
+ def resolve_colour_def(colour_def)
221
+ return nil if colour_def == '' || colour_def == 'default'
222
+
223
+ begin
224
+ id = parse_colour_id colour_def
225
+ if @palette.is_known? id
226
+ Colour.new @palette.get_colour_value id
227
+ else
228
+ Unknown.new id
229
+ end
230
+ rescue RuntimeError
231
+ begin
232
+ Colour.new parse_rgb_value colour_def
233
+ rescue RuntimeError
234
+ begin
235
+ colour_def = resolve_colour_name colour_def
236
+ if colour_def.is_a? String
237
+ resolve_colour_def colour_def
238
+ else
239
+ colour_def
240
+ end
241
+ rescue RuntimeError
242
+ raise "Invalid colour definition '#{colour_def}'."
243
+ end
244
+ end
245
+ end
246
+ end
247
+ end
248
+ end