ace-tmux 0.14.3 → 0.17.2
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +58 -0
- data/README.md +11 -3
- data/docs/demo/ace-tmux-getting-started.tape.yml +5 -1
- data/docs/getting-started.md +7 -3
- data/docs/usage.md +168 -10
- data/lib/ace/tmux/atoms/named_key_registry.rb +34 -0
- data/lib/ace/tmux/atoms/tmux_command_builder.rb +28 -1
- data/lib/ace/tmux/atoms/window_name_sanitizer.rb +25 -0
- data/lib/ace/tmux/cli/commands/attach.rb +26 -0
- data/lib/ace/tmux/cli/commands/capture.rb +41 -0
- data/lib/ace/tmux/cli/commands/detach.rb +28 -0
- data/lib/ace/tmux/cli/commands/list.rb +113 -48
- data/lib/ace/tmux/cli/commands/list_presets.rb +67 -0
- data/lib/ace/tmux/cli/commands/send.rb +169 -0
- data/lib/ace/tmux/cli/commands/wait.rb +57 -0
- data/lib/ace/tmux/cli/commands/window.rb +2 -3
- data/lib/ace/tmux/cli.rb +27 -2
- data/lib/ace/tmux/models/runtime_target.rb +46 -0
- data/lib/ace/tmux/molecules/local_process_inspector.rb +84 -0
- data/lib/ace/tmux/molecules/runtime_target_resolver.rb +267 -0
- data/lib/ace/tmux/molecules/tmux_executor.rb +8 -0
- data/lib/ace/tmux/molecules/wait_condition_validator.rb +25 -0
- data/lib/ace/tmux/organisms/control_surface.rb +523 -0
- data/lib/ace/tmux/organisms/session_manager.rb +17 -3
- data/lib/ace/tmux/organisms/window_manager.rb +8 -3
- data/lib/ace/tmux/version.rb +1 -1
- data/lib/ace/tmux.rb +11 -0
- metadata +15 -2
|
@@ -7,76 +7,141 @@ module Ace
|
|
|
7
7
|
module Tmux
|
|
8
8
|
module CLI
|
|
9
9
|
module Commands
|
|
10
|
-
# List available tmux presets
|
|
11
10
|
class List < Ace::Support::Cli::Command
|
|
12
11
|
include Ace::Support::Cli::Base
|
|
13
12
|
|
|
14
13
|
desc <<~DESC.strip
|
|
15
|
-
List
|
|
14
|
+
List live tmux sessions, windows, or panes
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
ace-tmux list [TYPE]
|
|
16
|
+
Default behavior lists panes in the resolved current window.
|
|
19
17
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
# List only session presets
|
|
26
|
-
$ ace-tmux list sessions
|
|
27
|
-
|
|
28
|
-
# List only window presets
|
|
29
|
-
$ ace-tmux list windows
|
|
18
|
+
Scope flags:
|
|
19
|
+
--all-panes List panes across the resolved session
|
|
20
|
+
--windows List windows in the resolved session
|
|
21
|
+
--sessions List all tmux sessions
|
|
30
22
|
DESC
|
|
31
23
|
|
|
32
24
|
example [
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"windows
|
|
36
|
-
"
|
|
25
|
+
" # List panes in the current window",
|
|
26
|
+
"--all-panes # List panes across the current session",
|
|
27
|
+
"--windows # List windows in the current session",
|
|
28
|
+
"--sessions # List tmux sessions",
|
|
29
|
+
"--session dev --window work # List panes in an explicit window"
|
|
37
30
|
]
|
|
38
31
|
|
|
39
|
-
|
|
32
|
+
option :session, type: :string, aliases: %w[-s], desc: "Target session name"
|
|
33
|
+
option :window, type: :string, aliases: %w[-w], desc: "Target window name, index, or tmux window id (@2)"
|
|
34
|
+
option :all_panes, type: :boolean, desc: "List panes across the resolved session"
|
|
35
|
+
option :windows, type: :boolean, desc: "List windows in the resolved session"
|
|
36
|
+
option :sessions, type: :boolean, desc: "List all tmux sessions"
|
|
37
|
+
|
|
38
|
+
def call(**options)
|
|
39
|
+
validate_scope!(options)
|
|
40
|
+
|
|
41
|
+
control = Organisms::ControlSurface.new
|
|
42
|
+
scope = selected_scope(options)
|
|
43
|
+
|
|
44
|
+
case scope
|
|
45
|
+
when :sessions
|
|
46
|
+
print_sessions(control.list_sessions)
|
|
47
|
+
when :windows
|
|
48
|
+
print_windows(control.list_windows(session: options[:session]))
|
|
49
|
+
when :all_panes
|
|
50
|
+
print_panes(control.list_panes(session: options[:session], all_panes: true))
|
|
51
|
+
else
|
|
52
|
+
print_panes(control.list_panes(session: options[:session], window: options[:window]))
|
|
53
|
+
end
|
|
54
|
+
rescue Ace::Tmux::Error => e
|
|
55
|
+
raise Ace::Support::Cli::Error, e.message
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def validate_scope!(options)
|
|
61
|
+
scopes = %i[all_panes windows sessions].select { |key| options[key] }
|
|
62
|
+
raise Ace::Support::Cli::Error, "Use only one of --all-panes, --windows, or --sessions" if scopes.length > 1
|
|
63
|
+
|
|
64
|
+
if options[:sessions] && (options[:session] || options[:window])
|
|
65
|
+
raise Ace::Support::Cli::Error, "--sessions does not accept --session or --window"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
if options[:windows] && options[:window]
|
|
69
|
+
raise Ace::Support::Cli::Error, "--windows does not accept --window"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
if options[:all_panes] && options[:window]
|
|
73
|
+
raise Ace::Support::Cli::Error, "--all-panes does not accept --window"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def selected_scope(options)
|
|
78
|
+
return :sessions if options[:sessions]
|
|
79
|
+
return :windows if options[:windows]
|
|
80
|
+
return :all_panes if options[:all_panes]
|
|
81
|
+
|
|
82
|
+
:panes
|
|
83
|
+
end
|
|
40
84
|
|
|
41
|
-
|
|
42
|
-
|
|
85
|
+
def print_sessions(rows)
|
|
86
|
+
return puts("No tmux sessions found.") if rows.empty?
|
|
43
87
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
88
|
+
table = build_table(
|
|
89
|
+
%w[SESSION ATTACHED WINDOWS],
|
|
90
|
+
rows.map { |row| [row[:session], row[:attached_clients].to_s, row[:window_count].to_s] }
|
|
47
91
|
)
|
|
92
|
+
puts table
|
|
93
|
+
end
|
|
48
94
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
95
|
+
def print_windows(rows)
|
|
96
|
+
return puts("No windows found.") if rows.empty?
|
|
97
|
+
|
|
98
|
+
table = build_table(
|
|
99
|
+
%w[A WINDOW SESSION:INDEX NAME PANES],
|
|
100
|
+
rows.map do |row|
|
|
101
|
+
[
|
|
102
|
+
row[:active] ? "*" : " ",
|
|
103
|
+
row[:id],
|
|
104
|
+
"#{row[:session]}:#{row[:index]}",
|
|
105
|
+
row[:name],
|
|
106
|
+
row[:pane_count].to_s
|
|
107
|
+
]
|
|
57
108
|
end
|
|
58
|
-
|
|
59
|
-
|
|
109
|
+
)
|
|
110
|
+
puts table
|
|
60
111
|
end
|
|
61
112
|
|
|
62
|
-
|
|
113
|
+
def print_panes(rows)
|
|
114
|
+
return puts("No panes found.") if rows.empty?
|
|
115
|
+
|
|
116
|
+
table = build_table(
|
|
117
|
+
%w[A PANE TARGET CMD CWD],
|
|
118
|
+
rows.map do |row|
|
|
119
|
+
[
|
|
120
|
+
row[:active] ? "*" : " ",
|
|
121
|
+
row[:pane_id],
|
|
122
|
+
row[:target],
|
|
123
|
+
row[:command],
|
|
124
|
+
row[:cwd]
|
|
125
|
+
]
|
|
126
|
+
end
|
|
127
|
+
)
|
|
128
|
+
puts table
|
|
129
|
+
end
|
|
63
130
|
|
|
64
|
-
def
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
"Unknown preset type: #{type}. Valid types: #{Molecules::PresetLoader::PRESET_TYPES.join(", ")}"
|
|
68
|
-
)
|
|
131
|
+
def build_table(headers, rows)
|
|
132
|
+
widths = headers.each_index.map do |index|
|
|
133
|
+
([headers[index]] + rows.map { |row| row[index].to_s }).map(&:length).max
|
|
69
134
|
end
|
|
135
|
+
|
|
136
|
+
lines = []
|
|
137
|
+
lines << render_row(headers, widths)
|
|
138
|
+
lines << render_row(widths.map { |width| "-" * width }, widths)
|
|
139
|
+
rows.each { |row| lines << render_row(row, widths) }
|
|
140
|
+
lines.join("\n")
|
|
70
141
|
end
|
|
71
142
|
|
|
72
|
-
def
|
|
73
|
-
|
|
74
|
-
if presets.empty?
|
|
75
|
-
puts " (none)"
|
|
76
|
-
else
|
|
77
|
-
presets.each { |name| puts " #{name}" }
|
|
78
|
-
end
|
|
79
|
-
puts
|
|
143
|
+
def render_row(values, widths)
|
|
144
|
+
values.each_with_index.map { |value, index| value.to_s.ljust(widths[index]) }.join(" ").rstrip
|
|
80
145
|
end
|
|
81
146
|
end
|
|
82
147
|
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ace/support/cli"
|
|
4
|
+
require "ace/core"
|
|
5
|
+
|
|
6
|
+
module Ace
|
|
7
|
+
module Tmux
|
|
8
|
+
module CLI
|
|
9
|
+
module Commands
|
|
10
|
+
class ListPresets < Ace::Support::Cli::Command
|
|
11
|
+
include Ace::Support::Cli::Base
|
|
12
|
+
|
|
13
|
+
desc <<~DESC.strip
|
|
14
|
+
List available tmux presets
|
|
15
|
+
|
|
16
|
+
Run without a type to list all preset groups, or pass one of:
|
|
17
|
+
sessions
|
|
18
|
+
windows
|
|
19
|
+
panes
|
|
20
|
+
DESC
|
|
21
|
+
|
|
22
|
+
example [
|
|
23
|
+
" # List all preset groups",
|
|
24
|
+
"sessions # List session presets",
|
|
25
|
+
"windows # List window presets",
|
|
26
|
+
"panes # List pane presets"
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
argument :type, required: false, desc: "Preset type: sessions, windows, or panes"
|
|
30
|
+
|
|
31
|
+
def call(type: nil, **_options)
|
|
32
|
+
preset_loader = Molecules::PresetLoader.new(gem_root: Tmux.gem_root)
|
|
33
|
+
|
|
34
|
+
if type
|
|
35
|
+
validate_type!(type)
|
|
36
|
+
display_type(type, preset_loader.list(type))
|
|
37
|
+
return
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
all = preset_loader.list_all
|
|
41
|
+
all.each { |preset_type, presets| display_type(preset_type, presets) }
|
|
42
|
+
puts "No presets found." if all.empty?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def validate_type!(type)
|
|
48
|
+
return if Molecules::PresetLoader::PRESET_TYPES.include?(type)
|
|
49
|
+
|
|
50
|
+
raise Ace::Support::Cli::Error,
|
|
51
|
+
"Unknown preset type: #{type}. Valid types: #{Molecules::PresetLoader::PRESET_TYPES.join(", ")}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def display_type(type, presets)
|
|
55
|
+
puts "#{type}:"
|
|
56
|
+
if presets.empty?
|
|
57
|
+
puts " (none)"
|
|
58
|
+
else
|
|
59
|
+
presets.each { |name| puts " #{name}" }
|
|
60
|
+
end
|
|
61
|
+
puts
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ace/support/cli"
|
|
4
|
+
require "ace/core"
|
|
5
|
+
|
|
6
|
+
module Ace
|
|
7
|
+
module Tmux
|
|
8
|
+
module CLI
|
|
9
|
+
module Commands
|
|
10
|
+
class Send < Ace::Support::Cli::Command
|
|
11
|
+
include Ace::Support::Cli::Base
|
|
12
|
+
|
|
13
|
+
DEFAULT_CAPTURE_LINES = 40
|
|
14
|
+
DEFAULT_CAPTURE_WAIT = 2.0
|
|
15
|
+
|
|
16
|
+
desc <<~DESC.strip
|
|
17
|
+
Send submitted commands, raw text, or named keys to a tmux pane
|
|
18
|
+
|
|
19
|
+
Bare --wait defaults to the interactive agent condition.
|
|
20
|
+
Bare --capture prints a post-send pane tail using default line and wait values.
|
|
21
|
+
DESC
|
|
22
|
+
|
|
23
|
+
example [
|
|
24
|
+
"--pane %1 --cmd 'bundle exec rake test'",
|
|
25
|
+
"--pane %8 --cmd 'Reply with exactly: pong' --wait --capture 20",
|
|
26
|
+
"--pane %1 --msg 'echo ready' --key Enter",
|
|
27
|
+
"--pane %1 --cmd 'echo done' --wait output --pattern done --capture 20"
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
option :session, type: :string, aliases: %w[-s], desc: "Target session name"
|
|
31
|
+
option :window, type: :string, aliases: %w[-w], desc: "Target window name, index, or tmux window id (@2)"
|
|
32
|
+
option :pane, type: :string, aliases: %w[-p], desc: "Target pane id (%8), full pane target (dev:work.1), or current-window pane shorthand (.1)"
|
|
33
|
+
option :cmd, type: :string, aliases: %w[-c], desc: "Command text to send and submit with Enter"
|
|
34
|
+
option :msg, type: :string, aliases: %w[-m], repeat: true, desc: "Literal text chunk to send without Enter"
|
|
35
|
+
option :key, type: :string, aliases: %w[-k], repeat: true, desc: "Named key to send (for example Enter, C-c)"
|
|
36
|
+
option :capture, type: :string, default: false, optional_value: true, desc: "Capture pane output after send as [lines[:wait]]; bare --capture defaults to 40 lines"
|
|
37
|
+
option :wait, type: :string, default: false, optional_value: true, desc: "Wait after send as [agent|output|window-exists|window-active|pane-exists|pane-exited]; bare --wait defaults to agent"
|
|
38
|
+
option :pattern, type: :string, desc: "Pattern to match when waiting for output"
|
|
39
|
+
option :timeout, type: :string, aliases: %w[-t], default: Organisms::ControlSurface::DEFAULT_TIMEOUT.to_s, desc: "Wait timeout in seconds when --wait is used"
|
|
40
|
+
option :interval, type: :string, aliases: %w[-i], default: Organisms::ControlSurface::DEFAULT_INTERVAL.to_s, desc: "Wait polling interval in seconds when --wait is used"
|
|
41
|
+
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
|
|
42
|
+
|
|
43
|
+
def call(**options)
|
|
44
|
+
control = Organisms::ControlSurface.new
|
|
45
|
+
cmd = normalized_cmd(options)
|
|
46
|
+
messages = normalized_messages(options)
|
|
47
|
+
keys = normalized_keys(options)
|
|
48
|
+
wait_condition = normalized_wait_condition(options.fetch(:wait, false))
|
|
49
|
+
|
|
50
|
+
validate_payloads!(cmd: cmd, messages: messages, keys: keys)
|
|
51
|
+
validate_wait_condition!(wait_condition: wait_condition, pattern: options[:pattern]) if wait_condition
|
|
52
|
+
|
|
53
|
+
capture_spec = parse_capture_spec(
|
|
54
|
+
options.fetch(:capture, false),
|
|
55
|
+
default_wait: wait_condition ? 0.0 : DEFAULT_CAPTURE_WAIT
|
|
56
|
+
)
|
|
57
|
+
wait_lines = capture_spec ? capture_spec[:lines] : DEFAULT_CAPTURE_LINES
|
|
58
|
+
baseline_output = wait_baseline_for(control, options, wait_condition, lines: wait_lines)
|
|
59
|
+
|
|
60
|
+
control.send_sequence(**target_options(options), command: cmd, messages: messages, keys: keys)
|
|
61
|
+
|
|
62
|
+
if wait_condition
|
|
63
|
+
control.wait_for_condition(
|
|
64
|
+
**target_options(options),
|
|
65
|
+
condition: wait_condition,
|
|
66
|
+
pattern: options[:pattern],
|
|
67
|
+
timeout: Float(options.fetch(:timeout, Organisms::ControlSurface::DEFAULT_TIMEOUT.to_s)),
|
|
68
|
+
interval: Float(options.fetch(:interval, Organisms::ControlSurface::DEFAULT_INTERVAL.to_s)),
|
|
69
|
+
lines: wait_lines,
|
|
70
|
+
baseline_output: baseline_output,
|
|
71
|
+
require_change: wait_condition == "agent"
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
if capture_spec
|
|
76
|
+
sleep(capture_spec[:wait]) if capture_spec[:wait].positive?
|
|
77
|
+
output = control.capture_recent_output(**target_options(options), lines: capture_spec[:lines])
|
|
78
|
+
puts output
|
|
79
|
+
elsif !options[:quiet]
|
|
80
|
+
puts success_message(cmd: cmd, messages: messages, keys: keys, wait_condition: wait_condition)
|
|
81
|
+
end
|
|
82
|
+
rescue Ace::Tmux::Error => e
|
|
83
|
+
raise Ace::Support::Cli::Error, e.message
|
|
84
|
+
rescue ArgumentError
|
|
85
|
+
raise Ace::Support::Cli::Error, "--timeout and --interval must be numeric"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def target_options(options)
|
|
91
|
+
options.slice(:session, :window, :pane)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def normalized_cmd(options)
|
|
95
|
+
value = options[:cmd].to_s
|
|
96
|
+
return nil if value.strip.empty?
|
|
97
|
+
|
|
98
|
+
value
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def normalized_messages(options)
|
|
102
|
+
Array(options[:msg]).map(&:to_s)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def normalized_keys(options)
|
|
106
|
+
Array(options[:key]).map(&:to_s).reject { |value| value.strip.empty? }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def validate_payloads!(cmd:, messages:, keys:)
|
|
110
|
+
raise Ace::Support::Cli::Error, "Provide at least one of --cmd, --msg, or --key" if cmd.nil? && messages.empty? && keys.empty?
|
|
111
|
+
raise Ace::Support::Cli::Error, "Use either --cmd or --msg, not both" if cmd && !messages.empty?
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def parse_capture_spec(raw, default_wait: DEFAULT_CAPTURE_WAIT)
|
|
115
|
+
return nil if raw == false
|
|
116
|
+
return { lines: DEFAULT_CAPTURE_LINES, wait: default_wait } if raw.nil? || raw == true || raw.to_s.strip.empty?
|
|
117
|
+
|
|
118
|
+
value = raw.to_s.strip
|
|
119
|
+
parts = value.split(":", 2)
|
|
120
|
+
|
|
121
|
+
lines = Integer(parts[0])
|
|
122
|
+
wait = parts[1] ? Float(parts[1]) : default_wait
|
|
123
|
+
raise ArgumentError if lines <= 0 || wait.negative?
|
|
124
|
+
|
|
125
|
+
{ lines: lines, wait: wait }
|
|
126
|
+
rescue ArgumentError
|
|
127
|
+
raise Ace::Support::Cli::Error, "Invalid --capture value '#{raw}'. Use [lines[:wait]] such as 40 or 40:2"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def normalized_wait_condition(raw)
|
|
131
|
+
return nil if raw == false
|
|
132
|
+
return "agent" if raw.nil? || raw == true || raw.to_s.strip.empty?
|
|
133
|
+
|
|
134
|
+
raw.to_s.strip
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def validate_wait_condition!(wait_condition:, pattern:)
|
|
138
|
+
Molecules::WaitConditionValidator.validate!(condition: wait_condition, pattern: pattern)
|
|
139
|
+
rescue Ace::Tmux::ValidationError => e
|
|
140
|
+
raise Ace::Support::Cli::Error, e.message
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def wait_baseline_for(control, options, wait_condition, lines:)
|
|
144
|
+
return nil unless %w[agent output].include?(wait_condition)
|
|
145
|
+
|
|
146
|
+
control.capture_recent_output(**target_options(options), lines: lines)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def success_message(cmd:, messages:, keys:, wait_condition:)
|
|
150
|
+
message =
|
|
151
|
+
if cmd
|
|
152
|
+
"Sent command"
|
|
153
|
+
elsif !messages.empty? && !keys.empty?
|
|
154
|
+
"Sent #{messages.length} message#{messages.length == 1 ? '' : 's'} and #{keys.length} key#{keys.length == 1 ? '' : 's'}"
|
|
155
|
+
elsif !messages.empty?
|
|
156
|
+
"Sent #{messages.length} message#{messages.length == 1 ? '' : 's'}"
|
|
157
|
+
else
|
|
158
|
+
"Sent #{keys.length} key#{keys.length == 1 ? '' : 's'}"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
return message unless wait_condition
|
|
162
|
+
|
|
163
|
+
"#{message} and waited for #{wait_condition}"
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ace/support/cli"
|
|
4
|
+
require "ace/core"
|
|
5
|
+
|
|
6
|
+
module Ace
|
|
7
|
+
module Tmux
|
|
8
|
+
module CLI
|
|
9
|
+
module Commands
|
|
10
|
+
class Wait < Ace::Support::Cli::Command
|
|
11
|
+
include Ace::Support::Cli::Base
|
|
12
|
+
|
|
13
|
+
desc <<~DESC.strip
|
|
14
|
+
Wait for a bounded tmux condition
|
|
15
|
+
|
|
16
|
+
Use --lines to control the pane tail observed by output and agent waits.
|
|
17
|
+
DESC
|
|
18
|
+
|
|
19
|
+
example [
|
|
20
|
+
"--pane %1 --for output --pattern 'Task context:' --lines 80",
|
|
21
|
+
"--pane %8 --for agent",
|
|
22
|
+
"--session dev --for window-active --window work-fs",
|
|
23
|
+
"--pane %1 --for pane-exited"
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
option :for, type: :string, aliases: %w[-f], desc: "Condition: agent|output|window-exists|window-active|pane-exists|pane-exited"
|
|
27
|
+
option :session, type: :string, aliases: %w[-s], desc: "Target session name"
|
|
28
|
+
option :window, type: :string, aliases: %w[-w], desc: "Target window name, index, or tmux window id (@2)"
|
|
29
|
+
option :pane, type: :string, aliases: %w[-p], desc: "Target pane id (%8), full pane target (dev:work.1), or current-window pane shorthand (.1)"
|
|
30
|
+
option :pattern, type: :string, desc: "Pattern to match when waiting for output"
|
|
31
|
+
option :lines, type: :integer, aliases: %w[-n], default: Organisms::ControlSurface::DEFAULT_LINES, desc: "Number of lines to observe for output and agent waits"
|
|
32
|
+
option :timeout, type: :string, aliases: %w[-t], default: "10.0", desc: "Timeout in seconds"
|
|
33
|
+
option :interval, type: :string, aliases: %w[-i], default: "0.2", desc: "Polling interval in seconds"
|
|
34
|
+
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
|
|
35
|
+
|
|
36
|
+
def call(**options)
|
|
37
|
+
Organisms::ControlSurface.new.wait_for_condition(
|
|
38
|
+
condition: options[:for],
|
|
39
|
+
session: options[:session],
|
|
40
|
+
window: options[:window],
|
|
41
|
+
pane: options[:pane],
|
|
42
|
+
pattern: options[:pattern],
|
|
43
|
+
lines: options[:lines],
|
|
44
|
+
timeout: Float(options[:timeout]),
|
|
45
|
+
interval: Float(options[:interval])
|
|
46
|
+
)
|
|
47
|
+
puts "Condition met: #{options[:for]}" unless options[:quiet]
|
|
48
|
+
rescue Ace::Tmux::Error => e
|
|
49
|
+
raise Ace::Support::Cli::Error, e.message
|
|
50
|
+
rescue ArgumentError
|
|
51
|
+
raise Ace::Support::Cli::Error, "--timeout and --interval must be numeric"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -59,9 +59,8 @@ module Ace
|
|
|
59
59
|
tmux: tmux_bin
|
|
60
60
|
)
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
manager.add_window(preset, session: options[:session], root: options[:root], name: options[:name])
|
|
62
|
+
puts "Adding window from preset '#{preset}'..." unless options[:quiet]
|
|
63
|
+
effective_name = manager.add_window(preset, session: options[:session], root: options[:root], name: options[:name])
|
|
65
64
|
puts "Window '#{effective_name}' added." unless options[:quiet]
|
|
66
65
|
rescue Ace::Tmux::PresetNotFoundError => e
|
|
67
66
|
raise Ace::Support::Cli::Error.new(e.message)
|
data/lib/ace/tmux/cli.rb
CHANGED
|
@@ -6,6 +6,12 @@ require_relative "../tmux"
|
|
|
6
6
|
require_relative "cli/commands/start"
|
|
7
7
|
require_relative "cli/commands/window"
|
|
8
8
|
require_relative "cli/commands/list"
|
|
9
|
+
require_relative "cli/commands/list_presets"
|
|
10
|
+
require_relative "cli/commands/send"
|
|
11
|
+
require_relative "cli/commands/capture"
|
|
12
|
+
require_relative "cli/commands/wait"
|
|
13
|
+
require_relative "cli/commands/attach"
|
|
14
|
+
require_relative "cli/commands/detach"
|
|
9
15
|
|
|
10
16
|
module Ace
|
|
11
17
|
module Tmux
|
|
@@ -19,13 +25,26 @@ module Ace
|
|
|
19
25
|
REGISTERED_COMMANDS = [
|
|
20
26
|
["start", "Start tmux session from preset"],
|
|
21
27
|
["window", "Add window to existing session"],
|
|
22
|
-
["list", "List
|
|
28
|
+
["list", "List live tmux sessions, windows, or panes"],
|
|
29
|
+
["--list-presets", "List available tmux presets"],
|
|
30
|
+
["send", "Send submitted commands, raw text, or named keys to a pane"],
|
|
31
|
+
["capture", "Capture recent output from a pane"],
|
|
32
|
+
["wait", "Wait for a bounded tmux condition"],
|
|
33
|
+
["attach", "Attach to a tmux session"],
|
|
34
|
+
["detach", "Detach clients from a tmux session"]
|
|
23
35
|
].freeze
|
|
24
36
|
|
|
25
37
|
HELP_EXAMPLES = [
|
|
26
38
|
"ace-tmux start dev # Launch dev session preset",
|
|
27
39
|
"ace-tmux window cc --root ~/project # Add window to session",
|
|
28
|
-
"ace-tmux list
|
|
40
|
+
"ace-tmux --list-presets windows # Available window presets",
|
|
41
|
+
"ace-tmux list # Panes in the current window",
|
|
42
|
+
"ace-tmux list --windows # Windows in the current session",
|
|
43
|
+
"ace-tmux send --pane %1 --cmd 'bundle exec rake test' # Send command",
|
|
44
|
+
"ace-tmux send --pane .1 --msg 'echo ready' --key Enter # Current-window pane shortcut",
|
|
45
|
+
"ace-tmux wait --session dev --for window-active --window work-fs # Wait for window",
|
|
46
|
+
"ace-tmux capture --pane %1 --lines 40 # Capture pane output",
|
|
47
|
+
"ace-tmux detach --session dev # Detach session clients"
|
|
29
48
|
].freeze
|
|
30
49
|
|
|
31
50
|
# Start the CLI
|
|
@@ -40,6 +59,12 @@ module Ace
|
|
|
40
59
|
register "start", CLI::Commands::Start.new
|
|
41
60
|
register "window", CLI::Commands::Window.new
|
|
42
61
|
register "list", CLI::Commands::List.new
|
|
62
|
+
register "--list-presets", CLI::Commands::ListPresets.new
|
|
63
|
+
register "send", CLI::Commands::Send.new
|
|
64
|
+
register "capture", CLI::Commands::Capture.new
|
|
65
|
+
register "wait", CLI::Commands::Wait.new
|
|
66
|
+
register "attach", CLI::Commands::Attach.new
|
|
67
|
+
register "detach", CLI::Commands::Detach.new
|
|
43
68
|
|
|
44
69
|
# Register version command
|
|
45
70
|
version_cmd = Ace::Support::Cli::VersionCommand.build(
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Tmux
|
|
5
|
+
module Models
|
|
6
|
+
class RuntimeTarget
|
|
7
|
+
attr_reader :session, :window, :pane, :source
|
|
8
|
+
|
|
9
|
+
def initialize(session: nil, window: nil, pane: nil, source:, raw_window_target: nil)
|
|
10
|
+
@session = presence(session)
|
|
11
|
+
@window = presence(window)
|
|
12
|
+
@pane = presence(pane)
|
|
13
|
+
@source = source.to_s
|
|
14
|
+
@raw_window_target = presence(raw_window_target)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def session_target
|
|
18
|
+
session
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def window_target
|
|
22
|
+
return @raw_window_target if @raw_window_target
|
|
23
|
+
return nil unless session && window
|
|
24
|
+
|
|
25
|
+
"#{session}:#{window}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def pane_target
|
|
29
|
+
return pane if pane&.start_with?("%")
|
|
30
|
+
return pane if pane&.start_with?("@")
|
|
31
|
+
return pane if pane&.include?(":")
|
|
32
|
+
return nil unless pane && window_target
|
|
33
|
+
|
|
34
|
+
"#{window_target}.#{pane}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def presence(value)
|
|
40
|
+
normalized = value.to_s.strip
|
|
41
|
+
normalized.empty? ? nil : normalized
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|