From 240c1cea4f8c864257b0626c96e5c0b6b1727285 Mon Sep 17 00:00:00 2001 From: Andrew Hamilton Date: Mon, 14 Sep 2026 07:30:04 +1000 Subject: [PATCH] feat: per-compositor app config in kick, slim kick-pi --- README.md | 10 ++ kick-pi | 127 ++------------------- kick.c | 268 +++++++++++++++++++++++++++++++++++++++++++- tests/test_smoke.py | 61 ++++++++++ 4 files changed, 342 insertions(+), 124 deletions(-) diff --git a/README.md b/README.md index a42ee0f..bd090be 100644 --- a/README.md +++ b/README.md @@ -346,6 +346,16 @@ Wayland compositor and draws every tab inside that single window. standalone on wlroots' headless backend (no DRM needed), useful in CI or when no TTY is available. +### Per-compositor app config + +Clients see a scoped view of `~/.config`: at startup kick builds a shadow +config home (symlinked passthrough, so nothing diverges) with only GTK +window controls pinned to close-only, plus a `DCONF_PROFILE` overlay +pinning the same key (dconf wins over settings files). Kick never +minimizes for anyone, so client-drawn minimize/maximize buttons would be +dead -- other compositors sharing the home directory are unaffected. +Set `KICK_NO_CONFIG_SHADOW=1` to disable the scoped view entirely. + `test_nested_host_primary_to_kick` exercises the host->nested primary selection path, which only exists over the ext-data-control bridge; it skips on hosts without it (e.g. GNOME), so use an ext-data-control host (labwc, diff --git a/kick-pi b/kick-pi index 338e08e..d6b8d59 100755 --- a/kick-pi +++ b/kick-pi @@ -2,12 +2,16 @@ # kick-pi -- Raspberry Pi session entry for kick. # # A labwc-pi analogue for running kick as the Pi's compositor instead of -# labwc: sets up the Pi OS session context, scopes GTK window controls to -# close-only for kick sessions (shadow XDG_CONFIG_HOME plus a DCONF_PROFILE -# overlay database -- see kick_shadow_config below), then execs kick, -# passing all arguments through untouched (screen mode, --panel, tabs, ...). +# labwc: sets up the Pi OS session context, then execs kick, passing all +# arguments through untouched (screen mode, --panel, tabs, ...). # Environment (KICK_MODE, KICK_LOG, ...) passes through equally untouched. # +# Per-compositor GTK window controls (close-only under kick, full under +# labwc) live in kick itself now: it builds a shadow config home plus a +# dconf overlay for its own children, so this wrapper carries none of that +# machinery. (A wrapper-level copy would only duplicate it; version mixes +# still behave: new wrapper + old kick simply scopes nothing.) +# # Deliberately NOT done here (kept minimal, by choice): # - logging (see kick-session for per-boot logs), # - labwc autostart files (kick-only: panels and apps come from kick's @@ -43,121 +47,6 @@ else chmod 700 "$XDG_RUNTIME_DIR" 2>/dev/null || true fi -# Per-compositor GTK window controls without touching shared state. -# Wayland leaves decoration policy to clients with no per-compositor -# channel, and kick never minimizes/maximizes for anyone, so client-drawn -# minimize/maximize buttons are dead under kick yet live under labwc -- -# while both sessions share one $HOME. Fix it with a shadow config home: -# every entry of ~/.config is symlinked in (writes pass through to the -# real files, nothing diverges), except gtk-3.0/settings.ini and -# gtk-4.0/settings.ini, which are regenerated from the real ones with only -# gtk-decoration-layout forced to close-only. labwc sessions keep seeing -# the real ~/.config (full buttons); kick sessions see the shadow. -# Crash-safe (rebuilt every login, never authoritative) and idempotent. -kick_pin_gtk_layout() { - # $1 = settings.ini path: force gtk-decoration-layout=:close under - # [Settings], creating file/section as needed, keeping everything else. - [ -f "$1" ] || : > "$1" 2>/dev/null || return 0 - tmp="$1.kick-pin-tmp" - awk ' - BEGIN { in_s = 0; seen_s = 0; done = 0 } - /^\[[^]]*\]/ { - seen_s = seen_s || ($0 == "[Settings]") - in_s = ($0 == "[Settings]") - print - next - } - in_s && !done && /^[[:space:]]*gtk-decoration-layout[[:space:]]*=/ { - print "gtk-decoration-layout=:close" - done = 1 - next - } - { print } - END { - if (!done) { - if (!seen_s) print "[Settings]" - print "gtk-decoration-layout=:close" - } - } - ' "$1" > "$tmp" 2>/dev/null && mv -f "$tmp" "$1" 2>/dev/null - rm -f "$tmp" 2>/dev/null || true -} - -# Per-compositor GTK window controls, part two: dconf. -# GtkSettings binds gtk-decoration-layout to org.gnome.desktop.wm.preferences -# button-layout, and the dconf binding wins over settings.ini -- so the -# shadow settings.ini alone cannot hide the buttons while the database -# carries the schema default (appmenu:minimize,maximize,close). Layer a -# kick-only user database in front via DCONF_PROFILE: reads fall through -# to the real databases unchanged, writes land in the overlay file, and -# labwc sessions (no DCONF_PROFILE) never see it. Tradeoff, stated plainly: -# settings changed under kick are written to the overlay database and are -# invisible under labwc; everything merely read behaves identically. -# $1 = shadow config dir. Skipped without the dconf CLI or a session bus. -kick_dconf_overlay() { - shadow_dir=$1 - command -v dconf >/dev/null 2>&1 || return 0 - [ -n "${DBUS_SESSION_BUS_ADDRESS:-}" ] || return 0 - profile="$shadow_dir/dconf-profile" - # NOTE: the database name must be a valid D-Bus object path element - # (alphanumerics only): dconf maps it to /ca/desrt/dconf/Writer/ - # and aborts on anything else -- so no hyphens here, ever. - { - echo "user-db:kickoverrides" - if [ -f /etc/dconf/profile/user ]; then - grep -v "^user-db:" /etc/dconf/profile/user 2>/dev/null || true - grep "^user-db:" /etc/dconf/profile/user 2>/dev/null || true - else - printf 'user-db:user\nsystem-db:local\nsystem-db:site\nsystem-db:distro\n' - fi - } > "$profile.kick-new" 2>/dev/null || return 0 - mv -f "$profile.kick-new" "$profile" 2>/dev/null || return 0 - export DCONF_PROFILE="$profile" - # Seed the pinned layout (idempotent; lands in kickoverrides, the - # first user database, leaving the real databases untouched). - dconf write /org/gnome/desktop/wm/preferences/button-layout \ - "':close'" 2>/dev/null || true -} - -kick_shadow_config() { - [ -n "${HOME:-}" ] || return 0 - real_config="$HOME/.config" - shadow="${XDG_CACHE_HOME:-$HOME/.cache}/kick/config" - mkdir -p "$shadow" 2>/dev/null || return 0 - if [ -d "$real_config" ]; then - for entry in "$real_config"/* "$real_config"/.*; do - case "$entry" in - "$real_config/."|"$real_config/..") continue ;; - esac - [ -e "$entry" ] || [ -L "$entry" ] || continue - base=${entry##*/} - [ -e "$shadow/$base" ] || [ -L "$shadow/$base" ] || \ - ln -s "$entry" "$shadow/$base" 2>/dev/null || true - done - fi - # Shadow copies (real directories, never symlinks): refreshed from the - # real files on every login so theme/icons/fonts track labwc-side edits, - # then pinned -- only the layout line ever diverges. When no real file - # exists the shadow copy is left alone. - for gtkdir in gtk-3.0 gtk-4.0; do - if [ -L "$shadow/$gtkdir" ]; then - rm -f "$shadow/$gtkdir" 2>/dev/null || continue - fi - mkdir -p "$shadow/$gtkdir" 2>/dev/null || continue - if [ -f "$real_config/$gtkdir/settings.ini" ]; then - cp -f "$real_config/$gtkdir/settings.ini" \ - "$shadow/$gtkdir/settings.ini" 2>/dev/null || true - fi - kick_pin_gtk_layout "$shadow/$gtkdir/settings.ini" - done - # Export last so earlier failures keep the real config rather than a - # half-built shadow. - export XDG_CONFIG_HOME="$shadow" - kick_dconf_overlay "$shadow" -} -kick_shadow_config -unset -f kick_shadow_config kick_pin_gtk_layout kick_dconf_overlay 2>/dev/null || true - # Software rendering on Pi models whose GPU lacks an MMU (same guard as # labwc-pi; kick's wlroots renderer autocreate honours WLR_RENDERER). if command -v raspi-config >/dev/null 2>&1; then diff --git a/kick.c b/kick.c index 70be374..af07146 100644 --- a/kick.c +++ b/kick.c @@ -41,6 +41,8 @@ const char *__ubsan_default_options(void) { #include #include #include +#include +#include #include @@ -11563,6 +11565,21 @@ frame_done: /* ===================== Launch client ===================== */ +/* Per-compositor session config shadowing (the kick-pi mechanism, owned + here so plain `kick` behaves the same everywhere): a shadow config home + where every ~/.config entry is symlinked in (writes pass through), + except gtk-3.0/gtk-4.0 settings.ini, regenerated with only + gtk-decoration-layout forced to close-only, plus a DCONF_PROFILE overlay + database pinning the same key (dconf wins over settings.ini). Labwc + sessions never see it; nested kick children do (per-compositor scoping). + Best-effort throughout: anything failing leaves config_shadow_ok false + and children inherit the plain environment. */ +static char shadow_config[1024]; +static char shadow_dconf_profile[1024]; +static bool config_shadow_ok; +static void config_shadow_build(void); +static void config_seed_dconf(void); + /* D-Bus/systemd activation environment, mirroring what labwc does on launch (DRM backend): D-Bus-activated services and systemd user units otherwise keep whatever WAYLAND_DISPLAY etc. they were started with -- @@ -11585,8 +11602,11 @@ static void activation_run(bool clear) { /* WAYLAND_DISPLAY is always exported (our socket, set below); the rest only when present in our own environment, so absent values are never pushed as empty over a live session's settings -- except on clear, - where empty is exactly labwc's documented way of blanking them. */ - const char *args[2 + 7 + 1]; + where empty is exactly labwc's documented way of blanking them. The + shadow config paths ride along the same way when shadowing is up, so + D-Bus-activated apps see the same per-compositor view as our own + children (standalone only, like the rest of this call). */ + const char *args[2 + 9 + 1]; int argc = 0; args[argc++] = "dbus-update-activation-environment"; args[argc++] = "--systemd"; @@ -11610,6 +11630,15 @@ static void activation_run(bool clear) { } } } + char shadow_values[2][1100]; + if (config_shadow_ok) { + snprintf(shadow_values[0], sizeof(shadow_values[0]), + "XDG_CONFIG_HOME=%s", clear ? "" : shadow_config); + args[argc++] = shadow_values[0]; + snprintf(shadow_values[1], sizeof(shadow_values[1]), + "DCONF_PROFILE=%s", clear ? "" : shadow_dconf_profile); + args[argc++] = shadow_values[1]; + } args[argc] = NULL; pid_t pid = fork(); if (pid == 0) { @@ -11651,6 +11680,225 @@ static void activation_update(void) { atexit(activation_clear_env); } +/* mkdir -p for the shadow tree (best-effort; failure just skips shadowing). */ +static void shadow_mkdir_p(const char *path) { + char tmp[1024]; + if (snprintf(tmp, sizeof(tmp), "%s", path) >= (int)sizeof(tmp)) + return; + for (char *p = tmp + 1; *p; p++) { + if (*p == '/') { + *p = '\0'; + mkdir(tmp, 0755); + *p = '/'; + } + } + mkdir(tmp, 0755); +} + +/* Force gtk-decoration-layout=:close under [Settings], creating + file/section as needed and keeping every other line byte-identical. */ +static void shadow_pin_gtk_layout(const char *path) { + static const char *const key = "gtk-decoration-layout=:close"; + char tmp[1024 + 32]; + if (snprintf(tmp, sizeof(tmp), "%s.kick-pin-%d-tmp", path, + (int)getpid()) >= (int)sizeof(tmp)) + return; + FILE *in = fopen(path, "r"); + FILE *out = fopen(tmp, "w"); + if (!out) { + if (in) fclose(in); + return; + } + bool in_settings = false, seen_settings = false, done = false; + char *line = NULL; + size_t cap = 0; + ssize_t n; + while (in && (n = getline(&line, &cap, in)) >= 0) { + if (line[0] == '[') { + size_t m = 0; + while (line[m] && line[m] != ']' && line[m] != '\n') m++; + in_settings = (m == 10 && memcmp(line, "[Settings]", 10) == 0); + seen_settings = seen_settings || in_settings; + fwrite(line, 1, (size_t)n, out); + continue; + } + if (in_settings && !done) { + size_t k = 0; + while (line[k] == ' ' || line[k] == '\t') k++; + size_t kl = strlen("gtk-decoration-layout"); + if (strncmp(line + k, "gtk-decoration-layout", kl) == 0) { + size_t j = k + kl; + while (line[j] == ' ' || line[j] == '\t') j++; + if (line[j] == '=') { + fprintf(out, "%s\n", key); + done = true; + continue; + } + } + } + fwrite(line, 1, (size_t)n, out); + } + free(line); + if (in) fclose(in); + if (!done) { + if (!seen_settings) + fputs("[Settings]\n", out); + fprintf(out, "%s\n", key); + } + fclose(out); + rename(tmp, path); +} + +/* Copy a regular file byte for byte; missing source is fine (skip). */ +static void shadow_copy_file(const char *src, const char *dst) { + FILE *in = fopen(src, "rb"); + if (!in) return; + FILE *out = fopen(dst, "wb"); + if (!out) { + fclose(in); + return; + } + char buf[8192]; + size_t n; + while ((n = fread(buf, 1, sizeof(buf), in)) > 0) + fwrite(buf, 1, n, out); + fclose(in); + fclose(out); +} + +/* Write the DCONF_PROFILE overlay: our kick-only user database first, then + the system profile verbatim (user databases last, like the wrapper did). + dconf database names must stay alphanumeric: dconf maps the name into a + D-Bus object path and aborts on anything else, so no hyphens, ever. */ +static void shadow_write_profile(const char *path) { + /* Atomic via temp + rename: parallel sessions share one shadow dir. */ + char tmp[1024 + 32]; + if (snprintf(tmp, sizeof(tmp), "%s.kick-prof-%d-tmp", path, + (int)getpid()) >= (int)sizeof(tmp)) + return; + FILE *out = fopen(tmp, "w"); + if (!out) return; + fputs("user-db:kickoverrides\n", out); + FILE *sys = fopen("/etc/dconf/profile/user", "r"); + if (sys) { + char *line = NULL; + size_t cap = 0; + ssize_t n; + /* Non-user lines first, user lines last (stable stacking order). */ + while ((n = getline(&line, &cap, sys)) >= 0) + if (strncmp(line, "user-db:", 8) != 0) + fwrite(line, 1, (size_t)n, out); + rewind(sys); + while ((n = getline(&line, &cap, sys)) >= 0) + if (strncmp(line, "user-db:", 8) == 0) + fwrite(line, 1, (size_t)n, out); + free(line); + fclose(sys); + } else { + fputs("user-db:user\nsystem-db:local\nsystem-db:site\n" + "system-db:distro\n", out); + } + fclose(out); + rename(tmp, path); +} + +/* Build the shadow config home (see the block comment above + shadow_config): symlinked passthrough plus pinned GTK layouts plus the + dconf overlay profile. Runs once at startup, before first clients. */ +static void config_shadow_build(void) { + if (getenv("KICK_NO_CONFIG_SHADOW")) + return; + const char *home = getenv("HOME"); + if (!home || !home[0]) + return; + const char *cache = getenv("XDG_CACHE_HOME"); + char base[1024]; + if (cache && cache[0]) { + if (snprintf(base, sizeof(base), "%s", cache) >= (int)sizeof(base)) + return; + } else { + if (snprintf(base, sizeof(base), "%s/.cache", home) >= (int)sizeof(base)) + return; + } + if (snprintf(shadow_config, sizeof(shadow_config), "%s/kick/config", + base) >= (int)sizeof(shadow_config)) + return; + shadow_mkdir_p(shadow_config); + DIR *d = NULL; + { + char real[1024]; + if (snprintf(real, sizeof(real), "%s/.config", home) >= (int)sizeof(real)) + return; + d = opendir(real); + if (d) { + struct dirent *de; + while ((de = readdir(d)) != NULL) { + if (strcmp(de->d_name, ".") == 0 || + strcmp(de->d_name, "..") == 0) + continue; + char src[2048], dst[2048]; + if (snprintf(src, sizeof(src), "%s/%s", real, + de->d_name) >= (int)sizeof(src)) + continue; + if (snprintf(dst, sizeof(dst), "%s/%s", shadow_config, + de->d_name) >= (int)sizeof(dst)) + continue; + struct stat st; + if (lstat(dst, &st) == 0) + continue; + symlink(src, dst); + } + closedir(d); + } + } + static const char *const gtkdirs[] = { "gtk-3.0", "gtk-4.0" }; + for (size_t gi = 0; gi < sizeof(gtkdirs) / sizeof(gtkdirs[0]); gi++) { + char sub[2048], rfile[2048], sfile[2048]; + if (snprintf(sub, sizeof(sub), "%s/%s", shadow_config, + gtkdirs[gi]) >= (int)sizeof(sub)) + continue; + struct stat st; + if (lstat(sub, &st) == 0 && S_ISLNK(st.st_mode)) + unlink(sub); + shadow_mkdir_p(sub); + if (snprintf(rfile, sizeof(rfile), "%s/.config/%s/settings.ini", + home, gtkdirs[gi]) >= (int)sizeof(rfile)) + continue; + if (snprintf(sfile, sizeof(sfile), "%s/settings.ini", + sub) >= (int)sizeof(sfile)) + continue; + shadow_copy_file(rfile, sfile); + shadow_pin_gtk_layout(sfile); + } + if (snprintf(shadow_dconf_profile, sizeof(shadow_dconf_profile), + "%s/dconf-profile", shadow_config) >= + (int)sizeof(shadow_dconf_profile)) + return; + shadow_write_profile(shadow_dconf_profile); + config_shadow_ok = true; + kick_log(KICK_LOG_INFO, "[config] shadow ready\n"); +} + +/* Seed the pinned dconf layout into the overlay database (idempotent; + lands in kickoverrides, leaving real databases untouched). Needs the + session bus like everything dconf; skipped silently without it. */ +static void config_seed_dconf(void) { + if (!config_shadow_ok) + return; + if (!getenv("DBUS_SESSION_BUS_ADDRESS")) + return; + if (!cmd_in_path("dconf")) + return; + pid_t pid = fork(); + if (pid == 0) { + setenv("DCONF_PROFILE", shadow_dconf_profile, 1); + execlp("dconf", "dconf", "write", + "/org/gnome/desktop/wm/preferences/button-layout", + "':close'", (char *)NULL); + _exit(127); + } +} + /* Does the first whitespace-delimited word of `cmd` resolve to an executable on PATH? Mirrors how `sh -c ` resolves the first word, so this is a cheap pre-launch "which" check. Returns 1 if found (or uncheckable, e.g. @@ -11715,6 +11963,13 @@ static void launch_client(struct kick_window *w) { if (!xdg_runtime) xdg_runtime = "/tmp"; setenv("WAYLAND_DISPLAY", kick_socket, 1); setenv("XDG_RUNTIME_DIR", xdg_runtime, 1); + /* Per-compositor config view (shadow home + dconf overlay), built + at startup; falls back to the inherited environment when + shadowing is off. */ + if (config_shadow_ok) { + setenv("XDG_CONFIG_HOME", shadow_config, 1); + setenv("DCONF_PROFILE", shadow_dconf_profile, 1); + } /* Toolkits self-select their connection: Wayland-capable ones see only our socket above, X11-only ones (emacs-gtk, xmessage) take DISPLAY below. Never force GDK_BACKEND/QT_QPA_PLATFORM to @@ -13112,9 +13367,12 @@ int main(int argc, char *argv[]) { set_active_tab(focus_tab); /* Fork every command-line command at once; the process-group match in - handle_new_toplevel gives each its own slot on arrival. Refresh the - D-Bus/systemd activation environment first so services started from - here on see this session's socket. */ + handle_new_toplevel gives each its own slot on arrival. Build the + shadow config first (children inherit the scoped view), then refresh + the D-Bus/systemd activation environment so services started from + here on see this session's socket (and the same scoped view). */ + config_shadow_build(); + config_seed_dconf(); activation_update(); launch_all_cli(); /* The --panel command, if any: a layer-shell client, forked separately since diff --git a/tests/test_smoke.py b/tests/test_smoke.py index 1ac4155..89ce9f1 100644 --- a/tests/test_smoke.py +++ b/tests/test_smoke.py @@ -1300,6 +1300,11 @@ def _spawn_standalone(extra_env, *apps, log=None): for key in ("XCURSOR_PATH", "XCURSOR_THEME"): if env.get(key): cmd.append("%s=%s" % (key, env[key])) + # Opt-outs under test (e.g. KICK_NO_CONFIG_SHADOW) ride the same + # explicit-var discipline as everything else above: env -i wipes the + # dict, so each var the compositor needs must be listed. + if env.get("KICK_NO_CONFIG_SHADOW"): + cmd.append("KICK_NO_CONFIG_SHADOW=" + env["KICK_NO_CONFIG_SHADOW"]) # NOTE: env -i wipes the dict above, so every var the compositor needs # must be listed explicitly -- a missing KICK_DUMP_DIR silently dumps # all standalone sessions into shared /tmp/host_N.ppm paths, and @@ -2992,6 +2997,62 @@ def test_background_fullscreen_keeps_normal_layout(tmp_path): _assert_alive(s, "compositor died") +def test_standalone_shadow_config_scopes_children(tmp_path): + """Standalone kick builds a shadow config home and scopes children to + it: a tab sees XDG_CONFIG_HOME/DCONF_PROFILE under the shadow, the + shadow gtk settings carry the pinned close-only layout plus the real + theme, and the dconf profile layers the kick-only database first.""" + home = tmp_path / "home" + (home / ".config" / "gtk-3.0").mkdir(parents=True) + (home / ".config" / "gtk-3.0" / "settings.ini").write_text( + "[Settings]\ngtk-theme=Adwaita\n") + out = tmp_path / "child-env.out" + with _run_standalone( + {"HOME": str(home)}, + "sh -c 'echo XDG_CONFIG_HOME=$XDG_CONFIG_HOME > \"%s\"; " + "echo DCONF_PROFILE=$DCONF_PROFILE >> \"%s\"'" % (out, out), + "%s '#ff0000'" % WL, + wait="any") as s: + assert _wait_out(out, "XDG_CONFIG_HOME=", timeout=15.0) + text = out.read_text() + shadow = os.path.join(str(home), ".cache", "kick", "config") + assert ("XDG_CONFIG_HOME=%s\n" % shadow) in text, \ + f"child missed shadow config home: {text!r}" + assert ("DCONF_PROFILE=%s/dconf-profile\n" % shadow) in text, \ + f"child missed dconf overlay profile: {text!r}" + pinned = open(os.path.join(shadow, "gtk-3.0", + "settings.ini")).read() + assert "gtk-decoration-layout=:close" in pinned, \ + f"layout not pinned: {pinned!r}" + assert "gtk-theme=Adwaita" in pinned, \ + f"real theme lost: {pinned!r}" + profile = open(os.path.join(shadow, "dconf-profile")).read() + assert "user-db:kickoverrides" in profile.splitlines(), \ + f"overlay db not first: {profile!r}" + _assert_alive(s, "compositor died") + + +def test_standalone_shadow_config_opt_out(tmp_path): + """KICK_NO_CONFIG_SHADOW disables the scoped view: children inherit + the plain environment with no XDG_CONFIG_HOME/DCONF_PROFILE override.""" + home = tmp_path / "home" + home.mkdir() + out = tmp_path / "child-env.out" + with _run_standalone( + {"HOME": str(home), "KICK_NO_CONFIG_SHADOW": "1"}, + "sh -c 'echo XDG_CONFIG_HOME=$XDG_CONFIG_HOME > \"%s\"; " + "echo DCONF_PROFILE=$DCONF_PROFILE >> \"%s\"'" % (out, out), + "%s '#ff0000'" % WL, + wait="any") as s: + assert _wait_out(out, "XDG_CONFIG_HOME=", timeout=15.0) + text = out.read_text() + assert "XDG_CONFIG_HOME=\n" in text, \ + f"opt-out leaked a shadow config home: {text!r}" + assert "DCONF_PROFILE=\n" in text, \ + f"opt-out leaked a dconf overlay profile: {text!r}" + _assert_alive(s, "compositor died") + + def test_alt_f11_toggles_host_fullscreen(tmp_path): """prefix+F11 toggles the kick window's fullscreen state in the host; the resulting configure fullscreens the active tab only (other tabs