Module: Rufio::TextUtils

Included in:
DialogRenderer
Defined in:
lib/rufio/text_utils.rb

Overview

Text utility methods for display width calculation and string manipulation Handles multi-byte characters (Japanese, etc.) correctly

Constant Summary collapse

FULLWIDTH_CHAR_WIDTH =

Character width constants

2
HALFWIDTH_CHAR_WIDTH =
1
MULTIBYTE_THRESHOLD =
1
ELLIPSIS_MIN_WIDTH =

Truncation constants

3
ELLIPSIS =
'...'
BREAK_POINT_THRESHOLD =

Line break constants

0.5

Class Method Summary collapse

Class Method Details

.char_width(char) ⇒ Object

Calculate width for a single character with caching



25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/rufio/text_utils.rb', line 25

def char_width(char)
  @char_width_cache[char] ||= begin
    case char
    when /[\u2500-\u257F]/
      HALFWIDTH_CHAR_WIDTH  # Box Drawing characters (罫線文字) - ターミナルでは幅1
    when /[\u3000-\u303F\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF\uFF00-\uFFEF\u2580-\u259F]/
      FULLWIDTH_CHAR_WIDTH  # Japanese characters (hiragana, katakana, kanji, full-width symbols, block elements)
    when /[\u0020-\u007E]/
      HALFWIDTH_CHAR_WIDTH  # ASCII characters
    else
      char.bytesize > MULTIBYTE_THRESHOLD ? FULLWIDTH_CHAR_WIDTH : HALFWIDTH_CHAR_WIDTH
    end
  end
end

.display_width(string) ⇒ Object

Calculate display width of a string Full-width characters (Japanese, etc.) count as 2, half-width as 1



42
43
44
45
46
47
48
# File 'lib/rufio/text_utils.rb', line 42

def display_width(string)
  width = 0
  string.each_char do |char|
    width += char_width(char)
  end
  width
end

.find_break_point(line, max_width) ⇒ Object

Find the best break point for wrapping text within max_width



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/rufio/text_utils.rb', line 97

def find_break_point(line, max_width)
  return line.length if display_width(line) <= max_width

  current_width = 0
  best_break_point = 0
  space_break_point = nil
  punct_break_point = nil

  line.each_char.with_index do |char, index|
    cw = char_width(char)
    break if current_width + cw > max_width

    current_width += cw
    best_break_point = index + 1

    # Record break point at space
    space_break_point = index + 1 if char == ' ' && current_width > max_width * BREAK_POINT_THRESHOLD

    # Record break point at Japanese punctuation
    punct_break_point = index + 1 if char.match?(/[、。,.!?]/) && current_width > max_width * BREAK_POINT_THRESHOLD
  end

  space_break_point || punct_break_point || best_break_point
end

.pad_string_to_width(string, target_width) ⇒ Object

Pad string to target_width with spaces



87
88
89
90
91
92
93
94
# File 'lib/rufio/text_utils.rb', line 87

def pad_string_to_width(string, target_width)
  current_width = display_width(string)
  if current_width >= target_width
    truncate_to_width(string, target_width)
  else
    string + ' ' * (target_width - current_width)
  end
end

.truncate_to_width(string, max_width) ⇒ Object

Truncate string to fit within max_width



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/rufio/text_utils.rb', line 51

def truncate_to_width(string, max_width)
  return string if display_width(string) <= max_width

  # If max_width is enough for ellipsis, truncate and add ellipsis
  if max_width >= ELLIPSIS_MIN_WIDTH
    result = ''
    current_width = 0
    target_width = max_width - ELLIPSIS_MIN_WIDTH

    string.each_char do |char|
      cw = char_width(char)
      break if current_width + cw > target_width

      result += char
      current_width += cw
    end

    result + ELLIPSIS
  else
    # Not enough room for ellipsis, just truncate
    result = ''
    current_width = 0

    string.each_char do |char|
      cw = char_width(char)
      break if current_width + cw > max_width

      result += char
      current_width += cw
    end

    result
  end
end

.wrap_preview_lines(lines, max_width) ⇒ Array<String>

Wrap preview lines to fit within max_width

Parameters:

  • lines (Array<String>)

    Lines to wrap

  • max_width (Integer)

    Maximum width for each line

Returns:

  • (Array<String>)

    Wrapped lines



126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/rufio/text_utils.rb', line 126

def wrap_preview_lines(lines, max_width)
  return lines if max_width <= 0

  wrapped = []
  lines.each do |line|
    # Handle encoding errors: scrub invalid UTF-8 sequences
    begin
      # Force UTF-8 encoding and replace invalid bytes
      line = line.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?')
      # Remove trailing whitespace
      line = line.rstrip
    rescue EncodingError, ArgumentError => e
      # If encoding fails completely, skip this line
      wrapped << '[encoding error]'
      next
    end

    # If line is empty, keep it
    if line.empty?
      wrapped << ''
      next
    end

    # If line fits within max_width, keep it as is
    begin
      if display_width(line) <= max_width
        wrapped << line
        next
      end
    rescue ArgumentError => e
      # If display_width fails, just truncate by byte length
      if line.bytesize <= max_width
        wrapped << line
        next
      end
    end

    # Split long lines
    current_line = []
    current_width = 0

    begin
      line.each_char do |char|
        cw = char_width(char)

        if current_width + cw > max_width
          # Start a new line
          wrapped << current_line.join
          current_line = [char]
          current_width = cw
        else
          current_line << char
          current_width += cw
        end
      end

      # Add remaining characters
      wrapped << current_line.join unless current_line.empty?
    rescue ArgumentError, EncodingError => e
      # If character iteration fails, just add the line truncated
      truncated = line.byteslice(0, [max_width, line.bytesize].min)
      wrapped << (truncated || line)
    end
  end

  wrapped
end