ratatui_ruby 0.1.0 → 0.3.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.
Files changed (127) hide show
  1. checksums.yaml +4 -4
  2. data/.builds/ruby-3.2.yml +52 -0
  3. data/.builds/ruby-3.3.yml +52 -0
  4. data/.builds/ruby-3.4.yml +52 -0
  5. data/.builds/ruby-4.0.0.yml +53 -0
  6. data/.pre-commit-config.yaml +9 -2
  7. data/AGENTS.md +53 -5
  8. data/CHANGELOG.md +51 -1
  9. data/README.md +38 -18
  10. data/REUSE.toml +5 -0
  11. data/Rakefile +3 -100
  12. data/{docs → doc}/contributors/index.md +2 -1
  13. data/doc/custom.css +8 -0
  14. data/doc/images/examples-calendar_demo.rb.png +0 -0
  15. data/doc/images/examples-chart_demo.rb.png +0 -0
  16. data/doc/images/examples-custom_widget.rb.png +0 -0
  17. data/doc/images/examples-list_styles.rb.png +0 -0
  18. data/doc/images/examples-popup_demo.rb.gif +0 -0
  19. data/doc/images/examples-quickstart_lifecycle.rb.png +0 -0
  20. data/doc/images/examples-scroll_text.rb.png +0 -0
  21. data/doc/images/examples-stock_ticker.rb.png +0 -0
  22. data/doc/images/examples-table_select.rb.png +0 -0
  23. data/{docs → doc}/index.md +1 -1
  24. data/{docs → doc}/quickstart.md +81 -11
  25. data/examples/analytics.rb +2 -1
  26. data/examples/calendar_demo.rb +55 -0
  27. data/examples/chart_demo.rb +84 -0
  28. data/examples/custom_widget.rb +43 -0
  29. data/examples/list_styles.rb +66 -0
  30. data/examples/login_form.rb +2 -1
  31. data/examples/popup_demo.rb +105 -0
  32. data/examples/quickstart_dsl.rb +30 -0
  33. data/examples/quickstart_lifecycle.rb +40 -0
  34. data/examples/readme_usage.rb +21 -0
  35. data/examples/scroll_text.rb +74 -0
  36. data/examples/stock_ticker.rb +13 -5
  37. data/examples/system_monitor.rb +2 -1
  38. data/examples/table_select.rb +70 -0
  39. data/examples/test_calendar_demo.rb +66 -0
  40. data/examples/test_list_styles.rb +61 -0
  41. data/examples/test_popup_demo.rb +62 -0
  42. data/examples/test_scroll_text.rb +130 -0
  43. data/examples/test_table_select.rb +37 -0
  44. data/ext/ratatui_ruby/.cargo/config.toml +5 -0
  45. data/ext/ratatui_ruby/Cargo.lock +260 -50
  46. data/ext/ratatui_ruby/Cargo.toml +5 -4
  47. data/ext/ratatui_ruby/extconf.rb +1 -1
  48. data/ext/ratatui_ruby/src/buffer.rs +54 -0
  49. data/ext/ratatui_ruby/src/events.rs +115 -107
  50. data/ext/ratatui_ruby/src/lib.rs +15 -6
  51. data/ext/ratatui_ruby/src/rendering.rs +18 -1
  52. data/ext/ratatui_ruby/src/style.rs +2 -1
  53. data/ext/ratatui_ruby/src/terminal.rs +27 -24
  54. data/ext/ratatui_ruby/src/widgets/calendar.rs +82 -0
  55. data/ext/ratatui_ruby/src/widgets/canvas.rs +1 -2
  56. data/ext/ratatui_ruby/src/widgets/center.rs +0 -2
  57. data/ext/ratatui_ruby/src/widgets/chart.rs +260 -0
  58. data/ext/ratatui_ruby/src/widgets/clear.rs +37 -0
  59. data/ext/ratatui_ruby/src/widgets/cursor.rs +1 -1
  60. data/ext/ratatui_ruby/src/widgets/layout.rs +2 -1
  61. data/ext/ratatui_ruby/src/widgets/list.rs +44 -5
  62. data/ext/ratatui_ruby/src/widgets/mod.rs +3 -1
  63. data/ext/ratatui_ruby/src/widgets/overlay.rs +2 -1
  64. data/ext/ratatui_ruby/src/widgets/paragraph.rs +10 -0
  65. data/ext/ratatui_ruby/src/widgets/table.rs +25 -6
  66. data/ext/ratatui_ruby/src/widgets/tabs.rs +2 -1
  67. data/lib/ratatui_ruby/dsl.rb +64 -0
  68. data/lib/ratatui_ruby/schema/calendar.rb +26 -0
  69. data/lib/ratatui_ruby/schema/chart.rb +81 -0
  70. data/lib/ratatui_ruby/schema/clear.rb +83 -0
  71. data/lib/ratatui_ruby/schema/list.rb +8 -2
  72. data/lib/ratatui_ruby/schema/paragraph.rb +7 -4
  73. data/lib/ratatui_ruby/schema/rect.rb +24 -0
  74. data/lib/ratatui_ruby/schema/table.rb +8 -2
  75. data/lib/ratatui_ruby/version.rb +1 -1
  76. data/lib/ratatui_ruby.rb +24 -2
  77. data/mise.toml +8 -0
  78. data/sig/ratatui_ruby/buffer.rbs +11 -0
  79. data/sig/ratatui_ruby/schema/calendar.rbs +13 -0
  80. data/sig/ratatui_ruby/schema/{line_chart.rbs → chart.rbs} +20 -1
  81. data/sig/ratatui_ruby/schema/list.rbs +4 -1
  82. data/sig/ratatui_ruby/schema/rect.rbs +14 -0
  83. data/tasks/bump/cargo_lockfile.rb +19 -0
  84. data/tasks/bump/changelog.rb +37 -0
  85. data/tasks/bump/comparison_links.rb +41 -0
  86. data/tasks/bump/header.rb +30 -0
  87. data/tasks/bump/history.rb +30 -0
  88. data/tasks/bump/manifest.rb +31 -0
  89. data/tasks/bump/ruby_gem.rb +35 -0
  90. data/tasks/bump/sem_ver.rb +34 -0
  91. data/tasks/bump/unreleased_section.rb +38 -0
  92. data/tasks/bump.rake +49 -0
  93. data/tasks/doc.rake +25 -0
  94. data/tasks/extension.rake +12 -0
  95. data/tasks/lint.rake +49 -0
  96. data/tasks/rdoc_config.rb +15 -0
  97. data/tasks/resources/build.yml.erb +65 -0
  98. data/tasks/resources/index.html.erb +38 -0
  99. data/tasks/resources/rubies.yml +7 -0
  100. data/tasks/sourcehut.rake +38 -0
  101. data/tasks/test.rake +31 -0
  102. data/tasks/website/index_page.rb +28 -0
  103. data/tasks/website/version.rb +117 -0
  104. data/tasks/website/version_menu.rb +68 -0
  105. data/tasks/website/versioned_documentation.rb +49 -0
  106. data/tasks/website/website.rb +53 -0
  107. data/tasks/website.rake +26 -0
  108. metadata +119 -28
  109. data/.build.yml +0 -34
  110. data/.ruby-version +0 -1
  111. data/CODE_OF_CONDUCT.md +0 -30
  112. data/CONTRIBUTING.md +0 -40
  113. data/docs/images/examples-stock_ticker.rb.png +0 -0
  114. data/ext/ratatui_ruby/src/widgets/linechart.rs +0 -154
  115. data/lib/ratatui_ruby/schema/line_chart.rb +0 -41
  116. /data/{docs → doc}/application_testing.md +0 -0
  117. /data/{docs → doc}/contributors/design/ruby_frontend.md +0 -0
  118. /data/{docs → doc}/contributors/design/rust_backend.md +0 -0
  119. /data/{docs → doc}/contributors/design.md +0 -0
  120. /data/{docs → doc}/images/examples-analytics.rb.png +0 -0
  121. /data/{docs → doc}/images/examples-box_demo.rb.png +0 -0
  122. /data/{docs → doc}/images/examples-dashboard.rb.png +0 -0
  123. /data/{docs → doc}/images/examples-login_form.rb.png +0 -0
  124. /data/{docs → doc}/images/examples-map_demo.rb.png +0 -0
  125. /data/{docs → doc}/images/examples-mouse_events.rb.png +0 -0
  126. /data/{docs → doc}/images/examples-scrollbar_demo.rb.png +0 -0
  127. /data/{docs → doc}/images/examples-system_monitor.rb.png +0 -0
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
5
+ # SPDX-License-Identifier: AGPL-3.0-or-later
6
+
7
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
8
+ require "ratatui_ruby"
9
+
10
+ # Demo: Scrollable Paragraph
11
+ # Shows how to scroll through long text content using arrow keys
12
+ class ScrollTextDemo
13
+ def initialize
14
+ @scroll_x = 0
15
+ @scroll_y = 0
16
+ @lines = (1..100).map { |i| "Line #{i}: This is a long line of text that can be scrolled horizontally" }
17
+ end
18
+
19
+ def run
20
+ RatatuiRuby.init_terminal
21
+ begin
22
+ loop do
23
+ draw
24
+ break if handle_input == :quit
25
+ end
26
+ ensure
27
+ RatatuiRuby.restore_terminal
28
+ end
29
+ end
30
+
31
+ def render
32
+ draw
33
+ end
34
+
35
+ def handle_input
36
+ event = RatatuiRuby.poll_event
37
+ return nil unless event
38
+
39
+ if event[:type] == :key
40
+ case event[:code]
41
+ when "up"
42
+ @scroll_y = [@scroll_y - 1, 0].max
43
+ when "down"
44
+ @scroll_y = [@scroll_y + 1, @lines.length].min
45
+ when "left"
46
+ @scroll_x = [@scroll_x - 1, 0].max
47
+ when "right"
48
+ @scroll_x = [@scroll_x + 1, 100].min
49
+ when "q"
50
+ return :quit
51
+ end
52
+ end
53
+ nil
54
+ end
55
+
56
+ private
57
+
58
+ def draw
59
+ text = @lines.join("\n")
60
+
61
+ paragraph = RatatuiRuby::Paragraph.new(
62
+ text: text,
63
+ scroll: [@scroll_y, @scroll_x],
64
+ block: RatatuiRuby::Block.new(
65
+ title: "Scrollable Text (X: #{@scroll_x}, Y: #{@scroll_y}) - Arrow keys to scroll, 'q' to quit",
66
+ borders: [:all]
67
+ )
68
+ )
69
+
70
+ RatatuiRuby.draw(paragraph)
71
+ end
72
+ end
73
+
74
+ ScrollTextDemo.new.run if __FILE__ == $PROGRAM_NAME
@@ -3,7 +3,8 @@
3
3
  # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
4
  # SPDX-License-Identifier: AGPL-3.0-or-later
5
5
 
6
- require_relative "../lib/ratatui_ruby"
6
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
7
+ require "ratatui_ruby"
7
8
 
8
9
  # Simulate a stock ticker
9
10
  class StockTickerApp
@@ -58,14 +59,21 @@ class StockTickerApp
58
59
  style: RatatuiRuby::Style.new(fg: :cyan),
59
60
  block: RatatuiRuby::Block.new(title: "Network Activity", borders: :all)
60
61
  ),
61
- RatatuiRuby::LineChart.new(
62
+ RatatuiRuby::Chart.new(
62
63
  datasets: [
63
64
  RatatuiRuby::Dataset.new(name: "RBY", data: @ruby_stock, color: :green),
64
65
  RatatuiRuby::Dataset.new(name: "RST", data: @rust_stock, color: :red),
65
66
  ],
66
- x_labels: [@counter.to_s],
67
- y_labels: %w[0 50 100],
68
- y_bounds: [0.0, 100.0],
67
+ x_axis: RatatuiRuby::Axis.new(
68
+ title: "Time",
69
+ bounds: [((@counter > 100) ? @counter - 100.0 : 0.0), @counter.to_f],
70
+ labels: [@counter.to_s]
71
+ ),
72
+ y_axis: RatatuiRuby::Axis.new(
73
+ title: "Price",
74
+ bounds: [0.0, 100.0],
75
+ labels: %w[0 50 100]
76
+ ),
69
77
  block: RatatuiRuby::Block.new(title: "Stock Ticker", borders: :all),
70
78
  ),
71
79
  ]
@@ -3,7 +3,8 @@
3
3
  # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
4
  # SPDX-License-Identifier: AGPL-3.0-or-later
5
5
 
6
- require_relative "../lib/ratatui_ruby"
6
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
7
+ require "ratatui_ruby"
7
8
 
8
9
  class SystemMonitorApp
9
10
  def initialize
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
5
+ # SPDX-License-Identifier: AGPL-3.0-or-later
6
+
7
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
8
+ require "bundler/setup"
9
+ require "ratatui_ruby"
10
+
11
+ # Sample process data
12
+ PROCESSES = [
13
+ { pid: 1234, name: "ruby", cpu: 15.2 },
14
+ { pid: 5678, name: "postgres", cpu: 8.7 },
15
+ { pid: 9012, name: "nginx", cpu: 3.1 },
16
+ { pid: 3456, name: "redis", cpu: 12.4 },
17
+ { pid: 7890, name: "sidekiq", cpu: 22.8 },
18
+ { pid: 2345, name: "webpack", cpu: 45.3 },
19
+ { pid: 6789, name: "node", cpu: 18.9 }
20
+ ].freeze
21
+
22
+ selected_index = 0
23
+
24
+ RatatuiRuby.init_terminal
25
+
26
+ begin
27
+ loop do
28
+ # Create table rows from process data
29
+ rows = PROCESSES.map { |p| [p[:pid].to_s, p[:name], "#{p[:cpu]}%"] }
30
+
31
+ # Define column widths
32
+ widths = [
33
+ RatatuiRuby::Constraint.length(8),
34
+ RatatuiRuby::Constraint.length(15),
35
+ RatatuiRuby::Constraint.length(10)
36
+ ]
37
+
38
+ # Create highlight style (yellow text)
39
+ highlight_style = RatatuiRuby::Style.new(fg: :yellow)
40
+
41
+ # Create table with selection
42
+ table = RatatuiRuby::Table.new(
43
+ header: ["PID", "Name", "CPU"],
44
+ rows: rows,
45
+ widths: widths,
46
+ selected_row: selected_index,
47
+ highlight_style: highlight_style,
48
+ highlight_symbol: "> ",
49
+ block: RatatuiRuby::Block.new(title: "Process Monitor (↑/↓ to select, q to quit)", borders: :all)
50
+ )
51
+
52
+ # Draw the table
53
+ RatatuiRuby.draw(table)
54
+
55
+ # Handle events
56
+ event = RatatuiRuby.poll_event
57
+ next unless event
58
+
59
+ case event[:code]
60
+ when "q"
61
+ break
62
+ when "down", "j"
63
+ selected_index = (selected_index + 1) % PROCESSES.length
64
+ when "up", "k"
65
+ selected_index = (selected_index - 1) % PROCESSES.length
66
+ end
67
+ end
68
+ ensure
69
+ RatatuiRuby.restore_terminal
70
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+
6
+ require "minitest/autorun"
7
+ require "ratatui_ruby"
8
+
9
+ module CalendarDemo
10
+ class TestCalendarDemo < Minitest::Test
11
+ def setup
12
+ RatatuiRuby.init_test_terminal(40, 20)
13
+ end
14
+
15
+ def test_demo_renders
16
+ # We can't easily test the loop in run, but we can test the rendering logic
17
+ now = Time.now
18
+ calendar = RatatuiRuby::Calendar.new(
19
+ year: now.year,
20
+ month: now.month,
21
+ header_style: RatatuiRuby::Style.new(fg: "yellow", modifiers: [:bold]),
22
+ block: RatatuiRuby::Block.new(title: " Calendar (q = quit) ", borders: [:all]),
23
+ )
24
+
25
+ # Constrain the calendar to 24x10 characters
26
+ view = RatatuiRuby::Layout.new(
27
+ direction: :vertical,
28
+ constraints: [
29
+ RatatuiRuby::Constraint.length(10),
30
+ RatatuiRuby::Constraint.min(0),
31
+ ],
32
+ children: [
33
+ RatatuiRuby::Layout.new(
34
+ direction: :horizontal,
35
+ constraints: [
36
+ RatatuiRuby::Constraint.length(24),
37
+ RatatuiRuby::Constraint.min(0),
38
+ ],
39
+ children: [calendar, nil],
40
+ ),
41
+ nil,
42
+ ],
43
+ )
44
+
45
+ RatatuiRuby.draw(view)
46
+ content = RatatuiRuby.get_buffer_content
47
+
48
+ assert_match(/Calendar \(q = quit\)/, content)
49
+ assert_match(/#{now.year}/, content)
50
+
51
+ # Verify the size constraint: the terminal is 40x20
52
+ # But the layout should be constrained to 24x10.
53
+ lines = content.split("\n")
54
+
55
+ # First 10 rows: first 24 chars can have content, 24..39 should be empty
56
+ 10.times do |i|
57
+ assert_equal " " * 16, lines[i][24..39], "Row #{i} should be empty after column 24"
58
+ end
59
+
60
+ # Rows 10..19 should be completely empty
61
+ (10..19).each do |i|
62
+ assert_equal " " * 40, lines[i], "Row #{i} should be completely empty"
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+
6
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
7
+ require "ratatui_ruby"
8
+ require "ratatui_ruby/test_helper"
9
+ require "minitest/autorun"
10
+ require_relative "list_styles"
11
+
12
+ class Minitest::Test
13
+ include RatatuiRuby::TestHelper
14
+ end
15
+
16
+ class TestListStyles < Minitest::Test
17
+ def setup
18
+ @app = ListStylesApp.new
19
+ end
20
+
21
+ def test_render_initial_state
22
+ with_test_terminal(50, 20) do
23
+ @app.render
24
+
25
+ assert buffer_content.any? { |line| line.include?(">> Item 1") }
26
+ assert buffer_content.any? { |line| line.include?(" Item 2") }
27
+ end
28
+ end
29
+
30
+ def test_navigation_down
31
+ inject_event("key", { code: "down" })
32
+ @app.handle_input
33
+
34
+ with_test_terminal(50, 20) do
35
+ @app.render
36
+ assert buffer_content.any? { |line| line.include?(" Item 1") }
37
+ assert buffer_content.any? { |line| line.include?(">> Item 2") }
38
+ end
39
+ end
40
+
41
+ def test_navigation_up
42
+ # Move down to Item 2
43
+ inject_event("key", { code: "down" })
44
+ @app.handle_input
45
+
46
+ # Move up back to Item 1
47
+ inject_event("key", { code: "up" })
48
+ @app.handle_input
49
+
50
+ with_test_terminal(50, 20) do
51
+ @app.render
52
+ assert buffer_content.any? { |line| line.include?(">> Item 1") }
53
+ end
54
+ end
55
+
56
+ def test_quit
57
+ inject_event("key", { code: "q" })
58
+ status = @app.handle_input
59
+ assert_equal :quit, status
60
+ end
61
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+
6
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
7
+ require "ratatui_ruby"
8
+ require "ratatui_ruby/test_helper"
9
+ require "minitest/autorun"
10
+ require_relative "popup_demo"
11
+
12
+ class Minitest::Test
13
+ include RatatuiRuby::TestHelper
14
+ end
15
+
16
+ class TestPopupDemo < Minitest::Test
17
+ def setup
18
+ @app = PopupDemo.new
19
+ end
20
+
21
+ def test_render_initial_state
22
+ with_test_terminal(60, 20) do
23
+ @app.render
24
+
25
+ # Should have background text
26
+ assert buffer_content.any? { |line| line.include?("BACKGROUND RED") }
27
+
28
+ # Should have popup with "Clear is DISABLED" message
29
+ assert buffer_content.any? { |line| line.include?("Clear is DISABLED") }
30
+ assert buffer_content.any? { |line| line.include?("Style Bleed: Popup is RED!") }
31
+ end
32
+ end
33
+
34
+ def test_toggle_clear
35
+ with_test_terminal(60, 20) do
36
+ # Initial state: Clear disabled
37
+ @app.render
38
+ assert buffer_content.any? { |line| line.include?("Clear is DISABLED") }
39
+
40
+ # Toggle Clear on
41
+ inject_event("key", { code: " " })
42
+ @app.handle_input
43
+
44
+ @app.render
45
+ assert buffer_content.any? { |line| line.include?("Clear is ENABLED") }
46
+ assert buffer_content.any? { |line| line.include?("Resets background to default") }
47
+
48
+ # Toggle Clear off
49
+ inject_event("key", { code: " " })
50
+ @app.handle_input
51
+
52
+ @app.render
53
+ assert buffer_content.any? { |line| line.include?("Clear is DISABLED") }
54
+ end
55
+ end
56
+
57
+ def test_quit
58
+ inject_event("key", { code: "q" })
59
+ status = @app.handle_input
60
+ assert_equal :quit, status
61
+ end
62
+ end
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
5
+ # SPDX-License-Identifier: AGPL-3.0-or-later
6
+
7
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
8
+ require "minitest/autorun"
9
+ require "ratatui_ruby/test_helper"
10
+
11
+ # Load the demo
12
+ require_relative "./scroll_text"
13
+
14
+ class TestScrollText < Minitest::Test
15
+ include RatatuiRuby::TestHelper
16
+
17
+ def setup
18
+ @demo = ScrollTextDemo.new
19
+ end
20
+
21
+ def test_demo_initialization
22
+ assert_instance_of ScrollTextDemo, @demo
23
+ end
24
+
25
+ def test_initial_rendering
26
+ with_test_terminal(60, 10) do
27
+ @demo.render
28
+ content = buffer_content
29
+
30
+ # Should show Line 1 at the top (inside the block border)
31
+ assert content[1].include?("Line 1")
32
+ # Should show title with scroll position 0, 0
33
+ assert content[0].include?("X: 0, Y: 0")
34
+ end
35
+ end
36
+
37
+ def test_scroll_down
38
+ inject_event("key", { code: "down" })
39
+ @demo.handle_input
40
+
41
+ with_test_terminal(60, 10) do
42
+ @demo.render
43
+ content = buffer_content
44
+
45
+ # After scrolling down once, Line 2 should be at the top
46
+ assert content[1].include?("Line 2"), "Expected Line 2 at top, got: #{content[1]}"
47
+ # Line 1 should not be visible (scrolled off)
48
+ refute content.any? { |line| line.include?("Line 1") }, "Line 1 should be scrolled off"
49
+ # Should show Y scroll position 1
50
+ assert content[0].include?("Y: 1")
51
+ end
52
+ end
53
+
54
+ def test_scroll_right
55
+ inject_event("key", { code: "right" })
56
+ @demo.handle_input
57
+
58
+ with_test_terminal(60, 10) do
59
+ @demo.render
60
+ content = buffer_content
61
+
62
+ # After scrolling right once, first character should be cut off
63
+ # "Line 1:" becomes "ine 1:"
64
+ assert content[1].include?("ine 1:"), "Expected horizontal scroll, got: #{content[1]}"
65
+ # Should show X scroll position 1
66
+ assert content[0].include?("X: 1")
67
+ end
68
+ end
69
+
70
+ def test_scroll_left_at_edge
71
+ # Try to scroll left when already at x=0
72
+ inject_event("key", { code: "left" })
73
+ @demo.handle_input
74
+
75
+ with_test_terminal(60, 10) do
76
+ @demo.render
77
+ content = buffer_content
78
+
79
+ # Should still show full "Line 1:" (can't scroll left past 0)
80
+ assert content[1].include?("Line 1:")
81
+ # Should still show X scroll position 0
82
+ assert content[0].include?("X: 0")
83
+ end
84
+ end
85
+
86
+ def test_scroll_up_at_top
87
+ # Try to scroll up when already at top (should stay at 0)
88
+ inject_event("key", { code: "up" })
89
+ @demo.handle_input
90
+
91
+ with_test_terminal(60, 10) do
92
+ @demo.render
93
+ content = buffer_content
94
+
95
+ # Should still show Line 1 at top (can't scroll above 0)
96
+ assert content[1].include?("Line 1")
97
+ # Should still show Y scroll position 0
98
+ assert content[0].include?("Y: 0")
99
+ end
100
+ end
101
+
102
+ def test_scroll_both_axes
103
+ # Scroll down 2 times and right 3 times
104
+ 2.times do
105
+ inject_event("key", { code: "down" })
106
+ @demo.handle_input
107
+ end
108
+ 3.times do
109
+ inject_event("key", { code: "right" })
110
+ @demo.handle_input
111
+ end
112
+
113
+ with_test_terminal(60, 10) do
114
+ @demo.render
115
+ content = buffer_content
116
+
117
+ # After scrolling down 2 and right 3, Line 3 should be at top with first 3 chars cut
118
+ # "Line 3:" becomes " 3:"
119
+ assert content[1].include?(" 3:"), "Expected Line 3 scrolled right by 3, got: #{content[1]}"
120
+ # Should show scroll position X: 3, Y: 2
121
+ assert content[0].include?("X: 3, Y: 2")
122
+ end
123
+ end
124
+
125
+ def test_quit
126
+ inject_event("key", { code: "q" })
127
+ status = @demo.handle_input
128
+ assert_equal :quit, status
129
+ end
130
+ end
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
5
+ # SPDX-License-Identifier: AGPL-3.0-or-later
6
+
7
+ require "test_helper"
8
+
9
+ # Smoke test to ensure the table_select example can be loaded and instantiated
10
+ class TestTableSelect < Minitest::Test
11
+ def test_table_select_smoke
12
+ # Verify the example can create a table with selection
13
+ rows = [["1", "test", "10%"]]
14
+ widths = [
15
+ RatatuiRuby::Constraint.length(8),
16
+ RatatuiRuby::Constraint.length(15),
17
+ RatatuiRuby::Constraint.length(10)
18
+ ]
19
+
20
+ table = RatatuiRuby::Table.new(
21
+ header: ["PID", "Name", "CPU"],
22
+ rows: rows,
23
+ widths: widths,
24
+ selected_row: 0,
25
+ highlight_style: RatatuiRuby::Style.new(fg: :yellow),
26
+ highlight_symbol: "> ",
27
+ block: RatatuiRuby::Block.new(title: "Test", borders: :all)
28
+ )
29
+
30
+ # Verify it can be drawn
31
+ with_test_terminal(40, 10) do
32
+ RatatuiRuby.draw(table)
33
+ # Just verify it doesn't crash
34
+ assert true
35
+ end
36
+ end
37
+ end
@@ -6,3 +6,8 @@ rustflags = ["-C", "link-arg=-undefined", "-C", "link-arg=dynamic_lookup"]
6
6
 
7
7
  [target.x86_64-apple-darwin]
8
8
  rustflags = ["-C", "link-arg=-undefined", "-C", "link-arg=dynamic_lookup"]
9
+
10
+ # Force dynamic linking on Alpine (musl) to fix "Dynamic loading not supported"
11
+ # errors when bindgen tries to load libclang.
12
+ [target.'cfg(target_env = "musl")']
13
+ rustflags = ["-C", "target-feature=-crt-static"]