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,58 @@
1
+ require_relative 'event'
2
+ require_relative 'emitter'
3
+ require_relative 'util'
4
+
5
+ module TermGui
6
+ # action manager - it notifies focused elements on user input
7
+ # WIP
8
+ # defines action concept semantics like "action", "input"
9
+ # "focus": user press tab and it set focus on the next focusable element
10
+ # "enter": user focus a button and press enter or space. The button will receive an "action" event.
11
+ # "input": user focus a textarea, press enter ("action") and this enters in the "edit" mode
12
+ # meaning all user input now it's being notified ONLY to the textarea. Nor even the focus manager or
13
+ # others will be notified.
14
+ # "escape": when in a textarea "edit" mode, user press ESCAPE to exit it, meaning now user input will
15
+ # be notified to all listeners. Example: when in edit mode, pressing TAB will just print a tab in the
16
+ # textarea and it won't change focus as usual. For that to happen user presses escape to exit the "edit" mode.
17
+ # Attributes enabling each action:
18
+ # "enterable"
19
+ # "focusable"
20
+ # in the case of scape it will be enabled iff enterable is true
21
+ # For example a Label is not focusable or enterable. A Button is focusable but not enterable. A textatra is focusable and enterable.
22
+ class ActionManager < Emitter
23
+ attr_accessor :keys
24
+
25
+ def initialize(event: nil, focus: nil)
26
+ super()
27
+ @event = event
28
+ @focus = focus
29
+ @event.add_any_key_listener { |e| handle_key e }
30
+ install(:action)
31
+ @keys = ['enter']
32
+ end
33
+
34
+ def handle_key(e)
35
+ focused = @focus.focused
36
+ return unless focused && !focused.get_attribute('entered')
37
+
38
+ action_keys = to_array(focused.get_attribute('action-keys') || keys)
39
+ if action_keys.include?(e.key) && focused.get_attribute('focusable')
40
+ event = ActionEvent.new focused, e
41
+ focused_action = focused.get_attribute('action')
42
+ focused_action&.call(event)
43
+ trigger event.name, event
44
+ focused.trigger event.name, event
45
+ end
46
+ end
47
+ end
48
+
49
+ # An event representing an action, like a button "clicked"
50
+ class ActionEvent < NodeEvent
51
+ def initialize(target, original_event = nil)
52
+ super 'action', target, original_event
53
+ end
54
+ end
55
+ end
56
+
57
+ ActionEvent = TermGui::ActionEvent
58
+ ActionManager = TermGui::ActionManager
@@ -0,0 +1,90 @@
1
+ BOXES = {
2
+ single: {
3
+ topLeft: '┌',
4
+ topRight: '┐',
5
+ bottomRight: '┘',
6
+ bottomLeft: '└',
7
+ vertical: '│',
8
+ horizontal: '─'
9
+ },
10
+ double: {
11
+ topLeft: '╔',
12
+ topRight: '╗',
13
+ bottomRight: '╝',
14
+ bottomLeft: '╚',
15
+ vertical: '║',
16
+ horizontal: '═'
17
+ },
18
+ round: {
19
+ topLeft: '╭',
20
+ topRight: '╮',
21
+ bottomRight: '╯',
22
+ bottomLeft: '╰',
23
+ vertical: '│',
24
+ horizontal: '─'
25
+ },
26
+ bold: {
27
+ topLeft: '┏',
28
+ topRight: '┓',
29
+ bottomRight: '┛',
30
+ bottomLeft: '┗',
31
+ vertical: '┃',
32
+ horizontal: '━'
33
+ },
34
+ single_double: {
35
+ topLeft: '╓',
36
+ topRight: '╖',
37
+ bottomRight: '╜',
38
+ bottomLeft: '╙',
39
+ vertical: '║',
40
+ horizontal: '─'
41
+ },
42
+ double_single: {
43
+ topLeft: '╒',
44
+ topRight: '╕',
45
+ bottomRight: '╛',
46
+ bottomLeft: '╘',
47
+ vertical: '│',
48
+ horizontal: '═'
49
+ },
50
+ classic: {
51
+ topLeft: '+',
52
+ topRight: '+',
53
+ bottomRight: '+',
54
+ bottomLeft: '+',
55
+ vertical: '|',
56
+ horizontal: '-'
57
+ }
58
+ }.freeze
59
+
60
+ def boxes
61
+ BOXES
62
+ end
63
+
64
+ def draw_box(width: 0, height: 0, style: :single, content: ' ')
65
+ box = BOXES[style ? style.to_sym : :single]
66
+ lines = []
67
+
68
+ (0..height - 1).each do |y|
69
+ line = []
70
+ (0..width - 1).each do |x|
71
+ line .push(if y.zero? && x.zero?
72
+ box[:topLeft]
73
+ elsif y == height - 1 && x.zero?
74
+ box[:bottomLeft]
75
+ elsif y.zero? && x == width - 1
76
+ box[:topRight]
77
+ elsif y == height - 1 && x == width - 1
78
+ box[:bottomRight]
79
+ elsif y == height - 1 || y.zero?
80
+ box[:horizontal]
81
+ elsif x == width - 1 || x.zero?
82
+ box[:vertical]
83
+ else
84
+ content
85
+ end)
86
+ end
87
+ lines.push(line.join(''))
88
+ end
89
+ lines
90
+ end
@@ -0,0 +1,174 @@
1
+ # # # fast acceptable color comparision by myself
2
+ # # def color_distance1(c1, c2)
3
+ # # sum = 0
4
+ # # c1.each_index do |i|
5
+ # # sum += (c1[i].abs2 - c2[i].abs2).abs
6
+ # # end
7
+ # # sum
8
+ # # end
9
+
10
+ # def color_distance3(c1, c2)
11
+ # sum = 0
12
+ # coef = [1, 1, 1]
13
+ # c1.each_index do |i|
14
+ # sum += ((c1[i] - c2[i]) * coef[i]).abs2
15
+ # end
16
+ # sum
17
+ # end
18
+
19
+ # # // As it happens, comparing how similar two colors are is really hard. Here is
20
+ # # // one of the simplest solutions, which doesn't require conversion to another
21
+ # # // color space, posted on stackoverflow[1]. Maybe someone better at math can
22
+ # # // propose a superior solution.
23
+ # # // [1] http://stackoverflow.com/questions/1633828
24
+ # def color_distance2(c1, c2)
25
+ # # function colorDistance(r1, g1, b1, r2, g2, b2) {
26
+ # # return Math.pow(30 * (r1 - r2), 2)
27
+ # # + Math.pow(59 * (g1 - g2), 2)
28
+ # # + Math.pow(11 * (b1 - b2), 2);
29
+ # # }
30
+ # (30 * (c1[0] - c2[0])).abs2 + (59 * (c1[1] - c2[1])).abs2 + (11 * (c1[2] - c2[2])).abs2
31
+ # end
32
+
33
+ # # // This might work well enough for a terminal's colors: treat RGB as XYZ in a
34
+ # # // 3-dimensional space and go midway between the two points.
35
+ # # exports.mixColors = function(c1, c2, alpha) {
36
+ # # // if (c1 === 0x1ff) return c1;
37
+ # # // if (c2 === 0x1ff) return c1;
38
+ # # if (c1 === 0x1ff) c1 = 0;
39
+ # # if (c2 === 0x1ff) c2 = 0;
40
+ # # if (alpha == null) alpha = 0.5;
41
+
42
+ # # c1 = exports.vcolors[c1];
43
+ # # var r1 = c1[0];
44
+ # # var g1 = c1[1];
45
+ # # var b1 = c1[2];
46
+
47
+ # # c2 = exports.vcolors[c2];
48
+ # # var r2 = c2[0];
49
+ # # var g2 = c2[1];
50
+ # # var b2 = c2[2];
51
+
52
+ # # r1 += (r2 - r1) * alpha | 0;
53
+ # # g1 += (g2 - g1) * alpha | 0;
54
+ # # b1 += (b2 - b1) * alpha | 0;
55
+
56
+ # # return exports.match([r1, g1, b1]);
57
+ # # };
58
+
59
+ # # the rest is obsoleted by src/tco
60
+
61
+ # # color enumerations and utilities. See http://ascii-table.com/ansi-escape-sequences.phpsss
62
+ # require_relative 'key'
63
+
64
+ # # obsolete
65
+ # COLORS = {
66
+ # black: 0,
67
+ # red: 1,
68
+ # green: 2,
69
+ # yellow: 3,
70
+ # blue: 4,
71
+ # magenta: 5,
72
+ # cyan: 6,
73
+ # white: 7
74
+ # }.freeze
75
+
76
+ # # obsolete
77
+ # def color_names
78
+ # %w[black red green yellow blue magenta cyan white]
79
+ # end
80
+
81
+ # # obsolete
82
+ # def random_color
83
+ # color_names.sample
84
+ # end
85
+
86
+ # # obsolete
87
+ # def color_text(content, fg = nil, bg = nil)
88
+ # colored = color(fg, bg)
89
+ # colored << content
90
+ # colored << "#{CSI}0m"
91
+ # end
92
+
93
+ # # obsolete
94
+ # def color(fg = nil, bg = nil)
95
+ # colored = ''
96
+ # colored << color_to_escape(fg, 30) if fg
97
+ # colored << color_to_escape(bg, 40) if bg
98
+ # end
99
+
100
+ # # obsolete
101
+ # def color_to_escape(name, layer)
102
+ # short_name = name.to_s.sub(/\Abright_/, '')
103
+ # color = COLORS.fetch(short_name.to_sym)
104
+ # escape = "#{CSI}#{layer + color}"
105
+ # escape << ';1' if short_name.size < name.size
106
+ # escape << 'm'
107
+ # escape
108
+ # end
109
+
110
+ # # # https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters
111
+ # # ATTRIBUTES = {
112
+ # # normal: 0,
113
+
114
+ # # bold: 1,
115
+ # # boldOff: 22,
116
+
117
+ # # faint: 2,
118
+ # # faintOff: 22,
119
+
120
+ # # italic: 3,
121
+ # # italicOff: 23,
122
+
123
+ # # underline: 4,
124
+ # # underlineOff: 24,
125
+
126
+ # # blink: 5,
127
+ # # slowBlink: 5,
128
+ # # rapidBlink: 6,
129
+ # # blinkOff: 25,
130
+ # # slowBlinkOff: 25,
131
+ # # rapidBlinkOff: 25,
132
+
133
+ # # inverse: 7,
134
+ # # inverseOff: 27,
135
+
136
+ # # defaultForeground: 39,
137
+ # # defaultBackground: 49,
138
+
139
+ # # invisible: 8,
140
+ # # invisibleOff: 28,
141
+
142
+ # # fraktur: 20,
143
+ # # frakturOff: 23,
144
+
145
+ # # framed: 51,
146
+ # # encircled: 52,
147
+ # # overlined: 53,
148
+
149
+ # # framedOff: 54,
150
+ # # encircledOff: 54
151
+ # # }.freeze
152
+
153
+ # # # Usage: screen.write attributes(blink: true).
154
+ # # # Attributes supported: bold, inverse, blink, slowBlink, rapidBlink, invisible, fraktur, framed, encircled, normal,
155
+ # # # italic, underline, faint
156
+ # # #
157
+ # # # Esc[Value;...;Valuem Set Graphics Mode:
158
+ # # # Calls the graphics functions specified by the following values.
159
+ # # # These specified functions remain active until the next occurrence of this escape sequence. Graphics mode changes the
160
+ # # # colors and attributes of text (such as bold and underline) displayed on the screen
161
+ # # def attributes(**args)
162
+ # # output = []
163
+ # # args.keys.each do |key|
164
+ # # if args[key] == true
165
+ # # output.push ATTRIBUTES[key].to_s
166
+ # # elsif args[key] == false
167
+ # # output.push ATTRIBUTES[:blinkOff].to_s if key == :blink
168
+ # # output.push "#{CSI}#{ATTRIBUTES[:boldOff]}" if key == :bold
169
+ # # # TODO: the rest
170
+ # # end
171
+ # # end
172
+ # # # p output
173
+ # # !output.empty? ? "#{CSI}#{output.join(';')}m" : ''
174
+ # # end
@@ -0,0 +1,69 @@
1
+ require_relative 'style'
2
+ module TermGui
3
+ # minimalist artificial screen cursor. TODO: cursor can be manipulated via ansi escape codes - see npm.org/blessed
4
+ class Cursor
5
+ attr_accessor :interval, :x, :y, :enabled, :off, :on
6
+
7
+ def initialize(
8
+ on_interval: 0.3, off_interval: on_interval / 2,
9
+ x: 0, y: 0, enabled: false,
10
+ # by default we render the cursor next to its real x-coordinate
11
+ x_offset: 1,
12
+ screen: nil, off: ' ', on: '_',
13
+ on_style: Style.new(bg: 'white', fg: 'black'), off_style: Style.new(bg: 'black', fg: 'white')
14
+ )
15
+ @x = x
16
+ @y = y
17
+ @on_interval = on_interval
18
+ @off_interval = off_interval
19
+ @enabled = enabled
20
+ @x_offset = x_offset
21
+ @screen = screen
22
+ throw 'screen must be provided' unless @screen
23
+ @state = 0
24
+ @off = off
25
+ @on = on
26
+ @on_style = on_style
27
+ @off_style = off_style
28
+ end
29
+
30
+ def enable
31
+ disable
32
+ @enabled = true
33
+ @state = 0
34
+ tick
35
+ end
36
+
37
+ def draw_off
38
+ @screen.text(x: @x + @x_offset, y: @y, text: @off, style: @off_style)
39
+ end
40
+
41
+ def draw_on
42
+ @screen.text(x: @x + @x_offset, y: @y, text: @on, style: @on_style)
43
+ end
44
+
45
+ def disable
46
+ @enabled = false
47
+ @screen.clear_timeout @timeout if @timeout
48
+ end
49
+
50
+ protected
51
+
52
+ # renders the cursor according to its state, toggling the state and finally schedulling to call it self according to on_interval, off_interval
53
+ def tick
54
+ if @state == 0
55
+ @state = 1
56
+ draw_on
57
+ elsif @state == 1
58
+ @state = 0
59
+ draw_off
60
+ else
61
+ throw 'unknown state ' + @state
62
+ end
63
+ @screen.clear_timeout @timeout if @timeout
64
+ @timeout = @screen.set_timeout(@state == 0 ? @on_interval : @off_interval) { tick } if @enabled
65
+ end
66
+ end
67
+ end
68
+
69
+ Cursor = TermGui::Cursor
@@ -0,0 +1,152 @@
1
+ require_relative '../cursor'
2
+ require_relative '../log'
3
+ require_relative '../key'
4
+ require_relative 'editor_base_handlers'
5
+
6
+ # See TODO.md section Editor for status and TODOs
7
+ module TermGui
8
+ class EditorBase
9
+ include EditorBaseHandlers
10
+
11
+ attr_accessor :x, :y, :width, :height
12
+
13
+ def initialize(
14
+ text: '', screen: nil, x: 0, y: 0,
15
+ width: screen.width, height: screen.height,
16
+ cursor_x: nil, cursor_y: nil,
17
+ # if true other party will listen keys and call handle_key - if false it will listen any key by itlself
18
+ managed: false
19
+ )
20
+ @screen = screen || (throw 'screen not given')
21
+ @x = x
22
+ @y = y
23
+ @width = width
24
+ @height = height
25
+ @managed = managed
26
+ @cursor = Cursor.new(screen: @screen)
27
+ self.text = text
28
+ @cursor.y = @y + cursor_y if cursor_y
29
+ @cursor.x = @x + cursor_x if cursor_x
30
+ end
31
+
32
+ def cursor_x
33
+ @cursor.x - @x
34
+ end
35
+
36
+ def cursor_y
37
+ @cursor.y - @y
38
+ end
39
+
40
+ def text=(text)
41
+ @lines = text.split('\n')
42
+ @cursor.y = @y + @lines.length - 1
43
+ @cursor.x = @x + current_line.length - 1
44
+ end
45
+
46
+ def value=(v)
47
+ self.text = v
48
+ end
49
+
50
+ def text
51
+ @lines.join('\n')
52
+ end
53
+
54
+ def value
55
+ text
56
+ end
57
+
58
+ def enable(and_render = true)
59
+ disable
60
+ @cursor.enable
61
+ unless @managed
62
+ @key_listener = @screen.event.add_any_key_listener do |event|
63
+ handle_key event
64
+ end
65
+ end
66
+ render if and_render
67
+ end
68
+
69
+ def disable
70
+ @cursor.disable
71
+ @screen.event.remove_any_key_listener @key_listener if @key_listener
72
+ @key_listener = nil
73
+ end
74
+
75
+ def enabled
76
+ @cursor.enabled
77
+ end
78
+
79
+ def render
80
+ @screen.clear
81
+ # @screen.rect(x: x, y: y, width: width, height: height, style: Style.new)
82
+ @screen.render
83
+ @lines.each_with_index do |line, i|
84
+ @screen.text(x: @x, y: @y + i, text: line)
85
+ end
86
+ end
87
+
88
+ # public so keys can be programmatically simlated - useful for tests instead of calling screen.event.handle_keythat emits globally.
89
+ def handle_key(event)
90
+ if !enabled
91
+ return
92
+ elsif ['down'].include? event.key
93
+ cursor_down
94
+ elsif ['up'].include? event.key
95
+ cursor_up
96
+ elsif ['right'].include? event.key
97
+ cursor_right
98
+ elsif ['left'].include? event.key
99
+ cursor_left
100
+ elsif ['enter'].include? event.key
101
+ handle_enter
102
+ elsif ['tab'].include? event.key
103
+ insert_chars(' ') # TODO: hack - adding a tab will break since we assume all chars width is 1 (1 column per char) - it will also fail for unicode chars width>1
104
+ elsif ['S-tab'].include? event.key
105
+ # TODO: remove tab line prefix
106
+ elsif event.key == 'backspace'
107
+ handle_backspace
108
+ elsif event.key == 'delete'
109
+ handle_delete
110
+ elsif alphanumeric?(event.raw) || %w[space tab].include?(event.key)
111
+ insert_chars(event.raw)
112
+ end
113
+
114
+ @cursor.off = current_char || @cursor.off
115
+ @cursor.on = current_char || @cursor.on
116
+ render
117
+ @cursor.draw_on
118
+ end
119
+
120
+ protected
121
+
122
+ def current_line
123
+ @lines[current_y] || ''
124
+ end
125
+
126
+ def current_line=(value)
127
+ @lines[current_y] = value || ''
128
+ end
129
+
130
+ def current_char
131
+ current_line[[[current_x, current_line.length].min, 0].max] || ' '
132
+ end
133
+
134
+ def current_x
135
+ @cursor.x - @x
136
+ end
137
+
138
+ def current_x=(x)
139
+ @cursor.x = @x + x
140
+ end
141
+
142
+ def current_y
143
+ @cursor.y - @y
144
+ end
145
+
146
+ def current_y=(y)
147
+ @cursor.y = @y + y
148
+ end
149
+ end
150
+ end
151
+
152
+ EditorBase = TermGui::EditorBase