gitterApp 0.1.10__tar.gz

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.
Files changed (39) hide show
  1. gitterapp-0.1.10/BusinessLogic/GitManager.py +263 -0
  2. gitterapp-0.1.10/BusinessLogic/toml_helper.py +23 -0
  3. gitterapp-0.1.10/LICENSE.md +23 -0
  4. gitterapp-0.1.10/PKG-INFO +95 -0
  5. gitterapp-0.1.10/README.md +83 -0
  6. gitterapp-0.1.10/TUI/GitFlow/finish_feature.py +54 -0
  7. gitterapp-0.1.10/TUI/GitFlow/finish_release.py +52 -0
  8. gitterapp-0.1.10/TUI/GitFlow/start_feature.py +48 -0
  9. gitterapp-0.1.10/TUI/GitFlow/start_release.py +81 -0
  10. gitterapp-0.1.10/TUI/Help/about_gitter.py +47 -0
  11. gitterapp-0.1.10/TUI/Help/help_menu.py +45 -0
  12. gitterapp-0.1.10/TUI/Help/markdown_viewer.py +34 -0
  13. gitterapp-0.1.10/TUI/Menu/FileMenu.py +34 -0
  14. gitterapp-0.1.10/TUI/Menu/GitMenu.py +46 -0
  15. gitterapp-0.1.10/TUI/Menu/MenuBar.py +14 -0
  16. gitterapp-0.1.10/TUI/Menu/ViewMenu.py +29 -0
  17. gitterapp-0.1.10/TUI/MenuApp.py +424 -0
  18. gitterapp-0.1.10/TUI/commit/git_commit.py +122 -0
  19. gitterapp-0.1.10/TUI/commit/git_staging.py +117 -0
  20. gitterapp-0.1.10/TUI/debug/rich_log.py +105 -0
  21. gitterapp-0.1.10/TUI/project/ProjectView.py +199 -0
  22. gitterapp-0.1.10/TUI/project/ReleaseNotes.py +32 -0
  23. gitterapp-0.1.10/TUI/project/add_or_edit_project.py +154 -0
  24. gitterapp-0.1.10/gitterApp.egg-info/PKG-INFO +95 -0
  25. gitterapp-0.1.10/gitterApp.egg-info/SOURCES.txt +37 -0
  26. gitterapp-0.1.10/gitterApp.egg-info/dependency_links.txt +1 -0
  27. gitterapp-0.1.10/gitterApp.egg-info/entry_points.txt +2 -0
  28. gitterapp-0.1.10/gitterApp.egg-info/requires.txt +3 -0
  29. gitterapp-0.1.10/gitterApp.egg-info/top_level.txt +3 -0
  30. gitterapp-0.1.10/model/GitLog.py +112 -0
  31. gitterapp-0.1.10/model/GitStatus.py +141 -0
  32. gitterapp-0.1.10/model/GitStatusFile.py +11 -0
  33. gitterapp-0.1.10/model/Issue.py +10 -0
  34. gitterapp-0.1.10/model/MainFile.py +59 -0
  35. gitterapp-0.1.10/model/MainFileManager.py +54 -0
  36. gitterapp-0.1.10/model/Project.py +197 -0
  37. gitterapp-0.1.10/model/Release.py +34 -0
  38. gitterapp-0.1.10/pyproject.toml +22 -0
  39. gitterapp-0.1.10/setup.cfg +4 -0
@@ -0,0 +1,263 @@
1
+
2
+ import subprocess
3
+
4
+ from model.GitLog import GitLog
5
+ from model.GitStatus import GitStatus
6
+
7
+
8
+ class GitManager:
9
+ """Runs git CLI commands via subprocess for a given repository path."""
10
+
11
+ def __init__(self, repo_path: str):
12
+ """
13
+ :param repo_path: Absolute path to the git repository.
14
+ """
15
+ self.repo = repo_path
16
+
17
+ def get_status(self):
18
+ """
19
+ Returns the current working tree status as a GitStatus object.
20
+
21
+ :returns: GitStatus with staged, unstaged, and untracked file lists,
22
+ or a string error message if no repository is set.
23
+ """
24
+ if self.repo is None:
25
+ return "No repository initialized"
26
+
27
+ theStatus = subprocess.run(["git", "-C", self.repo, "status"], capture_output=True, text=True)
28
+
29
+ result = GitStatus()
30
+ result.process_status_response(theStatus.stdout)
31
+
32
+ return result
33
+
34
+ def commit(self, message: str, add_unstaged: bool = False) -> tuple:
35
+ """
36
+ Creates a commit with the given message.
37
+
38
+ :param message: The commit message.
39
+ :param add_unstaged: If True, passes -a to also stage tracked modified files.
40
+ :returns: (success, output) tuple where success is True if returncode == 0.
41
+ """
42
+ args = ["git", "-C", self.repo, "commit"]
43
+ if add_unstaged:
44
+ args.append("-a")
45
+ args += ["-m", message]
46
+ result = subprocess.run(args, capture_output=True, text=True)
47
+ success = result.returncode == 0
48
+ output = result.stdout.strip() or result.stderr.strip()
49
+ return success, output
50
+
51
+ def stage(self, filename: str) -> tuple:
52
+ """
53
+ Stages a single file.
54
+
55
+ :param filename: Path to the file to stage, relative to the repository root.
56
+ :returns: (success, output) tuple.
57
+ """
58
+ result = subprocess.run(
59
+ ["git", "-C", self.repo, "add", filename],
60
+ capture_output=True, text=True,
61
+ )
62
+ return result.returncode == 0, result.stdout.strip() or result.stderr.strip()
63
+
64
+ def stage_all(self) -> tuple:
65
+ """
66
+ Stages all changes in the repository (git add -A).
67
+
68
+ :returns: (success, output) tuple.
69
+ """
70
+ result = subprocess.run(
71
+ ["git", "-C", self.repo, "add", "-A"],
72
+ capture_output=True, text=True,
73
+ )
74
+ return result.returncode == 0, result.stdout.strip() or result.stderr.strip()
75
+
76
+ def unstage(self, filename: str) -> tuple:
77
+ """
78
+ Unstages a single file, keeping working tree changes intact.
79
+
80
+ :param filename: Path to the file to unstage, relative to the repository root.
81
+ :returns: (success, output) tuple.
82
+ """
83
+ result = subprocess.run(
84
+ ["git", "-C", self.repo, "restore", "--staged", filename],
85
+ capture_output=True, text=True,
86
+ )
87
+ return result.returncode == 0, result.stdout.strip() or result.stderr.strip()
88
+
89
+ def unstage_all(self) -> tuple:
90
+ """
91
+ Unstages all staged changes, keeping working tree changes intact.
92
+
93
+ :returns: (success, output) tuple.
94
+ """
95
+ result = subprocess.run(
96
+ ["git", "-C", self.repo, "restore", "--staged", "."],
97
+ capture_output=True, text=True,
98
+ )
99
+ return result.returncode == 0, result.stdout.strip() or result.stderr.strip()
100
+
101
+ def push(self, remote: str = "origin", branch: str = "") -> tuple:
102
+ """
103
+ Pushes commits to a remote repository.
104
+
105
+ :param remote: The remote name to push to (default: "origin").
106
+ :param branch: The branch to push. If empty, uses the current tracking branch.
107
+ :returns: (success, output) tuple.
108
+ """
109
+ args = ["git", "-C", self.repo, "push", remote]
110
+ if branch:
111
+ args.append(branch)
112
+ result = subprocess.run(args, capture_output=True, text=True)
113
+ return result.returncode == 0, result.stdout.strip() or result.stderr.strip()
114
+
115
+ def push_main_and_develop(self, remote: str = "origin") -> tuple:
116
+ """
117
+ Pushes both the main/master branch and develop to the remote.
118
+
119
+ :param remote: The remote name to push to (default: "origin").
120
+ :returns: (success, output) tuple.
121
+ """
122
+ main_branch = self._detect_main_branch()
123
+ return self._run_sequence(
124
+ ["push", remote, main_branch],
125
+ ["push", remote, "develop"],
126
+ )
127
+
128
+ def pull(self, remote: str = "origin", branch: str = "") -> tuple:
129
+ """
130
+ Pulls and merges changes from a remote repository.
131
+
132
+ :param remote: The remote name to pull from (default: "origin").
133
+ :param branch: The branch to pull. If empty, uses the current tracking branch.
134
+ :returns: (success, output) tuple.
135
+ """
136
+ args = ["git", "-C", self.repo, "pull", remote]
137
+ if branch:
138
+ args.append(branch)
139
+ result = subprocess.run(args, capture_output=True, text=True)
140
+ return result.returncode == 0, result.stdout.strip() or result.stderr.strip()
141
+
142
+ def fetch(self, remote: str = "origin", prune: bool = False) -> tuple:
143
+ """
144
+ Fetches changes from a remote without merging.
145
+
146
+ :param remote: The remote name to fetch from (default: "origin").
147
+ :param prune: If True, removes remote-tracking branches that no longer exist on the remote.
148
+ :returns: (success, output) tuple.
149
+ """
150
+ args = ["git", "-C", self.repo, "fetch", remote]
151
+ if prune:
152
+ args.append("--prune")
153
+ result = subprocess.run(args, capture_output=True, text=True)
154
+ return result.returncode == 0, result.stdout.strip() or result.stderr.strip()
155
+
156
+ def _run(self, *args) -> tuple:
157
+ """Run a single git command, returning (success, output)."""
158
+ result = subprocess.run(
159
+ ["git", "-C", self.repo] + list(args),
160
+ capture_output=True, text=True,
161
+ )
162
+ return result.returncode == 0, result.stdout.strip() or result.stderr.strip()
163
+
164
+ def _run_sequence(self, *commands) -> tuple:
165
+ """Run a sequence of git command arg-lists, stopping on first failure."""
166
+ output_lines = []
167
+ for args in commands:
168
+ success, output = self._run(*args)
169
+ if output:
170
+ output_lines.append(output)
171
+ if not success:
172
+ return False, "\n".join(output_lines)
173
+ return True, "\n".join(output_lines)
174
+
175
+ def flow_feature_start(self, name: str) -> tuple:
176
+ """
177
+ Starts a feature branch from develop: checkout develop, create feature/<name>.
178
+
179
+ :param name: Feature branch name (without the feature/ prefix).
180
+ :returns: (success, output) tuple.
181
+ """
182
+ safe_name = name.replace(" ", "_")
183
+ return self._run_sequence(
184
+ ["checkout", "develop"],
185
+ ["checkout", "-b", f"feature/{safe_name}"],
186
+ )
187
+
188
+ def flow_feature_finish(self, name: str) -> tuple:
189
+ """
190
+ Finishes a feature branch: merge --no-ff into develop, delete the branch.
191
+
192
+ :param name: Feature branch name (without the feature/ prefix).
193
+ :returns: (success, output) tuple.
194
+ """
195
+ branch = f"feature/{name}"
196
+ return self._run_sequence(
197
+ ["checkout", "develop"],
198
+ ["merge", "--no-ff", branch, "-m", f"Merge feature '{name}' into develop"],
199
+ ["branch", "-d", branch],
200
+ )
201
+
202
+ def flow_release_start(self, version: str) -> tuple:
203
+ """
204
+ Starts a release branch from develop: checkout develop, create release/<version>.
205
+
206
+ :param version: Release version string (e.g. "1.2.0").
207
+ :returns: (success, output) tuple.
208
+ """
209
+ return self._run_sequence(
210
+ ["checkout", "develop"],
211
+ ["checkout", "-b", f"release/{version}"],
212
+ )
213
+
214
+ def _detect_main_branch(self) -> str:
215
+ """Returns 'main' if it exists as a local branch, otherwise 'master'."""
216
+ result = subprocess.run(
217
+ ["git", "-C", self.repo, "show-ref", "--verify", "--quiet", "refs/heads/main"],
218
+ capture_output=True, text=True,
219
+ )
220
+ return "main" if result.returncode == 0 else "master"
221
+
222
+ def flow_release_finish(self, version: str) -> tuple:
223
+ """
224
+ Finishes a release branch: merge into main/master, tag, merge into develop, delete branch, merge main/master back into develop.
225
+
226
+ :param version: Release version string (e.g. "1.2.0").
227
+ :returns: (success, output) tuple.
228
+ """
229
+ main_branch = self._detect_main_branch()
230
+ branch = f"release/{version}"
231
+ return self._run_sequence(
232
+ ["checkout", main_branch],
233
+ ["merge", "--no-ff", branch, "-m", f"Release {version}"],
234
+ ["tag", "-a", version, "-m", f"Release {version}"],
235
+ ["checkout", "develop"],
236
+ ["merge", "--no-ff", branch, "-m", f"Merge release '{version}' into develop"],
237
+ ["branch", "-d", branch],
238
+ ["merge", "--no-ff", main_branch, "-m", f"Merge {main_branch} into develop after release {version}"],
239
+ )
240
+
241
+ def get_logs(self, limit: int = 1000, branch: str = ""):
242
+ """
243
+ Returns parsed git log entries for the repository.
244
+
245
+ :param limit: Maximum number of commits to retrieve (default: 1000).
246
+ :param branch: Branch to retrieve logs for. If empty, uses the current branch.
247
+ :returns: List of parsed GitLog entries.
248
+ """
249
+ # GitterLogger.log( f"**********************************\nGetting logs for branch {branch} at {self.repo}\n**********************************" )
250
+
251
+ if self.repo is None:
252
+ return "No repository initialized"
253
+
254
+ args = ["git", "-C", self.repo, "log", "--decorate", "-n", f"{limit}"]
255
+ if (branch != ""):
256
+ args.append( branch)
257
+
258
+ theLogs = subprocess.run( args, capture_output=True, text=True)
259
+
260
+ # GitterLogger.log( f"Git logs: {theLogs.stdout}" )
261
+ # GitterLogger.log( f"Git error: {theLogs.stderr}" )
262
+
263
+ return GitLog.parse_logstring( theLogs.stdout )
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env python3
2
+
3
+ from pathlib import Path
4
+
5
+ class TomlHelper:
6
+ def __init__(self):
7
+ self.filename = Path(__file__).parent.parent / "pyproject.toml"
8
+
9
+ """Helper class for working with TOML files."""
10
+ def get_version(self) -> str:
11
+ with open(self.filename, "r") as file:
12
+ for line in file:
13
+ strip_line = line.strip()
14
+ if strip_line.startswith("version"):
15
+ return strip_line.split("=")[1].strip()
16
+
17
+ return "ERROR: Version not found in pyproject.toml"
18
+
19
+
20
+ if __name__ == "__main__":
21
+ helper = TomlHelper()
22
+ version = helper.get_version()
23
+ print(f"Project version: {version}")
@@ -0,0 +1,23 @@
1
+
2
+
3
+ # MIT License
4
+
5
+ Copyright 2026 Bobby Skinner
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the “Software”), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ **THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.**
@@ -0,0 +1,95 @@
1
+ Metadata-Version: 2.4
2
+ Name: gitterApp
3
+ Version: 0.1.10
4
+ Summary: Git repo manager get status of multiple git repos in one place
5
+ Requires-Python: >=3.9
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE.md
8
+ Requires-Dist: gitstatus>=1.2
9
+ Requires-Dist: textual>=8.1.1
10
+ Requires-Dist: textual-dev>=1.8.0
11
+ Dynamic: license-file
12
+
13
+ # Gitter
14
+
15
+ A terminal dashboard for monitoring multiple Git repositories. Built with Python and [Textual](https://github.com/Textualize/textual).
16
+
17
+ Gitter shows the status, current release, and upcoming issues for all your configured repos at a glance — and lets you stage files and write commits without leaving the terminal.
18
+
19
+ ---
20
+
21
+ ## Features
22
+
23
+ - **Multi-repo dashboard** — see branch status, release version, and next-release issues for all projects in one view
24
+ - **Release notes** — commits are parsed into tagged releases and issues, displayed as a Markdown panel
25
+ - **Git commit modal** — type/issue/summary fields auto-filled from the branch name, with a staging view for reviewing changes before committing
26
+ - **Stage/unstage files** — double-click files to stage or unstage; one-click stage all / unstage all
27
+ - **CLI interface** — `status` and `issues` commands with Rich-formatted tables for use in scripts or pipelines
28
+ - **Auto-refresh** — repos are polled every 90 seconds in the background
29
+
30
+ ## Requirements
31
+
32
+ - Python 3.9+
33
+ - Git installed and available on `PATH`
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ git clone https://github.com/bobbyskinnerart/Gitter.git
39
+ cd Gitter
40
+ python -m venv .venv
41
+ source .venv/bin/activate # Windows: .venv\Scripts\activate
42
+ pip install -r requirements.txt
43
+ ```
44
+
45
+ ## Usage
46
+
47
+ ### TUI (interactive dashboard)
48
+
49
+ ```bash
50
+ python main.py tui
51
+ ```
52
+
53
+ ### CLI commands
54
+
55
+ ```bash
56
+ python main.py status # Table of all projects with status and release info
57
+ python main.py issues # All issues grouped by project and release
58
+ python main.py issues -p <project> # Filter by project name
59
+ python main.py issues -r <release> # Filter by release tag
60
+ python main.py version # Print version
61
+ ```
62
+
63
+ ### Key bindings (TUI)
64
+
65
+ | Key | Action |
66
+ |-----|--------|
67
+ | `Ctrl+K` | Commit selected project |
68
+ | `Ctrl+A` | Add project |
69
+ | `Ctrl+E` | Edit selected project |
70
+ | `Ctrl+D` | Delete selected project |
71
+ | `Ctrl+R` | Toggle release notes panel |
72
+ | `Ctrl+L` | Toggle log panel |
73
+ | `Ctrl+Q` | Quit |
74
+
75
+ Double-click a project row to edit it.
76
+
77
+ ## Configuration
78
+
79
+ Projects are stored in `~/.gitter` as JSON. Add a repo via `Ctrl+A` in the TUI or by editing the file directly.
80
+
81
+ Each project entry supports:
82
+
83
+ | Field | Description |
84
+ |-------|-------------|
85
+ | `name` | Display name |
86
+ | `directory` | Absolute path to the git repo |
87
+ | `tagBranch` | Branch where release tags live (e.g. `main`) |
88
+ | `issuePrefixes` | Issue prefixes to detect in commit messages (e.g. `GIT`, `PROJ`) |
89
+ | `prPatterns` | PR number patterns (regex) |
90
+ | `groups` | Optional grouping labels |
91
+ | `favorite` | Mark as favorite |
92
+
93
+ ## License
94
+
95
+ MIT — see [LICENSE.md](LICENSE.md) for details.
@@ -0,0 +1,83 @@
1
+ # Gitter
2
+
3
+ A terminal dashboard for monitoring multiple Git repositories. Built with Python and [Textual](https://github.com/Textualize/textual).
4
+
5
+ Gitter shows the status, current release, and upcoming issues for all your configured repos at a glance — and lets you stage files and write commits without leaving the terminal.
6
+
7
+ ---
8
+
9
+ ## Features
10
+
11
+ - **Multi-repo dashboard** — see branch status, release version, and next-release issues for all projects in one view
12
+ - **Release notes** — commits are parsed into tagged releases and issues, displayed as a Markdown panel
13
+ - **Git commit modal** — type/issue/summary fields auto-filled from the branch name, with a staging view for reviewing changes before committing
14
+ - **Stage/unstage files** — double-click files to stage or unstage; one-click stage all / unstage all
15
+ - **CLI interface** — `status` and `issues` commands with Rich-formatted tables for use in scripts or pipelines
16
+ - **Auto-refresh** — repos are polled every 90 seconds in the background
17
+
18
+ ## Requirements
19
+
20
+ - Python 3.9+
21
+ - Git installed and available on `PATH`
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ git clone https://github.com/bobbyskinnerart/Gitter.git
27
+ cd Gitter
28
+ python -m venv .venv
29
+ source .venv/bin/activate # Windows: .venv\Scripts\activate
30
+ pip install -r requirements.txt
31
+ ```
32
+
33
+ ## Usage
34
+
35
+ ### TUI (interactive dashboard)
36
+
37
+ ```bash
38
+ python main.py tui
39
+ ```
40
+
41
+ ### CLI commands
42
+
43
+ ```bash
44
+ python main.py status # Table of all projects with status and release info
45
+ python main.py issues # All issues grouped by project and release
46
+ python main.py issues -p <project> # Filter by project name
47
+ python main.py issues -r <release> # Filter by release tag
48
+ python main.py version # Print version
49
+ ```
50
+
51
+ ### Key bindings (TUI)
52
+
53
+ | Key | Action |
54
+ |-----|--------|
55
+ | `Ctrl+K` | Commit selected project |
56
+ | `Ctrl+A` | Add project |
57
+ | `Ctrl+E` | Edit selected project |
58
+ | `Ctrl+D` | Delete selected project |
59
+ | `Ctrl+R` | Toggle release notes panel |
60
+ | `Ctrl+L` | Toggle log panel |
61
+ | `Ctrl+Q` | Quit |
62
+
63
+ Double-click a project row to edit it.
64
+
65
+ ## Configuration
66
+
67
+ Projects are stored in `~/.gitter` as JSON. Add a repo via `Ctrl+A` in the TUI or by editing the file directly.
68
+
69
+ Each project entry supports:
70
+
71
+ | Field | Description |
72
+ |-------|-------------|
73
+ | `name` | Display name |
74
+ | `directory` | Absolute path to the git repo |
75
+ | `tagBranch` | Branch where release tags live (e.g. `main`) |
76
+ | `issuePrefixes` | Issue prefixes to detect in commit messages (e.g. `GIT`, `PROJ`) |
77
+ | `prPatterns` | PR number patterns (regex) |
78
+ | `groups` | Optional grouping labels |
79
+ | `favorite` | Mark as favorite |
80
+
81
+ ## License
82
+
83
+ MIT — see [LICENSE.md](LICENSE.md) for details.
@@ -0,0 +1,54 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+
5
+ from textual import on
6
+ from textual.app import ComposeResult
7
+ from textual.screen import ModalScreen
8
+ from textual.widgets import Button, Label
9
+ from textual.containers import Horizontal, Vertical
10
+
11
+ from model.Project import Project
12
+
13
+
14
+ class FinishFeatureModal(ModalScreen[bool]):
15
+ """Modal confirming finish of the current git-flow feature branch.
16
+
17
+ Dismisses with True on Finish, or False on Cancel.
18
+ """
19
+
20
+ def __init__(self, project: Project):
21
+ super().__init__()
22
+ self._project = project
23
+ branch = project.status.branch if project.status else ""
24
+ self._feature_name = branch.split("/", 1)[1] if "/" in branch else branch
25
+
26
+ def compose(self) -> ComposeResult:
27
+ yield Vertical(
28
+ Label("Finish Feature", id="finish_feature_title"),
29
+ Label(
30
+ f"Do you want to finish the feature: [bold cyan]{self._feature_name}[/bold cyan]?",
31
+ id="finish_feature_message",
32
+ ),
33
+ Horizontal(
34
+ Button("Cancel", variant="default", id="btn_cancel"),
35
+ Button("Finish", variant="primary", id="btn_finish"),
36
+ id="finish_feature_buttons",
37
+ ),
38
+ id="finish_feature_container",
39
+ )
40
+
41
+ def on_mount(self) -> None:
42
+ self.query_one("#btn_finish", Button).focus()
43
+
44
+ @on(Button.Pressed, "#btn_cancel")
45
+ def handle_cancel(self) -> None:
46
+ self.dismiss(False)
47
+
48
+ @on(Button.Pressed, "#btn_finish")
49
+ def handle_finish(self) -> None:
50
+ self.dismiss(True)
51
+
52
+ @property
53
+ def feature_name(self) -> str:
54
+ return self._feature_name
@@ -0,0 +1,52 @@
1
+ from __future__ import annotations
2
+
3
+ from textual import on
4
+ from textual.app import ComposeResult
5
+ from textual.screen import ModalScreen
6
+ from textual.widgets import Button, Label
7
+ from textual.containers import Horizontal, Vertical
8
+
9
+ from model.Project import Project
10
+
11
+
12
+ class FinishReleaseModal(ModalScreen[bool]):
13
+ """Modal confirming finish of the current git-flow release branch.
14
+
15
+ Dismisses with True on Finish, or False on Cancel.
16
+ """
17
+
18
+ def __init__(self, project: Project):
19
+ super().__init__()
20
+ self._project = project
21
+ branch = project.status.branch if project.status else ""
22
+ self._version = branch.split("/", 1)[1] if "/" in branch else branch
23
+
24
+ def compose(self) -> ComposeResult:
25
+ yield Vertical(
26
+ Label("Finish Release", id="finish_release_title"),
27
+ Label(
28
+ f"Do you want to finish release: [bold cyan]{self._version}[/bold cyan]?",
29
+ id="finish_release_message",
30
+ ),
31
+ Horizontal(
32
+ Button("Cancel", variant="default", id="btn_cancel"),
33
+ Button("Finish", variant="primary", id="btn_finish"),
34
+ id="finish_release_buttons",
35
+ ),
36
+ id="finish_release_container",
37
+ )
38
+
39
+ def on_mount(self) -> None:
40
+ self.query_one("#btn_finish", Button).focus()
41
+
42
+ @on(Button.Pressed, "#btn_cancel")
43
+ def handle_cancel(self) -> None:
44
+ self.dismiss(False)
45
+
46
+ @on(Button.Pressed, "#btn_finish")
47
+ def handle_finish(self) -> None:
48
+ self.dismiss(True)
49
+
50
+ @property
51
+ def version(self) -> str:
52
+ return self._version
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+
5
+ from textual import on
6
+ from textual.app import ComposeResult
7
+ from textual.screen import ModalScreen
8
+ from textual.widgets import Button, Input, Label
9
+ from textual.containers import Horizontal, Vertical
10
+
11
+ from model.Project import Project
12
+
13
+
14
+ class StartFeatureModal(ModalScreen[Optional[str]]):
15
+ """Modal for starting a new git-flow feature branch.
16
+
17
+ Dismisses with the feature name string on Start, or None on Cancel.
18
+ """
19
+
20
+ def __init__(self, project: Project):
21
+ super().__init__()
22
+ self._project = project
23
+
24
+ def compose(self) -> ComposeResult:
25
+ yield Vertical(
26
+ Label(f"Start Feature — {self._project.name}", id="start_feature_title"),
27
+ Label("Feature name", classes="start_feature_label"),
28
+ Input(placeholder="my-feature", id="feature_name"),
29
+ Horizontal(
30
+ Button("Cancel", variant="default", id="btn_cancel"),
31
+ Button("Start", variant="primary", id="btn_start"),
32
+ id="start_feature_buttons",
33
+ ),
34
+ id="start_feature_container",
35
+ )
36
+
37
+ def on_mount(self) -> None:
38
+ self.query_one("#feature_name", Input).focus()
39
+
40
+ @on(Button.Pressed, "#btn_cancel")
41
+ def handle_cancel(self) -> None:
42
+ self.dismiss(None)
43
+
44
+ @on(Button.Pressed, "#btn_start")
45
+ def handle_start(self) -> None:
46
+ name = self.query_one("#feature_name", Input).value.strip()
47
+ if name:
48
+ self.dismiss(name)