diff --git a/CHANGES.md b/CHANGES.md index 85339e3..39ac924 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,15 @@ +# 0.12.0 (2026-03-25) + +#### Added + +- **Follow mode for scratch pane**: The scratch pane can now follow across window and session switches, keeping a persistent bird's-eye view of lemon sessions. Enable with `lemonaid tmux scratch --follow`. Uses shell-based tmux hooks (~5ms per switch, no Python on the hot path). Height is preserved across switches. `prefix+l` toggles focus instead of hiding. `q` temporarily parks the pane until next `prefix+l`. +- **`scratch_height` config**: Default scratch pane height is now configurable via `scratch_height` in `[tmux-session]` (default `"10%"`). No more `--height` flag needed in tmux keybindings. +- **`follow_scratch` config**: Set `follow_scratch = true` in `[tmux-session]` to auto-enable follow for new tmux servers on first scratch pane creation. + +#### Changed + +- **Scratch pane state files renamed**: `scratch-pane-.json` → `tmux-scratch--pane` (plain text). Legacy files are cleaned up automatically on first use. + # 0.11.0 (2026-03-24) #### Added diff --git a/README.md b/README.md index 2b24a13..56793e3 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ The TUI doesn't need to be running for notifications to arrive (hooks write dire - **Terminal integration**: Hit enter to jump directly to the waiting session's pane (supports [`tmux`](docs/tmux.md) and [WezTerm](docs/wezterm.md)) - **Session history & resume**: Browse archived sessions across all projects, filter by name/cwd/branch, and resume directly or copy the command - **Bootstrap**: `lemonaid claude bootstrap` imports historical Claude sessions from before lemonaid was installed into the archive -- **Scratch pane** (`tmux`): Toggle an always-on inbox with a keybinding - no startup delay, auto-hides after selection +- **Scratch pane** (`tmux`): Toggle an always-on inbox with a keybinding — no startup delay. Optional **follow mode** keeps the pane visible across all window/session switches - **Auto-refresh TUI**: See new notifications appear without losing your place ### Assorted helpers diff --git a/docs/config.md b/docs/config.md index 8778198..d38bc40 100644 --- a/docs/config.md +++ b/docs/config.md @@ -12,6 +12,8 @@ See [wezterm.md](wezterm.md). | Key | Default | Description | |-----|---------|-------------| +| `scratch_height` | `"10"` | Default height for the scratch pane in rows. Percentages (e.g. `"15%"`) are accepted but resize detection won't work with them. | +| `follow_scratch` | `false` | Bootstrap follow mode for new tmux servers. When the scratch pane is first toggled on a server, this determines whether follow is enabled by default. See [tmux.md](tmux.md#follow-mode). | | `resume_window` | `0` | 0-based index into the template window list: which window to replace with the resume command when spawning a tmux session from history (`T`). Set to `1` if your lemon is in the second tab. | ### `[tmux-session.templates]` diff --git a/docs/tmux.md b/docs/tmux.md index 7474768..22e7841 100644 --- a/docs/tmux.md +++ b/docs/tmux.md @@ -69,41 +69,89 @@ Add to your `~/.tmux.conf`: bind-key l run-shell 'lemonaid tmux scratch' ``` -Reload tmux config: - -```bash -tmux source-file ~/.tmux.conf -``` - ### Usage 1. Press `prefix + l` to show the lma inbox as a split at the top of your current window -2. Select a notification with Enter - you'll switch to that session and the lma pane auto-hides +2. Select a notification with Enter — you'll switch to that session and the lma pane auto-hides 3. Press `prefix + l` again to bring it back The keybinding is "idempotent" in that pressing it always gets you to the scratch pane: - If the pane is hidden → show it -- If the pane is visible but not focused → select it +- If the pane is visible but not focused → focus it - If the pane is visible and focused → hide it ### How it works 1. First toggle creates a tmux session (`_lma_scratch`) running `lma --scratch` -2. The pane is joined into your current window as a 30% top split +2. The pane is joined into your current window as a top split 3. When you select a notification, lma auto-dismisses by breaking the pane to its own window 4. Subsequent toggles show/select/hide the same pane (no restart, instant response) -State is tracked per tmux server in `~/.local/state/lemonaid/scratch-pane-.json`, so multiple tmux servers won't conflict. +State is tracked per tmux server in `~/.local/state/lemonaid/`, so multiple tmux servers won't conflict. -### Options +### Follow mode + +Follow mode keeps the scratch pane visible across window and session switches — it follows you everywhere, giving a persistent bird's-eye view of your lemon sessions. + +#### Enabling follow ```bash -# Customize the pane height -lemonaid tmux scratch --height 40% +lemonaid tmux scratch --follow +``` -# See what action was taken -lemonaid tmux scratch -v # prints: created, shown, or hidden +This enables follow for the current tmux server and prints `set-hook` lines to add to `.tmux.conf` for persistence across tmux restarts: + +```tmux +set-hook -g after-select-window[100] 'run-shell -b "~/.local/state/lemonaid/tmux-scratch-follow.sh"' +set-hook -g session-window-changed[100] 'run-shell -b "~/.local/state/lemonaid/tmux-scratch-follow.sh"' +set-hook -g client-session-changed[100] 'run-shell -b "~/.local/state/lemonaid/tmux-scratch-follow.sh"' +``` + +The hooks run a shell script (~5ms per switch, no Python) that moves the scratch pane into your current window. + +#### Behavior in follow mode + +- **Switching sessions/windows**: The scratch pane follows automatically, preserving its height +- **`prefix + l`**: Toggles focus between the scratch pane and your main pane (never hides) +- **`q` in lma**: Temporarily parks the pane. The next `prefix + l` brings it back with follow still active +- **Selecting a notification**: Switches to that session; the scratch pane follows via the hook +- **Resizing**: Manual resizes are preserved across switches + +#### Disabling follow + +```bash +lemonaid tmux scratch --unfollow +``` + +This disables follow for the current tmux server session. The hooks in `.tmux.conf` remain but become no-ops. Re-run `--follow` to re-enable. + +#### Config bootstrap + +To have follow enabled by default for new tmux servers: + +```toml +[tmux-session] +follow_scratch = true +``` + +When the scratch pane is first toggled on a new server, the follow state file is bootstrapped from this config value. After that, `--follow` / `--unfollow` controls the per-server state independently. + +### Options + +The default scratch pane height (in rows) is configurable: + +```toml +[tmux-session] +scratch_height = "12" +``` + +In follow mode, if you manually resize the pane (drag the border), a `Save pane height` hint appears in the status bar. Press `S` to persist the new height. + +The `--height` CLI flag overrides the config for a single invocation: + +```bash +lemonaid tmux scratch --height 15 ``` ## Session Templates diff --git a/pyproject.toml b/pyproject.toml index bff8ee6..1598c07 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lemonaid" -version = "0.11.0" +version = "0.12.0" description = "Attention inbox for managing notifications from lemons and other background tools" readme = "README.md" requires-python = ">=3.11" diff --git a/src/lemonaid/config.py b/src/lemonaid/config.py index b906c53..917939d 100644 --- a/src/lemonaid/config.py +++ b/src/lemonaid/config.py @@ -43,6 +43,11 @@ class TmuxSessionConfig: # 0-based index into the template window list: which window to replace # with the resume command when spawning a session from history. resume_window: int = 0 + # Default height for the scratch pane in rows. Percentages (e.g. "15%") + # are accepted but resize detection won't work reliably with them. + scratch_height: str = "10" + # When true, the scratch pane follows across window/session switches. + follow_scratch: bool = False def get_template(self, name: str) -> list[str] | None: """Get a template by name.""" @@ -79,6 +84,7 @@ class TuiConfig: """Configuration for the TUI.""" transparent: bool = False # Use ANSI colors for terminal transparency + refresh_interval: float = 0.33 # Seconds between TUI refreshes keybindings: KeybindingsConfig = field(default_factory=KeybindingsConfig) # Override the label shown for each backend in the TUI. # Keys are channel prefixes (claude, codex, openclaw, opencode); values are display strings. @@ -143,6 +149,8 @@ def _parse_config(data: dict[str, Any]) -> Config: tmux_session = TmuxSessionConfig( templates=tmux_session_data.get("templates", {}), resume_window=tmux_session_data.get("resume_window", 0), + scratch_height=tmux_session_data.get("scratch_height", "30%"), + follow_scratch=tmux_session_data.get("follow_scratch", False), ) tui_data = data.get("tui", {}) @@ -157,6 +165,7 @@ def _parse_config(data: dict[str, Any]) -> Config: ) tui = TuiConfig( transparent=tui_data.get("transparent", False), + refresh_interval=tui_data.get("refresh_interval", 0.33), keybindings=keybindings, backend_labels=tui_data.get("backend_labels", {}), ) diff --git a/src/lemonaid/inbox/tui/app.py b/src/lemonaid/inbox/tui/app.py index 269b817..a0fb6d5 100644 --- a/src/lemonaid/inbox/tui/app.py +++ b/src/lemonaid/inbox/tui/app.py @@ -25,6 +25,13 @@ start_unified_watcher, ) from ...log import get_logger +from ...tmux.scratch import ( + _clear_state, + _hide, + height_has_drifted, + is_follow_enabled, + save_current_height, +) from ...tmux.session import spawn_session_for_resume from .. import db from .screens import RenameScreen @@ -234,6 +241,9 @@ def _setup_keybindings(self) -> None: # Patch Claude (always hidden, always 'P') self.bind("P", "patch_claude", description="Patch Claude", show=False) + # Save scratch pane height (hidden, shown dynamically when height drifts) + self.bind("S", "save_scratch_height", description="Save Height", show=False) + # Cross-table arrow navigation (always active) self.bind("up", "cursor_up", description="Up", show=False) self.bind("down", "cursor_down", description="Down", show=False) @@ -279,7 +289,7 @@ def on_mount(self) -> None: self.query_one("#history_filter", Input).display = False self._refresh_notifications() - self.set_interval(1.0, self._refresh_notifications) + self.set_interval(self.config.tui.refresh_interval, self._refresh_notifications) # Start transcript watchers for auto-dismiss, message updates, and exit detection start_unified_watcher( backends=cast( @@ -509,6 +519,9 @@ def _refresh_notifications(self, *, stay_on_unread: bool = False) -> None: if self._claude_patch_status == "unpatched": status_text += " | [bold cyan]P[/]atch Claude for faster notifications" + if self._scratch_mode and is_follow_enabled() and height_has_drifted(): + status_text += " | [bold cyan]S[/]ave pane height" + status.update(status_text) def action_quit(self) -> None: @@ -682,7 +695,7 @@ def _resume_session(self, *, copy_only: bool = False) -> None: except (subprocess.CalledProcessError, FileNotFoundError): self.notify(f"Resume: {cmd_str}", severity="information") - if self._scratch_mode and not copy_only: + if self._scratch_mode and not copy_only and not is_follow_enabled(): self._hide_scratch_pane() def action_copy_resume(self) -> None: @@ -908,14 +921,25 @@ def action_patch_claude(self) -> None: self._refresh_notifications() + def action_save_scratch_height(self) -> None: + """Save the current scratch pane height for follow mode.""" + if not self._scratch_mode: + return + + save_current_height() + self.notify("Pane height saved") + self._refresh_notifications() + def _hide_scratch_pane(self) -> None: - """Hide this pane by breaking it to a new window (for scratch mode).""" + """Hide the scratch pane back to its holding session. + + Clears the pane state file so follow hooks become no-ops until + the user re-opens with prefix+l (which rewrites the state file). + """ pane_id = os.environ.get("TMUX_PANE") if pane_id: - subprocess.run( - ["tmux", "break-pane", "-d", "-s", pane_id], - capture_output=True, - ) + _clear_state() + _hide(pane_id) def _get_active_for_watcher( self, @@ -1001,8 +1025,9 @@ def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None: self.config, switch_source=notification.switch_source, ) - # In scratch/auto-dismiss mode, hide the pane after navigation - if self._scratch_mode: + # In scratch mode, hide the pane after navigation — unless + # follow mode is active, in which case the hook will re-show it. + if self._scratch_mode and not is_follow_enabled(): self._hide_scratch_pane() diff --git a/src/lemonaid/tmux/cli.py b/src/lemonaid/tmux/cli.py index 20372e9..8f3d80c 100644 --- a/src/lemonaid/tmux/cli.py +++ b/src/lemonaid/tmux/cli.py @@ -6,7 +6,7 @@ from ..config import load_config from .navigation import go_back, swap_back_location -from .scratch import toggle_scratch +from .scratch import ensure_scratch, set_follow, toggle_scratch from .session import create_session @@ -32,8 +32,16 @@ def cmd_swap(args: argparse.Namespace) -> None: def cmd_scratch(args: argparse.Namespace) -> None: """Toggle the scratch lma pane.""" - result = toggle_scratch(height=args.height) - # Optionally print result for debugging + config = load_config() + height = args.height or config.tmux_session.scratch_height + + if args.follow is not None: + result = set_follow(height=height, enable=args.follow) + elif args.ensure: + result = ensure_scratch(height=height) + else: + result = toggle_scratch(height=height, follow_default=config.tmux_session.follow_scratch) + if args.verbose: print(result) @@ -121,8 +129,28 @@ def setup_parser(subparsers: argparse._SubParsersAction) -> None: ) scratch_parser.add_argument( "--height", - default="30%", - help="Height of the scratch pane when shown (default: 30%%)", + default=None, + help="Height of the scratch pane (default: from config, or 10%%)", + ) + scratch_parser.add_argument( + "--ensure", + action="store_true", + help="Show the scratch pane if hidden, but never hide it (for hooks)", + ) + follow_group = scratch_parser.add_mutually_exclusive_group() + follow_group.add_argument( + "--follow", + action="store_const", + const=True, + default=None, + help="Keep the scratch pane visible across all windows (installs tmux hooks)", + ) + follow_group.add_argument( + "--unfollow", + dest="follow", + action="store_const", + const=False, + help="Stop following across windows (removes tmux hooks)", ) scratch_parser.add_argument( "-v", diff --git a/src/lemonaid/tmux/scratch.py b/src/lemonaid/tmux/scratch.py index bed16c2..5df8450 100644 --- a/src/lemonaid/tmux/scratch.py +++ b/src/lemonaid/tmux/scratch.py @@ -3,16 +3,24 @@ Provides a toggleable "always-on" lma pane that can be shown/hidden without restarting the TUI (avoiding startup latency). -State is tracked per tmux server in ~/.local/state/lemonaid/scratch-pane-.json -to avoid conflicts when running multiple tmux servers. +Per-server state files in ~/.local/state/lemonaid/: + tmux-scratch--pane — pane ID (e.g. "%6"). Present = pane is alive. + tmux-scratch--follow — follow flag. "on" = follow active, empty = disabled. + Missing = first run, bootstrap from config. + tmux-scratch--height — last known pane height in rows. Survives recreates. + tmux-scratch-follow.sh — shell hook script, generated once. """ -import json import os import subprocess +import sys +from pathlib import Path +from ..log import get_logger from .navigation import get_state_path +_log = get_logger("tmux.scratch") + _SCRATCH_SESSION = "_lma_scratch" @@ -29,30 +37,116 @@ def _get_server_name() -> str: return "default" -def _get_state() -> dict | None: - """Load scratch pane state (pane_id of the lma pane).""" - path = get_state_path() / f"scratch-pane-{_get_server_name()}.json" +def _state_path() -> Path: + return get_state_path() / f"tmux-scratch-{_get_server_name()}-pane" + + +def _height_path() -> Path: + return get_state_path() / f"tmux-scratch-{_get_server_name()}-height" + + +def _get_pane_id() -> str | None: + """Load the scratch pane ID from the state file.""" + path = _state_path() if not path.exists(): return None - try: - return json.loads(path.read_text()) - except (json.JSONDecodeError, KeyError): - return None + pane_id = path.read_text().strip() + return pane_id or None + + +def _save_pane_id(pane_id: str) -> None: + """Save the scratch pane ID.""" + _state_path().write_text(pane_id) + + +def save_current_height() -> None: + """Save the scratch pane's current height in rows. + + Only called on explicit user action (keybinding), never automatically. + """ + pane_id = _get_pane_id() + if not pane_id: + return + + result = subprocess.run( + ["tmux", "display-message", "-t", pane_id, "-p", "#{pane_height}"], + capture_output=True, + text=True, + ) + if result.returncode != 0: + return + + rows = result.stdout.strip() + if rows: + _log.info("save_current_height: %s rows", rows) + _height_path().write_text(rows) + + +def height_has_drifted() -> bool: + """Check if the scratch pane's current height differs from the saved value. + + Only works reliably when the saved value is a row count (not a percentage). + Returns False if the saved value is a percentage, since we can't compare + accurately across different window sizes. + """ + pane_id = _get_pane_id() + if not pane_id: + return False + + saved = _height_path().read_text().strip() if _height_path().exists() else "" + if not saved or "%" in saved: + return False + + result = subprocess.run( + ["tmux", "display-message", "-t", pane_id, "-p", "#{pane_height}"], + capture_output=True, + text=True, + ) + if result.returncode != 0: + return False -def _save_state(pane_id: str) -> None: - """Save scratch pane state.""" - path = get_state_path() / f"scratch-pane-{_get_server_name()}.json" - path.write_text(json.dumps({"pane_id": pane_id})) + try: + return int(result.stdout.strip()) != int(saved) + except ValueError: + return False def _clear_state() -> None: - """Clear scratch pane state.""" - path = get_state_path() / f"scratch-pane-{_get_server_name()}.json" + """Clear scratch pane state. Follow hooks become no-ops until prefix+l.""" + path = _state_path() if path.exists(): path.unlink() +def _follow_path() -> Path: + return get_state_path() / f"tmux-scratch-{_get_server_name()}-follow" + + +def is_follow_enabled() -> bool: + """Check if follow mode is active for this tmux server.""" + path = _follow_path() + if not path.exists(): + return False + + return path.read_text().strip() == "on" + + +def set_follow_enabled(enabled: bool) -> None: + """Set follow mode for this tmux server. Empty file = disabled.""" + _follow_path().write_text("on" if enabled else "") + + +def bootstrap_follow(config_default: bool) -> None: + """Create the follow file from config if it doesn't exist yet. + + Called on first scratch pane creation for this server. + """ + path = _follow_path() + if not path.exists(): + path.write_text("on" if config_default else "") + + def _pane_exists(pane_id: str) -> bool: """Check if the scratch pane still exists and is ours. @@ -126,12 +220,21 @@ def _get_session_pane() -> str | None: def _create_pane() -> str: """Create the scratch pane with lma running. Returns pane_id.""" + # Clean up legacy state files from pre-0.12 + for name in ( + f"scratch-pane-{_get_server_name()}.json", + f"scratch-pane-{_get_server_name()}", + ): + legacy = get_state_path() / name + if legacy.exists(): + legacy.unlink() + # Check if session already exists (recovery: state file lost but session exists) if _session_exists(): pane_id = _get_session_pane() if pane_id: _mark_pane(pane_id) - _save_state(pane_id) + _save_pane_id(pane_id) return pane_id # Get current window dimensions to size the detached session properly @@ -180,7 +283,7 @@ def _create_pane() -> str: ) pane_id = result.stdout.strip() _mark_pane(pane_id) - _save_state(pane_id) + _save_pane_id(pane_id) return pane_id @@ -260,39 +363,169 @@ def _create_and_show(height: str) -> str: ) pane_id = _create_pane() _show(pane_id, height, target_pane) + _log.info("_create_and_show: seeding height file with %s", height) + _height_path().write_text(height) return "created" -def toggle_scratch(height: str = "30%") -> str: - """Toggle the scratch lma pane. Returns 'shown', 'hidden', 'selected', or 'created'.""" - # Capture current pane early - tmux context can change during operations +def ensure_scratch(height: str = "10") -> str: + """Ensure the scratch pane is visible in the current window. + + Like toggle, but never hides — only creates or shows. + Returns 'shown', 'created', or 'already_visible'. + """ current_pane = _get_current_pane() - state = _get_state() + pane_id = _get_pane_id() - if state is None: + if pane_id is None or not _pane_exists(pane_id): return _create_and_show(height) - pane_id = state["pane_id"] + current_window = _get_current_window() + pane_window = _get_pane_window(pane_id) + + if pane_window == current_window: + return "already_visible" - if not _pane_exists(pane_id): + if not _show(pane_id, height, current_pane): + return _create_and_show(height) + + return "shown" + + +def _follow_script_path() -> Path: + return get_state_path() / "tmux-scratch-follow.sh" + + +def _write_follow_script(height: str = "10") -> Path: + """Write the follow hook script to disk. + + Pure shell — reads the pane ID from the state file, checks if + it's already in the current window, and joins it if not. ~5ms + on the hot path (already visible). + """ + script_path = _follow_script_path() + state_dir = str(get_state_path()) + # Parse tmux server name from $TMUX the same way the Python code does + script_path.write_text( + f"""#!/bin/sh +# lemonaid scratch-follow hook — do not edit, regenerated by lemonaid +server=$(basename "$(echo "$TMUX" | cut -d, -f1)") +[ -n "$server" ] || server=default +dir={state_dir} + +# Is follow enabled for this server? +grep -q on "$dir/tmux-scratch-$server-follow" 2>/dev/null || exit 0 + +# Is there a pane to show? +pane=$(cat "$dir/tmux-scratch-$server-pane" 2>/dev/null) +[ -n "$pane" ] || exit 0 + +# Already in this window? +cur=$(tmux display -p '#{{window_id}}') +tgt=$(tmux display -t "$pane" -p '#{{window_id}}' 2>/dev/null) || exit 0 +[ "$cur" = "$tgt" ] && exit 0 + +height=$(cat "$dir/tmux-scratch-$server-height" 2>/dev/null) +[ -n "$height" ] || height=10 + +cur_pane=$(tmux display -p '#{{pane_id}}') +tmux join-pane -v -b -l "$height" -s "$pane" 2>/dev/null +tmux select-pane -t "$cur_pane" 2>/dev/null +exit 0 +""" + ) + script_path.chmod(0o755) + return script_path + + +def _check_tmux_conf_hooks() -> bool: + """Check if something that looks like the follow hooks is in .tmux.conf.""" + tmux_conf = Path.home() / ".tmux.conf" + if not tmux_conf.exists(): + return False + + return "scratch-follow" in tmux_conf.read_text() + + +def set_follow(height: str = "10", enable: bool = True) -> str: + """Enable or disable follow mode for this tmux server. + + When enabled, installs tmux hooks for the current server session + and generates the follow script. Warns if .tmux.conf doesn't have + the hooks (so follow won't persist across tmux restarts). + """ + set_follow_enabled(enable) + + if enable: + ensure_scratch(height) + _write_follow_script(height) + _install_hooks() + + if not _check_tmux_conf_hooks(): + script = _follow_script_path() + print( + f"\nFollow enabled for this tmux server session.\n" + f"To persist across tmux restarts, add these to .tmux.conf:\n\n" + f" set-hook -g after-select-window[100] 'run-shell -b \"{script}\"'\n" + f" set-hook -g session-window-changed[100] 'run-shell -b \"{script}\"'\n" + f" set-hook -g client-session-changed[100] 'run-shell -b \"{script}\"'\n", + file=sys.stderr, + ) + + return "follow enabled" + + return "follow disabled" + + +def _install_hooks() -> None: + """Install the tmux hooks that call the follow script.""" + script = _follow_script_path() + for hook in ("after-select-window", "session-window-changed", "client-session-changed"): + subprocess.run( + ["tmux", "set-hook", "-g", f"{hook}[100]", f"run-shell -b '{script}'"], + capture_output=True, + ) + + +def toggle_scratch(height: str = "10", follow_default: bool = False) -> str: + """Toggle the scratch lma pane. Returns 'shown', 'hidden', 'selected', or 'created'. + + In follow mode, the pane is never hidden via toggle — use q in lma to dismiss. + follow_default is the config value, used to bootstrap the follow file on first run. + """ + bootstrap_follow(follow_default) + follow = is_follow_enabled() + + current_pane = _get_current_pane() + pane_id = _get_pane_id() + + if pane_id is None or not _pane_exists(pane_id): return _create_and_show(height) current_window = _get_current_window() pane_window = _get_pane_window(pane_id) if pane_window == current_window: - # Pane is visible - either select it or hide it if current_pane == pane_id: - # Already focused on scratch pane, hide it + if follow: + # Focus the next pane (the main content pane below) + subprocess.run( + ["tmux", "select-pane", "-t", ":.+"], + capture_output=True, + ) + return "defocused" + if not _hide(pane_id): return _create_and_show(height) + return "hidden" else: - # Scratch pane visible but not focused, select it if not _select_pane(pane_id): return _create_and_show(height) + return "selected" else: if not _show(pane_id, height, current_pane): return _create_and_show(height) + return "shown"