Demonstrates a selectable list of items with interactive attribute cycling.
Users need to browse and select from collections of data. Lists are fundamental to terminal interfaces, but managing selection state, scrolling, and styling can be complex.
This demo showcases the List widget. It provides an interactive playground where you can cycle through different configurations, styles, and behaviors in real-time.
Use it to understand how to implement menus, file browsers, or any selectable collection of items.
Examples
Run the demo from the terminal:
ruby examples/widget_list_demo/app.rb

Source Code
# frozen_string_literal: true # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com> # SPDX-License-Identifier: AGPL-3.0-or-later $LOAD_PATH.unshift File.expand_path("../../lib", __dir__) require "ratatui_ruby" # Demonstrates a selectable list of items with interactive attribute cycling. # # Users need to browse and select from collections of data. Lists are fundamental to terminal interfaces, but managing selection state, scrolling, and styling can be complex. # # This demo showcases the <tt>List</tt> widget. It provides an interactive playground where you can cycle through different configurations, styles, and behaviors in real-time. # # Use it to understand how to implement menus, file browsers, or any selectable collection of items. # # === Examples # # Run the demo from the terminal: # # ruby examples/widget_list_demo/app.rb # # rdoc-image:/doc/images/widget_list_demo.png class WidgetListDemo # Initializes the demo with example data and default configuration. def initialize @selected_index = nil @item_sets = [ { name: "Large List", items: (1..200).map { |i| "Item #{i}" }, }, { name: "Colors", items: [ "Red", "Orange", "Yellow", "Green", "Cyan", "Blue", "Indigo", "Violet", "Scarlet", "Crimson", "Maroon", "Brown", "Tan", "Beige", "Khaki", "Gold", "Silver", "White", "Gray", "Black", "Pink", "Magenta", "Turquoise", "Teal", "Coral", "Salmon", "Peach", "Lavender", "Lilac", "Olive", "Lime", "Navy", "Charcoal", "Ivory", "Azure", ], }, { name: "Fruits", items: [ "Apple", "Apricot", "Avocado", "Banana", "Blueberry", "Blackberry", "Cherry", "Cranberry", "Cucumber", "Date", "Dragonfruit", "Elderberry", "Fig", "Grape", "Grapefruit", "Guava", "Honeydew", "Huckleberry", "Jackfruit", "Kiwi", "Kumquat", "Lemon", "Lime", "Lychee", "Mango", "Melon", "Mulberry", "Nectarine", "Olive", "Orange", "Papaya", "Passion Fruit", "Peach", "Pear", "Persimmon", "Pineapple", "Plum", "Pomegranate", "Prune", "Rambutan", "Raspberry", "Starfruit", "Strawberry", "Tangerine", "Watermelon", "Ugli Fruit", ], }, { name: "Programming", items: [ "Ruby", "Rust", "Python", "JavaScript", "Go", "C++", "C#", "Java", "Kotlin", "Swift", "Objective-C", "PHP", "TypeScript", "Perl", "Lua", "R", "Scala", "Haskell", "Elixir", "Clojure", "Groovy", "Closure", "VB.NET", "F#", "Erlang", "Lisp", "Scheme", "Prolog", "Fortran", "COBOL", "Pascal", "Delphi", "Ada", "Bash", "Sh", "Tcl", "Awk", "sed", "Vim Script", "PowerShell", "Batch", "Assembly", "Wasm", "WebAssembly", "Julia", "Matlab", "Octave", "BASIC", ], }, ] @item_set_index = 0 @highlight_symbol_names = [">> ", "▶ ", "→ ", "• ", "★ "] @highlight_symbol_index = 0 @direction_configs = [ { name: "Top to Bottom", direction: :top_to_bottom }, { name: "Bottom to Top", direction: :bottom_to_top }, ] @direction_index = 0 @highlight_spacing_configs = [ { name: "When Selected", spacing: :when_selected }, { name: "Always", spacing: :always }, { name: "Never", spacing: :never }, ] @highlight_spacing_index = 0 @repeat_modes = [ { name: "Off", repeat: false }, { name: "On", repeat: true }, ] @repeat_index = 0 @scroll_padding_configs = [ { name: "None", padding: nil }, { name: "1 item", padding: 1 }, { name: "2 items", padding: 2 }, ] @scroll_padding_index = 0 end # Runs the demo application. # # This method enters the terminal alternate screen, starts the main loop, and handles cleanup on exit. def run RatatuiRuby.run do |tui| @tui = tui # Initialize styles that require @tui @highlight_styles = [ { name: "Blue Bold", style: @tui.style(fg: :blue, modifiers: [:bold]) }, { name: "Yellow on Black", style: @tui.style(fg: :yellow, bg: :black) }, { name: "Green Italic", style: @tui.style(fg: :green, modifiers: [:italic]) }, { name: "White Reversed", style: @tui.style(fg: :white, modifiers: [:reversed]) }, { name: "Cyan Bold", style: @tui.style(fg: :cyan, modifiers: [:bold]) }, ] @highlight_style_index = 0 @base_styles = [ { name: "None", style: nil }, { name: "Dark Gray", style: @tui.style(fg: :dark_gray) }, { name: "White on Black", style: @tui.style(fg: :white, bg: :black) }, ] @base_style_index = 0 @hotkey_style = @tui.style(modifiers: [:bold, :underlined]) loop do render break if handle_input == :quit sleep 0.05 end end end # :nodoc: private def render items = @item_sets[@item_set_index][:items] selection_label = @selected_index.nil? ? "none" : @selected_index.to_s direction_config = @direction_configs[@direction_index] spacing_config = @highlight_spacing_configs[@highlight_spacing_index] repeat_config = @repeat_modes[@repeat_index] highlight_style_config = @highlight_styles[@highlight_style_index] highlight_symbol = @highlight_symbol_names[@highlight_symbol_index] base_style_config = @base_styles[@base_style_index] scroll_padding_config = @scroll_padding_configs[@scroll_padding_index] @tui.draw do |frame| # Split into main content and control panel main_area, control_area = @tui.layout_split( frame.area, direction: :vertical, constraints: [ @tui.constraint_fill(1), @tui.constraint_length(7), ] ) # Split main content into title and list title_area, list_area = @tui.layout_split( main_area, direction: :vertical, constraints: [ @tui.constraint_length(1), @tui.constraint_fill(1), ] ) # Render title title = @tui.paragraph(text: "List Widget Demo - Interactive Attribute Cycling") frame.render_widget(title, title_area) # Render list list = @tui.list( items:, selected_index: @selected_index, style: base_style_config[:style], highlight_style: highlight_style_config[:style], highlight_symbol:, repeat_highlight_symbol: repeat_config[:repeat], highlight_spacing: spacing_config[:spacing], direction: direction_config[:direction], scroll_padding: scroll_padding_config[:padding], block: @tui.block( title: "#{@item_sets[@item_set_index][:name]} (Selection: #{selection_label})", borders: [:all] ) ) frame.render_widget(list, list_area) # Render control panel control_panel = @tui.block( title: "Controls", borders: [:all], children: [ @tui.paragraph( text: [ @tui.text_line(spans: [ @tui.text_span(content: "i", style: @hotkey_style), @tui.text_span(content: ": Items "), @tui.text_span(content: "↑/↓", style: @hotkey_style), @tui.text_span(content: ": Navigate "), @tui.text_span(content: "x", style: @hotkey_style), @tui.text_span(content: ": Select "), @tui.text_span(content: "h", style: @hotkey_style), @tui.text_span(content: ": Highlight (#{highlight_style_config[:name]})"), ]), @tui.text_line(spans: [ @tui.text_span(content: "y", style: @hotkey_style), @tui.text_span(content: ": Symbol (#{highlight_symbol}) "), @tui.text_span(content: "d", style: @hotkey_style), @tui.text_span(content: ": Direction (#{direction_config[:name]})"), ]), @tui.text_line(spans: [ @tui.text_span(content: "s", style: @hotkey_style), @tui.text_span(content: ": Spacing (#{spacing_config[:name]}) "), @tui.text_span(content: "p", style: @hotkey_style), @tui.text_span(content: ": Scroll Padding (#{scroll_padding_config[:name]})"), ]), @tui.text_line(spans: [ @tui.text_span(content: "b", style: @hotkey_style), @tui.text_span(content: ": Base (#{base_style_config[:name]}) "), @tui.text_span(content: "r", style: @hotkey_style), @tui.text_span(content: ": Repeat (#{repeat_config[:name]}) "), @tui.text_span(content: "q", style: @hotkey_style), @tui.text_span(content: ": Quit"), ]), ] ), ] ) frame.render_widget(control_panel, control_area) end end # :nodoc: private def handle_input case @tui.poll_event in { type: :key, code: "q" } | { type: :key, code: "c", modifiers: ["ctrl"] } :quit in type: :key, code: "i" @item_set_index = (@item_set_index + 1) % @item_sets.size @selected_index = nil in type: :key, code: "up" items = @item_sets[@item_set_index][:items] @selected_index = (@selected_index || 0) - 1 @selected_index = items.size - 1 if @selected_index.negative? in type: :key, code: "down" items = @item_sets[@item_set_index][:items] @selected_index = ((@selected_index || -1) + 1) % items.size in type: :key, code: "x" @selected_index = @selected_index.nil? ? 0 : nil in type: :key, code: "h" @highlight_style_index = (@highlight_style_index + 1) % @highlight_styles.size in type: :key, code: "y" @highlight_symbol_index = (@highlight_symbol_index + 1) % @highlight_symbol_names.size in type: :key, code: "d" @direction_index = (@direction_index + 1) % @direction_configs.size in type: :key, code: "s" @highlight_spacing_index = (@highlight_spacing_index + 1) % @highlight_spacing_configs.size in type: :key, code: "b" @base_style_index = (@base_style_index + 1) % @base_styles.size in type: :key, code: "r" @repeat_index = (@repeat_index + 1) % @repeat_modes.size in type: :key, code: "p" @scroll_padding_index = (@scroll_padding_index + 1) % @scroll_padding_configs.size else nil end end end WidgetListDemo.new.run if __FILE__ == $PROGRAM_NAME