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,102 @@
1
+ # taken from https://github.com/pazdera/word_wrap/blob/master/lib/word_wrap/wrapper.rb
2
+ #
3
+ # Copyright (c) 2014, 2015 Radek Pazdera
4
+ # Distributed under the MIT License
5
+ #
6
+ # TODO: Refactor similar passages out of the two functions into a common one
7
+ class Wrapper
8
+ def initialize(text, width)
9
+ @text = text
10
+ @width = width
11
+ end
12
+
13
+ def fit
14
+ lines = []
15
+ next_line = ''
16
+ @text.lines do |line|
17
+ line.chomp! "\n"
18
+ if line.empty?
19
+ unless next_line.empty?
20
+ lines.push next_line
21
+ next_line = ''
22
+ end
23
+ lines.push ''
24
+ end
25
+
26
+ words = line.split ' '
27
+
28
+ words.each do |word|
29
+ word.chomp! "\n"
30
+
31
+ if next_line.length + word.length < @width
32
+ if !next_line.empty?
33
+ next_line << ' ' << word
34
+ else
35
+ next_line = word
36
+ end
37
+ else
38
+ if word.length >= @width
39
+ lines.push next_line unless next_line == ''
40
+ lines.push word
41
+ next_line = ''
42
+ else
43
+ lines.push next_line
44
+ next_line = word
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ lines.push next_line
51
+ if next_line.length <= 0
52
+ lines.join("\n")
53
+ else
54
+ lines.join("\n") + "\n"
55
+ end
56
+ end
57
+
58
+ def wrap
59
+ output = []
60
+
61
+ @text.lines do |line|
62
+ line.chomp! "\n"
63
+ if line.length > @width
64
+ new_lines = split_line(line, @width)
65
+ while new_lines.length > 1 && new_lines[1].length > @width
66
+ output.push new_lines[0]
67
+ new_lines = split_line new_lines[1], @width
68
+ end
69
+ output += new_lines
70
+ else
71
+ output.push line
72
+ end
73
+ end
74
+ output.map(&:rstrip!)
75
+ output.join("\n") + "\n"
76
+ end
77
+
78
+ def split_line(line, width)
79
+ at = line.index(/\s/)
80
+ last_at = at
81
+
82
+ while !at.nil? && at < width
83
+ last_at = at
84
+ at = line.index(/\s/, last_at + 1)
85
+ end
86
+
87
+ if last_at.nil?
88
+ [line]
89
+ else
90
+ [line[0, last_at], line[last_at + 1, line.length]]
91
+ end
92
+ end
93
+ end
94
+
95
+ def wrap_text(text, width)
96
+ lines = text.split("\n")
97
+ a = lines.map do |line|
98
+ w = Wrapper.new(line, width).wrap
99
+ w.split("\n")
100
+ end
101
+ a.flatten
102
+ end
@@ -0,0 +1,33 @@
1
+ require_relative 'label'
2
+
3
+ module TermGui
4
+ module Widget
5
+ # A button widget. Usage:
6
+ # Button.new(text: 'click me', style: {bg: 'blue'}, action: proc {|e| p 'actioned!'})
7
+ class Button < Label
8
+ def initialize(**args, &block)
9
+ super
10
+ @name = 'button'
11
+ install(:action)
12
+ set_attribute(:focusable, true)
13
+ end
14
+
15
+ def default_style
16
+ s = super
17
+ s.border = Border.new(fg: '#779966')
18
+ s.bg = '#336688'
19
+ s.fg = '#111111'
20
+ s.focus&.fg = 'red'
21
+ s.focus.bold = true
22
+ s.focus.underline = true
23
+ s.focus&.bg = 'grey'
24
+ s.focus&.border = Border.new(fg: 'green')
25
+ s.action&.bg = 'red'
26
+ s.action&.border = Border.new(fg: 'magenta', bold: true, bg: 'white')
27
+ s
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ Button = TermGui::Widget::Button
@@ -0,0 +1,47 @@
1
+ require_relative 'button'
2
+ require_relative '../log'
3
+ require_relative '../util'
4
+ require_relative '../screen'
5
+
6
+ module TermGui
7
+ module Widget
8
+ # Similar to HTMLInputElement type="checkbox"
9
+ class CheckBox < Button
10
+ def initialize(**args)
11
+ super
12
+ @name = 'checkbox'
13
+ set_attribute('action-keys', %w[enter space])
14
+ set_attribute('label', text || get_attribute('label') || unique('Option'))
15
+ update_text get_attribute('value') || get_attribute('checked') || args[:value] || args[:checked] || false
16
+ on(:action) do |e|
17
+ update_text !get_attribute('value')
18
+ render
19
+ trigger(:input, TermGui::InputEvent.new(self, value, e))
20
+ trigger(:change, TermGui::ChangeEvent.new(self, value, e))
21
+ end
22
+ end
23
+
24
+ def value
25
+ get_attribute('value')
26
+ end
27
+
28
+ def value=(v)
29
+ update_text v
30
+ end
31
+
32
+ def update_text(v = nil)
33
+ set_attribute('value', v) unless v == nil
34
+ self.text = "#{get_attribute('value') ? '[x]' : '[ ]'} #{get_attribute('label')}"
35
+ end
36
+
37
+ def default_style
38
+ s = super
39
+ s.border = nil
40
+ s.action = nil
41
+ s
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ CheckBox = TermGui::Widget::CheckBox
@@ -0,0 +1,30 @@
1
+ require_relative '../element'
2
+ module TermGui
3
+ module Widget
4
+ # Column container. A column child is rendered at the bottom of the previous child - all of them in one column.
5
+ # By default it will have height==0.999
6
+ class Col < Element
7
+ attr_accessor :gap
8
+ def initialize(**args)
9
+ # p(({height: 0.99999}.merge(args))[:height])
10
+ super({ height: 0.9999999 }.merge(args))
11
+ # p height, abs_height
12
+ @name = 'col'
13
+ @gap = args[:gap]||0
14
+ end
15
+
16
+ def layout
17
+ # init_y = abs_content_y
18
+ last_y = abs_content_y
19
+ @children.each do |c|
20
+ # last_y += 1 if c.style.border
21
+ c.abs_y = last_y
22
+ last_y += c.abs_height + @gap
23
+ # last_y += 1 if c.style.border
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ Col = TermGui::Widget::Col
@@ -0,0 +1,106 @@
1
+ require_relative '../termgui'
2
+
3
+ module TermGui
4
+ module Widget
5
+ # analog to HTMLImageElement. Note that the image will be loaded only on render. Also it will be resized according to abs_content
6
+ # Properties:
7
+ # src: file path or TermGui::Image ot load
8
+ # transparent_color color to blend transparent pixels, by default style.bg
9
+ # ignore_alpha. if true alpha channel is ignored (could be faster)
10
+ # use_bg (true by default) will paint pixels using style.bg
11
+ # use_fg (false by default) will paint using style.fg
12
+ class Image < Element
13
+ attr_accessor :pan_x, :pan_y, :zoom
14
+ def initialize(**args)
15
+ super
16
+ @name = 'image'
17
+ @src = args[:src]
18
+ @use_bg = args[:use_bg] == nil ? true : args[:use_bg]
19
+ @use_fg = args[:use_fg] == nil ? false : args[:use_fg]
20
+ @transparent_color = args[:transparent_color]
21
+ @ignore_alpha = args[:ignore_alpha]
22
+ @zoom = args[:zoom] || 1.0
23
+ @pan_x = args[:pan_x] || 0.0
24
+ @pan_y = args[:pan_y] || 0.0
25
+ @chs = args[:chs] || [get_attribute('ch') || ' ']
26
+ @x_factor = args[:x_factor] || 2.0 # to improve non squared terminal "pixels"
27
+ @y_factor = args[:y_factor] || 1.2
28
+ end
29
+
30
+ def image
31
+ if !@image && @src
32
+ @image = @src.is_a?(String) ? TermGui::Image.new(@src) : @src
33
+ end
34
+ factor = (@image.width * 2.0 - abs_content_width) < (@image.height - abs_content_height) ?
35
+ @zoom * @x_factor * @image.width.to_f / abs_content_width.to_f :
36
+ @zoom * @y_factor * @image.height.to_f / abs_content_height.to_f
37
+
38
+ @resized_image = @image.scale(
39
+ width: (@image.width.to_f * @x_factor / factor).to_i,
40
+ height: (@image.height.to_f * @y_factor / factor).to_i
41
+ )
42
+ # root_screen.text(text: " #{[factor, @image.height, @image.width, @resized_image.width, @resized_image.height, abs_content_width,abs_content_height, abs_content_x, abs_content_y]}", y: 3, x: 55)
43
+ if @pan_x + @pan_y > 0
44
+ @resized_image = @resized_image.crop(
45
+ x: (@resized_image.width.to_f * @pan_x).to_i,
46
+ y: (@resized_image.height.to_f * @pan_y).to_i,
47
+ width: (@resized_image.width.to_f - @resized_image.width.to_f * @pan_x).to_i,
48
+ height: (@resized_image.height.to_f - @resized_image.height.to_f * @pan_y).to_i
49
+ )
50
+ end
51
+ @image
52
+ end
53
+
54
+ def chs=(v)
55
+ if @chs.join != v.join
56
+ @chs = v
57
+ self.dirty = true
58
+ end
59
+ end
60
+
61
+ def original_image
62
+ @image
63
+ end
64
+
65
+ def current_image
66
+ @resized_image
67
+ end
68
+
69
+ def render_self(screen)
70
+ [
71
+ super,
72
+ image ? screen.image(
73
+ x: abs_content_x,
74
+ y: abs_content_y,
75
+ w: abs_content_width,
76
+ h: abs_content_height,
77
+ transparent_color: !@ignore_alpha ? TermGui.to_rgb(@transparent_color || final_style.bg || '#000000') : nil,
78
+ image: @resized_image,
79
+ bg: @use_bg,
80
+ fg: @use_fg,
81
+ style: final_style,
82
+ ch: @chs
83
+ ) : ''
84
+ ].join('')
85
+ end
86
+
87
+ def src=(v)
88
+ @src = v
89
+ @image = nil
90
+ refresh
91
+ end
92
+
93
+ def refresh(force_render = false)
94
+ @resized_image = nil
95
+ image
96
+ if force_render
97
+ self.dirty = true
98
+ clear
99
+ render
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+
106
+ Image = TermGui::Widget::Image
@@ -0,0 +1,40 @@
1
+ require_relative '../element'
2
+ module TermGui
3
+ module Widget
4
+ # an inline layout - similar to HTML display: inline - so I can add arbitrary elements.
5
+ # it will break when there is no more space and resize itself.
6
+ # TODO: almost there...
7
+ class Inline < Element
8
+ attr_accessor :horizontal_gap, :vertical_gap
9
+ def initialize(**args)
10
+ super({ width: 0.9999999 }.merge(args))
11
+ @name = 'inline'
12
+ @horizontal_gap = args[:horizontal_gap]||0
13
+ @vertical_gap = args[:vertical_gap]||0
14
+ end
15
+
16
+ def layout
17
+ last_y = abs_content_y
18
+ row_max_height = 1+ @vertical_gap
19
+ last_x = abs_content_x
20
+ total_height=0
21
+ @children.each do |c|
22
+ c.abs_x = last_x
23
+ c.abs_y = last_y
24
+ row_max_height = [row_max_height, c.abs_height].max
25
+ last_x += c.abs_width + @horizontal_gap
26
+ if last_x + @horizontal_gap+c.abs_width > abs_content_y + abs_content_width
27
+ last_x = abs_content_x
28
+ last_y += row_max_height + @vertical_gap
29
+ total_height+=row_max_height+@vertical_gap
30
+ row_max_height = 1+ @vertical_gap
31
+ end
32
+ end
33
+ self.height = [total_height + abs_padding.top + abs_padding.bottom + row_max_height, self.abs_height].max ##TODO borders
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ Inline = TermGui::Widget::Inline
40
+
@@ -0,0 +1,73 @@
1
+ require_relative 'button'
2
+ require_relative '../enterable'
3
+ require_relative '../util'
4
+ require_relative '../screen'
5
+
6
+ module TermGui
7
+ module Widget
8
+ # analog to HTMLInputElement type="number"
9
+ class InputNumber < Button
10
+ include Enterable
11
+ def initialize(**args)
12
+ super
13
+ @name = 'input-number'
14
+ set_attribute('escape-on-blur', get_attribute('escape-on-blur') == nil ? true : get_attribute('escape-on-blur'))
15
+ set_attribute('action-on-focus', get_attribute('action-on-focus') == nil ? true : get_attribute('action-on-focus'))
16
+ end
17
+
18
+ def value
19
+ v = parse_float(text)
20
+ v == nil ? v : (v.to_i - v == 0 ? v.to_i : v)
21
+ end
22
+
23
+ def value=(v)
24
+ self.text = v.to_s
25
+ end
26
+
27
+ def handle_key(event)
28
+ if !super(event)
29
+ if event.key == 'up'
30
+ on_input value + 1, event
31
+ true
32
+ elsif event.key == 'down'
33
+ on_input value - 1, event
34
+ true
35
+ elsif numeric? event.key
36
+ self.text = text + event.key
37
+ if parse_float(text) == nil
38
+ render
39
+ else
40
+ on_input value, event
41
+ end
42
+ true
43
+ elsif event.key == 'backspace'
44
+ self.text = text.slice(0, [text.length - 1, 0].max)
45
+ if parse_float(text) == nil
46
+ render
47
+ else
48
+ on_input value, event
49
+ end
50
+ true
51
+ end
52
+ else
53
+ true
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ InputNumber = TermGui::Widget::InputNumber
61
+
62
+ # # p parse_float('9.')
63
+
64
+ # sb = nil
65
+ # s = Screen.new(children: [
66
+ # Button.new(text: 'hello', x: 0.7, y: 0.6, action: proc { |e|
67
+ # e.target.text = sb.value.to_s
68
+ # e.target.render
69
+ # }),
70
+ # (sb = InputNumber.new(x: 2, y: 1, width: 0.5, height: 0.5, value: 12))
71
+ # ])
72
+
73
+ # s.start
@@ -0,0 +1,85 @@
1
+ require_relative 'button'
2
+ require_relative '../enterable'
3
+ require_relative '../log'
4
+ require_relative '../cursor'
5
+
6
+ module TermGui
7
+ module Widget
8
+ # One line text input box, analog to HTMLInputElement
9
+ class InputBox < Button
10
+ include Enterable
11
+ def initialize(**args)
12
+ super({height: 1, text: args[:value]||args[:text] }.merge(args))
13
+ # super
14
+ @name = 'input'
15
+ if args[:dynamic_width]
16
+ on(:input) do |e|
17
+ update_width(e.value)
18
+ cursor.x = abs_content_x + e.value.length - 2
19
+ end
20
+ end
21
+ # set_attribute('escape-on-blur', get_attribute('escape-on-blur') == nil ? true : get_attribute('escape-on-blur'))
22
+ # set_attribute('action-on-focus', get_attribute('action-on-focus') == nil ? true : get_attribute('action-on-focus'))
23
+ on(:enter) do
24
+ cursor.enable
25
+ end
26
+ on(:escape) do
27
+ cursor.disable
28
+ args[:change]&.call(value)
29
+ end
30
+ end
31
+
32
+ def cursor
33
+ @cursor ||= Cursor.new(x: abs_content_x + value.length - 2, y: abs_content_y, enabled: false, screen: root_screen)
34
+ @cursor
35
+ end
36
+
37
+ def value=(value)
38
+ @text = value
39
+ end
40
+
41
+ def value
42
+ text
43
+ end
44
+
45
+ def between(v, min, max)
46
+ [[v, min].max, max].min
47
+ end
48
+
49
+ def current_x
50
+ cursor.x - abs_content_x
51
+ end
52
+
53
+ def handle_key(event)
54
+ if !super(event)
55
+ if event.key == 'backspace'
56
+ on_input value[0..between(current_x - 1, 0, value.length - 1)] + value[between(current_x + 1, 0, value.length - 1)..(value.length - 1)], event
57
+ cursor.x = [cursor.x - 1, abs_content_x - 1].max
58
+ render
59
+ true
60
+ elsif alphanumeric? event.key
61
+ on_input value[0..between(current_x, 0, value.length - 1)] + event.key + value[between(current_x + 1, 0, value.length - 1)..(value.length - 1)], event
62
+ cursor.x = cursor.x + 1
63
+ render
64
+ true
65
+ elsif ['right'].include? event.key
66
+ cursor.x = [cursor.x + 1, abs_content_x + value.length - 1].min
67
+ render
68
+ true
69
+ elsif ['left'].include? event.key
70
+ cursor.x = [cursor.x - 1, abs_content_x - 1].max
71
+ render
72
+ true
73
+ else
74
+ false
75
+ end
76
+ else
77
+ true
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+
84
+ InputBox = TermGui::Widget::InputBox
85
+