ratatui_ruby-tea 0.2.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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +8 -0
  3. data/CHANGELOG.md +41 -0
  4. data/README.md +1 -1
  5. data/doc/concepts/application_architecture.md +182 -3
  6. data/examples/app_fractal_dashboard/README.md +60 -0
  7. data/examples/app_fractal_dashboard/app.rb +67 -0
  8. data/examples/app_fractal_dashboard/bags/custom_shell_input.rb +77 -0
  9. data/examples/app_fractal_dashboard/bags/custom_shell_modal.rb +73 -0
  10. data/examples/app_fractal_dashboard/bags/custom_shell_output.rb +86 -0
  11. data/examples/app_fractal_dashboard/bags/disk_usage.rb +44 -0
  12. data/examples/app_fractal_dashboard/bags/network_panel.rb +45 -0
  13. data/examples/app_fractal_dashboard/bags/ping.rb +43 -0
  14. data/examples/app_fractal_dashboard/bags/stats_panel.rb +45 -0
  15. data/examples/app_fractal_dashboard/bags/system_info.rb +43 -0
  16. data/examples/app_fractal_dashboard/bags/uptime.rb +43 -0
  17. data/examples/app_fractal_dashboard/dashboard/base.rb +74 -0
  18. data/examples/app_fractal_dashboard/dashboard/update_helpers.rb +86 -0
  19. data/examples/app_fractal_dashboard/dashboard/update_manual.rb +87 -0
  20. data/examples/app_fractal_dashboard/dashboard/update_router.rb +43 -0
  21. data/examples/verify_readme_usage/README.md +1 -1
  22. data/examples/verify_readme_usage/app.rb +1 -1
  23. data/examples/{widget_cmd_exec → widget_command_system}/app.rb +18 -18
  24. data/lib/ratatui_ruby/tea/command.rb +145 -0
  25. data/lib/ratatui_ruby/tea/router.rb +337 -0
  26. data/lib/ratatui_ruby/tea/runtime.rb +99 -39
  27. data/lib/ratatui_ruby/tea/shortcuts.rb +51 -0
  28. data/lib/ratatui_ruby/tea/version.rb +1 -1
  29. data/lib/ratatui_ruby/tea.rb +59 -1
  30. data/sig/ratatui_ruby/tea/command.rbs +47 -0
  31. data/sig/ratatui_ruby/tea/router.rbs +99 -0
  32. metadata +26 -8
  33. data/lib/ratatui_ruby/tea/cmd.rb +0 -88
  34. data/sig/ratatui_ruby/tea/cmd.rbs +0 -32
  35. /data/examples/{widget_cmd_exec → widget_command_system}/README.md +0 -0
  36. /data/sig/examples/{widget_cmd_exec → widget_command_system}/app.rbs +0 -0
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
+ # SPDX-License-Identifier: MIT-0
6
+ #++
7
+
8
+ # Fetches and displays disk usage via +df -h+.
9
+ # A bag for fetching and displaying disk usage.
10
+ module DiskUsage
11
+ Command = RatatuiRuby::Tea::Command
12
+
13
+ Model = Data.define(:output, :loading)
14
+ INITIAL = Model.new(output: "Press 'd' for disk usage", loading: false)
15
+
16
+ VIEW = lambda do |model, tui, disabled: false|
17
+ text_style = if disabled && model.output == INITIAL.output
18
+ tui.style(fg: :dark_gray)
19
+ else
20
+ nil
21
+ end
22
+
23
+ tui.paragraph(
24
+ text: tui.text_span(content: model.output, style: text_style),
25
+ block: tui.block(title: "Disk Usage", borders: [:all], border_style: { fg: :cyan })
26
+ )
27
+ end
28
+
29
+ UPDATE = lambda do |message, model|
30
+ case message
31
+ in [:disk_usage, { stdout:, status: 0 }]
32
+ lines = Ractor.make_shareable(stdout.lines.first(4).join.strip)
33
+ [model.with(output: lines, loading: false), nil]
34
+ in [:disk_usage, { stderr:, _status: }]
35
+ [model.with(output: Ractor.make_shareable("Error: #{stderr.strip}"), loading: false), nil]
36
+ else
37
+ [model, nil]
38
+ end
39
+ end
40
+
41
+ def self.fetch_command
42
+ Command.system("df -h", :disk_usage)
43
+ end
44
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
+ # SPDX-License-Identifier: MIT-0
6
+ #++
7
+
8
+ require_relative "ping"
9
+ require_relative "uptime"
10
+
11
+ # Composes Ping and Uptime in a horizontal layout.
12
+ module NetworkPanel
13
+ Model = Data.define(:ping, :uptime)
14
+
15
+ INITIAL = Model.new(
16
+ ping: Ping::INITIAL,
17
+ uptime: Uptime::INITIAL
18
+ )
19
+
20
+ VIEW = lambda do |model, tui, disabled: false|
21
+ tui.layout(
22
+ direction: :horizontal,
23
+ constraints: [tui.constraint_percentage(50), tui.constraint_percentage(50)],
24
+ children: [
25
+ Ping::VIEW.call(model.ping, tui, disabled:),
26
+ Uptime::VIEW.call(model.uptime, tui, disabled:),
27
+ ]
28
+ )
29
+ end
30
+
31
+ UPDATE = lambda do |message, model|
32
+ case message
33
+ in [:ping, *rest]
34
+ child_message = [:ping, *rest]
35
+ new_child, command = Ping::UPDATE.call(child_message, model.ping)
36
+ [model.with(ping: new_child), command]
37
+ in [:uptime, *rest]
38
+ child_message = [:uptime, *rest]
39
+ new_child, command = Uptime::UPDATE.call(child_message, model.uptime)
40
+ [model.with(uptime: new_child), command]
41
+ else
42
+ [model, nil]
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
+ # SPDX-License-Identifier: MIT-0
6
+ #++
7
+
8
+ # Pings localhost to check network connectivity.
9
+ # A bag for pinging localhost.
10
+ module Ping
11
+ Command = RatatuiRuby::Tea::Command
12
+
13
+ Model = Data.define(:output, :loading)
14
+ INITIAL = Model.new(output: "Press 'p' for ping", loading: false)
15
+
16
+ VIEW = lambda do |model, tui, disabled: false|
17
+ text_style = if disabled && model.output == INITIAL.output
18
+ tui.style(fg: :dark_gray)
19
+ else
20
+ nil
21
+ end
22
+
23
+ tui.paragraph(
24
+ text: tui.text_span(content: model.output, style: text_style),
25
+ block: tui.block(title: "Ping", borders: [:all], border_style: { fg: :magenta })
26
+ )
27
+ end
28
+
29
+ UPDATE = lambda do |message, model|
30
+ case message
31
+ in [:ping, { stdout:, status: 0 }]
32
+ [model.with(output: Ractor.make_shareable(stdout.strip), loading: false), nil]
33
+ in [:ping, { stderr:, _status: }]
34
+ [model.with(output: Ractor.make_shareable("Error: #{stderr.strip}"), loading: false), nil]
35
+ else
36
+ [model, nil]
37
+ end
38
+ end
39
+
40
+ def self.fetch_command
41
+ Command.system("ping -c 1 localhost", :ping)
42
+ end
43
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
+ # SPDX-License-Identifier: MIT-0
6
+ #++
7
+
8
+ require_relative "system_info"
9
+ require_relative "disk_usage"
10
+
11
+ # Composes SystemInfo and DiskUsage in a horizontal layout.
12
+ module StatsPanel
13
+ Model = Data.define(:system_info, :disk_usage)
14
+
15
+ INITIAL = Model.new(
16
+ system_info: SystemInfo::INITIAL,
17
+ disk_usage: DiskUsage::INITIAL
18
+ )
19
+
20
+ VIEW = lambda do |model, tui, disabled: false|
21
+ tui.layout(
22
+ direction: :horizontal,
23
+ constraints: [tui.constraint_percentage(50), tui.constraint_percentage(50)],
24
+ children: [
25
+ SystemInfo::VIEW.call(model.system_info, tui, disabled:),
26
+ DiskUsage::VIEW.call(model.disk_usage, tui, disabled:),
27
+ ]
28
+ )
29
+ end
30
+
31
+ UPDATE = lambda do |message, model|
32
+ case message
33
+ in [:system_info, *rest]
34
+ child_message = [:system_info, *rest]
35
+ new_child, command = SystemInfo::UPDATE.call(child_message, model.system_info)
36
+ [model.with(system_info: new_child), command]
37
+ in [:disk_usage, *rest]
38
+ child_message = [:disk_usage, *rest]
39
+ new_child, command = DiskUsage::UPDATE.call(child_message, model.disk_usage)
40
+ [model.with(disk_usage: new_child), command]
41
+ else
42
+ [model, nil]
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
+ # SPDX-License-Identifier: MIT-0
6
+ #++
7
+
8
+ # Fetches and displays system information via +uname -a+.
9
+ # A bag for fetching and displaying system information.
10
+ module SystemInfo
11
+ Command = RatatuiRuby::Tea::Command
12
+
13
+ Model = Data.define(:output, :loading)
14
+ INITIAL = Model.new(output: "Press 's' for system info", loading: false)
15
+
16
+ VIEW = lambda do |model, tui, disabled: false|
17
+ text_style = if disabled && model.output == INITIAL.output
18
+ tui.style(fg: :dark_gray)
19
+ else
20
+ nil
21
+ end
22
+
23
+ tui.paragraph(
24
+ text: tui.text_span(content: model.output, style: text_style),
25
+ block: tui.block(title: "System Info", borders: [:all], border_style: { fg: :cyan })
26
+ )
27
+ end
28
+
29
+ UPDATE = lambda do |message, model|
30
+ case message
31
+ in [:system_info, { stdout:, status: 0 }]
32
+ [model.with(output: Ractor.make_shareable(stdout.strip), loading: false), nil]
33
+ in [:system_info, { stderr:, _status: }]
34
+ [model.with(output: Ractor.make_shareable("Error: #{stderr.strip}"), loading: false), nil]
35
+ else
36
+ [model, nil]
37
+ end
38
+ end
39
+
40
+ def self.fetch_command
41
+ Command.system("uname -a", :system_info)
42
+ end
43
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
+ # SPDX-License-Identifier: MIT-0
6
+ #++
7
+
8
+ # Displays system uptime.
9
+ # A bag for displaying system uptime.
10
+ module Uptime
11
+ Command = RatatuiRuby::Tea::Command
12
+
13
+ Model = Data.define(:output, :loading)
14
+ INITIAL = Model.new(output: "Press 'u' for uptime", loading: false)
15
+
16
+ VIEW = lambda do |model, tui, disabled: false|
17
+ text_style = if disabled && model.output == INITIAL.output
18
+ tui.style(fg: :dark_gray)
19
+ else
20
+ nil
21
+ end
22
+
23
+ tui.paragraph(
24
+ text: tui.text_span(content: model.output, style: text_style),
25
+ block: tui.block(title: "Uptime", borders: [:all], border_style: { fg: :magenta })
26
+ )
27
+ end
28
+
29
+ UPDATE = lambda do |message, model|
30
+ case message
31
+ in [:uptime, { stdout:, status: 0 }]
32
+ [model.with(output: Ractor.make_shareable(stdout.strip), loading: false), nil]
33
+ in [:uptime, { stderr:, _status: }]
34
+ [model.with(output: Ractor.make_shareable("Error: #{stderr.strip}"), loading: false), nil]
35
+ else
36
+ [model, nil]
37
+ end
38
+ end
39
+
40
+ def self.fetch_command
41
+ Command.system("uptime", :uptime)
42
+ end
43
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
+ # SPDX-License-Identifier: MIT-0
6
+ #++
7
+
8
+ require_relative "../bags/stats_panel"
9
+ require_relative "../bags/network_panel"
10
+ require_relative "../bags/custom_shell_modal"
11
+
12
+ # Shared Model, INITIAL, and VIEW for the Dashboard.
13
+ #
14
+ # This module is extended by the three UPDATE variants to demonstrate
15
+ # the progression from verbose manual routing to declarative DSL.
16
+ module DashboardBase
17
+ Command = RatatuiRuby::Tea::Command
18
+
19
+ Model = Data.define(:stats, :network, :shell_modal)
20
+
21
+ INITIAL = Model.new(
22
+ stats: StatsPanel::INITIAL,
23
+ network: NetworkPanel::INITIAL,
24
+ shell_modal: CustomShellModal::INITIAL
25
+ )
26
+
27
+ VIEW = lambda do |model, tui|
28
+ modal_active = CustomShellModal.active?(model.shell_modal)
29
+ hotkey, label_style = if modal_active
30
+ [tui.style(fg: :dark_gray), tui.style(fg: :dark_gray)]
31
+ else
32
+ [tui.style(modifiers: [:bold, :underlined]), nil]
33
+ end
34
+ dim = tui.style(fg: :dark_gray)
35
+
36
+ controls = tui.paragraph(
37
+ text: [
38
+ tui.text_line(spans: [
39
+ tui.text_span(content: "s", style: hotkey),
40
+ tui.text_span(content: ": System ", style: label_style),
41
+ tui.text_span(content: "d", style: hotkey),
42
+ tui.text_span(content: ": Disk ", style: label_style),
43
+ tui.text_span(content: "p", style: hotkey),
44
+ tui.text_span(content: ": Ping ", style: label_style),
45
+ tui.text_span(content: "u", style: hotkey),
46
+ tui.text_span(content: ": Uptime ", style: label_style),
47
+ tui.text_span(content: "c", style: hotkey),
48
+ tui.text_span(content: ": Custom ", style: label_style),
49
+ tui.text_span(content: "q", style: hotkey),
50
+ tui.text_span(content: ": Quit", style: label_style),
51
+ ]),
52
+ ],
53
+ block: tui.block(title: "Fractal Dashboard", borders: [:all], border_style: dim)
54
+ )
55
+
56
+ dashboard = tui.layout(
57
+ direction: :vertical,
58
+ constraints: [tui.constraint_fill(1), tui.constraint_fill(1), tui.constraint_length(3)],
59
+ children: [
60
+ StatsPanel::VIEW.call(model.stats, tui, disabled: modal_active),
61
+ NetworkPanel::VIEW.call(model.network, tui, disabled: modal_active),
62
+ controls,
63
+ ]
64
+ )
65
+
66
+ # Compose modal overlay if active
67
+ modal_widget = CustomShellModal::VIEW.call(model.shell_modal, tui)
68
+ if modal_widget
69
+ tui.overlay(layers: [dashboard, modal_widget])
70
+ else
71
+ dashboard
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
+ # SPDX-License-Identifier: MIT-0
6
+ #++
7
+
8
+ require_relative "base"
9
+
10
+ # UPDATE using Tea.route and Tea.delegate helpers.
11
+ #
12
+ # This is the medium-verbosity approach: routing helpers reduce boilerplate
13
+ # while keeping the case statement visible. A good middle ground.
14
+ module DashboardHelpers
15
+ Command = RatatuiRuby::Tea::Command
16
+ Tea = RatatuiRuby::Tea
17
+
18
+ # Shared with other UPDATE variants
19
+ Model = DashboardBase::Model
20
+ INITIAL = DashboardBase::INITIAL
21
+ VIEW = DashboardBase::VIEW
22
+
23
+ UPDATE = lambda do |message, model|
24
+ # Global Force Quit
25
+ return [model, RatatuiRuby::Tea::Command.exit] if message.respond_to?(:ctrl_c?) && message.ctrl_c?
26
+
27
+ # IMPORTANT: Route command results BEFORE modal intercept.
28
+ # Async command results must always reach their destination, even when a
29
+ # modal is active. Only user input (keys/mouse) should be blocked.
30
+
31
+ # Route streaming command output to modal
32
+ if (result = Tea.delegate(message, :shell_output, CustomShellModal::UPDATE, model.shell_modal))
33
+ new_modal, command = result
34
+ return [model.with(shell_modal: new_modal), command]
35
+ end
36
+
37
+ # Route to child bags
38
+ if (result = Tea.delegate(message, :stats, StatsPanel::UPDATE, model.stats))
39
+ new_child, command = result
40
+ return [model.with(stats: new_child), command && Tea.route(command, :stats)]
41
+ end
42
+
43
+ if (result = Tea.delegate(message, :network, NetworkPanel::UPDATE, model.network))
44
+ new_child, command = result
45
+ return [model.with(network: new_child), command && Tea.route(command, :network)]
46
+ end
47
+
48
+ # Modal intercepts user input (not command results)
49
+ if CustomShellModal.active?(model.shell_modal)
50
+ new_modal, command = CustomShellModal::UPDATE.call(message, model.shell_modal)
51
+ return [model.with(shell_modal: new_modal), command]
52
+ end
53
+
54
+ # Handle user input
55
+ case message
56
+ in _ if message.q? || message.ctrl_c?
57
+ Command.exit
58
+
59
+ in _ if message.c?
60
+ [model.with(shell_modal: CustomShellModal.open), nil]
61
+
62
+ in _ if message.s?
63
+ command = Tea.route(SystemInfo.fetch_command, :stats)
64
+ new_stats = model.stats.with(system_info: model.stats.system_info.with(loading: true))
65
+ [model.with(stats: new_stats), command]
66
+
67
+ in _ if message.d?
68
+ command = Tea.route(DiskUsage.fetch_command, :stats)
69
+ new_stats = model.stats.with(disk_usage: model.stats.disk_usage.with(loading: true))
70
+ [model.with(stats: new_stats), command]
71
+
72
+ in _ if message.p?
73
+ command = Tea.route(Ping.fetch_command, :network)
74
+ new_network = model.network.with(ping: model.network.ping.with(loading: true))
75
+ [model.with(network: new_network), command]
76
+
77
+ in _ if message.u?
78
+ command = Tea.route(Uptime.fetch_command, :network)
79
+ new_network = model.network.with(uptime: model.network.uptime.with(loading: true))
80
+ [model.with(network: new_network), command]
81
+
82
+ else
83
+ model
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
+ # SPDX-License-Identifier: MIT-0
6
+ #++
7
+
8
+ require_relative "base"
9
+
10
+ # UPDATE using verbose manual routing.
11
+ #
12
+ # This is the most explicit approach: full pattern matching, explicit
13
+ # Command.map calls, manual model updates. Maximum control, maximum boilerplate.
14
+ module DashboardManual
15
+ Command = RatatuiRuby::Tea::Command
16
+
17
+ # Shared with other UPDATE variants
18
+ Model = DashboardBase::Model
19
+ INITIAL = DashboardBase::INITIAL
20
+ VIEW = DashboardBase::VIEW
21
+
22
+ UPDATE = lambda do |message, model|
23
+ # Global Force Quit
24
+ return [model, RatatuiRuby::Tea::Command.exit] if message.respond_to?(:ctrl_c?) && message.ctrl_c?
25
+
26
+ # IMPORTANT: Route command results BEFORE modal intercept.
27
+ # Async command results must always reach their destination, even when a
28
+ # modal is active. Only user input (keys/mouse) should be blocked.
29
+ case message
30
+ # Route command results to panels
31
+ in [:stats, *rest]
32
+ new_panel, command = StatsPanel::UPDATE.call(rest, model.stats)
33
+ mapped_command = command ? Command.map(command) { |child_result| [:stats, *child_result] } : nil
34
+ return [model.with(stats: new_panel), mapped_command]
35
+
36
+ in [:network, *rest]
37
+ new_panel, command = NetworkPanel::UPDATE.call(rest, model.network)
38
+ mapped_command = command ? Command.map(command) { |child_result| [:network, *child_result] } : nil
39
+ return [model.with(network: new_panel), mapped_command]
40
+
41
+ in [:shell_output, *rest]
42
+ # Route streaming command output to modal
43
+ new_modal, command = CustomShellModal::UPDATE.call(message, model.shell_modal)
44
+ return [model.with(shell_modal: new_modal), command]
45
+ else
46
+ nil # Fall through to input handling
47
+ end
48
+
49
+ # Modal intercepts user input (not command results)
50
+ if CustomShellModal.active?(model.shell_modal)
51
+ new_modal, command = CustomShellModal::UPDATE.call(message, model.shell_modal)
52
+ return [model.with(shell_modal: new_modal), command]
53
+ end
54
+
55
+ case message
56
+ # Handle user input
57
+ in _ if message.q? || message.ctrl_c?
58
+ Command.exit
59
+
60
+ in _ if message.c?
61
+ [model.with(shell_modal: CustomShellModal.open), nil]
62
+
63
+ in _ if message.s?
64
+ command = Command.map(SystemInfo.fetch_command) { |r| [:stats, *r] }
65
+ new_stats = model.stats.with(system_info: model.stats.system_info.with(loading: true))
66
+ [model.with(stats: new_stats), command]
67
+
68
+ in _ if message.d?
69
+ command = Command.map(DiskUsage.fetch_command) { |r| [:stats, *r] }
70
+ new_stats = model.stats.with(disk_usage: model.stats.disk_usage.with(loading: true))
71
+ [model.with(stats: new_stats), command]
72
+
73
+ in _ if message.p?
74
+ command = Command.map(Ping.fetch_command) { |r| [:network, *r] }
75
+ new_network = model.network.with(ping: model.network.ping.with(loading: true))
76
+ [model.with(network: new_network), command]
77
+
78
+ in _ if message.u?
79
+ command = Command.map(Uptime.fetch_command) { |r| [:network, *r] }
80
+ new_network = model.network.with(uptime: model.network.uptime.with(loading: true))
81
+ [model.with(network: new_network), command]
82
+
83
+ else
84
+ model
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
+ # SPDX-License-Identifier: MIT-0
6
+ #++
7
+
8
+ require_relative "base"
9
+
10
+ # UPDATE using the declarative Tea::Router DSL.
11
+ #
12
+ # This is the minimal-boilerplate approach: declare routes and keymaps,
13
+ # let from_router generate the UPDATE lambda. Maximum DX, least control.
14
+ module DashboardRouter
15
+ include RatatuiRuby::Tea::Router
16
+
17
+ Command = RatatuiRuby::Tea::Command
18
+
19
+ # Shared with other UPDATE variants
20
+ Model = DashboardBase::Model
21
+ INITIAL = DashboardBase::INITIAL
22
+ VIEW = DashboardBase::VIEW
23
+
24
+ route :stats, to: StatsPanel
25
+ route :network, to: NetworkPanel
26
+
27
+ # Guard: only handle keys when modal is not active
28
+ MODAL_INACTIVE = -> (model) { !CustomShellModal.active?(model.shell_modal) }
29
+
30
+ keymap do
31
+ key :ctrl_c, -> { Command.exit }
32
+ only when: MODAL_INACTIVE do
33
+ key :q, -> { Command.exit }
34
+ key :s, -> { SystemInfo.fetch_command }
35
+ key :d, -> { DiskUsage.fetch_command }
36
+ key :p, -> { Ping.fetch_command }
37
+ key :u, -> { Uptime.fetch_command }
38
+ key :c, -> { CustomShellModal.open }
39
+ end
40
+ end
41
+
42
+ UPDATE = from_router
43
+ end
@@ -35,7 +35,7 @@ end
35
35
 
36
36
  UPDATE = -> (msg, model) do
37
37
  if msg.q? || msg.ctrl_c?
38
- RatatuiRuby::Tea::Cmd.quit
38
+ RatatuiRuby::Tea::Command.exit
39
39
  else
40
40
  model
41
41
  end
@@ -29,7 +29,7 @@ class VerifyReadmeUsage
29
29
 
30
30
  UPDATE = -> (msg, model) do
31
31
  if msg.q? || msg.ctrl_c?
32
- RatatuiRuby::Tea::Cmd.quit
32
+ RatatuiRuby::Tea::Command.exit
33
33
  else
34
34
  model
35
35
  end
@@ -10,7 +10,7 @@ $LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
10
10
  require "ratatui_ruby"
11
11
  require "ratatui_ruby/tea"
12
12
 
13
- # Demonstrates the Cmd.exec command for running shell commands.
13
+ # Demonstrates the Command.execute command for running shell commands.
14
14
  #
15
15
  # This example shows how to execute shell commands and handle both success
16
16
  # and failure cases using pattern matching in update. The split layout shows
@@ -23,7 +23,7 @@ require "ratatui_ruby/tea"
23
23
  # ruby examples/widget_cmd_exec/app.rb
24
24
  #
25
25
  # rdoc-image:/doc/images/widget_cmd_exec.png
26
- class WidgetCmdExec
26
+ class WidgetCommandSystem
27
27
  Model = Data.define(:result, :loading, :last_command)
28
28
  INITIAL = Model.new(
29
29
  result: "Press a key to run a command...",
@@ -44,7 +44,7 @@ class WidgetCmdExec
44
44
  "cyan"
45
45
  end
46
46
 
47
- title = model.last_command ? "Output: #{model.last_command}" : "Cmd.exec Demo"
47
+ title = model.last_command ? "Output: #{model.last_command}" : "Command.execute Demo"
48
48
  content_text = model.loading ? "Running command..." : model.result
49
49
 
50
50
  # 1. Main Output Widget
@@ -97,8 +97,8 @@ class WidgetCmdExec
97
97
  )
98
98
  end
99
99
 
100
- UPDATE = -> (msg, model) do
101
- case msg
100
+ UPDATE = -> (message, model) do
101
+ case message
102
102
  # Handle command results
103
103
  in [:got_output, { stdout:, status: 0 }]
104
104
  [model.with(result: stdout.strip.freeze, loading: false), nil]
@@ -106,19 +106,19 @@ class WidgetCmdExec
106
106
  [model.with(result: "Error (exit #{status}): #{stderr.strip}".freeze, loading: false), nil]
107
107
 
108
108
  # Handle key presses
109
- in _ if msg.q? || msg.ctrl_c?
110
- RatatuiRuby::Tea::Cmd.quit
111
- in _ if msg.d?
112
- [model.with(loading: true, last_command: "ls -la"), RatatuiRuby::Tea::Cmd.exec("ls -la", :got_output)]
113
- in _ if msg.u?
114
- [model.with(loading: true, last_command: "uname -a"), RatatuiRuby::Tea::Cmd.exec("uname -a", :got_output)]
115
- in _ if msg.s?
116
- cmd = "sleep 3 && echo 'Slept for 3s'"
117
- [model.with(loading: true, last_command: cmd.freeze), RatatuiRuby::Tea::Cmd.exec(cmd, :got_output)]
118
- in _ if msg.f?
109
+ in _ if message.q? || message.ctrl_c?
110
+ RatatuiRuby::Tea::Command.exit
111
+ in _ if message.d?
112
+ [model.with(loading: true, last_command: "ls -la"), RatatuiRuby::Tea::Command.system("ls -la", :got_output)]
113
+ in _ if message.u?
114
+ [model.with(loading: true, last_command: "uname -a"), RatatuiRuby::Tea::Command.system("uname -a", :got_output)]
115
+ in _ if message.s?
116
+ command = "sleep 3 && echo 'Slept for 3s'"
117
+ [model.with(loading: true, last_command: cmd.freeze), RatatuiRuby::Tea::Command.system(cmd, :got_output)]
118
+ in _ if message.f?
119
119
  # Intentional failure to demonstrate error handling
120
- cmd = "ls /nonexistent_path_12345"
121
- [model.with(loading: true, last_command: cmd.freeze), RatatuiRuby::Tea::Cmd.exec(cmd, :got_output)]
120
+ command = "ls /nonexistent_path_12345"
121
+ [model.with(loading: true, last_command: cmd.freeze), RatatuiRuby::Tea::Command.system(cmd, :got_output)]
122
122
  else
123
123
  model
124
124
  end
@@ -129,4 +129,4 @@ class WidgetCmdExec
129
129
  end
130
130
  end
131
131
 
132
- WidgetCmdExec.new.run if __FILE__ == $PROGRAM_NAME
132
+ WidgetCommandSystem.new.run if __FILE__ == $PROGRAM_NAME