termgui 0.0.5 → 0.0.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +14 -0
- data/LICENSE +19 -0
- data/README.md +321 -0
- data/TODO.md +259 -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.rb +110 -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/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 +101 -1
@@ -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
|