muxr 0.1.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +46 -0
- data/LICENSE.txt +21 -0
- data/README.md +211 -0
- data/bin/muxr +137 -0
- data/lib/muxr/application.rb +669 -0
- data/lib/muxr/client.rb +145 -0
- data/lib/muxr/command_dispatcher.rb +65 -0
- data/lib/muxr/drawer.rb +44 -0
- data/lib/muxr/input_handler.rb +218 -0
- data/lib/muxr/layout_manager.rb +91 -0
- data/lib/muxr/pane.rb +52 -0
- data/lib/muxr/protocol.rb +73 -0
- data/lib/muxr/pty_process.rb +92 -0
- data/lib/muxr/renderer.rb +468 -0
- data/lib/muxr/session.rb +87 -0
- data/lib/muxr/terminal.rb +817 -0
- data/lib/muxr/version.rb +3 -0
- data/lib/muxr/window.rb +110 -0
- data/lib/muxr.rb +18 -0
- data/muxr.gemspec +42 -0
- metadata +99 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 6e431ecab7ea6e49cf31e2865c3c870603847e422c9c1e5dfc8b86ff413b95f5
|
|
4
|
+
data.tar.gz: e6db50aba85439befa78b6c89ec5dcd09f6d3cfa66195e0d552f403c229b7932
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 2f0513da3cfba34c6efbc73c398934ada15ccb00b29958d4b5bf70b397cb7110819da8915acbbd13db4a8dd9d922551e3bf70d09c2dbf6247e2ebea598d2f9b5
|
|
7
|
+
data.tar.gz: 9f0dcefe6bd25d5173044d88b81fc4efac0872b5961f010ed33e370335f341f2ee2ec98b5d0ed1ee50036a3675ba96ad3b33b50d95397ef32c6726eaabe0a29a
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to muxr are documented here. The format roughly follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions
|
|
5
|
+
follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0] - 2026-05-11
|
|
10
|
+
|
|
11
|
+
Initial release.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- Client/server architecture over a Unix domain socket at
|
|
15
|
+
`~/.muxr/sockets/<name>.sock`. `Ctrl-a d` detaches the client; the
|
|
16
|
+
server (and every shell it owns) keeps running, so reattaching gives
|
|
17
|
+
back the exact same panes with full history.
|
|
18
|
+
- Ctrl-a prefix keybindings: `c` (new pane), `n`/`p` (next/prev),
|
|
19
|
+
`a` (toggle last pane), `1`..`9` (jump to pane by label), `k` (close),
|
|
20
|
+
`Tab` (cycle layout), `Enter` (promote to master), `~` (toggle drawer),
|
|
21
|
+
`d` (detach), `q` (kill session with `y/n` confirm), `:` (command
|
|
22
|
+
prompt), `?` (help), `C-a` (send literal `C-a`).
|
|
23
|
+
- Three layouts (`tall`, `grid`, `monocle`) implemented as pure functions
|
|
24
|
+
of pane count and screen area.
|
|
25
|
+
- Quake-style drawer overlay with a persistent shell PTY that survives
|
|
26
|
+
hide/toggle; `drawer reset` is the only way to kill it.
|
|
27
|
+
- Per-pane scrollback (bounded 5000-row ring) with `Ctrl-a [` copy-mode
|
|
28
|
+
and vi-style navigation (`j`/`k`/`d`/`u`/`f`/`b`/Space/`g`/`G`).
|
|
29
|
+
- Visual selection inside scrollback: `v` for character, `C-v` for
|
|
30
|
+
block; `y`/Enter yanks into an internal buffer and pipes to `pbcopy`.
|
|
31
|
+
`Ctrl-a ]` pastes the yank buffer into the focused pane.
|
|
32
|
+
- Command prompt (`Ctrl-a :`): `layout`, `drawer`, `save`, `restore`,
|
|
33
|
+
`sessions`/`ls`, `new`, `close`, `next`, `prev`, `master`, `detach`,
|
|
34
|
+
`quit`.
|
|
35
|
+
- CLI flags: `--list`, `--version`, `--help`, `-s <name>`.
|
|
36
|
+
- Session persistence to `~/.muxr/sessions/<name>.json` as cold-storage
|
|
37
|
+
fallback (the live session lives in the running server between
|
|
38
|
+
detaches).
|
|
39
|
+
- Real VT100 emulator per pane: cursor movement, SGR (16-color,
|
|
40
|
+
256-color, truecolor, underline subparameters and underline color),
|
|
41
|
+
erase/insert/delete, autowrap, scroll regions, UTF-8 across PTY read
|
|
42
|
+
boundaries.
|
|
43
|
+
- Renderer that composes one frame and diff-emits ANSI to STDOUT.
|
|
44
|
+
|
|
45
|
+
[Unreleased]: https://github.com/roelbondoc/muxr/compare/v0.1.0...HEAD
|
|
46
|
+
[0.1.0]: https://github.com/roelbondoc/muxr/releases/tag/v0.1.0
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Roel Bondoc
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# muxr
|
|
2
|
+
|
|
3
|
+
A keyboard-driven terminal multiplexer in pure Ruby. `muxr` (Ruby + Unix)
|
|
4
|
+
combines the familiar keybindings of **GNU Screen**, the automatic tiling
|
|
5
|
+
of **xmonad**, and a **Quake-style drop-down drawer**. Panes are treated
|
|
6
|
+
like tiling-window-manager clients — you never resize them by hand;
|
|
7
|
+
the active layout decides geometry.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
┌ #1 ★ (tall) ──────────────┬ #2 ──────────────────────────┐
|
|
11
|
+
│ master pane │ stacked slave pane │
|
|
12
|
+
│ │ │
|
|
13
|
+
│ ├──────────────────────────────┤
|
|
14
|
+
│ │ stacked slave pane │
|
|
15
|
+
│ │ │
|
|
16
|
+
└───────────────────────────┴──────────────────────────────┘
|
|
17
|
+
┌ Drawer ─────────────────────────────────────────────────┐
|
|
18
|
+
│ persistent overlay shell, opens from the bottom │
|
|
19
|
+
└─────────────────────────────────────────────────────────┘
|
|
20
|
+
[default] panes:3 layout:tall focused:#1 drawer:shown muxr ^a ?
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Install / run
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
git clone https://github.com/roelbondoc/muxr
|
|
27
|
+
cd muxr
|
|
28
|
+
bin/muxr # attach the "default" session (auto-spawn if needed)
|
|
29
|
+
bin/muxr work # attach (or start) a named session
|
|
30
|
+
bin/muxr --list # list saved sessions and exit
|
|
31
|
+
bin/muxr --help
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Requires **Ruby ≥ 3.4**. No runtime gems — just `PTY`, `IO.console`, `JSON`,
|
|
35
|
+
`Socket`, and `FileUtils` from stdlib.
|
|
36
|
+
|
|
37
|
+
`bin/muxr` is the client. The first invocation for a session daemonizes a
|
|
38
|
+
server in the background; subsequent invocations attach to it over a Unix
|
|
39
|
+
socket. `Ctrl-a d` detaches the client and leaves the server (and every
|
|
40
|
+
shell it owns) running, so reattaching gives you back the exact same panes
|
|
41
|
+
with their full history.
|
|
42
|
+
|
|
43
|
+
## Keybindings (Ctrl-a prefix)
|
|
44
|
+
|
|
45
|
+
| Keys | Action |
|
|
46
|
+
|----------------|---------------------------------------------------------|
|
|
47
|
+
| `C-a c` | new pane |
|
|
48
|
+
| `C-a n` / `p` | focus next / previous pane |
|
|
49
|
+
| `C-a a` | toggle last (previously focused) pane |
|
|
50
|
+
| `C-a 1` … `9` | jump to pane by its label |
|
|
51
|
+
| `C-a k` | close focused pane (or hide drawer) |
|
|
52
|
+
| `C-a Tab` | cycle layout (`tall` → `grid` → `monocle`) |
|
|
53
|
+
| `C-a Enter` | promote focused pane to master |
|
|
54
|
+
| `C-a ~` | toggle drawer |
|
|
55
|
+
| `C-a [` | enter scrollback / copy-mode |
|
|
56
|
+
| `C-a ]` | paste internal yank buffer into focused pane |
|
|
57
|
+
| `C-a d` | detach (server keeps running) |
|
|
58
|
+
| `C-a q` | kill session (asks `kill session? (y/n)`) |
|
|
59
|
+
| `C-a :` | command prompt |
|
|
60
|
+
| `C-a ?` | help |
|
|
61
|
+
| `C-a C-a` | send literal `C-a` to focused pane |
|
|
62
|
+
|
|
63
|
+
### Scrollback and copy-mode
|
|
64
|
+
|
|
65
|
+
Each pane keeps a bounded (5000-row) scrollback ring. `C-a [` enters
|
|
66
|
+
scrollback with vi-style navigation; the status bar shows a key hint and
|
|
67
|
+
the pane title gains `[scrollback N/M]`.
|
|
68
|
+
|
|
69
|
+
| Keys | Action |
|
|
70
|
+
|-------------------------|-------------------------------------|
|
|
71
|
+
| `j` / `k` | scroll one line |
|
|
72
|
+
| `d` / `u` (or `C-d`/`C-u`) | half page |
|
|
73
|
+
| `f` / `b` / Space (or `C-f`/`C-b`) | full page |
|
|
74
|
+
| `g` / `G` | top / bottom |
|
|
75
|
+
| `q` / `Esc` / `C-c` | exit back to live view |
|
|
76
|
+
|
|
77
|
+
Press `v` inside scrollback to enter a movable-cursor selection mode
|
|
78
|
+
(`h j k l`, `0`/`$`, `g`/`G`, `C-d`/`C-u`, `C-f`/`C-b`, Space). Press `v`
|
|
79
|
+
again to anchor a character selection or `C-v` to anchor a block
|
|
80
|
+
(rectangular) selection — toggling between the two preserves the anchor.
|
|
81
|
+
`y` or Enter yanks the selection into an internal buffer and pipes it to
|
|
82
|
+
`pbcopy` in the background (silent no-op when `pbcopy` is unavailable).
|
|
83
|
+
`C-a ]` writes the yank buffer back into the focused pane.
|
|
84
|
+
|
|
85
|
+
## Commands (typed after `C-a :`)
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
layout {tall|grid|monocle} # also: layout (no arg) → cycle
|
|
89
|
+
drawer {toggle|show|hide|reset}
|
|
90
|
+
save # persist session to ~/.muxr/sessions/<name>.json
|
|
91
|
+
restore # show path to saved session
|
|
92
|
+
sessions | ls # list saved sessions
|
|
93
|
+
new | close | next | prev | master
|
|
94
|
+
detach | quit # quit asks for y/n confirmation
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Architecture
|
|
98
|
+
|
|
99
|
+
muxr runs as **two processes** that talk over a Unix domain socket at
|
|
100
|
+
`~/.muxr/sockets/<name>.sock`. The server owns the PTYs and all session
|
|
101
|
+
state; the client is a thin TTY front-end that comes and goes across
|
|
102
|
+
detach/reattach.
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
Client (foreground, owns the TTY) Server (daemon, owns the PTYs)
|
|
106
|
+
├─ STDIN in raw mode + alt screen Application (event loop, lifecycle)
|
|
107
|
+
├─ SIGWINCH → RESIZE frame ├─ Session ─ Window ─ Pane[ ] ─ Terminal + PTYProcess
|
|
108
|
+
│ │ └─ Drawer ─ Pane
|
|
109
|
+
└─ Protocol ├─ Renderer – diff-emits ANSI as OUTPUT frames
|
|
110
|
+
◄── OUTPUT bytes ──── Renderer ◄────────────┤ InputHandler – Ctrl-a state machine
|
|
111
|
+
──── INPUT bytes ───► InputHandler ├─ CommandDispatcher – parses ":"-prefixed commands
|
|
112
|
+
──── HELLO/RESIZE ──► apply_size ├─ LayoutManager – pure (layout, count, area) → [Rect]
|
|
113
|
+
◄── BYE ───────────── disconnect_client └─ UNIXServer listener (one client at a time)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Frames are length-prefixed (`[1-byte type][4-byte BE length][payload]`):
|
|
117
|
+
`H` hello, `I` input, `R` resize, `B` bye, `O` output.
|
|
118
|
+
|
|
119
|
+
The server's event loop is single-threaded `IO.select` over the listening
|
|
120
|
+
socket, the attached client (when present), every pane PTY, and the
|
|
121
|
+
drawer PTY. Layouts are pure — `LayoutManager` has no mutable state, so
|
|
122
|
+
the renderer recomputes geometry on every tick after a resize or
|
|
123
|
+
pane add/remove without bookkeeping.
|
|
124
|
+
|
|
125
|
+
`Ctrl-a d` detaches the client but leaves the server (and its shells)
|
|
126
|
+
running; reattaching gives you back the same panes with their full
|
|
127
|
+
history. `Ctrl-a q` and `:quit` flash `kill session? (y/n)` in the status
|
|
128
|
+
bar and only tear the server down on `y` — there is no "kill without
|
|
129
|
+
confirm" keybinding by design.
|
|
130
|
+
|
|
131
|
+
The drawer's PTY is **never torn down** when the drawer is hidden — its
|
|
132
|
+
shell process keeps running so the next toggle restores the previous
|
|
133
|
+
session. Its initial working directory is inherited from whatever pane
|
|
134
|
+
was focused when the drawer was first created; only `drawer reset` kills
|
|
135
|
+
the PTY.
|
|
136
|
+
|
|
137
|
+
The per-pane `Terminal` is a real VT100 emulator (cursor movement, SGR
|
|
138
|
+
including 256-color/truecolor and underline subparameters, erase/insert/
|
|
139
|
+
delete, autowrap, scroll regions). Scrollback is composited into the
|
|
140
|
+
visible grid through a view-offset that auto-tracks new rows while
|
|
141
|
+
scrolled back, so reviewed content stays frozen.
|
|
142
|
+
|
|
143
|
+
## Session persistence
|
|
144
|
+
|
|
145
|
+
Sessions live in `~/.muxr/sessions/<name>.json`:
|
|
146
|
+
|
|
147
|
+
```json
|
|
148
|
+
{
|
|
149
|
+
"name": "default",
|
|
150
|
+
"layout": "tall",
|
|
151
|
+
"focused_index": 0,
|
|
152
|
+
"master_index": 0,
|
|
153
|
+
"panes": [{"cwd": "/home/me/code"}, {"cwd": "/tmp"}],
|
|
154
|
+
"drawer": {"visible": true, "cwd": "/home/me/code"}
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The JSON file is mainly a **cold-storage fallback**. Between detaches the
|
|
159
|
+
live session lives inside the running server process, so `Ctrl-a d` then
|
|
160
|
+
`bin/muxr <name>` reattaches to the exact same shells with their full
|
|
161
|
+
history. The JSON only matters once the server is gone (after `Ctrl-a q`
|
|
162
|
+
or a reboot): re-launching `muxr <name>` rebuilds pane and drawer shells
|
|
163
|
+
using the saved working directories. Shell command history within those
|
|
164
|
+
panes is **not** persisted — that's the job of your shell's own history
|
|
165
|
+
file. Run `:save` from inside muxr to write the snapshot.
|
|
166
|
+
|
|
167
|
+
## Development
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
bundle install # only minitest and rake
|
|
171
|
+
rake test # full suite (100+ unit tests)
|
|
172
|
+
|
|
173
|
+
# Run a single file or test
|
|
174
|
+
ruby -Ilib -Itest test/test_layout_manager.rb
|
|
175
|
+
ruby -Ilib -Itest test/test_terminal.rb -n test_csi_cursor_position
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Tests cover the layout algorithms, drawer state machine, window pane
|
|
179
|
+
ordering, session JSON round-trip, the client/server framing protocol,
|
|
180
|
+
the input-handler state machine (including scrollback and selection
|
|
181
|
+
modes), the renderer's diff-emit, and the VT100 emulator's cursor
|
|
182
|
+
movement, SGR (including colon-subparameter and underline-color forms),
|
|
183
|
+
erase, scroll-region, and autowrap handling. PTY-dependent code paths
|
|
184
|
+
are exercised via dependency injection so tests don't spawn shells.
|
|
185
|
+
|
|
186
|
+
On-disk layout:
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
~/.muxr/
|
|
190
|
+
├─ sessions/<name>.json structural snapshot written by `:save`
|
|
191
|
+
├─ sockets/<name>.sock server's Unix listener (auto-managed)
|
|
192
|
+
└─ logs/<name>.log server stdout/stderr
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Contributing
|
|
196
|
+
|
|
197
|
+
Contributions are welcome from anyone, with one requirement: **the code
|
|
198
|
+
must be generated by a frontier LLM** (e.g. Claude, GPT, Gemini at their
|
|
199
|
+
current top-tier model). Hand-written patches will not be accepted.
|
|
200
|
+
|
|
201
|
+
When you open a PR, please:
|
|
202
|
+
|
|
203
|
+
- State which model produced the change in the PR description.
|
|
204
|
+
- Include the prompt(s) you used, or a short summary of the conversation
|
|
205
|
+
that produced the diff.
|
|
206
|
+
- Drive the model yourself — review, push back, iterate. You are
|
|
207
|
+
responsible for the patch: it should pass `rake test`, follow the
|
|
208
|
+
conventions in `CLAUDE.md`, and not regress existing behavior.
|
|
209
|
+
|
|
210
|
+
Bug reports, feature requests, and design discussion in issues are
|
|
211
|
+
welcome regardless of how they're written.
|
data/bin/muxr
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
|
5
|
+
require "muxr"
|
|
6
|
+
require "fileutils"
|
|
7
|
+
require "rbconfig"
|
|
8
|
+
require "socket"
|
|
9
|
+
|
|
10
|
+
if ARGV.include?("-v") || ARGV.include?("--version")
|
|
11
|
+
puts "muxr #{Muxr::VERSION}"
|
|
12
|
+
exit 0
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
if ARGV.include?("-l") || ARGV.include?("--list")
|
|
16
|
+
names = Muxr::Session.list
|
|
17
|
+
puts names.join("\n") unless names.empty?
|
|
18
|
+
exit 0
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
if ARGV.include?("-h") || ARGV.include?("--help")
|
|
22
|
+
puts <<~USAGE
|
|
23
|
+
muxr #{Muxr::VERSION} — a tiling terminal multiplexer
|
|
24
|
+
|
|
25
|
+
Usage:
|
|
26
|
+
muxr attach the default session (auto-spawn if needed)
|
|
27
|
+
muxr <name> attach (or start) the named session
|
|
28
|
+
muxr -s <name> same as above
|
|
29
|
+
muxr --list list saved session files and exit
|
|
30
|
+
muxr --version print version and exit
|
|
31
|
+
muxr --help this help
|
|
32
|
+
|
|
33
|
+
Keybindings (Ctrl-a prefix):
|
|
34
|
+
C-a c new pane C-a n / p next / prev pane
|
|
35
|
+
C-a a toggle last pane C-a 1..9 jump to pane by number
|
|
36
|
+
C-a k close pane C-a Tab cycle layout
|
|
37
|
+
C-a ~ toggle drawer C-a Enter promote to master
|
|
38
|
+
C-a : command prompt C-a ? toggle help
|
|
39
|
+
C-a d detach (server stays running)
|
|
40
|
+
C-a q kill session (with y/n confirmation)
|
|
41
|
+
C-a C-a send literal C-a
|
|
42
|
+
USAGE
|
|
43
|
+
exit 0
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Internal: run as the server process. bin/muxr fork-execs itself with this
|
|
47
|
+
# flag when no server is alive for the requested session.
|
|
48
|
+
if ARGV.include?("--server")
|
|
49
|
+
args = ARGV.reject { |a| a == "--server" }
|
|
50
|
+
# Detach from the controlling terminal so the server survives the parent's
|
|
51
|
+
# shell closing. nochdir=true preserves cwd (the first pane inherits it);
|
|
52
|
+
# noclose=true keeps the stdin/out/err redirections Process.spawn set up.
|
|
53
|
+
begin
|
|
54
|
+
Process.daemon(true, true)
|
|
55
|
+
rescue NotImplementedError
|
|
56
|
+
# Platforms without Process.daemon (rare on POSIX) just run inline.
|
|
57
|
+
end
|
|
58
|
+
begin
|
|
59
|
+
Muxr::Application.new(args).run
|
|
60
|
+
rescue => e
|
|
61
|
+
$stderr.puts "muxr server: #{e.class}: #{e.message}"
|
|
62
|
+
$stderr.puts e.backtrace.first(20).join("\n")
|
|
63
|
+
exit 1
|
|
64
|
+
end
|
|
65
|
+
exit 0
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# ---- client mode (the default) ----
|
|
69
|
+
|
|
70
|
+
def session_name_from(argv)
|
|
71
|
+
idx = argv.index("-s") || argv.index("--session")
|
|
72
|
+
return argv[idx + 1] if idx && argv[idx + 1]
|
|
73
|
+
argv.find { |a| !a.start_with?("-") } || "default"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def probe_socket(path)
|
|
77
|
+
return :missing unless File.exist?(path)
|
|
78
|
+
begin
|
|
79
|
+
UNIXSocket.new(path).close
|
|
80
|
+
:alive
|
|
81
|
+
rescue Errno::ECONNREFUSED, Errno::ENOENT
|
|
82
|
+
File.unlink(path) rescue nil
|
|
83
|
+
:stale
|
|
84
|
+
rescue Errno::EACCES
|
|
85
|
+
:forbidden
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def spawn_server(name, log_path)
|
|
90
|
+
FileUtils.mkdir_p(File.dirname(log_path))
|
|
91
|
+
log = File.open(log_path, "a")
|
|
92
|
+
pid = Process.spawn(
|
|
93
|
+
RbConfig.ruby, __FILE__, "--server", name,
|
|
94
|
+
in: "/dev/null",
|
|
95
|
+
out: log,
|
|
96
|
+
err: log,
|
|
97
|
+
pgroup: true
|
|
98
|
+
)
|
|
99
|
+
log.close
|
|
100
|
+
Process.detach(pid)
|
|
101
|
+
pid
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def wait_for_socket(path, deadline)
|
|
105
|
+
until File.exist?(path)
|
|
106
|
+
return false if Time.now >= deadline
|
|
107
|
+
sleep 0.05
|
|
108
|
+
end
|
|
109
|
+
true
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
name = session_name_from(ARGV)
|
|
113
|
+
socket_path = Muxr::Application.socket_path_for(name)
|
|
114
|
+
|
|
115
|
+
case probe_socket(socket_path)
|
|
116
|
+
when :forbidden
|
|
117
|
+
$stderr.puts "muxr: cannot access #{socket_path} (permission denied)"
|
|
118
|
+
exit 1
|
|
119
|
+
when :alive
|
|
120
|
+
# great — attach.
|
|
121
|
+
when :missing, :stale
|
|
122
|
+
log_path = File.join(Dir.home, ".muxr", "logs", "#{name}.log")
|
|
123
|
+
spawn_server(name, log_path)
|
|
124
|
+
unless wait_for_socket(socket_path, Time.now + 3.0)
|
|
125
|
+
$stderr.puts "muxr: server did not start within 3s (see #{log_path})"
|
|
126
|
+
exit 1
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
client = Muxr::Client.new(name)
|
|
131
|
+
begin
|
|
132
|
+
client.connect
|
|
133
|
+
rescue Errno::ECONNREFUSED, Errno::ENOENT => e
|
|
134
|
+
$stderr.puts "muxr: cannot connect to server at #{socket_path}: #{e.message}"
|
|
135
|
+
exit 1
|
|
136
|
+
end
|
|
137
|
+
exit(client.run || 0)
|