ratatui_ruby 0.1.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 (119) hide show
  1. checksums.yaml +7 -0
  2. data/.build.yml +34 -0
  3. data/.pre-commit-config.yaml +9 -0
  4. data/.rubocop.yml +8 -0
  5. data/.ruby-version +1 -0
  6. data/AGENTS.md +119 -0
  7. data/CHANGELOG.md +15 -0
  8. data/CODE_OF_CONDUCT.md +30 -0
  9. data/CONTRIBUTING.md +40 -0
  10. data/LICENSE +15 -0
  11. data/LICENSES/AGPL-3.0-or-later.txt +661 -0
  12. data/LICENSES/BSD-2-Clause.txt +9 -0
  13. data/LICENSES/CC-BY-SA-4.0.txt +427 -0
  14. data/LICENSES/CC0-1.0.txt +121 -0
  15. data/LICENSES/MIT.txt +21 -0
  16. data/README.md +86 -0
  17. data/REUSE.toml +17 -0
  18. data/Rakefile +108 -0
  19. data/docs/application_testing.md +96 -0
  20. data/docs/contributors/design/ruby_frontend.md +100 -0
  21. data/docs/contributors/design/rust_backend.md +61 -0
  22. data/docs/contributors/design.md +11 -0
  23. data/docs/contributors/index.md +16 -0
  24. data/docs/images/examples-analytics.rb.png +0 -0
  25. data/docs/images/examples-box_demo.rb.png +0 -0
  26. data/docs/images/examples-dashboard.rb.png +0 -0
  27. data/docs/images/examples-login_form.rb.png +0 -0
  28. data/docs/images/examples-map_demo.rb.png +0 -0
  29. data/docs/images/examples-mouse_events.rb.png +0 -0
  30. data/docs/images/examples-scrollbar_demo.rb.png +0 -0
  31. data/docs/images/examples-stock_ticker.rb.png +0 -0
  32. data/docs/images/examples-system_monitor.rb.png +0 -0
  33. data/docs/index.md +18 -0
  34. data/docs/quickstart.md +126 -0
  35. data/examples/analytics.rb +87 -0
  36. data/examples/box_demo.rb +71 -0
  37. data/examples/dashboard.rb +72 -0
  38. data/examples/login_form.rb +114 -0
  39. data/examples/map_demo.rb +58 -0
  40. data/examples/mouse_events.rb +95 -0
  41. data/examples/scrollbar_demo.rb +75 -0
  42. data/examples/stock_ticker.rb +85 -0
  43. data/examples/system_monitor.rb +93 -0
  44. data/examples/test_analytics.rb +65 -0
  45. data/examples/test_box_demo.rb +38 -0
  46. data/examples/test_dashboard.rb +38 -0
  47. data/examples/test_login_form.rb +63 -0
  48. data/examples/test_map_demo.rb +100 -0
  49. data/examples/test_stock_ticker.rb +39 -0
  50. data/examples/test_system_monitor.rb +40 -0
  51. data/ext/ratatui_ruby/.cargo/config.toml +8 -0
  52. data/ext/ratatui_ruby/.gitignore +4 -0
  53. data/ext/ratatui_ruby/Cargo.lock +698 -0
  54. data/ext/ratatui_ruby/Cargo.toml +16 -0
  55. data/ext/ratatui_ruby/extconf.rb +12 -0
  56. data/ext/ratatui_ruby/src/events.rs +279 -0
  57. data/ext/ratatui_ruby/src/lib.rs +105 -0
  58. data/ext/ratatui_ruby/src/rendering.rs +31 -0
  59. data/ext/ratatui_ruby/src/style.rs +149 -0
  60. data/ext/ratatui_ruby/src/terminal.rs +131 -0
  61. data/ext/ratatui_ruby/src/widgets/barchart.rs +73 -0
  62. data/ext/ratatui_ruby/src/widgets/block.rs +12 -0
  63. data/ext/ratatui_ruby/src/widgets/canvas.rs +146 -0
  64. data/ext/ratatui_ruby/src/widgets/center.rs +81 -0
  65. data/ext/ratatui_ruby/src/widgets/cursor.rs +29 -0
  66. data/ext/ratatui_ruby/src/widgets/gauge.rs +50 -0
  67. data/ext/ratatui_ruby/src/widgets/layout.rs +82 -0
  68. data/ext/ratatui_ruby/src/widgets/linechart.rs +154 -0
  69. data/ext/ratatui_ruby/src/widgets/list.rs +62 -0
  70. data/ext/ratatui_ruby/src/widgets/mod.rs +18 -0
  71. data/ext/ratatui_ruby/src/widgets/overlay.rs +20 -0
  72. data/ext/ratatui_ruby/src/widgets/paragraph.rs +56 -0
  73. data/ext/ratatui_ruby/src/widgets/scrollbar.rs +68 -0
  74. data/ext/ratatui_ruby/src/widgets/sparkline.rs +59 -0
  75. data/ext/ratatui_ruby/src/widgets/table.rs +117 -0
  76. data/ext/ratatui_ruby/src/widgets/tabs.rs +51 -0
  77. data/lib/ratatui_ruby/output.rb +7 -0
  78. data/lib/ratatui_ruby/schema/bar_chart.rb +28 -0
  79. data/lib/ratatui_ruby/schema/block.rb +23 -0
  80. data/lib/ratatui_ruby/schema/canvas.rb +62 -0
  81. data/lib/ratatui_ruby/schema/center.rb +19 -0
  82. data/lib/ratatui_ruby/schema/constraint.rb +33 -0
  83. data/lib/ratatui_ruby/schema/cursor.rb +17 -0
  84. data/lib/ratatui_ruby/schema/gauge.rb +24 -0
  85. data/lib/ratatui_ruby/schema/layout.rb +22 -0
  86. data/lib/ratatui_ruby/schema/line_chart.rb +41 -0
  87. data/lib/ratatui_ruby/schema/list.rb +22 -0
  88. data/lib/ratatui_ruby/schema/overlay.rb +15 -0
  89. data/lib/ratatui_ruby/schema/paragraph.rb +37 -0
  90. data/lib/ratatui_ruby/schema/scrollbar.rb +33 -0
  91. data/lib/ratatui_ruby/schema/sparkline.rb +24 -0
  92. data/lib/ratatui_ruby/schema/style.rb +31 -0
  93. data/lib/ratatui_ruby/schema/table.rb +24 -0
  94. data/lib/ratatui_ruby/schema/tabs.rb +22 -0
  95. data/lib/ratatui_ruby/test_helper.rb +75 -0
  96. data/lib/ratatui_ruby/version.rb +10 -0
  97. data/lib/ratatui_ruby.rb +87 -0
  98. data/sig/ratatui_ruby/ratatui_ruby.rbs +16 -0
  99. data/sig/ratatui_ruby/schema/bar_chart.rbs +14 -0
  100. data/sig/ratatui_ruby/schema/block.rbs +11 -0
  101. data/sig/ratatui_ruby/schema/canvas.rbs +62 -0
  102. data/sig/ratatui_ruby/schema/center.rbs +11 -0
  103. data/sig/ratatui_ruby/schema/constraint.rbs +13 -0
  104. data/sig/ratatui_ruby/schema/cursor.rbs +10 -0
  105. data/sig/ratatui_ruby/schema/gauge.rbs +13 -0
  106. data/sig/ratatui_ruby/schema/layout.rbs +11 -0
  107. data/sig/ratatui_ruby/schema/line_chart.rbs +20 -0
  108. data/sig/ratatui_ruby/schema/list.rbs +11 -0
  109. data/sig/ratatui_ruby/schema/overlay.rbs +9 -0
  110. data/sig/ratatui_ruby/schema/paragraph.rbs +11 -0
  111. data/sig/ratatui_ruby/schema/scrollbar.rbs +20 -0
  112. data/sig/ratatui_ruby/schema/sparkline.rbs +12 -0
  113. data/sig/ratatui_ruby/schema/style.rbs +13 -0
  114. data/sig/ratatui_ruby/schema/table.rbs +13 -0
  115. data/sig/ratatui_ruby/schema/tabs.rbs +11 -0
  116. data/sig/ratatui_ruby/test_helper.rbs +11 -0
  117. data/sig/ratatui_ruby/version.rbs +6 -0
  118. data/vendor/goodcop/base.yml +1047 -0
  119. metadata +196 -0
@@ -0,0 +1,65 @@
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 "analytics"
11
+
12
+ class Minitest::Test
13
+ include RatatuiRuby::TestHelper
14
+ end
15
+
16
+ class TestAnalytics < Minitest::Test
17
+ def setup
18
+ @app = AnalyticsApp.new
19
+ end
20
+
21
+ def test_render_initial_state
22
+ with_test_terminal(50, 20) do
23
+ @app.render
24
+
25
+ # Check Tabs
26
+ assert buffer_content.any? { |line| line.include?("Revenue") }
27
+ assert buffer_content.any? { |line| line.include?("Traffic") }
28
+ assert buffer_content.any? { |line| line.include?("Errors") }
29
+
30
+ # Check initial selected tab content
31
+ assert buffer_content.any? { |line| line.include?("Analytics: Revenue") }
32
+ end
33
+ end
34
+
35
+ def test_navigation_right
36
+ inject_event("key", { code: "right" })
37
+ @app.handle_input
38
+
39
+ with_test_terminal(50, 20) do
40
+ @app.render
41
+ assert buffer_content.any? { |line| line.include?("Analytics: Traffic") }
42
+ end
43
+ end
44
+
45
+ def test_navigation_left
46
+ # Move right to Traffic
47
+ inject_event("key", { code: "right" })
48
+ @app.handle_input
49
+
50
+ # Move left back to Revenue
51
+ inject_event("key", { code: "left" })
52
+ @app.handle_input
53
+
54
+ with_test_terminal(50, 20) do
55
+ @app.render
56
+ assert buffer_content.any? { |line| line.include?("Analytics: Revenue") }
57
+ end
58
+ end
59
+
60
+ def test_quit
61
+ inject_event("key", { code: "q" })
62
+ status = @app.handle_input
63
+ assert_equal :quit, status
64
+ end
65
+ end
@@ -0,0 +1,38 @@
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 "box_demo"
11
+
12
+ class Minitest::Test
13
+ include RatatuiRuby::TestHelper
14
+ end
15
+
16
+ class TestBoxDemo < Minitest::Test
17
+ def setup
18
+ @app = BoxDemoApp.new
19
+ end
20
+
21
+ def test_render_initial_state
22
+ with_test_terminal(40, 10) do
23
+ @app.render
24
+ assert buffer_content.any? { |line| line.include?("Box Demo") }
25
+ assert buffer_content.any? { |line| line.include?("Press Arrow Keys") }
26
+ end
27
+ end
28
+
29
+ def test_interaction
30
+ inject_event("key", { code: "up" })
31
+ @app.handle_input
32
+
33
+ with_test_terminal(40, 10) do
34
+ @app.render
35
+ assert buffer_content.any? { |line| line.include?("Up Pressed!") }
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,38 @@
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 "dashboard"
11
+
12
+ class Minitest::Test
13
+ include RatatuiRuby::TestHelper
14
+ end
15
+
16
+ class TestDashboard < Minitest::Test
17
+ def setup
18
+ @app = DashboardApp.new
19
+ end
20
+
21
+ def test_render_initial_state
22
+ with_test_terminal(60, 20) do
23
+ @app.render
24
+ assert buffer_content.any? { |line| line.include?("Item 1") }
25
+ assert buffer_content.any? { |line| line.include?("You selected: Item 1") }
26
+ end
27
+ end
28
+
29
+ def test_navigation
30
+ inject_event("key", { code: "down" })
31
+ @app.handle_input
32
+
33
+ with_test_terminal(60, 20) do
34
+ @app.render
35
+ assert buffer_content.any? { |line| line.include?("You selected: Item 2") }
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,63 @@
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 "login_form"
11
+
12
+ class Minitest::Test
13
+ include RatatuiRuby::TestHelper
14
+ end
15
+
16
+ class TestLoginForm < Minitest::Test
17
+ def setup
18
+ @app = LoginFormApp.new
19
+ end
20
+
21
+ def test_render_initial_state
22
+ with_test_terminal(60, 20) do
23
+ @app.render
24
+ assert buffer_content.any? { |line| line.include?("Enter Username:") }
25
+ # Initial cursor position check could be tricky without inspecting cursor state,
26
+ # but we can rely on visual cursor rendering if we had a way to check attributes,
27
+ # or just assume the widget logic puts it there.
28
+ end
29
+ end
30
+
31
+ def test_input_handling
32
+ # Type 'a'
33
+ inject_event("key", { code: "a" })
34
+ @app.handle_input
35
+
36
+ with_test_terminal(60, 20) do
37
+ @app.render
38
+ assert buffer_content.any? { |line| line.include?("Enter Username: [ a ]") }
39
+ end
40
+ end
41
+
42
+ def test_popup_flow
43
+ # Enter username 'user'
44
+ %w[u s e r].each do |char|
45
+ inject_event("key", { code: char })
46
+ @app.handle_input
47
+ end
48
+
49
+ # Press Enter
50
+ inject_event("key", { code: "enter" })
51
+ @app.handle_input
52
+
53
+ with_test_terminal(60, 20) do
54
+ @app.render
55
+ assert buffer_content.any? { |line| line.include?("Successful!") }
56
+ end
57
+
58
+ # Press 'q' to quit popup/app
59
+ inject_event("key", { code: "q" })
60
+ status = @app.handle_input
61
+ assert_equal :quit, status
62
+ end
63
+ end
@@ -0,0 +1,100 @@
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 "map_demo"
11
+
12
+ class TestMapDemo < Minitest::Test
13
+ include RatatuiRuby::TestHelper
14
+
15
+ def test_map_demo_renders
16
+ # Use a specific terminal size to make assertions reliable
17
+ with_test_terminal(20, 10) do
18
+ view = MapDemo.view(0.0)
19
+ RatatuiRuby.draw(view)
20
+
21
+ expected_buffer = [
22
+ "┌World Map Canvas──┐",
23
+ "│⣀⣀⣠⣶⣶⣶⡖⣲⠂⣶⣒⣦⣤⠶⢤⣤⣄⣀│",
24
+ "│⠹⠓⢦ ⠻⣿⡟⠉⣿⡿⣋⡁⠁ ⣴⠿⠋│",
25
+ "│⢀⡀⠘⣆⣴⡏ ⢀⡟⠻⣿⣧⣀⣀⣹⠟ │",
26
+ "│ ⠁ ⠈⢻⠿⣄⠘⢦⡔⢚⡏⠻⣽⣿⣀⡀ │",
27
+ "│⠄ ⠘⡆⣸⠃ ⡇⣼⡆ ⠈⡻⢿⣻⠤│",
28
+ "│ ⣷⠃ ⠙⠃ ⡀ ⠓⢳⢇⡆│",
29
+ "│ ⢀⡀⣻ ⣀⣀⣀⣤⣁⣤⣤⣄⣀⡀│",
30
+ "│⠾⠿⠉⠉⠙⠛⠛⠋⠁ ⠘⠧│",
31
+ "└──────────────────┘",
32
+ ]
33
+
34
+ expected_buffer.each_with_index do |line, i|
35
+ assert_equal line, buffer_content[i], "Line #{i} should match"
36
+ end
37
+ end
38
+ end
39
+
40
+ def test_quit
41
+ # We need to use with_test_terminal because MapDemo.run calls init_terminal and draw
42
+ with_test_terminal(80, 24) do
43
+ # Inject 'q' event
44
+ inject_event("key", { code: "q" })
45
+
46
+ # MapDemo.run should exit immediately after polling the 'q' event
47
+ # We use Timeout to prevent hanging if it's not quittable
48
+ require "timeout"
49
+ Timeout.timeout(1) do
50
+ MapDemo.run
51
+ end
52
+ end
53
+ end
54
+
55
+ def test_map_demo_animation
56
+ # Test that the animation actually happens within the run loop
57
+ with_test_terminal(20, 10) do
58
+ # We want to verify that the radius changes.
59
+ # We'll mock RatatuiRuby.draw to capture the views being drawn.
60
+ draw_calls = []
61
+
62
+ # We need to preserve the original behavior of draw if we want to see it in the test terminal,
63
+ # but for this test, we just want to inspect the view objects.
64
+
65
+ # Using a lambda as a mock for RatatuiRuby.draw
66
+ original_draw = RatatuiRuby.method(:draw)
67
+ RatatuiRuby.define_singleton_method(:draw) do |view|
68
+ draw_calls << view
69
+ original_draw.call(view)
70
+ end
71
+
72
+ begin
73
+ MapDemo.stub :sleep, nil do
74
+ # Inject some non-quitting events to let it loop
75
+ # radius starts at 0.0
76
+ # 1st loop: radius becomes 0.5, draws, polls 'up', sleeps
77
+ # 2nd loop: radius becomes 1.0, draws, polls 'up', sleeps
78
+ # 3rd loop: radius becomes 1.5, draws, polls 'q', breaks
79
+ inject_event("key", { code: "up" })
80
+ inject_event("key", { code: "up" })
81
+ inject_event("key", { code: "q" })
82
+
83
+ MapDemo.run
84
+ end
85
+ ensure
86
+ # Restore original draw method
87
+ RatatuiRuby.define_singleton_method(:draw, &original_draw)
88
+ end
89
+
90
+ assert_operator draw_calls.size, :>=, 3
91
+
92
+ # Check that the radius in each view is increasing
93
+ radii = draw_calls.map { |v| v.shapes.find { |s| s.is_a?(RatatuiRuby::Circle) }.radius }
94
+
95
+ assert_equal 0.5, radii[0]
96
+ assert_equal 1.0, radii[1]
97
+ assert_equal 1.5, radii[2]
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,39 @@
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 "stock_ticker"
11
+
12
+ class Minitest::Test
13
+ include RatatuiRuby::TestHelper
14
+ end
15
+
16
+ class TestStockTicker < Minitest::Test
17
+ def setup
18
+ @app = StockTickerApp.new
19
+ end
20
+
21
+ def test_render
22
+ with_test_terminal(60, 20) do
23
+ @app.render
24
+ assert buffer_content.any? { |line| line.include?("Network Activity") }
25
+ assert buffer_content.any? { |line| line.include?("Stock Ticker") }
26
+ # It's a dynamic simulation, so exact content varies,
27
+ # but titles should be stable.
28
+ end
29
+ end
30
+
31
+ def test_update
32
+ # Render multiple times to ensure no crash on update
33
+ with_test_terminal(60, 20) do
34
+ @app.render
35
+ @app.render
36
+ assert buffer_content.any? { |line| line.include?("Stock Ticker") }
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,40 @@
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 "system_monitor"
11
+
12
+ class Minitest::Test
13
+ include RatatuiRuby::TestHelper
14
+ end
15
+
16
+ class TestSystemMonitor < Minitest::Test
17
+ def setup
18
+ @app = SystemMonitorApp.new
19
+ end
20
+
21
+ def test_render_initial_state
22
+ with_test_terminal(60, 20) do
23
+ @app.render
24
+ assert buffer_content.any? { |line| line.include?("Processes") }
25
+ assert buffer_content.any? { |line| line.include?("Memory Usage") }
26
+ assert buffer_content.any? { |line| line.include?("50%") }
27
+ end
28
+ end
29
+
30
+ def test_interaction
31
+ # Increase percentage
32
+ inject_event("key", { code: "up" })
33
+ @app.handle_input
34
+
35
+ with_test_terminal(60, 20) do
36
+ @app.render
37
+ assert buffer_content.any? { |line| line.include?("55%") }
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,8 @@
1
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
+ # SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ [target.aarch64-apple-darwin]
5
+ rustflags = ["-C", "link-arg=-undefined", "-C", "link-arg=dynamic_lookup"]
6
+
7
+ [target.x86_64-apple-darwin]
8
+ rustflags = ["-C", "link-arg=-undefined", "-C", "link-arg=dynamic_lookup"]
@@ -0,0 +1,4 @@
1
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
+ # SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ /target/