Open a new terminal tab with Ctrl+Shift+T ($TERMINAL)

Adds a new-tab binding that launches a terminal (default alacritty,
overridable via the $TERMINAL environment variable, e.g.
TERMINAL="foot -e htop" tabbed-wlroots ...), inserting it right of the
active tab and activating it once it maps.

Also lets handle_new_toplevel claim exactly the add_tab()-prepared slot
instead of an earlier stale free one (a closed tab's hole), so the new
terminal keeps its position next to the tab it was launched from.
This commit is contained in:
Andrew Hamilton 2026-08-15 09:16:09 +10:00
parent 348c22ce4d
commit 0632f8e3d0
2 changed files with 108 additions and 7 deletions

View file

@ -98,6 +98,12 @@ struct tabbed_app {
GLuint icon_tex;
int icon_w, icon_h;
bool icon_resolved; /* true once a load attempt has been made */
/* Slot created by add_tab() (Ctrl+Shift+T) awaiting its toplevel. Lets
handle_new_toplevel claim exactly this slot instead of an earlier
stale free one (a closed tab's hole), so a new terminal keeps its
"right of the current tab" position. */
bool pending_claim;
};
struct tabbed_server {
@ -205,6 +211,13 @@ static volatile sig_atomic_t needs_redraw;
static bool frame_pending;
static bool shutting_down;
/* Terminal launched for the Ctrl+Shift+T "new tab" binding. Defaults to
alacritty (sane no-config font size, unlike foot's 8pt); overridable via
the $TERMINAL environment variable. The value is used verbatim by sh -c,
so it may carry arguments. */
static const char *term_cmd = "alacritty";
static bool term_key_held; /* Ctrl+Shift+T currently down (repeat latch) */
/* ===================== Clipboard bridge ===================== */
enum bridge_backend {
@ -287,6 +300,7 @@ static void set_active_surface(struct wlr_xdg_toplevel *toplevel);
static void render_and_present(void);
static struct tabbed_app *app_for_toplevel(struct wlr_xdg_toplevel *toplevel);
static int app_index(struct tabbed_app *app);
static int add_tab(const char *cmd);
/* Logging levels: quiet by default so a plain launch writes nothing to the
launching terminal except fatal errors. TABBED_LOG=1 shows status lines,
@ -1479,11 +1493,20 @@ static void handle_new_toplevel(struct wl_listener *listener, void *data) {
struct wlr_xdg_toplevel *toplevel = data;
struct tabbed_app *app = NULL;
for (int i = 0; i < server.num_apps; i++)
if (!server.apps[i]->toplevel) {
/* Prefer a slot prepared by add_tab() (Ctrl+Shift+T): its position was
chosen to sit right next to the tab the user launched it from, so an
earlier stale free slot (a closed tab's hole) must not steal it. */
for (int i = 0; i < server.num_apps && !app; i++)
if (!server.apps[i]->toplevel && server.apps[i]->pending_claim)
app = server.apps[i];
break;
}
/* Otherwise claim the first free slot, as before (CLI startup tabs are
claimed in connection order). */
if (!app)
for (int i = 0; i < server.num_apps; i++)
if (!server.apps[i]->toplevel) {
app = server.apps[i];
break;
}
if (!app) {
/* No slot for this toplevel (e.g. a dialog/second window opened by
an already-running client). Give it its own tab so it appears on
@ -1520,6 +1543,7 @@ static void handle_new_toplevel(struct wl_listener *listener, void *data) {
}
app->toplevel = toplevel;
app->pending_claim = false;
app->tree = wlr_scene_xdg_surface_create(&server.scene->tree,
toplevel->base);
/* hidden until it becomes the active tab */
@ -2787,6 +2811,30 @@ static void host_kbd_key(void *data, struct wl_keyboard *kbd, uint32_t serial,
}
return;
}
/* Ctrl+Shift+T opens a new terminal tab ($TERMINAL, default alacritty).
The tab is inserted right after the current one and becomes active when
it maps. Grabbed here so the focused client never sees it; a latch
drops host-synthesized repeats so one hold can't open a terminal per
repeat. */
{
bool ctrl = (host.host_mods_depressed & (1u << 2)) != 0;
bool shift = (host.host_mods_depressed & (1u << 0)) != 0;
bool is_t = server.xkb_state
? (sym == XKB_KEY_t || sym == XKB_KEY_T)
: key == 20;
if (ctrl && shift && is_t) {
kbd_repeat_cancel();
if (state == WL_KEYBOARD_KEY_STATE_PRESSED) {
if (!term_key_held) {
term_key_held = true;
add_tab(term_cmd);
}
} else {
term_key_held = false;
}
return;
}
}
/* any other key activity stops the tab-switch repeat */
kbd_repeat_cancel();
struct wlr_keyboard_key_event event = {
@ -3428,6 +3476,38 @@ static void launch_client(int idx) {
}
}
/* Add a new tab launching `cmd` and start it, exactly as if `cmd` had been
given as a command-line argument. The tab is inserted in the slot order
right after the currently active tab (falling back to the far right if
none is active), so the new tab sits to its right in the cycle. Returns
the new app index, or -1 on allocation failure. */
static int add_tab(const char *cmd) {
if (apps_ensure_cap(server.num_apps + 1) != 0) return -1;
struct tabbed_app *app = calloc(1, sizeof(*app));
if (!app) return -1;
app->cmd = strdup(cmd);
app->pid = -1;
app->pending_claim = true;
int at = server.num_apps;
struct tabbed_app *active_app = app_for_toplevel(server.active);
if (active_app)
for (int i = 0; i < server.num_apps; i++)
if (server.apps[i] == active_app) {
at = i + 1;
break;
}
if (at < server.num_apps)
memmove(&server.apps[at + 1], &server.apps[at],
(server.num_apps - at) * sizeof(*server.apps));
server.apps[at] = app;
int idx = at;
server.num_apps++;
launch_client(idx);
return idx;
}
/* ===================== Crash handler ===================== */
static void crash_handler(int sig, siginfo_t *info, void *ctx) {
@ -3655,13 +3735,22 @@ int main(int argc, char *argv[]) {
tabbed_log(TABBED_LOG_INFO, "Wayland socket: %s\n", tabbed_socket);
/* Ctrl+Shift+T opens a terminal named by $TERMINAL when set, else the
alacritty default. Every command-line argument is one tab launcher. */
{
const char *env_term = getenv("TERMINAL");
if (env_term && env_term[0])
term_cmd = env_term;
}
int total_apps = argc - 1;
if (total_apps == 0) {
fprintf(stderr, "usage: %s <prog> [prog...]\n"
" Each <prog> is one shell command; quote it when it "
"takes args, e.g. \"firefox -no-remote\".\n"
" Arguments written bare (starting with '-') launch a "
"bogus extra tab.\n", argv[0]);
"bogus extra tab.\n"
" $TERMINAL names the terminal Ctrl+Shift+T opens; "
"default alacritty.\n", argv[0]);
remove_socket_file();
return 1;
}
@ -3707,7 +3796,8 @@ int main(int argc, char *argv[]) {
}
tabbed_log(TABBED_LOG_INFO, "tabbed-wlroots: running. "
"Alt+Up/Down switches tabs.\n");
"Alt+Up/Down switches tabs; Ctrl+Shift+T opens a terminal "
"(%s).\n", term_cmd);
/* Main event loop: service both the server and the host displays */
int server_fd = wl_event_loop_get_fd(server.loop);