rcurses 4.8.3 → 4.9.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6368e4d70cf29ddd6652b9112bdff1ca4912c436e6a731fb40f8f2e088f9f11a
4
- data.tar.gz: 05cb89f361374c80212eea238028e071e898060de1e3432f1a3a72cc5ded1821
3
+ metadata.gz: 998bc4ac36e7e3b51fc910a59bf1ffdf933870a8975ef205b572d4f3c1fe377a
4
+ data.tar.gz: 7bd9a0993fda8a06545969b7a4bea36f80e782e7dcefd477d11389f6764c0dff
5
5
  SHA512:
6
- metadata.gz: c774f36556d7fe4abfd514a8c04ba1cb56b6e19cc0a00e1d460c24c2490ca1a005b58db330f03a1a3db9cf9d8994fe76a37f61369ee74b3a2ba70a8ca54c6c9f
7
- data.tar.gz: 5f202003dd8e302b4c03b72fcf3366573d7f336418d8b4eec5516d83c88c6c44fb8e82b8c0f9267afb0deb3c843de2a74d7faac2f63139de6c509eb2bf77eb2f
6
+ metadata.gz: 99f1957fafce2eaaa495c7fa9230e5724643ec82e3f772420846ce1ead3d461d0682dc9df57066bc24286f32f63861a631e370d00a8ff805bac033e6ad81a94d
7
+ data.tar.gz: b4a41c371b9e8555b4f8f76fb6a45c5b50a43a7ada8e992aba9340f22ba6f84ee48513fce45294291eb0396b86b8540cc10eda37a9bd93aeb3fff1e3e79141cf
@@ -8,15 +8,20 @@ module Rcurses
8
8
  def restore; print(Gem.win_platform? ? CSI + 'u' : ESC + '8'); end # Restore cursor position
9
9
  def pos # Query cursor current position
10
10
  res = ''
11
- $stdin.raw do |stdin|
12
- $stdout << CSI + '6n' # The actual ANSI get-position
13
- $stdout.flush
14
- while (c = stdin.getc) != 'R'
15
- res << c if c
11
+ begin
12
+ $stdin.raw do |stdin|
13
+ $stdout << CSI + '6n' # The actual ANSI get-position
14
+ $stdout.flush
15
+ while (c = stdin.getc) != 'R'
16
+ res << c if c
17
+ end
16
18
  end
19
+ m = res.match(/(?<row>\d+);(?<col>\d+)/)
20
+ return m[:row].to_i, m[:col].to_i
21
+ rescue Errno::EIO, Errno::ENOTTY, IOError
22
+ # Return safe defaults if terminal state is compromised
23
+ return 1, 1
17
24
  end
18
- m = res.match(/(?<row>\d+);(?<col>\d+)/)
19
- return m[:row].to_i, m[:col].to_i
20
25
  end
21
26
  def rowget
22
27
  row, _col = pos
data/lib/rcurses/input.rb CHANGED
@@ -7,6 +7,9 @@ module Rcurses
7
7
  c = t ? Timeout.timeout(t) { $stdin.getch } : $stdin.getch
8
8
  rescue Timeout::Error
9
9
  return nil
10
+ rescue Errno::EIO, Errno::ENOTTY, IOError => e
11
+ # Handle terminal focus loss or disconnection gracefully
12
+ return "ESC"
10
13
  end
11
14
 
12
15
  # 2) If it's ESC, grab any quick trailing bytes
@@ -15,7 +18,8 @@ module Rcurses
15
18
  if IO.select([$stdin], nil, nil, 0.05)
16
19
  begin
17
20
  seq << $stdin.read_nonblock(16)
18
- rescue IO::WaitReadable, EOFError
21
+ rescue IO::WaitReadable, EOFError, Errno::EIO, Errno::ENOTTY
22
+ # Handle various terminal state errors gracefully
19
23
  end
20
24
  end
21
25
  end
@@ -115,7 +119,8 @@ module Rcurses
115
119
  while IO.select([$stdin], nil, nil, 0)
116
120
  begin
117
121
  $stdin.read_nonblock(4096)
118
- rescue IO::WaitReadable, EOFError
122
+ rescue IO::WaitReadable, EOFError, Errno::EIO, Errno::ENOTTY
123
+ # Handle terminal state errors during flush
119
124
  break
120
125
  end
121
126
  end
data/lib/rcurses/pane.rb CHANGED
@@ -1,25 +1,68 @@
1
1
  module Rcurses
2
- # A simple display_width function that approximates how many columns a string occupies.
3
- # This is a simplified version that may need adjustments for full Unicode support.
2
+ # Enhanced display_width function with better Unicode support
4
3
  def self.display_width(str)
5
4
  width = 0
6
5
  str.each_char do |char|
7
6
  cp = char.ord
7
+
8
+ # Handle NUL and control characters
8
9
  if cp == 0
9
10
  # NUL – no width
11
+ next
10
12
  elsif cp < 32 || (cp >= 0x7F && cp < 0xA0)
11
13
  # Control characters: no width
12
- width += 0
13
- # Approximate common wide ranges:
14
- elsif (cp >= 0x1100 && cp <= 0x115F) ||
15
- cp == 0x2329 || cp == 0x232A ||
16
- (cp >= 0x2E80 && cp <= 0xA4CF) ||
17
- (cp >= 0xAC00 && cp <= 0xD7A3) ||
18
- (cp >= 0xF900 && cp <= 0xFAFF) ||
19
- (cp >= 0xFE10 && cp <= 0xFE19) ||
20
- (cp >= 0xFE30 && cp <= 0xFE6F) ||
21
- (cp >= 0xFF00 && cp <= 0xFF60) ||
22
- (cp >= 0xFFE0 && cp <= 0xFFE6)
14
+ next
15
+ end
16
+
17
+ # Handle combining characters (zero width)
18
+ if (cp >= 0x0300 && cp <= 0x036F) || # Combining Diacritical Marks
19
+ (cp >= 0x1AB0 && cp <= 0x1AFF) || # Combining Diacritical Marks Extended
20
+ (cp >= 0x1DC0 && cp <= 0x1DFF) || # Combining Diacritical Marks Supplement
21
+ (cp >= 0x20D0 && cp <= 0x20FF) || # Combining Diacritical Marks for Symbols
22
+ (cp >= 0xFE20 && cp <= 0xFE2F) # Combining Half Marks
23
+ next
24
+ end
25
+
26
+ # Handle wide characters (East Asian width)
27
+ if (cp >= 0x1100 && cp <= 0x115F) || # Hangul Jamo
28
+ (cp >= 0x2329 && cp <= 0x232A) || # Left/Right-Pointing Angle Bracket
29
+ (cp >= 0x2E80 && cp <= 0x2EFF) || # CJK Radicals Supplement
30
+ (cp >= 0x2F00 && cp <= 0x2FDF) || # Kangxi Radicals
31
+ (cp >= 0x2FF0 && cp <= 0x2FFF) || # Ideographic Description Characters
32
+ (cp >= 0x3000 && cp <= 0x303E) || # CJK Symbols and Punctuation
33
+ (cp >= 0x3041 && cp <= 0x3096) || # Hiragana
34
+ (cp >= 0x30A1 && cp <= 0x30FA) || # Katakana
35
+ (cp >= 0x3105 && cp <= 0x312D) || # Bopomofo
36
+ (cp >= 0x3131 && cp <= 0x318E) || # Hangul Compatibility Jamo
37
+ (cp >= 0x3190 && cp <= 0x31BA) || # Kanbun
38
+ (cp >= 0x31C0 && cp <= 0x31E3) || # CJK Strokes
39
+ (cp >= 0x31F0 && cp <= 0x31FF) || # Katakana Phonetic Extensions
40
+ (cp >= 0x3200 && cp <= 0x32FF) || # Enclosed CJK Letters and Months
41
+ (cp >= 0x3300 && cp <= 0x33FF) || # CJK Compatibility
42
+ (cp >= 0x3400 && cp <= 0x4DBF) || # CJK Unified Ideographs Extension A
43
+ (cp >= 0x4E00 && cp <= 0x9FFF) || # CJK Unified Ideographs
44
+ (cp >= 0xA960 && cp <= 0xA97F) || # Hangul Jamo Extended-A
45
+ (cp >= 0xAC00 && cp <= 0xD7A3) || # Hangul Syllables
46
+ (cp >= 0xD7B0 && cp <= 0xD7FF) || # Hangul Jamo Extended-B
47
+ (cp >= 0xF900 && cp <= 0xFAFF) || # CJK Compatibility Ideographs
48
+ (cp >= 0xFE10 && cp <= 0xFE19) || # Vertical Forms
49
+ (cp >= 0xFE30 && cp <= 0xFE6F) || # CJK Compatibility Forms
50
+ (cp >= 0xFF00 && cp <= 0xFF60) || # Fullwidth Forms
51
+ (cp >= 0xFFE0 && cp <= 0xFFE6) || # Fullwidth Forms
52
+ (cp >= 0x1F000 && cp <= 0x1F02F) || # Mahjong Tiles
53
+ (cp >= 0x1F030 && cp <= 0x1F09F) || # Domino Tiles
54
+ (cp >= 0x1F100 && cp <= 0x1F1FF) || # Enclosed Alphanumeric Supplement
55
+ (cp >= 0x1F200 && cp <= 0x1F2FF) || # Enclosed Ideographic Supplement
56
+ (cp >= 0x1F300 && cp <= 0x1F5FF) || # Miscellaneous Symbols and Pictographs
57
+ (cp >= 0x1F600 && cp <= 0x1F64F) || # Emoticons
58
+ (cp >= 0x1F650 && cp <= 0x1F67F) || # Ornamental Dingbats
59
+ (cp >= 0x1F680 && cp <= 0x1F6FF) || # Transport and Map Symbols
60
+ (cp >= 0x1F700 && cp <= 0x1F77F) || # Alchemical Symbols
61
+ (cp >= 0x1F780 && cp <= 0x1F7FF) || # Geometric Shapes Extended
62
+ (cp >= 0x1F800 && cp <= 0x1F8FF) || # Supplemental Arrows-C
63
+ (cp >= 0x1F900 && cp <= 0x1F9FF) || # Supplemental Symbols and Pictographs
64
+ (cp >= 0x20000 && cp <= 0x2FFFF) || # CJK Unified Ideographs Extension B-F
65
+ (cp >= 0x30000 && cp <= 0x3FFFF) # CJK Unified Ideographs Extension G
23
66
  width += 2
24
67
  else
25
68
  width += 1
@@ -32,13 +75,19 @@ module Rcurses
32
75
  require 'clipboard' # Ensure the 'clipboard' gem is installed
33
76
  include Cursor
34
77
  include Input
78
+
79
+ # Compiled regex patterns for performance
80
+ ANSI_REGEX = /\e\[[0-9;]*m/.freeze
81
+ SGR_REGEX = /\e\[\d+(?:;\d+)*m/.freeze
35
82
  attr_accessor :x, :y, :w, :h, :fg, :bg
36
83
  attr_accessor :border, :scroll, :text, :ix, :index, :align, :prompt
37
84
  attr_accessor :moreup, :moredown
38
85
  attr_accessor :record, :history
86
+ attr_accessor :updates_suspended
39
87
 
40
88
  def initialize(x = 1, y = 1, w = 1, h = 1, fg = nil, bg = nil)
41
- @max_h, @max_w = IO.console.winsize
89
+ @terminal_size_cache = nil
90
+ @terminal_size_time = nil
42
91
  @x = x
43
92
  @y = y
44
93
  @w = w
@@ -54,6 +103,8 @@ module Rcurses
54
103
  @pos = 0 # For cursor tracking during editing:
55
104
  @record = false # Don't record history unless explicitly set to true
56
105
  @history = [] # History array
106
+ @updates_suspended = false
107
+ @lazy_cache_limit = 1000 # Limit for lazy text cache
57
108
  end
58
109
 
59
110
  def text=(new_text)
@@ -82,6 +133,24 @@ module Rcurses
82
133
  full_refresh
83
134
  end
84
135
 
136
+ def suspend_updates
137
+ @updates_suspended = true
138
+ end
139
+
140
+ def resume_updates
141
+ @updates_suspended = false
142
+ refresh
143
+ end
144
+
145
+ def get_terminal_size
146
+ now = Time.now
147
+ if @terminal_size_cache.nil? || @terminal_size_time.nil? || (now - @terminal_size_time) > 0.5
148
+ @terminal_size_cache = IO.console.winsize
149
+ @terminal_size_time = now
150
+ end
151
+ @terminal_size_cache
152
+ end
153
+
85
154
  def move(dx, dy)
86
155
  @x += dx
87
156
  @y += dy
@@ -90,7 +159,8 @@ module Rcurses
90
159
 
91
160
  def linedown
92
161
  @ix += 1
93
- @ix = @text.split("\n").length if @ix > @text.split("\n").length - 1
162
+ text_lines = @text.split("\n")
163
+ @ix = text_lines.length if @ix > text_lines.length - 1
94
164
  refresh
95
165
  end
96
166
 
@@ -102,7 +172,8 @@ module Rcurses
102
172
 
103
173
  def pagedown
104
174
  @ix = @ix + @h - 1
105
- @ix = @text.split("\n").length - @h if @ix > @text.split("\n").length - @h
175
+ text_lines = @text.split("\n")
176
+ @ix = text_lines.length - @h if @ix > text_lines.length - @h
106
177
  refresh
107
178
  end
108
179
 
@@ -113,7 +184,8 @@ module Rcurses
113
184
  end
114
185
 
115
186
  def bottom
116
- @ix = @text.split("\n").length - @h
187
+ text_lines = @text.split("\n")
188
+ @ix = text_lines.length - @h
117
189
  refresh
118
190
  end
119
191
 
@@ -160,7 +232,15 @@ module Rcurses
160
232
  # Diff-based refresh that minimizes flicker.
161
233
  # In this updated version we lazily process only the raw lines required to fill the pane.
162
234
  def refresh(cont = @text)
163
- @max_h, @max_w = IO.console.winsize
235
+ return if @updates_suspended
236
+
237
+ # Check if we're in batch mode and suspend updates accordingly
238
+ if Rcurses.batch_mode?
239
+ Rcurses.add_to_batch(self)
240
+ return
241
+ end
242
+
243
+ @max_h, @max_w = get_terminal_size
164
244
 
165
245
  if @border
166
246
  @w = @max_w - 2 if @w > @max_w - 2
@@ -184,11 +264,17 @@ module Rcurses
184
264
 
185
265
  # Lazy evaluation: If the content or pane width has changed, reinitialize the lazy cache.
186
266
  if !defined?(@cached_text) || @cached_text != cont || @cached_w != @w
187
- @raw_txt = cont.split("\n").map { |line| line.chomp("\r") }
267
+ @raw_txt = cont.split("\n")
188
268
  @lazy_txt = [] # This will hold the processed (wrapped) lines as needed.
189
269
  @lazy_index = 0 # Pointer to the next raw line to process.
190
270
  @cached_text = cont.dup
191
271
  @cached_w = @w
272
+
273
+ # Clear cache if it gets too large to prevent memory leaks
274
+ if @lazy_txt.size > @lazy_cache_limit
275
+ @lazy_txt.clear
276
+ @lazy_index = 0
277
+ end
192
278
  end
193
279
 
194
280
  content_rows = @h
@@ -221,11 +307,11 @@ module Rcurses
221
307
  hl = pl / 2
222
308
  case @align
223
309
  when "l"
224
- line_str = @txt[l].c(fmt) + " ".c(fmt) * pl
310
+ line_str = @txt[l].pure.c(fmt) + " ".c(fmt) * pl
225
311
  when "r"
226
- line_str = " ".c(fmt) * pl + @txt[l].c(fmt)
312
+ line_str = " ".c(fmt) * pl + @txt[l].pure.c(fmt)
227
313
  when "c"
228
- line_str = " ".c(fmt) * hl + @txt[l].c(fmt) + " ".c(fmt) * (pl - hl)
314
+ line_str = " ".c(fmt) * hl + @txt[l].pure.c(fmt) + " ".c(fmt) * (pl - hl)
229
315
  end
230
316
  else
231
317
  line_str = " ".c(fmt) * @w
@@ -605,14 +691,13 @@ module Rcurses
605
691
  "\e[7m" => "\e[27m"
606
692
  }
607
693
  close_sequences = open_sequences.values + ["\e[0m"]
608
- ansi_regex = /\e\[[0-9;]*m/
609
694
  result = []
610
695
  tokens = line.scan(/(\e\[[0-9;]*m|[^\e]+)/).flatten.compact
611
696
  current_line = ''
612
697
  current_line_length = 0
613
698
  active_sequences = []
614
699
  tokens.each do |token|
615
- if token.match?(ansi_regex)
700
+ if token.match?(ANSI_REGEX)
616
701
  current_line << token
617
702
  if close_sequences.include?(token)
618
703
  if token == "\e[0m"
@@ -627,7 +712,7 @@ module Rcurses
627
712
  else
628
713
  words = token.scan(/\s+|\S+/)
629
714
  words.each do |word|
630
- word_length = Rcurses.display_width(word.gsub(ansi_regex, ''))
715
+ word_length = Rcurses.display_width(word.gsub(ANSI_REGEX, ''))
631
716
  if current_line_length + word_length <= w
632
717
  current_line << word
633
718
  current_line_length += word_length
@@ -638,11 +723,12 @@ module Rcurses
638
723
  current_line_length = 0
639
724
  end
640
725
  while word_length > w
641
- part = word[0, w]
726
+ # Split safely respecting UTF-8 boundaries and display width
727
+ part = safe_substring_by_width(word, w)
642
728
  current_line << part
643
729
  result << current_line
644
- word = word[w..-1]
645
- word_length = Rcurses.display_width(word.gsub(ansi_regex, ''))
730
+ word = word[part.length..-1]
731
+ word_length = Rcurses.display_width(word.gsub(ANSI_REGEX, ''))
646
732
  current_line = active_sequences.join
647
733
  current_line_length = 0
648
734
  end
@@ -657,6 +743,23 @@ module Rcurses
657
743
  result << current_line unless current_line.empty?
658
744
  result
659
745
  end
746
+
747
+ # Helper method to safely split strings by display width while respecting UTF-8 boundaries
748
+ def safe_substring_by_width(str, max_width)
749
+ return str if Rcurses.display_width(str) <= max_width
750
+
751
+ result = ''
752
+ current_width = 0
753
+
754
+ str.each_char do |char|
755
+ char_width = Rcurses.display_width(char)
756
+ break if current_width + char_width > max_width
757
+ result += char
758
+ current_width += char_width
759
+ end
760
+
761
+ result
762
+ end
660
763
  end
661
764
  end
662
765
 
data/lib/rcurses.rb CHANGED
@@ -5,7 +5,7 @@
5
5
  # Web_site: http://isene.com/
6
6
  # Github: https://github.com/isene/rcurses
7
7
  # License: Public domain
8
- # Version: 4.8.3: Bugfix: Text carriege return corner case fix
8
+ # Version: 4.9.0: Major performance improvements - memory leak fixes, terminal dimension caching, batch updates, better Unicode support, enhanced error handling
9
9
 
10
10
  require 'io/console' # Basic gem for rcurses
11
11
  require 'io/wait' # stdin handling
@@ -52,6 +52,51 @@ module Rcurses
52
52
 
53
53
  @cleaned_up = true
54
54
  end
55
+
56
+ # Public: Batch multiple pane updates to prevent flickering
57
+ def batch_updates
58
+ @batched_panes = []
59
+ yield
60
+ @batched_panes.each(&:resume_updates)
61
+ @batched_panes = nil
62
+ end
63
+
64
+ # Internal: Track panes that need updating in batch mode
65
+ def add_to_batch(pane)
66
+ if @batched_panes
67
+ pane.suspend_updates unless @batched_panes.include?(pane)
68
+ @batched_panes << pane unless @batched_panes.include?(pane)
69
+ end
70
+ end
71
+
72
+ # Public: Check if we're in batch mode
73
+ def batch_mode?
74
+ !@batched_panes.nil?
75
+ end
76
+
77
+ # Content caching system for improved performance
78
+ def self.cache_get(key)
79
+ @content_cache ||= {}
80
+ @content_cache[key]
81
+ end
82
+
83
+ def self.cache_set(key, value)
84
+ @content_cache ||= {}
85
+ @content_cache_limit ||= 100
86
+
87
+ # Simple LRU eviction when cache gets too large
88
+ if @content_cache.size >= @content_cache_limit
89
+ # Remove oldest entries (simplified LRU)
90
+ keys_to_remove = @content_cache.keys.first(@content_cache.size - @content_cache_limit + 10)
91
+ keys_to_remove.each { |k| @content_cache.delete(k) }
92
+ end
93
+
94
+ @content_cache[key] = value
95
+ end
96
+
97
+ def self.cache_clear
98
+ @content_cache = {}
99
+ end
55
100
  end
56
101
 
57
102
  # Kick off initialization as soon as the library is required.
@@ -1,6 +1,9 @@
1
1
  # string_extensions.rb
2
2
 
3
3
  class String
4
+ # Compiled regex patterns for performance
5
+ ANSI_SGR_REGEX = /\e\[\d+(?:;\d+)*m/.freeze
6
+ ANSI_SEQUENCE_REGEX = /\e\[\d+(?:;\d+)*m/.freeze
4
7
  # 256-color or truecolor RGB foregroundbreset only the fg (SGR 39)
5
8
  def fg(color)
6
9
  sp, ep = if color.to_s =~ /\A[0-9A-Fa-f]{6}\z/
@@ -93,12 +96,20 @@ class String
93
96
 
94
97
  # Strip all ANSI SGR sequences
95
98
  def pure
96
- gsub(/\e\[\d+(?:;\d+)*m/, '')
99
+ gsub(ANSI_SGR_REGEX, '')
97
100
  end
98
101
 
99
102
  # Remove stray leading/trailing reset if the string has no other styling
100
103
  def clean_ansi
101
- gsub(/\A(?:\e\[0m)+/, '').gsub(/\e\[0m\z/, '')
104
+ # If we have opening ANSI codes without proper closing, just use pure
105
+ # to avoid unbalanced sequences that can corrupt terminal display
106
+ temp = gsub(/\A(?:\e\[0m)+/, '').gsub(/\e\[0m\z/, '')
107
+ # Check if we have unbalanced ANSI sequences (opening codes without closing)
108
+ if temp =~ /\e\[[\d;]+m/ && temp !~ /\e\[0m\z/
109
+ pure
110
+ else
111
+ temp
112
+ end
102
113
  end
103
114
 
104
115
  # Truncate the *visible* length to n, but preserve embedded ANSI
@@ -108,7 +119,7 @@ class String
108
119
  i = 0
109
120
 
110
121
  while i < length && count < n
111
- if self[i] == "\e" && (m = self[i..-1].match(/\A(\e\[\d+(?:;\d+)*m)/))
122
+ if self[i] == "\e" && (m = self[i..-1].match(/\A(#{ANSI_SEQUENCE_REGEX.source})/))
112
123
  out << m[1]
113
124
  i += m[1].length
114
125
  else
@@ -131,7 +142,7 @@ class String
131
142
  count, out, i, injected = 0, '', 0, false
132
143
 
133
144
  while i < length
134
- if self[i] == "\e" && (m = self[i..-1].match(/\A(\e\[\d+(?:;\d+)*m)/))
145
+ if self[i] == "\e" && (m = self[i..-1].match(/\A(#{ANSI_SEQUENCE_REGEX.source})/))
135
146
  out << m[1]
136
147
  i += m[1].length
137
148
  else
@@ -146,7 +157,7 @@ class String
146
157
  end
147
158
 
148
159
  unless injected
149
- if out =~ /(\e\[\d+(?:;\d+)*m)\z/
160
+ if out =~ /(#{ANSI_SEQUENCE_REGEX.source})\z/
150
161
  trailing = $1
151
162
  out = out[0...-trailing.length] + insertion + trailing
152
163
  else
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rcurses
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.8.3
4
+ version: 4.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Geir Isene
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-06-17 00:00:00.000000000 Z
11
+ date: 2025-07-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: clipboard
@@ -29,9 +29,9 @@ description: 'Create curses applications for the terminal easier than ever. Crea
29
29
  up text (in panes or anywhere in the terminal) in bold, italic, underline, reverse
30
30
  color, blink and in any 256 terminal colors for foreground and background. Use a
31
31
  simple editor to let users edit text in panes. Left, right or center align text
32
- in panes. Cursor movement around the terminal. New in 3.8: Fixed border fragments
33
- upon utf-8 characters. 4.8: Bugfix: Removed stray ansi codes. 4.8.3: Bugfix: Text
34
- carriege return corner case fix.'
32
+ in panes. Cursor movement around the terminal. 4.9.0: Major performance improvements
33
+ - memory leak fixes, terminal dimension caching, batch updates, better Unicode support,
34
+ enhanced error handling.'
35
35
  email: g@isene.com
36
36
  executables: []
37
37
  extensions: []