aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-26 16:11:42 +0200
committeruser <user@clank>2026-08-26 16:11:42 +0200
commit087b2bd1d31dac24f8b0ffe79c33f4ebf1f60bd3 (patch)
tree282ce9a0e243333bb769bae231426cb4f9cf3a86 /src
parentcompact: shared borders instead of gaps (D23) (diff)
downloadslosh-087b2bd1d31dac24f8b0ffe79c33f4ebf1f60bd3.tar.gz
slosh-087b2bd1d31dac24f8b0ffe79c33f4ebf1f60bd3.tar.xz
slosh-087b2bd1d31dac24f8b0ffe79c33f4ebf1f60bd3.zip
macos: the kernel still knows, it just spells /proc differently
Portability fixes contributed by our macOS developer, applied with one hardening tweak (below). Darwin has no /proc, so everything that read one gets the native answer: live_cwd via proc_pidinfo(PROC_PIDVNODEPATHINFO), the foreground command via sysctl(KERN_PROCARGS2) parsed back into the NUL-separated shape /proc/<pid>/cmdline gives, and SLOSH_BIN via _NSGetExecutablePath plus realpath. The pty gets its winsize set on the slave after opening it, because Darwin attaches no line discipline to the master until then and the earlier TIOCSWINSZ was silently dropped -- a pane born 0x0. The shader repl binds tab in libedit's own words, since macOS ships Python's readline as a libedit wrapper that accepts GNU syntax in silence. Two finds are cross-platform bugs: a toast wider than the screen minus the gap computed a negative x that uint16_t made ~65533, drawing the longest messages -- the ones most worth reading -- off screen; now the middle is elided (a path's tail is the half you needed) and x clamps. And a pane label was 128 bytes, which a Homebrew interpreter's full path exceeds before its arguments even start; dump-layout wrote the truncation down and a restore ran it, so it is 1024 now. Saving a layout learned cwd_as_named: the kernel answers with the physical path, and on macOS /tmp is a symlink to /private/tmp, so a layout that said cwd="/tmp" stopped round-tripping through a save. If realpath(named) is where the pane still is, the named spelling wins. Tests follow: realpath'd tmpdirs, the scroll test reads the shell's first prompt instead of spelling out bash 5.3, and wrapped pane text is joined before matching a long path. One tweak on top of the contribution: the elision needs five columns for a head, the dots and a tail -- below that `avail - 3` goes negative, and negative as size_t is a pointer into the weeds; a terminal that narrow gets a plain clip.
Diffstat (limited to 'src')
-rw-r--r--src/app.c26
-rw-r--r--src/app_session.c54
-rw-r--r--src/pane.c52
-rw-r--r--src/pty.c5
-rw-r--r--src/server.c13
5 files changed, 141 insertions, 9 deletions
diff --git a/src/app.c b/src/app.c
index 323419c..e72d2a7 100644
--- a/src/app.c
+++ b/src/app.c
@@ -605,10 +605,32 @@ void draw_toasts(app_t *a, screen_t *s) {
char line[160];
snprintf(line, sizeof line, " %s ", text);
uint16_t w = (uint16_t)strlen(line);
- if (w >= s->cols) w = (uint16_t)(s->cols - 1);
+ /* Too wide: elide the middle rather than the end. Almost every toast that
+ * overflows is about a path, and a path's last component is the part you
+ * needed -- clipping the tail told you "cannot open /var/folders/5q/g_q5"
+ * and made you go and look for the rest. */
+ uint16_t avail = (uint16_t)(s->cols > 1 ? s->cols - 1 : 1);
+ if (w > avail && avail >= 5) { /* room for a head, the dots and a tail */
+ size_t head = (size_t)(avail - 3) / 2, tail = (size_t)avail - 3 - head;
+ char elided[160];
+ snprintf(elided, sizeof elided, "%.*s...%s", (int)head, line,
+ line + strlen(line) - tail);
+ memcpy(line, elided, sizeof line);
+ w = avail;
+ } else if (w > avail) {
+ /* A terminal too narrow for the dots gets a plain clip: `avail - 3`
+ * below five goes negative, and negative as size_t is a pointer into
+ * the weeds. */
+ w = avail;
+ }
uint16_t y = (uint16_t)(bottom - i);
if (y >= s->rows) break;
- uint16_t x = (uint16_t)(s->cols - w - CFG.gap * CFG.gap_aspect);
+ /* Clamped, not wrapped: a toast wider than the screen minus the gap used
+ * to compute a negative x, which as uint16_t became ~65533 and drew the
+ * whole message off-screen -- the long messages, which are the ones you
+ * most want to read, were the ones that vanished. */
+ int xi = (int)s->cols - (int)w - (int)(CFG.gap * CFG.gap_aspect);
+ uint16_t x = xi > 0 ? (uint16_t)xi : 0;
char clipped[160];
snprintf(clipped, sizeof clipped, "%.*s", (int)w, line);
screen_text(s, x, y, clipped, TOAST_FG, TOAST_BG, ATTR_BOLD);
diff --git a/src/app_session.c b/src/app_session.c
index 634239f..57371aa 100644
--- a/src/app_session.c
+++ b/src/app_session.c
@@ -2,8 +2,13 @@
#define _GNU_SOURCE
#include "app.h"
+#ifdef __APPLE__
+#include <libproc.h> /* Darwin has no /proc; live_cwd() uses this instead */
+#endif
+
#include <ghostty/vt.h>
#include <ctype.h>
+#include <limits.h>
#include <stdarg.h>
#include <time.h>
#include <sys/stat.h>
@@ -494,6 +499,10 @@ uint32_t app_workspace_tab(app_t *a, const char *slug) {
return t ? t->id : 0;
}
+/* Defined with live_cwd below; declared here because the workspace writer
+ * needs it first. */
+static const char *cwd_as_named(const pane_t *p, const char *live);
+
/* Make every tab this apply just built a member of the workspace.
*
* A tab the layout gave a purpose of its own keeps it and is not a member --
@@ -624,8 +633,11 @@ static bool tab_project(app_t *a, const tab_t *t, const char *path,
if (!t->focus) return false;
char cwdbuf[4096];
- return project_find(roots, depth,
- live_cwd(t->focus->pane, cwdbuf, sizeof cwdbuf), out);
+ return project_find(
+ roots, depth,
+ cwd_as_named(t->focus->pane,
+ live_cwd(t->focus->pane, cwdbuf, sizeof cwdbuf)),
+ out);
}
bool app_workspace_save(app_t *a, uint32_t tab, const char *path, int suspend,
@@ -761,6 +773,19 @@ static void sb_quoted(strbuf_t *b, const char *key, const char *val) {
* not where it was started. The kernel knows; nothing else does. */
const char *live_cwd(const pane_t *p, char *buf, size_t cap) {
pid_t pid = pane_pid(p);
+#ifdef __APPLE__
+ /* Darwin has no /proc, and the kernel still knows: libproc answers the same
+ * question from libc, no entitlement and no privilege needed for a process
+ * we started ourselves. */
+ if (pid > 0) {
+ struct proc_vnodepathinfo vpi;
+ if (proc_pidinfo(pid, PROC_PIDVNODEPATHINFO, 0, &vpi, sizeof vpi) ==
+ (int)sizeof vpi) {
+ snprintf(buf, cap, "%s", vpi.pvi_cdir.vip_path);
+ return buf;
+ }
+ }
+#endif
if (pid > 0) {
char link[64];
snprintf(link, sizeof link, "/proc/%d/cwd", (int)pid);
@@ -770,7 +795,26 @@ const char *live_cwd(const pane_t *p, char *buf, size_t cap) {
return buf;
}
}
- return pane_start_cwd(p); /* not running, or not Linux: what it was given */
+ return pane_start_cwd(
+ p); /* not running, or no kernel answer: what it was given */
+}
+
+/* The name the pane was given, when that is another spelling of where it is.
+ *
+ * The kernel answers with the physical path, and on macOS /tmp is a symlink to
+ * /private/tmp -- so a layout that said cwd="/tmp" came back saying
+ * cwd="/private/tmp", and a file that had been portable stopped round-tripping
+ * through a save. If the pane has actually moved, the two no longer agree and
+ * the live path wins, which is the whole point of asking the kernel. */
+static const char *cwd_as_named(const pane_t *p, const char *live) {
+ const char *start = pane_start_cwd(p);
+ /* Absolute only: a pane started with cwd="." is not saying "wherever slosh
+ * happens to be", and writing "." back out would make the dump mean
+ * something different every time it is read. */
+ if (!start || start[0] != '/' || !live) return live;
+ char real[PATH_MAX];
+ if (realpath(start, real) && strcmp(real, live) == 0) return start;
+ return live;
}
/* Everything one dump needs to know, so dump_node does not have to ask the app
@@ -853,7 +897,9 @@ static void dump_node(node_t *n, strbuf_t *b, int depth, dumpctx_t *ctx) {
* another machine. A directory outside the base stays absolute: it is not a
* fact about the project. */
sb_quoted(b, "cwd",
- path_relative(live_cwd(n->pane, cwdbuf, sizeof cwdbuf), ctx->base));
+ path_relative(
+ cwd_as_named(n->pane, live_cwd(n->pane, cwdbuf, sizeof cwdbuf)),
+ ctx->base));
char cmdbuf[4096];
const char *command = dump_command(n, cmdbuf, sizeof cmdbuf);
sb_quoted(b, "command", command);
diff --git a/src/pane.c b/src/pane.c
index 5cf0bab..08e86fb 100644
--- a/src/pane.c
+++ b/src/pane.c
@@ -3,6 +3,10 @@
#define _GNU_SOURCE
#include "slosh.h"
+#ifdef __APPLE__
+#include <sys/sysctl.h> /* KERN_PROCARGS2: Darwin has no /proc/<pid>/cmdline */
+#endif
+
#include <errno.h>
#include <stdio.h>
#include <ghostty/vt.h>
@@ -61,7 +65,11 @@ struct pane {
bool ephemeral;
char **argv;
char *cwd;
- char label[128];
+ /* A command line, not a word: a layout can name an interpreter by its full
+ * path (Homebrew's python3 resolves to 124 characters before its arguments
+ * even start), and a truncated label is not merely cosmetic -- it is what
+ * `dump-layout` writes down and what a restore then tries to run. */
+ char label[1024];
/* Selection anchor, in viewport coordinates, while a drag is in progress. */
bool selecting;
@@ -446,15 +454,53 @@ const char *pane_foreground(const pane_t *p, char *buf, size_t cap) {
pid_t fg = tcgetpgrp(p->pty.fd);
if (fg <= 0 || fg == p->pty.pid) return NULL; /* the pane's own shell */
+ char raw[4096];
+ size_t n = 0;
+#ifdef __APPLE__
+ /* No /proc: KERN_PROCARGS2 is where Darwin keeps a process's argv. The block
+ * is argc as an int, then the executable path, then padding, then argc
+ * NUL-terminated arguments -- so we step over the path and the padding to
+ * reach argv[0], and hand back the same NUL-separated shape Linux gives.
+ *
+ * The size is asked for rather than assumed: the block carries the process's
+ * whole environment after the arguments, and a buffer that cannot hold all of
+ * it comes back rearranged rather than merely short -- which parsed as a
+ * command missing its last argument. */
+ size_t need = 0;
+ int mib[3] = {CTL_KERN, KERN_PROCARGS2, (int)fg};
+ if (sysctl(mib, 3, NULL, &need, NULL, 0) != 0 || need < sizeof(int))
+ return NULL;
+ char *args = malloc(need);
+ if (!args) return NULL;
+ size_t asz = need;
+ if (sysctl(mib, 3, args, &asz, NULL, 0) != 0 || asz < sizeof(int)) {
+ free(args);
+ return NULL;
+ }
+ int argc = 0;
+ memcpy(&argc, args, sizeof argc);
+ size_t i = sizeof(int);
+ while (i < asz && args[i]) i++; /* the executable path */
+ while (i < asz && !args[i]) i++; /* the padding after it */
+ for (int a = 0; a < argc && i < asz; a++) {
+ size_t l = strlen(args + i) + 1;
+ if (n + l >= sizeof raw) break;
+ memcpy(raw + n, args + i, l);
+ n += l;
+ i += l;
+ }
+ free(args);
+ if (!n) return NULL;
+#else
char path[64];
snprintf(path, sizeof path, "/proc/%d/cmdline", (int)fg);
FILE *f = fopen(path, "rb");
if (!f) return NULL; /* gone between asking and reading, or not Linux */
- char raw[4096];
- size_t n = fread(raw, 1, sizeof raw - 1, f);
+ n = fread(raw, 1, sizeof raw - 1, f);
fclose(f);
if (!n) return NULL; /* a zombie or a kernel thread has no command line */
raw[n] = 0;
+#endif
size_t len = 0;
buf[0] = 0;
diff --git a/src/pty.c b/src/pty.c
index 33afb10..7f7ddb5 100644
--- a/src/pty.c
+++ b/src/pty.c
@@ -51,6 +51,11 @@ int pty_spawn(pty_t *p, const char *const argv[], uint16_t cols, uint16_t rows,
int slave = open(slave_name, O_RDWR);
if (slave < 0) _exit(127);
ioctl(slave, TIOCSCTTY, 0);
+ /* Darwin attaches no line discipline to the master until the slave is
+ * opened, so the TIOCSWINSZ above returns ENOTTY there and the size is
+ * dropped: the pane starts 0x0 and stays that way until something else
+ * resizes it. Setting it here, on the slave we just opened, works on both. */
+ ioctl(slave, TIOCSWINSZ, &ws);
dup2(slave, STDIN_FILENO);
dup2(slave, STDOUT_FILENO);
dup2(slave, STDERR_FILENO);
diff --git a/src/server.c b/src/server.c
index 4679e04..ab811d9 100644
--- a/src/server.c
+++ b/src/server.c
@@ -4,6 +4,10 @@
#define _GNU_SOURCE
#include "server.h"
+#ifdef __APPLE__
+#include <mach-o/dyld.h> /* _NSGetExecutablePath: Darwin has no /proc/self/exe */
+#endif
+
#include "config.h"
/* The config-file watcher speaks the OS's native change-notification API:
@@ -338,11 +342,20 @@ void session_env(const char *name) {
else
unsetenv("SLOSH_SESSION");
char self[512];
+#ifdef __APPLE__
+ /* Darwin's answer to /proc/self/exe. What it hands back can still contain
+ * symlinks and .., so realpath() finishes the job. */
+ char raw[512];
+ uint32_t sz = sizeof raw;
+ if (_NSGetExecutablePath(raw, &sz) == 0 && realpath(raw, self))
+ setenv("SLOSH_BIN", self, 1);
+#else
ssize_t sn = readlink("/proc/self/exe", self, sizeof self - 1);
if (sn > 0) {
self[sn] = 0;
setenv("SLOSH_BIN", self, 1);
}
+#endif
}
int server_run(const char *name, const char *const argv[], uint16_t cols,