termgui 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +14 -0
  3. data/README.md +321 -0
  4. data/lib/termgui.rb +1 -0
  5. data/src/action.rb +58 -0
  6. data/src/box.rb +90 -0
  7. data/src/color.rb +174 -0
  8. data/src/cursor.rb +69 -0
  9. data/src/editor/editor_base.rb +152 -0
  10. data/src/editor/editor_base_handlers.rb +116 -0
  11. data/src/element.rb +61 -0
  12. data/src/element_bounds.rb +111 -0
  13. data/src/element_box.rb +64 -0
  14. data/src/element_render.rb +102 -0
  15. data/src/element_style.rb +51 -0
  16. data/src/emitter.rb +102 -0
  17. data/src/emitter_state.rb +19 -0
  18. data/src/enterable.rb +93 -0
  19. data/src/event.rb +92 -0
  20. data/src/focus.rb +102 -0
  21. data/src/geometry.rb +53 -0
  22. data/src/image.rb +60 -0
  23. data/src/input.rb +85 -0
  24. data/src/input_grab.rb +17 -0
  25. data/src/input_time.rb +97 -0
  26. data/src/key.rb +114 -0
  27. data/src/log.rb +24 -0
  28. data/src/node.rb +117 -0
  29. data/src/node_attributes.rb +27 -0
  30. data/src/node_visit.rb +52 -0
  31. data/src/renderer.rb +119 -0
  32. data/src/renderer_cursor.rb +18 -0
  33. data/src/renderer_draw.rb +28 -0
  34. data/src/renderer_image.rb +31 -0
  35. data/src/renderer_print.rb +40 -0
  36. data/src/screen.rb +96 -0
  37. data/src/screen_element.rb +59 -0
  38. data/src/screen_input.rb +43 -0
  39. data/src/screen_renderer.rb +53 -0
  40. data/src/style.rb +149 -0
  41. data/src/tco/colouring.rb +248 -0
  42. data/src/tco/config.rb +57 -0
  43. data/src/tco/palette.rb +603 -0
  44. data/src/tco/tco_termgui.rb +30 -0
  45. data/src/termgui.rb +29 -0
  46. data/src/util/css.rb +98 -0
  47. data/src/util/css_query.rb +23 -0
  48. data/src/util/easing.rb +364 -0
  49. data/src/util/hash_object.rb +131 -0
  50. data/src/util/imagemagick.rb +27 -0
  51. data/src/util/justify.rb +20 -0
  52. data/src/util/unicode-categories.rb +572 -0
  53. data/src/util/wrap.rb +102 -0
  54. data/src/util.rb +110 -0
  55. data/src/widget/button.rb +33 -0
  56. data/src/widget/checkbox.rb +47 -0
  57. data/src/widget/col.rb +30 -0
  58. data/src/widget/image.rb +106 -0
  59. data/src/widget/inline.rb +40 -0
  60. data/src/widget/input_number.rb +73 -0
  61. data/src/widget/inputbox.rb +85 -0
  62. data/src/widget/label.rb +33 -0
  63. data/src/widget/modal.rb +69 -0
  64. data/src/widget/row.rb +26 -0
  65. data/src/widget/selectbox.rb +100 -0
  66. data/src/widget/textarea.rb +54 -0
  67. data/src/xml/xml.rb +80 -0
  68. data/test/action_test.rb +34 -0
  69. data/test/box_test.rb +15 -0
  70. data/test/css_test.rb +39 -0
  71. data/test/editor/editor_base_test.rb +201 -0
  72. data/test/element_bounds_test.rb +77 -0
  73. data/test/element_box_test.rb +8 -0
  74. data/test/element_render_test.rb +124 -0
  75. data/test/element_style_test.rb +85 -0
  76. data/test/element_test.rb +10 -0
  77. data/test/emitter_test.rb +108 -0
  78. data/test/event_test.rb +19 -0
  79. data/test/focus_test.rb +37 -0
  80. data/test/geometry_test.rb +12 -0
  81. data/test/input_test.rb +47 -0
  82. data/test/key_test.rb +14 -0
  83. data/test/log_test.rb +21 -0
  84. data/test/node_test.rb +105 -0
  85. data/test/performance/performance1.rb +48 -0
  86. data/test/renderer_test.rb +74 -0
  87. data/test/renderer_test_rect.rb +4 -0
  88. data/test/screen_test.rb +58 -0
  89. data/test/style_test.rb +18 -0
  90. data/test/termgui_test.rb +10 -0
  91. data/test/test_all.rb +30 -0
  92. data/test/util_hash_object_test.rb +93 -0
  93. data/test/util_test.rb +26 -0
  94. data/test/widget/checkbox_test.rb +99 -0
  95. data/test/widget/col_test.rb +87 -0
  96. data/test/widget/inline_test.rb +40 -0
  97. data/test/widget/label_test.rb +94 -0
  98. data/test/widget/row_test.rb +40 -0
  99. data/test/wrap_test.rb +11 -0
  100. data/test/xml_test.rb +77 -0
  101. metadata +148 -0
@@ -0,0 +1,51 @@
1
+ require_relative 'element_bounds'
2
+ require_relative 'util'
3
+
4
+ # adds utilities around style
5
+ module ElementStyle
6
+ def style
7
+ set_attribute('style', ElementStyle.default_style) if get_attribute('style') == nil
8
+ get_attribute('style')
9
+ end
10
+
11
+ def style=(style)
12
+ s = style.instance_of?(Hash) ? Style.from_hash(style) : style
13
+ set_attribute('style', s)
14
+ self.dirty = true
15
+ end
16
+
17
+ def style_assign(style)
18
+ self.style = self.style.assign(style)
19
+ end
20
+
21
+ def get_style(name)
22
+ s = get_attribute('style')
23
+ s.get(name)
24
+ end
25
+
26
+ def default_style
27
+ Style.new
28
+ end
29
+
30
+ # while "normal" style is defined in @style, focused extra style is defined in @style.focus,
31
+ # so dependently on attributes like `focused` this method performs computation of the "final" style
32
+ def final_style
33
+ result = parent && get_attribute('style-cascade') != 'prevent' ? parent.final_style.clone .assign(style) : style.clone
34
+ result.assign(style.focus) if get_attribute('focused')
35
+ result.assign(style.enter) if get_attribute('entered')
36
+ result.assign(style.action) if get_attribute('actioned')
37
+ result
38
+ end
39
+
40
+ # computes current border style according to style, style.border, style.focus.border, etc in the right order
41
+ def border_style
42
+ s = final_style
43
+ if border
44
+ s = s.assign(border)
45
+ s.assign(style.focus&.border) if get_attribute('focused')
46
+ s.assign(style.enter&.border) if get_attribute('entered')
47
+ s.assign(style.action&.border) if get_attribute('actioned')
48
+ end
49
+ s
50
+ end
51
+ end
data/src/emitter.rb ADDED
@@ -0,0 +1,102 @@
1
+ require_relative 'emitter_state'
2
+ require_relative 'util'
3
+
4
+ module TermGui
5
+ # Basic event emitter, similar to Node's Emitter
6
+ # adapted from https://medium.com/@kopilov.vlad/use-event-emitter-in-ruby-6b289fe2e7b4
7
+ class Emitter
8
+ include EmitterState
9
+
10
+ # turn on the event
11
+ # @param {String, Symbol, (String|Symbol)[]} event_names
12
+ # @return {nil}
13
+ def install(event_names)
14
+ to_array(event_names).each do |event_name|
15
+ events[event_name.to_sym] ||= []
16
+ end
17
+ end
18
+
19
+ # turn off the event
20
+ # @param {String, Symbol, (String|Symbol)[]} event_names
21
+ # @return {nil}
22
+ def uninstall(event_names)
23
+ to_array(event_names).each do |event_name|
24
+ events.delete(event_name.to_sym)
25
+ end
26
+ end
27
+
28
+ # subscribe to event
29
+ # @param {String, Symbol, (String|Symbol)[]} event_names
30
+ # @param handler_proc [Proc]
31
+ # @return {Proc}
32
+ def subscribe(event_names, handler_proc = nil, &block)
33
+ throw 'No block or handler given' if handler_proc == nil && !block_given?
34
+ handler = handler_proc == nil ? block : handler_proc
35
+ to_array(event_names).each do |event_name|
36
+ events[event_name.to_sym]&.push handler
37
+ end
38
+ handler
39
+ end
40
+
41
+ alias add_listener subscribe
42
+ alias on subscribe
43
+
44
+ # unsubscribe to event
45
+ # @param {String, Symbol, (String|Symbol)[]} event_names
46
+ # @param handler [Proc]
47
+ # @return {nil}
48
+ def unsubscribe(event_names, handler)
49
+ to_array(event_names).each do |event_name|
50
+ events[event_name.to_sym]&.reject! do |item|
51
+ item == handler
52
+ end
53
+ end
54
+ end
55
+
56
+ alias remove_listener unsubscribe
57
+ alias off unsubscribe
58
+
59
+ # emit the event
60
+ # @param {String, Symbol} event_name
61
+ # @param {Event} event
62
+ # @return {nil}
63
+ def emit(event_name, event = Event.new(event_name))
64
+ events[event_name.to_sym]&.each do |h|
65
+ h.call(event)
66
+ end
67
+ end
68
+
69
+ alias trigger emit
70
+
71
+ # get array of existing events
72
+ # @return [Array<Symbols>]
73
+ def all_events
74
+ events.keys
75
+ end
76
+
77
+ # get array of existing events with stat
78
+ # @return [Array<Symbols, Fixnum>]
79
+ def all_events_with_stat
80
+ events
81
+ .map { |name, arr| [name, arr.size] }
82
+ .flatten
83
+ end
84
+
85
+ def once(event_name, handler_proc = nil, &block)
86
+ throw 'No block or handler given' if handler_proc == nil && !block_given?
87
+ handler = handler_proc == nil ? block : handler_proc
88
+ listener = on(event_name) do |event|
89
+ handler.call(event)
90
+ off(event_name, listener)
91
+ end
92
+ end
93
+
94
+ private
95
+
96
+ def events
97
+ @events ||= {}
98
+ end
99
+ end
100
+ end
101
+
102
+ Emitter = TermGui::Emitter
@@ -0,0 +1,19 @@
1
+ # adds save/restore event listeners state to Emitter
2
+ module EmitterState
3
+ # saves current emitter listeners state under given name
4
+ def emitter_save(name)
5
+ @emitter_state ||= {}
6
+ @emitter_state[name.to_sym] = @events
7
+ end
8
+
9
+ # loads a previously saved emitter state with given name.
10
+ # After this call this emitter will notify a different set of listeners
11
+ def emitter_load(name)
12
+ @emitter_state ||= {}
13
+ @events = @emitter_state[name.to_sym] || @events
14
+ end
15
+
16
+ def emitter_reset
17
+ @events = {}
18
+ end
19
+ end
data/src/enterable.rb ADDED
@@ -0,0 +1,93 @@
1
+ require_relative 'event'
2
+
3
+ module TermGui
4
+ class InputEvent < NodeEvent
5
+ attr_accessor :value
6
+ def initialize(target, value = target.value, original_event = nil)
7
+ super 'input', target, original_event
8
+ @value = value
9
+ end
10
+ end
11
+
12
+ class ChangeEvent < NodeEvent
13
+ attr_accessor :value
14
+ def initialize(target, value = target.value, original_event = nil)
15
+ super 'change', target, original_event
16
+ @value = value
17
+ end
18
+ end
19
+
20
+ class EscapeEvent < NodeEvent
21
+ def initialize(target, original_event = nil)
22
+ super 'escape', target, original_event
23
+ end
24
+ end
25
+
26
+ class EnterEvent < NodeEvent
27
+ def initialize(target, original_event = nil)
28
+ super 'enter', target, original_event
29
+ end
30
+ end
31
+
32
+ module Enterable
33
+ def initialize(**args)
34
+ super
35
+ self.value = args[:value] || ''
36
+ @key_listener = nil
37
+ set_attribute(:focusable, true)
38
+ set_attribute(:enterable, true)
39
+ set_attribute(:actionable, true)
40
+ install(%i[input action enter change escape focus blur])
41
+ on(:action) do |event|
42
+ return unless root_screen && get_attribute('entered')
43
+ set_attribute('entered', true)
44
+ @key_listener = proc { |e| handle_key e }
45
+ root_screen.event.add_any_key_listener @key_listener
46
+ on('change', args[:change]) if args[:change]
47
+ on('input', args[:input]) if args[:input]
48
+ on('escape', args[:escape]) if args[:escape]
49
+ trigger('enter', EnterEvent.new(self, event))
50
+ end
51
+ on(%i[blur escape change]) do
52
+ return unless root_screen
53
+ set_attribute('entered', false)
54
+ end
55
+ end
56
+
57
+ def handle_key(event)
58
+ return unless root_screen
59
+ if !get_attribute('focused')
60
+ trigger('change', ChangeEvent.new(self, value, event))
61
+ root_screen.event.remove_any_key_listener @key_listener
62
+ true
63
+ elsif to_array(get_attribute('escape-keys') || 'escape').include? event.key
64
+ trigger('escape', EscapeEvent.new(self, event))
65
+ root_screen.event.remove_any_key_listener @key_listener
66
+ true
67
+ else
68
+ false
69
+ end
70
+ end
71
+
72
+ def value=(_value)
73
+ throw 'subclass must implementation'
74
+ end
75
+
76
+ def value
77
+ throw 'subclass must implementation'
78
+ end
79
+
80
+ protected
81
+
82
+ def on_input(value, event = nil)
83
+ return unless root_screen
84
+ self.value = value
85
+ trigger('input', InputEvent.new(self, value, event))
86
+ end
87
+ end
88
+ end
89
+
90
+ Enterable = TermGui::Enterable
91
+ InputEvent = TermGui::InputEvent
92
+ ChangeEvent = TermGui::ChangeEvent
93
+ EscapeEvent = TermGui::EscapeEvent
data/src/event.rb ADDED
@@ -0,0 +1,92 @@
1
+ require_relative 'input'
2
+ require_relative 'util'
3
+
4
+ module TermGui
5
+ # # Base event class. Independent of Element.
6
+ # class Event
7
+ # attr_reader :name
8
+
9
+ # def initialize(name)
10
+ # @name = name
11
+ # end
12
+ # end
13
+
14
+ # Event related with a Node (`target`) and a native event (`original_event`).
15
+ class NodeEvent < Event
16
+ attr_reader :target, :original_event
17
+
18
+ def initialize(name, target, original_event = nil)
19
+ super name
20
+ @target = target
21
+ @original_event = original_event
22
+ end
23
+ end
24
+
25
+ # Represents a keyboard event. Independent of Element.
26
+ class KeyEvent < Event
27
+ attr_reader :key, :raw
28
+
29
+ def initialize(key, raw = name_to_char(key))
30
+ super 'key'
31
+ @key = key
32
+ @raw = raw
33
+ end
34
+
35
+ def to_s
36
+ "KeyEvent#{{ name: @name, key: @key, raw: @raw }}"
37
+ end
38
+ end
39
+
40
+ # responsible of observe/emit user input events (KeyEvent)
41
+ class EventManager
42
+ def initialize(input = Input.new)
43
+ @key_listeners = {}
44
+ @any_key_listener = []
45
+ input.add_listener('key') { |e| handle_key e }
46
+ end
47
+
48
+ def add_key_listener(keys, listener = nil, &block)
49
+ the_listener = listener == nil ? block : listener
50
+ throw 'No listener provided' if the_listener == nil
51
+ keys = (keys.is_a? String) ? [keys] : keys
52
+ keys.each do |key|
53
+ @key_listeners[key] = @key_listeners[key] || []
54
+ @key_listeners[key].push the_listener
55
+ end
56
+ the_listener
57
+ end
58
+
59
+ def remove_key_listener(key, listener)
60
+ @key_listeners[key] = @key_listeners[key] || []
61
+ @key_listeners[key].delete listener
62
+ end
63
+
64
+ def add_any_key_listener(listener = nil, &block)
65
+ the_listener = listener == nil ? block : listener
66
+ throw 'No listener provided' if the_listener == nil
67
+ @any_key_listener.push the_listener
68
+ the_listener
69
+ end
70
+
71
+ def remove_any_key_listener(listener)
72
+ @any_key_listener.delete listener
73
+ end
74
+
75
+ # can be used to programmatically simulate keypressed (useful for tests)
76
+ def handle_key(e)
77
+ key = e.key
78
+ @key_listeners[key] = @key_listeners[key] || []
79
+ @key_listeners[key].each do |listener|
80
+ listener.call(e)
81
+ end
82
+ @any_key_listener.each do |listener|
83
+ listener.call(e)
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ Event = TermGui::Event
90
+ NodeEvent = TermGui::NodeEvent
91
+ KeyEvent = TermGui::KeyEvent
92
+ EventManager = TermGui::EventManager
data/src/focus.rb ADDED
@@ -0,0 +1,102 @@
1
+ require_relative 'emitter'
2
+ require_relative 'element'
3
+ require_relative 'log'
4
+ require_relative 'event'
5
+
6
+ module TermGui
7
+ # provides support for focused, focusable attributes management and emit focus-related events
8
+ # TODO: make events extend NodeEvent
9
+ class FocusManager < Emitter
10
+ attr_reader :keys, :focused
11
+
12
+ def initialize(
13
+ root: nil, # the root element inside of which to look up for focusables
14
+ event: nil, # EventManager instance - needed for subscribe to key events
15
+ keys: { next: ['tab'], prev: ['S-tab'] }, # the keys for focusing the next and previous focusable
16
+ focus_first: true # if true will set focus (attribute focused == true) on the first focusable automatically
17
+ )
18
+ throw 'root Element and Event EventManager are required' unless root && event
19
+ install(:focus)
20
+ @root = root
21
+ @keys = keys
22
+ @event = event
23
+ @focus_first = focus_first
24
+ init
25
+ @event.add_any_key_listener { |e| handle_key e }
26
+ @root.on(:after_start) do
27
+ init
28
+ end
29
+ end
30
+
31
+ def init
32
+ focusables.each { |n| n.set_attribute(:focused, false) }
33
+ if @focus_first
34
+ self.focused = focusables.first
35
+ @focused&.render if @focused6.is_a? Element
36
+ end
37
+ end
38
+
39
+ def focusables
40
+ @root.query_by_attribute(:focusable, true)
41
+ end
42
+
43
+ # focus next focusable node
44
+ def focus_next
45
+ i = focusables.index(@focused) || 0
46
+ new_i = i == focusables.length - 1 ? 0 : i + 1
47
+ self.focused = focusables[new_i] if focusables[new_i]
48
+ end
49
+
50
+ # focus previous focusable node
51
+ def focus_prev
52
+ i = focusables.index(@focused) || 0
53
+ new_i = i.zero? ? focusables.length - 1 : i - 1
54
+ self.focused = focusables[new_i] if focusables[new_i]
55
+ end
56
+
57
+ def focused=(focused)
58
+ previous = @focused
59
+ @focused&.set_attribute(:focused, false)
60
+ @focused = focused
61
+ @focused&.set_attribute(:focused, true)
62
+ emit :focus, focused: @focused, previous: previous
63
+ previous&.emit :blur, BlurEvent.new(previous, @focused)
64
+ focus_event = FocusEvent.new(@focused, previous)
65
+ @focused&.emit :focus, focus_event
66
+ if @focused&.get_attribute('action-on-focus')
67
+ event = ActionEvent.new @focused, focus_event
68
+ @focused.get_attribute('action')&.call(event)
69
+ @focused.trigger event.name, event
70
+ end
71
+ end
72
+
73
+ protected
74
+
75
+ def handle_key(e)
76
+ return if @focused&.get_attribute('entered') && !@focused&.get_attribute('escape-on-blur')
77
+ if @keys[:next].include? e.key
78
+ focus_next
79
+ elsif @keys[:prev].include? e.key
80
+ focus_prev
81
+ end
82
+ end
83
+ end
84
+
85
+ class BlurEvent < NodeEvent
86
+ attr_accessor :focused
87
+ def initialize(target, focused, original_event = nil)
88
+ super 'blur', target, original_event
89
+ @focused = focused
90
+ end
91
+ end
92
+
93
+ class FocusEvent < NodeEvent
94
+ attr_accessor :previous
95
+ def initialize(target, previous, original_event = nil)
96
+ super 'focus', target, original_event
97
+ @previous = previous
98
+ end
99
+ end
100
+ end
101
+
102
+ FocusManager = TermGui::FocusManager
data/src/geometry.rb ADDED
@@ -0,0 +1,53 @@
1
+ module TermGui
2
+ # represents a scroll viewport offset
3
+ class Offset
4
+ attr_accessor :left, :top
5
+
6
+ def initialize(left: 0, top: 0)
7
+ @left = left
8
+ @top = top
9
+ end
10
+ end
11
+
12
+ # Represents a rectangle in the form of {top, left, bottom, top}
13
+ class Bounds < Offset
14
+ attr_accessor :right, :bottom
15
+
16
+ def initialize(left: 0, right: 0, top: 0, bottom: 0)
17
+ super(left: left, top: top)
18
+ @right = right
19
+ @bottom = bottom
20
+ end
21
+
22
+ def self.from_hash(hash)
23
+ merge_hash_into_object hash, Bounds.new
24
+ end
25
+ end
26
+
27
+ class Point
28
+ attr_accessor :x, :y
29
+ def initialize(x: 0, y: 0)
30
+ @x = x
31
+ @y = y
32
+ end
33
+ end
34
+
35
+ class Rectangle < Point
36
+ attr_accessor :width, :height
37
+
38
+ def initialize(x: 0, y: 0, width: 0, height: 0)
39
+ super(x: x, y: y)
40
+ @width = width
41
+ @height = height
42
+ end
43
+
44
+ def self.from_hash(hash)
45
+ merge_hash_into_object hash, Rectangle.new
46
+ end
47
+ end
48
+ end
49
+
50
+ Offset = TermGui::Offset
51
+ Bounds = TermGui::Bounds
52
+ Point = TermGui::Point
53
+ Rectangle = TermGui::Rectangle
data/src/image.rb ADDED
@@ -0,0 +1,60 @@
1
+ require 'chunky_png'
2
+ require 'fileutils'
3
+ require_relative 'util/imagemagick'
4
+ # small facade for image decoding and processing
5
+ # Right now only supports reading png thanks to chunky_png
6
+ module TermGui
7
+ class Image
8
+ attr_reader :path
9
+
10
+ def initialize(image = nil, path = image.is_a?(String) ? image : 'unknown.png')
11
+ if image.is_a?(String)
12
+ if File.extname(image).capitalize != '.png'
13
+ if !image_magick_available
14
+ throw 'Cannot create image from non PNG file without imagemagick available'
15
+ else
16
+ image = convert(image)
17
+ end
18
+ end
19
+ end
20
+ @image = image.is_a?(String) ? ChunkyPNG::Image.from_file(image) : image
21
+ @path = path
22
+ end
23
+
24
+ def rgb(x = 0, y = 0, transparent_color = nil)
25
+ p = @image[x, y]
26
+ if transparent_color
27
+ bg = ChunkyPNG::Color.rgb(transparent_color[0], transparent_color[1], transparent_color[2])
28
+ p = ChunkyPNG::Color. interpolate_quick(p, bg, ChunkyPNG::Color.a(p))
29
+ end
30
+ [ChunkyPNG::Color.r(p), ChunkyPNG::Color.g(p), ChunkyPNG::Color.b(p)]
31
+ end
32
+
33
+ def rgba(x = 0, y = 0)
34
+ p = @image[x, y]
35
+ [ChunkyPNG::Color.r(p), ChunkyPNG::Color.g(p), ChunkyPNG::Color.b(p), ChunkyPNG::Color.a(p)]
36
+ end
37
+
38
+ def width
39
+ @image.width
40
+ end
41
+
42
+ def height
43
+ @image.height
44
+ end
45
+
46
+ # returns a new image which is the result of scaling this image to given dimentions
47
+ def scale(width: @image.width, height: @image.height,
48
+ algorithm: 'bilineal') # bilinear, nearest_neighbor
49
+ if algorithm == 'nearest_neighbor'
50
+ Image.new(@image.resample_nearest_neighbor(width, height), path)
51
+ else
52
+ Image.new(@image.resample_bilinear(width, height), path)
53
+ end
54
+ end
55
+
56
+ def crop(x: 0, y: 0, width: @image.width, height: @image.height)
57
+ Image.new(@image.crop(x, y, width, height), path)
58
+ end
59
+ end
60
+ end
data/src/input.rb ADDED
@@ -0,0 +1,85 @@
1
+ require_relative 'emitter'
2
+ require_relative 'log'
3
+ require_relative 'event'
4
+ require_relative 'input_time'
5
+ require_relative 'input_grab'
6
+ require 'io/console'
7
+ require 'io/wait'
8
+ require_relative 'key'
9
+
10
+ module TermGui
11
+ # responsible of listening stdin and event loop
12
+ class Input < Emitter
13
+ include InputTime
14
+ include InputGrab
15
+
16
+ attr_reader :interval, :stdin, :stopped
17
+
18
+ def initialize(stdin = $stdin, interval = 0.0000001)
19
+ super
20
+ @interval = interval
21
+ @stdin = stdin
22
+ @stopped = true
23
+ install(:key)
24
+ end
25
+
26
+ def stop
27
+ @stopped = true
28
+ end
29
+
30
+ # starts listening for user input. Implemented like an event loop reading from @input_stream each @interval
31
+ def start
32
+ return self unless @stopped
33
+
34
+ @stdin.raw do |io|
35
+ @io = io
36
+ @stopped = false
37
+ loop do
38
+ tick
39
+ break if @stopped
40
+ end
41
+ end
42
+ end
43
+
44
+ # Once it `start`s this method is called on the loop to read input. Calls update_status which update timers (timeout - interval).
45
+ # Could be useful for applications with their own event loops or time long blocking tasks to notify input/timeouts
46
+ def tick
47
+ char = get_char_or_sequence
48
+ if char
49
+ key = char.inspect
50
+ key = key[1..key.length - 2]
51
+ key = char_to_name(key) || char
52
+ emit_key char, key
53
+ else
54
+ sleep @interval
55
+ end
56
+ update_status
57
+ end
58
+
59
+ def emit_key(char, key = char)
60
+ event = KeyEvent.new key, char
61
+ emit 'key', event
62
+ end
63
+
64
+ def write(s)
65
+ @stdin.write s
66
+ end
67
+
68
+ protected
69
+
70
+ def get_char_or_sequence(io = @io)
71
+ if io.ready?
72
+ result = io.sysread(1)
73
+ while (CSI.start_with?(result) ||
74
+ (result.start_with?(CSI) &&
75
+ !result.codepoints[-1].between?(64, 126))) &&
76
+ (next_char = get_char_or_sequence(io))
77
+ result << next_char
78
+ end
79
+ result
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ Input = TermGui::Input
data/src/input_grab.rb ADDED
@@ -0,0 +1,17 @@
1
+ require_relative 'util'
2
+
3
+ # Add Input support for grab, this is stop notifying listeners and notify others. Based on EmitterState
4
+ module InputGrab
5
+ def grab(block = nil, &b)
6
+ the_block = block == nil ? b : block
7
+ throw 'Block not given' unless the_block
8
+ emitter_save('grab')
9
+ emitter_reset
10
+ install(:key)
11
+ subscribe(:key, the_block)
12
+ end
13
+
14
+ def ungrab
15
+ emitter_load('grab')
16
+ end
17
+ end