WidgetGauge

Demonstrates completion visualization with interactive attribute cycling.

Long-running tasks create anxiety. Users need to know that the system is working and how much is left to do.

This demo showcases the Gauge widget. It provides an interactive playground where you can cycle through different ratios, colors, and label templates in real-time.

Use it to understand how to communicate progress and task status in your terminal interface.

Example

Run the demo from the terminal:

ruby examples/widget_gauge/app.rb

Source Code

# frozen_string_literal: true

#--
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
# SPDX-License-Identifier: MIT-0
#++

$LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
require "ratatui_ruby"

# Demonstrates completion visualization with interactive attribute cycling.
#
# Long-running tasks create anxiety. Users need to know that the system is working and how much is left to do.
#
# This demo showcases the <tt>Gauge</tt> widget. It provides an interactive playground where you can cycle through different ratios, colors, and label templates in real-time.
#
# Use it to understand how to communicate progress and task status in your terminal interface.
#
# === Example
#
# Run the demo from the terminal:
#
#   ruby examples/widget_gauge/app.rb
#
# rdoc-image:/doc/images/widget_gauge.png
class WidgetGauge
  def initialize
    @ratio = 0.65
    @ratios = [0.0, 0.25, 0.5, 0.65, 0.8, 0.95, 1.0]
    @ratio_index = 3

    # Demonstrates both ratio (0.0-1.0) and percent (0-100) input modes
    @input_modes = [:ratio, :percent]
    @input_mode_index = 0

    @gauge_colors = [
      { name: "Green", color: :green },
      { name: "Yellow", color: :yellow },
      { name: "Red", color: :red },
      { name: "Cyan", color: :cyan },
      { name: "Blue", color: :blue },
    ]
    @gauge_color_index = 0

    @bg_styles = nil # Initialized in run when @tui is available
    @bg_style_index = 1

    @use_unicode_options = [true, false]
    @use_unicode_index = 0

    @label_modes = [
      { name: "Percentage", template: -> (ratio) { "#{(ratio * 100).to_i}%" } },
      { name: "Ratio (decimal)", template: -> (ratio) { format("%.2f", ratio) } },
      { name: "Progress", template: -> (ratio) { "Progress: #{(ratio * 100).to_i}%" } },
      { name: "None", template: -> (_ratio) { nil } },
    ]
    @label_mode_index = 0
    @hotkey_style = nil # Initialized in run when @tui is available
  end

  def run
    RatatuiRuby.run do |tui|
      @tui = tui

      # Initialize styles using tui helpers
      @bg_styles = [
        { name: "None", style: nil },
        { name: "Dark Gray BG", style: tui.style(fg: :dark_gray) },
        { name: "White on Black", style: tui.style(fg: :white, bg: :black) },
        { name: "Bold White", style: tui.style(fg: :white, modifiers: [:bold]) },
      ]
      @hotkey_style = tui.style(modifiers: [:bold, :underlined])

      loop do
        render
        break if handle_input == :quit
      end
    end
  end

  private def render
    @ratio = @ratios[@ratio_index]
    gauge_color = @gauge_colors[@gauge_color_index][:color]
    bg_style = @bg_styles[@bg_style_index][:style]
    use_unicode = @use_unicode_options[@use_unicode_index]
    label_template = @label_modes[@label_mode_index][:template]

    gauge_style = @tui.style(fg: gauge_color)
    label = label_template.call(@ratio)

    @tui.draw do |frame|
      # Split into main content and control panel
      main_area, controls_area = @tui.layout_split(
        frame.area,
        direction: :vertical,
        constraints: [
          @tui.constraint_fill(1),
          @tui.constraint_length(6),
        ]
      )

      # Split main area into title, gauges, and spacer
      title_area, gauge1_area, gauge2_area, gauge3_area, spacer_area = @tui.layout_split(
        main_area,
        direction: :vertical,
        constraints: [
          @tui.constraint_length(1),
          @tui.constraint_fill(1),
          @tui.constraint_fill(1),
          @tui.constraint_fill(1),
          @tui.constraint_length(1),
        ]
      )

      # Render title
      title = @tui.paragraph(
        text: "Gauge Widget",
        style: @tui.style(modifiers: [:bold])
      )
      frame.render_widget(title, title_area)

      # Gauge 1: Main interactive gauge
      # Demonstrates both ratio (0.0-1.0) and percent (0-100) input modes
      input_mode = @input_modes[@input_mode_index]
      gauge_opts = if input_mode == :percent
        { percent: (@ratio * 100).to_i } # percent: accepts 0-100
      else
        { ratio: @ratio } # ratio: accepts 0.0-1.0
      end
      gauge1 = @tui.gauge(
        **gauge_opts,
        label:,
        style: bg_style,
        gauge_style:,
        use_unicode:,
        block: @tui.block(title: "Interactive Gauge (#{input_mode}:)")
      )
      frame.render_widget(gauge1, gauge1_area)

      # Gauge 2: Inverse ratio for comparison
      gauge2 = @tui.gauge(
        ratio: 1.0 - @ratio,
        label: label_template.call(1.0 - @ratio),
        style: bg_style,
        gauge_style:,
        use_unicode:,
        block: @tui.block(title: "Inverse (1.0 - ratio)")
      )
      frame.render_widget(gauge2, gauge2_area)

      # Gauge 3: Fixed at different stages
      gauge3 = @tui.gauge(
        ratio: [@ratio, 0.5].max,
        label: "Min 50%",
        style: @tui.style(fg: :dark_gray),
        gauge_style: @tui.style(fg: :magenta),
        use_unicode:,
        block: @tui.block(title: "Min Threshold (Magenta)")
      )
      frame.render_widget(gauge3, gauge3_area)

      # Render empty spacer
      spacer = @tui.paragraph(text: "")
      frame.render_widget(spacer, spacer_area)

      # Bottom controls panel
      controls = @tui.block(
        title: "Controls",
        borders: [:all],
        children: [
          @tui.paragraph(
            text: [
              # Navigation & General
              @tui.text_line(spans: [
                @tui.text_span(content: "←/→", style: @hotkey_style),
                @tui.text_span(content: ": Adjust Ratio (#{format('%.2f', @ratio)})  "),
                @tui.text_span(content: "q", style: @hotkey_style),
                @tui.text_span(content: ": Quit"),
              ]),
              # Styling
              @tui.text_line(spans: [
                @tui.text_span(content: "g", style: @hotkey_style),
                @tui.text_span(content: ": Color (#{@gauge_colors[@gauge_color_index][:name]})  "),
                @tui.text_span(content: "b", style: @hotkey_style),
                @tui.text_span(content: ": Background (#{@bg_styles[@bg_style_index][:name]})"),
              ]),
              # Options
              @tui.text_line(spans: [
                @tui.text_span(content: "u", style: @hotkey_style),
                @tui.text_span(content: ": Unicode (#{use_unicode ? 'On' : 'Off'})  "),
                @tui.text_span(content: "l", style: @hotkey_style),
                @tui.text_span(content: ": Label (#{@label_modes[@label_mode_index][:name]})  "),
                @tui.text_span(content: "i", style: @hotkey_style),
                @tui.text_span(content: ": Input (#{@input_modes[@input_mode_index]}:)"),
              ]),
            ]
          ),
        ]
      )
      frame.render_widget(controls, controls_area)
    end
  end

  private def handle_input
    case @tui.poll_event
    in { type: :key, code: "q" } | { type: :key, code: "c", modifiers: ["ctrl"] }
      :quit
    in type: :key, code: "right"
      @ratio_index = (@ratio_index + 1) % @ratios.length
    in type: :key, code: "left"
      @ratio_index = (@ratio_index - 1) % @ratios.length
    in type: :key, code: "g"
      @gauge_color_index = (@gauge_color_index + 1) % @gauge_colors.length
    in type: :key, code: "b"
      @bg_style_index = (@bg_style_index + 1) % @bg_styles.length
    in type: :key, code: "u"
      @use_unicode_index = (@use_unicode_index + 1) % @use_unicode_options.length
    in type: :key, code: "l"
      @label_mode_index = (@label_mode_index + 1) % @label_modes.length
    in type: :key, code: "i"
      @input_mode_index = (@input_mode_index + 1) % @input_modes.length
    else
      # Ignore other events
      nil
    end
  end
end

WidgetGauge.new.run if __FILE__ == $PROGRAM_NAME