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.
- checksums.yaml +7 -0
- data/Gemfile +14 -0
- data/README.md +321 -0
- data/lib/termgui.rb +1 -0
- data/src/action.rb +58 -0
- data/src/box.rb +90 -0
- data/src/color.rb +174 -0
- data/src/cursor.rb +69 -0
- data/src/editor/editor_base.rb +152 -0
- data/src/editor/editor_base_handlers.rb +116 -0
- data/src/element.rb +61 -0
- data/src/element_bounds.rb +111 -0
- data/src/element_box.rb +64 -0
- data/src/element_render.rb +102 -0
- data/src/element_style.rb +51 -0
- data/src/emitter.rb +102 -0
- data/src/emitter_state.rb +19 -0
- data/src/enterable.rb +93 -0
- data/src/event.rb +92 -0
- data/src/focus.rb +102 -0
- data/src/geometry.rb +53 -0
- data/src/image.rb +60 -0
- data/src/input.rb +85 -0
- data/src/input_grab.rb +17 -0
- data/src/input_time.rb +97 -0
- data/src/key.rb +114 -0
- data/src/log.rb +24 -0
- data/src/node.rb +117 -0
- data/src/node_attributes.rb +27 -0
- data/src/node_visit.rb +52 -0
- data/src/renderer.rb +119 -0
- data/src/renderer_cursor.rb +18 -0
- data/src/renderer_draw.rb +28 -0
- data/src/renderer_image.rb +31 -0
- data/src/renderer_print.rb +40 -0
- data/src/screen.rb +96 -0
- data/src/screen_element.rb +59 -0
- data/src/screen_input.rb +43 -0
- data/src/screen_renderer.rb +53 -0
- data/src/style.rb +149 -0
- data/src/tco/colouring.rb +248 -0
- data/src/tco/config.rb +57 -0
- data/src/tco/palette.rb +603 -0
- data/src/tco/tco_termgui.rb +30 -0
- data/src/termgui.rb +29 -0
- data/src/util/css.rb +98 -0
- data/src/util/css_query.rb +23 -0
- data/src/util/easing.rb +364 -0
- data/src/util/hash_object.rb +131 -0
- data/src/util/imagemagick.rb +27 -0
- data/src/util/justify.rb +20 -0
- data/src/util/unicode-categories.rb +572 -0
- data/src/util/wrap.rb +102 -0
- data/src/util.rb +110 -0
- data/src/widget/button.rb +33 -0
- data/src/widget/checkbox.rb +47 -0
- data/src/widget/col.rb +30 -0
- data/src/widget/image.rb +106 -0
- data/src/widget/inline.rb +40 -0
- data/src/widget/input_number.rb +73 -0
- data/src/widget/inputbox.rb +85 -0
- data/src/widget/label.rb +33 -0
- data/src/widget/modal.rb +69 -0
- data/src/widget/row.rb +26 -0
- data/src/widget/selectbox.rb +100 -0
- data/src/widget/textarea.rb +54 -0
- data/src/xml/xml.rb +80 -0
- data/test/action_test.rb +34 -0
- data/test/box_test.rb +15 -0
- data/test/css_test.rb +39 -0
- data/test/editor/editor_base_test.rb +201 -0
- data/test/element_bounds_test.rb +77 -0
- data/test/element_box_test.rb +8 -0
- data/test/element_render_test.rb +124 -0
- data/test/element_style_test.rb +85 -0
- data/test/element_test.rb +10 -0
- data/test/emitter_test.rb +108 -0
- data/test/event_test.rb +19 -0
- data/test/focus_test.rb +37 -0
- data/test/geometry_test.rb +12 -0
- data/test/input_test.rb +47 -0
- data/test/key_test.rb +14 -0
- data/test/log_test.rb +21 -0
- data/test/node_test.rb +105 -0
- data/test/performance/performance1.rb +48 -0
- data/test/renderer_test.rb +74 -0
- data/test/renderer_test_rect.rb +4 -0
- data/test/screen_test.rb +58 -0
- data/test/style_test.rb +18 -0
- data/test/termgui_test.rb +10 -0
- data/test/test_all.rb +30 -0
- data/test/util_hash_object_test.rb +93 -0
- data/test/util_test.rb +26 -0
- data/test/widget/checkbox_test.rb +99 -0
- data/test/widget/col_test.rb +87 -0
- data/test/widget/inline_test.rb +40 -0
- data/test/widget/label_test.rb +94 -0
- data/test/widget/row_test.rb +40 -0
- data/test/wrap_test.rb +11 -0
- data/test/xml_test.rb +77 -0
- 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
|