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,64 @@
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
+ module RatatuiRuby
7
+ ##
8
+ # A wrapper class that provides a concise DSL for creating widgets and interacting
9
+ # with the terminal within the +main_loop+.
10
+ #
11
+ # This class is yielded to the block provided to {RatatuiRuby.main_loop}.
12
+ # It uses metaprogramming to delegate method calls to {RatatuiRuby} module functions
13
+ # and to act as a factory for {RatatuiRuby} widget classes.
14
+ #
15
+ # == Features
16
+ #
17
+ # 1. **Widget Shorthand**: Provides factory methods for every widget class.
18
+ # Converts snake_case method calls (e.g., +paragraph+) into CamelCase class instantiations
19
+ # (e.g., +RatatuiRuby::Paragraph.new+).
20
+ #
21
+ # 2. **Method Shorthand**: Aliases module functions of {RatatuiRuby}, allowing you
22
+ # to call methods like +draw+ and +poll_event+ directly on the DSL object.
23
+ #
24
+ # == Example
25
+ #
26
+ # RatatuiRuby.main_loop do |tui|
27
+ # # Create UI using shorthand methods
28
+ # view = tui.paragraph(
29
+ # text: "Hello World",
30
+ # block: tui.block(borders: [:all])
31
+ # )
32
+ #
33
+ # # Use module aliases to draw and handle events
34
+ # tui.draw(view)
35
+ # event = tui.poll_event
36
+ #
37
+ # break if event && event[:code] == "q"
38
+ # end
39
+ class DSL
40
+ # Wrap methods directly
41
+ RatatuiRuby.singleton_methods(false).each do |method_name|
42
+ define_method(method_name) do |*args, **kwargs, &block|
43
+ RatatuiRuby.public_send(method_name, *args, **kwargs, &block)
44
+ end
45
+ end
46
+
47
+ # Wrap classes as snake_case factories
48
+ RatatuiRuby.constants.each do |const_name|
49
+ next if const_name == :Buffer
50
+
51
+ klass = RatatuiRuby.const_get(const_name)
52
+ next unless klass.is_a?(Class)
53
+
54
+ method_name = const_name.to_s
55
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
56
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
57
+ .downcase
58
+
59
+ define_method(method_name) do |*args, **kwargs, &block|
60
+ klass.new(*args, **kwargs, &block)
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,26 @@
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
+ module RatatuiRuby
7
+ # A Monthly Calendar widget.
8
+ #
9
+ # [year] Integer (e.g., 2025)
10
+ # [month] Integer (1-12)
11
+ # [day_style] Style (Style for regular days)
12
+ # [header_style] Style (Style for the month title)
13
+ # [block] Block
14
+ class Calendar < Data.define(:year, :month, :day_style, :header_style, :block)
15
+ # Creates a new Calendar.
16
+ #
17
+ # [year] Integer (e.g., 2025)
18
+ # [month] Integer (1-12)
19
+ # [day_style] Style (Style for regular days)
20
+ # [header_style] Style (Style for the month title)
21
+ # [block] Block
22
+ def initialize(year:, month:, day_style: nil, header_style: nil, block: nil)
23
+ super
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,81 @@
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
+ module RatatuiRuby
7
+ # Defines an Axis for a Chart
8
+ # [title] String
9
+ # [bounds] Array<Float> [min, max]
10
+ # [labels] Array<String>
11
+ # [style] Style
12
+ class Axis < Data.define(:title, :bounds, :labels, :style)
13
+ # Creates a new Axis.
14
+ #
15
+ # [title] String
16
+ # [bounds] Array<Float> [min, max]
17
+ # [labels] Array<String>
18
+ # [style] Style
19
+ def initialize(title: "", bounds: [0.0, 10.0], labels: [], style: nil)
20
+ super
21
+ end
22
+ end
23
+
24
+ # Defines a Dataset for a Chart.
25
+ # [name] The name of the dataset.
26
+ # [data] Array of arrays [[x, y], [x, y]] (Floats).
27
+ # [color] The color of the line.
28
+ # [marker] Symbol (:dot, :braille, :block, :bar)
29
+ # [graph_type] Symbol (:line, :scatter)
30
+ class Dataset < Data.define(:name, :data, :color, :marker, :graph_type)
31
+ # Creates a new Dataset.
32
+ #
33
+ # [name] The name of the dataset.
34
+ # [data] Array of arrays [[x, y], [x, y]] (Floats).
35
+ # [color] The color of the line.
36
+ # [marker] Symbol (:dot, :braille, :block, :bar)
37
+ # [graph_type] Symbol (:line, :scatter)
38
+ def initialize(name:, data:, color: "reset", marker: :dot, graph_type: :line)
39
+ super
40
+ end
41
+ end
42
+
43
+ # A generic Cartesian chart.
44
+ # [datasets] Array<Dataset>
45
+ # [x_axis] Axis
46
+ # [y_axis] Axis
47
+ # [block] Block
48
+ # [style] Style (base style)
49
+ class Chart < Data.define(:datasets, :x_axis, :y_axis, :block, :style)
50
+ # Creates a new Chart widget.
51
+ #
52
+ # [datasets] Array<Dataset>
53
+ # [x_axis] Axis
54
+ # [y_axis] Axis
55
+ # [block] Block
56
+ # [style] Style (base style)
57
+ def initialize(datasets:, x_axis:, y_axis:, block: nil, style: nil)
58
+ super
59
+ end
60
+ end
61
+
62
+ # A complex chart widget. (Legacy/Alias for Chart)
63
+ #
64
+ # [datasets] Array of Dataset objects.
65
+ # [x_labels] Array of Strings for the X-axis labels.
66
+ # [y_labels] Array of Strings for the Y-axis labels.
67
+ # [y_bounds] Array of two Floats [min, max] for the Y-axis.
68
+ # [block] Optional block widget to wrap the chart.
69
+ class LineChart < Data.define(:datasets, :x_labels, :y_labels, :y_bounds, :block)
70
+ # Creates a new LineChart widget.
71
+ #
72
+ # [datasets] Array of Dataset objects.
73
+ # [x_labels] Array of Strings for the X-axis labels.
74
+ # [y_labels] Array of Strings for the Y-axis labels.
75
+ # [y_bounds] Array of two Floats [min, max] for the Y-axis.
76
+ # [block] Optional block widget to wrap the chart.
77
+ def initialize(datasets:, x_labels: [], y_labels: [], y_bounds: [0.0, 100.0], block: nil)
78
+ super
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,83 @@
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
+ module RatatuiRuby
7
+ # A widget that clears (resets) the terminal buffer in the area it is rendered into.
8
+ #
9
+ # The Clear widget is essential for creating opaque popups and modals. Without it,
10
+ # background content or styles (like background colors) will "bleed through"
11
+ # empty spaces or transparent widgets.
12
+ #
13
+ # > [!TIP]
14
+ # > Use `Clear` to prevent "Style Bleed". If a widget rendered behind the popup
15
+ # > has a background color, widgets rendered on top with `Style.default` will
16
+ # > inherit that background color unless you `Clear` the area first.
17
+ #
18
+ # == Usage with Overlay
19
+ #
20
+ # Because RatatuiRuby uses an immediate-mode UI pattern, you must use {Overlay} to
21
+ # layer widgets properly. The typical pattern for creating an opaque popup is:
22
+ #
23
+ # background = Paragraph.new(text: "Background content...")
24
+ # popup = Paragraph.new(
25
+ # text: "Popup content",
26
+ # block: Block.new(title: "Popup", borders: [:all])
27
+ # )
28
+ #
29
+ # # Create an opaque popup by layering: background -> Clear -> popup
30
+ # ui = Overlay.new(
31
+ # layers: [
32
+ # background,
33
+ # Center.new(
34
+ # child: Overlay.new(
35
+ # layers: [
36
+ # Clear.new, # Erases background in this area
37
+ # popup # Draws on top of cleared area
38
+ # ]
39
+ # ),
40
+ # width_percent: 50,
41
+ # height_percent: 40
42
+ # )
43
+ # ]
44
+ # )
45
+ #
46
+ # Without the Clear widget, the background text would be visible through the
47
+ # empty spaces in the popup.
48
+ #
49
+ # == Optional Block Parameter
50
+ #
51
+ # You can optionally provide a {Block} to draw borders around the cleared area:
52
+ #
53
+ # Clear.new(block: Block.new(title: "Cleared Area", borders: [:all]))
54
+ #
55
+ # This is equivalent to:
56
+ #
57
+ # Overlay.new(
58
+ # layers: [
59
+ # Clear.new,
60
+ # Block.new(title: "Cleared Area", borders: [:all])
61
+ # ]
62
+ # )
63
+ #
64
+ # [block] Optional {Block} widget to render on top of the cleared area.
65
+ #
66
+ # @see Overlay
67
+ # @see Center
68
+ # @see Block
69
+ class Clear < Data.define(:block)
70
+ # Creates a new Clear widget.
71
+ #
72
+ # @param block [Block, nil] Optional block widget to render on top of the cleared area.
73
+ #
74
+ # @example Basic usage
75
+ # Clear.new
76
+ #
77
+ # @example With a border
78
+ # Clear.new(block: Block.new(title: "Modal", borders: [:all]))
79
+ def initialize(block: nil)
80
+ super
81
+ end
82
+ end
83
+ end
@@ -8,14 +8,20 @@ module RatatuiRuby
8
8
  #
9
9
  # [items] An array of strings to display in the list.
10
10
  # [selected_index] The index of the currently selected item, or nil if none.
11
+ # [style] The base style for all items.
12
+ # [highlight_style] The style for the selected item.
13
+ # [highlight_symbol] The symbol to display in front of the selected item.
11
14
  # [block] An optional Block widget to wrap the list.
12
- class List < Data.define(:items, :selected_index, :block)
15
+ class List < Data.define(:items, :selected_index, :style, :highlight_style, :highlight_symbol, :block)
13
16
  # Creates a new List.
14
17
  #
15
18
  # [items] An array of strings to display in the list.
16
19
  # [selected_index] The index of the currently selected item, or nil if none.
20
+ # [style] The base style for all items.
21
+ # [highlight_style] The style for the selected item.
22
+ # [highlight_symbol] The symbol to display in front of the selected item.
17
23
  # [block] An optional Block widget to wrap the list.
18
- def initialize(items: [], selected_index: nil, block: nil)
24
+ def initialize(items: [], selected_index: nil, style: nil, highlight_style: nil, highlight_symbol: "> ", block: nil)
19
25
  super
20
26
  end
21
27
  end
@@ -9,7 +9,8 @@ module RatatuiRuby
9
9
  # [text] the text to display.
10
10
  # [style] the style to apply (Style object).
11
11
  # [block] an optional Block widget to wrap the paragraph.
12
- class Paragraph < Data.define(:text, :style, :block, :wrap, :align)
12
+ # [scroll] scroll offset as (y, x) array matching ratatui convention.
13
+ class Paragraph < Data.define(:text, :style, :block, :wrap, :align, :scroll)
13
14
  # Creates a new Paragraph.
14
15
  #
15
16
  # [text] the text to display.
@@ -17,7 +18,8 @@ module RatatuiRuby
17
18
  # [block] the block to wrap the paragraph.
18
19
  # [wrap] whether to wrap text at width.
19
20
  # [align] alignment (:left, :center, :right).
20
- def initialize(text:, style: Style.default, block: nil, wrap: false, align: :left)
21
+ # [scroll] scroll offset as (y, x) array (default: [0, 0]).
22
+ def initialize(text:, style: Style.default, block: nil, wrap: false, align: :left, scroll: [0, 0])
21
23
  super
22
24
  end
23
25
 
@@ -29,9 +31,10 @@ module RatatuiRuby
29
31
  # [block] the block to wrap the paragraph.
30
32
  # [wrap] whether to wrap text at width.
31
33
  # [align] alignment (:left, :center, :right).
32
- def self.new(text:, style: nil, fg: nil, bg: nil, block: nil, wrap: false, align: :left)
34
+ # [scroll] scroll offset as (y, x) array (default: [0, 0]).
35
+ def self.new(text:, style: nil, fg: nil, bg: nil, block: nil, wrap: false, align: :left, scroll: [0, 0])
33
36
  style ||= Style.new(fg:, bg:)
34
- super(text:, style:, block:, wrap:, align:)
37
+ super(text:, style:, block:, wrap:, align:, scroll:)
35
38
  end
36
39
  end
37
40
  end
@@ -0,0 +1,24 @@
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
+ module RatatuiRuby
7
+ # A rectangle in the terminal grid.
8
+ #
9
+ # [x] The x-coordinate of the top-left corner.
10
+ # [y] The y-coordinate of the top-left corner.
11
+ # [width] The width of the rectangle.
12
+ # [height] The height of the rectangle.
13
+ class Rect < Data.define(:x, :y, :width, :height)
14
+ # Creates a new Rect.
15
+ #
16
+ # [x] The x-coordinate of the top-left corner.
17
+ # [y] The y-coordinate of the top-left corner.
18
+ # [width] The width of the rectangle.
19
+ # [height] The height of the rectangle.
20
+ def initialize(x: 0, y: 0, width: 0, height: 0)
21
+ super
22
+ end
23
+ end
24
+ end
@@ -9,15 +9,21 @@ module RatatuiRuby
9
9
  # [header] An array of strings or Paragraphs representing the header row.
10
10
  # [rows] An array of arrays of strings or Paragraphs representing the data rows.
11
11
  # [widths] An array of Constraint objects defining column widths.
12
+ # [highlight_style] The style for the selected row.
13
+ # [highlight_symbol] The symbol to display in front of the selected row.
14
+ # [selected_row] The index of the currently selected row, or nil if none.
12
15
  # [block] An optional Block widget to wrap the table.
13
- class Table < Data.define(:header, :rows, :widths, :block)
16
+ class Table < Data.define(:header, :rows, :widths, :highlight_style, :highlight_symbol, :selected_row, :block)
14
17
  # Creates a new Table.
15
18
  #
16
19
  # [header] An array of strings or Paragraphs representing the header row.
17
20
  # [rows] An array of arrays of strings or Paragraphs representing the data rows.
18
21
  # [widths] An array of Constraint objects defining column widths.
22
+ # [highlight_style] The style for the selected row.
23
+ # [highlight_symbol] The symbol to display in front of the selected row.
24
+ # [selected_row] The index of the currently selected row, or nil if none.
19
25
  # [block] An optional Block widget to wrap the table.
20
- def initialize(header: nil, rows: [], widths: [], block: nil)
26
+ def initialize(header: nil, rows: [], widths: [], highlight_style: nil, highlight_symbol: "> ", selected_row: nil, block: nil)
21
27
  super
22
28
  end
23
29
  end
@@ -6,5 +6,5 @@
6
6
  module RatatuiRuby
7
7
  # The version of the ratatui_ruby gem.
8
8
  # See https://semver.org/spec/v2.0.0.html
9
- VERSION = "0.1.0"
9
+ VERSION = "0.3.0"
10
10
  end
data/lib/ratatui_ruby.rb CHANGED
@@ -4,6 +4,7 @@
4
4
  # SPDX-License-Identifier: AGPL-3.0-or-later
5
5
 
6
6
  require_relative "ratatui_ruby/version"
7
+ require_relative "ratatui_ruby/schema/rect"
7
8
  require_relative "ratatui_ruby/schema/paragraph"
8
9
  require_relative "ratatui_ruby/schema/layout"
9
10
  require_relative "ratatui_ruby/schema/block"
@@ -15,12 +16,14 @@ require_relative "ratatui_ruby/schema/table"
15
16
  require_relative "ratatui_ruby/schema/tabs"
16
17
  require_relative "ratatui_ruby/schema/bar_chart"
17
18
  require_relative "ratatui_ruby/schema/sparkline"
18
- require_relative "ratatui_ruby/schema/line_chart"
19
+ require_relative "ratatui_ruby/schema/chart"
20
+ require_relative "ratatui_ruby/schema/clear"
19
21
  require_relative "ratatui_ruby/schema/cursor"
20
22
  require_relative "ratatui_ruby/schema/overlay"
21
23
  require_relative "ratatui_ruby/schema/center"
22
24
  require_relative "ratatui_ruby/schema/scrollbar"
23
25
  require_relative "ratatui_ruby/schema/canvas"
26
+ require_relative "ratatui_ruby/schema/calendar"
24
27
 
25
28
  begin
26
29
  require "ratatui_ruby/ratatui_ruby"
@@ -69,7 +72,7 @@ module RatatuiRuby
69
72
  # Polls for a keyboard event.
70
73
  #
71
74
  # poll_event
72
- # # => { type: "key", code: "char", value: "a", modifiers: [] }
75
+ # # => { type: :key, code: "a", modifiers: ["ctrl"] }
73
76
  #
74
77
  # (Native method implemented in Rust)
75
78
 
@@ -84,4 +87,23 @@ module RatatuiRuby
84
87
  # inject_test_event("key", { code: "a" })
85
88
  #
86
89
  # (Native method implemented in Rust)
90
+
91
+ ##
92
+ # Provides a convenience wrapper for the main TUI loop.
93
+ # Initializes the terminal, runs the loop, and ensures the terminal is restored.
94
+ #
95
+ # RatatuiRuby.main_loop do
96
+ # draw RatatuiRuby::Paragraph.new(text: "Hello")
97
+ # event = RatatuiRuby::poll_event
98
+ # break if event && event[:type] == :key && event[:code] == "q"
99
+ # end
100
+ def self.main_loop
101
+ require_relative "ratatui_ruby/dsl"
102
+ init_terminal
103
+ loop do
104
+ yield DSL.new
105
+ end
106
+ ensure
107
+ restore_terminal
108
+ end
87
109
  end
data/mise.toml ADDED
@@ -0,0 +1,8 @@
1
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
+ # SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ [tools]
5
+ ruby = "3.4.8"
6
+ rust = "1.91.1"
7
+ python = "3.12"
8
+ pre-commit = "latest"
@@ -0,0 +1,11 @@
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
+ module RatatuiRuby
7
+ class Buffer
8
+ def set_string: (Integer x, Integer y, String string, Style style) -> void
9
+ def area: () -> Rect
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
+ # SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ module RatatuiRuby
5
+ class Calendar < Data
6
+ attr_reader year: Integer
7
+ attr_reader month: Integer
8
+ attr_reader day_style: Style?
9
+ attr_reader header_style: Style?
10
+ attr_reader block: Block?
11
+ def self.new: (year: Integer, month: Integer, ?day_style: Style?, ?header_style: Style?, ?block: Block?) -> Calendar
12
+ end
13
+ end
@@ -2,11 +2,30 @@
2
2
  # SPDX-License-Identifier: AGPL-3.0-or-later
3
3
 
4
4
  module RatatuiRuby
5
+ class Axis < Data
6
+ attr_reader title: String
7
+ attr_reader bounds: [Float, Float]
8
+ attr_reader labels: Array[String]
9
+ attr_reader style: Style?
10
+ def self.new: (?title: String, ?bounds: [Float, Float], ?labels: Array[String], ?style: Style?) -> Axis
11
+ end
12
+
5
13
  class Dataset < Data
6
14
  attr_reader name: String | Symbol
7
15
  attr_reader data: Array[[Float, Float]]
8
16
  attr_reader color: String | Symbol
9
- def self.new: (name: String | Symbol, data: Array[[Float, Float]], ?color: String | Symbol) -> Dataset
17
+ attr_reader marker: Symbol
18
+ attr_reader graph_type: Symbol
19
+ def self.new: (name: String | Symbol, data: Array[[Float, Float]], ?color: String | Symbol, ?marker: Symbol, ?graph_type: Symbol) -> Dataset
20
+ end
21
+
22
+ class Chart < Data
23
+ attr_reader datasets: Array[Dataset]
24
+ attr_reader x_axis: Axis
25
+ attr_reader y_axis: Axis
26
+ attr_reader block: Block?
27
+ attr_reader style: Style?
28
+ def self.new: (datasets: Array[Dataset], x_axis: Axis, y_axis: Axis, ?block: Block?, ?style: Style?) -> Chart
10
29
  end
11
30
 
12
31
  class LineChart < Data
@@ -5,7 +5,10 @@ module RatatuiRuby
5
5
  class List < Data
6
6
  attr_reader items: Array[String]
7
7
  attr_reader selected_index: Integer?
8
+ attr_reader style: Style?
9
+ attr_reader highlight_style: Style?
10
+ attr_reader highlight_symbol: String?
8
11
  attr_reader block: Block?
9
- def self.new: (?items: Array[String], ?selected_index: Integer?, ?block: Block?) -> List
12
+ def self.new: (?items: Array[String], ?selected_index: Integer?, ?style: Style?, ?highlight_style: Style?, ?highlight_symbol: String?, ?block: Block?) -> List
10
13
  end
11
14
  end
@@ -0,0 +1,14 @@
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
+ module RatatuiRuby
7
+ class Rect < Data
8
+ attr_reader x: Integer
9
+ attr_reader y: Integer
10
+ attr_reader width: Integer
11
+ attr_reader height: Integer
12
+ def self.new: (x: Integer, y: Integer, width: Integer, height: Integer) -> instance
13
+ end
14
+ end
@@ -0,0 +1,19 @@
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
+ # Lockfiles need to be refreshed by a tool after Manifests are changed.
7
+ class CargoLockfile < Data.define(:path, :dir, :name)
8
+ def exists?
9
+ File.exist?(path)
10
+ end
11
+
12
+ def refresh
13
+ return unless exists?
14
+
15
+ Dir.chdir(dir) do
16
+ system("cargo update -p #{name} --offline")
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,37 @@
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_relative "comparison_links"
7
+ require_relative "unreleased_section"
8
+ require_relative "history"
9
+ require_relative "header"
10
+
11
+ # Changelog manages the project's CHANGELOG.md file.
12
+ class Changelog
13
+ # Creates a new Changelog for the file at the given path.
14
+ def initialize(path: "CHANGELOG.md")
15
+ @path = path
16
+ end
17
+
18
+ # Releases a new version in the changelog.
19
+ # This moves the unreleased changes to a new version heading and resets the unreleased section.
20
+ def release(new_version)
21
+ content = File.read(@path)
22
+
23
+ header = Header.parse(content)
24
+ unreleased = UnreleasedSection.parse(content)
25
+ links = ComparisonLinks.parse(content)
26
+
27
+ raise "Could not parse CHANGELOG.md" unless header && unreleased && links
28
+
29
+ history = History.parse(content, header.length, unreleased.to_s.length, links.to_s)
30
+
31
+ links.update(new_version)
32
+ history.add(unreleased.as_version(new_version))
33
+
34
+ File.write(@path, "#{header}#{UnreleasedSection.fresh}\n\n#{history}\n#{links}")
35
+ nil
36
+ end
37
+ end
@@ -0,0 +1,41 @@
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
+ # ComparisonLinks manages the git comparison links at the bottom of the changelog.
7
+ class ComparisonLinks
8
+ PATTERN = /^(\[Unreleased\]: .*)$/m
9
+
10
+ # Extracts the comparison links from the given content.
11
+ def self.parse(content)
12
+ match = content.match(PATTERN)
13
+ new(match[1].strip) if match
14
+ end
15
+
16
+ # Creates a new ComparisonLinks from the given links text.
17
+ def initialize(links)
18
+ @links = links.dup
19
+ end
20
+
21
+ # Updates the comparison links for the new version.
22
+ def update(new_version)
23
+ pattern = %r{^\[Unreleased\]: (.*?/compare/)v(.*)\.\.\.HEAD$}
24
+ match = @links.match(pattern)
25
+ return unless match
26
+
27
+ base_url = match[1]
28
+ prev_version = match[2]
29
+
30
+ new_unreleased = "[Unreleased]: #{base_url}v#{new_version}...HEAD"
31
+ new_version_link = "[#{new_version}]: #{base_url}v#{prev_version}...v#{new_version}"
32
+
33
+ @links.sub!(pattern, "#{new_unreleased}\n#{new_version_link}")
34
+ nil
35
+ end
36
+
37
+ # Returns the current state of the links as a string.
38
+ def to_s
39
+ @links
40
+ end
41
+ end
@@ -0,0 +1,30 @@
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
+ # Header manages the header section of the changelog.
7
+ class Header
8
+ PATTERN = /^(.*?)(?=## \[Unreleased\])/m
9
+
10
+ # Extracts the header section from the given content.
11
+ def self.parse(content)
12
+ match = content.match(PATTERN)
13
+ new(match[1]) if match
14
+ end
15
+
16
+ # Creates a new Header from the given content.
17
+ def initialize(content)
18
+ @content = content.dup
19
+ end
20
+
21
+ # Returns the length of the header content.
22
+ def length
23
+ @content.length
24
+ end
25
+
26
+ # Returns the current state of the header as a string.
27
+ def to_s
28
+ @content
29
+ end
30
+ end