forestui 0.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
forestui/cli.py ADDED
@@ -0,0 +1,169 @@
1
+ """Command-line interface for forestui."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import shutil
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ import click
11
+
12
+ from forestui import __version__
13
+
14
+
15
+ def get_window_name(dev_mode: bool = False) -> str:
16
+ """Get the tmux window name based on dev mode flag."""
17
+ if dev_mode:
18
+ from datetime import datetime
19
+
20
+ hhmm = datetime.now().strftime("%H%M")
21
+ return f"forestui-dev-{hhmm}"
22
+ return "forestui"
23
+
24
+
25
+ def rename_tmux_window(name: str) -> None:
26
+ """Rename the current tmux window."""
27
+ from forestui.services.tmux import get_tmux_service
28
+
29
+ get_tmux_service().rename_window(name)
30
+
31
+
32
+ def slugify(text: str) -> str:
33
+ """Convert text to a safe slug for tmux session names."""
34
+ import re
35
+
36
+ # Convert to lowercase and replace spaces/special chars with dashes
37
+ slug = re.sub(r"[^a-zA-Z0-9]+", "-", text.lower())
38
+ # Remove leading/trailing dashes
39
+ return slug.strip("-")
40
+
41
+
42
+ def ensure_tmux(
43
+ forest_path: str | None,
44
+ debug_mode: bool = False,
45
+ no_self_update: bool = False,
46
+ dev_mode: bool = False,
47
+ ) -> None:
48
+ """Ensure forestui is running inside tmux, or exec into tmux."""
49
+ # Already inside tmux - good to go
50
+ if os.environ.get("TMUX"):
51
+ return
52
+
53
+ # Check if tmux is available
54
+ tmux_path = shutil.which("tmux")
55
+ if not tmux_path:
56
+ click.echo("Error: forestui requires tmux to be installed.", err=True)
57
+ click.echo("", err=True)
58
+ click.echo("Install tmux:", err=True)
59
+ click.echo(" macOS: brew install tmux", err=True)
60
+ click.echo(" Ubuntu: sudo apt install tmux", err=True)
61
+ click.echo(" Fedora: sudo dnf install tmux", err=True)
62
+ sys.exit(1)
63
+
64
+ # Determine forest folder name for session naming
65
+ if forest_path:
66
+ forest_folder = Path(forest_path).expanduser().resolve().name
67
+ else:
68
+ forest_folder = "forest" # default ~/forest
69
+
70
+ session_name = f"forestui-{slugify(forest_folder)}"
71
+
72
+ # Build the forestui command with arguments
73
+ forestui_cmd = "forestui"
74
+ if debug_mode:
75
+ forestui_cmd += " --debug"
76
+ if no_self_update:
77
+ forestui_cmd += " --no-self-update"
78
+ if dev_mode:
79
+ forestui_cmd += " --dev"
80
+ if forest_path:
81
+ forestui_cmd += f" {forest_path}"
82
+
83
+ # Check if session already exists
84
+ import subprocess
85
+
86
+ result = subprocess.run(
87
+ ["tmux", "has-session", "-t", session_name],
88
+ capture_output=True,
89
+ )
90
+ session_exists = result.returncode == 0
91
+
92
+ if session_exists:
93
+ # Session exists: check if forestui window is already running
94
+ result = subprocess.run(
95
+ ["tmux", "select-window", "-t", f"{session_name}:forestui"],
96
+ capture_output=True,
97
+ )
98
+ forestui_window_exists = result.returncode == 0
99
+
100
+ if not forestui_window_exists:
101
+ # forestui was killed but session remains - create new window
102
+ subprocess.run(
103
+ [
104
+ "tmux",
105
+ "new-window",
106
+ "-t",
107
+ session_name,
108
+ "-n",
109
+ "forestui",
110
+ forestui_cmd,
111
+ ],
112
+ )
113
+
114
+ os.execvp("tmux", ["tmux", "attach-session", "-t", session_name])
115
+ else:
116
+ # No session: create new one with forestui as initial command
117
+ os.execvp("tmux", ["tmux", "new-session", "-s", session_name, forestui_cmd])
118
+
119
+
120
+ @click.command()
121
+ @click.argument("forest_path", required=False, default=None)
122
+ @click.option(
123
+ "--no-self-update",
124
+ "no_self_update",
125
+ is_flag=True,
126
+ help="Disable automatic updates on startup",
127
+ )
128
+ @click.option(
129
+ "--debug",
130
+ "debug_mode",
131
+ is_flag=True,
132
+ help="Run with Textual devtools enabled",
133
+ )
134
+ @click.option(
135
+ "--dev",
136
+ "dev_mode",
137
+ is_flag=True,
138
+ help="Dev mode: use timestamped window name (forestui-dev-HHMM)",
139
+ )
140
+ @click.version_option(version=__version__, prog_name="forestui")
141
+ def main(
142
+ forest_path: str | None, no_self_update: bool, debug_mode: bool, dev_mode: bool
143
+ ) -> None:
144
+ """forestui - Git Worktree Manager
145
+
146
+ A terminal UI for managing Git worktrees, inspired by forest for macOS.
147
+
148
+ FOREST_PATH: Optional path to forest directory (default: ~/forest)
149
+ """
150
+ ensure_tmux(forest_path, debug_mode, no_self_update, dev_mode)
151
+
152
+ if debug_mode:
153
+ os.environ["TEXTUAL"] = "devtools"
154
+
155
+ if no_self_update:
156
+ os.environ["FORESTUI_NO_AUTO_UPDATE"] = "1"
157
+
158
+ rename_tmux_window(get_window_name(dev_mode))
159
+
160
+ # Import here to avoid circular imports and speed up --help/--version
161
+ from forestui.app import run_app
162
+ from forestui.services.settings import set_forest_path
163
+
164
+ set_forest_path(forest_path)
165
+ run_app()
166
+
167
+
168
+ if __name__ == "__main__":
169
+ main()
@@ -0,0 +1,21 @@
1
+ """UI Components for forestui."""
2
+
3
+ from forestui.components.modals import (
4
+ AddRepositoryModal,
5
+ AddWorktreeModal,
6
+ ConfirmDeleteModal,
7
+ SettingsModal,
8
+ )
9
+ from forestui.components.repository_detail import RepositoryDetail
10
+ from forestui.components.sidebar import Sidebar
11
+ from forestui.components.worktree_detail import WorktreeDetail
12
+
13
+ __all__ = [
14
+ "AddRepositoryModal",
15
+ "AddWorktreeModal",
16
+ "ConfirmDeleteModal",
17
+ "RepositoryDetail",
18
+ "SettingsModal",
19
+ "Sidebar",
20
+ "WorktreeDetail",
21
+ ]
@@ -0,0 +1,76 @@
1
+ """Shared message classes for detail view components."""
2
+
3
+ from uuid import UUID
4
+
5
+ from textual.message import Message
6
+
7
+
8
+ class OpenInEditor(Message):
9
+ """Request to open a path in editor."""
10
+
11
+ def __init__(self, path: str) -> None:
12
+ self.path = path
13
+ super().__init__()
14
+
15
+
16
+ class OpenInTerminal(Message):
17
+ """Request to open a path in terminal."""
18
+
19
+ def __init__(self, path: str) -> None:
20
+ self.path = path
21
+ super().__init__()
22
+
23
+
24
+ class OpenInFileManager(Message):
25
+ """Request to open a path in file manager."""
26
+
27
+ def __init__(self, path: str) -> None:
28
+ self.path = path
29
+ super().__init__()
30
+
31
+
32
+ class StartClaudeSession(Message):
33
+ """Request to start a new Claude session."""
34
+
35
+ def __init__(self, path: str) -> None:
36
+ self.path = path
37
+ super().__init__()
38
+
39
+
40
+ class StartClaudeYoloSession(Message):
41
+ """Request to start a Claude YOLO session."""
42
+
43
+ def __init__(self, path: str) -> None:
44
+ self.path = path
45
+ super().__init__()
46
+
47
+
48
+ class ContinueClaudeSession(Message):
49
+ """Request to continue an existing Claude session."""
50
+
51
+ def __init__(self, session_id: str, path: str) -> None:
52
+ self.session_id = session_id
53
+ self.path = path
54
+ super().__init__()
55
+
56
+
57
+ class ContinueClaudeYoloSession(Message):
58
+ """Request to continue an existing Claude session in YOLO mode."""
59
+
60
+ def __init__(self, session_id: str, path: str) -> None:
61
+ self.session_id = session_id
62
+ self.path = path
63
+ super().__init__()
64
+
65
+
66
+ class ConfigureClaudeCommand(Message):
67
+ """Request to configure custom Claude command for a repository or worktree."""
68
+
69
+ def __init__(
70
+ self,
71
+ repo_id: UUID,
72
+ worktree_id: UUID | None = None,
73
+ ) -> None:
74
+ self.repo_id = repo_id
75
+ self.worktree_id = worktree_id
76
+ super().__init__()