muxr 0.1.4 → 0.1.6
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 +111 -1
- data/README.md +210 -65
- data/bin/muxr +61 -0
- data/bin/muxr-mcp +412 -0
- data/lib/muxr/application.rb +276 -26
- data/lib/muxr/command_dispatcher.rb +2 -0
- data/lib/muxr/control_server.rb +670 -0
- data/lib/muxr/drawer.rb +9 -2
- data/lib/muxr/foreground_command.rb +86 -0
- data/lib/muxr/input_handler.rb +133 -27
- data/lib/muxr/key_parser.rb +89 -0
- data/lib/muxr/layout_manager.rb +59 -0
- data/lib/muxr/pane.rb +60 -3
- data/lib/muxr/renderer.rb +145 -33
- data/lib/muxr/session.rb +13 -2
- data/lib/muxr/terminal.rb +81 -2
- data/lib/muxr/version.rb +1 -1
- data/lib/muxr.rb +2 -0
- data/muxr.gemspec +3 -1
- data/skills/muxr-control/SKILL.md +190 -0
- metadata +7 -1
data/lib/muxr/application.rb
CHANGED
|
@@ -25,12 +25,16 @@ module Muxr
|
|
|
25
25
|
DEFAULT_WIDTH = 80
|
|
26
26
|
DEFAULT_HEIGHT = 24
|
|
27
27
|
|
|
28
|
-
attr_reader :session, :renderer, :input, :session_name
|
|
28
|
+
attr_reader :session, :renderer, :input, :session_name, :control_server
|
|
29
29
|
|
|
30
30
|
def self.socket_path_for(name)
|
|
31
31
|
File.join(SOCKETS_DIR, "#{name}.sock")
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
def self.control_socket_path_for(name)
|
|
35
|
+
File.join(SOCKETS_DIR, "#{name}.ctrl.sock")
|
|
36
|
+
end
|
|
37
|
+
|
|
34
38
|
# Names of sessions whose server socket is currently accepting connections.
|
|
35
39
|
# Stale sockets (file exists, no listener) are skipped but left in place;
|
|
36
40
|
# cleanup happens on the next attach attempt.
|
|
@@ -60,15 +64,23 @@ module Muxr
|
|
|
60
64
|
@message = nil
|
|
61
65
|
@message_expires = nil
|
|
62
66
|
@help_visible = false
|
|
63
|
-
@next_pane_id = 0
|
|
64
67
|
@current_client = nil
|
|
65
68
|
@client_write_buffer = +"".b
|
|
66
69
|
@listening_socket = nil
|
|
67
70
|
@socket_path = self.class.socket_path_for(@session_name)
|
|
71
|
+
@control_socket_path = self.class.control_socket_path_for(@session_name)
|
|
72
|
+
@control_server = nil
|
|
68
73
|
@paste_buffer = +""
|
|
69
74
|
@last_render_at = nil
|
|
75
|
+
@foreground_poller = nil
|
|
70
76
|
end
|
|
71
77
|
|
|
78
|
+
# Interval for the background thread that refreshes each pane's
|
|
79
|
+
# foreground-command label. Picked to feel responsive (a long-running
|
|
80
|
+
# `npm test` shows up within a second of starting) without burning CPU
|
|
81
|
+
# on macOS, where each tick costs a `ps` fork+exec per pane.
|
|
82
|
+
FOREGROUND_POLL_INTERVAL = 0.75
|
|
83
|
+
|
|
72
84
|
attr_reader :paste_buffer
|
|
73
85
|
|
|
74
86
|
def run
|
|
@@ -87,12 +99,14 @@ module Muxr
|
|
|
87
99
|
target&.write(data)
|
|
88
100
|
end
|
|
89
101
|
|
|
90
|
-
def new_pane
|
|
91
|
-
cwd
|
|
92
|
-
|
|
102
|
+
def new_pane(cwd: nil)
|
|
103
|
+
cwd ||= focused_pane&.cwd
|
|
104
|
+
pane = make_pane(cwd: cwd)
|
|
105
|
+
@session.window.add_pane(pane)
|
|
93
106
|
@session.focus_drawer = false
|
|
94
107
|
@session.window.focused_index = @session.window.panes.length - 1
|
|
95
108
|
invalidate
|
|
109
|
+
pane
|
|
96
110
|
end
|
|
97
111
|
|
|
98
112
|
def focus_next
|
|
@@ -134,6 +148,60 @@ module Muxr
|
|
|
134
148
|
invalidate
|
|
135
149
|
end
|
|
136
150
|
|
|
151
|
+
# Move focus to the pane spatially adjacent in `direction` (:left/:right/
|
|
152
|
+
# :up/:down). Called by the normal-mode hjkl bindings. Pulling the live
|
|
153
|
+
# layout rects keeps this in sync with whatever the renderer is showing.
|
|
154
|
+
# Monocle has no meaningful direction (every rect is identical) so we
|
|
155
|
+
# fall back to linear nav so hjkl still does something.
|
|
156
|
+
def focus_direction(direction)
|
|
157
|
+
return if @session.window.panes.empty?
|
|
158
|
+
if @session.focus_drawer && @session.drawer&.visible?
|
|
159
|
+
@session.focus_drawer = false
|
|
160
|
+
invalidate
|
|
161
|
+
return
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
win = @session.window
|
|
165
|
+
idx = LayoutManager.neighbor(current_pane_rects, win.focused_index, direction)
|
|
166
|
+
if idx.nil? && win.layout == :monocle
|
|
167
|
+
case direction
|
|
168
|
+
when :right, :down then win.focus_next
|
|
169
|
+
when :left, :up then win.focus_prev
|
|
170
|
+
end
|
|
171
|
+
invalidate
|
|
172
|
+
return
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
return unless idx
|
|
176
|
+
win.focus_index(idx)
|
|
177
|
+
invalidate
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Explicit layout set, used by the normal-mode t/g/m bindings and the
|
|
181
|
+
# `:layout <name>` command.
|
|
182
|
+
def set_layout(layout)
|
|
183
|
+
@session.window.set_layout(layout)
|
|
184
|
+
flash("layout: #{@session.window.layout}")
|
|
185
|
+
invalidate
|
|
186
|
+
rescue ArgumentError => e
|
|
187
|
+
flash(e.message)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Bound to `i` in normal mode — drops the user into the historical
|
|
191
|
+
# Ctrl-a-prefixed multiplexer mode.
|
|
192
|
+
def enter_passthrough_mode
|
|
193
|
+
@input.enter_passthrough_mode
|
|
194
|
+
flash("passthrough mode (^a esc to return)")
|
|
195
|
+
invalidate
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Bound to `Ctrl-a Esc` from passthrough — return to normal mode.
|
|
199
|
+
def enter_normal_mode
|
|
200
|
+
@input.enter_normal_mode
|
|
201
|
+
flash("normal mode")
|
|
202
|
+
invalidate
|
|
203
|
+
end
|
|
204
|
+
|
|
137
205
|
def close_focused
|
|
138
206
|
if @session.focus_drawer && @session.drawer&.visible?
|
|
139
207
|
hide_drawer
|
|
@@ -156,15 +224,30 @@ module Muxr
|
|
|
156
224
|
invalidate
|
|
157
225
|
end
|
|
158
226
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
227
|
+
# Toggle the privacy flag on the focused pane. Private panes are
|
|
228
|
+
# redacted from the MCP control surface (panes.list strips cwd; read /
|
|
229
|
+
# send_input / run / subscribe / kill all refuse). Only the human can
|
|
230
|
+
# flip this — there is intentionally no control method to do it.
|
|
231
|
+
def toggle_private_focused
|
|
232
|
+
pane = focused_pane
|
|
233
|
+
return unless pane
|
|
234
|
+
pane.toggle_private!
|
|
235
|
+
flash(pane.private? ? "pane #{pane.id} marked private (hidden from MCP)" : "pane #{pane.id} unmarked private")
|
|
165
236
|
invalidate
|
|
166
237
|
end
|
|
167
238
|
|
|
239
|
+
def toggle_drawer
|
|
240
|
+
toggle_drawer_kind(command: nil)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Ctrl-a C / :claude — opens a drawer whose shell is `claude`, with
|
|
244
|
+
# MUXR_SESSION + MUXR_CONTROL_SOCKET + MUXR_FOCUSED_PANE in the env so
|
|
245
|
+
# the muxr-mcp bridge inside that claude process auto-attaches to this
|
|
246
|
+
# session.
|
|
247
|
+
def toggle_claude_drawer
|
|
248
|
+
toggle_drawer_kind(command: "claude")
|
|
249
|
+
end
|
|
250
|
+
|
|
168
251
|
def show_drawer
|
|
169
252
|
ensure_drawer
|
|
170
253
|
@session.drawer.show!
|
|
@@ -464,6 +547,20 @@ module Muxr
|
|
|
464
547
|
end
|
|
465
548
|
end
|
|
466
549
|
|
|
550
|
+
# Live pane rects for the current layout/size, computed the same way the
|
|
551
|
+
# Renderer does so spatial neighbor lookup matches what the user sees.
|
|
552
|
+
def current_pane_rects
|
|
553
|
+
win = @session.window
|
|
554
|
+
area = LayoutManager::Rect.new(0, 0, @session.width, @session.height - 1)
|
|
555
|
+
LayoutManager.compute(
|
|
556
|
+
win.layout,
|
|
557
|
+
win.panes.length,
|
|
558
|
+
area,
|
|
559
|
+
focused_index: win.focused_index,
|
|
560
|
+
master_index: win.master_index
|
|
561
|
+
)
|
|
562
|
+
end
|
|
563
|
+
|
|
467
564
|
def focused_pane
|
|
468
565
|
@session.window.focused_pane
|
|
469
566
|
end
|
|
@@ -477,20 +574,32 @@ module Muxr
|
|
|
477
574
|
@listening_socket = UNIXServer.new(@socket_path)
|
|
478
575
|
File.chmod(0o600, @socket_path) rescue nil
|
|
479
576
|
|
|
577
|
+
# Sibling control socket — multi-client, NDJSON, used by bin/muxr-mcp
|
|
578
|
+
# and any other programmatic driver. Connected control clients do not
|
|
579
|
+
# count as "attached", so a Claude Code session can poke the muxr
|
|
580
|
+
# server without contending with the human's TTY client.
|
|
581
|
+
@control_server = ControlServer.new(self, @control_socket_path)
|
|
582
|
+
@control_server.start
|
|
583
|
+
|
|
480
584
|
@session = Session.new(name: @session_name, width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT)
|
|
481
585
|
@renderer = Renderer.new(out: FramedOutput.new(self))
|
|
482
586
|
@input = InputHandler.new(self)
|
|
483
587
|
|
|
484
|
-
|
|
485
|
-
|
|
588
|
+
saved = Session.load(@session_name)
|
|
589
|
+
first_id = saved && saved.dig("panes", 0, "id")
|
|
590
|
+
@session.window.add_pane(make_pane(id: first_id))
|
|
486
591
|
|
|
487
|
-
restore_panes_if_saved
|
|
592
|
+
restore_panes_if_saved(saved) if saved
|
|
488
593
|
|
|
489
594
|
@running = true
|
|
595
|
+
start_foreground_poller
|
|
490
596
|
end
|
|
491
597
|
|
|
492
598
|
def teardown
|
|
599
|
+
stop_foreground_poller
|
|
493
600
|
disconnect_client
|
|
601
|
+
@control_server&.stop
|
|
602
|
+
@control_server = nil
|
|
494
603
|
if @listening_socket
|
|
495
604
|
@listening_socket.close rescue nil
|
|
496
605
|
end
|
|
@@ -508,6 +617,7 @@ module Muxr
|
|
|
508
617
|
@session.window.panes.each { |p| read_ios << p.io if p.alive? }
|
|
509
618
|
drawer_pane = @session.drawer&.pane
|
|
510
619
|
read_ios << drawer_pane.io if drawer_pane&.alive?
|
|
620
|
+
read_ios.concat(@control_server.read_ios) if @control_server
|
|
511
621
|
|
|
512
622
|
write_ios = []
|
|
513
623
|
@session.window.panes.each do |p|
|
|
@@ -517,6 +627,7 @@ module Muxr
|
|
|
517
627
|
write_ios << drawer_pane.writer_io
|
|
518
628
|
end
|
|
519
629
|
write_ios << @current_client if @current_client && !@client_write_buffer.empty?
|
|
630
|
+
write_ios.concat(@control_server.write_ios) if @control_server
|
|
520
631
|
|
|
521
632
|
timeout = @message ? 0.25 : SELECT_TIMEOUT
|
|
522
633
|
# If a render is queued but we're inside the frame-rate budget, wake
|
|
@@ -540,6 +651,8 @@ module Muxr
|
|
|
540
651
|
accept_client
|
|
541
652
|
elsif io == @current_client
|
|
542
653
|
consume_client_frame
|
|
654
|
+
elsif @control_server&.owns?(io)
|
|
655
|
+
@control_server.handle_read(io)
|
|
543
656
|
else
|
|
544
657
|
consume_pane_io(io)
|
|
545
658
|
end
|
|
@@ -548,13 +661,18 @@ module Muxr
|
|
|
548
661
|
ready_w&.each do |io|
|
|
549
662
|
if io == @current_client
|
|
550
663
|
drain_client_writes
|
|
664
|
+
elsif @control_server&.owns?(io)
|
|
665
|
+
@control_server.handle_write(io)
|
|
551
666
|
else
|
|
552
667
|
pane = pane_for_writer_io(io)
|
|
553
668
|
pane&.drain_writes
|
|
554
669
|
end
|
|
555
670
|
end
|
|
556
671
|
|
|
672
|
+
@control_server&.tick
|
|
673
|
+
|
|
557
674
|
prune_dead_panes
|
|
675
|
+
prune_dead_drawer
|
|
558
676
|
expire_message
|
|
559
677
|
|
|
560
678
|
if @session.window.panes.empty?
|
|
@@ -647,7 +765,14 @@ module Muxr
|
|
|
647
765
|
pane = pane_for_io(io)
|
|
648
766
|
return unless pane
|
|
649
767
|
data = pane.read_from_pty
|
|
650
|
-
|
|
768
|
+
if data
|
|
769
|
+
invalidate
|
|
770
|
+
# Notify the control surface so any pending pane.run waiters reset
|
|
771
|
+
# their idle window and any pane.subscribe clients get a new frame.
|
|
772
|
+
# read_from_pty already fed the bytes into the Terminal; the control
|
|
773
|
+
# server pulls the resulting text out of pane.terminal.dump_text.
|
|
774
|
+
@control_server&.on_pane_output(pane.id, data) if pane.id.is_a?(String)
|
|
775
|
+
end
|
|
651
776
|
end
|
|
652
777
|
|
|
653
778
|
def pane_for_io(io)
|
|
@@ -671,6 +796,25 @@ module Muxr
|
|
|
671
796
|
invalidate
|
|
672
797
|
end
|
|
673
798
|
|
|
799
|
+
# When the shell (or claude) inside the drawer exits, tear the drawer
|
|
800
|
+
# down so the next Ctrl-a ~ / Ctrl-a C spawns a fresh one. Without this
|
|
801
|
+
# the drawer pane stays mounted around a dead PTY and looks like the
|
|
802
|
+
# multiplexer is wedged.
|
|
803
|
+
def prune_dead_drawer
|
|
804
|
+
drawer = @session.drawer
|
|
805
|
+
return unless drawer
|
|
806
|
+
pane = drawer.pane
|
|
807
|
+
return unless pane
|
|
808
|
+
return if pane.alive?
|
|
809
|
+
kind = drawer.command ? "#{drawer.command} drawer" : "drawer"
|
|
810
|
+
drawer.close
|
|
811
|
+
@session.drawer = nil
|
|
812
|
+
@session.focus_drawer = false
|
|
813
|
+
renderer.reset_frame!
|
|
814
|
+
flash("#{kind} exited")
|
|
815
|
+
invalidate
|
|
816
|
+
end
|
|
817
|
+
|
|
674
818
|
def expire_message
|
|
675
819
|
return unless @message_expires
|
|
676
820
|
if Time.now >= @message_expires
|
|
@@ -729,6 +873,55 @@ module Muxr
|
|
|
729
873
|
# Fire-and-forget pipe to pbcopy. Runs on its own thread so even a slow
|
|
730
874
|
# macOS pbcopy doesn't stall the event loop. Silent when pbcopy is absent
|
|
731
875
|
# (Linux/headless) — selection still goes to the internal buffer.
|
|
876
|
+
# Background thread that walks every pane and writes its foreground
|
|
877
|
+
# command back onto pane.foreground_command. Lives off the event loop
|
|
878
|
+
# because the macOS `ps` path is fork+exec'y; on Linux the procfs reads
|
|
879
|
+
# would be fast enough on the main thread but a single code path is
|
|
880
|
+
# easier to reason about. Atomic pointer writes (MRI GVL) mean we don't
|
|
881
|
+
# need a lock for the renderer's per-frame read.
|
|
882
|
+
def start_foreground_poller
|
|
883
|
+
return if @foreground_poller
|
|
884
|
+
@foreground_poller = Thread.new do
|
|
885
|
+
while @running
|
|
886
|
+
begin
|
|
887
|
+
poll_foreground_commands
|
|
888
|
+
rescue StandardError
|
|
889
|
+
# Never let a poller crash kill the server. If the lookup keeps
|
|
890
|
+
# failing the titles just won't show commands — that's fine.
|
|
891
|
+
end
|
|
892
|
+
sleep FOREGROUND_POLL_INTERVAL
|
|
893
|
+
end
|
|
894
|
+
end
|
|
895
|
+
end
|
|
896
|
+
|
|
897
|
+
def stop_foreground_poller
|
|
898
|
+
thread = @foreground_poller
|
|
899
|
+
@foreground_poller = nil
|
|
900
|
+
return unless thread
|
|
901
|
+
# @running has already been flipped off; the thread exits on its next
|
|
902
|
+
# wake. join with a small timeout so we don't hang teardown if the
|
|
903
|
+
# thread is mid-`ps`.
|
|
904
|
+
thread.join(2.0) || thread.kill
|
|
905
|
+
end
|
|
906
|
+
|
|
907
|
+
def poll_foreground_commands
|
|
908
|
+
# Snapshot so add/remove on the main thread can't trip us mid-iter.
|
|
909
|
+
panes = @session.window.panes.dup
|
|
910
|
+
drawer_pane = @session.drawer&.pane
|
|
911
|
+
panes << drawer_pane if drawer_pane
|
|
912
|
+
changed = false
|
|
913
|
+
panes.each do |pane|
|
|
914
|
+
next unless pane.alive?
|
|
915
|
+
next unless pane.respond_to?(:pid) && pane.pid
|
|
916
|
+
name = ForegroundCommand.lookup(pane.pid)
|
|
917
|
+
if pane.foreground_command != name
|
|
918
|
+
pane.foreground_command = name
|
|
919
|
+
changed = true
|
|
920
|
+
end
|
|
921
|
+
end
|
|
922
|
+
invalidate if changed
|
|
923
|
+
end
|
|
924
|
+
|
|
732
925
|
def spawn_pbcopy(text)
|
|
733
926
|
Thread.new do
|
|
734
927
|
IO.popen("pbcopy", "w") { |io| io.write(text) }
|
|
@@ -738,20 +931,62 @@ module Muxr
|
|
|
738
931
|
end
|
|
739
932
|
end
|
|
740
933
|
|
|
741
|
-
def make_pane(cwd: nil)
|
|
742
|
-
|
|
743
|
-
Pane.new(id: @next_pane_id, rows: 24, cols: 80, cwd: cwd)
|
|
934
|
+
def make_pane(cwd: nil, id: nil)
|
|
935
|
+
Pane.new(id: id, rows: 24, cols: 80, cwd: cwd)
|
|
744
936
|
end
|
|
745
937
|
|
|
746
|
-
def ensure_drawer
|
|
938
|
+
def ensure_drawer(command: nil)
|
|
747
939
|
return if @session.drawer
|
|
748
940
|
cwd = focused_pane&.cwd
|
|
749
|
-
pane = Pane.new(
|
|
750
|
-
|
|
941
|
+
pane = Pane.new(
|
|
942
|
+
id: :drawer,
|
|
943
|
+
rows: 10,
|
|
944
|
+
cols: 80,
|
|
945
|
+
cwd: cwd,
|
|
946
|
+
command: command,
|
|
947
|
+
env_overrides: drawer_env
|
|
948
|
+
)
|
|
949
|
+
@session.drawer = Drawer.new(pane: pane, origin_cwd: cwd, command: command)
|
|
950
|
+
end
|
|
951
|
+
|
|
952
|
+
# Toggle the drawer; if a different kind is currently up, tear it down
|
|
953
|
+
# and replace it with the requested kind. Keeps the drawer slot a single
|
|
954
|
+
# PTY so users don't end up with a confusing menagerie of overlays.
|
|
955
|
+
def toggle_drawer_kind(command:)
|
|
956
|
+
current = @session.drawer
|
|
957
|
+
if current.nil?
|
|
958
|
+
ensure_drawer(command: command)
|
|
959
|
+
@session.drawer.show!
|
|
960
|
+
@session.focus_drawer = true
|
|
961
|
+
elsif current.command == command
|
|
962
|
+
current.toggle!
|
|
963
|
+
@session.focus_drawer = current.visible?
|
|
964
|
+
else
|
|
965
|
+
current.close
|
|
966
|
+
@session.drawer = nil
|
|
967
|
+
ensure_drawer(command: command)
|
|
968
|
+
@session.drawer.show!
|
|
969
|
+
@session.focus_drawer = true
|
|
970
|
+
end
|
|
971
|
+
renderer.reset_frame!
|
|
972
|
+
invalidate
|
|
751
973
|
end
|
|
752
974
|
|
|
753
|
-
|
|
754
|
-
|
|
975
|
+
# Env vars exposed to every drawer PTY. The MCP bridge reads these to
|
|
976
|
+
# auto-connect to the right session; MUXR_DRAWER_SELF lets it refuse
|
|
977
|
+
# drawer.* methods so a claude drawer can't recurse into its own PTY.
|
|
978
|
+
def drawer_env
|
|
979
|
+
env = {
|
|
980
|
+
"MUXR_SESSION" => @session_name.to_s,
|
|
981
|
+
"MUXR_CONTROL_SOCKET" => @control_socket_path.to_s,
|
|
982
|
+
"MUXR_DRAWER_SELF" => "1"
|
|
983
|
+
}
|
|
984
|
+
focused = focused_pane
|
|
985
|
+
env["MUXR_FOCUSED_PANE"] = focused.id.to_s if focused&.id.is_a?(String)
|
|
986
|
+
env
|
|
987
|
+
end
|
|
988
|
+
|
|
989
|
+
def restore_panes_if_saved(data)
|
|
755
990
|
return unless data
|
|
756
991
|
|
|
757
992
|
if data["layout"] && Window::LAYOUTS.include?(data["layout"].to_sym)
|
|
@@ -759,15 +994,30 @@ module Muxr
|
|
|
759
994
|
end
|
|
760
995
|
|
|
761
996
|
panes_data = data["panes"] || []
|
|
997
|
+
# Restore privacy flag for the already-created first pane.
|
|
998
|
+
if panes_data[0] && panes_data[0]["private"] && @session.window.panes[0]
|
|
999
|
+
@session.window.panes[0].mark_private!
|
|
1000
|
+
end
|
|
762
1001
|
panes_data[1..]&.each do |entry|
|
|
763
1002
|
cwd = entry["cwd"]
|
|
764
|
-
|
|
1003
|
+
id = entry["id"]
|
|
1004
|
+
pane = make_pane(cwd: cwd, id: id)
|
|
1005
|
+
pane.mark_private! if entry["private"]
|
|
1006
|
+
@session.window.add_pane(pane)
|
|
765
1007
|
end
|
|
766
1008
|
|
|
767
1009
|
if data["drawer"]
|
|
768
1010
|
cwd = data["drawer"]["cwd"]
|
|
769
|
-
|
|
770
|
-
|
|
1011
|
+
command = data["drawer"]["command"]
|
|
1012
|
+
pane = Pane.new(
|
|
1013
|
+
id: :drawer,
|
|
1014
|
+
rows: 10,
|
|
1015
|
+
cols: 80,
|
|
1016
|
+
cwd: cwd,
|
|
1017
|
+
command: command,
|
|
1018
|
+
env_overrides: drawer_env
|
|
1019
|
+
)
|
|
1020
|
+
drawer = Drawer.new(pane: pane, origin_cwd: cwd, command: command)
|
|
771
1021
|
drawer.visible = !!data["drawer"]["visible"]
|
|
772
1022
|
@session.drawer = drawer
|
|
773
1023
|
@session.focus_drawer = drawer.visible?
|
|
@@ -16,6 +16,8 @@ module Muxr
|
|
|
16
16
|
case cmd
|
|
17
17
|
when "layout" then handle_layout(args)
|
|
18
18
|
when "drawer" then handle_drawer(args)
|
|
19
|
+
when "claude" then @app.toggle_claude_drawer
|
|
20
|
+
when "private" then @app.toggle_private_focused
|
|
19
21
|
when "save" then @app.save_session
|
|
20
22
|
when "restore" then @app.restore_session
|
|
21
23
|
when "sessions", "ls" then @app.list_sessions
|