From b75cb802cc2cc7a52cfad0aa95830d8362402010 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 7 Jun 2025 05:38:44 -0500 Subject: [PATCH 1/3] types(workspace-imports) Example typings via `NotRequried` --- src/tmuxp/_internal/types.py | 60 ++++++++++++++++++++++++++++++++ src/tmuxp/workspace/importers.py | 55 ++++++++++++++++------------- 2 files changed, 91 insertions(+), 24 deletions(-) diff --git a/src/tmuxp/_internal/types.py b/src/tmuxp/_internal/types.py index a3521f5832..16239ae7dd 100644 --- a/src/tmuxp/_internal/types.py +++ b/src/tmuxp/_internal/types.py @@ -35,3 +35,63 @@ class PluginConfigSchema(TypedDict): tmuxp_min_version: NotRequired[str] tmuxp_max_version: NotRequired[str] tmuxp_version_incompatible: NotRequired[list[str]] + + +class ShellCommandConfig(TypedDict): + """Shell command configuration.""" + + cmd: str + enter: NotRequired[bool] + suppress_history: NotRequired[bool] + + +ShellCommandValue = t.Union[ + str, ShellCommandConfig, list[t.Union[str, ShellCommandConfig]] +] + + +class PaneConfig(TypedDict, total=False): + """Pane configuration.""" + + shell_command: NotRequired[ShellCommandValue] + shell_command_before: NotRequired[ShellCommandValue] + start_directory: NotRequired[str] + environment: NotRequired[dict[str, str]] + focus: NotRequired[str | bool] + suppress_history: NotRequired[bool] + + +PaneValue = t.Union[str, PaneConfig] + + +class WindowConfig(TypedDict, total=False): + """Window configuration.""" + + window_name: str + start_directory: NotRequired[str] + shell_command_before: NotRequired[ShellCommandValue] + layout: NotRequired[str] + options: NotRequired[dict[str, t.Any]] + options_after: NotRequired[dict[str, t.Any]] + environment: NotRequired[dict[str, str]] + focus: NotRequired[str | bool] + suppress_history: NotRequired[bool] + panes: NotRequired[list[PaneValue]] + + +class WorkspaceConfig(TypedDict, total=False): + """Complete tmuxp workspace configuration.""" + + session_name: str | None # Can be None during import + start_directory: NotRequired[str] + before_script: NotRequired[str] + shell_command_before: NotRequired[ShellCommandValue] + shell_command: NotRequired[ShellCommandValue] # Used in import + environment: NotRequired[dict[str, str]] + global_options: NotRequired[dict[str, t.Any]] + options: NotRequired[dict[str, t.Any]] + config: NotRequired[str] # tmux config file path + socket_name: NotRequired[str] # tmux socket name + plugins: NotRequired[list[str | PluginConfigSchema]] + suppress_history: NotRequired[bool] + windows: list[WindowConfig] diff --git a/src/tmuxp/workspace/importers.py b/src/tmuxp/workspace/importers.py index fda361ca6f..462507a559 100644 --- a/src/tmuxp/workspace/importers.py +++ b/src/tmuxp/workspace/importers.py @@ -4,8 +4,11 @@ import typing as t +if t.TYPE_CHECKING: + from tmuxp._internal.types import WindowConfig, WorkspaceConfig -def import_tmuxinator(workspace_dict: dict[str, t.Any]) -> dict[str, t.Any]: + +def import_tmuxinator(workspace_dict: dict[str, t.Any]) -> WorkspaceConfig: """Return tmuxp workspace from a `tmuxinator`_ yaml workspace. .. _tmuxinator: https://github.com/aziz/tmuxinator @@ -18,8 +21,9 @@ def import_tmuxinator(workspace_dict: dict[str, t.Any]) -> dict[str, t.Any]: Returns ------- dict + A dictionary conforming to WorkspaceConfig structure. """ - tmuxp_workspace: dict[str, t.Any] = {} + tmuxp_workspace: WorkspaceConfig = {"windows": []} # type: ignore[typeddict-item] if "project_name" in workspace_dict: tmuxp_workspace["session_name"] = workspace_dict.pop("project_name") @@ -51,7 +55,6 @@ def import_tmuxinator(workspace_dict: dict[str, t.Any]) -> dict[str, t.Any]: if "socket_name" in workspace_dict: tmuxp_workspace["socket_name"] = workspace_dict["socket_name"] - tmuxp_workspace["windows"] = [] if "tabs" in workspace_dict: workspace_dict["windows"] = workspace_dict.pop("tabs") @@ -76,33 +79,33 @@ def import_tmuxinator(workspace_dict: dict[str, t.Any]) -> dict[str, t.Any]: "rbenv shell {}".format(workspace_dict["rbenv"]), ) - for window_dict in workspace_dict["windows"]: - for k, v in window_dict.items(): - window_dict = {"window_name": k} + for window_item in workspace_dict["windows"]: + for k, v in window_item.items(): + new_window: WindowConfig = {"window_name": k} if isinstance(v, str) or v is None: - window_dict["panes"] = [v] - tmuxp_workspace["windows"].append(window_dict) + new_window["panes"] = [v] + tmuxp_workspace["windows"].append(new_window) continue if isinstance(v, list): - window_dict["panes"] = v - tmuxp_workspace["windows"].append(window_dict) + new_window["panes"] = v + tmuxp_workspace["windows"].append(new_window) continue if "pre" in v: - window_dict["shell_command_before"] = v["pre"] + new_window["shell_command_before"] = v["pre"] if "panes" in v: - window_dict["panes"] = v["panes"] + new_window["panes"] = v["panes"] if "root" in v: - window_dict["start_directory"] = v["root"] + new_window["start_directory"] = v["root"] if "layout" in v: - window_dict["layout"] = v["layout"] - tmuxp_workspace["windows"].append(window_dict) + new_window["layout"] = v["layout"] + tmuxp_workspace["windows"].append(new_window) return tmuxp_workspace -def import_teamocil(workspace_dict: dict[str, t.Any]) -> dict[str, t.Any]: +def import_teamocil(workspace_dict: dict[str, t.Any]) -> WorkspaceConfig: """Return tmuxp workspace from a `teamocil`_ yaml workspace. .. _teamocil: https://github.com/remiprev/teamocil @@ -112,6 +115,11 @@ def import_teamocil(workspace_dict: dict[str, t.Any]) -> dict[str, t.Any]: workspace_dict : dict python dict for tmuxp workspace + Returns + ------- + dict + A dictionary conforming to WorkspaceConfig structure. + Notes ----- Todos: @@ -122,7 +130,7 @@ def import_teamocil(workspace_dict: dict[str, t.Any]) -> dict[str, t.Any]: - clear - cmd_separator """ - tmuxp_workspace: dict[str, t.Any] = {} + tmuxp_workspace: WorkspaceConfig = {"windows": []} # type: ignore[typeddict-item] if "session" in workspace_dict: workspace_dict = workspace_dict["session"] @@ -132,21 +140,20 @@ def import_teamocil(workspace_dict: dict[str, t.Any]) -> dict[str, t.Any]: if "root" in workspace_dict: tmuxp_workspace["start_directory"] = workspace_dict.pop("root") - tmuxp_workspace["windows"] = [] for w in workspace_dict["windows"]: - window_dict = {"window_name": w["name"]} + window_dict: WindowConfig = {"window_name": w["name"]} if "clear" in w: - window_dict["clear"] = w["clear"] + # TODO: handle clear attribute + pass if "filters" in w: if "before" in w["filters"]: - for _b in w["filters"]["before"]: - window_dict["shell_command_before"] = w["filters"]["before"] + window_dict["shell_command_before"] = w["filters"]["before"] if "after" in w["filters"]: - for _b in w["filters"]["after"]: - window_dict["shell_command_after"] = w["filters"]["after"] + # TODO: handle shell_command_after + pass if "root" in w: window_dict["start_directory"] = w.pop("root") From af667652975417fde052e8be31728bc8531af00c Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 7 Jun 2025 05:44:05 -0500 Subject: [PATCH 2/3] !squash more --- src/tmuxp/cli/import_config.py | 2 ++ src/tmuxp/workspace/importers.py | 19 ++++++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/tmuxp/cli/import_config.py b/src/tmuxp/cli/import_config.py index a014e1558e..eaa346ca0d 100644 --- a/src/tmuxp/cli/import_config.py +++ b/src/tmuxp/cli/import_config.py @@ -17,6 +17,8 @@ if t.TYPE_CHECKING: import argparse + from tmuxp._internal.types import WorkspaceConfig + def get_tmuxinator_dir() -> pathlib.Path: """Return tmuxinator configuration directory. diff --git a/src/tmuxp/workspace/importers.py b/src/tmuxp/workspace/importers.py index 462507a559..653480deaa 100644 --- a/src/tmuxp/workspace/importers.py +++ b/src/tmuxp/workspace/importers.py @@ -23,7 +23,7 @@ def import_tmuxinator(workspace_dict: dict[str, t.Any]) -> WorkspaceConfig: dict A dictionary conforming to WorkspaceConfig structure. """ - tmuxp_workspace: WorkspaceConfig = {"windows": []} # type: ignore[typeddict-item] + tmuxp_workspace: WorkspaceConfig = {"session_name": None, "windows": []} if "project_name" in workspace_dict: tmuxp_workspace["session_name"] = workspace_dict.pop("project_name") @@ -75,6 +75,15 @@ def import_tmuxinator(workspace_dict: dict[str, t.Any]) -> WorkspaceConfig: if "rbenv" in workspace_dict: if "shell_command_before" not in tmuxp_workspace: tmuxp_workspace["shell_command_before"] = [] + else: + # Ensure shell_command_before is a list + current = tmuxp_workspace["shell_command_before"] + if isinstance(current, str): + tmuxp_workspace["shell_command_before"] = [current] + elif isinstance(current, dict): + tmuxp_workspace["shell_command_before"] = [current] + # Now we can safely append + assert isinstance(tmuxp_workspace["shell_command_before"], list) tmuxp_workspace["shell_command_before"].append( "rbenv shell {}".format(workspace_dict["rbenv"]), ) @@ -83,10 +92,14 @@ def import_tmuxinator(workspace_dict: dict[str, t.Any]) -> WorkspaceConfig: for k, v in window_item.items(): new_window: WindowConfig = {"window_name": k} - if isinstance(v, str) or v is None: + if isinstance(v, str): new_window["panes"] = [v] tmuxp_workspace["windows"].append(new_window) continue + if v is None: + new_window["panes"] = [""] # Empty pane + tmuxp_workspace["windows"].append(new_window) + continue if isinstance(v, list): new_window["panes"] = v tmuxp_workspace["windows"].append(new_window) @@ -130,7 +143,7 @@ def import_teamocil(workspace_dict: dict[str, t.Any]) -> WorkspaceConfig: - clear - cmd_separator """ - tmuxp_workspace: WorkspaceConfig = {"windows": []} # type: ignore[typeddict-item] + tmuxp_workspace: WorkspaceConfig = {"session_name": None, "windows": []} if "session" in workspace_dict: workspace_dict = workspace_dict["session"] From a4a60104970bfe41296fadc3ba19ed443ca7b1d3 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 7 Jun 2025 05:47:59 -0500 Subject: [PATCH 3/3] !squash wip --- src/tmuxp/_internal/types.py | 5 ++++- src/tmuxp/cli/import_config.py | 6 ++++-- src/tmuxp/workspace/importers.py | 14 +++----------- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/tmuxp/_internal/types.py b/src/tmuxp/_internal/types.py index 16239ae7dd..7565ebdce6 100644 --- a/src/tmuxp/_internal/types.py +++ b/src/tmuxp/_internal/types.py @@ -59,9 +59,10 @@ class PaneConfig(TypedDict, total=False): environment: NotRequired[dict[str, str]] focus: NotRequired[str | bool] suppress_history: NotRequired[bool] + target: NotRequired[str] -PaneValue = t.Union[str, PaneConfig] +PaneValue = t.Union[str, PaneConfig, None] class WindowConfig(TypedDict, total=False): @@ -70,7 +71,9 @@ class WindowConfig(TypedDict, total=False): window_name: str start_directory: NotRequired[str] shell_command_before: NotRequired[ShellCommandValue] + shell_command_after: NotRequired[ShellCommandValue] layout: NotRequired[str] + clear: NotRequired[bool] options: NotRequired[dict[str, t.Any]] options_after: NotRequired[dict[str, t.Any]] environment: NotRequired[dict[str, str]] diff --git a/src/tmuxp/cli/import_config.py b/src/tmuxp/cli/import_config.py index eaa346ca0d..5e0210729c 100644 --- a/src/tmuxp/cli/import_config.py +++ b/src/tmuxp/cli/import_config.py @@ -134,7 +134,7 @@ def create_import_subparser( class ImportConfigFn(t.Protocol): """Typing for import configuration callback function.""" - def __call__(self, workspace_dict: dict[str, t.Any]) -> dict[str, t.Any]: + def __call__(self, workspace_dict: dict[str, t.Any]) -> WorkspaceConfig: """Execute tmuxp import function.""" ... @@ -146,7 +146,9 @@ def import_config( ) -> None: """Import a configuration from a workspace_file.""" existing_workspace_file = ConfigReader._from_file(pathlib.Path(workspace_file)) - cfg_reader = ConfigReader(importfunc(existing_workspace_file)) + cfg_reader = ConfigReader( + t.cast(dict[t.Any, t.Any], importfunc(existing_workspace_file)) + ) workspace_file_format = prompt_choices( "Convert to", diff --git a/src/tmuxp/workspace/importers.py b/src/tmuxp/workspace/importers.py index 653480deaa..967f7a44f9 100644 --- a/src/tmuxp/workspace/importers.py +++ b/src/tmuxp/workspace/importers.py @@ -55,7 +55,6 @@ def import_tmuxinator(workspace_dict: dict[str, t.Any]) -> WorkspaceConfig: if "socket_name" in workspace_dict: tmuxp_workspace["socket_name"] = workspace_dict["socket_name"] - if "tabs" in workspace_dict: workspace_dict["windows"] = workspace_dict.pop("tabs") @@ -92,14 +91,10 @@ def import_tmuxinator(workspace_dict: dict[str, t.Any]) -> WorkspaceConfig: for k, v in window_item.items(): new_window: WindowConfig = {"window_name": k} - if isinstance(v, str): + if isinstance(v, str) or v is None: new_window["panes"] = [v] tmuxp_workspace["windows"].append(new_window) continue - if v is None: - new_window["panes"] = [""] # Empty pane - tmuxp_workspace["windows"].append(new_window) - continue if isinstance(v, list): new_window["panes"] = v tmuxp_workspace["windows"].append(new_window) @@ -153,20 +148,17 @@ def import_teamocil(workspace_dict: dict[str, t.Any]) -> WorkspaceConfig: if "root" in workspace_dict: tmuxp_workspace["start_directory"] = workspace_dict.pop("root") - for w in workspace_dict["windows"]: window_dict: WindowConfig = {"window_name": w["name"]} if "clear" in w: - # TODO: handle clear attribute - pass + window_dict["clear"] = w["clear"] if "filters" in w: if "before" in w["filters"]: window_dict["shell_command_before"] = w["filters"]["before"] if "after" in w["filters"]: - # TODO: handle shell_command_after - pass + window_dict["shell_command_after"] = w["filters"]["after"] if "root" in w: window_dict["start_directory"] = w.pop("root")