wip: feat: workspaces tmux support

This commit is contained in:
Waylon Walker 2025-11-25 18:33:39 -06:00
parent 575e51eaa9
commit d5d081f743

View file

@ -10,6 +10,7 @@
# ] # ]
# /// # ///
from __future__ import annotations
import os import os
import re import re
import shutil import shutil
@ -18,7 +19,9 @@ from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import List, Optional, Tuple from typing import List, Optional, Tuple
import typer import typer
from pydantic import Field from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict from pydantic_settings import BaseSettings, SettingsConfigDict
from rich.console import Console from rich.console import Console
@ -233,7 +236,7 @@ def read_workspace_readme(ws_dir: Path) -> Tuple[str, str]:
return ws_dir.name, "" return ws_dir.name, ""
# First non-empty line must be '# ...' per spec # First non-empty line must be '# ...' per spec
first_non_empty_idx = next((i for i, l in enumerate(lines) if l.strip()), None) first_non_empty_idx = next((i for i, line in enumerate(lines) if line.strip()), None)
if first_non_empty_idx is None: if first_non_empty_idx is None:
return ws_dir.name, "" return ws_dir.name, ""
@ -1087,5 +1090,262 @@ def status_workspace(
if desc: if desc:
console.print(Panel(desc, title="Workspace description")) console.print(Panel(desc, title="Workspace description"))
def not_in_tmux() -> bool:
"""Return True if not inside tmux or zellij."""
return not os.environ.get("TMUX") and not os.environ.get("ZELLIJ")
def run_tmux(args: list[str], *, clear_tmux_env: bool = False) -> subprocess.CompletedProcess:
"""Run a tmux command."""
env = os.environ.copy()
if clear_tmux_env:
env["TMUX"] = ""
return subprocess.run(
["tmux", *args],
env=env,
check=False,
)
def session_exists(session_name: str) -> bool:
"""Check if the tmux session exists."""
result = run_tmux(["has-session", "-t", f"={session_name}"])
return result.returncode == 0
def pick_project(base_dir: Path) -> Optional[str]:
"""Use fzf to pick a subdirectory (project) from base_dir."""
if not base_dir.is_dir():
typer.echo(f"[ta] {base_dir} is not a directory.", err=True)
return None
subdirs = sorted(
[p.name for p in base_dir.iterdir() if p.is_dir()]
)
if not subdirs:
typer.echo(f"[ta] No subdirectories found in {base_dir}.", err=True)
return None
if not shutil.which("fzf"):
typer.echo("[ta] fzf not found in PATH.", err=True)
return None
proc = subprocess.run(
["fzf", "--reverse", f"--header=Select project from {base_dir.name} >"],
input="\n".join(subdirs),
text=True,
capture_output=True,
)
if proc.returncode != 0:
# Cancelled or failed
return None
choice = proc.stdout.strip()
return choice or None
def create_detached_session(
session_name: str,
path_name: Path,
start_mode: bool,
) -> bool:
"""
Create a detached session.
- If start_mode: just a single window.
- Else: split layout with nvim on top.
"""
# Run tmux as if not in tmux (clear TMUX env)
if start_mode:
r = run_tmux(
["new-session", "-Ad", "-s", session_name, "-c", str(path_name)],
clear_tmux_env=True,
)
return r.returncode == 0
else:
r = run_tmux(
["new-session", "-Ad", "-s", session_name, "-c", str(path_name)],
clear_tmux_env=True,
)
if r.returncode != 0:
return False
r = run_tmux(
[
"split-window",
"-vb",
"-t",
session_name,
"-c",
str(path_name),
"-p",
"70",
],
clear_tmux_env=True,
)
if r.returncode != 0:
return False
r = run_tmux(
[
"send-keys",
"-t",
session_name,
"nvim '+Telescope find_files'",
"Enter",
],
clear_tmux_env=True,
)
return r.returncode == 0
def create_if_needed_and_attach(
session_name: str,
path_name: Path,
start_mode: bool,
) -> bool:
"""
Mimic the bash logic:
- If not in tmux: `tmux new-session -As ... -c path_name`
- Else:
- If session doesn't exist: create_detached_session()
- Then `tmux switch-client -t session_name`
"""
if not_in_tmux():
r = run_tmux(
["new-session", "-As", session_name, "-c", str(path_name)],
)
return r.returncode == 0
# Inside tmux
if not session_exists(session_name):
if not create_detached_session(session_name, path_name, start_mode):
return False
r = run_tmux(["switch-client", "-t", session_name])
return r.returncode == 0
def attach_to_first_session() -> None:
"""Fallback: attach to first tmux session and open choose-tree."""
# Attach to the first listed session
list_proc = subprocess.run(
["tmux", "list-sessions", "-F", "#{session_name}"],
text=True,
capture_output=True,
)
if list_proc.returncode != 0 or not list_proc.stdout.strip():
typer.echo("[ta] No tmux sessions found to attach to.", err=True)
raise typer.Exit(1)
first_session = list_proc.stdout.strip().splitlines()[0]
attach_proc = run_tmux(["attach-session", "-t", first_session])
if attach_proc.returncode != 0:
raise typer.Exit(attach_proc.returncode)
# After attach, show choose-tree (this will run in the attached session)
run_tmux(["choose-tree", "-Za"])
tmux_app = typer.Typer(
name="tmux",
help="tmux commands",
add_completion=False,
)
@tmux_app.command('attach')
def attach(
ctx: typer.Context,
workspace: Optional[str] = typer.Option(
None,
"--workspace",
"-w",
help=(
"Workspace directory name to show status for. "
"If omitted, uses the workspace containing the current directory."
),
),
):
"""
Attach or create a session for a repo in a workspace.
"""
# ws_dir = get_workspace_dir(workspace)
_settings, _repos_dir, workspaces_dir = get_ctx_paths(ctx)
ws_dir = find_workspace_dir(workspaces_dir, workspace)
if not ws_dir:
console.print(f"[red]Workspace '{workspace}' does not exist at {ws_dir}[/red]")
raise typer.Exit(1)
# pick repo in workspace
repo = pick_repo_with_iterfzf([dir for dir in ws_dir.iterdir() if dir.is_dir()])
if not repo:
console.print("[red]No repo selected. Exiting.[/red]")
raise typer.Exit(1)
print(ws_dir, repo)
session_name = f'{ws_dir.name}|{repo.name}'
print(f'{ws_dir.name}|{repo.name}')
create_if_needed_and_attach(session_name, repo, False)
app.add_typer(tmux_app)
# @app.command()
# def main(
# dir: Optional[Path] = typer.Argument(
# None,
# help="Base directory containing projects. If omitted, auto-attach or --start behavior.",
# ),
# start: bool = typer.Option(
# False,
# "--start",
# help="Start a new session based on the current directory.",
# ),
# ):
# # Replicate initial "DIR=$1" and attach-or-start logic
# # If no args and no --start
# if dir is None and not start:
# if not_in_tmux():
# # Try to attach to an existing session
# attach_proc = run_tmux(["attach"])
# if attach_proc.returncode == 0:
# # Successfully attached; exit like the bash script
# raise typer.Exit(1)
# # If attach failed, fall through to start mode
# start = True
# else:
# # In tmux and no dir/start: nothing to do
# raise typer.Exit(1)
#
# # Figure out session_name and path_name
# if start:
# path_name = Path.cwd()
# session_name = path_name.name.replace(".", "_")
# else:
# if dir is None:
# typer.echo("[ta] DIR argument is required unless --start is used.", err=True)
# raise typer.Exit(1)
#
# dir = dir.expanduser().resolve()
# project = pick_project(dir)
# if not project:
# # cancelled or error
# raise typer.Exit(1)
#
# session_name = project.replace(".", "_")
# path_name = (dir / project).resolve()
#
# typer.echo(f'session name is "{session_name}"')
# typer.echo(f"path name is {path_name}")
#
# if not session_name:
# raise typer.Exit(1)
#
# # Try main attach/create flow; on failure, fall back
# ok = create_if_needed_and_attach(session_name, path_name, start_mode=start)
# if not ok:
# attach_to_first_session()
#
if __name__ == "__main__": if __name__ == "__main__":
app() app()