1902 lines
69 KiB
Diff
1902 lines
69 KiB
Diff
diff --git a/config.c b/config.c
|
||
index 481d4c4f..7684b570 100644
|
||
--- a/config.c
|
||
+++ b/config.c
|
||
@@ -148,6 +148,21 @@ static const char *const binding_action_map[] = {
|
||
[BIND_ACTION_THEME_SWITCH_LIGHT] = "color-theme-switch-light",
|
||
[BIND_ACTION_THEME_TOGGLE] = "color-theme-toggle",
|
||
|
||
+ /* Tab actions */
|
||
+ [BIND_ACTION_TAB_NEW] = "tab-new",
|
||
+ [BIND_ACTION_TAB_CLOSE] = "tab-close",
|
||
+ [BIND_ACTION_TAB_NEXT] = "tab-next",
|
||
+ [BIND_ACTION_TAB_PREV] = "tab-prev",
|
||
+ [BIND_ACTION_TAB_1] = "tab-1",
|
||
+ [BIND_ACTION_TAB_2] = "tab-2",
|
||
+ [BIND_ACTION_TAB_3] = "tab-3",
|
||
+ [BIND_ACTION_TAB_4] = "tab-4",
|
||
+ [BIND_ACTION_TAB_5] = "tab-5",
|
||
+ [BIND_ACTION_TAB_6] = "tab-6",
|
||
+ [BIND_ACTION_TAB_7] = "tab-7",
|
||
+ [BIND_ACTION_TAB_8] = "tab-8",
|
||
+ [BIND_ACTION_TAB_9] = "tab-9",
|
||
+
|
||
/* Mouse-specific actions */
|
||
[BIND_ACTION_SCROLLBACK_UP_MOUSE] = "scrollback-up-mouse",
|
||
[BIND_ACTION_SCROLLBACK_DOWN_MOUSE] = "scrollback-down-mouse",
|
||
@@ -1816,6 +1831,84 @@ parse_section_csd(struct context *ctx)
|
||
}
|
||
}
|
||
|
||
+static bool
|
||
+parse_section_tabs(struct context *ctx)
|
||
+{
|
||
+ struct config *conf = ctx->conf;
|
||
+ const char *key = ctx->key;
|
||
+
|
||
+ if (streq(key, "enabled"))
|
||
+ return value_to_bool(ctx, &conf->tabs.enabled);
|
||
+
|
||
+ else if (streq(key, "position")) {
|
||
+ _Static_assert(sizeof(conf->tabs.position) == sizeof(int),
|
||
+ "enum is not 32-bit");
|
||
+ return value_to_enum(
|
||
+ ctx,
|
||
+ (const char *[]){"top", "bottom", NULL},
|
||
+ (int *)&conf->tabs.position);
|
||
+ }
|
||
+
|
||
+ else if (streq(key, "style")) {
|
||
+ _Static_assert(sizeof(conf->tabs.style) == sizeof(int),
|
||
+ "enum is not 32-bit");
|
||
+ return value_to_enum(
|
||
+ ctx,
|
||
+ (const char *[]){"rounded", "square", "gradient", NULL},
|
||
+ (int *)&conf->tabs.style);
|
||
+ }
|
||
+
|
||
+ else if (streq(key, "layout")) {
|
||
+ _Static_assert(sizeof(conf->tabs.layout) == sizeof(int),
|
||
+ "enum is not 32-bit");
|
||
+ return value_to_enum(
|
||
+ ctx,
|
||
+ (const char *[]){"span", "floating", NULL},
|
||
+ (int *)&conf->tabs.layout);
|
||
+ }
|
||
+
|
||
+ else if (streq(key, "height"))
|
||
+ return value_to_uint16(ctx, 10, &conf->tabs.height);
|
||
+
|
||
+ else if (streq(key, "title-max-length"))
|
||
+ return value_to_uint16(ctx, 10, &conf->tabs.title_max_length);
|
||
+
|
||
+ else if (streq(key, "tab-width"))
|
||
+ return value_to_uint16(ctx, 10, &conf->tabs.tab_width);
|
||
+
|
||
+ else if (streq(key, "tab-padding"))
|
||
+ return value_to_uint16(ctx, 10, &conf->tabs.tab_padding);
|
||
+
|
||
+ else if (streq(key, "label-padding"))
|
||
+ return value_to_uint16(ctx, 10, &conf->tabs.label_padding);
|
||
+
|
||
+ else if (streq(key, "margin"))
|
||
+ return value_to_uint16(ctx, 10, &conf->tabs.margin);
|
||
+
|
||
+ else if (streq(key, "corner-radius"))
|
||
+ return value_to_uint16(ctx, 10, &conf->tabs.corner_radius);
|
||
+
|
||
+ else if (streq(key, "background"))
|
||
+ return value_to_color(ctx, &conf->tabs.colors.bg, false);
|
||
+
|
||
+ else if (streq(key, "foreground"))
|
||
+ return value_to_color(ctx, &conf->tabs.colors.fg, false);
|
||
+
|
||
+ else if (streq(key, "active-background"))
|
||
+ return value_to_color(ctx, &conf->tabs.colors.active_bg, false);
|
||
+
|
||
+ else if (streq(key, "active-foreground"))
|
||
+ return value_to_color(ctx, &conf->tabs.colors.active_fg, false);
|
||
+
|
||
+ else if (streq(key, "inherit-cwd"))
|
||
+ return value_to_bool(ctx, &conf->tabs.inherit_cwd);
|
||
+
|
||
+ else {
|
||
+ LOG_CONTEXTUAL_ERR("not a valid tabs option: %s", key);
|
||
+ return false;
|
||
+ }
|
||
+}
|
||
+
|
||
static void
|
||
free_binding_aux(struct binding_aux *aux)
|
||
{
|
||
@@ -3062,6 +3155,7 @@ enum section {
|
||
SECTION_ENVIRONMENT,
|
||
SECTION_TWEAK,
|
||
SECTION_TOUCH,
|
||
+ SECTION_TABS,
|
||
|
||
/* Deprecated */
|
||
SECTION_COLORS,
|
||
@@ -3098,6 +3192,7 @@ static const struct {
|
||
[SECTION_ENVIRONMENT] = {&parse_section_environment, "environment"},
|
||
[SECTION_TWEAK] = {&parse_section_tweak, "tweak"},
|
||
[SECTION_TOUCH] = {&parse_section_touch, "touch"},
|
||
+ [SECTION_TABS] = {&parse_section_tabs, "tabs"},
|
||
|
||
/* Deprecated */
|
||
[SECTION_COLORS] = {&parse_section_colors, "colors"},
|
||
@@ -3343,6 +3438,10 @@ add_default_key_bindings(struct config *conf)
|
||
{BIND_ACTION_FONT_SIZE_RESET, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_0}}},
|
||
{BIND_ACTION_FONT_SIZE_RESET, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_KP_0}}},
|
||
{BIND_ACTION_SPAWN_TERMINAL, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_n}}},
|
||
+ {BIND_ACTION_TAB_NEW, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_t}}},
|
||
+ {BIND_ACTION_TAB_CLOSE, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_w}}},
|
||
+ {BIND_ACTION_TAB_NEXT, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_Tab}}},
|
||
+ {BIND_ACTION_TAB_PREV, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_Tab}}},
|
||
{BIND_ACTION_SHOW_URLS_LAUNCH, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_o}}},
|
||
{BIND_ACTION_UNICODE_INPUT, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_u}}},
|
||
{BIND_ACTION_PROMPT_PREV, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_z}}},
|
||
@@ -3630,6 +3729,27 @@ config_load(struct config *conf, const char *conf_path,
|
||
.long_press_delay = 400,
|
||
},
|
||
|
||
+ .tabs = {
|
||
+ .enabled = false,
|
||
+ .inherit_cwd = false,
|
||
+ .position = CONF_TABS_POSITION_TOP,
|
||
+ .style = CONF_TABS_STYLE_ROUNDED,
|
||
+ .layout = CONF_TABS_LAYOUT_SPAN,
|
||
+ .height = 26,
|
||
+ .tab_width = 200,
|
||
+ .tab_padding = 8,
|
||
+ .label_padding = 8,
|
||
+ .margin = 4,
|
||
+ .corner_radius = 6,
|
||
+ .title_max_length = 100,
|
||
+ .colors = {
|
||
+ .bg = 0x1c1c1c,
|
||
+ .fg = 0xb0b0b0,
|
||
+ .active_bg = 0x3a3a3a,
|
||
+ .active_fg = 0xffffff,
|
||
+ },
|
||
+ },
|
||
+
|
||
.env_vars = tll_init(),
|
||
#if defined(UTMP_DEFAULT_HELPER_PATH)
|
||
.utmp_helper_path = ((strlen(UTMP_DEFAULT_HELPER_PATH) > 0 &&
|
||
diff --git a/config.h b/config.h
|
||
index f8e99df3..905f7079 100644
|
||
--- a/config.h
|
||
+++ b/config.h
|
||
@@ -466,6 +466,37 @@ struct config {
|
||
uint32_t long_press_delay;
|
||
} touch;
|
||
|
||
+ struct {
|
||
+ bool enabled;
|
||
+ enum {
|
||
+ CONF_TABS_POSITION_TOP,
|
||
+ CONF_TABS_POSITION_BOTTOM,
|
||
+ } position;
|
||
+ enum {
|
||
+ CONF_TABS_STYLE_ROUNDED,
|
||
+ CONF_TABS_STYLE_SQUARE,
|
||
+ CONF_TABS_STYLE_GRADIENT,
|
||
+ } style;
|
||
+ enum {
|
||
+ CONF_TABS_LAYOUT_SPAN,
|
||
+ CONF_TABS_LAYOUT_FLOATING,
|
||
+ } layout;
|
||
+ uint16_t height; /* pill height; bar = height + margin in floating mode */
|
||
+ uint16_t tab_width; /* max tab width in floating mode */
|
||
+ uint16_t tab_padding; /* gap between tabs in floating mode */
|
||
+ uint16_t label_padding; /* horizontal padding around the label inside each tab pill */
|
||
+ uint16_t margin; /* edge gap in floating mode (added to bar height) */
|
||
+ uint16_t corner_radius;
|
||
+ uint16_t title_max_length; /* max chars in composite window title before eliding with "..." */
|
||
+ bool inherit_cwd;
|
||
+ struct {
|
||
+ uint32_t bg;
|
||
+ uint32_t fg;
|
||
+ uint32_t active_bg;
|
||
+ uint32_t active_fg;
|
||
+ } colors;
|
||
+ } tabs;
|
||
+
|
||
user_notifications_t notifications;
|
||
};
|
||
|
||
diff --git a/cursor-shape.c b/cursor-shape.c
|
||
index c195a554..e68411c0 100644
|
||
--- a/cursor-shape.c
|
||
+++ b/cursor-shape.c
|
||
@@ -16,6 +16,7 @@ cursor_shape_to_string(enum cursor_shape shape)
|
||
[CURSOR_SHAPE_NONE] = {NULL},
|
||
[CURSOR_SHAPE_HIDDEN] = {"hidden", NULL},
|
||
[CURSOR_SHAPE_LEFT_PTR] = {"default", "left_ptr", NULL},
|
||
+ [CURSOR_SHAPE_POINTER] = {"pointer", "hand1", NULL},
|
||
[CURSOR_SHAPE_TEXT] = {"text", "xterm", NULL},
|
||
[CURSOR_SHAPE_TOP_LEFT_CORNER] = {"nw-resize", "top_left_corner", NULL},
|
||
[CURSOR_SHAPE_TOP_RIGHT_CORNER] = {"ne-resize", "top_right_corner", NULL},
|
||
@@ -37,6 +38,7 @@ cursor_shape_to_server_shape(enum cursor_shape shape)
|
||
{
|
||
static const enum wp_cursor_shape_device_v1_shape table[CURSOR_SHAPE_COUNT] = {
|
||
[CURSOR_SHAPE_LEFT_PTR] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT,
|
||
+ [CURSOR_SHAPE_POINTER] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_POINTER,
|
||
[CURSOR_SHAPE_TEXT] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_TEXT,
|
||
[CURSOR_SHAPE_TOP_LEFT_CORNER] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NW_RESIZE,
|
||
[CURSOR_SHAPE_TOP_RIGHT_CORNER] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NE_RESIZE,
|
||
diff --git a/cursor-shape.h b/cursor-shape.h
|
||
index 13690588..51a411ed 100644
|
||
--- a/cursor-shape.h
|
||
+++ b/cursor-shape.h
|
||
@@ -8,6 +8,7 @@ enum cursor_shape {
|
||
CURSOR_SHAPE_HIDDEN,
|
||
|
||
CURSOR_SHAPE_LEFT_PTR,
|
||
+ CURSOR_SHAPE_POINTER,
|
||
CURSOR_SHAPE_TEXT,
|
||
CURSOR_SHAPE_TOP_LEFT_CORNER,
|
||
CURSOR_SHAPE_TOP_RIGHT_CORNER,
|
||
diff --git a/foot.ini b/foot.ini
|
||
index 81419d88..8b735fe1 100644
|
||
--- a/foot.ini
|
||
+++ b/foot.ini
|
||
@@ -175,6 +175,24 @@
|
||
# Same builtin defaults as [color], except for:
|
||
# dim-blend-towards=white
|
||
|
||
+[tabs]
|
||
+enabled=yes
|
||
+position=bottom
|
||
+style=rounded # rounded | square | gradient
|
||
+layout=floating
|
||
+height=26
|
||
+# tab-width=200 (max width per tab in floating mode)
|
||
+# tab-padding=8 (gap between tabs in floating mode)
|
||
+# label-padding=8 (horizontal padding around the label inside each tab pill)
|
||
+# margin=4 (edge gap; auto-added to bar height, does not squish pill)
|
||
+# corner-radius=6 (corner rounding in pixels)
|
||
+# title-max-length=100 (multi-tab window title elides at this many chars with "...")
|
||
+# background=1c1c1c
|
||
+# foreground=b0b0b0
|
||
+# active-background=3a3a3a
|
||
+# active-foreground=ffffff
|
||
+# inherit-cwd=no (new tabs open in the active tab's cwd; requires OSC 7 shell support)
|
||
+
|
||
[csd]
|
||
# preferred=server
|
||
# size=26
|
||
diff --git a/input.c b/input.c
|
||
index 6a829a70..92e9fc1a 100644
|
||
--- a/input.c
|
||
+++ b/input.c
|
||
@@ -457,6 +457,45 @@ execute_binding(struct seat *seat, struct terminal *term,
|
||
term_shutdown(term);
|
||
return true;
|
||
|
||
+ case BIND_ACTION_TAB_NEW:
|
||
+ term_tab_new(term, 0, NULL, NULL, term->shutdown.cb, term->shutdown.cb_data);
|
||
+ return true;
|
||
+
|
||
+ case BIND_ACTION_TAB_CLOSE:
|
||
+ term_tab_close(term);
|
||
+ return true;
|
||
+
|
||
+ case BIND_ACTION_TAB_NEXT: {
|
||
+ struct wl_window *win = term->window;
|
||
+ if (win->tab_count > 1)
|
||
+ term_tab_switch(win, (win->active_tab + 1) % win->tab_count);
|
||
+ return true;
|
||
+ }
|
||
+
|
||
+ case BIND_ACTION_TAB_PREV: {
|
||
+ struct wl_window *win = term->window;
|
||
+ if (win->tab_count > 1)
|
||
+ term_tab_switch(win,
|
||
+ win->active_tab == 0 ? win->tab_count - 1 : win->active_tab - 1);
|
||
+ return true;
|
||
+ }
|
||
+
|
||
+ case BIND_ACTION_TAB_1:
|
||
+ case BIND_ACTION_TAB_2:
|
||
+ case BIND_ACTION_TAB_3:
|
||
+ case BIND_ACTION_TAB_4:
|
||
+ case BIND_ACTION_TAB_5:
|
||
+ case BIND_ACTION_TAB_6:
|
||
+ case BIND_ACTION_TAB_7:
|
||
+ case BIND_ACTION_TAB_8:
|
||
+ case BIND_ACTION_TAB_9: {
|
||
+ size_t idx = (size_t)(action - BIND_ACTION_TAB_1);
|
||
+ struct wl_window *win = term->window;
|
||
+ if (idx < win->tab_count)
|
||
+ term_tab_switch(win, idx);
|
||
+ return true;
|
||
+ }
|
||
+
|
||
case BIND_ACTION_REGEX_LAUNCH:
|
||
case BIND_ACTION_REGEX_COPY:
|
||
if (binding->aux->type != BINDING_AUX_REGEX)
|
||
@@ -2510,6 +2549,7 @@ wl_pointer_enter(void *data, struct wl_pointer *wl_pointer,
|
||
case TERM_SURF_BORDER_RIGHT:
|
||
case TERM_SURF_BORDER_TOP:
|
||
case TERM_SURF_BORDER_BOTTOM:
|
||
+ case TERM_SURF_TAB_BAR:
|
||
break;
|
||
|
||
case TERM_SURF_BUTTON_MINIMIZE:
|
||
@@ -2582,7 +2622,7 @@ wl_pointer_leave(void *data, struct wl_pointer *wl_pointer,
|
||
if (surface != NULL) {
|
||
/* Sway 1.4 sends this event with a NULL surface when we destroy the window */
|
||
const struct wl_window UNUSED *win = wl_surface_get_user_data(surface);
|
||
- xassert(old_moused == win->term);
|
||
+ xassert(old_moused->window == win);
|
||
}
|
||
|
||
enum term_surface active_surface = old_moused->active_surface;
|
||
@@ -2609,6 +2649,7 @@ wl_pointer_leave(void *data, struct wl_pointer *wl_pointer,
|
||
case TERM_SURF_BORDER_RIGHT:
|
||
case TERM_SURF_BORDER_TOP:
|
||
case TERM_SURF_BORDER_BOTTOM:
|
||
+ case TERM_SURF_TAB_BAR:
|
||
break;
|
||
}
|
||
|
||
@@ -2693,6 +2734,7 @@ wl_pointer_motion(void *data, struct wl_pointer *wl_pointer,
|
||
case TERM_SURF_BORDER_RIGHT:
|
||
case TERM_SURF_BORDER_TOP:
|
||
case TERM_SURF_BORDER_BOTTOM:
|
||
+ case TERM_SURF_TAB_BAR:
|
||
break;
|
||
}
|
||
|
||
@@ -2743,6 +2785,7 @@ wl_pointer_motion(void *data, struct wl_pointer *wl_pointer,
|
||
case TERM_SURF_BORDER_RIGHT:
|
||
case TERM_SURF_BORDER_TOP:
|
||
case TERM_SURF_BORDER_BOTTOM:
|
||
+ case TERM_SURF_TAB_BAR:
|
||
break;
|
||
|
||
case TERM_SURF_GRID: {
|
||
@@ -3294,6 +3337,21 @@ wl_pointer_button(void *data, struct wl_pointer *wl_pointer,
|
||
break;
|
||
}
|
||
|
||
+ case TERM_SURF_TAB_BAR: {
|
||
+ if (state != WL_POINTER_BUTTON_STATE_PRESSED)
|
||
+ break;
|
||
+ if (button != BTN_LEFT)
|
||
+ break;
|
||
+
|
||
+ size_t idx = term_tab_bar_hit_test(term, seat->mouse.x, seat->mouse.y);
|
||
+ if (idx == SIZE_MAX || idx >= term->window->tab_count)
|
||
+ break;
|
||
+ if (idx == term->window->active_tab)
|
||
+ break;
|
||
+ term_tab_switch(term->window, idx);
|
||
+ break;
|
||
+ }
|
||
+
|
||
case TERM_SURF_NONE:
|
||
BUG("Invalid surface type");
|
||
break;
|
||
diff --git a/key-binding.h b/key-binding.h
|
||
index c4a04e99..acc11a7a 100644
|
||
--- a/key-binding.h
|
||
+++ b/key-binding.h
|
||
@@ -49,6 +49,21 @@ enum bind_action_normal {
|
||
BIND_ACTION_THEME_SWITCH_LIGHT,
|
||
BIND_ACTION_THEME_TOGGLE,
|
||
|
||
+ /* Tab actions */
|
||
+ BIND_ACTION_TAB_NEW,
|
||
+ BIND_ACTION_TAB_CLOSE,
|
||
+ BIND_ACTION_TAB_NEXT,
|
||
+ BIND_ACTION_TAB_PREV,
|
||
+ BIND_ACTION_TAB_1,
|
||
+ BIND_ACTION_TAB_2,
|
||
+ BIND_ACTION_TAB_3,
|
||
+ BIND_ACTION_TAB_4,
|
||
+ BIND_ACTION_TAB_5,
|
||
+ BIND_ACTION_TAB_6,
|
||
+ BIND_ACTION_TAB_7,
|
||
+ BIND_ACTION_TAB_8,
|
||
+ BIND_ACTION_TAB_9,
|
||
+
|
||
/* Mouse specific actions - i.e. they require a mouse coordinate */
|
||
BIND_ACTION_SCROLLBACK_UP_MOUSE,
|
||
BIND_ACTION_SCROLLBACK_DOWN_MOUSE,
|
||
@@ -61,7 +76,7 @@ enum bind_action_normal {
|
||
BIND_ACTION_SELECT_QUOTE,
|
||
BIND_ACTION_SELECT_ROW,
|
||
|
||
- BIND_ACTION_KEY_COUNT = BIND_ACTION_THEME_TOGGLE + 1,
|
||
+ BIND_ACTION_KEY_COUNT = BIND_ACTION_TAB_9 + 1,
|
||
BIND_ACTION_COUNT = BIND_ACTION_SELECT_ROW + 1,
|
||
};
|
||
|
||
diff --git a/osc.c b/osc.c
|
||
index 82793fb5..90704b70 100644
|
||
--- a/osc.c
|
||
+++ b/osc.c
|
||
@@ -15,6 +15,7 @@
|
||
#include "macros.h"
|
||
#include "notify.h"
|
||
#include "selection.h"
|
||
+#include "render.h"
|
||
#include "terminal.h"
|
||
#include "uri.h"
|
||
#include "util.h"
|
||
@@ -468,6 +469,7 @@ osc_set_pwd(struct terminal *term, char *string)
|
||
LOG_DBG("OSC7: pwd: %s", path);
|
||
free(term->cwd);
|
||
term->cwd = path;
|
||
+ render_refresh_tab_bar(term);
|
||
} else
|
||
free(path);
|
||
|
||
diff --git a/pgo/pgo.c b/pgo/pgo.c
|
||
index 4ff4111c..96ddcce7 100644
|
||
--- a/pgo/pgo.c
|
||
+++ b/pgo/pgo.c
|
||
@@ -71,6 +71,7 @@ void render_refresh_csd(struct terminal *term) {}
|
||
void render_refresh_title(struct terminal *term) {}
|
||
void render_refresh_app_id(struct terminal *term) {}
|
||
void render_refresh_icon(struct terminal *term) {}
|
||
+void render_refresh_tab_bar(struct terminal *term) {}
|
||
|
||
void render_overlay(struct terminal *term) {}
|
||
|
||
diff --git a/render.c b/render.c
|
||
index f74e9251..d7887d45 100644
|
||
--- a/render.c
|
||
+++ b/render.c
|
||
@@ -60,6 +60,7 @@ static struct {
|
||
} presentation_statistics = {0};
|
||
|
||
static void fdm_hook_refresh_pending_terminals(struct fdm *fdm, void *data);
|
||
+static void render_tab_bar(struct terminal *term);
|
||
|
||
struct renderer *
|
||
render_init(struct fdm *fdm, struct wayland *wayl)
|
||
@@ -4282,14 +4283,87 @@ render_urls(struct terminal *term)
|
||
}
|
||
}
|
||
|
||
+/* Build "tab1 | tab2 | ... | tabN" composite. Result is xmalloc'd; caller frees.
|
||
+ * If the codepoint count exceeds max_chars, the tail is replaced with "...". */
|
||
+static char *
|
||
+build_composite_title(const struct wl_window *win, size_t max_chars)
|
||
+{
|
||
+ /* Sum the bytes needed for the full untruncated composite */
|
||
+ size_t cap = 1;
|
||
+ for (size_t i = 0; i < win->tab_count; i++) {
|
||
+ const struct terminal *tab = win->tabs[i];
|
||
+ const char *t = tab->window_title != NULL ? tab->window_title : "foot";
|
||
+ cap += strlen(t) + (i > 0 ? 3 : 0);
|
||
+ }
|
||
+
|
||
+ char *full = xmalloc(cap);
|
||
+ size_t pos = 0;
|
||
+ for (size_t i = 0; i < win->tab_count; i++) {
|
||
+ const struct terminal *tab = win->tabs[i];
|
||
+ const char *t = tab->window_title != NULL ? tab->window_title : "foot";
|
||
+ if (i > 0) {
|
||
+ memcpy(full + pos, " | ", 3);
|
||
+ pos += 3;
|
||
+ }
|
||
+ size_t tlen = strlen(t);
|
||
+ memcpy(full + pos, t, tlen);
|
||
+ pos += tlen;
|
||
+ }
|
||
+ full[pos] = '\0';
|
||
+
|
||
+ /* Count codepoints */
|
||
+ size_t total = 0;
|
||
+ const unsigned char *p = (const unsigned char *)full;
|
||
+ while (*p != '\0') {
|
||
+ utf8proc_int32_t cp;
|
||
+ utf8proc_ssize_t consumed = utf8proc_iterate(p, -1, &cp);
|
||
+ if (consumed <= 0) break;
|
||
+ p += consumed;
|
||
+ total++;
|
||
+ }
|
||
+
|
||
+ if (total <= max_chars)
|
||
+ return full;
|
||
+
|
||
+ /* Walk the prefix to find the byte cut for (max_chars - 3) codepoints */
|
||
+ const size_t target = max_chars > 3 ? max_chars - 3 : 0;
|
||
+ p = (const unsigned char *)full;
|
||
+ size_t k = 0;
|
||
+ size_t cut = 0;
|
||
+ while (*p != '\0' && k < target) {
|
||
+ utf8proc_int32_t cp;
|
||
+ utf8proc_ssize_t consumed = utf8proc_iterate(p, -1, &cp);
|
||
+ if (consumed <= 0) break;
|
||
+ p += consumed;
|
||
+ cut = (size_t)((const char *)p - full);
|
||
+ k++;
|
||
+ }
|
||
+
|
||
+ char *result = xmalloc(cut + 4);
|
||
+ memcpy(result, full, cut);
|
||
+ memcpy(result + cut, "...", 3);
|
||
+ result[cut + 3] = '\0';
|
||
+ free(full);
|
||
+ return result;
|
||
+}
|
||
+
|
||
static void
|
||
render_update_title(struct terminal *term)
|
||
{
|
||
static const size_t max_len = 2048;
|
||
|
||
- const char *title = term->window_title != NULL ? term->window_title : "foot";
|
||
- char *copy = NULL;
|
||
+ struct wl_window *win = term->window;
|
||
+ char *composite = NULL;
|
||
+ const char *title;
|
||
+
|
||
+ if (win != NULL && win->tab_count > 1) {
|
||
+ composite = build_composite_title(win, term->conf->tabs.title_max_length);
|
||
+ title = composite;
|
||
+ } else {
|
||
+ title = term->window_title != NULL ? term->window_title : "foot";
|
||
+ }
|
||
|
||
+ char *copy = NULL;
|
||
if (strlen(title) > max_len) {
|
||
copy = xstrndup(title, max_len);
|
||
title = copy;
|
||
@@ -4297,6 +4371,7 @@ render_update_title(struct terminal *term)
|
||
|
||
xdg_toplevel_set_title(term->window->xdg_toplevel, title);
|
||
free(copy);
|
||
+ free(composite);
|
||
}
|
||
|
||
static void
|
||
@@ -4614,10 +4689,28 @@ render_resize(struct terminal *term, int width, int height, uint8_t opts)
|
||
/* Padding */
|
||
const int max_pad_x = (width - min_width) / 2;
|
||
const int max_pad_y = (height - min_height) / 2;
|
||
+ /* Total bar height = pill height + margin (margin only used in floating layout).
|
||
+ * Only reserve space for the bar when more than one tab is open. */
|
||
+ const bool tab_bar_visible =
|
||
+ term->conf->tabs.enabled && term->window->tab_count > 1;
|
||
+ const uint16_t tabs_bar_logical =
|
||
+ tab_bar_visible
|
||
+ ? (term->conf->tabs.height +
|
||
+ (term->conf->tabs.layout == CONF_TABS_LAYOUT_FLOATING
|
||
+ ? term->conf->tabs.margin : 0))
|
||
+ : 0;
|
||
+ const int conf_pad_top = term->conf->pad_top +
|
||
+ (tab_bar_visible &&
|
||
+ term->conf->tabs.position == CONF_TABS_POSITION_TOP
|
||
+ ? tabs_bar_logical : 0);
|
||
+ const int conf_pad_bottom = term->conf->pad_bottom +
|
||
+ (tab_bar_visible &&
|
||
+ term->conf->tabs.position == CONF_TABS_POSITION_BOTTOM
|
||
+ ? tabs_bar_logical : 0);
|
||
const int pad_left = min(max_pad_x, scale * term->conf->pad_left);
|
||
const int pad_right = min(max_pad_x, scale * term->conf->pad_right);
|
||
- const int pad_top = min(max_pad_y, scale * term->conf->pad_top);
|
||
- const int pad_bottom= min(max_pad_y, scale * term->conf->pad_bottom);
|
||
+ const int pad_top = min(max_pad_y, scale * conf_pad_top);
|
||
+ const int pad_bottom= min(max_pad_y, scale * conf_pad_bottom);
|
||
|
||
if (is_floating &&
|
||
(opts & RESIZE_BY_CELLS) &&
|
||
@@ -4709,8 +4802,8 @@ render_resize(struct terminal *term, int width, int height, uint8_t opts)
|
||
(center == CENTER_FULLSCREEN && term->window->is_fullscreen);
|
||
|
||
if (centered_padding && !term->window->is_resizing) {
|
||
- term->margins.left = total_x_pad / 2;
|
||
- term->margins.top = total_y_pad / 2;
|
||
+ term->margins.left = max(pad_left, min(total_x_pad - pad_right, total_x_pad / 2));
|
||
+ term->margins.top = max(pad_top, min(total_y_pad - pad_bottom, total_y_pad / 2));
|
||
} else {
|
||
term->margins.left = pad_left;
|
||
term->margins.top = pad_top;
|
||
@@ -5133,6 +5226,10 @@ fdm_hook_refresh_pending_terminals(struct fdm *fdm, void *data)
|
||
if (unlikely(term->shutdown.in_progress || !term->window->is_configured))
|
||
continue;
|
||
|
||
+ /* Skip inactive tabs - they process PTY data but don't render */
|
||
+ if (term->window->term != term)
|
||
+ continue;
|
||
+
|
||
bool grid = term->render.refresh.grid;
|
||
bool csd = term->render.refresh.csd;
|
||
bool search = term->is_searching && term->render.refresh.search;
|
||
@@ -5165,6 +5262,8 @@ fdm_hook_refresh_pending_terminals(struct fdm *fdm, void *data)
|
||
render_search_box(term);
|
||
if (urls)
|
||
render_urls(term);
|
||
+ if (term->conf->tabs.enabled)
|
||
+ render_tab_bar(term);
|
||
if (grid | csd | search | urls)
|
||
grid_render(term);
|
||
|
||
@@ -5180,6 +5279,15 @@ fdm_hook_refresh_pending_terminals(struct fdm *fdm, void *data)
|
||
term->render.pending.csd |= csd;
|
||
term->render.pending.search |= search;
|
||
term->render.pending.urls |= urls;
|
||
+
|
||
+ /* The tab bar is a desync subsurface — its commits apply
|
||
+ * independently of the parent surface, so we can repaint it
|
||
+ * now instead of deferring to the frame callback (which never
|
||
+ * calls render_tab_bar anyway). Without this, title changes
|
||
+ * while a frame is in flight leave the tab label stale until
|
||
+ * something else dirties the grid. */
|
||
+ if (grid && term->conf->tabs.enabled)
|
||
+ render_tab_bar(term);
|
||
}
|
||
}
|
||
|
||
@@ -5310,6 +5418,509 @@ render_refresh_urls(struct terminal *term)
|
||
term->render.refresh.urls = true;
|
||
}
|
||
|
||
+void
|
||
+render_refresh_tab_bar(struct terminal *term)
|
||
+{
|
||
+ if (term->conf->tabs.enabled)
|
||
+ term->render.refresh.grid = true; /* triggers full re-render which includes tab bar */
|
||
+}
|
||
+
|
||
+static void
|
||
+draw_rounded_corner_aa(pixman_image_t *pix, const pixman_color_t *color,
|
||
+ int dx, int dy, int r,
|
||
+ int cx_inside, int cy_inside)
|
||
+{
|
||
+ if (r <= 0)
|
||
+ return;
|
||
+
|
||
+ const int stride = (r + 3) & ~3; /* A8 stride aligned to 4 bytes */
|
||
+ uint8_t *data = xcalloc(stride * r, sizeof(uint8_t));
|
||
+
|
||
+ /* Circle center in local (0..r, 0..r) coords of the corner tile. */
|
||
+ const float cx = cx_inside > 0 ? (float)r : 0.f;
|
||
+ const float cy = cy_inside > 0 ? (float)r : 0.f;
|
||
+ const float r_f = (float)r;
|
||
+
|
||
+ for (int py = 0; py < r; py++) {
|
||
+ for (int px = 0; px < r; px++) {
|
||
+ const float ex = (float)px + 0.5f - cx;
|
||
+ const float ey = (float)py + 0.5f - cy;
|
||
+ const float dist = sqrtf(ex * ex + ey * ey);
|
||
+ float cov = r_f - dist + 0.5f;
|
||
+ if (cov <= 0.f)
|
||
+ cov = 0.f;
|
||
+ else if (cov >= 1.f)
|
||
+ cov = 1.f;
|
||
+ data[py * stride + px] = (uint8_t)(cov * 255.f + 0.5f);
|
||
+ }
|
||
+ }
|
||
+
|
||
+ pixman_image_t *mask = pixman_image_create_bits(
|
||
+ PIXMAN_a8, r, r, (uint32_t *)data, stride);
|
||
+ pixman_image_t *src = pixman_image_create_solid_fill(color);
|
||
+ pixman_image_composite32(PIXMAN_OP_OVER,
|
||
+ src, mask, pix, 0, 0, 0, 0, dx, dy, r, r);
|
||
+ pixman_image_unref(mask);
|
||
+ pixman_image_unref(src);
|
||
+ free(data);
|
||
+}
|
||
+
|
||
+static void
|
||
+draw_rounded_rect(pixman_image_t *pix, const pixman_color_t *color,
|
||
+ int x, int y, int w, int h, int r, unsigned corners)
|
||
+{
|
||
+ if (r <= 0 || w <= 0 || h <= 0) {
|
||
+ pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1,
|
||
+ &(pixman_rectangle16_t){x, y, w, h});
|
||
+ return;
|
||
+ }
|
||
+ r = min(r, min(w / 2, h / 2));
|
||
+
|
||
+ const bool tl = corners & 1;
|
||
+ const bool tr = corners & 2;
|
||
+ const bool bl = corners & 4;
|
||
+ const bool br = corners & 8;
|
||
+
|
||
+ /* Fill body regions. Corners handled separately if rounded. */
|
||
+ /* Top band */
|
||
+ pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1,
|
||
+ &(pixman_rectangle16_t){x + (tl ? r : 0), y,
|
||
+ w - (tl ? r : 0) - (tr ? r : 0), r});
|
||
+ /* Middle band (full width) */
|
||
+ pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1,
|
||
+ &(pixman_rectangle16_t){x, y + r, w, h - 2 * r});
|
||
+ /* Bottom band */
|
||
+ pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1,
|
||
+ &(pixman_rectangle16_t){x + (bl ? r : 0), y + h - r,
|
||
+ w - (bl ? r : 0) - (br ? r : 0), r});
|
||
+
|
||
+ /* Square corners: fill the r×r square. */
|
||
+ if (!tl)
|
||
+ pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1,
|
||
+ &(pixman_rectangle16_t){x, y, r, r});
|
||
+ if (!tr)
|
||
+ pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1,
|
||
+ &(pixman_rectangle16_t){x + w - r, y, r, r});
|
||
+ if (!bl)
|
||
+ pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1,
|
||
+ &(pixman_rectangle16_t){x, y + h - r, r, r});
|
||
+ if (!br)
|
||
+ pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1,
|
||
+ &(pixman_rectangle16_t){x + w - r, y + h - r, r, r});
|
||
+
|
||
+ /* Rounded corners: anti-aliased quarter-circles. */
|
||
+ if (tl) draw_rounded_corner_aa(pix, color, x, y, r, 1, 1);
|
||
+ if (tr) draw_rounded_corner_aa(pix, color, x + w - r, y, r, -1, 1);
|
||
+ if (bl) draw_rounded_corner_aa(pix, color, x, y + h - r, r, 1, -1);
|
||
+ if (br) draw_rounded_corner_aa(pix, color, x + w - r, y + h - r, r, -1, -1);
|
||
+}
|
||
+
|
||
+static int
|
||
+tab_label_build(char *buf, size_t bufsz,
|
||
+ const struct terminal *tab, size_t idx)
|
||
+{
|
||
+ const char *title = NULL;
|
||
+ if (tab->window_title_has_been_set && tab->window_title != NULL)
|
||
+ title = tab->window_title;
|
||
+ else if (tab->cwd != NULL) {
|
||
+ const char *slash = strrchr(tab->cwd, '/');
|
||
+ title = (slash != NULL && slash[1] != '\0') ? slash + 1 : tab->cwd;
|
||
+ }
|
||
+ int len = snprintf(buf, bufsz, "%zu: %s", idx + 1,
|
||
+ title != NULL ? title : "");
|
||
+ if (len < 0)
|
||
+ return 0;
|
||
+ if ((size_t)len >= bufsz)
|
||
+ len = (int)bufsz - 1;
|
||
+ return len;
|
||
+}
|
||
+
|
||
+static int
|
||
+tab_label_width(struct fcft_font *font, const char *buf, int len)
|
||
+{
|
||
+ if (font == NULL || len <= 0)
|
||
+ return 0;
|
||
+
|
||
+ int total = 0;
|
||
+ const unsigned char *p = (const unsigned char *)buf;
|
||
+ const unsigned char *end = p + len;
|
||
+ while (p < end) {
|
||
+ utf8proc_int32_t cp;
|
||
+ utf8proc_ssize_t consumed = utf8proc_iterate(p, end - p, &cp);
|
||
+ if (consumed <= 0 || cp < 0) {
|
||
+ p++;
|
||
+ continue;
|
||
+ }
|
||
+ p += consumed;
|
||
+
|
||
+ const struct fcft_glyph *g = fcft_rasterize_char_utf32(
|
||
+ font, (uint32_t)cp, FCFT_SUBPIXEL_NONE);
|
||
+ if (g != NULL)
|
||
+ total += g->advance.x;
|
||
+ }
|
||
+ return total;
|
||
+}
|
||
+
|
||
+static void
|
||
+render_tab_label(pixman_image_t *pix, struct fcft_font *font,
|
||
+ const pixman_color_t *fg, const struct terminal *tab,
|
||
+ size_t idx, int x, int y, int max_x, float scale)
|
||
+{
|
||
+ char label_buf[256];
|
||
+ int label_len = tab_label_build(label_buf, sizeof(label_buf), tab, idx);
|
||
+ if (label_len <= 0 || font == NULL)
|
||
+ return;
|
||
+
|
||
+ const int pad = (int)roundf(tab->conf->tabs.label_padding * scale);
|
||
+ int gx = x + pad;
|
||
+ const int clip_x = max_x - pad;
|
||
+
|
||
+ const unsigned char *p = (const unsigned char *)label_buf;
|
||
+ const unsigned char *end = p + label_len;
|
||
+ while (p < end) {
|
||
+ utf8proc_int32_t cp;
|
||
+ utf8proc_ssize_t consumed = utf8proc_iterate(p, end - p, &cp);
|
||
+ if (consumed <= 0 || cp < 0) {
|
||
+ p++;
|
||
+ continue;
|
||
+ }
|
||
+ p += consumed;
|
||
+
|
||
+ const struct fcft_glyph *glyph = fcft_rasterize_char_utf32(
|
||
+ font, (uint32_t)cp, FCFT_SUBPIXEL_NONE);
|
||
+ if (glyph == NULL)
|
||
+ continue;
|
||
+
|
||
+ if (gx + glyph->advance.x > clip_x)
|
||
+ break;
|
||
+
|
||
+ if (pixman_image_get_format(glyph->pix) == PIXMAN_a8r8g8b8) {
|
||
+ pixman_image_composite32(PIXMAN_OP_OVER,
|
||
+ glyph->pix, NULL, pix, 0, 0, 0, 0,
|
||
+ gx + glyph->x, y - glyph->y,
|
||
+ glyph->width, glyph->height);
|
||
+ } else {
|
||
+ pixman_image_t *src = pixman_image_create_solid_fill(fg);
|
||
+ pixman_image_composite32(PIXMAN_OP_OVER,
|
||
+ src, glyph->pix, pix, 0, 0, 0, 0,
|
||
+ gx + glyph->x, y - glyph->y,
|
||
+ glyph->width, glyph->height);
|
||
+ pixman_image_unref(src);
|
||
+ }
|
||
+ gx += glyph->advance.x;
|
||
+ }
|
||
+}
|
||
+
|
||
+/* Greyscale ramp indices for the gradient tab style.
|
||
+ * The 256-color palette greyscale ramp lives at indices 232..255. */
|
||
+#define GRADIENT_BAR_BG_IDX 234
|
||
+#define GRADIENT_PILL_ACTIVE_IDX 250
|
||
+#define GRADIENT_PILL_INACTIVE_IDX 240
|
||
+#define GRADIENT_FG_ACTIVE_IDX 232
|
||
+#define GRADIENT_FG_INACTIVE_IDX 250
|
||
+
|
||
+/* Hardcoded fade level masks (braille bitmasks); index = fade level (1..6).
|
||
+ * Mirrors the visual progression: ⠐ ⠡ ⡐ ⢔ ⣑ ⣪ */
|
||
+static const uint8_t gradient_fade_masks[7] = {
|
||
+ 0x00, 0x10, 0x21, 0x50, 0x94, 0xD1, 0xEA,
|
||
+};
|
||
+
|
||
+/* Braille bit (0..7) → (col, row) within the 2-col × 4-row dot grid */
|
||
+static const struct { uint8_t col, row; } gradient_dot_pos[8] = {
|
||
+ {0, 0}, {0, 1}, {0, 2}, {1, 0}, {1, 1}, {1, 2}, {0, 3}, {1, 3},
|
||
+};
|
||
+
|
||
+static void
|
||
+draw_gradient_pill(pixman_image_t *pix, const struct terminal *term,
|
||
+ uint8_t pill_idx, uint8_t bar_bg_idx, bool gamma_correct,
|
||
+ int x, int y, int w, int h, float scale)
|
||
+{
|
||
+ const int n_levels = (int)ALEN(gradient_fade_masks) - 1; /* 6 */
|
||
+ const int cell_w = max(2, (int)roundf(scale * 4));
|
||
+ const int fade_w = n_levels * cell_w;
|
||
+
|
||
+ const pixman_color_t pill = color_hex_to_pixman(
|
||
+ term->colors.table[pill_idx], gamma_correct);
|
||
+
|
||
+ if (w <= 2 * fade_w) {
|
||
+ /* Tab too narrow for full fades — draw it solid */
|
||
+ pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, &pill,
|
||
+ 1, &(pixman_rectangle16_t){x, y, w, h});
|
||
+ return;
|
||
+ }
|
||
+
|
||
+ /* Solid pill interior */
|
||
+ pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, &pill,
|
||
+ 1, &(pixman_rectangle16_t){x + fade_w, y, w - 2 * fade_w, h});
|
||
+
|
||
+ const int dot_size = max(1, (int)roundf(scale));
|
||
+ const float sub_w = (float)cell_w / 2.0f;
|
||
+ const float sub_h = (float)h / 4.0f;
|
||
+
|
||
+ const int inner = (int)pill_idx - 1;
|
||
+ const int outer = (int)bar_bg_idx;
|
||
+ const int range = inner - outer;
|
||
+ const int t_den = max(1, n_levels - 1);
|
||
+
|
||
+ for (int side = 0; side < 2; side++) {
|
||
+ const bool left = (side == 0);
|
||
+ for (int i = 1; i <= n_levels; i++) {
|
||
+ /* i == 1 is the outermost cell, i == n_levels the innermost */
|
||
+ const int cell_x = left
|
||
+ ? x + (i - 1) * cell_w
|
||
+ : x + w - i * cell_w;
|
||
+
|
||
+ const uint8_t band_idx = (uint8_t)(
|
||
+ outer + (range * (i - 1) + t_den / 2) / t_den);
|
||
+ const uint8_t dot_idx = band_idx > 233 ? band_idx - 2 : 232;
|
||
+
|
||
+ const pixman_color_t band = color_hex_to_pixman(
|
||
+ term->colors.table[band_idx], gamma_correct);
|
||
+ pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, &band,
|
||
+ 1, &(pixman_rectangle16_t){cell_x, y, cell_w, h});
|
||
+
|
||
+ const uint8_t mask = gradient_fade_masks[i];
|
||
+ const pixman_color_t dot = color_hex_to_pixman(
|
||
+ term->colors.table[dot_idx], gamma_correct);
|
||
+
|
||
+ pixman_rectangle16_t dot_rects[8];
|
||
+ int n_dots = 0;
|
||
+ for (int b = 0; b < 8; b++) {
|
||
+ if (!(mask & (1u << b)))
|
||
+ continue;
|
||
+ const int dx = (int)roundf((gradient_dot_pos[b].col + 0.5f) * sub_w)
|
||
+ - dot_size / 2;
|
||
+ const int dy = (int)roundf((gradient_dot_pos[b].row + 0.5f) * sub_h)
|
||
+ - dot_size / 2;
|
||
+ dot_rects[n_dots++] = (pixman_rectangle16_t){
|
||
+ cell_x + dx, y + dy, dot_size, dot_size,
|
||
+ };
|
||
+ }
|
||
+ if (n_dots > 0)
|
||
+ pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, &dot,
|
||
+ n_dots, dot_rects);
|
||
+ }
|
||
+ }
|
||
+}
|
||
+
|
||
+static void
|
||
+render_tab_bar(struct terminal *term)
|
||
+{
|
||
+ struct wl_window *win = term->window;
|
||
+ if (win->tab_bar.sub == NULL)
|
||
+ return;
|
||
+
|
||
+ const struct config *conf = term->conf;
|
||
+ const float scale = term->scale;
|
||
+ const bool floating = (conf->tabs.layout == CONF_TABS_LAYOUT_FLOATING);
|
||
+ const bool pos_top = (conf->tabs.position == CONF_TABS_POSITION_TOP);
|
||
+
|
||
+ /* pill height is always conf->tabs.height; total bar = pill + margin in floating */
|
||
+ const int margin_px = floating ? (int)roundf(scale * conf->tabs.margin) : 0;
|
||
+ const int tab_h_px = (int)roundf(scale * conf->tabs.height);
|
||
+ const int total_h = tab_h_px + margin_px;
|
||
+ const int width = term->width;
|
||
+
|
||
+ if (width <= 0 || total_h <= 0)
|
||
+ return;
|
||
+
|
||
+ /* Don't draw the tab bar unless more than one tab is open */
|
||
+ if (win->tab_count <= 1) {
|
||
+ win->tab_layout.count = 0;
|
||
+ wl_surface_attach(win->tab_bar.surface.surf, NULL, 0, 0);
|
||
+ wl_surface_commit(win->tab_bar.surface.surf);
|
||
+ return;
|
||
+ }
|
||
+
|
||
+ struct buffer_chain *chain = term->render.chains.tab_bar;
|
||
+ struct buffer *buf = shm_get_buffer(chain, width, total_h);
|
||
+ if (buf == NULL)
|
||
+ return;
|
||
+
|
||
+ const bool gamma_correct = wayl_do_linear_blending(win->term->wl, conf);
|
||
+ const bool rounded = (conf->tabs.style == CONF_TABS_STYLE_ROUNDED);
|
||
+ const bool gradient = (conf->tabs.style == CONF_TABS_STYLE_GRADIENT);
|
||
+ const int r = rounded ? (int)roundf(scale * conf->tabs.corner_radius) : 0;
|
||
+
|
||
+ /* Clear buffer: transparent for floating (shows terminal behind gaps), bg for span.
|
||
+ * Gradient style overrides the configured bar bg with a fixed greyscale ramp index. */
|
||
+ const pixman_color_t transparent = {0, 0, 0, 0};
|
||
+ const pixman_color_t bg_color = gradient
|
||
+ ? color_hex_to_pixman(term->colors.table[GRADIENT_BAR_BG_IDX], gamma_correct)
|
||
+ : color_hex_to_pixman(conf->tabs.colors.bg, gamma_correct);
|
||
+ pixman_image_fill_rectangles(PIXMAN_OP_SRC, buf->pix[0],
|
||
+ floating ? &transparent : &bg_color,
|
||
+ 1, &(pixman_rectangle16_t){0, 0, width, total_h});
|
||
+
|
||
+ const size_t tab_count = win->tab_count;
|
||
+ if (tab_count == 0)
|
||
+ goto commit;
|
||
+
|
||
+ struct fcft_font *font = term->fonts[0];
|
||
+
|
||
+ /* Per-tab layout arrays */
|
||
+ int tab_xs[TAB_MAX];
|
||
+ int tab_ws[TAB_MAX];
|
||
+ int tab_y, tab_h;
|
||
+
|
||
+ const size_t n = min(tab_count, (size_t)TAB_MAX);
|
||
+
|
||
+ if (floating) {
|
||
+ const int pad_px = (int)roundf(scale * conf->tabs.tab_padding);
|
||
+ const int max_tw = (int)roundf(scale * conf->tabs.tab_width);
|
||
+ const int label_pad = (int)roundf(scale * conf->tabs.label_padding);
|
||
+ const int min_tw = max(2 * label_pad, 1);
|
||
+
|
||
+ /* Measure each tab's natural width (label + padding), capped at max_tw */
|
||
+ int natural_w[TAB_MAX];
|
||
+ int total_w = 0;
|
||
+ for (size_t i = 0; i < n; i++) {
|
||
+ char buf_s[256];
|
||
+ int len = tab_label_build(buf_s, sizeof(buf_s), win->tabs[i], i);
|
||
+ int lw = tab_label_width(font, buf_s, len);
|
||
+ int w = lw + 2 * label_pad;
|
||
+ if (w > max_tw) w = max_tw;
|
||
+ if (w < min_tw) w = min_tw;
|
||
+ natural_w[i] = w;
|
||
+ total_w += w;
|
||
+ }
|
||
+ const int total_pads = pad_px * ((int)n - 1);
|
||
+ int bar_w = total_w + total_pads;
|
||
+
|
||
+ /* Shrink uniformly if we overflow the bar width */
|
||
+ if (bar_w > width && total_w > 0) {
|
||
+ const int avail = max(width - total_pads, (int)n);
|
||
+ int used = 0;
|
||
+ for (size_t i = 0; i < n; i++) {
|
||
+ int w = (int)((int64_t)natural_w[i] * avail / total_w);
|
||
+ if (w < 1) w = 1;
|
||
+ tab_ws[i] = w;
|
||
+ used += w;
|
||
+ }
|
||
+ /* Distribute rounding residue to the first few tabs */
|
||
+ int residue = avail - used;
|
||
+ for (size_t i = 0; i < n && residue > 0; i++, residue--)
|
||
+ tab_ws[i]++;
|
||
+ bar_w = avail + total_pads;
|
||
+ } else {
|
||
+ for (size_t i = 0; i < n; i++)
|
||
+ tab_ws[i] = natural_w[i];
|
||
+ }
|
||
+
|
||
+ int x_cur = (width - bar_w) / 2;
|
||
+ for (size_t i = 0; i < n; i++) {
|
||
+ tab_xs[i] = x_cur;
|
||
+ x_cur += tab_ws[i] + pad_px;
|
||
+ }
|
||
+
|
||
+ tab_y = pos_top ? margin_px : 0;
|
||
+ tab_h = tab_h_px;
|
||
+ } else {
|
||
+ const int base = width / (int)n;
|
||
+ int x_cur = 0;
|
||
+ for (size_t i = 0; i < n; i++) {
|
||
+ tab_xs[i] = x_cur;
|
||
+ tab_ws[i] = (i + 1 == n) ? (width - x_cur) : base;
|
||
+ x_cur += base;
|
||
+ }
|
||
+ tab_y = 0;
|
||
+ tab_h = tab_h_px;
|
||
+ }
|
||
+
|
||
+ /* Text baseline centered within the pill */
|
||
+ const int text_baseline = tab_y + (font
|
||
+ ? (tab_h - (int)(font->ascent + font->descent)) / 2 + (int)font->ascent
|
||
+ : tab_h / 2);
|
||
+
|
||
+ for (size_t i = 0; i < n; i++) {
|
||
+ struct terminal *tab = win->tabs[i];
|
||
+ const bool is_active = (i == win->active_tab);
|
||
+ const int x = tab_xs[i];
|
||
+ const int w = tab_ws[i];
|
||
+
|
||
+ const pixman_color_t fg_color = gradient
|
||
+ ? color_hex_to_pixman(
|
||
+ term->colors.table[is_active
|
||
+ ? GRADIENT_FG_ACTIVE_IDX : GRADIENT_FG_INACTIVE_IDX],
|
||
+ gamma_correct)
|
||
+ : color_hex_to_pixman(
|
||
+ is_active ? conf->tabs.colors.active_fg : conf->tabs.colors.fg,
|
||
+ gamma_correct);
|
||
+
|
||
+ if (gradient) {
|
||
+ const uint8_t pill_idx = is_active
|
||
+ ? GRADIENT_PILL_ACTIVE_IDX : GRADIENT_PILL_INACTIVE_IDX;
|
||
+ draw_gradient_pill(buf->pix[0], term, pill_idx, GRADIENT_BAR_BG_IDX,
|
||
+ gamma_correct, x, tab_y, w, tab_h, scale);
|
||
+ } else if (rounded) {
|
||
+ /* Floating: all 4 corners rounded. Span: only the open edge rounded. */
|
||
+ const pixman_color_t tab_bg = color_hex_to_pixman(
|
||
+ is_active ? conf->tabs.colors.active_bg : conf->tabs.colors.bg,
|
||
+ gamma_correct);
|
||
+ unsigned corners;
|
||
+ if (floating) {
|
||
+ corners = 0xf; /* all corners */
|
||
+ } else if (pos_top) {
|
||
+ corners = 0x3; /* top-left + top-right */
|
||
+ } else {
|
||
+ corners = 0xc; /* bottom-left + bottom-right */
|
||
+ }
|
||
+ draw_rounded_rect(buf->pix[0], &tab_bg, x, tab_y, w, tab_h, r, corners);
|
||
+ } else {
|
||
+ const pixman_color_t tab_bg = color_hex_to_pixman(
|
||
+ is_active ? conf->tabs.colors.active_bg : conf->tabs.colors.bg,
|
||
+ gamma_correct);
|
||
+ pixman_image_fill_rectangles(PIXMAN_OP_SRC, buf->pix[0], &tab_bg,
|
||
+ 1, &(pixman_rectangle16_t){x, tab_y, w, tab_h});
|
||
+ }
|
||
+
|
||
+ /* Span: separator between inactive tabs (skipped for gradient — fades
|
||
+ * already provide visual separation) */
|
||
+ if (!floating && !is_active && i + 1 < n && !gradient) {
|
||
+ const pixman_color_t sep = color_hex_to_pixman(
|
||
+ conf->tabs.colors.bg, gamma_correct);
|
||
+ pixman_image_fill_rectangles(PIXMAN_OP_SRC, buf->pix[0], &sep,
|
||
+ 1, &(pixman_rectangle16_t){x + w - 1, tab_y + 2, 1, tab_h - 4});
|
||
+ }
|
||
+
|
||
+ if (font != NULL)
|
||
+ render_tab_label(buf->pix[0], font, &fg_color, tab, i,
|
||
+ x, text_baseline, x + w, scale);
|
||
+ }
|
||
+
|
||
+ /* Publish layout for hit-testing */
|
||
+ for (size_t i = 0; i < n; i++) {
|
||
+ win->tab_layout.xs[i] = tab_xs[i];
|
||
+ win->tab_layout.ws[i] = tab_ws[i];
|
||
+ }
|
||
+ win->tab_layout.y = tab_y;
|
||
+ win->tab_layout.h = tab_h;
|
||
+ win->tab_layout.count = n;
|
||
+
|
||
+commit: ;
|
||
+ if (tab_count == 0)
|
||
+ win->tab_layout.count = 0;
|
||
+ const int y_pos = pos_top ? 0 : term->height - total_h;
|
||
+ wl_subsurface_set_position(win->tab_bar.sub, 0, (int)roundf(y_pos / scale));
|
||
+
|
||
+ wayl_surface_scale(win, &win->tab_bar.surface, buf, scale);
|
||
+ wl_surface_attach(win->tab_bar.surface.surf, buf->wl_buf, 0, 0);
|
||
+ wl_surface_damage_buffer(win->tab_bar.surface.surf, 0, 0, width, total_h);
|
||
+
|
||
+ if (!floating) {
|
||
+ struct wl_region *region = wl_compositor_create_region(term->wl->compositor);
|
||
+ if (region != NULL) {
|
||
+ wl_region_add(region, 0, 0, width, total_h);
|
||
+ wl_surface_set_opaque_region(win->tab_bar.surface.surf, region);
|
||
+ wl_region_destroy(region);
|
||
+ }
|
||
+ } else {
|
||
+ wl_surface_set_opaque_region(win->tab_bar.surface.surf, NULL);
|
||
+ }
|
||
+
|
||
+ wl_surface_commit(win->tab_bar.surface.surf);
|
||
+}
|
||
+
|
||
bool
|
||
render_xcursor_set(struct seat *seat, struct terminal *term,
|
||
enum cursor_shape shape)
|
||
diff --git a/render.h b/render.h
|
||
index e6674ab2..0277d2be 100644
|
||
--- a/render.h
|
||
+++ b/render.h
|
||
@@ -27,6 +27,7 @@ void render_refresh_csd(struct terminal *term);
|
||
void render_refresh_search(struct terminal *term);
|
||
void render_refresh_title(struct terminal *term);
|
||
void render_refresh_urls(struct terminal *term);
|
||
+void render_refresh_tab_bar(struct terminal *term);
|
||
bool render_xcursor_set(
|
||
struct seat *seat, struct terminal *term, enum cursor_shape shape);
|
||
bool render_xcursor_is_valid(const struct seat *seat, const char *cursor);
|
||
diff --git a/terminal.c b/terminal.c
|
||
index 8eafbcbe..5446b1ea 100644
|
||
--- a/terminal.c
|
||
+++ b/terminal.c
|
||
@@ -1371,6 +1371,7 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper,
|
||
.url = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL),
|
||
.csd = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL),
|
||
.overlay = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL),
|
||
+ .tab_bar = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL),
|
||
},
|
||
.scrollback_lines = conf->scrollback.lines,
|
||
.app_sync_updates.timer_fd = app_sync_updates_fd,
|
||
@@ -1504,6 +1505,394 @@ close_fds:
|
||
return NULL;
|
||
}
|
||
|
||
+struct terminal *
|
||
+term_tab_new(struct terminal *primary,
|
||
+ int argc, char *const *argv, const char *const *envp,
|
||
+ void (*shutdown_cb)(void *data, int exit_code), void *shutdown_data)
|
||
+{
|
||
+ struct wl_window *win = primary->window;
|
||
+
|
||
+ if (win->tab_count >= TAB_MAX) {
|
||
+ LOG_ERR("maximum number of tabs (%d) reached", TAB_MAX);
|
||
+ return NULL;
|
||
+ }
|
||
+
|
||
+ const struct config *conf = primary->conf;
|
||
+ struct fdm *fdm = primary->fdm;
|
||
+ struct reaper *reaper = primary->reaper;
|
||
+ struct wayland *wayl = primary->wl;
|
||
+
|
||
+ int ptmx = -1;
|
||
+ int flash_fd = -1;
|
||
+ int delay_lower_fd = -1;
|
||
+ int delay_upper_fd = -1;
|
||
+ int app_sync_updates_fd = -1;
|
||
+ int title_update_fd = -1;
|
||
+ int icon_update_fd = -1;
|
||
+ int app_id_update_fd = -1;
|
||
+
|
||
+ struct terminal *term = malloc(sizeof(*term));
|
||
+ if (unlikely(term == NULL)) {
|
||
+ LOG_ERRNO("malloc() failed");
|
||
+ return NULL;
|
||
+ }
|
||
+
|
||
+ ptmx = posix_openpt(PTY_OPEN_FLAGS);
|
||
+ if (ptmx < 0) {
|
||
+ LOG_ERRNO("failed to open PTY for new tab");
|
||
+ goto close_fds;
|
||
+ }
|
||
+ if ((flash_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 ||
|
||
+ (delay_lower_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 ||
|
||
+ (delay_upper_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 ||
|
||
+ (app_sync_updates_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 ||
|
||
+ (title_update_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 ||
|
||
+ (icon_update_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 ||
|
||
+ (app_id_update_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0)
|
||
+ {
|
||
+ LOG_ERRNO("failed to create timer FDs for new tab");
|
||
+ goto close_fds;
|
||
+ }
|
||
+
|
||
+ if (ioctl(ptmx, (unsigned int)TIOCSWINSZ,
|
||
+ &(struct winsize){
|
||
+ .ws_row = (unsigned short)primary->rows,
|
||
+ .ws_col = (unsigned short)primary->cols}) < 0)
|
||
+ {
|
||
+ LOG_ERRNO("failed to set TIOCSWINSZ for new tab");
|
||
+ goto close_fds;
|
||
+ }
|
||
+
|
||
+ key_binding_new_for_conf(wayl->key_binding_manager, wayl, conf);
|
||
+
|
||
+ int ptmx_flags;
|
||
+ if ((ptmx_flags = fcntl(ptmx, F_GETFL)) < 0 ||
|
||
+ fcntl(ptmx, F_SETFL, ptmx_flags | O_NONBLOCK) < 0)
|
||
+ {
|
||
+ LOG_ERRNO("failed to configure ptmx as non-blocking");
|
||
+ goto err;
|
||
+ }
|
||
+
|
||
+ const enum shm_bit_depth desired_bit_depth =
|
||
+ conf->tweak.surface_bit_depth == SHM_BITS_AUTO
|
||
+ ? wayl_do_linear_blending(wayl, conf) ? SHM_BITS_16 : SHM_BITS_8
|
||
+ : conf->tweak.surface_bit_depth;
|
||
+
|
||
+ const struct color_theme *theme = NULL;
|
||
+ switch (conf->initial_color_theme) {
|
||
+ case COLOR_THEME_DARK: theme = &conf->colors_dark; break;
|
||
+ case COLOR_THEME_LIGHT: theme = &conf->colors_light; break;
|
||
+ case COLOR_THEME_1: BUG("COLOR_THEME_1 should not be used"); break;
|
||
+ case COLOR_THEME_2: BUG("COLOR_THEME_2 should not be used"); break;
|
||
+ }
|
||
+
|
||
+ *term = (struct terminal){
|
||
+ .fdm = fdm,
|
||
+ .reaper = reaper,
|
||
+ .conf = conf,
|
||
+ .is_tab = true,
|
||
+ .slave = -1,
|
||
+ .ptmx = ptmx,
|
||
+ .ptmx_buffers = tll_init(),
|
||
+ .ptmx_paste_buffers = tll_init(),
|
||
+ .font_sizes = {
|
||
+ xmalloc(sizeof(term->font_sizes[0][0]) * conf->fonts[0].count),
|
||
+ xmalloc(sizeof(term->font_sizes[1][0]) * conf->fonts[1].count),
|
||
+ xmalloc(sizeof(term->font_sizes[2][0]) * conf->fonts[2].count),
|
||
+ xmalloc(sizeof(term->font_sizes[3][0]) * conf->fonts[3].count),
|
||
+ },
|
||
+ .font_dpi = 0.,
|
||
+ .font_dpi_before_unmap = -1.,
|
||
+ .font_subpixel = (theme->alpha == 0xffff
|
||
+ ? FCFT_SUBPIXEL_DEFAULT
|
||
+ : FCFT_SUBPIXEL_NONE),
|
||
+ .cursor_keys_mode = CURSOR_KEYS_NORMAL,
|
||
+ .keypad_keys_mode = KEYPAD_NUMERICAL,
|
||
+ .reverse_wrap = true,
|
||
+ .auto_margin = true,
|
||
+ .window_title_stack = tll_init(),
|
||
+ .scale = primary->scale,
|
||
+ .scale_before_unmap = -1,
|
||
+ .flash = {.fd = flash_fd},
|
||
+ .blink = {.fd = -1},
|
||
+ .vt = {.state = 0},
|
||
+ .colors = {
|
||
+ .fg = theme->fg,
|
||
+ .bg = theme->bg,
|
||
+ .alpha = theme->alpha,
|
||
+ .cursor_fg = (theme->use_custom.cursor ? 1u << 31 : 0) | theme->cursor.text,
|
||
+ .cursor_bg = (theme->use_custom.cursor ? 1u << 31 : 0) | theme->cursor.cursor,
|
||
+ .selection_fg = theme->selection_fg,
|
||
+ .selection_bg = theme->selection_bg,
|
||
+ .active_theme = conf->initial_color_theme,
|
||
+ },
|
||
+ .color_stack = {.stack = NULL, .size = 0, .idx = 0},
|
||
+ .origin = ORIGIN_ABSOLUTE,
|
||
+ .cursor_style = conf->cursor.style,
|
||
+ .cursor_blink = {
|
||
+ .decset = false,
|
||
+ .deccsusr = conf->cursor.blink.enabled,
|
||
+ .state = CURSOR_BLINK_ON,
|
||
+ .fd = -1,
|
||
+ },
|
||
+ .selection = {
|
||
+ .coords = {.start = {-1, -1}, .end = {-1, -1}},
|
||
+ .pivot = {.start = {-1, -1}, .end = {-1, -1}},
|
||
+ .auto_scroll = {.fd = -1},
|
||
+ },
|
||
+ .normal = {.scroll_damage = tll_init(), .sixel_images = tll_init()},
|
||
+ .alt = {.scroll_damage = tll_init(), .sixel_images = tll_init()},
|
||
+ .grid = &term->normal,
|
||
+ .composed = NULL,
|
||
+ .alt_scrolling = conf->mouse.alternate_scroll_mode,
|
||
+ .meta = {.esc_prefix = true, .eight_bit = true},
|
||
+ .num_lock_modifier = true,
|
||
+ .bell_action_enabled = true,
|
||
+ .tab_stops = tll_init(),
|
||
+ .wl = wayl,
|
||
+ .window = win,
|
||
+ .render = {
|
||
+ .chains = {
|
||
+ .grid = shm_chain_new(wayl, true, 1 + conf->render_worker_count,
|
||
+ desired_bit_depth, &render_buffer_release_callback, term),
|
||
+ .search = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL),
|
||
+ .scrollback_indicator = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL),
|
||
+ .render_timer = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL),
|
||
+ .url = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL),
|
||
+ .csd = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL),
|
||
+ .overlay = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL),
|
||
+ .tab_bar = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL),
|
||
+ },
|
||
+ .scrollback_lines = conf->scrollback.lines,
|
||
+ .app_sync_updates.timer_fd = app_sync_updates_fd,
|
||
+ .title = {.timer_fd = title_update_fd},
|
||
+ .icon = {.timer_fd = icon_update_fd},
|
||
+ .app_id = {.timer_fd = app_id_update_fd},
|
||
+ .workers = {
|
||
+ .count = conf->render_worker_count,
|
||
+ .queue = tll_init(),
|
||
+ },
|
||
+ },
|
||
+ .delayed_render_timer = {
|
||
+ .is_armed = false,
|
||
+ .lower_fd = delay_lower_fd,
|
||
+ .upper_fd = delay_upper_fd,
|
||
+ },
|
||
+ .sixel = {
|
||
+ .scrolling = true,
|
||
+ .use_private_palette = true,
|
||
+ .palette_size = SIXEL_MAX_COLORS,
|
||
+ .max_width = SIXEL_MAX_WIDTH,
|
||
+ .max_height = SIXEL_MAX_HEIGHT,
|
||
+ },
|
||
+ .shutdown = {
|
||
+ .terminate_timeout_fd = -1,
|
||
+ .cb = shutdown_cb,
|
||
+ .cb_data = shutdown_data,
|
||
+ },
|
||
+ .foot_exe = xstrdup(primary->foot_exe),
|
||
+ .cwd = xstrdup(
|
||
+ conf->tabs.inherit_cwd
|
||
+ ? win->term->cwd
|
||
+ : (getenv("HOME") != NULL ? getenv("HOME") : "/")),
|
||
+ .grapheme_shaping = conf->tweak.grapheme_shaping,
|
||
+#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
|
||
+ .ime_enabled = true,
|
||
+#endif
|
||
+ .active_notifications = tll_init(),
|
||
+ };
|
||
+
|
||
+ pixman_region32_init(&term->render.last_overlay_clip);
|
||
+ term_update_ascii_printer(term);
|
||
+ memcpy(term->colors.table, theme->table, sizeof(term->colors.table));
|
||
+
|
||
+ /* Inherit font sizes from primary so the new tab matches its zoom level */
|
||
+ for (size_t i = 0; i < 4; i++) {
|
||
+ const struct config_font_list *font_list = &conf->fonts[i];
|
||
+ for (size_t j = 0; j < font_list->count; j++)
|
||
+ term->font_sizes[i][j] = primary->font_sizes[i][j];
|
||
+ }
|
||
+
|
||
+ for (size_t i = 0; i < ALEN(term->notification_icons); i++)
|
||
+ term->notification_icons[i].tmp_file_fd = -1;
|
||
+
|
||
+ if (!fdm_add(fdm, flash_fd, EPOLLIN, &fdm_flash, term) ||
|
||
+ !fdm_add(fdm, delay_lower_fd, EPOLLIN, &fdm_delayed_render, term) ||
|
||
+ !fdm_add(fdm, delay_upper_fd, EPOLLIN, &fdm_delayed_render, term) ||
|
||
+ !fdm_add(fdm, app_sync_updates_fd, EPOLLIN, &fdm_app_sync_updates_timeout, term) ||
|
||
+ !fdm_add(fdm, title_update_fd, EPOLLIN, &fdm_title_update_timeout, term) ||
|
||
+ !fdm_add(fdm, icon_update_fd, EPOLLIN, &fdm_icon_update_timeout, term) ||
|
||
+ !fdm_add(fdm, app_id_update_fd, EPOLLIN, &fdm_app_id_update_timeout, term))
|
||
+ {
|
||
+ goto err;
|
||
+ }
|
||
+
|
||
+ add_utmp_record(conf, reaper, ptmx);
|
||
+
|
||
+ if ((term->slave = slave_spawn(
|
||
+ term->ptmx, argc, term->cwd, argv, envp, &conf->env_vars,
|
||
+ conf->term, conf->shell, conf->login_shell,
|
||
+ &conf->notifications)) == -1)
|
||
+ {
|
||
+ goto err;
|
||
+ }
|
||
+
|
||
+ reaper_add(term->reaper, term->slave, &fdm_client_terminated, term);
|
||
+
|
||
+ /* Load fonts (uses primary's scale) */
|
||
+ if (!term_font_dpi_changed(term, 0.))
|
||
+ goto err;
|
||
+
|
||
+ term->font_subpixel = get_font_subpixel(term);
|
||
+
|
||
+ /* Resize grid to match current window dimensions. cell_width and
|
||
+ * cell_height were already computed by term_set_fonts above, using the
|
||
+ * inherited font_sizes — inheriting primary's cell geometry would mask
|
||
+ * that, since primary may be mid-zoom or mid-reload. */
|
||
+ term->width = primary->width;
|
||
+ term->height = primary->height;
|
||
+
|
||
+ tll_push_back(wayl->terms, term);
|
||
+
|
||
+ /* Register in window's tab list */
|
||
+ win->tabs[win->tab_count++] = term;
|
||
+
|
||
+ /* Enable ptmx I/O now (window is already configured) */
|
||
+ fdm_add(term->fdm, term->ptmx, EPOLLIN, &fdm_ptmx, term);
|
||
+
|
||
+ if (!initialize_render_workers(term))
|
||
+ goto err;
|
||
+
|
||
+ /* Switch to the new tab */
|
||
+ term_tab_switch(win, win->tab_count - 1);
|
||
+
|
||
+ return term;
|
||
+
|
||
+err:
|
||
+ term->shutdown.in_progress = true;
|
||
+ term_destroy(term);
|
||
+ return NULL;
|
||
+
|
||
+close_fds:
|
||
+ if (ptmx >= 0) close(ptmx);
|
||
+ fdm_del(fdm, flash_fd);
|
||
+ fdm_del(fdm, delay_lower_fd);
|
||
+ fdm_del(fdm, delay_upper_fd);
|
||
+ fdm_del(fdm, app_sync_updates_fd);
|
||
+ fdm_del(fdm, title_update_fd);
|
||
+ fdm_del(fdm, icon_update_fd);
|
||
+ fdm_del(fdm, app_id_update_fd);
|
||
+ free(term);
|
||
+ return NULL;
|
||
+}
|
||
+
|
||
+void
|
||
+term_tab_switch(struct wl_window *win, size_t idx)
|
||
+{
|
||
+ if (idx >= win->tab_count)
|
||
+ return;
|
||
+
|
||
+ struct terminal *prev = win->term;
|
||
+ /* render_resize expects logical (unscaled) sizes; prev->width/height
|
||
+ * are physical pixels. Convert back to logical so render_resize's
|
||
+ * internal scale multiplication doesn't double-apply it. */
|
||
+ int cur_width = (int)roundf(prev->width / prev->scale);
|
||
+ int cur_height = (int)roundf(prev->height / prev->scale);
|
||
+
|
||
+ win->active_tab = idx;
|
||
+ win->term = win->tabs[idx];
|
||
+
|
||
+ struct terminal *term = win->term;
|
||
+
|
||
+ /* Inherit the surface kind so cursor updates work without a pointer re-enter */
|
||
+ term->active_surface = prev->active_surface;
|
||
+
|
||
+ /* Update keyboard and mouse focus for all seats looking at this window.
|
||
+ * Do this before term_kbd_focus_out(prev) so its guard (checking whether
|
||
+ * any seat still points to it) sees that no seat does. */
|
||
+ tll_foreach(term->wl->seats, it) {
|
||
+ struct seat *seat = &it->item;
|
||
+ if (seat->kbd_focus != NULL && seat->kbd_focus->window == win)
|
||
+ seat->kbd_focus = term;
|
||
+ if (seat->mouse_focus != NULL && seat->mouse_focus->window == win)
|
||
+ seat->mouse_focus = term;
|
||
+ }
|
||
+
|
||
+ /* prev loses keyboard focus now that no seat points to it */
|
||
+ term_kbd_focus_out(prev);
|
||
+
|
||
+ bool has_kbd_focus = false;
|
||
+ tll_foreach(term->wl->seats, it) {
|
||
+ if (it->item.kbd_focus == term) {
|
||
+ has_kbd_focus = true;
|
||
+ break;
|
||
+ }
|
||
+ }
|
||
+ if (has_kbd_focus)
|
||
+ term_visual_focus_in(term);
|
||
+ else
|
||
+ term_visual_focus_out(term);
|
||
+
|
||
+ /* Scale changes (fractional-scale-v1, preferred_buffer_scale, output
|
||
+ * enter/leave) only notify win->term — inactive tabs miss them. Catch
|
||
+ * the incoming tab up on scale, DPI, and font size before we resize. */
|
||
+ const float old_scale = term->scale;
|
||
+ term_update_scale(term);
|
||
+ term_font_dpi_changed(term, old_scale);
|
||
+ term_font_subpixel_changed(term);
|
||
+
|
||
+ /* Cancel the outgoing tab's in-flight frame_callback. Its listener data
|
||
+ * points at prev, so when it fires it services prev's empty pending bits
|
||
+ * and nothing gets drawn. Dropping it lets fdm_hook_refresh_pending_terminals
|
||
+ * take the immediate-render path for the new active tab. */
|
||
+ if (win->frame_callback != NULL) {
|
||
+ wl_callback_destroy(win->frame_callback);
|
||
+ win->frame_callback = NULL;
|
||
+ }
|
||
+
|
||
+ /* Use current window dimensions, not the inactive tab's stale ones.
|
||
+ * render_resize allocates the grid rows — term_kbd_focus_in must come
|
||
+ * after this because cursor_refresh dereferences cur_row. */
|
||
+ render_resize(term, cur_width, cur_height, RESIZE_FORCE);
|
||
+
|
||
+ if (has_kbd_focus)
|
||
+ term_kbd_focus_in(term);
|
||
+
|
||
+ render_refresh_csd(term);
|
||
+ render_refresh_tab_bar(term);
|
||
+ render_refresh_title(term);
|
||
+ term_xcursor_update(term);
|
||
+}
|
||
+
|
||
+void
|
||
+term_tab_close(struct terminal *term)
|
||
+{
|
||
+ term_shutdown(term);
|
||
+}
|
||
+
|
||
+size_t
|
||
+term_tab_bar_hit_test(const struct terminal *term, int x, int y)
|
||
+{
|
||
+ const struct wl_window *win = term->window;
|
||
+ if (win->tab_bar.sub == NULL)
|
||
+ return SIZE_MAX;
|
||
+
|
||
+ const size_t n = win->tab_layout.count;
|
||
+ if (n == 0)
|
||
+ return SIZE_MAX;
|
||
+
|
||
+ if (y < win->tab_layout.y || y >= win->tab_layout.y + win->tab_layout.h)
|
||
+ return SIZE_MAX;
|
||
+
|
||
+ for (size_t i = 0; i < n; i++) {
|
||
+ const int tx = win->tab_layout.xs[i];
|
||
+ const int tw = win->tab_layout.ws[i];
|
||
+ if (x >= tx && x < tx + tw)
|
||
+ return i;
|
||
+ }
|
||
+ return SIZE_MAX;
|
||
+}
|
||
+
|
||
void
|
||
term_window_configured(struct terminal *term)
|
||
{
|
||
@@ -1585,10 +1974,10 @@ static void
|
||
shutdown_maybe_done(struct terminal *term)
|
||
{
|
||
bool shutdown_done =
|
||
- term->window == NULL && term->shutdown.client_has_terminated;
|
||
+ term->shutdown.fdm_done && term->shutdown.client_has_terminated;
|
||
|
||
- LOG_DBG("window=%p, slave-has-been-reaped=%d --> %s",
|
||
- (void *)term->window, term->shutdown.client_has_terminated,
|
||
+ LOG_DBG("fdm_done=%d, slave-has-been-reaped=%d --> %s",
|
||
+ term->shutdown.fdm_done, term->shutdown.client_has_terminated,
|
||
(shutdown_done
|
||
? "shutdown done, calling term_destroy()"
|
||
: "no action"));
|
||
@@ -1632,26 +2021,35 @@ fdm_shutdown(struct fdm *fdm, int fd, int events, void *data)
|
||
/* Kill the event FD */
|
||
fdm_del(term->fdm, fd);
|
||
|
||
- wayl_win_destroy(term->window);
|
||
- term->window = NULL;
|
||
+ struct wl_window *win = term->window;
|
||
|
||
- struct wayland *wayl = term->wl;
|
||
+ if (win != NULL && win->tab_count <= 1) {
|
||
+ /*
|
||
+ * Last (or only) tab — destroy the whole window.
|
||
+ *
|
||
+ * Normally we'd get unmapped when we destroy the Wayland
|
||
+ * surface above. However, it appears that under certain
|
||
+ * conditions, those events are deferred (for example, when
|
||
+ * a screen locker is active), and thus we can get here
|
||
+ * without having been unmapped.
|
||
+ */
|
||
+ wayl_win_destroy(win);
|
||
+ term->window = NULL;
|
||
|
||
+ struct wayland *wayl = term->wl;
|
||
+ tll_foreach(wayl->seats, it) {
|
||
+ if (it->item.kbd_focus == term)
|
||
+ it->item.kbd_focus = NULL;
|
||
+ if (it->item.mouse_focus == term)
|
||
+ it->item.mouse_focus = NULL;
|
||
+ }
|
||
+ }
|
||
/*
|
||
- * Normally we'd get unmapped when we destroy the Wayland
|
||
- * above.
|
||
- *
|
||
- * However, it appears that under certain conditions, those events
|
||
- * are deferred (for example, when a screen locker is active), and
|
||
- * thus we can get here without having been unmapped.
|
||
+ * Multi-tab case: leave term->window intact so term_destroy() can
|
||
+ * use its existing tab-removal and tab-switch logic.
|
||
*/
|
||
- tll_foreach(wayl->seats, it) {
|
||
- if (it->item.kbd_focus == term)
|
||
- it->item.kbd_focus = NULL;
|
||
- if (it->item.mouse_focus == term)
|
||
- it->item.mouse_focus = NULL;
|
||
- }
|
||
|
||
+ term->shutdown.fdm_done = true;
|
||
shutdown_maybe_done(term);
|
||
return true;
|
||
}
|
||
@@ -1840,7 +2238,99 @@ term_destroy(struct terminal *term)
|
||
fdm_del(term->fdm, term->shutdown.terminate_timeout_fd);
|
||
|
||
if (term->window != NULL) {
|
||
- wayl_win_destroy(term->window);
|
||
+ struct wl_window *win = term->window;
|
||
+
|
||
+ /* Remove ourselves from the window's tab list */
|
||
+ bool was_active = (win->term == term);
|
||
+ size_t our_idx = win->tab_count; /* invalid sentinel */
|
||
+ for (size_t i = 0; i < win->tab_count; i++) {
|
||
+ if (win->tabs[i] == term) {
|
||
+ our_idx = i;
|
||
+ memmove(&win->tabs[i], &win->tabs[i + 1],
|
||
+ (win->tab_count - i - 1) * sizeof(win->tabs[0]));
|
||
+ win->tab_count--;
|
||
+ if (win->active_tab > 0 && win->active_tab >= win->tab_count)
|
||
+ win->active_tab = win->tab_count - 1;
|
||
+ else if (our_idx < win->active_tab)
|
||
+ win->active_tab--;
|
||
+ break;
|
||
+ }
|
||
+ }
|
||
+
|
||
+ if (win->tab_count == 0) {
|
||
+ /* Last tab - destroy the window normally */
|
||
+ wayl_win_destroy(win);
|
||
+ } else {
|
||
+ /* Other tabs remain - just purge our render chains */
|
||
+
|
||
+ /* Cancel any pending frame callback that still holds a pointer
|
||
+ * to this terminal, preventing a use-after-free when the
|
||
+ * compositor fires it after term is freed. */
|
||
+ if (win->frame_callback != NULL) {
|
||
+ wl_callback_destroy(win->frame_callback);
|
||
+ win->frame_callback = NULL;
|
||
+ }
|
||
+
|
||
+ render_wait_for_preapply_damage(term);
|
||
+ shm_purge(term->render.chains.search);
|
||
+ shm_purge(term->render.chains.scrollback_indicator);
|
||
+ shm_purge(term->render.chains.render_timer);
|
||
+ shm_purge(term->render.chains.grid);
|
||
+ shm_purge(term->render.chains.url);
|
||
+ shm_purge(term->render.chains.csd);
|
||
+ shm_purge(term->render.chains.tab_bar);
|
||
+ shm_purge(term->render.chains.overlay);
|
||
+
|
||
+ /* Switch to the new active tab if needed */
|
||
+ if (was_active) {
|
||
+ struct terminal *next = win->tabs[win->active_tab];
|
||
+ win->term = next;
|
||
+ next->active_surface = term->active_surface;
|
||
+
|
||
+ /* Update keyboard and mouse focus */
|
||
+ bool has_kbd_focus = false;
|
||
+ tll_foreach(next->wl->seats, it) {
|
||
+ struct seat *seat = &it->item;
|
||
+ if (seat->kbd_focus == term)
|
||
+ seat->kbd_focus = next;
|
||
+ if (seat->mouse_focus == term)
|
||
+ seat->mouse_focus = next;
|
||
+ if (seat->kbd_focus == next)
|
||
+ has_kbd_focus = true;
|
||
+ }
|
||
+
|
||
+ if (has_kbd_focus) {
|
||
+ term_kbd_focus_in(next);
|
||
+ term_visual_focus_in(next);
|
||
+ } else {
|
||
+ term_visual_focus_out(next);
|
||
+ }
|
||
+
|
||
+ render_resize(next,
|
||
+ (int)roundf(term->width / term->scale),
|
||
+ (int)roundf(term->height / term->scale),
|
||
+ RESIZE_FORCE);
|
||
+ render_refresh_csd(next);
|
||
+ render_refresh_tab_bar(next);
|
||
+ render_refresh_title(next);
|
||
+ term_xcursor_update(next);
|
||
+ } else {
|
||
+ /* If we just dropped to a single tab, the surviving
|
||
+ * active tab needs to resize to reclaim the space the
|
||
+ * tab bar was occupying. */
|
||
+ if (win->tab_count == 1) {
|
||
+ struct terminal *active = win->term;
|
||
+ render_resize(active,
|
||
+ (int)roundf(active->width / active->scale),
|
||
+ (int)roundf(active->height / active->scale),
|
||
+ RESIZE_FORCE);
|
||
+ }
|
||
+ /* Just update the tab bar to reflect removed tab */
|
||
+ render_refresh_tab_bar(win->term);
|
||
+ render_refresh_title(win->term);
|
||
+ }
|
||
+ }
|
||
+
|
||
term->window = NULL;
|
||
}
|
||
|
||
@@ -1917,6 +2407,7 @@ term_destroy(struct terminal *term)
|
||
shm_chain_free(term->render.chains.url);
|
||
shm_chain_free(term->render.chains.csd);
|
||
shm_chain_free(term->render.chains.overlay);
|
||
+ shm_chain_free(term->render.chains.tab_bar);
|
||
pixman_region32_fini(&term->render.last_overlay_clip);
|
||
|
||
tll_free(term->tab_stops);
|
||
@@ -3637,6 +4128,12 @@ term_xcursor_update_for_seat(struct terminal *term, struct seat *seat)
|
||
shape = CURSOR_SHAPE_LEFT_PTR;
|
||
break;
|
||
|
||
+ case TERM_SURF_TAB_BAR: {
|
||
+ size_t idx = term_tab_bar_hit_test(term, seat->mouse.x, seat->mouse.y);
|
||
+ shape = (idx != SIZE_MAX) ? CURSOR_SHAPE_POINTER : CURSOR_SHAPE_LEFT_PTR;
|
||
+ break;
|
||
+ }
|
||
+
|
||
case TERM_SURF_BORDER_LEFT:
|
||
case TERM_SURF_BORDER_RIGHT:
|
||
case TERM_SURF_BORDER_TOP:
|
||
@@ -3680,6 +4177,7 @@ term_set_window_title(struct terminal *term, const char *title)
|
||
free(term->window_title);
|
||
term->window_title = xstrdup(title);
|
||
render_refresh_title(term);
|
||
+ render_refresh_tab_bar(term);
|
||
term->window_title_has_been_set = true;
|
||
}
|
||
|
||
@@ -4448,6 +4946,8 @@ term_surface_kind(const struct terminal *term, const struct wl_surface *surface)
|
||
return TERM_SURF_BUTTON_MAXIMIZE;
|
||
else if (surface == term->window->csd.surface[CSD_SURF_CLOSE].surface.surf)
|
||
return TERM_SURF_BUTTON_CLOSE;
|
||
+ else if (surface == term->window->tab_bar.surface.surf)
|
||
+ return TERM_SURF_TAB_BAR;
|
||
else
|
||
return TERM_SURF_NONE;
|
||
}
|
||
diff --git a/terminal.h b/terminal.h
|
||
index 446d5f23..876db0d7 100644
|
||
--- a/terminal.h
|
||
+++ b/terminal.h
|
||
@@ -361,6 +361,7 @@ enum term_surface {
|
||
TERM_SURF_BUTTON_MINIMIZE,
|
||
TERM_SURF_BUTTON_MAXIMIZE,
|
||
TERM_SURF_BUTTON_CLOSE,
|
||
+ TERM_SURF_TAB_BAR,
|
||
};
|
||
|
||
enum overlay_style {
|
||
@@ -402,6 +403,7 @@ struct terminal {
|
||
struct fdm *fdm;
|
||
struct reaper *reaper;
|
||
const struct config *conf;
|
||
+ bool is_tab; /* Secondary tab terminal (shares window with primary) */
|
||
|
||
void (*ascii_printer)(struct terminal *term, char32_t c);
|
||
union {
|
||
@@ -645,6 +647,7 @@ struct terminal {
|
||
struct buffer_chain *url;
|
||
struct buffer_chain *csd;
|
||
struct buffer_chain *overlay;
|
||
+ struct buffer_chain *tab_bar;
|
||
} chains;
|
||
|
||
/* Scheduled for rendering, as soon-as-possible */
|
||
@@ -811,6 +814,7 @@ struct terminal {
|
||
struct {
|
||
bool in_progress;
|
||
bool client_has_terminated;
|
||
+ bool fdm_done;
|
||
int terminate_timeout_fd;
|
||
int exit_status;
|
||
int next_signal;
|
||
@@ -842,6 +846,15 @@ struct terminal *term_init(
|
||
int argc, char *const *argv, const char *const *envp,
|
||
void (*shutdown_cb)(void *data, int exit_code), void *shutdown_data);
|
||
|
||
+struct terminal *term_tab_new(
|
||
+ struct terminal *primary,
|
||
+ int argc, char *const *argv, const char *const *envp,
|
||
+ void (*shutdown_cb)(void *data, int exit_code), void *shutdown_data);
|
||
+
|
||
+void term_tab_switch(struct wl_window *win, size_t idx);
|
||
+void term_tab_close(struct terminal *term);
|
||
+size_t term_tab_bar_hit_test(const struct terminal *term, int x, int y);
|
||
+
|
||
bool term_shutdown(struct terminal *term);
|
||
int term_destroy(struct terminal *term);
|
||
|
||
diff --git a/wayland.c b/wayland.c
|
||
index f5737c1e..ed61d2e7 100644
|
||
--- a/wayland.c
|
||
+++ b/wayland.c
|
||
@@ -2163,6 +2163,19 @@ wayl_win_init(struct terminal *term, const char *token)
|
||
goto out;
|
||
}
|
||
|
||
+ if (conf->tabs.enabled) {
|
||
+ if (!wayl_win_subsurface_new(win, &win->tab_bar, true)) {
|
||
+ LOG_ERR("failed to create tab bar surface");
|
||
+ goto out;
|
||
+ }
|
||
+ wl_subsurface_set_desync(win->tab_bar.sub);
|
||
+ }
|
||
+
|
||
+ /* Initialize tab list with primary terminal */
|
||
+ win->tabs[0] = term;
|
||
+ win->tab_count = 1;
|
||
+ win->active_tab = 0;
|
||
+
|
||
switch (conf->tweak.render_timer) {
|
||
case RENDER_TIMER_OSD:
|
||
case RENDER_TIMER_BOTH:
|
||
@@ -2221,6 +2234,12 @@ wayl_win_destroy(struct wl_window *win)
|
||
wl_surface_commit(win->search.surface.surf);
|
||
}
|
||
|
||
+ /* Tab bar */
|
||
+ if (win->tab_bar.surface.surf != NULL) {
|
||
+ wl_surface_attach(win->tab_bar.surface.surf, NULL, 0, 0);
|
||
+ wl_surface_commit(win->tab_bar.surface.surf);
|
||
+ }
|
||
+
|
||
/* URLs */
|
||
tll_foreach(win->urls, it) {
|
||
wl_surface_attach(it->item.surf.surface.surf, NULL, 0, 0);
|
||
@@ -2257,6 +2276,7 @@ wayl_win_destroy(struct wl_window *win)
|
||
wayl_win_subsurface_destroy(&win->scrollback_indicator);
|
||
wayl_win_subsurface_destroy(&win->render_timer);
|
||
wayl_win_subsurface_destroy(&win->overlay);
|
||
+ wayl_win_subsurface_destroy(&win->tab_bar);
|
||
|
||
shm_purge(term->render.chains.search);
|
||
shm_purge(term->render.chains.scrollback_indicator);
|
||
@@ -2264,6 +2284,7 @@ wayl_win_destroy(struct wl_window *win)
|
||
shm_purge(term->render.chains.grid);
|
||
shm_purge(term->render.chains.url);
|
||
shm_purge(term->render.chains.csd);
|
||
+ shm_purge(term->render.chains.tab_bar);
|
||
|
||
tll_foreach(win->xdg_tokens, it) {
|
||
xdg_activation_token_v1_destroy(it->item->xdg_token);
|
||
diff --git a/wayland.h b/wayland.h
|
||
index 9cbd1023..9e1a3a6d 100644
|
||
--- a/wayland.h
|
||
+++ b/wayland.h
|
||
@@ -401,6 +401,22 @@ struct wl_window {
|
||
struct wayl_sub_surface scrollback_indicator;
|
||
struct wayl_sub_surface render_timer;
|
||
struct wayl_sub_surface overlay;
|
||
+ struct wayl_sub_surface tab_bar;
|
||
+
|
||
+ /* Tab management */
|
||
+#define TAB_MAX 32
|
||
+ struct terminal *tabs[TAB_MAX];
|
||
+ size_t tab_count;
|
||
+ size_t active_tab;
|
||
+
|
||
+ /* Tab bar geometry cache, filled by render_tab_bar, read by hit-testing */
|
||
+ struct {
|
||
+ int xs[TAB_MAX];
|
||
+ int ws[TAB_MAX];
|
||
+ int y;
|
||
+ int h;
|
||
+ size_t count;
|
||
+ } tab_layout;
|
||
|
||
struct wl_callback *frame_callback;
|
||
|