mxup 0.2.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.
@@ -0,0 +1,253 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mxup
4
+ # Public programmatic API. Composes the focused helpers — nothing here
5
+ # should contain real business logic, only wiring and delegation.
6
+ class Runner
7
+ # Delay (seconds) between the two Ctrl-C presses during a restart, and
8
+ # between Ctrl-C and sending the new command. In production 1s is enough
9
+ # to let the pane's shell redraw its prompt; tests shorten this to avoid
10
+ # paying multi-second waits for a purely cosmetic settle.
11
+ DEFAULT_INTERRUPT_DELAY = 1.0
12
+
13
+ class << self
14
+ attr_writer :interrupt_delay
15
+
16
+ def interrupt_delay
17
+ @interrupt_delay || DEFAULT_INTERRUPT_DELAY
18
+ end
19
+ end
20
+
21
+ def initialize(config, dry_run: false, layout: nil)
22
+ @config = config
23
+ @session = config.session
24
+ @dry_run = dry_run
25
+ @layout = layout
26
+ end
27
+
28
+ # --- primary commands --------------------------------------------------
29
+
30
+ def up
31
+ handle_profile_switch
32
+ reconciler.up
33
+ end
34
+
35
+ def status(lines:)
36
+ status_view.render(lines: lines)
37
+ end
38
+
39
+ def down
40
+ unless Tmux.has_session?(@session)
41
+ $stdout.puts "Session #{@session} is not running."
42
+ return
43
+ end
44
+
45
+ if @dry_run
46
+ $stdout.puts "[dry-run] Would gracefully stop session #{@session}"
47
+ return
48
+ end
49
+
50
+ graceful_stop.call
51
+ Tmux.kill_session(@session)
52
+ launcher.cleanup
53
+ $stdout.puts "Session #{@session} killed."
54
+ end
55
+
56
+ def restart(window_names)
57
+ abort "Session #{@session} is not running." unless Tmux.has_session?(@session)
58
+ launcher.write_all unless @dry_run
59
+
60
+ targets = resolve_restart_targets(window_names)
61
+
62
+ targets.each do |win|
63
+ target_ref = resolver.pane_target(win.name)
64
+ if target_ref
65
+ restart_existing(win, target_ref)
66
+ else
67
+ create_missing(win)
68
+ end
69
+ end
70
+ end
71
+
72
+ def switch_layout(target_layout)
73
+ layout_manager.switch(target_layout)
74
+ end
75
+
76
+ def show_layouts
77
+ layout_manager.show(
78
+ active: Tmux.has_session?(@session) ? resolver.stored_layout : nil
79
+ )
80
+ end
81
+
82
+ def target(window_names)
83
+ abort "Session #{@session} is not running." unless Tmux.has_session?(@session)
84
+
85
+ requested = Array(window_names).flat_map { |n| n.split(',') }.reject(&:empty?)
86
+
87
+ if requested.empty?
88
+ print_all_targets
89
+ else
90
+ print_requested_targets(requested)
91
+ end
92
+ end
93
+
94
+ def exec(target_spec, command, lines: 50, timeout: nil, force: false, quiet: false)
95
+ exec_runner.run(target_spec, command,
96
+ lines: lines, timeout: timeout,
97
+ force: force, quiet: quiet)
98
+ end
99
+
100
+ private
101
+
102
+ # --- profile switching -------------------------------------------------
103
+
104
+ # Before `up` runs, make sure we aren't stepping on a different profile
105
+ # of the same group. If the live session was brought up under a different
106
+ # profile, tear it down first so the new profile starts from a clean
107
+ # slate. Running profiles are expected to be mutually exclusive.
108
+ def handle_profile_switch
109
+ return unless @config.profile
110
+ return unless Tmux.has_session?(@session)
111
+
112
+ stored = Tmux.show_environment(@session, 'MXUP_PROFILE')
113
+ return if stored.nil? || stored == @config.profile
114
+
115
+ if @dry_run
116
+ $stdout.puts "[dry-run] Would tear down profile '#{stored}' before " \
117
+ "bringing up '#{@config.profile}'"
118
+ return
119
+ end
120
+
121
+ $stdout.puts "Switching profile: '#{stored}' -> '#{@config.profile}' " \
122
+ '(tearing down current profile first)...'
123
+ down
124
+ end
125
+
126
+ # --- restart helpers ---------------------------------------------------
127
+
128
+ def resolve_restart_targets(names)
129
+ return @config.windows if names.nil? || names.empty?
130
+
131
+ names.flat_map { |n| n.split(',') }.map do |n|
132
+ @config.windows.find { |w| w.name == n } ||
133
+ abort("Window '#{n}' not found in config.")
134
+ end
135
+ end
136
+
137
+ def restart_existing(win, target_ref)
138
+ if @dry_run
139
+ $stdout.puts "[dry-run] Would restart #{win.name}"
140
+ return
141
+ end
142
+ # Two C-c's handle both "foreground is command" and "foreground is a
143
+ # sub-prompt waiting on input"; the sleep gives the shell time to redraw.
144
+ delay = Runner.interrupt_delay
145
+ Tmux.send_interrupt(@session, target_ref)
146
+ sleep delay
147
+ Tmux.send_interrupt(@session, target_ref)
148
+ sleep delay
149
+ Tmux.send_keys(@session, target_ref, launcher.command_for(win))
150
+ $stdout.puts " #{win.name}: restarted"
151
+ end
152
+
153
+ def create_missing(win)
154
+ if @dry_run
155
+ $stdout.puts "[dry-run] Would create #{win.name}"
156
+ else
157
+ Tmux.new_window(@session, win.name, win.root)
158
+ Tmux.set_pane_title(@session, win.name, 0, win.name)
159
+ Tmux.send_keys(@session, win.name, launcher.command_for(win))
160
+ $stdout.puts " #{win.name}: created"
161
+ end
162
+ end
163
+
164
+ # --- target helpers ----------------------------------------------------
165
+
166
+ def print_all_targets
167
+ @config.windows.each do |win|
168
+ t = resolver.pane_target(win.name)
169
+ $stdout.puts(t ? "#{win.name}\t#{@session}:#{t}" : "#{win.name}\t(not running)")
170
+ end
171
+ end
172
+
173
+ def print_requested_targets(requested)
174
+ requested.each do |wn|
175
+ @config.windows.find { |w| w.name == wn } ||
176
+ abort("Window '#{wn}' not found in config.")
177
+
178
+ t = resolver.pane_target(wn)
179
+ abort("Window '#{wn}' is not currently in the session.") unless t
180
+
181
+ if requested.size == 1
182
+ $stdout.puts "#{@session}:#{t}"
183
+ else
184
+ $stdout.puts "#{wn}\t#{@session}:#{t}"
185
+ end
186
+ end
187
+ end
188
+
189
+ # --- lazily-built collaborators ---------------------------------------
190
+
191
+ def resolver
192
+ @resolver ||= PaneResolver.new(@config, session: @session, layout_override: @layout)
193
+ end
194
+
195
+ def launcher
196
+ @launcher ||= Launcher.new(@config)
197
+ end
198
+
199
+ def layout_manager
200
+ @layout_manager ||= LayoutManager.new(
201
+ @config, session: @session, dry_run: @dry_run, resolver: resolver
202
+ )
203
+ end
204
+
205
+ def reconciler
206
+ @reconciler ||= Reconciler.new(
207
+ @config,
208
+ launcher: launcher, resolver: resolver,
209
+ layout_manager: layout_manager, dry_run: @dry_run
210
+ )
211
+ end
212
+
213
+ def status_view
214
+ @status_view ||= StatusView.new(@config, resolver: resolver)
215
+ end
216
+
217
+ def exec_runner
218
+ @exec_runner ||= ExecRunner.new(@config, resolver: resolver, dry_run: @dry_run)
219
+ end
220
+
221
+ def graceful_stop
222
+ @graceful_stop ||= GracefulStop.new(@session)
223
+ end
224
+
225
+ # --- backward-compat shims (referenced by the unit tests) -------------
226
+
227
+ # These delegate to the launcher / layout manager so tests that poke at
228
+ # Runner's internals via .send(:...) keep working unchanged.
229
+ def assemble_command(win)
230
+ launcher.command_for(win)
231
+ end
232
+
233
+ def write_launcher_scripts
234
+ launcher.write_all
235
+ end
236
+
237
+ def launcher_script_path(name)
238
+ launcher.script_path(name)
239
+ end
240
+
241
+ def session_runtime_dir
242
+ launcher.session_runtime_dir
243
+ end
244
+
245
+ def cleanup_runtime_dir
246
+ launcher.cleanup
247
+ end
248
+
249
+ def generate_wait_block(_name, spec)
250
+ launcher.build_wait_block(spec)
251
+ end
252
+ end
253
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ module Mxup
6
+ # Renders `mxup status` output.
7
+ class StatusView
8
+ INDENT = ' '
9
+
10
+ def initialize(config, resolver:, out: nil)
11
+ @config = config
12
+ @session = config.session
13
+ @resolver = resolver
14
+ @out_override = out
15
+ end
16
+
17
+ def out
18
+ @out_override || $stdout
19
+ end
20
+
21
+ def render(lines:)
22
+ unless Tmux.has_session?(@session)
23
+ out.puts "SESSION: #{@session} — NOT RUNNING"
24
+ out.puts "Start with: mxup up #{@session}"
25
+ return
26
+ end
27
+
28
+ active = @resolver.active_layout
29
+ created = Tmux.session_created(@session)
30
+ created_at = Time.at(created.to_i).strftime('%Y-%m-%d %H:%M:%S')
31
+ stored_prof = Tmux.show_environment(@session, 'MXUP_PROFILE')
32
+ bits = []
33
+ bits << "profile: #{stored_prof}" if stored_prof
34
+ bits << "layout: #{active}" if active
35
+ suffix = bits.empty? ? '' : " (#{bits.join(', ')})"
36
+ out.puts "SESSION: #{@session} — up since #{created_at}#{suffix}"
37
+ out.puts
38
+
39
+ panes = Tmux.list_panes(@session)
40
+ declared_names = @config.windows.map(&:name)
41
+ printed = Set.new
42
+ order = @config.effective_window_order(active)
43
+
44
+ order.each do |entry|
45
+ if entry[:type] == :group
46
+ render_group(entry[:group], panes, lines, printed)
47
+ else
48
+ render_standalone(entry[:name], panes, lines, declared_names, printed)
49
+ end
50
+ end
51
+
52
+ render_extras(panes, lines, printed)
53
+ end
54
+
55
+ private
56
+
57
+ def render_group(group, panes, lines, printed)
58
+ group_panes = panes.select { |p| p[:name] == group.name }
59
+ .sort_by { |p| p[:pane_index] }
60
+
61
+ if group_panes.empty?
62
+ group.window_names.each do |wn|
63
+ out.puts "[?] #{wn} MISSING (group: #{group.name})"
64
+ out.puts
65
+ end
66
+ return
67
+ end
68
+
69
+ header_idx = group_panes.first[:window_index]
70
+ out.puts "[#{header_idx}] #{group.name} " \
71
+ "(#{group.window_names.join(', ')}) split=#{group.split}"
72
+
73
+ group.window_names.each_with_index do |wn, idx|
74
+ printed << wn
75
+ pane = group_panes.find { |p| p[:title] == wn } ||
76
+ group_panes.find { |p| p[:pane_index] == idx }
77
+ if pane.nil?
78
+ out.puts " #{wn}: MISSING"
79
+ next
80
+ end
81
+ out.puts " #{wn}:"
82
+ print_pane(pane, wn, lines, indent: ' ')
83
+ end
84
+ out.puts
85
+ end
86
+
87
+ def render_standalone(name, panes, lines, declared, printed)
88
+ printed << name
89
+ pane = panes.find { |p| p[:name] == name && p[:pane_index] == 0 }
90
+ if pane.nil?
91
+ out.puts "[?] #{name} MISSING"
92
+ out.puts
93
+ return
94
+ end
95
+
96
+ tag = declared.include?(pane[:name]) ? '' : ' [NOT IN CONFIG]'
97
+ out.puts "[#{pane[:window_index]}] #{pane[:name]}#{tag}"
98
+ print_pane(pane, name, lines, indent: INDENT)
99
+ out.puts
100
+ end
101
+
102
+ # Surface any panes that weren't already printed (unexpected windows).
103
+ def render_extras(panes, lines, printed)
104
+ panes.each do |pane|
105
+ logical = pane[:title].to_s.empty? ? pane[:name] : pane[:title]
106
+ next if printed.include?(logical) || printed.include?(pane[:name])
107
+
108
+ out.puts "[#{pane[:window_index]}] #{pane[:name]} [NOT IN CONFIG]"
109
+ print_pane(pane, pane[:name], lines, indent: INDENT)
110
+ out.puts
111
+ end
112
+ end
113
+
114
+ def print_pane(pane, logical_name, lines, indent: INDENT)
115
+ idle = SHELLS.include?(pane[:fg_cmd])
116
+ status = idle ? idle_status(pane) : running_status(pane, indent)
117
+
118
+ in_group = pane[:name] != logical_name
119
+ target = in_group ? Tmux.pane_target(pane[:name], pane[:pane_index]) : pane[:name]
120
+ out.puts "#{indent}target: #{@session}:#{target}"
121
+ out.puts "#{indent}cwd: #{pane[:cwd]}"
122
+ out.puts "#{indent}#{status}"
123
+
124
+ tail = Tmux.capture_pane(@session, target)
125
+ .split("\n")
126
+ .reject { |l| l.strip.empty? }
127
+ .last(lines)
128
+ return if tail.empty?
129
+
130
+ out.puts "#{indent}--- last output (up to #{lines} lines) ---"
131
+ tail.each { |l| out.puts "#{indent}#{l}" }
132
+ out.puts "#{indent}---"
133
+ end
134
+
135
+ def idle_status(pane)
136
+ "IDLE (shell: #{pane[:fg_cmd]}, pid=#{pane[:pid]})"
137
+ end
138
+
139
+ def running_status(pane, indent)
140
+ leaf = ProcessProbe.leaf_pid(pane[:pid])
141
+ cmd = ProcessProbe.ps_field(leaf, 'args') || pane[:fg_cmd]
142
+ elapsed = ProcessProbe.ps_field(leaf, 'etime')&.strip || '?'
143
+ started = ProcessProbe.ps_field(leaf, 'lstart') || '?'
144
+ "RUNNING pid=#{leaf} elapsed=#{elapsed} fg=#{pane[:fg_cmd]}\n" \
145
+ "#{indent}command: #{cmd}\n" \
146
+ "#{indent}started: #{started}"
147
+ end
148
+ end
149
+ end
data/lib/mxup/tmux.rb ADDED
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'shellwords'
4
+
5
+ module Mxup
6
+ # Thin wrapper over the tmux(1) CLI. Every method shells out; nothing here
7
+ # knows about mxup's own config. Kept as a module so callers can reference
8
+ # Mxup::Tmux.list_panes(...) without instantiating anything.
9
+ module Tmux
10
+ module_function
11
+
12
+ # --- inspection --------------------------------------------------------
13
+
14
+ def has_session?(name)
15
+ system("tmux has-session -t #{esc(name)} 2>/dev/null")
16
+ end
17
+
18
+ def session_created(name)
19
+ `tmux display-message -t #{esc(name)} -p '\#{session_created}'`.strip
20
+ end
21
+
22
+ def list_windows(session)
23
+ `tmux list-windows -t #{esc(session)} -F '\#{window_index}|\#{window_name}'`
24
+ .strip.split("\n").map do |line|
25
+ idx, name = line.split('|', 2)
26
+ { index: idx.to_i, name: name }
27
+ end
28
+ end
29
+
30
+ def list_panes(session)
31
+ fmt = '#{window_index}|#{window_name}|#{pane_index}|#{pane_pid}|' \
32
+ '#{pane_current_path}|#{pane_current_command}|#{pane_title}'
33
+ `tmux list-panes -t #{esc(session)} -s -F '#{fmt}'`
34
+ .strip.split("\n").map do |line|
35
+ win_idx, name, pane_idx, pid, cwd, fg, title = line.split('|', 7)
36
+ { window_index: win_idx.to_i, name: name, pane_index: pane_idx.to_i,
37
+ pid: pid.to_i, cwd: cwd, fg_cmd: fg, title: title.to_s }
38
+ end
39
+ end
40
+
41
+ def pane_count(session, window)
42
+ `tmux list-panes -t #{esc(session)}:#{esc(window)} 2>/dev/null`
43
+ .strip.split("\n").size
44
+ end
45
+
46
+ # Capture the pane's *entire* scrollback (-S - is "start of history") so
47
+ # callers can filter and tail as they see fit. We deliberately don't pass
48
+ # -S -N here: if the interesting output scrolled past the visible window,
49
+ # a lower bound would silently hide it.
50
+ def capture_pane(session, target, _lines = nil)
51
+ `tmux capture-pane -t #{esc(session)}:#{esc(target)} -p -S - 2>/dev/null`
52
+ end
53
+
54
+ # --- mutation ----------------------------------------------------------
55
+
56
+ def new_session(name, first_window_name, root)
57
+ system("tmux new-session -d -s #{esc(name)} -n #{esc(first_window_name)} -c #{esc(root)}")
58
+ end
59
+
60
+ def new_window(session, name, root)
61
+ system("tmux new-window -t #{esc(session)} -n #{esc(name)} -c #{esc(root)}")
62
+ end
63
+
64
+ def split_window(session, window, root, target_pane: nil)
65
+ target = target_pane \
66
+ ? "#{esc(session)}:#{esc(window)}.#{target_pane}" \
67
+ : "#{esc(session)}:#{esc(window)}"
68
+ system("tmux split-window -t #{target} -c #{esc(root)} -d")
69
+ end
70
+
71
+ def select_layout(session, window, layout)
72
+ system("tmux select-layout -t #{esc(session)}:#{esc(window)} #{esc(layout)}")
73
+ end
74
+
75
+ def join_pane(session, src_window, dst_window)
76
+ system("tmux join-pane -s #{esc(session)}:#{esc(src_window)} -t #{esc(session)}:#{esc(dst_window)} -d")
77
+ end
78
+
79
+ # Break a pane out into its own window. Without an explicit -t target,
80
+ # tmux places the new window in the *current client's* session, which
81
+ # would send it to whichever session the user happens to be attached to
82
+ # when running tests. Always pin the destination to the source session.
83
+ def break_pane(session, window, pane_index, new_window_name)
84
+ system("tmux break-pane -s #{esc(session)}:#{esc(window)}.#{pane_index} " \
85
+ "-t #{esc(session)}: -n #{esc(new_window_name)} -d")
86
+ end
87
+
88
+ def rename_window(session, old_name, new_name)
89
+ system("tmux rename-window -t #{esc(session)}:#{esc(old_name)} #{esc(new_name)}")
90
+ end
91
+
92
+ def set_pane_title(session, window, pane_index, title)
93
+ system("tmux select-pane -t #{esc(session)}:#{esc(window)}.#{pane_index} -T #{esc(title)}")
94
+ end
95
+
96
+ def set_environment(session, key, value)
97
+ system("tmux set-environment -t #{esc(session)} #{esc(key)} #{esc(value)}")
98
+ end
99
+
100
+ # Returns the value stored in the session's environment, or nil if unset
101
+ # (tmux prefixes unset entries with '-').
102
+ def show_environment(session, key)
103
+ out = `tmux show-environment -t #{esc(session)} #{esc(key)} 2>/dev/null`.strip
104
+ return nil if out.empty? || out.start_with?('-')
105
+ out.split('=', 2).last
106
+ end
107
+
108
+ def kill_window(session, name)
109
+ system("tmux kill-window -t #{esc(session)}:#{esc(name)}")
110
+ end
111
+
112
+ def kill_session(name)
113
+ system("tmux kill-session -t #{esc(name)}")
114
+ end
115
+
116
+ def send_keys(session, target, keys)
117
+ system("tmux send-keys -t #{esc(session)}:#{esc(target)} #{esc(keys)} Enter")
118
+ end
119
+
120
+ def send_interrupt(session, target)
121
+ system("tmux send-keys -t #{esc(session)}:#{esc(target)} C-c")
122
+ end
123
+
124
+ def move_window(session, src_name, dst_index)
125
+ system("tmux move-window -s #{esc(session)}:#{esc(src_name)} -t #{esc(session)}:#{dst_index} 2>/dev/null")
126
+ end
127
+
128
+ def swap_window(session, idx_a, idx_b)
129
+ system("tmux swap-window -s #{esc(session)}:#{idx_a} -t #{esc(session)}:#{idx_b}")
130
+ end
131
+
132
+ # --- helpers -----------------------------------------------------------
133
+
134
+ # Format a pane target string "window.index" for use in other tmux calls.
135
+ def pane_target(window, pane_index)
136
+ "#{window}.#{pane_index}"
137
+ end
138
+
139
+ def esc(val)
140
+ Shellwords.escape(val.to_s)
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mxup
4
+ VERSION = '0.2.0'
5
+ end
data/lib/mxup.rb ADDED
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Declarative tmux session manager with reconciliation.
4
+ #
5
+ # Public entry points:
6
+ # Mxup::CLI.new.run(argv) — command-line dispatch
7
+ # Mxup::Config.new(path) — parse a YAML config
8
+ # Mxup::Runner.new(config) — programmatic API (up/down/status/...)
9
+ #
10
+ # Internals are organised into focused modules under lib/mxup/:
11
+ # Config / Window / PaneGroup / WaitSpec — pure data
12
+ # Tmux — thin tmux(1) wrapper
13
+ # Launcher — per-window launcher scripts
14
+ # PaneResolver — logical-name → tmux target
15
+ # Reconciler — `up` / `reconcile` orchestration
16
+ # LayoutManager — layout switching (flatten + regroup)
17
+ # StatusView — `status` rendering
18
+ # ExecRunner — `exec` with marker + timeout
19
+ # GracefulStop — cooperative SIGINT-then-wait
20
+ # ProcessProbe — pane → real leaf process info
21
+ # Runner — facade delegating to the above
22
+ # CLI — argv parsing + command dispatch
23
+
24
+ require_relative 'mxup/version'
25
+
26
+ module Mxup
27
+ CONFIG_DIR = File.expand_path('~/.config/mxup')
28
+ RUNTIME_DIR = File.expand_path('~/.local/share/mxup')
29
+ SHELLS = %w[zsh bash sh fish dash].freeze
30
+ end
31
+
32
+ require_relative 'mxup/config'
33
+ require_relative 'mxup/tmux'
34
+ require_relative 'mxup/launcher'
35
+ require_relative 'mxup/process_probe'
36
+ require_relative 'mxup/pane_resolver'
37
+ require_relative 'mxup/graceful_stop'
38
+ require_relative 'mxup/layout_manager'
39
+ require_relative 'mxup/reconciler'
40
+ require_relative 'mxup/status_view'
41
+ require_relative 'mxup/exec_runner'
42
+ require_relative 'mxup/runner'
43
+ require_relative 'mxup/cli'
metadata ADDED
@@ -0,0 +1,99 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mxup
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Vladislav Saifulin
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-04-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: minitest
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '5.20'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '5.20'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '13.1'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '13.1'
41
+ description: |
42
+ mxup brings a live tmux session into agreement with a YAML description of
43
+ windows, commands, and layouts. It creates missing windows, restarts crashed
44
+ ones, removes undeclared ones, and leaves healthy windows alone. Re-running
45
+ `mxup up` is always safe.
46
+ email:
47
+ - vladislav.saifulin@jetbrains.com
48
+ executables:
49
+ - mxup
50
+ extensions: []
51
+ extra_rdoc_files: []
52
+ files:
53
+ - LICENSE
54
+ - README.md
55
+ - bin/mxup
56
+ - examples/myapp-dev.yml
57
+ - lib/mxup.rb
58
+ - lib/mxup/cli.rb
59
+ - lib/mxup/config.rb
60
+ - lib/mxup/exec_runner.rb
61
+ - lib/mxup/graceful_stop.rb
62
+ - lib/mxup/launcher.rb
63
+ - lib/mxup/layout_manager.rb
64
+ - lib/mxup/pane_resolver.rb
65
+ - lib/mxup/process_probe.rb
66
+ - lib/mxup/reconciler.rb
67
+ - lib/mxup/runner.rb
68
+ - lib/mxup/status_view.rb
69
+ - lib/mxup/tmux.rb
70
+ - lib/mxup/version.rb
71
+ homepage: https://github.com/Recognized/mxup
72
+ licenses:
73
+ - MIT
74
+ metadata:
75
+ homepage_uri: https://github.com/Recognized/mxup
76
+ source_code_uri: https://github.com/Recognized/mxup
77
+ bug_tracker_uri: https://github.com/Recognized/mxup/issues
78
+ changelog_uri: https://github.com/Recognized/mxup/releases
79
+ rubygems_mfa_required: 'true'
80
+ post_install_message:
81
+ rdoc_options: []
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: 3.1.0
89
+ required_rubygems_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ requirements: []
95
+ rubygems_version: 3.5.22
96
+ signing_key:
97
+ specification_version: 4
98
+ summary: Declarative tmux session manager with reconciliation.
99
+ test_files: []