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
data/src/input_time.rb ADDED
@@ -0,0 +1,97 @@
1
+ require_relative 'util'
2
+
3
+ # Add Input support for set_timeout and set_interval based on input event loop.
4
+ # Assumes `@time = Time.now` is assigned on each iteration.
5
+ module InputTime
6
+ def initialize(*_args)
7
+ @time = Time.now
8
+ @timeout_listeners = []
9
+ @interval_listeners = []
10
+ @wait_timeout = 5
11
+ @wait_interval = 0.1
12
+ end
13
+
14
+ # register a timeout listener that will be called in given seconds (aprox).
15
+ # Returns a listener object that can be used with clear_timeout to remove the listener
16
+ def set_timeout(seconds = @interval, block = nil, &proc_block)
17
+ the_block = block == nil ? proc_block : block
18
+ throw 'No block provided' if the_block == nil
19
+ listener = { seconds: seconds, block: the_block, target: @time + seconds }
20
+ @timeout_listeners.push listener
21
+ listener
22
+ end
23
+
24
+ # clears a previoulsy registered timeout listener. Use the same block (or what set_timeout returned)
25
+ def clear_timeout(listener)
26
+ @timeout_listeners.delete listener
27
+ end
28
+
29
+ # similar to HTML window.setInterval
30
+ def set_interval(seconds = @interval, block = nil, &proc_block)
31
+ seconds = seconds == nil ? @interval : seconds
32
+ the_block = block == nil ? proc_block : block
33
+ throw 'No block provided' if the_block == nil
34
+ listener = { seconds: seconds || @interval, block: the_block, next: @time + seconds }
35
+ @interval_listeners.push listener
36
+ listener
37
+ end
38
+
39
+ # clears a previoulsy registered interval listener. Use the same block (or what set_interval returned)
40
+ def clear_interval(listener)
41
+ @interval_listeners.delete listener
42
+ end
43
+
44
+ def wait_for(predicate, timeout = @wait_timeout, interval = @wait_interval, &block)
45
+ throw 'No block provided' unless block
46
+ interval_listener = nil
47
+ timeout_listener = set_timeout(timeout) do
48
+ clear_interval interval_listener
49
+ block.call true
50
+ end
51
+ interval_listener = set_interval(interval) do
52
+ if predicate.call
53
+ clear_interval interval_listener
54
+ clear_timeout timeout_listener
55
+ block.call false
56
+ end
57
+ end
58
+ [timeout_listener, interval_listener]
59
+ end
60
+
61
+ def clear_wait_for(listeners)
62
+ clear_interval listeners[1]
63
+ clear_timeout listeners[0]
64
+ end
65
+
66
+ def stop
67
+ super
68
+ @timeout_listeners = []
69
+ @interval_listeners = []
70
+ end
71
+
72
+ protected
73
+
74
+ def update_status
75
+ @time = Time.now
76
+ dispatch_set_interval
77
+ dispatch_set_timeout
78
+ end
79
+
80
+ def dispatch_set_timeout
81
+ @timeout_listeners.each do |listener|
82
+ if listener[:target] < @time
83
+ listener[:block].call
84
+ @timeout_listeners.delete listener
85
+ end
86
+ end
87
+ end
88
+
89
+ def dispatch_set_interval
90
+ @interval_listeners.each do |listener|
91
+ if listener[:next] < @time
92
+ listener[:block].call
93
+ listener[:next] = @time + listener[:seconds]
94
+ end
95
+ end
96
+ end
97
+ end
data/src/key.rb ADDED
@@ -0,0 +1,114 @@
1
+ # maps charsequences like '\n', '\e[1;7C' to objects like {name: 'enter'}, {name: 'right', control: true, meta: true}
2
+ # TODO: support meta-d ('\xC3': 'C-', '\xB0': 'C-') which is defined by two characters
3
+
4
+ require_relative 'log'
5
+
6
+ CSI = "\e[".freeze
7
+
8
+ CHAR_NAMES = {
9
+
10
+ '\e[A': 'up',
11
+ '\e[1;3A': 'M-up',
12
+ 'e[1;2A': 'S-up',
13
+ '\e[1;6A': 'S-C-up',
14
+ '\e[1;7A': 'M-C-up',
15
+
16
+ '\e[B': 'down',
17
+ '\e[1;3B': 'M-down',
18
+ 'e[1;2B': 'S-down',
19
+ '\e[1;6B': 'S-C-down',
20
+ '\e[1;7B': 'M-C-down',
21
+
22
+ '\e[D': 'left',
23
+ '\eb': 'M-left',
24
+ 'e[1;2D': 'S-left',
25
+ '\e[1;6D': 'S-C-left',
26
+ '\e[1;7D': 'M-C-left',
27
+
28
+ '\e[C': 'right',
29
+ '\ef': 'M-right',
30
+ 'e[1;2C': 'S-right',
31
+ '\e[1;6C': 'S-C-right',
32
+ '\e[1;7C': 'M-C-right',
33
+
34
+ '\e': 'escape',
35
+ '\r': 'enter',
36
+ '\t': 'tab',
37
+ '\e[Z': 'S-tab',
38
+
39
+ '\e[3~': 'delete',
40
+ "\u232B": 'delete',
41
+
42
+ '\x7F': 'backspace',
43
+ ' ': 'space',
44
+
45
+ # control-x
46
+ '\x01': 'C-a',
47
+ '\x02': 'C-b',
48
+ '\x03': 'C-c',
49
+ '\x04': 'C-d',
50
+ '\x05': 'C-e',
51
+ '\x06': 'C-f',
52
+ '\a': 'C-g',
53
+ '\b': 'C-h',
54
+ # '\t': "C-i",
55
+ '\n': 'C-j',
56
+ '\v': 'C-k',
57
+ '\f': 'C-l',
58
+ '\x0E': 'C-n',
59
+ '\x0F': 'C-o',
60
+ '\x10': 'C-p',
61
+ '\x11': 'C-q',
62
+ '\x12': 'C-r',
63
+ '\x13': 'C-s',
64
+ '\x14': 'C-t',
65
+ '\x15': 'C-u',
66
+ '\x16': 'C-v',
67
+ '\x18': 'C-x',
68
+ '\x19': 'C-y',
69
+ '\x1A': 'C-z'
70
+ }.freeze
71
+
72
+ def name_to_char(name)
73
+ char = CHAR_NAMES.keys.find { |c| CHAR_NAMES[c] == name }
74
+ char ? char.to_s : name
75
+ end
76
+
77
+ def char_to_name(ch)
78
+ CHAR_NAMES[:"#{ch}"]
79
+ end
80
+
81
+ ALPHANUMERICS =
82
+ ('a'...'z').to_a
83
+ .concat(('A'..'Z').to_a)
84
+ .concat(('0'...'9').to_a)
85
+ .concat(%w[z Z 9])
86
+ .concat(',.-;:_ç´Ç¨+`*^{}[]¡\'¡¿?ªº\\!"·$%&/()=|@#¢∞¬÷“”'.split(''))
87
+
88
+ def alphanumeric?(c)
89
+ ALPHANUMERICS.include? c
90
+ end
91
+
92
+ NUMERICS = ['.'].concat(('0'...'9').to_a).push('9')
93
+
94
+ def numeric?(c)
95
+ NUMERICS.include? c
96
+ end
97
+
98
+ BRAILE = ['⠋', ',⠇', '⠉', '⠃', '⠙', '⢮', '⢯', '⢱', '⢲', '⢳', '⢴', '⢵', '⢶', '⢷', '⢸', '⢹', '⢺', '⢻', '⢼', '⢽', '⢾', '⠂', '⠃', '⠄', '⠅', '⠆', '⠇', '⠈', '⠉', '⠊', '⠋', '⠍', '⠎', '⠏', '⠐', '⠑', '⠒', '⠓', '⠕', '⠖', '⠗', '⠘', '⠙', '⠚', '⠛', '⠜', '⠝', '⠞', '⠟', '⠠', '⠡', '⠢', '⠣', '⠤', '⠥', '⠦', '⠧', '⠨', '⠩', '⠫', '⠬', '⠭', '⠮', '⠯', '⠰', '⠱', '⠲', '⠳', '⠴', '⠶', '⠷', '⠹', '⠺', '⠻', '⠼', '⠽', '⠾', '⠿', '⡀', '⡁', '⡂', '⡄', '⡅', '⡆', '⡇', '⡈', '⡉', '⡊', '⡋', '⡌', '⡍', '⡏', '⡐', '⡑', '⡒', '⡓', '⡔', '⡕', '⡖', '⡗', '⡘', '⡚', '⡛', '⡝', '⡞', '⡟', '⡠', '⡡', '⡣', '⡤', '⡥', '⡦', '⡧', '⡨', '⡩', '⡪', '⡫', '⡬', '⡭', '⡮', '⡯', '⡱', '⡲', '⡳', '⡴', '⡵', '⡶', '⡷', '⡸', '⡹', '⡺', '⡻', '⡼', '⡽', '⡾', '⡿', '⢀', '⢂', '⢃', '⢄', '⢅', '⢆', '⢇', '⢈', '⢉', '⢊', '⢋', '⢍', '⢎', '⢏', '⢐', '⢑', '⢒', '⢓', '⢔', '⢕', '⢖', '⢗', '⢘', '⢙', '⢚', '⢛', '⢜', '⢝', '⢞', '⢟', '⢠', '⢡', '⢣', '⢤', '⢥', '⢦', '⢧', '⢨', '⢩', '⢪', '⢫', '⢬', '⢭', '⢮', '⢯', '⢱', '⢲', '⢳', '⢴', '⢵', '⢶', '⢷', '⢸', '⢹', '⢺', '⢻', '⢼', '⢽', '⢾', '⢿', '⣀', '⣂', '⣃', '⣄', '⣅', '⣆', '⣇', '⣈', '⣉', '⣊', '⣋', '⣍', '⣎', '⣏', '⣐', '⣑', '⣓', '⣔', '⣕', '⣖', '⣗', '⣙', '⣚', '⣛', '⣜', '⣝', '⣞', '⣟', '⣠', '⣢', '⣣', '⣥', '⣦', '⣧', '⣨', '⣩', '⣪', '⣫', '⣬', '⣭', '⣮', '⣯', '⣱', '⣲', '⣳', '⣴', '⣵', '⣶', '⣷', '⣸', '⣹', '⣺', '⣻', '⣼', '⣽', '⣾', '⣿'].freeze
99
+
100
+ BRAILE_FILLED = ['⢮', '⢯', '⢱', '⢲', '⢳', '⢴', '⢵', '⢶', '⢷', '⢻', '⢼', '⢽', '⢾', '⠟', '⠮', '⠯', '⠳', '⠴', '⠶', '⠷', '⠹', '⠺', '⠻', '⠼', '⠽', '⠾', '⠿', '⡝', '⡞', '⡟', '⡪', '⡫', '⡬', '⡭', '⡮', '⡯', '⡱', '⡲', '⡳', '⡴', '⡵', '⡶', '⡷', '⡸', '⡹', '⡺', '⡻', '⡼', '⡽', '⡾', '⡿', '⢜', '⢝', '⢞', '⢟', '⢭', '⢮', '⢯', '⢱', '⢲', '⢳', '⢴', '⢵', '⢶', '⢷', '⢸', '⢹', '⢺', '⢻', '⢼', '⢽', '⢾', '⢿', '⣀', '⣂', '⣃', '⣄', '⣅', '⣆', '⣇', '⣈', '⣉', '⣊', '⣋', '⣍', '⣎', '⣏', '⣐', '⣑', '⣓', '⣔', '⣕', '⣖', '⣗', '⣙', '⣚', '⣛', '⣜', '⣝', '⣞', '⣟', '⣠', '⣢', '⣣', '⣥', '⣦', '⣧', '⣨', '⣩', '⣪', '⣫', '⣬', '⣭', '⣮', '⣯', '⣱', '⣲', '⣳', '⣴', '⣵', '⣶', '⣷', '⣸', '⣹', '⣺', '⣻', '⣼', '⣽', '⣾', '⣿'].freeze
101
+
102
+ BLOCKS = ['▀', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█', '▉', '▊', '▋', '▌', '▍', '▎', '▏', '▐', '░', '▒', '▓', '▔', '▕', '▖', '▗', '▘', '▙', '▚', '▛', '▜', '▝', '▞', '▟', '■', '□', '▢', '▣', '▤', '▥', '▦', '▧', '▨', '▩', '▪', '▫', '▬', '▭', '▮', '▯'].freeze
103
+
104
+ GEOMETRIC_SHAPES = ['■', '□', '▢', '▣', '▤', '▥', '▦', '▧', '▨', '▩', '▪', '▫', '▬', '▭', '▮', '▯', '▰', '▱', '▲', '△', '▴', '▵', '▶', '▷', '▸', '▹', '►', '▻', '▼', '▽', '▾', '▿', '◀', '◁', '◂', '◃', '◄', '◅', '◆', '◇', '◈', '◉', '◊', '○', '◌', '◍', '◎', '●', '◐', '◑', '◒', '◓', '◔', '◕', '◖', '◗', '◘', '◙', '◚', '◛', '◜', '◝', '◞', '◟', '◠', '◡', '◢', '◣', '◤', '◥', '◦', '◧', '◨', '◩', '◪', '◫', '◬', '◭', '◮', '◯', '◰', '◱', '◲', '◳', '◴', '◵', '◶', '◷', '◸', '◹', '◺', '◻', '◼', '◽', '◾', '◿'].freeze
105
+
106
+ KANGXI = ['⼁', '⼂', '⼃', '⼄', '⼅', '⼆', '⼇', '⼈', '⼉', '⼊', '⼋', '⼌', '⼍', '⼎', '⼏', '⼐', '⼑', '⼒', '⼓', '⼔', '⼕', '⼖', '⼗', '⼘', '⼙', '⼚', '⼛', '⼜', '⼝', '⼞', '⼟', '⼠', '⼡', '⼢', '⼣', '⼤', '⼥', '⼦', '⼧', '⼨', '⼩', '⼪', '⼫', '⼬', '⼭', '⼮', '⼯', '⼰', '⼱', '⼲', '⼳', '⼴', '⼵', '⼶', '⼷', '⼸', '⼹', '⼺', '⼻', '⼼', '⼽', '⼾', '⼿', '⽀', '⽁', '⽂', '⽃', '⽄', '⽅', '⽆', '⽇', '⽈', '⽉', '⽊', '⽋', '⽌', '⽍', '⽎', '⽏', '⽐', '⽑', '⽒', '⽓', '⽔', '⽕', '⽖', '⽗', '⽘', '⽙', '⽚', '⽛', '⽜', '⽝', '⽞', '⽟', '⽠', '⽡', '⽢', '⽣', '⽤', '⽥', '⽦', '⽧', '⽨', '⽩', '⽪', '⽫', '⽬', '⽭', '⽮', '⽯', '⽰', '⽱', '⽲', '⽳', '⽴', '⽵', '⽶', '⽷', '⽸', '⽹', '⽺', '⽻', '⽼', '⽽', '⽾', '⽿', '⾀', '⾁', '⾂', '⾃', '⾄', '⾅', '⾆', '⾇', '⾈', '⾉', '⾊', '⾋', '⾌', '⾍', '⾎', '⾏', '⾐', '⾑', '⾒', '⾓', '⾔', '⾕', '⾖', '⾗', '⾘', '⾙', '⾚', '⾛', '⾜', '⾝', '⾞', '⾟', '⾠', '⾡', '⾢', '⾣', '⾤', '⾥', '⾦', '⾧', '⾨', '⾩', '⾪', '⾫', '⾬', '⾭', '⾮', '⾯', '⾰', '⾱', '⾲', '⾳', '⾴', '⾵', '⾶', '⾷', '⾸', '⾹', '⾺', '⾻', '⾼', '⾽', '⾾', '⾿', '⿀', '⿁', '⿂', '⿃', '⿄', '⿅', '⿆', '⿇', '⿈', '⿉', '⿊', '⿋', '⿌', '⿍', '⿎', '⿏', '⿐', '⿑', '⿒', '⿓', '⿔', '⿕'].freeze
107
+
108
+ Yijing = ['䷀', '䷁', '䷂', '䷃', '䷄', '䷅', '䷆', '䷇', '䷈', '䷉', '䷊', '䷋', '䷌', '䷍', '䷎', '䷏', '䷐', '䷑', '䷒', '䷓', '䷔', '䷕', '䷖', '䷗', '䷘', '䷙', '䷚', '䷛', '䷜', '䷝', '䷞', '䷟', '䷠', '䷡', '䷢', '䷣', '䷤', '䷥', '䷦', '䷧', '䷨', '䷩', '䷪', '䷫', '䷬', '䷭', '䷮', '䷯', '䷰', '䷱', '䷲', '䷳', '䷴', '䷵', '䷶', '䷷', '䷸', '䷹', '䷺', '䷻', '䷼', '䷽', '䷾', '䷿'].freeze
109
+
110
+ SAURASHTRA = ['ꢂ', 'ꢃ', 'ꢄ', 'ꢅ', 'ꢆ', 'ꢇ', 'ꢈ', 'ꢉ', 'ꢊ', 'ꢋ', 'ꢌ', 'ꢍ', 'ꢎ', 'ꢏ', 'ꢐ', 'ꢑ', 'ꢒ', 'ꢓ', 'ꢔ', 'ꢕ', 'ꢖ', 'ꢗ', 'ꢘ', 'ꢙ', 'ꢚ', 'ꢛ', 'ꢜ', 'ꢝ', 'ꢞ', 'ꢟ', 'ꢠ', 'ꢡ', 'ꢢ', 'ꢣ', 'ꢤ', 'ꢥ', 'ꢦ', 'ꢧ', 'ꢨ', 'ꢩ', 'ꢪ', 'ꢫ', 'ꢬ', 'ꢭ', 'ꢮ', 'ꢯ', 'ꢰ', 'ꢱ', 'ꢲ', 'ꢳ', '꣎', '꣏', '꣐', '꣑', '꣒', '꣓', '꣔', '꣕', '꣖', '꣗', '꣘', '꣙'].freeze
111
+
112
+ LINEAR_B_IDEOGRAMS = %w[𐁜 𐁝 𐂀 𐂁 𐂂 𐂃 𐂄 𐂅 𐂆 𐂇 𐂈 𐂉 𐂊 𐂋 𐂌 𐂍 𐂎 𐂏 𐂐 𐂑 𐂒 𐂓 𐂔 𐂕 𐂖 𐂗 𐂘 𐂙 𐂚 𐂛 𐂜 𐂝 𐂞 𐂟 𐂠 𐂡 𐂢 𐂣 𐂤 𐂥 𐂦 𐂧 𐂨 𐂩 𐂪 𐂫 𐂬 𐂭 𐂮 𐂯 𐂰 𐂱 𐂲 𐂳 𐂴 𐂵 𐂶 𐂷 𐂸 𐂹 𐂺 𐂻 𐂼 𐂽 𐂾 𐂿 𐃀 𐃁 𐃂 𐃃 𐃄 𐃅 𐃆 𐃇 𐃈 𐃉 𐃊 𐃋 𐃌 𐃍 𐃎 𐃏 𐃐 𐃑 𐃒 𐃓 𐃔 𐃕 𐃖 𐃗 𐃘 𐃙 𐃚 𐃛 𐃜 𐃝 𐃞 𐃟 𐃠 𐃡 𐃢 𐃣 𐃤 𐃥 𐃦 𐃧 𐃨 𐃩 𐃪 𐃫 𐃬 𐃭 𐃮 𐃯 𐃰 𐃱 𐃲 𐃳 𐃴 𐃵 𐃶 𐃷 𐃸 𐃹 𐃺].freeze
113
+
114
+ CUNEIFORM = %w[𒀀 𒀁 𒀂 𒀃 𒀄 𒀅 𒀆 𒀇 𒀈 𒀉 𒀊 𒀋 𒀌 𒀍 𒀎 𒀏 𒀐 𒀑 𒀒 𒀓 𒀔 𒀕 𒀖 𒀗 𒀘 𒀙 𒀚 𒀛 𒀜 𒀝 𒀞 𒀟 𒀠 𒀡 𒀢 𒀣 𒀤 𒀥 𒀦 𒀧 𒀨 𒀩 𒀪 𒀫 𒀬 𒀭 𒀮 𒀯 𒀰 𒀱 𒀲 𒀳 𒀴 𒀵 𒀶 𒀷 𒀸 𒀹 𒀺 𒀻 𒀼 𒀽 𒀾 𒀿 𒁀 𒁁 𒁂 𒁃 𒁄 𒁅 𒁆 𒁇 𒁈 𒁉 𒁊 𒁋 𒁌 𒁍 𒁎 𒁏 𒁐 𒁑 𒁒 𒁓 𒁔 𒁕 𒁖 𒁗 𒁘 𒁙 𒁚 𒁛 𒁜 𒁝 𒁞 𒁟 𒁠 𒁡 𒁢 𒁣 𒁤 𒁥 𒁦 𒁧 𒁨 𒁩 𒁪 𒁫 𒁬 𒁭 𒁮 𒁯 𒁰 𒁱 𒁲 𒁳 𒁴 𒁵 𒁶 𒁷 𒁸 𒁹 𒁺 𒁻 𒁼 𒁽 𒁾 𒁿 𒂀 𒂁 𒂂 𒂃 𒂄 𒂅 𒂆 𒂇 𒂈 𒂉 𒂊 𒂋 𒂌 𒂍 𒂎 𒂏 𒂐 𒂑 𒂒 𒂓 𒂔 𒂕 𒂖 𒂗 𒂘 𒂙 𒂚 𒂛 𒂜 𒂝 𒂞 𒂟 𒂠 𒂡 𒂢 𒂣 𒂤 𒂥 𒂦 𒂧 𒂨 𒂩 𒂪 𒂫 𒂬 𒂭 𒂮 𒂯 𒂰 𒂱 𒂲 𒂳 𒂴 𒂵 𒂶 𒂷 𒂸 𒂹 𒂺 𒂻 𒂼 𒂽 𒂾 𒂿 𒃀 𒃁 𒃂 𒃃 𒃄 𒃅 𒃆 𒃇 𒃈 𒃉 𒃊 𒃋 𒃌 𒃍 𒃎 𒃏 𒃐 𒃑 𒃒 𒃓 𒃔 𒃕 𒃖 𒃗 𒃘 𒃙 𒃚 𒃛 𒃜 𒃝 𒃞 𒃟 𒃠 𒃡 𒃢 𒃣 𒃤 𒃥 𒃦 𒃧 𒃨 𒃩 𒃪 𒃫 𒃬 𒃭 𒃮 𒃯 𒃰 𒃱 𒃲 𒃳 𒃴 𒃵 𒃶 𒃷 𒃸 𒃹 𒃺 𒃻 𒃼 𒃽 𒃾 𒃿 𒄀 𒄁 𒄂 𒄃 𒄄 𒄅 𒄆 𒄇 𒄈 𒄉 𒄊 𒄋 𒄌 𒄍 𒄎 𒄏 𒄐 𒄑 𒄒 𒄓 𒄔 𒄕 𒄖 𒄗 𒄘 𒄙 𒄚 𒄛 𒄜 𒄝 𒄞 𒄟 𒄠 𒄡 𒄢 𒄣 𒄤 𒄥 𒄦 𒄧 𒄨 𒄩 𒄪 𒄫 𒄬 𒄭 𒄮 𒄯 𒄰 𒄱 𒄲 𒄳 𒄴 𒄵 𒄶 𒄷 𒄸 𒄹 𒄺 𒄻 𒄼 𒄽 𒄾 𒄿 𒅀 𒅁 𒅂 𒅃 𒅄 𒅅 𒅆 𒅇 𒅈 𒅉 𒅊 𒅋 𒅌 𒅍 𒅎 𒅏 𒅐 𒅑 𒅒 𒅓 𒅔 𒅕 𒅖 𒅗 𒅘 𒅙 𒅚 𒅛 𒅜 𒅝 𒅞 𒅟 𒅠 𒅡 𒅢 𒅣 𒅤 𒅥 𒅦 𒅧 𒅨 𒅩 𒅪 𒅫 𒅬 𒅭 𒅮 𒅯 𒅰 𒅱 𒅲 𒅳 𒅴 𒅵 𒅶 𒅷 𒅸 𒅹 𒅺 𒅻 𒅼 𒅽 𒅾 𒅿 𒆀 𒆁 𒆂 𒆃 𒆄 𒆅 𒆆 𒆇 𒆈 𒆉 𒆊 𒆋 𒆌 𒆍 𒆎 𒆏 𒆐 𒆑 𒆒 𒆓 𒆔 𒆕 𒆖 𒆗 𒆘 𒆙 𒆚 𒆛 𒆜 𒆝 𒆞 𒆟 𒆠 𒆡 𒆢 𒆣 𒆤 𒆥 𒆦 𒆧 𒆨 𒆩 𒆪 𒆫 𒆬 𒆭 𒆮 𒆯 𒆰 𒆱 𒆲 𒆳 𒆴 𒆵 𒆶 𒆷 𒆸 𒆹 𒆺 𒆻 𒆼 𒆽 𒆾 𒆿 𒇀 𒇁 𒇂 𒇃 𒇄 𒇅 𒇆 𒇇 𒇈 𒇉 𒇊 𒇋 𒇌 𒇍 𒇎 𒇏 𒇐 𒇑 𒇒 𒇓 𒇔 𒇕 𒇖 𒇗 𒇘 𒇙 𒇚 𒇛 𒇜 𒇝 𒇞 𒇟 𒇠 𒇡 𒇢 𒇣 𒇤 𒇥 𒇦 𒇧 𒇨 𒇩 𒇪 𒇫 𒇬 𒇭 𒇮 𒇯 𒇰 𒇱 𒇲 𒇳 𒇴 𒇵 𒇶 𒇷 𒇸 𒇹 𒇺 𒇻 𒇼 𒇽 𒇾 𒇿 𒈀 𒈁 𒈂 𒈃 𒈄 𒈅 𒈆 𒈇 𒈈 𒈉 𒈊 𒈋 𒈌 𒈍 𒈎 𒈏 𒈐 𒈑 𒈒 𒈓 𒈔 𒈕 𒈖 𒈗 𒈘 𒈙 𒈚 𒈛 𒈜 𒈝 𒈞 𒈟 𒈠 𒈡 𒈢 𒈣 𒈤 𒈥 𒈦 𒈧 𒈨 𒈩 𒈪 𒈫 𒈬 𒈭 𒈮 𒈯 𒈰 𒈱 𒈲 𒈳 𒈴 𒈵 𒈶 𒈷 𒈸 𒈹 𒈺 𒈻 𒈼 𒈽 𒈾 𒈿 𒉀 𒉁 𒉂 𒉃 𒉄 𒉅 𒉆 𒉇 𒉈 𒉉 𒉊 𒉋 𒉌 𒉍 𒉎 𒉏 𒉐 𒉑 𒉒 𒉓 𒉔 𒉕 𒉖 𒉗 𒉘 𒉙 𒉚 𒉛 𒉜 𒉝 𒉞 𒉟 𒉠 𒉡 𒉢 𒉣 𒉤 𒉥 𒉦 𒉧 𒉨 𒉩 𒉪 𒉫 𒉬 𒉭 𒉮 𒉯 𒉰 𒉱 𒉲 𒉳 𒉴 𒉵 𒉶 𒉷 𒉸 𒉹 𒉺 𒉻 𒉼 𒉽 𒉾 𒉿 𒊀 𒊁 𒊂 𒊃 𒊄 𒊅 𒊆 𒊇 𒊈 𒊉 𒊊 𒊋 𒊌 𒊍 𒊎 𒊏 𒊐 𒊑 𒊒 𒊓 𒊔 𒊕 𒊖 𒊗 𒊘 𒊙 𒊚 𒊛 𒊜 𒊝 𒊞 𒊟 𒊠 𒊡 𒊢 𒊣 𒊤 𒊥 𒊦 𒊧 𒊨 𒊩 𒊪 𒊫 𒊬 𒊭 𒊮 𒊯 𒊰 𒊱 𒊲 𒊳 𒊴 𒊵 𒊶 𒊷 𒊸 𒊹 𒊺 𒊻 𒊼 𒊽 𒊾 𒊿 𒋀 𒋁 𒋂 𒋃 𒋄 𒋅 𒋆 𒋇 𒋈 𒋉 𒋊 𒋋 𒋌 𒋍 𒋎 𒋏 𒋐 𒋑 𒋒 𒋓 𒋔 𒋕 𒋖 𒋗 𒋘 𒋙 𒋚 𒋛 𒋜 𒋝 𒋞 𒋟 𒋠 𒋡 𒋢 𒋣 𒋤 𒋥 𒋦 𒋧 𒋨 𒋩 𒋪 𒋫 𒋬 𒋭 𒋮 𒋯 𒋰 𒋱 𒋲 𒋳 𒋴 𒋵 𒋶 𒋷 𒋸 𒋹 𒋺 𒋻 𒋼 𒋽 𒋾 𒋿 𒌀 𒌁 𒌂 𒌃 𒌄 𒌅 𒌆 𒌇 𒌈 𒌉 𒌊 𒌋 𒌌 𒌍 𒌎 𒌏 𒌐 𒌑 𒌒 𒌓 𒌔 𒌕 𒌖 𒌗 𒌘 𒌙 𒌚 𒌛 𒌜 𒌝 𒌞 𒌟 𒌠 𒌡 𒌢 𒌣 𒌤 𒌥 𒌦 𒌧 𒌨 𒌩 𒌪 𒌫 𒌬 𒌭 𒌮 𒌯 𒌰 𒌱 𒌲 𒌳 𒌴 𒌵 𒌶 𒌷 𒌸 𒌹 𒌺 𒌻 𒌼 𒌽 𒌾 𒌿 𒍀 𒍁 𒍂 𒍃 𒍄 𒍅 𒍆 𒍇 𒍈 𒍉 𒍊 𒍋 𒍌 𒍍 𒍎 𒍏 𒍐 𒍑 𒍒 𒍓 𒍔 𒍕 𒍖 𒍗 𒍘 𒍙 𒍚 𒍛 𒍜 𒍝 𒍞 𒍟 𒍠 𒍡 𒍢 𒍣 𒍤 𒍥 𒍦 𒍧 𒍨 𒍩 𒍪 𒍫 𒍬 𒍭 𒍮 𒍯 𒍰 𒍱 𒍲 𒍳 𒍴 𒍵 𒍶 𒍷 𒍸 𒍹 𒍺 𒍻 𒍼 𒍽 𒍾 𒍿 𒎀 𒎁 𒎂 𒎃 𒎄 𒎅 𒎆 𒎇 𒎈 𒎉 𒎊 𒎋 𒎌 𒎍 𒎎 𒎏 𒎐 𒎑 𒎒 𒎓 𒎔 𒎕 𒎖 𒎗 𒎘 𒎙].freeze
data/src/log.rb ADDED
@@ -0,0 +1,24 @@
1
+ # Adds logging to file and screen capabilities
2
+ module Log
3
+ def log_file=(file)
4
+ @file = file
5
+ end
6
+
7
+ def log(*args)
8
+ file = @file || 'tmp_log.txt'
9
+ s = args.nil? ? 'nil' : args.to_s
10
+ s = "------\n#{s}\n"
11
+ File.open(file, 'a') { |f| f.puts s }
12
+ end
13
+ end
14
+
15
+ # default logger class
16
+ class DefaultLogger
17
+ include Log
18
+ end
19
+
20
+ DEFAULT_LOGGER = DefaultLogger.new
21
+
22
+ def log(*args)
23
+ DEFAULT_LOGGER.log *args
24
+ end
data/src/node.rb ADDED
@@ -0,0 +1,117 @@
1
+ require_relative 'util'
2
+ require_relative 'node_attributes'
3
+ require_relative 'node_visit'
4
+ require_relative 'emitter'
5
+
6
+ module TermGui
7
+ # analog to HTML DOM Node class
8
+ # Ways of declaring node hierarchies by using parent and children props, or append_child or append_to methods. For declarative complex structures probably you want to use children
9
+ # `main = Row.new(parent: screen, height: 0.5, children: [Button.new(text: 'clickme', Col.new(width: 0.8, x: 0.2, children: [Label.new(text: 'hello')]))])`
10
+ # or
11
+ # `main = screen.append_child(Row.new(height: 0.5, children: [Button.new(text: 'clickme', Col.new(width: 0.8, x: 0.2, children: [Label.new(text: 'hello')]))]))`
12
+ # or
13
+ # `main = Row.new(height: 0.5, children: [Button.new(text: 'clickme', Col.new(width: 0.8, x: 0.2, children: [Label.new(text: 'hello')]))]).append_to(screen)`
14
+ class Node < Emitter
15
+ include NodeVisit
16
+
17
+ attr_reader :children, :text, :parent, :name
18
+ attr_writer :parent, :text
19
+
20
+ def initialize(**args)
21
+ @name = args[:name] || 'node'
22
+ @attributes = Attributes.new args[:attributes] || {}
23
+ @children = args[:children] || []
24
+ children.each { |child| child.parent = self }
25
+ @text = args[:text] || ''
26
+ args[:parent]&.append_children(self)
27
+ install(%i[after_render before_render])
28
+ end
29
+
30
+ # returns child so we can write: `button = screen.append_child Row.new(text: 'click me')`
31
+ def append_children(*children)
32
+ children.each do |child|
33
+ @children.push(child)
34
+ child.parent = self
35
+ end
36
+ children
37
+ end
38
+
39
+ def append_child(child)
40
+ (append_children child)[0]
41
+ end
42
+
43
+ def insert_children(index = 0, *children)
44
+ @children.insert(index, *children)
45
+ children
46
+ end
47
+
48
+ def prepend_child(child)
49
+ insert_child(0, child)
50
+ end
51
+
52
+ def insert_child(index, child)
53
+ (insert_children index, child)[0]
54
+ end
55
+
56
+ def remove_children(*children)
57
+ children.each do |child|
58
+ @children.delete(child)
59
+ child.parent = self
60
+ end
61
+ children
62
+ end
63
+
64
+ def remove_child(child)
65
+ (remove_children child)[0]
66
+ end
67
+
68
+ def append_to(parent)
69
+ parent.append_child(self)
70
+ self
71
+ end
72
+
73
+ def remove
74
+ parent&.remove_child(self)
75
+ self.parent = nil
76
+ end
77
+
78
+ def empty
79
+ children.each do |child|
80
+ child.parent = nil
81
+ end
82
+ @children = []
83
+ self
84
+ end
85
+
86
+ def attributes(attrs = nil)
87
+ attrs&.each_key { |key| set_attribute(key.to_s, attrs[key]) }
88
+ @attributes
89
+ end
90
+
91
+ def attributes=(attrs = nil)
92
+ attributes(attrs)
93
+ end
94
+
95
+ def set_attribute(name, value)
96
+ @attributes.set_attribute(name, value)
97
+ self
98
+ end
99
+
100
+ def get_attribute(name)
101
+ @attributes.get_attribute(name)
102
+ end
103
+
104
+ def to_s
105
+ "Node(name: #{name}, children: [#{children.map(&:to_s).join(', ')}])"
106
+ end
107
+
108
+ def pretty_print(d = 0)
109
+ "#{(' ' * d)}<#{name} #{(attributes.pairs.map { |p| "#{p[:name]}=#{pretty_print_attribute p[:value]}" }).join(' ')}>\n#{' ' * (d + 1)}#{text ? " #{text}\n#{' ' * d}" : ''}#{children.map { |c| c.pretty_print d + 1 }.join("\n" + (' ' * d))}\n#{(' ' * (d + 1))}</#{name}>"
110
+ end
111
+
112
+ def pretty_print_attribute(a)
113
+ a.respond_to?(:pretty_print) ? a.pretty_print : a.to_s
114
+ end
115
+ end
116
+ end
117
+ Node = TermGui::Node
@@ -0,0 +1,27 @@
1
+ # Manages Node's attributes
2
+ class Attributes
3
+ def initialize(attrs = {})
4
+ @attrs = attrs
5
+ end
6
+
7
+ def names
8
+ @attrs.keys
9
+ end
10
+
11
+ def pairs
12
+ @attrs.keys.map { |n| { name: n, value: @attrs[n] } }
13
+ end
14
+
15
+ def set_attribute(name, value)
16
+ @attrs[name.to_sym] = value
17
+ self
18
+ end
19
+
20
+ def get_attribute(name)
21
+ @attrs[name.to_sym]
22
+ end
23
+
24
+ def to_s
25
+ @attrs.to_s
26
+ end
27
+ end
data/src/node_visit.rb ADDED
@@ -0,0 +1,52 @@
1
+ # adds node recirsive visit support and node query operations
2
+ module NodeVisit
3
+ def query_by_attribute(attr, value)
4
+ result = []
5
+ visit_node(self, proc { |n|
6
+ result.push n if n.attributes.get_attribute(attr) == value
7
+ false
8
+ })
9
+ result
10
+ end
11
+
12
+ def query_by_name(name)
13
+ result = []
14
+ visit_node(self, proc { |n|
15
+ result.push n if n.name == name
16
+ false
17
+ })
18
+ result
19
+ end
20
+
21
+ def query_one_by_attribute(attr, value)
22
+ result = nil
23
+ p = proc do |n|
24
+ if n.attributes.get_attribute(attr) == value
25
+ result = n
26
+ true
27
+ else
28
+ false
29
+ end
30
+ end
31
+ visit_node(self, p)
32
+ result
33
+ end
34
+
35
+ def visit(visitor, children_first = true)
36
+ visit_node(self, visitor, children_first)
37
+ end
38
+ end
39
+
40
+ # visit given node children bottom-up. If visitor returns truthy then visiting finishes
41
+ def visit_node(node, visitor, children_first = true)
42
+ result = nil
43
+ unless children_first
44
+ result = visitor.call node
45
+ return result if result
46
+ end
47
+ result = some(node.children, proc { |child| visit_node child, visitor, children_first })
48
+ return result if result
49
+
50
+ result = visitor.call node if children_first
51
+ result
52
+ end
data/src/renderer.rb ADDED
@@ -0,0 +1,119 @@
1
+ require_relative 'style'
2
+ require_relative 'key'
3
+ require_relative 'renderer_print'
4
+ require_relative 'renderer_cursor'
5
+ require_relative 'renderer_image'
6
+ require_relative 'renderer_draw'
7
+
8
+ module TermGui
9
+ # Responsible of (TODO: we should split Renderer into several delegate classes
10
+ # * build charsequences to render text on a position. these are directly write to $stdout by screen
11
+ # * maintain bitmap-like buffer of current screen state
12
+ # * manages current applied style
13
+ # TODO: add line, empty-rect and more drawing primitives
14
+ class Renderer
15
+ include RendererPrint
16
+ include RendererCursor
17
+ include RendererImage
18
+ include RendererDraw
19
+
20
+ attr_reader :width, :height, :buffer, :style
21
+ attr_writer :style, :no_buffer
22
+
23
+ def initialize(width = 80, height = 20, no_buffer = false)
24
+ @width = width
25
+ @height = height
26
+ @style = Style.new
27
+ @no_buffer = no_buffer
28
+ self.fast_colouring = true
29
+ unless no_buffer
30
+ @buffer = (0...@height).to_a.map do
31
+ (0...@width).to_a.map do
32
+ Pixel.new
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ # all writing must be done using me
39
+ def write(x, y, s, style = nil)
40
+ if y < @height && y >= 0
41
+ # TODO: x could be negative now, trunc ch to respect screen bounds - if not negative values are printed at the right
42
+ if x < 0
43
+ s = s[[x * -1, s.length].min..s.length]
44
+ x = 0
45
+ end
46
+ unless @no_buffer
47
+ (x...[x + s.length, @width].min).to_a.each do |i|
48
+ @buffer[y][i].ch = s[i - x]
49
+ @buffer[y][i].style = (style || @style).clone
50
+ end
51
+ end
52
+ # apply the style after writing to the buffer so it don't contains escape secuences just the chars
53
+ s = style == nil ? s : style.print(s)
54
+ "#{move x, y + 1}#{s}" # TODO: investigate why y + 1
55
+ else
56
+ ''
57
+ end
58
+ end
59
+
60
+ def move(x, y)
61
+ Renderer.move x, y
62
+ end
63
+
64
+ def self.move(x, y)
65
+ "#{CSI}#{y};#{x}H"
66
+ end
67
+
68
+ def text(x: 0, y: 0, text: ' ', style: nil)
69
+ write(x, y, text, style)
70
+ end
71
+
72
+ def rect(x: 0, y: 0, width: 5, height: 3, ch: Pixel.EMPTY_CH, style: nil)
73
+ s = []
74
+ ch = Pixel.EMPTY_CH if ch == nil
75
+ height.times do |y_|
76
+ s .push write(x, y + y_, ch * width, style).to_s
77
+ end
78
+ s.join('')
79
+ end
80
+
81
+ def clear
82
+ @style = Style.new
83
+ unless @no_buffer
84
+ @buffer.each_index do |y|
85
+ @buffer[y].each do |p|
86
+ p.ch = Pixel.EMPTY_CH
87
+ p.style.reset
88
+ end
89
+ end
90
+ end
91
+ "#{CSI}0m#{CSI}2J"
92
+ end
93
+
94
+ def style_assign(style)
95
+ @style.assign(style)
96
+ end
97
+
98
+ def fast_colouring=(value)
99
+ Style.fast_colouring(value)
100
+ end
101
+ end
102
+
103
+ # Represents a pixel in renderer's buffer
104
+ class Pixel
105
+ attr_accessor :ch, :style
106
+
107
+ def self.EMPTY_CH
108
+ ' '
109
+ end
110
+
111
+ def initialize(ch = Pixel.EMPTY_CH, style = Style.new)
112
+ @ch = ch
113
+ @style = style
114
+ end
115
+ end
116
+ end
117
+
118
+ Renderer = TermGui::Renderer
119
+ Pixel = TermGui::Pixel
@@ -0,0 +1,18 @@
1
+ # takes care of cursor related ansi escape sequences
2
+ module RendererCursor
3
+ def cursor_save
4
+ "#{CSI}s"
5
+ end
6
+
7
+ def cursor_restore
8
+ "#{CSI}u"
9
+ end
10
+
11
+ def cursor_show
12
+ "#{CSI}?25h"
13
+ end
14
+
15
+ def cursor_hide
16
+ "#{CSI}?25l"
17
+ end
18
+ end
@@ -0,0 +1,28 @@
1
+ require 'chunky_png'
2
+
3
+ # takes care of drawing shapes. Based on Image (uses chunky_png Canvas)
4
+ # TODO: we should use TermGui::Image and not ChunkyPNG's so we add value there too
5
+ module RendererDraw
6
+ def circle(x: nil, y: nil, radius: nil, stroke_ch: ' ', stroke: nil, fill: nil, fill_ch: stroke_ch)
7
+ canvas = draw_canvas
8
+ canvas.circle(x, y, radius, ChunkyPNG::Color::BLACK, ChunkyPNG::Color::WHITE)
9
+ radius = []
10
+ canvas.height.times do |y2|
11
+ canvas.width.times do |x2|
12
+ if canvas[x2, y2] == ChunkyPNG::Color::BLACK && stroke
13
+ radius.push text(x: x2, y: y2, text: stroke_ch, style: stroke)
14
+ elsif canvas[x2, y2] == ChunkyPNG::Color::WHITE && fill
15
+ radius.push text(x: x2, y: y2, text: fill_ch, style: fill)
16
+ end
17
+ end
18
+ end
19
+ radius.join
20
+ end
21
+
22
+ private
23
+
24
+ def draw_canvas
25
+ # TODO: reuse the canvas
26
+ ChunkyPNG::Canvas.new(width, height, ChunkyPNG::Color::TRANSPARENT)
27
+ end
28
+ end
@@ -0,0 +1,31 @@
1
+ require_relative 'image'
2
+
3
+ # takes care rendering given Images
4
+ module RendererImage
5
+ # renders given Image or file path at given x, y coords. Currently only PNG format supported.
6
+ # by default bg attribute is used and space is printed for each pixel but this could be configured using fg, bg, and ch
7
+ # ch can be an array of chars in which case a random one is taken for each pixel
8
+ def image(x: 0, y: 0, image: nil, ch: ' ', style: Style.new, fg: false, bg: true, h: height - y, w: width - x,
9
+ transparent_color: nil) # if a [r,g,b] color is given, then alpha channel will be considered to mix colors accordingly
10
+ output = []
11
+ image = image.is_a?(String) ? TermGui::Image.new(image) : image
12
+ (y..[y + image.height - 1, height - 1].min).to_a.each do |y2|
13
+ (x..[x + image.width - 1, width - 1].min).to_a.each do |x2|
14
+ pixel = image.rgb(x2 - x, y2 - y, transparent_color)
15
+ style.bg = pixel if bg
16
+ style.fg = pixel if fg
17
+ output .push text(x: x2, y: y2, text: ch.is_a?(Array) ? ch.sample : ch, style: style) if x2 < w && y2 < h
18
+ end
19
+ end
20
+ output.join('')
21
+ end
22
+
23
+ private
24
+
25
+ def mix_colors(fg, bg, alpha = 0.5)
26
+ r = (fg[0] * alpha + bg[0] * (1 - alpha)).to_i
27
+ g = (fg[1] * alpha + bg[1] * (1 - alpha)).to_i
28
+ b = (fg[2] * alpha + bg[2] * (1 - alpha)).to_i
29
+ [r, g, b]
30
+ end
31
+ end