@vibelord/lanes 1.0.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.
- package/LICENSE +21 -0
- package/README.md +70 -0
- package/bin/lanes-install.mjs +158 -0
- package/lib/plan.mjs +65 -0
- package/package.json +37 -0
- package/payload/lanes/README.md +152 -0
- package/payload/lanes/VERSION +1 -0
- package/payload/lanes/bin/lanes +1035 -0
- package/payload/lanes/client/ghostty/common.conf +22 -0
- package/payload/lanes/client/ghostty/macos.conf +6 -0
- package/payload/lanes/client/ghostty/omarchy.conf +19 -0
- package/payload/lanes/client/install.sh +16 -0
- package/payload/lanes/client/lanes-launch +315 -0
- package/payload/lanes/client/lanes-upload +147 -0
- package/payload/lanes/client/lanes-window +36 -0
- package/payload/lanes/client/macos-helper.applescript +14 -0
- package/payload/lanes/client/omarchy/discover.sh +56 -0
- package/payload/lanes/client/omarchy/lanes.conf +5 -0
- package/payload/lanes/client/omarchy/lanes.desktop +9 -0
- package/payload/lanes/client/omarchy/lanes.lua +14 -0
- package/payload/lanes/client/ssh.stanza.in +54 -0
- package/payload/lanes/container/env.sh +16 -0
- package/payload/lanes/container/lane +268 -0
- package/payload/lanes/container/lane-run +34 -0
- package/payload/lanes/docs/ARCHITECTURE.md +93 -0
- package/payload/lanes/docs/HOST-GUARD.md +108 -0
- package/payload/lanes/docs/INSTALL.md +192 -0
- package/payload/lanes/docs/RUNBOOK.md +385 -0
- package/payload/lanes/docs/SHARING.md +124 -0
- package/payload/lanes/docs/ZMX-RING0.md +51 -0
- package/payload/lanes/docs/images/desktop.png +0 -0
- package/payload/lanes/docs/images/iphone.png +0 -0
- package/payload/lanes/holders/exec.sh +21 -0
- package/payload/lanes/holders/tmux.sh +111 -0
- package/payload/lanes/holders/zmx.sh +22 -0
- package/payload/lanes/holders.lock +5 -0
- package/payload/lanes/install/enable-phone.sh +43 -0
- package/payload/lanes/install/enable-web.sh +16 -0
- package/payload/lanes/install/host-install.sh +72 -0
- package/payload/lanes/install/services.py +61 -0
- package/payload/lanes/install.sh +603 -0
- package/payload/lanes/libexec/cursor_commit.py +159 -0
- package/payload/lanes/libexec/cursor_sync.py +166 -0
- package/payload/lanes/libexec/guard.py +231 -0
- package/payload/lanes/libexec/lanes_lib.py +610 -0
- package/payload/lanes/libexec/legacy_sessions.py +130 -0
- package/payload/lanes/libexec/session_actions.py +276 -0
- package/payload/lanes/libexec/sidebar +8 -0
- package/payload/lanes/libexec/sidebar.py +872 -0
- package/payload/lanes/libexec/statusd.py +764 -0
- package/payload/lanes/libexec/tmpscan.py +921 -0
- package/payload/lanes/libexec/view.sh +179 -0
- package/payload/lanes/scripts/adopt-sharing-candidate.py +109 -0
- package/payload/lanes/site/README.md +28 -0
- package/payload/lanes/site/beyondwork/known_hosts +1 -0
- package/payload/lanes/site/beyondwork/site.conf +11 -0
- package/payload/lanes/site/site.example.conf +10 -0
- package/payload/lanes/sudo/host-guard.sh +356 -0
- package/payload/lanes/sudo/one-time-root.sh +32 -0
- package/payload/lanes/systemd/lanes-ensure.service +7 -0
- package/payload/lanes/systemd/lanes-ensure.timer +10 -0
- package/payload/lanes/systemd/lanes-gc.service +7 -0
- package/payload/lanes/systemd/lanes-gc.timer +9 -0
- package/payload/lanes/systemd/lanes-guard.service +8 -0
- package/payload/lanes/systemd/lanes-guard.timer +10 -0
- package/payload/lanes/systemd/lanes-mobile.service +23 -0
- package/payload/lanes/systemd/lanes-statusd.service +15 -0
- package/payload/lanes/systemd/lanes-sync.service +10 -0
- package/payload/lanes/systemd/lanes-sync.timer +10 -0
- package/payload/lanes/systemd/lanes-tmpreap.service +11 -0
- package/payload/lanes/systemd/lanes-tmpreap.timer +13 -0
- package/payload/lanes/systemd/lanes-web.service +13 -0
- package/payload/lanes/systemd/lanes.slice +11 -0
- package/payload/lanes/web/Caddyfile +37 -0
- package/payload/lanes/web/README.md +35 -0
- package/payload/lanes/web/icon-192.png +0 -0
- package/payload/lanes/web/icon.svg +4 -0
- package/payload/lanes/web/index.html +30 -0
- package/payload/lanes/web/manifest.webmanifest +15 -0
- package/payload/lanes/web/mobile.css +301 -0
- package/payload/lanes/web/mobile.js +367 -0
- package/payload/lanes/workspaces.example/README.md +27 -0
- package/payload/lanes/workspaces.example/catalog.json +7 -0
- package/payload/lanes/workspaces.example/commit-targets.json +8 -0
- package/payload/lanes/workspaces.example/terminals.example.json +19 -0
- package/payload/lanes/workspaces.example/workspaces.product.json +15 -0
- package/payload/lanes/workspaces.example/workspaces.research.json +16 -0
- package/payload/lanes/zellij/config.kdl +360 -0
- package/payload/manifest.json +31 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# lanes window — shared by macOS and Omarchy. Loaded by a SEPARATE Ghostty instance
|
|
2
|
+
# (`--config-default-files=false --config-file=…`), so the everyday Ghostty config is never modified.
|
|
3
|
+
# The everyday config is inherited first (fonts, theme, padding); `?` = optional include.
|
|
4
|
+
config-file = ?~/.config/ghostty/config.ghostty
|
|
5
|
+
config-file = ?~/.config/ghostty/config
|
|
6
|
+
|
|
7
|
+
# The window IS the client: closing it just detaches; nothing on the server stops.
|
|
8
|
+
confirm-close-surface = false
|
|
9
|
+
shell-integration = none
|
|
10
|
+
window-save-state = never
|
|
11
|
+
quit-after-last-window-closed = true
|
|
12
|
+
# Last backstop. lanes-launch holds and reattaches by itself and never ends; should it end anyway (killed, a usage
|
|
13
|
+
# error), the window stays open with the reason on screen — a window that vanishes reads as "my lanes are gone".
|
|
14
|
+
wait-after-command = true
|
|
15
|
+
|
|
16
|
+
# zellij on the server owns copy/paste through OSC 52; tmux/zmx own lane history.
|
|
17
|
+
clipboard-write = allow
|
|
18
|
+
clipboard-read = ask
|
|
19
|
+
copy-on-select = clipboard
|
|
20
|
+
mouse-shift-capture = never
|
|
21
|
+
scrollback-limit = 10000000
|
|
22
|
+
link-url = true
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# macOS specifics for the lanes window.
|
|
2
|
+
# Ghostty 1.3.1: CVDisplayLink init failures surface as OutOfMemory; keep the proven workaround.
|
|
3
|
+
window-vsync = false
|
|
4
|
+
macos-titlebar-style = transparent
|
|
5
|
+
# Left Option is Alt for the Ctrl-Alt zellij scheme; right Option still types special characters.
|
|
6
|
+
macos-option-as-alt = left
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Omarchy / Hyprland specifics for the lanes window (Ghostty GTK, dedicated instance).
|
|
2
|
+
# Look & feel is inherited from ~/.config/ghostty/config (Omarchy's default: theme include, JetBrainsMono Nerd
|
|
3
|
+
# Font, shift+enter as CSI-u, control+insert / shift+insert copy & paste), so Omarchy theme switches follow.
|
|
4
|
+
# Do NOT include the theme file here as well: Ghostty reports a second include of the same file as
|
|
5
|
+
# "config-file …: cycle detected" and opens its configuration-errors window.
|
|
6
|
+
|
|
7
|
+
# org.omarchy.* is matched by Omarchy's `+terminal` window tag (default/hypr/apps/terminals.lua), so Super+C /
|
|
8
|
+
# Super+V send Ctrl+Insert / Shift+Insert here. Without the tag Omarchy would send Ctrl+C — into an agent.
|
|
9
|
+
class = org.omarchy.lanes
|
|
10
|
+
# A dedicated instance must load its own config: single-instance mode would reuse a running Ghostty's config.
|
|
11
|
+
gtk-single-instance = false
|
|
12
|
+
window-decoration = none
|
|
13
|
+
|
|
14
|
+
# GTK defaults bind ctrl+alt+arrows to goto_split:* (performable, so they pass through without splits);
|
|
15
|
+
# unbinding keeps the zellij scheme deterministic: Ctrl-Alt-←/→ = workspace, Ctrl-Alt-↑/↓ = lane.
|
|
16
|
+
keybind = ctrl+alt+up=unbind
|
|
17
|
+
keybind = ctrl+alt+down=unbind
|
|
18
|
+
keybind = ctrl+alt+left=unbind
|
|
19
|
+
keybind = ctrl+alt+right=unbind
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Former entry point, kept for muscle memory and old notes. The installer now lives one level up: ../install.sh
|
|
3
|
+
# client/install.sh [--parallel] [device] -> ../install.sh --link [--device device]
|
|
4
|
+
# client/install.sh --switch -> ../install.sh --link --coder-launch
|
|
5
|
+
# client/install.sh --rollback -> put a previous coder-launch back
|
|
6
|
+
set -eu
|
|
7
|
+
root=$(cd "$(dirname "$0")/.." && pwd)
|
|
8
|
+
case "${1:-}" in
|
|
9
|
+
--switch) exec "$root/install.sh" --link --coder-launch ;;
|
|
10
|
+
--rollback)
|
|
11
|
+
bin=$HOME/.local/bin
|
|
12
|
+
if [ -e "$bin/coder-launch-legacy" ]; then rm -f "$bin/coder-launch"; mv "$bin/coder-launch-legacy" "$bin/coder-launch"; echo ' coder-launch is the previous client again'
|
|
13
|
+
else echo ' nothing to roll back'; fi ;;
|
|
14
|
+
--parallel) shift; exec "$root/install.sh" --link ${1:+--device "$1"} ;;
|
|
15
|
+
*) exec "$root/install.sh" --link ${1:+--device "$1"} ;;
|
|
16
|
+
esac
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# lanes-launch — client for the lanes workspace server (macOS and Linux; POSIX sh, no dependencies).
|
|
3
|
+
# lanes-launch [device] attach this terminal to the device's view session (default: see below)
|
|
4
|
+
# lanes-launch <workspace> same, and bring that workspace's tab to the front (e.g. `coder-launch worlds`)
|
|
5
|
+
# lanes-launch <device> --viewports N a wide view session: N (2-3) viewports side by side per workspace
|
|
6
|
+
# lanes-launch lane <lane-id> attach straight to ONE lane, no multiplexer in between
|
|
7
|
+
# lanes-launch ls list lanes with live status
|
|
8
|
+
# lanes-launch --helper <device> [--service | --while-pid PID] run only the upload helper
|
|
9
|
+
# (--service: the per-machine background service that install.sh sets up)
|
|
10
|
+
# It never ends by itself: after network loss, sleep, or a server that dropped or refused it, it says so, waits and
|
|
11
|
+
# reattaches (Enter = now). Closing the window (or Ctrl-C) ends it. Log: ~/.local/state/lanes-client/launch-<target>.log
|
|
12
|
+
set -u
|
|
13
|
+
host=${LANES_HOST:-lanes}
|
|
14
|
+
primary=$host
|
|
15
|
+
fallback=${LANES_FALLBACK_HOST:-lanes-public}
|
|
16
|
+
remote='$HOME/.local/bin/lanes'
|
|
17
|
+
config=${XDG_CONFIG_HOME:-$HOME/.config}/lanes
|
|
18
|
+
state=${XDG_STATE_HOME:-$HOME/.local/state}/lanes-client # logs and helper.alive; `install.sh --check` reads them
|
|
19
|
+
|
|
20
|
+
default_device() {
|
|
21
|
+
if [ -r "$config/device" ]; then head -n 1 "$config/device"; return; fi
|
|
22
|
+
name=$(hostname -s 2>/dev/null || hostname)
|
|
23
|
+
printf '%s' "$name" | tr 'A-Z' 'a-z' | tr -c 'a-z0-9_.-' '-' | sed 's/-*$//'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
while_pid=
|
|
27
|
+
service=
|
|
28
|
+
extra=
|
|
29
|
+
case "${1:-}" in
|
|
30
|
+
--helper)
|
|
31
|
+
mode=helper target=${2:?device}; shift 2
|
|
32
|
+
while [ $# -gt 0 ]; do
|
|
33
|
+
case "$1" in
|
|
34
|
+
--service) service=1; shift ;;
|
|
35
|
+
--while-pid) while_pid=${2:?pid}; shift 2 ;;
|
|
36
|
+
# Said out loud: an option that is dropped silently is how a client ran "as a service" without being one.
|
|
37
|
+
*) echo "lanes-launch: ignoring unknown helper option $1" >&2; shift ;;
|
|
38
|
+
esac
|
|
39
|
+
done ;;
|
|
40
|
+
ls) exec ssh -o RequestTTY=no "$host" "$remote ls" ;;
|
|
41
|
+
lane)
|
|
42
|
+
[ -n "${2:-}" ] || { echo 'usage: lanes-launch lane <lane-id>' >&2; exit 64; }
|
|
43
|
+
mode=open target=$2 ;;
|
|
44
|
+
-h|--help) sed -n '2,11p' "$0"; exit 0 ;;
|
|
45
|
+
*) mode=attach target=${1:-$(default_device)} ;;
|
|
46
|
+
esac
|
|
47
|
+
if [ "$mode" = attach ] && [ "${2:-}" = --viewports ]; then
|
|
48
|
+
case "${3:-}" in 1|2|3) extra="--viewports $3" ;; *) echo '--viewports takes 1, 2 or 3' >&2; exit 64 ;; esac
|
|
49
|
+
fi
|
|
50
|
+
case "$target" in ''|-*|*[!A-Za-z0-9_.-]*) echo "invalid name: $target" >&2; exit 64 ;; esac
|
|
51
|
+
client="$(hostname -s 2>/dev/null || echo client)-$$"
|
|
52
|
+
client=$(printf '%s' "$client" | tr -c 'A-Za-z0-9_.-' '-')
|
|
53
|
+
|
|
54
|
+
# ---- log: what a window that showed nothing never left behind ------------------------------------------------------
|
|
55
|
+
# One line per connection event, rotated to .1 at 256 KiB. The service writes to stderr instead: launchd / journald
|
|
56
|
+
# keep that, and `install.sh --check` points there. Logging must never be the reason an attach fails.
|
|
57
|
+
logfile=
|
|
58
|
+
mkdir -p "$state" 2>/dev/null && logfile=$state/launch-$target.log
|
|
59
|
+
log() {
|
|
60
|
+
if [ -n "$service" ]; then printf '%s %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >&2; return 0; fi
|
|
61
|
+
[ -n "$logfile" ] || return 0
|
|
62
|
+
size=0; [ ! -f "$logfile" ] || size=$(wc -c < "$logfile")
|
|
63
|
+
if [ "$((size))" -gt 262144 ]; then mv -f "$logfile" "$logfile.1" 2>/dev/null || :; fi
|
|
64
|
+
printf '%s [%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$$" "$*" >> "$logfile" 2>/dev/null || :
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# Run ssh as an async child so HUP/TERM (window closed, logout) end it immediately; a foreground child
|
|
68
|
+
# would defer the trap until ssh returned. Keep the terminal on fd 9: dash gives async jobs /dev/null.
|
|
69
|
+
child=
|
|
70
|
+
reader=
|
|
71
|
+
holding=
|
|
72
|
+
now=
|
|
73
|
+
limit=
|
|
74
|
+
helper_pid=
|
|
75
|
+
stop() {
|
|
76
|
+
trap - HUP TERM INT
|
|
77
|
+
exec 2>/dev/null # bash reports every child it sees die of a signal: noise in a terminal that stays open
|
|
78
|
+
if [ -n "$child" ]; then kill -TERM "$child" 2>/dev/null || :; fi
|
|
79
|
+
if [ -n "$reader" ]; then kill -TERM "$reader" 2>/dev/null || :; fi
|
|
80
|
+
end_helper
|
|
81
|
+
log 'stopped: window closed, or a signal'
|
|
82
|
+
exit 0
|
|
83
|
+
}
|
|
84
|
+
end_helper() {
|
|
85
|
+
[ -n "${helper_pid:-}" ] || return 0
|
|
86
|
+
pkill -TERM -P "$helper_pid" 2>/dev/null || :
|
|
87
|
+
kill -TERM "$helper_pid" 2>/dev/null || :
|
|
88
|
+
}
|
|
89
|
+
# INT too: in a plain terminal Ctrl-C is the way out, now that nothing else ends the client.
|
|
90
|
+
trap stop HUP TERM INT
|
|
91
|
+
# With $limit set, a watchdog ends a child that the server leaves hanging (macOS has no timeout(1)).
|
|
92
|
+
run() {
|
|
93
|
+
exec 9<&0
|
|
94
|
+
"$@" <&9 9<&- &
|
|
95
|
+
child=$!
|
|
96
|
+
exec 9<&-
|
|
97
|
+
dog=
|
|
98
|
+
if [ -n "$limit" ]; then ( sleep "$limit"; kill -TERM "$child" ) >/dev/null 2>&1 & dog=$!; fi
|
|
99
|
+
rc=0
|
|
100
|
+
wait "$child" || rc=$?
|
|
101
|
+
child=
|
|
102
|
+
if [ -n "$dog" ]; then kill -TERM "$dog" 2>/dev/null || :; wait "$dog" 2>/dev/null || :; fi
|
|
103
|
+
return "$rc"
|
|
104
|
+
}
|
|
105
|
+
# hold <seconds> [jitter]: wait, but Enter continues at once. No raw mode, so there is no terminal state to restore
|
|
106
|
+
# when the window closes mid-wait: a line reader signals this shell instead. End of input (no terminal) is no Enter.
|
|
107
|
+
trap 'if [ -n "$holding" ]; then now=1; [ -z "$child" ] || { kill -TERM "$child"; wait "$child"; } 2>/dev/null; fi' USR1
|
|
108
|
+
hold() {
|
|
109
|
+
now=; holding=1
|
|
110
|
+
exec 9<&0
|
|
111
|
+
( IFS= read -r line <&9 && kill -USR1 "$$" ) 2>/dev/null &
|
|
112
|
+
reader=$!
|
|
113
|
+
exec 9<&-
|
|
114
|
+
run sleep "$1" || :
|
|
115
|
+
# Jitter is a second sleep: should its number ever be malformed, the whole wait is still served — no tight loop.
|
|
116
|
+
if [ -z "$now" ] && [ -n "${2:-}" ]; then run sleep "$2" || :; fi
|
|
117
|
+
holding=
|
|
118
|
+
kill -TERM "$reader" 2>/dev/null || :
|
|
119
|
+
wait "$reader" 2>/dev/null || :
|
|
120
|
+
reader=
|
|
121
|
+
}
|
|
122
|
+
# The fallback host counts only if the ssh config really defines it (an undefined alias resolves to itself).
|
|
123
|
+
fallback_defined() {
|
|
124
|
+
resolved=$(ssh -G "$fallback" 2>/dev/null | awk '$1 == "hostname" { print $2; exit }')
|
|
125
|
+
[ -n "$resolved" ] && [ "$resolved" != "$fallback" ]
|
|
126
|
+
}
|
|
127
|
+
# ---- upload helper: serves the sidebar's upload box ------------------------------------------------------------
|
|
128
|
+
# A drop on the terminal only gives the SERVER a local path string; the file lives here. This helper waits for
|
|
129
|
+
# such requests and uploads the files with lanes-upload. It is the security boundary: it reads ONLY from folders
|
|
130
|
+
# listed in ~/.config/lanes/upload-allow (asks once per new folder on macOS) and never from credential folders.
|
|
131
|
+
allowed_path() {
|
|
132
|
+
real=$(cd "$(dirname "$1")" 2>/dev/null && pwd -P)/$(basename "$1")
|
|
133
|
+
case "$real" in
|
|
134
|
+
"$HOME"/.ssh/*|"$HOME"/.gnupg/*|"$HOME"/.aws/*|"$HOME"/.kube/*|"$HOME"/.config/*|"$HOME"/.docker/*|\
|
|
135
|
+
"$HOME"/Library/Keychains/*|"$HOME"/Library/Application\ Support/*[Kk]eychain*|*/.git/*|/etc/*|/private/etc/*) return 1 ;;
|
|
136
|
+
esac
|
|
137
|
+
[ -e "$real" ] || return 1
|
|
138
|
+
while IFS= read -r dir; do
|
|
139
|
+
[ -n "$dir" ] || continue
|
|
140
|
+
case "$real" in "$dir"/*) return 0 ;; esac
|
|
141
|
+
done < "$config/upload-allow" 2>/dev/null
|
|
142
|
+
folder=$(dirname "$real")
|
|
143
|
+
if [ "$(uname -s)" = Darwin ]; then
|
|
144
|
+
answer=$(/usr/bin/osascript -e "display dialog \"The lanes upload box wants to send a file from:\n\n$folder\n\nAllow uploads from this folder?\" with title \"Lanes upload\" buttons {\"Deny\", \"Allow once\", \"Always allow\"} default button \"Allow once\" giving up after 60" -e 'button returned of result' 2>/dev/null)
|
|
145
|
+
case "$answer" in
|
|
146
|
+
'Always allow') printf '%s\n' "$folder" >> "$config/upload-allow"; return 0 ;;
|
|
147
|
+
'Allow once') return 0 ;;
|
|
148
|
+
esac
|
|
149
|
+
fi
|
|
150
|
+
return 1
|
|
151
|
+
}
|
|
152
|
+
# Epoch mtime. GNU form first: GNU `stat -f` means "file system" and prints that before failing; BSD rejects -c silently.
|
|
153
|
+
mtime() { stat -c %Y "$1" 2>/dev/null || stat -f %m "$1" 2>/dev/null; }
|
|
154
|
+
# The machine's helper SERVICE touches helper.alive on every READY/PING line (one every 5 s): serving = under 90 s old.
|
|
155
|
+
service_serving() {
|
|
156
|
+
seen=$(mtime "$state/helper.alive") || return 1
|
|
157
|
+
[ -n "$seen" ] && [ $(( $(date +%s) - seen )) -lt 90 ]
|
|
158
|
+
}
|
|
159
|
+
upload_helper() {
|
|
160
|
+
[ -x "$HOME/.local/bin/lanes-upload" ] || return 0
|
|
161
|
+
dflt=$(default_device)
|
|
162
|
+
# One helper serves the machine: the server spools the requests of a suffixed view (mac-2, mac-w2) under the BASE
|
|
163
|
+
# device and remembers the asking view itself, so the line protocol and `--device` below need nothing new.
|
|
164
|
+
case "$1" in "$dflt"-*) helper_target=$dflt ;; *) helper_target=$1 ;; esac
|
|
165
|
+
mkdir -p "$config"
|
|
166
|
+
[ -e "$config/upload-allow" ] || for dir in "$HOME/Desktop" "$HOME/Downloads" "$HOME/Pictures" "$HOME/Documents" "$HOME/Lanes Uploads" \
|
|
167
|
+
/var/folders /private/var/folders /tmp /private/tmp; do [ -d "$dir" ] && (cd "$dir" && pwd -P) >> "$config/upload-allow"; done
|
|
168
|
+
errfile=/dev/null; [ ! -d "$state" ] || errfile=$state/helper.err # ssh's last complaint only: bounded by design
|
|
169
|
+
up= # '' unknown, 1 serving, 0 down: only the transitions are logged (nobody rotates the launchd log)
|
|
170
|
+
pause=3
|
|
171
|
+
while :; do
|
|
172
|
+
if [ -n "$while_pid" ] && ! kill -0 "$while_pid" 2>/dev/null; then exit 0; fi # the window it serves is gone
|
|
173
|
+
# A window's own helper is the fallback only. While the service serves this machine it opens no connection
|
|
174
|
+
# (each one is a long-lived process on the server); when the service goes quiet it takes over within ~2 min.
|
|
175
|
+
if [ -z "$service" ] && [ "$helper_target" = "$dflt" ] && service_serving; then sleep 30; continue; fi
|
|
176
|
+
if ssh -o RequestTTY=no -o BatchMode=yes -o ServerAliveInterval=15 -o ServerAliveCountMax=4 "$primary" "$remote upload-requests $helper_target --default-device $dflt" 2>"$errfile" |
|
|
177
|
+
( heard=1
|
|
178
|
+
while IFS= read -r line; do
|
|
179
|
+
case "$line" in
|
|
180
|
+
READY|PING)
|
|
181
|
+
heard=0
|
|
182
|
+
[ -z "$service" ] || touch "$state/helper.alive" 2>/dev/null
|
|
183
|
+
[ "$up" = 1 ] || { up=1; log "upload helper: serving '$helper_target' via $primary"; } ;;
|
|
184
|
+
BEGIN\ *) set -- $line; req=$2; req_board=$3; req_paste=$4; list=$(mktemp) ;;
|
|
185
|
+
P\ *) printf '%s\n' "${line#P }" >> "$list" ;;
|
|
186
|
+
END)
|
|
187
|
+
refused=; set --
|
|
188
|
+
while IFS= read -r path; do
|
|
189
|
+
if allowed_path "$path"; then set -- "$@" "$path"; else refused="$refused $(basename "$path")"; fi
|
|
190
|
+
done < "$list"
|
|
191
|
+
rm -f "$list"
|
|
192
|
+
if [ $# -gt 0 ] && [ -z "$refused" ]; then
|
|
193
|
+
flag=; [ "$req_paste" = 1 ] && flag=--paste
|
|
194
|
+
upload_err=$(mktemp)
|
|
195
|
+
log "upload $req: sending $# item(s) to $req_board"
|
|
196
|
+
LANES_DEFAULT_DEVICE=$dflt "$HOME/.local/bin/lanes-upload" -w "$req_board" $flag --device "$helper_target" --request "$req" "$@" </dev/null >/dev/null 2>"$upload_err" &
|
|
197
|
+
upload_pid=$!
|
|
198
|
+
while kill -0 "$upload_pid" 2>/dev/null; do
|
|
199
|
+
# A large transfer (or a macOS access prompt) must not make a serving helper look dead.
|
|
200
|
+
[ -z "$service" ] || touch "$state/helper.alive" 2>/dev/null
|
|
201
|
+
sleep 1
|
|
202
|
+
done
|
|
203
|
+
if wait "$upload_pid"; then
|
|
204
|
+
log "upload $req: complete"
|
|
205
|
+
else
|
|
206
|
+
detail=$(head -n 1 "$upload_err")
|
|
207
|
+
[ -n "$detail" ] || detail='upload failed on the client'
|
|
208
|
+
log "upload $req failed: $detail"
|
|
209
|
+
case "$detail" in
|
|
210
|
+
*'Operation not permitted'*) detail='macOS denied folder access: allow Lanes Helper in System Settings > Privacy & Security > Files and Folders' ;;
|
|
211
|
+
esac
|
|
212
|
+
# Diagnostics contain filenames: quote them as one remote argument, never shell code.
|
|
213
|
+
quoted=$(printf '%s' "$detail" | sed "s/'/'\\\\''/g; s/^/'/; s/\$/'/")
|
|
214
|
+
ssh -n -o RequestTTY=no -o BatchMode=yes "$primary" "$remote upload-result $helper_target $req $quoted --default-device $dflt" 2>/dev/null
|
|
215
|
+
fi
|
|
216
|
+
rm -f "$upload_err"
|
|
217
|
+
else
|
|
218
|
+
quoted=$(printf '%s' "not allowed on this machine:$refused - run: lanes-upload --allow FOLDER" | sed "s/'/'\\\\''/g; s/^/'/; s/\$/'/")
|
|
219
|
+
ssh -n -o RequestTTY=no -o BatchMode=yes "$primary" "$remote upload-result $helper_target $req $quoted --default-device $dflt" 2>/dev/null
|
|
220
|
+
fi ;;
|
|
221
|
+
esac
|
|
222
|
+
done
|
|
223
|
+
exit "$heard"
|
|
224
|
+
); then
|
|
225
|
+
up=1; pause=3 # the server ends every stream after 15 min by design: come straight back
|
|
226
|
+
else
|
|
227
|
+
[ "$up" = 0 ] || log "upload helper: no connection to $primary ($(head -n 1 "$errfile" 2>/dev/null)); retrying, at most once a minute"
|
|
228
|
+
up=0
|
|
229
|
+
fi
|
|
230
|
+
sleep "$pause"
|
|
231
|
+
# Back off while the server never answers. As a service this loop runs around the clock, also against a key
|
|
232
|
+
# that is refused or an sshd that is thrashing: 3 s forever would be load on exactly the host that is in trouble.
|
|
233
|
+
if [ "$up" = 0 ]; then pause=$((pause * 2)); [ "$pause" -le 60 ] || pause=60; fi
|
|
234
|
+
done
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
# Real jitter. awk's bare srand() seeds from the clock's SECOND, so windows that woke together all drew the same
|
|
238
|
+
# number; the pid differs per window. Up to 1 s + 25 % of the wait. (LC_ALL=C: `sleep` does not take a decimal comma.)
|
|
239
|
+
jitter() {
|
|
240
|
+
LC_ALL=C awk -v seed="$(( $$ + $(date +%s) ))" -v d="$1" 'BEGIN { srand(seed); rand(); printf "%.1f", rand() * (1 + d / 4) }' 2>/dev/null || :
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
fails=0
|
|
244
|
+
delay=1
|
|
245
|
+
if [ "$mode" = helper ]; then
|
|
246
|
+
# Default signal handling: a trap would wait for the foreground ssh pipeline (up to 15 min), and launchd kills a
|
|
247
|
+
# job that sits on TERM for 20 s — a reinstall would find the old job still loaded. Nothing here needs cleanup:
|
|
248
|
+
# launchd (process group) and systemd (control group) take the ssh child along.
|
|
249
|
+
trap - HUP TERM INT
|
|
250
|
+
upload_helper "$target"; exit 0
|
|
251
|
+
fi
|
|
252
|
+
if [ "$mode" = attach ]; then
|
|
253
|
+
while_pid=$$ # ends the helper also when this client is killed without a chance to clean up
|
|
254
|
+
upload_helper "$target" </dev/null >/dev/null 2>&1 &
|
|
255
|
+
helper_pid=$!
|
|
256
|
+
fi
|
|
257
|
+
while :; do
|
|
258
|
+
printf '\033]0;lanes · %s\007' "$target"
|
|
259
|
+
# Said before ssh draws anything: a server under pressure needs many seconds for banner and key exchange, and a
|
|
260
|
+
# window that stays blank meanwhile reads as dead.
|
|
261
|
+
printf '\033[2mlanes: connecting to %s…\033[0m\r\n' "$host"
|
|
262
|
+
log "connecting: $mode $target via $host"
|
|
263
|
+
started=$(date +%s)
|
|
264
|
+
if [ "$mode" = attach ]; then
|
|
265
|
+
run ssh "$host" "$remote attach $target --client $client --default-device $(default_device) $extra" || :
|
|
266
|
+
else
|
|
267
|
+
run ssh "$host" "$remote open $target" || :
|
|
268
|
+
fi
|
|
269
|
+
lasted=$(( $(date +%s) - started ))
|
|
270
|
+
log "ended: exit $rc after ${lasted}s via $host"
|
|
271
|
+
if [ "$lasted" -ge 30 ]; then delay=1; fails=0; host=$primary; fi
|
|
272
|
+
# NOTHING below exits: a client that ends closes its window, and a closed window reads as "my lanes are gone".
|
|
273
|
+
if [ "$rc" = 0 ]; then
|
|
274
|
+
# 0 is not proof of a deliberate detach: zellij also ends a client it found too slow with 0 (a thrashing host
|
|
275
|
+
# did that after 24 s). Say what happened first — the question to the server below can take seconds itself.
|
|
276
|
+
fails=0
|
|
277
|
+
printf '\r\n\033[2mlanes: the view ended (detached, or dropped by a busy server). Your lanes keep running.\033[0m\r\n'
|
|
278
|
+
if [ "$mode" = attach ] && [ "$lasted" -ge 3 ]; then
|
|
279
|
+
# The server just rebuilt this view session (new layout)? That also ends the client with 0: come straight back.
|
|
280
|
+
limit=10; rebuilt=
|
|
281
|
+
run ssh -n -o RequestTTY=no -o BatchMode=yes -o ConnectTimeout=10 "$host" "$remote rebuilt $target --default-device $(default_device)" 2>/dev/null && rebuilt=1
|
|
282
|
+
limit=
|
|
283
|
+
if [ -n "$rebuilt" ]; then
|
|
284
|
+
printf '\033[2mlanes: the view session was rebuilt; reattaching…\033[0m\r\n'
|
|
285
|
+
run sleep 2 || :
|
|
286
|
+
continue
|
|
287
|
+
fi
|
|
288
|
+
fi
|
|
289
|
+
pause=$delay; [ "$pause" -ge 10 ] || pause=10
|
|
290
|
+
what=reattaching
|
|
291
|
+
elif [ "$rc" -ge 64 ] && [ "$rc" -le 78 ]; then
|
|
292
|
+
# 64-78 = the server refused the request (bad name, unknown lane). Retrying soon cannot help; its message
|
|
293
|
+
# stays on screen, and the slow retry picks up a fix made on the server.
|
|
294
|
+
fails=0
|
|
295
|
+
printf '\r\n\033[2mlanes: the server refused this (exit %s, see above).\033[0m\r\n' "$rc"
|
|
296
|
+
pause=60
|
|
297
|
+
what='trying again'
|
|
298
|
+
else
|
|
299
|
+
# Two transport failures in a row on the primary path: try the fallback path once it exists. (25 s: a connect
|
|
300
|
+
# that times out takes ConnectTimeout 20 — it must count, or a dead tailnet never reaches the fallback.)
|
|
301
|
+
if [ "$rc" = 255 ] && [ "$lasted" -lt 25 ]; then fails=$((fails + 1)); else fails=0; fi
|
|
302
|
+
if [ "$fails" -ge 2 ] && [ "$host" = "$primary" ] && fallback_defined; then
|
|
303
|
+
host=$fallback; fails=0; delay=1
|
|
304
|
+
printf '\r\n\033[2mlanes: %s unreachable, using %s\033[0m\r\n' "$primary" "$fallback"
|
|
305
|
+
elif [ "$fails" -ge 2 ] && [ "$host" = "$fallback" ]; then
|
|
306
|
+
host=$primary; fails=0
|
|
307
|
+
fi
|
|
308
|
+
printf '\r\n\033[2mlanes: connection ended (%s).\033[0m\r\n' "$rc"
|
|
309
|
+
pause=$delay
|
|
310
|
+
what=reconnecting
|
|
311
|
+
fi
|
|
312
|
+
printf '\033[2mlanes: %s in about %ss — Enter = now; closing the window (or Ctrl-C) = quit\033[0m\r\n' "$what" "$pause"
|
|
313
|
+
hold "$pause" "$(jitter "$pause")"
|
|
314
|
+
delay=$((delay * 2)); if [ "$delay" -gt 60 ]; then delay=60; fi
|
|
315
|
+
done
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# lanes-upload — put local files into a board's uploads/ folder on the workspace server (the same folder Cursor
|
|
3
|
+
# users drop files into), over the direct `lanes` SSH path. macOS and Linux; POSIX sh, no dependencies.
|
|
4
|
+
#
|
|
5
|
+
# lanes-upload [-w <board>] [--paste] <file|dir>... upload; default board = the one this device's view shows
|
|
6
|
+
# lanes-upload --clipboard [-w <board>] [--paste] upload the image (or copied files) on the clipboard
|
|
7
|
+
# lanes-upload --watch-run process the watch folder once (run by launchd / systemd)
|
|
8
|
+
# lanes-upload --allow <dir> | --allowed folders the in-window upload box may read from on THIS machine
|
|
9
|
+
#
|
|
10
|
+
# --paste also type the uploaded path(s) into the lane you are looking at, ready for the agent
|
|
11
|
+
# Afterwards the in-container path(s) are on your clipboard and shown in a notification.
|
|
12
|
+
# Watch folder: ~/Lanes Uploads/ -> the board your view is showing
|
|
13
|
+
# ~/Lanes Uploads/<board>/ -> that board (sent files move to .sent/<date>/)
|
|
14
|
+
set -u
|
|
15
|
+
host=${LANES_HOST:-lanes}
|
|
16
|
+
config=${XDG_CONFIG_HOME:-$HOME/.config}/lanes
|
|
17
|
+
watch_root=$(cat "$config/uploads-dir" 2>/dev/null || echo "$HOME/Lanes Uploads")
|
|
18
|
+
device=$(head -n 1 "$config/device" 2>/dev/null || echo mac)
|
|
19
|
+
os=$(uname -s)
|
|
20
|
+
PATH=/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/opt/homebrew/bin:$PATH
|
|
21
|
+
|
|
22
|
+
notify() {
|
|
23
|
+
if [ "$os" = Darwin ]; then
|
|
24
|
+
/usr/bin/osascript -e "display notification \"$2\" with title \"$1\"" >/dev/null 2>&1 || :
|
|
25
|
+
elif command -v notify-send >/dev/null 2>&1; then notify-send -a Lanes "$1" "$2" 2>/dev/null || :; fi
|
|
26
|
+
}
|
|
27
|
+
to_clipboard() {
|
|
28
|
+
if [ "$os" = Darwin ]; then printf '%s' "$1" | /usr/bin/pbcopy
|
|
29
|
+
elif command -v wl-copy >/dev/null 2>&1; then printf '%s' "$1" | wl-copy 2>/dev/null || :; fi
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# send <board-or-empty> <paste:0|1> <path>... -> prints the remote paths
|
|
33
|
+
send() (
|
|
34
|
+
board=$1; paste=$2; shift 2
|
|
35
|
+
target="--active $device"; [ -n "$board" ] && target="$board --device $device"
|
|
36
|
+
flags=; [ "$paste" = 1 ] && flags=--paste
|
|
37
|
+
[ -n "${request:-}" ] && flags="$flags --request $request"
|
|
38
|
+
[ -n "${LANES_DEFAULT_DEVICE:-}" ] && flags="$flags --default-device $LANES_DEFAULT_DEVICE"
|
|
39
|
+
set -- "$@"
|
|
40
|
+
args=
|
|
41
|
+
for item in "$@"; do
|
|
42
|
+
[ -e "$item" ] || { echo "lanes-upload: no such file: $item" >&2; return 66; }
|
|
43
|
+
dir=$(cd "$(dirname "$item")" && pwd); base=$(basename "$item")
|
|
44
|
+
args="$args -C $(printf '%s' "$dir" | sed "s/'/'\\\\''/g; s/^/'/; s/\$/'/") $(printf '%s' "./$base" | sed "s/'/'\\\\''/g; s/^/'/; s/\$/'/")"
|
|
45
|
+
done
|
|
46
|
+
# COPYFILE_DISABLE / --no-mac-metadata: no AppleDouble ._ files in the archive.
|
|
47
|
+
extra=; [ "$os" = Darwin ] && extra=--no-mac-metadata
|
|
48
|
+
# POSIX sh only returns the last pipeline command's status. Keep tar's status separately: a denied file
|
|
49
|
+
# otherwise produces a valid empty/partial archive, and a watch-folder upload can be marked sent anyway.
|
|
50
|
+
transfer=$(mktemp -d) || return 1
|
|
51
|
+
trap 'rm -rf "$transfer"' EXIT
|
|
52
|
+
( eval "COPYFILE_DISABLE=1 tar $extra -cf - $args"; echo "$?" > "$transfer/tar-status" ) |
|
|
53
|
+
ssh -o RequestTTY=no -o BatchMode=yes "$host" "\$HOME/.local/bin/lanes upload $target $flags"
|
|
54
|
+
ssh_status=$?
|
|
55
|
+
tar_status=$(cat "$transfer/tar-status" 2>/dev/null || echo 1)
|
|
56
|
+
[ "$tar_status" = 0 ] || { echo "lanes-upload: could not read every file (tar exit $tar_status)" >&2; return 1; }
|
|
57
|
+
return "$ssh_status"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
finish() { # remote paths on stdin
|
|
61
|
+
paths=$(cat)
|
|
62
|
+
[ -n "$paths" ] || return 1
|
|
63
|
+
printf '%s\n' "$paths"
|
|
64
|
+
to_clipboard "$(printf '%s' "$paths" | tr '\n' ' ' | sed 's/ $//')"
|
|
65
|
+
notify 'Uploaded to lanes' "$(printf '%s' "$paths" | head -n 3 | tr '\n' ' ')"
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
clipboard_to_files() { # writes into $1, prints the file paths to upload
|
|
69
|
+
out=$1; stamp=$(date +%Y%m%d-%H%M%S)
|
|
70
|
+
if [ "$os" = Darwin ]; then
|
|
71
|
+
copied=$(/usr/bin/osascript -e 'try' -e 'POSIX path of (the clipboard as «class furl»)' -e 'end try' 2>/dev/null)
|
|
72
|
+
if [ -n "$copied" ] && [ -e "$copied" ]; then printf '%s\n' "$copied"; return 0; fi
|
|
73
|
+
/usr/bin/osascript -e 'try' -e "set png to the clipboard as «class PNGf»" \
|
|
74
|
+
-e "set f to open for access POSIX file \"$out/clip-$stamp.png\" with write permission" \
|
|
75
|
+
-e 'write png to f' -e 'close access f' -e 'end try' >/dev/null 2>&1
|
|
76
|
+
elif command -v wl-paste >/dev/null 2>&1; then
|
|
77
|
+
if wl-paste --list-types 2>/dev/null | grep -q '^image/png'; then wl-paste -t image/png > "$out/clip-$stamp.png"
|
|
78
|
+
elif wl-paste --list-types 2>/dev/null | grep -q '^text/uri-list'; then
|
|
79
|
+
wl-paste -t text/uri-list | sed -n 's|^file://||p' | while IFS= read -r p; do printf '%b\n' "$(printf '%s' "$p" | sed 's/%\([0-9A-Fa-f][0-9A-Fa-f]\)/\\x\1/g')"; done
|
|
80
|
+
return 0
|
|
81
|
+
fi
|
|
82
|
+
fi
|
|
83
|
+
if [ -s "$out/clip-$stamp.png" ]; then printf '%s\n' "$out/clip-$stamp.png"; else return 1; fi
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
watch_run() {
|
|
87
|
+
mkdir -p "$watch_root"
|
|
88
|
+
lock=$watch_root/.lock
|
|
89
|
+
mkdir "$lock" 2>/dev/null || { [ -n "$(find "$lock" -maxdepth 0 -mmin +5 2>/dev/null)" ] && rmdir "$lock" 2>/dev/null; exit 0; }
|
|
90
|
+
trap 'rmdir "$lock" 2>/dev/null' EXIT INT TERM
|
|
91
|
+
sleep 1 # let the writer finish
|
|
92
|
+
for dir in "$watch_root" "$watch_root"/*/; do
|
|
93
|
+
[ -d "$dir" ] || continue
|
|
94
|
+
dir=${dir%/}
|
|
95
|
+
board=; [ "$dir" != "$watch_root" ] && board=$(basename "$dir")
|
|
96
|
+
for item in "$dir"/*; do
|
|
97
|
+
[ -e "$item" ] || continue
|
|
98
|
+
[ "$dir" = "$watch_root" ] && [ -d "$item" ] && continue # board folders themselves
|
|
99
|
+
case "$(basename "$item")" in .*|README.txt|*.crdownload|*.part|*.download) continue ;; esac
|
|
100
|
+
# still being written? wait until its size is stable
|
|
101
|
+
a=$(du -sk "$item" 2>/dev/null | cut -f1); sleep 1; b=$(du -sk "$item" 2>/dev/null | cut -f1)
|
|
102
|
+
[ "$a" = "$b" ] || continue
|
|
103
|
+
if result=$(send "$board" 0 "$item"); then
|
|
104
|
+
sent=$dir/.sent/$(date +%Y-%m-%d); mkdir -p "$sent"; mv "$item" "$sent/" 2>/dev/null
|
|
105
|
+
printf '%s\n' "$result" | finish >/dev/null
|
|
106
|
+
else
|
|
107
|
+
notify 'Lanes upload failed' "$(basename "$item") — it stays in the folder and is retried on the next change"
|
|
108
|
+
fi
|
|
109
|
+
done
|
|
110
|
+
done
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
board=; paste=0; clip=0; request=
|
|
114
|
+
while [ $# -gt 0 ]; do
|
|
115
|
+
case "$1" in
|
|
116
|
+
--request) request=${2:?id}; shift 2 ;;
|
|
117
|
+
--device) device=${2:?device}; shift 2 ;;
|
|
118
|
+
--allow) dir=$(cd "${2:?dir}" && pwd -P) || exit 66; mkdir -p "$config"; grep -qxF "$dir" "$config/upload-allow" 2>/dev/null || printf '%s\n' "$dir" >> "$config/upload-allow"; echo "allowed: $dir"; exit 0 ;;
|
|
119
|
+
--allowed) cat "$config/upload-allow" 2>/dev/null; exit 0 ;;
|
|
120
|
+
-w|--workspace) board=${2:?board id}; shift 2 ;;
|
|
121
|
+
--paste) paste=1; shift ;;
|
|
122
|
+
--clipboard) clip=1; shift ;;
|
|
123
|
+
--watch-run) watch_run; exit 0 ;;
|
|
124
|
+
-h|--help) sed -n '2,13p' "$0"; exit 0 ;;
|
|
125
|
+
--) shift; break ;;
|
|
126
|
+
-*) echo "lanes-upload: unknown option $1" >&2; exit 64 ;;
|
|
127
|
+
*) break ;;
|
|
128
|
+
esac
|
|
129
|
+
done
|
|
130
|
+
case "$board" in *[!A-Za-z0-9_.-]*) echo 'lanes-upload: invalid board id' >&2; exit 64 ;; esac
|
|
131
|
+
case "$request" in *[!0-9-]*) echo 'lanes-upload: invalid request id' >&2; exit 64 ;; esac
|
|
132
|
+
|
|
133
|
+
if [ "$clip" = 1 ]; then
|
|
134
|
+
tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT
|
|
135
|
+
files=$(clipboard_to_files "$tmp") || { echo 'lanes-upload: the clipboard holds no image or copied file' >&2; notify 'Lanes upload' 'Clipboard holds no image or file'; exit 66; }
|
|
136
|
+
old_ifs=$IFS; IFS='
|
|
137
|
+
'
|
|
138
|
+
# shellcheck disable=SC2086
|
|
139
|
+
set -- $files
|
|
140
|
+
IFS=$old_ifs
|
|
141
|
+
fi
|
|
142
|
+
[ $# -gt 0 ] || { sed -n '2,13p' "$0"; exit 64; }
|
|
143
|
+
if result=$(send "$board" "$paste" "$@"); then
|
|
144
|
+
printf '%s\n' "$result" | finish
|
|
145
|
+
else
|
|
146
|
+
exit "$?"
|
|
147
|
+
fi
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Open the lanes window in its own terminal instance. usage: lanes-window [suffix]
|
|
3
|
+
# suffix: an independent view session "<device>-<suffix>" (second monitor / other half of an ultrawide).
|
|
4
|
+
# w2 / w3: a WIDE view session with 2 / 3 viewports side by side per workspace (for a full-width window).
|
|
5
|
+
# macOS: Ghostty. Linux: Ghostty when installed, otherwise Foot with the same window class.
|
|
6
|
+
set -u
|
|
7
|
+
config=${XDG_CONFIG_HOME:-$HOME/.config}/lanes
|
|
8
|
+
launch=$HOME/.local/bin/lanes-launch
|
|
9
|
+
class=org.omarchy.lanes
|
|
10
|
+
device=
|
|
11
|
+
file=$config/ghostty.conf
|
|
12
|
+
if [ -n "${1:-}" ]; then
|
|
13
|
+
case "$1" in *[!A-Za-z0-9_.-]*) echo 'invalid suffix' >&2; exit 64 ;; esac
|
|
14
|
+
device="$(head -n 1 "$config/device")-$1"
|
|
15
|
+
case "$1" in w2) device="$device --viewports 2" ;; w3) device="$device --viewports 3" ;; esac
|
|
16
|
+
file=$config/ghostty.$1.conf
|
|
17
|
+
sed "s|^command = .*|command = $launch $device|" "$config/ghostty.conf" > "$file"
|
|
18
|
+
fi
|
|
19
|
+
case "$(uname -s)" in
|
|
20
|
+
Darwin)
|
|
21
|
+
if ! open -Ra Ghostty.app 2>/dev/null; then
|
|
22
|
+
echo 'lanes-window: Ghostty is not installed (brew install --cask ghostty). Meanwhile: run lanes-launch in any terminal.' >&2
|
|
23
|
+
exit 69
|
|
24
|
+
fi
|
|
25
|
+
exec open -na Ghostty.app --args --config-default-files=false "--config-file=$file" ;;
|
|
26
|
+
esac
|
|
27
|
+
if command -v ghostty >/dev/null 2>&1; then
|
|
28
|
+
exec ghostty --config-default-files=false "--config-file=$file"
|
|
29
|
+
elif command -v foot >/dev/null 2>&1; then
|
|
30
|
+
# Same class, so Omarchy's terminal tag, the launch-or-focus bind and window rules behave identically.
|
|
31
|
+
# --hold = Ghostty's wait-after-command (common.conf): should the client ever end, the window stays, reason on screen.
|
|
32
|
+
# shellcheck disable=SC2086
|
|
33
|
+
exec foot --hold --app-id="$class" --title=lanes -e "$launch" $device
|
|
34
|
+
fi
|
|
35
|
+
echo 'lanes-window: install Ghostty (omarchy-pkg-add ghostty) or Foot' >&2
|
|
36
|
+
exit 69
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
-- A real app identity lets macOS ask for Downloads/Desktop/Documents access for the upload helper.
|
|
2
|
+
-- An anonymous /bin/sh launched directly by launchd is silently denied those folders.
|
|
3
|
+
on run
|
|
4
|
+
set userHome to POSIX path of (path to home folder)
|
|
5
|
+
set configRoot to do shell script "printf '%s' \"${XDG_CONFIG_HOME:-$HOME/.config}\""
|
|
6
|
+
set stateRoot to do shell script "printf '%s' \"${XDG_STATE_HOME:-$HOME/.local/state}\""
|
|
7
|
+
set deviceName to do shell script "/usr/bin/head -n 1 " & quoted form of (configRoot & "/lanes/device")
|
|
8
|
+
set launcher to userHome & ".local/bin/lanes-launch"
|
|
9
|
+
set logPath to stateRoot & "/lanes-client/helper-service.log"
|
|
10
|
+
-- launchd owns this app and its children; closing a terminal does not stop the helper.
|
|
11
|
+
try
|
|
12
|
+
do shell script "exec /bin/sh " & quoted form of launcher & " --helper " & quoted form of deviceName & " --service </dev/null >/dev/null 2>>" & quoted form of logPath
|
|
13
|
+
end try
|
|
14
|
+
end run
|