aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authoruser <user@clank>2026-09-02 14:57:13 +0200
committeruser <user@clank>2026-09-02 14:57:13 +0200
commitd698946464f25a0eb43730ffad15656f3247f56b (patch)
tree0d040eb64e433122f982ea45912eba37ab54822a /src
parentAdd competitive multiplayer support. (diff)
downloadslosh-d698946464f25a0eb43730ffad15656f3247f56b.tar.gz
slosh-d698946464f25a0eb43730ffad15656f3247f56b.tar.xz
slosh-d698946464f25a0eb43730ffad15656f3247f56b.zip
multi-attach: the config knobs
The patch made shared attach unconditional; these make it a policy. multi_attach false restores the classic displacing attach (EXIT_REPLACED, which old clients already understand). size_follows picks whose size the shared screen takes: active (the patch's rule), smallest (the tmux rule: filler, never a crop), or largest. The size fold lives in apply_canvas, so any attach, resize or departure can move the canvas under the folding policies -- including a non-active client leaving, which under smallest may have been the very client holding the floor down. A config reload re-applies both knobs to the running session.
Diffstat (limited to 'src')
-rw-r--r--src/app.c10
-rw-r--r--src/config.c27
-rw-r--r--src/server.c98
3 files changed, 123 insertions, 12 deletions
diff --git a/src/app.c b/src/app.c
index d161e6b..9c9829b 100644
--- a/src/app.c
+++ b/src/app.c
@@ -88,6 +88,16 @@ size_t app_config_files(const char **out, size_t max) {
return config_files(&CFG, out, max);
}
+bool app_cfg_multi_attach(void) {
+ ensure_config();
+ return CFG.multi_attach;
+}
+
+int app_cfg_size_follows(void) {
+ ensure_config();
+ return CFG.size_follows;
+}
+
/* ---- tree --------------------------------------------------------------- */
#include "app_internal.h"
diff --git a/src/config.c b/src/config.c
index 0c7af63..bfbfdf7 100644
--- a/src/config.c
+++ b/src/config.c
@@ -918,6 +918,8 @@ void config_defaults(config_t *c) {
* than as another row of it. */
c->status_pad = 4;
c->focus_follows_mouse = true;
+ c->multi_attach = true;
+ c->size_follows = SIZE_FOLLOWS_ACTIVE;
c->in_band_shaders = false;
/* Windows has no VEOF for the line discipline to act on, so ^D there is a
* key that does nothing until we make it mean something. POSIX already has
@@ -1607,6 +1609,14 @@ char *config_render(const config_t *c) {
cb_add(&b, "\n// ---- behaviour ----\n");
cb_add(&b, "focus_follows_mouse %s\n", yesno(c->focus_follows_mouse));
+ cb_add(&b,
+ "multi_attach %s // several terminals at once; off: a new "
+ "client displaces the old\n",
+ yesno(c->multi_attach));
+ cb_add(&b, "size_follows \"%s\" // or \"smallest\" / \"largest\"\n",
+ c->size_follows == SIZE_FOLLOWS_SMALLEST ? "smallest"
+ : c->size_follows == SIZE_FOLLOWS_LARGEST ? "largest"
+ : "active");
cb_add(&b, "in_band_shaders %s\n", yesno(c->in_band_shaders));
cb_add(&b,
"ctrl_d_exits %s // ^D at an idle prompt exits the shell; "
@@ -1861,6 +1871,7 @@ static const char *const KNOWN_TOP[] = {
"keys",
"min_mark",
"min_pane",
+ "multi_attach",
"min_split",
"modal_scrim",
"newtab_mark",
@@ -1875,6 +1886,7 @@ static const char *const KNOWN_TOP[] = {
"select_scroll_ms",
"shaders",
"shell",
+ "size_follows",
"splash_ms",
"states",
"status_bar",
@@ -2081,6 +2093,21 @@ static bool load_into(config_t *c, const char *path, int depth, char *err,
(uint16_t)kdl_arg_int(kdl_child(root, "status_pad"), 0, c->status_pad);
c->focus_follows_mouse = kdl_arg_bool(kdl_child(root, "focus_follows_mouse"),
0, c->focus_follows_mouse);
+ c->multi_attach =
+ kdl_arg_bool(kdl_child(root, "multi_attach"), 0, c->multi_attach);
+ const char *sizef = kdl_arg(kdl_child(root, "size_follows"), 0, NULL);
+ if (sizef) {
+ if (strcmp(sizef, "active") == 0)
+ c->size_follows = SIZE_FOLLOWS_ACTIVE;
+ else if (strcmp(sizef, "smallest") == 0)
+ c->size_follows = SIZE_FOLLOWS_SMALLEST;
+ else if (strcmp(sizef, "largest") == 0)
+ c->size_follows = SIZE_FOLLOWS_LARGEST;
+ else
+ complain(c, err, errcap, 0,
+ "size_follows wants active, smallest or largest, not \"%s\"",
+ sizef);
+ }
c->in_band_shaders =
kdl_arg_bool(kdl_child(root, "in_band_shaders"), 0, c->in_band_shaders);
c->ctrl_d_exits =
diff --git a/src/server.c b/src/server.c
index f8f3dd3..7443648 100644
--- a/src/server.c
+++ b/src/server.c
@@ -351,16 +351,49 @@ static bool push_clients(server_t *s) {
return ok;
}
-static void apply_client_size(server_t *s, conn_t *c) {
- bool changed = c->cols != s->screen.cols || c->rows != s->screen.rows;
+/* Size the canonical screen by policy (size_follows). "active" takes the
+ * active client's size; "smallest"/"largest" fold every attached display, so
+ * any attach, resize or departure can move the canvas. The active client
+ * still supplies the cell pixel size either way: graphics go to terminals,
+ * and the active one is the best guess at what a cell is worth. */
+static void apply_canvas(server_t *s) {
+ conn_t *a = active_conn(s);
+ int policy = app_cfg_size_follows();
+ uint16_t cols = 0, rows = 0;
+ if (policy == SIZE_FOLLOWS_ACTIVE) {
+ if (a) {
+ cols = a->cols;
+ rows = a->rows;
+ }
+ } else {
+ for (size_t i = 0; i < s->nconns; i++) {
+ conn_t *c = &s->conns[i];
+ if (!c->display || !c->cols || !c->rows) continue;
+ if (!cols) {
+ cols = c->cols;
+ rows = c->rows;
+ } else if (policy == SIZE_FOLLOWS_SMALLEST) {
+ if (c->cols < cols) cols = c->cols;
+ if (c->rows < rows) rows = c->rows;
+ } else {
+ if (c->cols > cols) cols = c->cols;
+ if (c->rows > rows) rows = c->rows;
+ }
+ }
+ }
+ if (!cols || !rows) { /* nobody is looking: leave the canvas standing */
+ request_paint(s, true);
+ return;
+ }
+ bool changed = cols != s->screen.cols || rows != s->screen.rows;
uint16_t old_w = 0, old_h = 0;
app_cell_px(s->app, &old_w, &old_h);
- uint16_t cell_w = c->cell_w ? c->cell_w : s->fallback_cell_w;
- uint16_t cell_h = c->cell_h ? c->cell_h : s->fallback_cell_h;
+ uint16_t cell_w = a && a->cell_w ? a->cell_w : s->fallback_cell_w;
+ uint16_t cell_h = a && a->cell_h ? a->cell_h : s->fallback_cell_h;
bool px_changed = cell_w != old_w || cell_h != old_h;
if (changed) {
- screen_resize(&s->screen, c->cols, c->rows);
- app_resize(s->app, c->cols, c->rows);
+ screen_resize(&s->screen, cols, rows);
+ app_resize(s->app, cols, rows);
}
if (px_changed) app_set_cell_px(s->app, cell_w, cell_h);
if (changed || px_changed) app_compose(s->app, &s->screen);
@@ -371,7 +404,7 @@ static void make_active(server_t *s, conn_t *c, bool new_activity) {
if (s->active_fd != c->fd) app_cancel_client_interaction(s->app);
s->active_fd = c->fd;
if (new_activity) c->activity = ++s->activity_seq;
- apply_client_size(s, c);
+ apply_canvas(s);
}
static void promote_latest(server_t *s) {
@@ -387,15 +420,24 @@ static void promote_latest(server_t *s) {
return;
}
s->active_fd = latest->fd;
- apply_client_size(s, latest);
+ apply_canvas(s);
}
+/* A non-active departure changes nothing under "active", but under
+ * "smallest"/"largest" the canvas may have been sized by the very client
+ * that just left. */
static void close_conn(server_t *s, size_t i) {
- if (conn_close(s, i)) promote_latest(s);
+ if (conn_close(s, i))
+ promote_latest(s);
+ else
+ apply_canvas(s);
}
static void drop_conn(server_t *s, size_t i, uint8_t reason) {
- if (conn_drop(s, i, reason)) promote_latest(s);
+ if (conn_drop(s, i, reason))
+ promote_latest(s);
+ else
+ apply_canvas(s);
}
static void drop_fd(server_t *s, int fd, uint8_t reason) {
@@ -451,7 +493,7 @@ static bool feed_client(server_t *s, conn_t *c, const uint8_t *data, size_t len,
input_timeout(c->in, client_event, &ctx);
else
input_feed(c->in, data, len, client_event, &ctx);
- if (ctx.activity) apply_client_size(s, c);
+ if (ctx.activity) apply_canvas(s);
if (input_pending(c->in)) c->esc_due = now_ms() + ESC_MS;
return ctx.activity;
}
@@ -830,6 +872,22 @@ int server_run(const char *name, const char *const argv[], uint16_t cols,
bool first_viewer = fresh && display_count(&s) == 0;
if (!c->display && !fresh) break;
+ /* multi_attach false is the classic rule: the new client displaces
+ * every display before it (EXIT_REPLACED, which old clients already
+ * understand). Dropping compacts the array, so the index -- and the
+ * pointer taken from it -- are re-derived afterwards. */
+ if (fresh && !app_cfg_multi_attach()) {
+ for (size_t j = 0; j < s.nconns;) {
+ if (j != ci && s.conns[j].display) {
+ conn_drop(&s, j, EXIT_REPLACED);
+ if (j < ci) ci--; /* the array compacted under us */
+ } else
+ j++;
+ }
+ c = &s.conns[ci];
+ first_viewer = display_count(&s) == 0;
+ }
+
uint16_t cols = c->cols ? c->cols : s.screen.cols;
uint16_t rows = c->rows ? c->rows : s.screen.rows;
if (m.len >= 4) {
@@ -868,7 +926,10 @@ int server_run(const char *name, const char *const argv[], uint16_t cols,
c->cell_w = cell_w;
c->cell_h = cell_h;
if (local_resize) screen_resize(&c->view, cols, rows);
- if (c->fd == s.active_fd) apply_client_size(&s, c);
+ /* Unconditional: under "active" a non-active resize changes
+ * nothing (apply_canvas reads the active client), but under
+ * "smallest"/"largest" every display's size is a vote. */
+ apply_canvas(&s);
}
update_viewport(c, &s.screen);
request_paint(&s, true);
@@ -960,6 +1021,19 @@ int server_run(const char *name, const char *const argv[], uint16_t cols,
* include becomes ten minutes of wondering. */
const char *why = app_config_complaint();
app_toast(s.app, why && *why ? why : "config reloaded");
+ /* The knobs this loop lives by may have moved. multi_attach turned
+ * off evicts every display but the active one -- the reloaded config
+ * says one client, so one client it is -- and a changed size_follows
+ * re-sizes the canvas to the policy now in force. */
+ if (!app_cfg_multi_attach()) {
+ for (size_t i = 0; i < s.nconns;) {
+ if (s.conns[i].display && s.conns[i].fd != s.active_fd)
+ drop_conn(&s, i, EXIT_REPLACED);
+ else
+ i++;
+ }
+ }
+ apply_canvas(&s);
} else {
app_toast(s.app, err[0] ? err : "config reload failed");
}