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,40 @@
|
|
1
|
+
require_relative 'style'
|
2
|
+
require_relative 'key'
|
3
|
+
|
4
|
+
# takes care of printing renderer buffer in different ways
|
5
|
+
module RendererPrint
|
6
|
+
# prints current buffer as string
|
7
|
+
def print
|
8
|
+
s = []
|
9
|
+
@buffer.each_index do |y|
|
10
|
+
@buffer[y].each do |p|
|
11
|
+
s .push p.ch
|
12
|
+
end
|
13
|
+
s .push '\n'
|
14
|
+
end
|
15
|
+
s.join('')
|
16
|
+
end
|
17
|
+
|
18
|
+
def print_rows
|
19
|
+
rows = []
|
20
|
+
@buffer.each_index do |y|
|
21
|
+
line = []
|
22
|
+
@buffer[y].each do |p|
|
23
|
+
line .push p.ch
|
24
|
+
end
|
25
|
+
rows.push(line.join(''))
|
26
|
+
end
|
27
|
+
rows
|
28
|
+
end
|
29
|
+
|
30
|
+
# prints to stdout a representation in ruby string concatenated syntax so its easy for devs copy&paste for test asserts
|
31
|
+
def print_dev_stdout
|
32
|
+
print.split('\\n').each { |line| puts "'#{line}\\n' + " }
|
33
|
+
end
|
34
|
+
|
35
|
+
def print_dev
|
36
|
+
s = "'' + \n"
|
37
|
+
print.split('\\n').each { |line| s = "#{s}#{line}\\n' + \n" }
|
38
|
+
s + "''"
|
39
|
+
end
|
40
|
+
end
|
data/src/screen.rb
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
require_relative 'element'
|
2
|
+
require_relative 'renderer'
|
3
|
+
require_relative 'input'
|
4
|
+
require_relative 'event'
|
5
|
+
require_relative 'focus'
|
6
|
+
require_relative 'action'
|
7
|
+
require_relative 'util'
|
8
|
+
require_relative 'screen_element'
|
9
|
+
require_relative 'screen_input'
|
10
|
+
require_relative 'screen_renderer'
|
11
|
+
|
12
|
+
module TermGui
|
13
|
+
# Main user API entry point
|
14
|
+
# Manages instances of Input, Event, Renderer (by default disabling its buffer)
|
15
|
+
# Is a Node so new elements can be append_child
|
16
|
+
# Once `start`is called it will block execution and start an event loop
|
17
|
+
# on each interval user input is read and event listeners are called
|
18
|
+
class Screen < Node
|
19
|
+
include ScreenElement
|
20
|
+
include ScreenInput
|
21
|
+
include ScreenRenderer
|
22
|
+
attr_reader :width, :height, :input_stream, :output_stream, :renderer, :input, :event, :focus, :action
|
23
|
+
attr_accessor :silent
|
24
|
+
|
25
|
+
def initialize(
|
26
|
+
children: [], text: '', attributes: {}, exit_keys: %w[q C-c], no_exit_keys: false,
|
27
|
+
width: nil, height: nil, silent: false
|
28
|
+
)
|
29
|
+
super(name: 'screen', children: children, text: text, attributes: attributes, parent: nil)
|
30
|
+
install(%i[destroy after_destroy start after_start])
|
31
|
+
@width = width == nil ? terminal_width : width
|
32
|
+
@height = height == nil ? terminal_height : height
|
33
|
+
@input_stream = $stdin
|
34
|
+
@silent = silent
|
35
|
+
@exit_keys = exit_keys
|
36
|
+
@output_stream = $stdout
|
37
|
+
@renderer = Renderer.new(@width, @height)
|
38
|
+
@input = Input.new
|
39
|
+
@event = EventManager.new @input
|
40
|
+
@focus = FocusManager.new(root: self, event: @event)
|
41
|
+
@action = ActionManager.new(focus: @focus, event: @event)
|
42
|
+
@renderer.no_buffer = true
|
43
|
+
install_exit_keys unless no_exit_keys
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.new_for_testing(**args)
|
47
|
+
instance = new(args.merge(no_exit_keys: true, silent: true))
|
48
|
+
instance.renderer.no_buffer = false
|
49
|
+
instance
|
50
|
+
end
|
51
|
+
|
52
|
+
def terminal_width
|
53
|
+
$stdout.winsize[1]
|
54
|
+
rescue StandardError
|
55
|
+
80
|
56
|
+
end
|
57
|
+
|
58
|
+
def terminal_height
|
59
|
+
$stdout.winsize[0]
|
60
|
+
rescue StandardError
|
61
|
+
24
|
62
|
+
end
|
63
|
+
|
64
|
+
# start listening for user input. This starts an user input event loop
|
65
|
+
# that ends when screen.destroy is called
|
66
|
+
def start(clean: false)
|
67
|
+
emit :start
|
68
|
+
unless clean
|
69
|
+
clear
|
70
|
+
cursor_hide # TODO: move this to a CursorManager :start listener
|
71
|
+
render
|
72
|
+
end
|
73
|
+
emit :after_start
|
74
|
+
yield if block_given?
|
75
|
+
@input.start
|
76
|
+
end
|
77
|
+
|
78
|
+
def destroy
|
79
|
+
emit :destroy
|
80
|
+
@input.stop
|
81
|
+
cursor_show # TODO: move this to a CursorManager :destroy listener
|
82
|
+
emit :after_destroy
|
83
|
+
end
|
84
|
+
|
85
|
+
# writes directly to @output_stream. Shouldn't be used directly since these changes won't be tracked by the buffer.
|
86
|
+
def write(s)
|
87
|
+
@output_stream.write s unless @silent
|
88
|
+
s
|
89
|
+
end
|
90
|
+
|
91
|
+
def alert
|
92
|
+
puts "\a"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
Screen = TermGui::Screen
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require_relative 'geometry'
|
2
|
+
|
3
|
+
# so screen emulates an Element. TODO: make Screen < Element and get rid of width and height and all of this...
|
4
|
+
module ScreenElement
|
5
|
+
# complies with Element#render and also is capable of rendering given elements
|
6
|
+
def render(element = nil)
|
7
|
+
if element == self || element.nil?
|
8
|
+
children.each { |child| child.render self }
|
9
|
+
elsif !element.nil?
|
10
|
+
element.render self
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def abs_x
|
15
|
+
0
|
16
|
+
end
|
17
|
+
|
18
|
+
def abs_y
|
19
|
+
0
|
20
|
+
end
|
21
|
+
|
22
|
+
def abs_width
|
23
|
+
@width
|
24
|
+
end
|
25
|
+
|
26
|
+
def style
|
27
|
+
@style ||= Style.new
|
28
|
+
@style
|
29
|
+
end
|
30
|
+
|
31
|
+
def style=(s)
|
32
|
+
@style = s
|
33
|
+
end
|
34
|
+
|
35
|
+
def final_style
|
36
|
+
style
|
37
|
+
end
|
38
|
+
|
39
|
+
def offset
|
40
|
+
v = get_attribute('offset')
|
41
|
+
unless v
|
42
|
+
v = Offset.new
|
43
|
+
set_attribute('offset', v)
|
44
|
+
end
|
45
|
+
v
|
46
|
+
end
|
47
|
+
|
48
|
+
def offset=(value)
|
49
|
+
set_attribute('offset', value)
|
50
|
+
end
|
51
|
+
|
52
|
+
def abs_height
|
53
|
+
@height
|
54
|
+
end
|
55
|
+
|
56
|
+
def root_screen
|
57
|
+
self
|
58
|
+
end
|
59
|
+
end
|
data/src/screen_input.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
# adds Input related methods to screen
|
2
|
+
module ScreenInput
|
3
|
+
attr_accessor :exit_keys
|
4
|
+
|
5
|
+
# Analog to HTML DOM / Node.js setTimeout() using input event loop
|
6
|
+
# @param {Number} seconds
|
7
|
+
def set_timeout(seconds = @input.interval, listener = nil, &block)
|
8
|
+
the_listener = listener == nil ? block : listener
|
9
|
+
throw 'No listener provided' if the_listener == nil
|
10
|
+
@input.set_timeout(seconds, the_listener)
|
11
|
+
end
|
12
|
+
|
13
|
+
def clear_timeout(listener)
|
14
|
+
@input.clear_timeout(listener)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Analog to HTML DOM / Node.js setInterval() using input event loop
|
18
|
+
# @param {Number} seconds
|
19
|
+
def set_interval(seconds = @input.interval, listener = nil, &block)
|
20
|
+
the_listener = listener == nil ? block : listener
|
21
|
+
throw 'No listener provided' if the_listener == nil
|
22
|
+
@input.set_interval(seconds, the_listener)
|
23
|
+
end
|
24
|
+
|
25
|
+
def clear_interval(listener)
|
26
|
+
@input.clear_interval(listener)
|
27
|
+
end
|
28
|
+
|
29
|
+
def install_exit_keys
|
30
|
+
return if @exit_keys_listener
|
31
|
+
|
32
|
+
@exit_keys_listener = @input.subscribe('key') do |e|
|
33
|
+
destroy if @exit_keys.include?(e.key)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def uninstall_exit_keys
|
38
|
+
return unless @exit_keys_listener
|
39
|
+
|
40
|
+
@input.off('key', @exit_keys_listener)
|
41
|
+
@exit_keys_listener = nil
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# adds rendering related methods to screen
|
2
|
+
module ScreenRenderer
|
3
|
+
# renders given text at given position
|
4
|
+
def text(x: 0, y: 0, text: ' ', style: nil)
|
5
|
+
write @renderer.text(x: x, y: y, text: text, style: style)
|
6
|
+
end
|
7
|
+
|
8
|
+
def rect(x: 0, y: 0, width: self.width, height: self.height, ch: Pixel.EMPTY_CH, style: nil)
|
9
|
+
write @renderer.rect(x: x, y: y, width: width, height: height, ch: ch, style: style)
|
10
|
+
end
|
11
|
+
|
12
|
+
def image(x: 0, y: 0, image: nil, ch: Pixel.EMPTY_CH, style: Style.new, fg: false, bg: true, transparent_color: nil, h: height - y, w: width - x)
|
13
|
+
write @renderer.image(x: x, y: y, image: image, ch: ch || Pixel.EMPTY_CH, style: style, fg: fg, bg: bg, transparent_color: transparent_color, h: h, w: w)
|
14
|
+
end
|
15
|
+
|
16
|
+
def circle(x: nil, y: nil, radius: nil, stroke_ch: ' ', stroke: nil, fill: nil, fill_ch: stroke_ch)
|
17
|
+
write @renderer.circle(x: x, y: y, radius: radius, stroke_ch: stroke_ch, stroke: stroke, fill: fill, fill_ch: fill_ch)
|
18
|
+
end
|
19
|
+
|
20
|
+
def clear
|
21
|
+
@renderer.style = Style.new
|
22
|
+
write "#{@renderer.clear}#{@renderer.style.print}"
|
23
|
+
end
|
24
|
+
|
25
|
+
def style=(style)
|
26
|
+
@renderer.style = style
|
27
|
+
write @renderer.style.print
|
28
|
+
end
|
29
|
+
|
30
|
+
def box(x, y, width, height, border_style = :classic, style = nil, content = ' ')
|
31
|
+
self.style = style if style
|
32
|
+
box = draw_box(width: width, height: height, style: border_style, content: content)
|
33
|
+
(box.map.with_index do |line, index|
|
34
|
+
text(x: x, y: y + index, text: line, style: style)
|
35
|
+
end).join('')
|
36
|
+
end
|
37
|
+
|
38
|
+
def print
|
39
|
+
@renderer.print
|
40
|
+
end
|
41
|
+
|
42
|
+
def cursor_move(x, y)
|
43
|
+
write @renderer.move(x, y)
|
44
|
+
end
|
45
|
+
|
46
|
+
def cursor_show
|
47
|
+
write @renderer.cursor_show
|
48
|
+
end
|
49
|
+
|
50
|
+
def cursor_hide
|
51
|
+
write @renderer.cursor_hide
|
52
|
+
end
|
53
|
+
end
|
data/src/style.rb
ADDED
@@ -0,0 +1,149 @@
|
|
1
|
+
require_relative 'util'
|
2
|
+
require_relative 'util/hash_object'
|
3
|
+
require_relative 'tco/tco_termgui'
|
4
|
+
|
5
|
+
module TermGui
|
6
|
+
# refers to properties directly implemented using ansi escape codes
|
7
|
+
# responsible of printing escape ansi codes for style
|
8
|
+
# Styles are data objects, supporting hash to instantiate, assign, equals, print
|
9
|
+
class BaseStyle
|
10
|
+
include HashObject
|
11
|
+
|
12
|
+
attr_accessor :fg, :bg, :underline, :bold, :blink, :inverse, :fraktur, :framed
|
13
|
+
|
14
|
+
def initialize(fg: nil, bg: nil, bold: nil, blink: nil, inverse: nil, underline: nil, framed: nil, fraktur: nil, bright: nil, wrap: nil, border: nil, padding: nil, style: nil)
|
15
|
+
# TODO: for some reason **args is not working here that's why we have all subclasses props
|
16
|
+
@fg = fg
|
17
|
+
@bg = bg
|
18
|
+
@bold = bold || bright
|
19
|
+
@blink = blink
|
20
|
+
@inverse = inverse
|
21
|
+
@underline = underline
|
22
|
+
@fraktur = fraktur
|
23
|
+
@framed = framed
|
24
|
+
end
|
25
|
+
|
26
|
+
# Prints the style as escape sequences.
|
27
|
+
# This method shouln't be overriden by subclasses since it only makes sense for basic properties defined here.
|
28
|
+
def print(s = nil)
|
29
|
+
if s == nil
|
30
|
+
TermGui.open_style(self)
|
31
|
+
else
|
32
|
+
TermGui.print(s, self)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.fast_colouring(value)
|
37
|
+
TermGui.fast_colouring(value)
|
38
|
+
end
|
39
|
+
|
40
|
+
def reset
|
41
|
+
@bg = @fg = @wrap = @border = nil
|
42
|
+
end
|
43
|
+
|
44
|
+
# returns true if self has the same properties of given hash or Style and each property value is equals (comparission using ==)
|
45
|
+
def equals(style)
|
46
|
+
object_equal(self, BaseStyle.from_hash(style))
|
47
|
+
end
|
48
|
+
|
49
|
+
# if a hash is given returns a new Style instance with given properties. If an Style instance if given, returns it.
|
50
|
+
# It also ensures focus, action and enter properties are defined cloning self if not
|
51
|
+
def self.from_hash(obj)
|
52
|
+
if obj == nil
|
53
|
+
return nil
|
54
|
+
elsif obj.instance_of?(Hash)
|
55
|
+
r = merge_hash_into_object obj, new
|
56
|
+
else
|
57
|
+
r = obj
|
58
|
+
end
|
59
|
+
|
60
|
+
if r.is_a? Style
|
61
|
+
r.focus = r.focus || r.clone
|
62
|
+
r.border = Border.from_hash(r.border) if r.border
|
63
|
+
r.action = r.action || r.clone
|
64
|
+
r.enter = r.enter || r.clone
|
65
|
+
end
|
66
|
+
r
|
67
|
+
end
|
68
|
+
|
69
|
+
def pretty_print(delete_nil = true, delete_empty = true)
|
70
|
+
h = to_hash
|
71
|
+
h.keys.each do |k|
|
72
|
+
h.delete k if delete_nil && h[k] == nil
|
73
|
+
if delete_empty && (h[k].respond_to?(:to_hash) || h[k].is_a?(Hash)) && object_variables_to_hash(h[k]).keys.reject { |k| h[k] == nil }.empty?
|
74
|
+
h.delete k
|
75
|
+
end
|
76
|
+
end
|
77
|
+
"{#{h.keys.map { |k| "#{k}: #{pretty_print_value(h[k])}" }.join(', ')}}" .split(/, [^\s]+: \{\}/).join('')
|
78
|
+
end
|
79
|
+
|
80
|
+
def pretty_print_value(v)
|
81
|
+
v.is_a?(String) ? v : v.respond_to?(:pretty_print) ? v.pretty_print : v.to_s
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.from_json(s)
|
85
|
+
r = from_hash(json_parse(s))
|
86
|
+
if r.is_a? Style
|
87
|
+
r.border = from_hash(r.border || new)
|
88
|
+
r.focus = from_hash(r.focus || new)
|
89
|
+
r.enter = from_hash(r.enter || new)
|
90
|
+
r.action = from_hash(r.action || new)
|
91
|
+
end
|
92
|
+
r
|
93
|
+
end
|
94
|
+
|
95
|
+
def bright
|
96
|
+
@bold
|
97
|
+
end
|
98
|
+
|
99
|
+
def bright=(value)
|
100
|
+
@bold = value
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Element style. This is the class of `element.style` - `get_attribute('style')``
|
105
|
+
class Style < BaseStyle
|
106
|
+
attr_accessor :border, :wrap, :padding, :focus, :enter, :action
|
107
|
+
|
108
|
+
def initialize(**args)
|
109
|
+
super
|
110
|
+
@wrap = args[:wrap]
|
111
|
+
# TODO: move this border checking & init to hash_object
|
112
|
+
if args[:border].nil?
|
113
|
+
@border = nil
|
114
|
+
elsif args[:border].instance_of? Border
|
115
|
+
@border = args[:border]
|
116
|
+
elsif args[:border].instance_of? Hash
|
117
|
+
@border = Border.new
|
118
|
+
@border.assign(args[:border])
|
119
|
+
end
|
120
|
+
padding = args[:padding]
|
121
|
+
focus = args[:focus] || clone
|
122
|
+
enter = args[:enter] || clone
|
123
|
+
action = args[:action] || clone
|
124
|
+
|
125
|
+
@padding = padding
|
126
|
+
@focus = focus
|
127
|
+
@enter = enter
|
128
|
+
@action = action
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# style for the border
|
133
|
+
class Border < BaseStyle
|
134
|
+
attr_reader :style
|
135
|
+
|
136
|
+
def initialize(**args)
|
137
|
+
super
|
138
|
+
@style = args[:style]&.to_s || 'single'
|
139
|
+
end
|
140
|
+
|
141
|
+
def style=(style)
|
142
|
+
@style = style&.to_s
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
BaseStyle = TermGui::BaseStyle
|
148
|
+
Border = TermGui::Border
|
149
|
+
Style = TermGui::Style
|
@@ -0,0 +1,248 @@
|
|
1
|
+
# rubocop:disable Layout/EndAlignment
|
2
|
+
|
3
|
+
# adapted from tco
|
4
|
+
# tco - terminal colouring application and library
|
5
|
+
# Copyright (c) 2013, 2014 Radek Pazdera
|
6
|
+
|
7
|
+
require_relative 'palette'
|
8
|
+
|
9
|
+
module Tco
|
10
|
+
class Colouring
|
11
|
+
ANSI_FG_BASE = 30
|
12
|
+
ANSI_BG_BASE = 40
|
13
|
+
|
14
|
+
attr_reader :palette
|
15
|
+
|
16
|
+
def initialize(configuration)
|
17
|
+
@palette = Palette.new configuration.options['palette']
|
18
|
+
@output_type = configuration.options['output']
|
19
|
+
@disabled = configuration.options['disabled']
|
20
|
+
|
21
|
+
configuration.colour_values.each do |id, value|
|
22
|
+
@palette.set_colour_value(parse_colour_id(id), parse_rgb_value(value))
|
23
|
+
end
|
24
|
+
|
25
|
+
@names = {}
|
26
|
+
configuration.names.each do |name, colour_def|
|
27
|
+
@names[name] = resolve_colour_def colour_def
|
28
|
+
end
|
29
|
+
|
30
|
+
@styles = {}
|
31
|
+
configuration.styles.each do |name, style|
|
32
|
+
@styles[name] = Style.new(resolve_colour_def(style[:fg]),
|
33
|
+
resolve_colour_def(style[:bg]),
|
34
|
+
style[:bright], style[:underline])
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Decorate a string according to the style passed. The input string
|
39
|
+
# is processed line-by-line (the escape sequences are added to each
|
40
|
+
# line). This is due to some problems I've been having with some
|
41
|
+
# terminal emulators not handling multi-line coloured sequences well.
|
42
|
+
def decorate(string, style)
|
43
|
+
# (fg, bg, bright, underline)
|
44
|
+
# fg = style.fg
|
45
|
+
# bg = style.bg
|
46
|
+
# bright = style.bright
|
47
|
+
# underline = style.underline
|
48
|
+
return string if !STDOUT.isatty || @output_type == :raw || @disabled
|
49
|
+
|
50
|
+
fg = get_colour_instance style.fg
|
51
|
+
bg = get_colour_instance style.bg
|
52
|
+
|
53
|
+
output = []
|
54
|
+
lines = string.lines.map(&:chomp)
|
55
|
+
lines = [''] if lines.length.zero?
|
56
|
+
lines.each do |line|
|
57
|
+
unless line.length < 0
|
58
|
+
line = case @palette.type
|
59
|
+
when 'ansi' then colour_ansi line, fg, bg
|
60
|
+
when 'extended' then colour_extended line, fg, bg
|
61
|
+
else raise "Unknown palette '#{@palette.type}'."
|
62
|
+
end
|
63
|
+
|
64
|
+
line = e(1) + line if style.bright
|
65
|
+
line = e(4) + line if style.underline
|
66
|
+
line = e(5) + line if style.blink
|
67
|
+
line = e(7) + line if style.inverse
|
68
|
+
line = e(20) + line if style.fraktur
|
69
|
+
line = e(51) + line if style.framed
|
70
|
+
|
71
|
+
if (style.bright || style.underline || style.blink || style.inverse || style.fraktur || style.framed) && (fg == nil) && (bg == nil)
|
72
|
+
line << e(0)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
output.push line
|
77
|
+
end
|
78
|
+
|
79
|
+
output << '' if string =~ /\n$/
|
80
|
+
output.join "\n"
|
81
|
+
end
|
82
|
+
|
83
|
+
def get_style(name)
|
84
|
+
raise "Style '#{name}' not found." unless @styles.key? name
|
85
|
+
|
86
|
+
@styles[name]
|
87
|
+
end
|
88
|
+
|
89
|
+
def set_output(output_type)
|
90
|
+
raise "Output '#{output_type}' not supported." unless %i[term raw].include? output_type
|
91
|
+
|
92
|
+
@output_type = output_type
|
93
|
+
end
|
94
|
+
|
95
|
+
def get_best_font_colour(background)
|
96
|
+
black = Tco::Colour.new([0, 0, 0])
|
97
|
+
white = Tco::Colour.new([255, 255, 255])
|
98
|
+
|
99
|
+
if background.is_a?(Colour) &&
|
100
|
+
(background - black).abs < (background - white).abs
|
101
|
+
return white
|
102
|
+
end
|
103
|
+
|
104
|
+
black
|
105
|
+
end
|
106
|
+
|
107
|
+
def get_colour_instance(value)
|
108
|
+
if value.is_a?(String)
|
109
|
+
resolve_colour_def value
|
110
|
+
elsif value.is_a?(Symbol)
|
111
|
+
resolve_colour_def value.to_s
|
112
|
+
elsif value.is_a?(Array)
|
113
|
+
Colour.new value
|
114
|
+
elsif value.is_a?(Colour) || value.is_a?(Unknown)
|
115
|
+
value
|
116
|
+
elsif value == nil
|
117
|
+
nil
|
118
|
+
else
|
119
|
+
raise "Colour value type '#{value.class}' not supported."
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
def e(seq)
|
126
|
+
if @output_type == :raw
|
127
|
+
"\\033[#{seq}m"
|
128
|
+
else
|
129
|
+
"\033[#{seq}m"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def colour_ansi(string, fg = nil, bg = nil)
|
134
|
+
unless fg == nil
|
135
|
+
colour_id = if fg.is_a? Unknown
|
136
|
+
fg.id
|
137
|
+
else
|
138
|
+
@palette.match_colour(fg)
|
139
|
+
end
|
140
|
+
string = e(colour_id + 30) + string
|
141
|
+
end
|
142
|
+
|
143
|
+
unless bg == nil
|
144
|
+
colour_id = if bg.is_a? Unknown
|
145
|
+
bg.id
|
146
|
+
else
|
147
|
+
@palette.match_colour(bg)
|
148
|
+
end
|
149
|
+
string = e(colour_id + 40) + string
|
150
|
+
end
|
151
|
+
|
152
|
+
string << e(0) unless (fg == nil) && (bg == nil)
|
153
|
+
|
154
|
+
string
|
155
|
+
end
|
156
|
+
|
157
|
+
def colour_extended(string, fg = nil, bg = nil)
|
158
|
+
unless fg == nil
|
159
|
+
colour_id = if fg.is_a? Unknown
|
160
|
+
fg.id
|
161
|
+
else
|
162
|
+
@palette.match_colour(fg)
|
163
|
+
end
|
164
|
+
string = e("38;5;#{colour_id}") + string
|
165
|
+
end
|
166
|
+
|
167
|
+
unless bg == nil
|
168
|
+
colour_id = if bg.is_a? Unknown
|
169
|
+
bg.id
|
170
|
+
else
|
171
|
+
@palette.match_colour(bg)
|
172
|
+
end
|
173
|
+
string = e("48;5;#{colour_id}") + string
|
174
|
+
end
|
175
|
+
|
176
|
+
string << e(0) unless (fg == nil) && (bg == nil)
|
177
|
+
|
178
|
+
string
|
179
|
+
end
|
180
|
+
|
181
|
+
def parse_colour_id(id_in_string)
|
182
|
+
id = String.new(id_in_string)
|
183
|
+
if id[0] == '@'
|
184
|
+
id[0] = ''
|
185
|
+
return id.to_i
|
186
|
+
end
|
187
|
+
|
188
|
+
raise "Invalid colour id #{id_in_string}."
|
189
|
+
end
|
190
|
+
|
191
|
+
def parse_rgb_value(rgb_value_in_string)
|
192
|
+
error_msg = "Invalid RGB value '#{rgb_value_in_string}'."
|
193
|
+
rgb_value = String.new rgb_value_in_string
|
194
|
+
if rgb_value[0] == '#'
|
195
|
+
rgb_value[0] = ''
|
196
|
+
elsif rgb_value[0..1] == '0x'
|
197
|
+
rgb_value[0..1] = ''
|
198
|
+
else
|
199
|
+
raise error_msg
|
200
|
+
end
|
201
|
+
|
202
|
+
raise error_msg unless rgb_value =~ /^[0-9a-fA-F]+$/
|
203
|
+
|
204
|
+
case rgb_value.length
|
205
|
+
when 3
|
206
|
+
rgb_value.scan(/./).map { |c| c.to_i 16 }
|
207
|
+
when 6
|
208
|
+
rgb_value.scan(/../).map { |c| c.to_i 16 }
|
209
|
+
else
|
210
|
+
raise error_msg
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
def resolve_colour_name(name)
|
215
|
+
raise "Name '#{name}' not found." unless @names.key? name
|
216
|
+
|
217
|
+
@names[name]
|
218
|
+
end
|
219
|
+
|
220
|
+
def resolve_colour_def(colour_def)
|
221
|
+
return nil if colour_def == '' || colour_def == 'default'
|
222
|
+
|
223
|
+
begin
|
224
|
+
id = parse_colour_id colour_def
|
225
|
+
if @palette.is_known? id
|
226
|
+
Colour.new @palette.get_colour_value id
|
227
|
+
else
|
228
|
+
Unknown.new id
|
229
|
+
end
|
230
|
+
rescue RuntimeError
|
231
|
+
begin
|
232
|
+
Colour.new parse_rgb_value colour_def
|
233
|
+
rescue RuntimeError
|
234
|
+
begin
|
235
|
+
colour_def = resolve_colour_name colour_def
|
236
|
+
if colour_def.is_a? String
|
237
|
+
resolve_colour_def colour_def
|
238
|
+
else
|
239
|
+
colour_def
|
240
|
+
end
|
241
|
+
rescue RuntimeError
|
242
|
+
raise "Invalid colour definition '#{colour_def}'."
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|