rufio 0.40.1 → 0.50.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.
@@ -22,20 +22,33 @@ module Rufio
22
22
  # Render the screen with differential updates
23
23
  #
24
24
  # @param screen [Screen] The back buffer to render
25
+ # @return [Boolean] true if rendering was performed, false if skipped
25
26
  def render(screen)
27
+ # CPU最適化: Dirty rowsが空の場合は完全にスキップ
28
+ dirty = screen.dirty_rows
29
+ if dirty.empty?
30
+ return false
31
+ end
32
+
26
33
  # Phase1: Only process dirty rows (rows that have changed)
27
- screen.dirty_rows.each do |y|
34
+ rendered_count = 0
35
+ dirty.each do |y|
28
36
  line = screen.row(y)
29
37
  next if line == @front[y] # Skip if content is actually the same
30
38
 
31
39
  # Move cursor to line y (1-indexed) and output the line
32
40
  @output.print "\e[#{y + 1};1H#{line}"
33
41
  @front[y] = line
42
+ rendered_count += 1
34
43
  end
35
44
 
36
45
  # Phase1: Clear dirty tracking after rendering
37
46
  screen.clear_dirty
38
- @output.flush
47
+
48
+ # Only flush if we actually rendered something
49
+ @output.flush if rendered_count > 0
50
+
51
+ true
39
52
  end
40
53
 
41
54
  # Resize the front buffer
@@ -0,0 +1,253 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "timeout"
5
+
6
+ module Rufio
7
+ # スクリプトを安全に実行するクラス
8
+ class ScriptExecutor
9
+ class << self
10
+ # スクリプトを実行する
11
+ # @param interpreter [String] インタープリタ(ruby, python3, bashなど)
12
+ # @param script_path [String] スクリプトのパス
13
+ # @param args [Array<String>] スクリプトへの引数
14
+ # @param timeout [Numeric, nil] タイムアウト秒数(nilの場合は無制限)
15
+ # @param chdir [String, nil] 作業ディレクトリ
16
+ # @param env [Hash, nil] 環境変数
17
+ # @return [Hash] 実行結果
18
+ def execute(interpreter, script_path, args = [], timeout: nil, chdir: nil, env: nil)
19
+ # 配列ベースのコマンドを構築(シェルインジェクション防止)
20
+ command = [interpreter, script_path, *args]
21
+
22
+ # オプションを構築
23
+ options = {}
24
+ options[:chdir] = chdir if chdir
25
+ # 環境変数をマージ(既存の環境変数を保持)
26
+ spawn_env = env || {}
27
+
28
+ execute_with_options(command, spawn_env, options, timeout)
29
+ rescue StandardError => e
30
+ build_error_result(e)
31
+ end
32
+
33
+ # DslCommandを実行する(タイプ別に分岐)
34
+ # @param dsl_command [DslCommand] 実行するDSLコマンド
35
+ # @param args [Array<String>] 追加の引数
36
+ # @param timeout [Numeric, nil] タイムアウト秒数
37
+ # @param chdir [String, nil] 作業ディレクトリ
38
+ # @param env [Hash, nil] 環境変数
39
+ # @return [Hash] 実行結果
40
+ def execute_command(dsl_command, args = [], timeout: nil, chdir: nil, env: nil)
41
+ case dsl_command.command_type
42
+ when :ruby
43
+ execute_ruby(dsl_command)
44
+ when :shell
45
+ execute_shell(dsl_command, timeout: timeout, chdir: chdir, env: env)
46
+ else
47
+ exec_args = dsl_command.to_execution_args
48
+ execute(exec_args[0], exec_args[1], args, timeout: timeout, chdir: chdir, env: env)
49
+ end
50
+ end
51
+
52
+ # inline Rubyコマンドを実行する
53
+ # @param dsl_command [DslCommand] 実行するDSLコマンド
54
+ # @return [Hash] 実行結果
55
+ def execute_ruby(dsl_command)
56
+ result = dsl_command.ruby_block.call
57
+ {
58
+ success: true,
59
+ exit_code: 0,
60
+ stdout: result.to_s,
61
+ stderr: "",
62
+ timeout: false
63
+ }
64
+ rescue StandardError => e
65
+ {
66
+ success: false,
67
+ exit_code: 1,
68
+ stdout: "",
69
+ stderr: "",
70
+ error: e.message,
71
+ timeout: false
72
+ }
73
+ end
74
+
75
+ # inline シェルコマンドを実行する
76
+ # @param dsl_command [DslCommand] 実行するDSLコマンド
77
+ # @param timeout [Numeric, nil] タイムアウト秒数
78
+ # @param chdir [String, nil] 作業ディレクトリ
79
+ # @param env [Hash, nil] 環境変数
80
+ # @return [Hash] 実行結果
81
+ def execute_shell(dsl_command, timeout: nil, chdir: nil, env: nil)
82
+ options = {}
83
+ options[:chdir] = chdir if chdir
84
+ spawn_env = env || {}
85
+
86
+ if timeout
87
+ execute_shell_with_timeout(dsl_command.shell_command, spawn_env, options, timeout)
88
+ else
89
+ execute_shell_without_timeout(dsl_command.shell_command, spawn_env, options)
90
+ end
91
+ rescue StandardError => e
92
+ build_error_result(e)
93
+ end
94
+
95
+ private
96
+
97
+ # コマンドを実行し、結果を返す
98
+ # @param command [Array<String>] 実行するコマンド
99
+ # @param env [Hash] 環境変数
100
+ # @param options [Hash] Open3オプション
101
+ # @param timeout_sec [Numeric, nil] タイムアウト秒数
102
+ # @return [Hash] 実行結果
103
+ def execute_with_options(command, env, options, timeout_sec)
104
+ if timeout_sec
105
+ execute_with_timeout(command, env, options, timeout_sec)
106
+ else
107
+ execute_without_timeout(command, env, options)
108
+ end
109
+ end
110
+
111
+ # タイムアウト付きで実行
112
+ def execute_with_timeout(command, env, options, timeout_sec)
113
+ stdout = ""
114
+ stderr = ""
115
+ status = nil
116
+ timed_out = false
117
+ pid = nil
118
+
119
+ begin
120
+ Timeout.timeout(timeout_sec) do
121
+ stdin, stdout_io, stderr_io, wait_thread = Open3.popen3(env, *command, **options)
122
+ pid = wait_thread.pid
123
+ stdin.close
124
+ stdout = stdout_io.read
125
+ stderr = stderr_io.read
126
+ stdout_io.close
127
+ stderr_io.close
128
+ status = wait_thread.value
129
+ end
130
+ rescue Timeout::Error
131
+ timed_out = true
132
+ # プロセスを終了
133
+ if pid
134
+ begin
135
+ Process.kill("TERM", pid)
136
+ sleep 0.1
137
+ Process.kill("KILL", pid)
138
+ rescue Errno::ESRCH, Errno::EPERM
139
+ # プロセスが既に終了している、または権限がない
140
+ end
141
+ end
142
+ end
143
+
144
+ if timed_out
145
+ {
146
+ success: false,
147
+ exit_code: nil,
148
+ stdout: stdout,
149
+ stderr: stderr,
150
+ timeout: true
151
+ }
152
+ else
153
+ {
154
+ success: status&.success? || false,
155
+ exit_code: status&.exitstatus || 1,
156
+ stdout: stdout,
157
+ stderr: stderr,
158
+ timeout: false
159
+ }
160
+ end
161
+ end
162
+
163
+ # タイムアウトなしで実行
164
+ def execute_without_timeout(command, env, options)
165
+ stdout, stderr, status = Open3.capture3(env, *command, **options)
166
+
167
+ {
168
+ success: status.success?,
169
+ exit_code: status.exitstatus,
170
+ stdout: stdout,
171
+ stderr: stderr,
172
+ timeout: false
173
+ }
174
+ end
175
+
176
+ # シェルコマンドをタイムアウト付きで実行
177
+ def execute_shell_with_timeout(shell_command, env, options, timeout_sec)
178
+ stdout = ""
179
+ stderr = ""
180
+ status = nil
181
+ timed_out = false
182
+ pid = nil
183
+
184
+ begin
185
+ Timeout.timeout(timeout_sec) do
186
+ stdin, stdout_io, stderr_io, wait_thread = Open3.popen3(env, shell_command, **options)
187
+ pid = wait_thread.pid
188
+ stdin.close
189
+ stdout = stdout_io.read
190
+ stderr = stderr_io.read
191
+ stdout_io.close
192
+ stderr_io.close
193
+ status = wait_thread.value
194
+ end
195
+ rescue Timeout::Error
196
+ timed_out = true
197
+ if pid
198
+ begin
199
+ Process.kill("TERM", pid)
200
+ sleep 0.1
201
+ Process.kill("KILL", pid)
202
+ rescue Errno::ESRCH, Errno::EPERM
203
+ # プロセスが既に終了している、または権限がない
204
+ end
205
+ end
206
+ end
207
+
208
+ if timed_out
209
+ {
210
+ success: false,
211
+ exit_code: nil,
212
+ stdout: stdout,
213
+ stderr: stderr,
214
+ timeout: true
215
+ }
216
+ else
217
+ {
218
+ success: status&.success? || false,
219
+ exit_code: status&.exitstatus || 1,
220
+ stdout: stdout,
221
+ stderr: stderr,
222
+ timeout: false
223
+ }
224
+ end
225
+ end
226
+
227
+ # シェルコマンドをタイムアウトなしで実行
228
+ def execute_shell_without_timeout(shell_command, env, options)
229
+ stdout, stderr, status = Open3.capture3(env, shell_command, **options)
230
+
231
+ {
232
+ success: status.success?,
233
+ exit_code: status.exitstatus,
234
+ stdout: stdout,
235
+ stderr: stderr,
236
+ timeout: false
237
+ }
238
+ end
239
+
240
+ # エラー結果を構築
241
+ def build_error_result(error)
242
+ {
243
+ success: false,
244
+ exit_code: 1,
245
+ stdout: "",
246
+ stderr: "",
247
+ error: error.message,
248
+ timeout: false
249
+ }
250
+ end
251
+ end
252
+ end
253
+ end
@@ -71,6 +71,10 @@ module Rufio
71
71
  @cached_bookmarks = nil
72
72
  @cached_bookmark_time = nil
73
73
  @bookmark_cache_ttl = 1.0 # 1秒間キャッシュ
74
+
75
+ # Command execution lamp (footer indicator)
76
+ @completion_lamp_message = nil
77
+ @completion_lamp_time = nil
74
78
  end
75
79
 
76
80
  def start(directory_listing, keybind_handler, file_preview, background_executor = nil)
@@ -159,75 +163,143 @@ module Rufio
159
163
  puts ConfigLoader.message('app.terminated')
160
164
  end
161
165
 
162
- # ゲームループパターンのmain_loop
166
+ # ゲームループパターンのmain_loop(CPU最適化版:フレームスキップ対応)
163
167
  # UPDATE → DRAW → RENDER → SLEEP のサイクル
168
+ # 変更がない場合は描画をスキップしてCPU使用率を削減
164
169
  def main_loop
165
- fps = 60
166
- interval = 1.0 / fps
170
+ # CPU最適化: 固定FPSをやめて、イベントドリブンに変更
171
+ # 最小スリープ時間(入力チェック間隔)
172
+ min_sleep_interval = 0.0333 # 30FPS(約33.33ms/フレーム)
173
+ check_interval = 0.1 # バックグラウンドタスクのチェック間隔
167
174
 
168
175
  # Phase 3: Screen/Rendererを初期化
169
176
  @screen = Screen.new(@screen_width, @screen_height)
170
177
  @renderer = Renderer.new(@screen_width, @screen_height)
171
178
 
179
+ # 初回描画
180
+ @screen.clear
181
+ draw_screen_to_buffer(@screen, nil, nil)
182
+ @renderer.render(@screen)
183
+
172
184
  last_notification_check = Time.now
185
+ last_lamp_check = Time.now
173
186
  notification_message = nil
174
187
  notification_time = nil
188
+ previous_notification = nil
189
+ previous_lamp_message = @completion_lamp_message
175
190
 
176
191
  # FPS計測用
177
192
  frame_times = []
178
193
  last_frame_time = Time.now
179
194
  current_fps = 0.0
195
+ last_fps_update = Time.now
196
+
197
+ # 再描画フラグ
198
+ needs_redraw = false
180
199
 
181
200
  while @running
182
201
  start = Time.now
183
202
 
203
+ # FPS計算(毎フレームで記録)- ループの最初で計測してsleep時間を含める
204
+ if @test_mode
205
+ frame_time = start - last_frame_time
206
+ last_frame_time = start
207
+ frame_times << frame_time
208
+ frame_times.shift if frame_times.size > 60 # 直近60フレームで平均
209
+
210
+ # FPS表示の更新は1秒ごと
211
+ if (start - last_fps_update) > 1.0
212
+ avg_frame_time = frame_times.sum / frame_times.size
213
+ current_fps = 1.0 / avg_frame_time if avg_frame_time > 0
214
+ last_fps_update = start
215
+ end
216
+
217
+ # test_modeでは毎フレーム描画してFPS計測の精度を上げる
218
+ needs_redraw = true
219
+ end
220
+
184
221
  # UPDATE phase - ノンブロッキング入力処理
185
- handle_input_nonblocking
222
+ # 入力があった場合は再描画が必要
223
+ had_input = handle_input_nonblocking
224
+ needs_redraw = true if had_input
186
225
 
187
- # バックグラウンドコマンドの完了チェック(0.5秒ごと)
188
- if @background_executor && (Time.now - last_notification_check) > 0.5
226
+ # バックグラウンドコマンドの完了チェック(0.1秒ごと)
227
+ if @background_executor && (start - last_notification_check) > check_interval
189
228
  if !@background_executor.running? && @background_executor.get_completion_message
190
- notification_message = @background_executor.get_completion_message
191
- notification_time = Time.now
229
+ completion_msg = @background_executor.get_completion_message
230
+ # 通知メッセージとして表示
231
+ notification_message = completion_msg
232
+ notification_time = start
233
+ # フッターのランプ表示用にも設定
234
+ @completion_lamp_message = completion_msg
235
+ @completion_lamp_time = start
192
236
  @background_executor.instance_variable_set(:@completion_message, nil) # メッセージをクリア
237
+ needs_redraw = true
193
238
  end
194
- last_notification_check = Time.now
239
+ last_notification_check = start
195
240
  end
196
241
 
197
- # FPS計算(移動平均)
198
- if @test_mode
199
- frame_time = Time.now - last_frame_time
200
- frame_times << frame_time
201
- frame_times.shift if frame_times.size > 60 # 直近60フレームで平均
202
- avg_frame_time = frame_times.sum / frame_times.size
203
- current_fps = 1.0 / avg_frame_time if avg_frame_time > 0
204
- last_frame_time = Time.now
242
+ # バックグラウンドコマンドの実行状態が変わった場合も再描画
243
+ if @background_executor
244
+ current_running = @background_executor.running?
245
+ if @last_bg_running != current_running
246
+ @last_bg_running = current_running
247
+ needs_redraw = true
248
+ end
205
249
  end
206
250
 
207
- # DRAW phase - Screenバッファに描画
208
- @screen.clear
209
- if notification_message && (Time.now - notification_time) < 3.0
210
- draw_screen_to_buffer(@screen, notification_message, current_fps)
211
- else
212
- notification_message = nil if notification_message
213
- draw_screen_to_buffer(@screen, nil, current_fps)
251
+ # 完了ランプの表示状態をチェック(0.5秒ごと)
252
+ if (start - last_lamp_check) > 0.5
253
+ current_lamp = @completion_lamp_message
254
+ if current_lamp != previous_lamp_message
255
+ previous_lamp_message = current_lamp
256
+ needs_redraw = true
257
+ end
258
+ # 完了ランプのタイムアウトチェック
259
+ if @completion_lamp_message && @completion_lamp_time && (start - @completion_lamp_time) >= 3.0
260
+ @completion_lamp_message = nil
261
+ needs_redraw = true
262
+ end
263
+ last_lamp_check = start
214
264
  end
215
265
 
216
- # RENDER phase - 差分レンダリング
217
- @renderer.render(@screen)
266
+ # 通知メッセージの変化をチェック
267
+ current_notification = notification_message && (start - notification_time) < 3.0 ? notification_message : nil
268
+ if current_notification != previous_notification
269
+ previous_notification = current_notification
270
+ notification_message = nil if current_notification.nil?
271
+ needs_redraw = true
272
+ end
273
+
274
+ # DRAW & RENDER phase - 変更があった場合のみ描画
275
+ if needs_redraw
276
+ # Screenバッファに描画(clearは呼ばない。必要な部分だけ更新)
277
+ if notification_message && (start - notification_time) < 3.0
278
+ draw_screen_to_buffer(@screen, notification_message, current_fps)
279
+ else
280
+ draw_screen_to_buffer(@screen, nil, current_fps)
281
+ end
282
+
283
+ # 差分レンダリング(dirty rowsのみ)
284
+ @renderer.render(@screen)
285
+
286
+ # 描画後にカーソルを画面外に移動
287
+ if !@command_mode_active
288
+ print "\e[#{@screen_height};#{@screen_width}H"
289
+ end
290
+
291
+ needs_redraw = false
292
+ end
218
293
 
219
294
  # コマンドモードがアクティブな場合はフローティングウィンドウを表示
220
295
  # Phase 4: 暫定的に直接描画(Screenバッファ外)
221
296
  if @command_mode_active
222
297
  @command_mode_ui.show_input_prompt(@command_input)
223
- else
224
- # カーソルを画面外に移動
225
- print "\e[#{@screen_height};#{@screen_width}H"
226
298
  end
227
299
 
228
- # SLEEP phase - FPS制御
300
+ # SLEEP phase - CPU使用率削減のため適切にスリープ
229
301
  elapsed = Time.now - start
230
- sleep_time = [interval - elapsed, 0].max
302
+ sleep_time = [min_sleep_interval - elapsed, 0].max
231
303
  sleep sleep_time if sleep_time > 0
232
304
  end
233
305
  end
@@ -829,13 +901,36 @@ module Rufio
829
901
  end
830
902
  bookmark_text = bookmark_parts.join(" ")
831
903
 
832
- # 右側の情報: FPS(test modeの時のみ)| ?:help
904
+ # 右側の情報: コマンド実行ランプ | FPS(test modeの時のみ)| ?:help
905
+ right_parts = []
906
+
907
+ # バックグラウンドコマンドの実行状態をランプで表示
908
+ if @background_executor
909
+ if @background_executor.running?
910
+ # 実行中ランプ(緑色の回転矢印)
911
+ command_name = @background_executor.current_command || "処理中"
912
+ right_parts << "\e[32m🔄\e[0m #{command_name}"
913
+ elsif @completion_lamp_message && @completion_lamp_time
914
+ # 完了ランプ(3秒間表示)
915
+ if (Time.now - @completion_lamp_time) < 3.0
916
+ right_parts << @completion_lamp_message
917
+ else
918
+ @completion_lamp_message = nil
919
+ @completion_lamp_time = nil
920
+ end
921
+ end
922
+ end
923
+
924
+ # FPS表示(test modeの時のみ)
833
925
  if @test_mode && fps
834
- right_info = "#{fps.round(1)} FPS | ?:help"
835
- else
836
- right_info = "?:help"
926
+ right_parts << "#{fps.round(1)} FPS"
837
927
  end
838
928
 
929
+ # ヘルプ表示
930
+ right_parts << "?:help"
931
+
932
+ right_info = right_parts.join(" | ")
933
+
839
934
  # ブックマーク一覧を利用可能な幅に収める
840
935
  available_width = @screen_width - right_info.length - 3
841
936
  if bookmark_text.length > available_width && available_width > 3
@@ -918,26 +1013,26 @@ module Rufio
918
1013
  # ノンブロッキング入力処理(ゲームループ用)
919
1014
  # IO.selectでタイムアウト付きで入力をチェック
920
1015
  def handle_input_nonblocking
921
- # 1msタイムアウトで入力待ち(60FPS = 16.67ms/frame)
922
- ready = IO.select([STDIN], nil, nil, 0.001)
923
- return unless ready
1016
+ # 0msタイムアウトで即座にチェック(30FPS = 33.33ms/frame)
1017
+ ready = IO.select([STDIN], nil, nil, 0)
1018
+ return false unless ready
924
1019
 
925
1020
  begin
926
1021
  # read_nonblockを使ってノンブロッキングで1文字読み取る
927
1022
  input = STDIN.read_nonblock(1)
928
1023
  rescue IO::WaitReadable, IO::EAGAINWaitReadable
929
1024
  # 入力が利用できない
930
- return
1025
+ return false
931
1026
  rescue Errno::ENOTTY, Errno::ENODEV
932
1027
  # ターミナルでない環境
933
- return
1028
+ return false
934
1029
  end
935
1030
 
936
1031
  # コマンドモードがアクティブな場合は、エスケープシーケンス処理をスキップ
937
1032
  # ESCキーをそのまま handle_command_input に渡す
938
1033
  if @command_mode_active
939
1034
  handle_command_input(input)
940
- return
1035
+ return true
941
1036
  end
942
1037
 
943
1038
  # 特殊キーの処理(エスケープシーケンス)(コマンドモード外のみ)
@@ -967,12 +1062,15 @@ module Rufio
967
1062
  end
968
1063
 
969
1064
  # キーバインドハンドラーに処理を委譲
970
- @keybind_handler.handle_key(input) if input
1065
+ result = @keybind_handler.handle_key(input) if input
971
1066
 
972
- # 終了処理(qキーのみ)
973
- if input == 'q'
1067
+ # 終了処理(qキーのみ、確認ダイアログの結果を確認)
1068
+ if input == 'q' && result == true
974
1069
  @running = false
975
1070
  end
1071
+
1072
+ # 入力があったことを返す
1073
+ true
976
1074
  end
977
1075
 
978
1076
  def handle_input
@@ -1028,10 +1126,10 @@ module Rufio
1028
1126
  end
1029
1127
 
1030
1128
  # キーバインドハンドラーに処理を委譲
1031
- _result = @keybind_handler.handle_key(input)
1129
+ result = @keybind_handler.handle_key(input)
1032
1130
 
1033
- # 終了処理(qキーのみ)
1034
- if input == 'q'
1131
+ # 終了処理(qキーのみ、確認ダイアログの結果を確認)
1132
+ if input == 'q' && result == true
1035
1133
  @running = false
1036
1134
  end
1037
1135
  end
@@ -126,8 +126,17 @@ module Rufio
126
126
 
127
127
  wrapped = []
128
128
  lines.each do |line|
129
- # Remove trailing whitespace
130
- line = line.rstrip
129
+ # Handle encoding errors: scrub invalid UTF-8 sequences
130
+ begin
131
+ # Force UTF-8 encoding and replace invalid bytes
132
+ line = line.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?')
133
+ # Remove trailing whitespace
134
+ line = line.rstrip
135
+ rescue EncodingError, ArgumentError => e
136
+ # If encoding fails completely, skip this line
137
+ wrapped << '[encoding error]'
138
+ next
139
+ end
131
140
 
132
141
  # If line is empty, keep it
133
142
  if line.empty?
@@ -136,31 +145,45 @@ module Rufio
136
145
  end
137
146
 
138
147
  # If line fits within max_width, keep it as is
139
- if display_width(line) <= max_width
140
- wrapped << line
141
- next
148
+ begin
149
+ if display_width(line) <= max_width
150
+ wrapped << line
151
+ next
152
+ end
153
+ rescue ArgumentError => e
154
+ # If display_width fails, just truncate by byte length
155
+ if line.bytesize <= max_width
156
+ wrapped << line
157
+ next
158
+ end
142
159
  end
143
160
 
144
161
  # Split long lines
145
162
  current_line = []
146
163
  current_width = 0
147
164
 
148
- line.each_char do |char|
149
- cw = char_width(char)
150
-
151
- if current_width + cw > max_width
152
- # Start a new line
153
- wrapped << current_line.join
154
- current_line = [char]
155
- current_width = cw
156
- else
157
- current_line << char
158
- current_width += cw
165
+ begin
166
+ line.each_char do |char|
167
+ cw = char_width(char)
168
+
169
+ if current_width + cw > max_width
170
+ # Start a new line
171
+ wrapped << current_line.join
172
+ current_line = [char]
173
+ current_width = cw
174
+ else
175
+ current_line << char
176
+ current_width += cw
177
+ end
159
178
  end
160
- end
161
179
 
162
- # Add remaining characters
163
- wrapped << current_line.join unless current_line.empty?
180
+ # Add remaining characters
181
+ wrapped << current_line.join unless current_line.empty?
182
+ rescue ArgumentError, EncodingError => e
183
+ # If character iteration fails, just add the line truncated
184
+ truncated = line.byteslice(0, [max_width, line.bytesize].min)
185
+ wrapped << (truncated || line)
186
+ end
164
187
  end
165
188
 
166
189
  wrapped
data/lib/rufio/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rufio
4
- VERSION = '0.40.1'
4
+ VERSION = '0.50.0'
5
5
  end