Module: Wp2txt::CliUI

Defined in:
lib/wp2txt/cli_ui.rb

Overview

CLI UI helper module for consistent styling

Constant Summary collapse

EXIT_SUCCESS =

Exit codes for CLI

0
EXIT_ERROR =
1
EXIT_PARTIAL =

Partial success (e.g., some articles not found)

2
ICONS =

Icons for status indicators

{
  success: "",
  error: "",
  warning: "!",
  info: "",
  arrow: "",
  bullet: "",
  check: "",
  cross: "",
  star: ""
}.freeze

Instance Method Summary collapse

Instance Method Details

#calculate_eta(processed, total, elapsed_seconds) ⇒ Float?

Calculate ETA based on current progress

Parameters:

  • processed (Integer)

    Items processed so far

  • total (Integer)

    Total items to process

  • elapsed_seconds (Numeric)

    Time elapsed in seconds

Returns:

  • (Float, nil)

    Estimated seconds remaining, or nil if cannot calculate



238
239
240
241
242
243
244
245
246
247
# File 'lib/wp2txt/cli_ui.rb', line 238

def calculate_eta(processed, total, elapsed_seconds)
  return nil if processed.zero? || total.nil? || total.zero?
  return nil if processed > total

  rate = processed.to_f / elapsed_seconds
  return nil if rate.zero?

  remaining = total - processed
  remaining / rate
end

#color_disabled_by_env?Boolean

Check if color is disabled by environment

Returns:

  • (Boolean)


39
40
41
42
# File 'lib/wp2txt/cli_ui.rb', line 39

def color_disabled_by_env?
  # NO_COLOR is a standard: https://no-color.org/
  ENV.key?("NO_COLOR") || ENV["TERM"] == "dumb"
end

#configure_ui(no_color: false, quiet: false) ⇒ Object

Configure UI settings

Parameters:

  • no_color (Boolean) (defaults to: false)

    Disable colors

  • quiet (Boolean) (defaults to: false)

    Suppress progress output



32
33
34
35
# File 'lib/wp2txt/cli_ui.rb', line 32

def configure_ui(no_color: false, quiet: false)
  @no_color = no_color || color_disabled_by_env?
  @quiet = quiet
end

#confirm?(message, default: false) ⇒ Boolean

Prompt for confirmation

Parameters:

  • message (String)

    Prompt message

  • default (Boolean) (defaults to: false)

    Default response

Returns:

  • (Boolean)

    User response



380
381
382
383
384
385
386
387
388
389
390
# File 'lib/wp2txt/cli_ui.rb', line 380

def confirm?(message, default: false)
  return default unless $stdin.tty?

  suffix = default ? "[Y/n]" : "[y/N]"
  print "#{message} #{suffix}: "

  response = $stdin.gets&.strip&.downcase
  return default if response.nil? || response.empty?

  %w[y yes].include?(response)
end

#count_articles_from_index(index_path) ⇒ Integer?

Count articles from multistream index file (quick count, not full load)

Parameters:

  • index_path (String)

    Path to index file

Returns:

  • (Integer, nil)

    Article count, or nil if cannot count



296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
# File 'lib/wp2txt/cli_ui.rb', line 296

def count_articles_from_index(index_path)
  count = 0

  begin
    if index_path.end_with?(".bz2")
      IO.popen(["bzcat", index_path], "r") do |io|
        io.each_line { count += 1 }
      end
    else
      File.foreach(index_path) { count += 1 }
    end
    count
  rescue StandardError
    nil
  end
end

#create_download_bar(filename, total_bytes) ⇒ TTY::ProgressBar, NullProgressBar

Create a download progress bar

Parameters:

  • filename (String)

    File being downloaded

  • total_bytes (Integer)

    Total size in bytes

Returns:

  • (TTY::ProgressBar, NullProgressBar)

    Configured progress bar or null in quiet mode



362
363
364
365
366
367
368
369
370
371
372
373
374
# File 'lib/wp2txt/cli_ui.rb', line 362

def create_download_bar(filename, total_bytes)
  return NullProgressBar.new if quiet?

  size_str = format_size(total_bytes)
  TTY::ProgressBar.new(
    "  #{filename} [:bar] :percent (:eta)",
    total: total_bytes,
    bar_format: :block,
    width: 25,
    hide_cursor: true,
    unknown: "#{size_str} (size unknown)"
  )
end

#create_progress_bar(message, total) ⇒ TTY::ProgressBar, NullProgressBar

Create a progress bar with consistent styling

Parameters:

  • message (String)

    Progress message

  • total (Integer)

    Total count

Returns:

  • (TTY::ProgressBar, NullProgressBar)

    Configured progress bar or null in quiet mode



346
347
348
349
350
351
352
353
354
355
356
# File 'lib/wp2txt/cli_ui.rb', line 346

def create_progress_bar(message, total)
  return NullProgressBar.new if quiet?

  TTY::ProgressBar.new(
    "#{message} [:bar] :percent (:current/:total) :eta",
    total: total,
    bar_format: :block,
    width: 30,
    hide_cursor: true
  )
end

#create_spinner(message) ⇒ TTY::Spinner, NullSpinner

Create a spinner with consistent styling

Parameters:

  • message (String)

    Spinner message

Returns:

  • (TTY::Spinner, NullSpinner)

    Configured spinner or null spinner in quiet mode



332
333
334
335
336
337
338
339
340
# File 'lib/wp2txt/cli_ui.rb', line 332

def create_spinner(message)
  return NullSpinner.new if quiet?

  TTY::Spinner.new(
    "[:spinner] #{message}",
    format: :dots,
    hide_cursor: true
  )
end

#estimate_from_file_size(input_path) ⇒ Integer?

Estimate article count from file size

Parameters:

  • input_path (String)

    Path to input file

Returns:

  • (Integer, nil)

    Estimated article count



316
317
318
319
320
321
322
323
324
325
326
327
# File 'lib/wp2txt/cli_ui.rb', line 316

def estimate_from_file_size(input_path)
  return nil unless File.exist?(input_path)

  size_mb = File.size(input_path) / (1024.0 * 1024)

  # Empirical estimates (articles per MB of compressed bz2):
  # - English Wikipedia: ~280 articles/MB
  # - Japanese Wikipedia: ~100 articles/MB (longer articles on average)
  # - Other languages: ~200 articles/MB (rough average)
  # Using conservative estimate of 200 articles/MB
  (size_mb * 200).to_i
end

#estimate_total_articles(input_path) ⇒ Integer?

Estimate total article count from multistream index or file size

Parameters:

  • input_path (String)

    Path to input file

Returns:

  • (Integer, nil)

    Estimated total articles, or nil if cannot estimate



252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
# File 'lib/wp2txt/cli_ui.rb', line 252

def estimate_total_articles(input_path)
  return nil unless input_path

  # Check for multistream index file
  index_path = find_multistream_index(input_path)
  if index_path && File.exist?(index_path)
    count = count_articles_from_index(index_path)
    return count if count && count > 0
  end

  # Fallback: estimate from file size
  # English Wikipedia: ~25GB compressed → ~7M articles ≈ 280 articles/MB
  # Other languages vary, but this gives a reasonable estimate
  estimate_from_file_size(input_path)
end

#find_multistream_index(input_path) ⇒ String?

Find multistream index file for a given input file

Parameters:

  • input_path (String)

    Path to multistream file

Returns:

  • (String, nil)

    Path to index file, or nil if not found



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
# File 'lib/wp2txt/cli_ui.rb', line 271

def find_multistream_index(input_path)
  return nil unless input_path.include?("multistream")

  # Pattern: *-multistream.xml.bz2 → *-multistream-index.txt.bz2
  base = input_path.sub(/multistream\.xml\.bz2$/, "multistream-index.txt.bz2")
  return base if File.exist?(base)

  # Try alternate patterns
  dir = File.dirname(input_path)
  basename = File.basename(input_path)

  # Extract language and date from filename
  if basename =~ /^(\w+wiki)-(\d+)-/
    lang = $1
    date = $2
    alt_path = File.join(dir, "#{lang}-#{date}-pages-articles-multistream-index.txt.bz2")
    return alt_path if File.exist?(alt_path)
  end

  nil
end

#format_duration(seconds) ⇒ String

Format duration in human-readable form

Parameters:

  • seconds (Float)

    Duration in seconds

Returns:

  • (String)

    Formatted duration



192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/wp2txt/cli_ui.rb', line 192

def format_duration(seconds)
  if seconds < 60
    "#{seconds.round(1)}s"
  elsif seconds < 3600
    mins = (seconds / 60).floor
    secs = (seconds % 60).round
    "#{mins}m #{secs}s"
  else
    hours = (seconds / 3600).floor
    mins = ((seconds % 3600) / 60).floor
    "#{hours}h #{mins}m"
  end
end

#format_eta(seconds) ⇒ String

Format ETA (Estimated Time of Arrival) in HH:MM:SS format

Parameters:

  • seconds (Numeric)

    Remaining seconds

Returns:

  • (String)

    Formatted ETA or "--:--:--" if nil/invalid



216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/wp2txt/cli_ui.rb', line 216

def format_eta(seconds)
  return "--:--:--" if seconds.nil? || seconds.negative? || !seconds.finite?

  seconds = seconds.to_i
  hours = seconds / 3600
  mins = (seconds % 3600) / 60
  secs = seconds % 60

  if hours > 99
    ">99h"
  elsif hours > 0
    format("%02d:%02d:%02d", hours, mins, secs)
  else
    format("%02d:%02d", mins, secs)
  end
end

#format_size(bytes) ⇒ String

Format file size in human-readable form

Parameters:

  • bytes (Integer)

    Size in bytes

Returns:

  • (String)

    Formatted size



209
210
211
# File 'lib/wp2txt/cli_ui.rb', line 209

def format_size(bytes)
  Wp2txt.format_file_size(bytes)
end

#no_color?Boolean

Check if color is disabled

Returns:

  • (Boolean)


52
53
54
# File 'lib/wp2txt/cli_ui.rb', line 52

def no_color?
  @no_color || false
end

#pastelObject

Initialize pastel for colors



57
58
59
# File 'lib/wp2txt/cli_ui.rb', line 57

def pastel
  @pastel ||= Pastel.new(enabled: !no_color?)
end

Print elapsed time

Parameters:

  • seconds (Float)

    Elapsed seconds



182
183
184
185
186
187
# File 'lib/wp2txt/cli_ui.rb', line 182

def print_elapsed_time(seconds)
  return if quiet?

  formatted = format_duration(seconds)
  puts pastel.dim("Completed in #{formatted}")
end

Print an error message (always shown, even in quiet mode)

Parameters:

  • message (String)

    Message



112
113
114
115
# File 'lib/wp2txt/cli_ui.rb', line 112

def print_error(message)
  # Errors are always shown
  $stderr.puts "#{pastel.red(ICONS[:error])} #{message}"
end

Print a section header with optional step indicator

Parameters:

  • title (String)

    Section title

  • step (Integer, nil) (defaults to: nil)

    Current step number

  • total_steps (Integer, nil) (defaults to: nil)

    Total number of steps



70
71
72
73
74
75
76
77
78
79
80
# File 'lib/wp2txt/cli_ui.rb', line 70

def print_header(title, step: nil, total_steps: nil)
  return if quiet?

  puts
  if step && total_steps
    step_indicator = pastel.dim("[#{step}/#{total_steps}]")
    puts "#{step_indicator} #{pastel.cyan.bold(title)}"
  else
    puts pastel.cyan.bold("═══ #{title} ═══")
  end
end

Print key-value info line

Parameters:

  • key (String)

    Label

  • value (String)

    Value

  • indent (Integer) (defaults to: 0)

    Indentation level



95
96
97
98
99
100
# File 'lib/wp2txt/cli_ui.rb', line 95

def print_info(key, value, indent: 0)
  return if quiet?

  prefix = "  " * indent
  puts "#{prefix}#{pastel.dim(key + ":")} #{value}"
end

Print an info message

Parameters:

  • message (String)

    Message



126
127
128
129
130
# File 'lib/wp2txt/cli_ui.rb', line 126

def print_info_message(message)
  return if quiet?

  puts "#{pastel.blue(ICONS[:info])} #{message}"
end

Print a list item with status

Parameters:

  • text (String)

    Item text

  • status (Symbol) (defaults to: :pending)

    :success, :error, :warning, :pending

  • indent (Integer) (defaults to: 1)

    Indentation level



136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/wp2txt/cli_ui.rb', line 136

def print_list_item(text, status: :pending, indent: 1)
  return if quiet?

  prefix = "  " * indent
  icon = case status
         when :success then pastel.green(ICONS[:check])
         when :error then pastel.red(ICONS[:cross])
         when :warning then pastel.yellow(ICONS[:warning])
         else pastel.dim(ICONS[:bullet])
         end
  puts "#{prefix}#{icon} #{text}"
end

Print a mode banner

Parameters:

  • mode (String)

    Mode name

  • details (Hash) (defaults to: {})

    Mode details



395
396
397
398
399
400
401
402
403
404
405
406
407
408
# File 'lib/wp2txt/cli_ui.rb', line 395

def print_mode_banner(mode, details = {})
  return if quiet?

  puts
  puts pastel.cyan.bold("" * 50)
  puts pastel.cyan.bold("  #{mode}")
  puts pastel.cyan.bold("" * 50)
  puts

  details.each do |key, value|
    print_info(key.to_s, value.to_s)
  end
  puts
end

Print a sub-header

Parameters:

  • title (String)

    Sub-header title



84
85
86
87
88
89
# File 'lib/wp2txt/cli_ui.rb', line 84

def print_subheader(title)
  return if quiet?

  puts
  puts pastel.bold("─── #{title} ───")
end

Print a success message

Parameters:

  • message (String)

    Message



104
105
106
107
108
# File 'lib/wp2txt/cli_ui.rb', line 104

def print_success(message)
  return if quiet?

  puts "#{pastel.green(ICONS[:success])} #{message}"
end

Print a completion summary box (always shown, even in quiet mode)

Parameters:

  • title (String)

    Summary title

  • stats (Hash)

    Statistics to display

  • status (Symbol) (defaults to: :success)

    :success, :warning, :error



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
# File 'lib/wp2txt/cli_ui.rb', line 153

def print_summary(title, stats, status: :success)
  # Summary is always shown (it's the final result)
  puts
  color = case status
          when :success then :green
          when :warning then :yellow
          when :error then :red
          else :white
          end

  # Calculate box width based on content
  width = 40
  title_line = " #{title}"
  content_lines = stats.map { |k, v| "  #{k}: #{v}" }

  # Draw box
  puts pastel.send(color, "#{"" * width}")
  puts pastel.send(color, "") + pastel.bold(title_line.ljust(width)) + pastel.send(color, "")
  puts pastel.send(color, "#{"" * width}")

  content_lines.each do |line|
    puts pastel.send(color, "") + line.ljust(width) + pastel.send(color, "")
  end

  puts pastel.send(color, "#{"" * width}")
end

Print a warning message (always shown, even in quiet mode)

Parameters:

  • message (String)

    Message



119
120
121
122
# File 'lib/wp2txt/cli_ui.rb', line 119

def print_warning(message)
  # Warnings are always shown
  $stderr.puts "#{pastel.yellow(ICONS[:warning])} #{message}"
end

#quiet?Boolean

Check if quiet mode is enabled

Returns:

  • (Boolean)


46
47
48
# File 'lib/wp2txt/cli_ui.rb', line 46

def quiet?
  @quiet || false
end

#reset_pastel!Object

Reset pastel instance (needed after configure_ui)



62
63
64
# File 'lib/wp2txt/cli_ui.rb', line 62

def reset_pastel!
  @pastel = nil
end