flutter-dev 0.1.4__tar.gz → 0.1.5__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.
- {flutter_dev-0.1.4 → flutter_dev-0.1.5}/CHANGELOG.md +14 -0
- {flutter_dev-0.1.4 → flutter_dev-0.1.5}/PKG-INFO +17 -3
- {flutter_dev-0.1.4 → flutter_dev-0.1.5}/README.md +15 -2
- {flutter_dev-0.1.4 → flutter_dev-0.1.5}/fdev.py +14 -5
- {flutter_dev-0.1.4 → flutter_dev-0.1.5}/flutter_dev.egg-info/SOURCES.txt +3 -0
- flutter_dev-0.1.5/managers/gui.py +531 -0
- {flutter_dev-0.1.4 → flutter_dev-0.1.5}/pyproject.toml +2 -1
- flutter_dev-0.1.5/tests/test_fdev_cli.py +43 -0
- flutter_dev-0.1.5/tests/test_gui.py +145 -0
- {flutter_dev-0.1.4 → flutter_dev-0.1.5}/.env.example +0 -0
- {flutter_dev-0.1.4 → flutter_dev-0.1.5}/LICENSE +0 -0
- {flutter_dev-0.1.4 → flutter_dev-0.1.5}/MANIFEST.in +0 -0
- {flutter_dev-0.1.4 → flutter_dev-0.1.5}/common_utils.py +0 -0
- {flutter_dev-0.1.4 → flutter_dev-0.1.5}/core/__init__.py +0 -0
- {flutter_dev-0.1.4 → flutter_dev-0.1.5}/core/constants.py +0 -0
- {flutter_dev-0.1.4 → flutter_dev-0.1.5}/core/state.py +0 -0
- {flutter_dev-0.1.4 → flutter_dev-0.1.5}/create_page.py +0 -0
- {flutter_dev-0.1.4 → flutter_dev-0.1.5}/gemini_api.py +0 -0
- {flutter_dev-0.1.4 → flutter_dev-0.1.5}/git_diff_output_editor.py +0 -0
- {flutter_dev-0.1.4 → flutter_dev-0.1.5}/install_legacy.py +0 -0
- {flutter_dev-0.1.4 → flutter_dev-0.1.5}/managers/__init__.py +0 -0
- {flutter_dev-0.1.4 → flutter_dev-0.1.5}/managers/ai.py +0 -0
- {flutter_dev-0.1.4 → flutter_dev-0.1.5}/managers/app.py +0 -0
- {flutter_dev-0.1.4 → flutter_dev-0.1.5}/managers/brew.py +0 -0
- {flutter_dev-0.1.4 → flutter_dev-0.1.5}/managers/build.py +0 -0
- {flutter_dev-0.1.4 → flutter_dev-0.1.5}/managers/datetime.py +0 -0
- {flutter_dev-0.1.4 → flutter_dev-0.1.5}/managers/device.py +0 -0
- {flutter_dev-0.1.4 → flutter_dev-0.1.5}/managers/doctor.py +0 -0
- {flutter_dev-0.1.4 → flutter_dev-0.1.5}/managers/git.py +0 -0
- {flutter_dev-0.1.4 → flutter_dev-0.1.5}/managers/git_account.py +0 -0
- {flutter_dev-0.1.4 → flutter_dev-0.1.5}/managers/localization.py +0 -0
- {flutter_dev-0.1.4 → flutter_dev-0.1.5}/managers/merge.py +0 -0
- {flutter_dev-0.1.4 → flutter_dev-0.1.5}/managers/mirror.py +0 -0
- {flutter_dev-0.1.4 → flutter_dev-0.1.5}/managers/project.py +0 -0
- {flutter_dev-0.1.4 → flutter_dev-0.1.5}/managers/web_deploy.py +0 -0
- {flutter_dev-0.1.4 → flutter_dev-0.1.5}/requirements.txt +0 -0
- {flutter_dev-0.1.4 → flutter_dev-0.1.5}/setup.cfg +0 -0
- {flutter_dev-0.1.4 → flutter_dev-0.1.5}/setup.py +0 -0
- {flutter_dev-0.1.4 → flutter_dev-0.1.5}/switch_ai.py +0 -0
- {flutter_dev-0.1.4 → flutter_dev-0.1.5}/tests/test_localization.py +0 -0
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented in this file.
|
|
4
4
|
|
|
5
|
+
## 0.1.5 - 2026-06-24
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added a Tkinter desktop dashboard that opens by running `fdev` with no command.
|
|
10
|
+
- Added GUI live log output with ANSI terminal color rendering.
|
|
11
|
+
- Added GUI buttons for all documented command groups, including mirror variants, AI provider shortcuts, and GitHub account subcommands.
|
|
12
|
+
- Added Python coverage for GUI command catalog, ANSI color parsing, and no-command CLI dispatch.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Kept command help available through `fdev help`, `fdev --help`, and `fdev -h`.
|
|
17
|
+
- Documented Tkinter requirements for the desktop dashboard.
|
|
18
|
+
|
|
5
19
|
## 0.1.4 - 2026-06-15
|
|
6
20
|
|
|
7
21
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flutter-dev
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
4
4
|
Summary: A cross-platform CLI toolkit for Flutter development workflows.
|
|
5
5
|
Author: Royal Court BD
|
|
6
6
|
License-Expression: MIT
|
|
@@ -22,6 +22,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.11
|
|
23
23
|
Classifier: Programming Language :: Python :: 3.12
|
|
24
24
|
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
25
26
|
Classifier: Topic :: Software Development :: Build Tools
|
|
26
27
|
Classifier: Topic :: Utilities
|
|
27
28
|
Requires-Python: >=3.8
|
|
@@ -41,7 +42,7 @@ The package is published on PyPI as `flutter-dev` and installs console commands
|
|
|
41
42
|
pip install flutter-dev
|
|
42
43
|
```
|
|
43
44
|
|
|
44
|
-
Current package metadata declares version `0.1.
|
|
45
|
+
Current package metadata declares version `0.1.5`, Python `>=3.8`, MIT license, and support for macOS, Linux, and Windows.
|
|
45
46
|
|
|
46
47
|
## Key Features
|
|
47
48
|
|
|
@@ -52,6 +53,7 @@ Current package metadata declares version `0.1.4`, Python `>=3.8`, MIT license,
|
|
|
52
53
|
- AI-assisted Git commits using Groq, Mistral, SambaNova, or OpenRouter-compatible chat completion APIs.
|
|
53
54
|
- Git workflow helpers for version tags, branch sync, deployment branch merge, discard, and GitHub account management through `gh`.
|
|
54
55
|
- Feature/page generator that creates a `lib/features/<name>` structure using the project's presenter/base-state pattern.
|
|
56
|
+
- Small Tkinter desktop dashboard opened by running `fdev` with no command, with live colored command logs.
|
|
55
57
|
- Environment diagnostics through `fdev doctor`.
|
|
56
58
|
- Small utilities for Homebrew cleanup, file merging from `paths.txt`, AI provider switching, and git diff prompt export.
|
|
57
59
|
|
|
@@ -71,6 +73,8 @@ fdev doctor
|
|
|
71
73
|
|
|
72
74
|
`pip install flutter-dev` installs the Python package, Python dependencies, and console scripts. It does not install Flutter, Dart, Git, Android SDK tools, CocoaPods, Firebase CLI, GitHub CLI, `scrcpy`, Java/JDK, or VS Code CLI.
|
|
73
75
|
|
|
76
|
+
The desktop dashboard uses Python Tkinter. Some Python distributions install this separately; for Homebrew Python, install the matching package such as `brew install python-tk@3.14`.
|
|
77
|
+
|
|
74
78
|
### Install From Source
|
|
75
79
|
|
|
76
80
|
```bash
|
|
@@ -118,12 +122,19 @@ Feature-specific external tools:
|
|
|
118
122
|
| GitHub CLI / `gh` | `fdev git ...` |
|
|
119
123
|
| VS Code CLI / `code` | Conflict helper in branch sync |
|
|
120
124
|
| Homebrew | `brew`, macOS `install-scrcpy` flow |
|
|
125
|
+
| Python Tkinter | Desktop dashboard opened by `fdev` |
|
|
121
126
|
|
|
122
127
|
## Quick Start
|
|
123
128
|
|
|
124
129
|
Run commands from the root of a Flutter project when they need `pubspec.yaml`, Android Gradle files, iOS files, or build outputs.
|
|
125
130
|
|
|
126
131
|
```bash
|
|
132
|
+
# Open the desktop dashboard
|
|
133
|
+
fdev
|
|
134
|
+
|
|
135
|
+
# Show command help
|
|
136
|
+
fdev help
|
|
137
|
+
|
|
127
138
|
# Check environment and optional tools
|
|
128
139
|
fdev doctor
|
|
129
140
|
|
|
@@ -225,6 +236,8 @@ Switching GitHub accounts runs `gh auth switch` and `gh auth setup-git`, but it
|
|
|
225
236
|
|
|
226
237
|
| Command | Description |
|
|
227
238
|
| --- | --- |
|
|
239
|
+
| `fdev` | Opens a small Tkinter desktop dashboard for running `fdev` commands with live colored logs and command input. The terminal process stays running while the window is open. |
|
|
240
|
+
| `fdev help` | Shows command help without opening the dashboard. |
|
|
228
241
|
| `fdev doctor` | Checks platform, Python, Flutter/Dart/Git, Python packages, Android/iOS tools, optional CLIs, `.env`, Flutter project detection, and global `fdev` availability. |
|
|
229
242
|
| `fdev ai` | Shows the current AI provider and opens an interactive provider switch prompt. |
|
|
230
243
|
| `fdev ai <provider>` | Switches `DEFAULT_AI_SERVICE` to `groq`, `mistral`, `sambanova`, or `openrouter`. |
|
|
@@ -373,7 +386,8 @@ The installed Python layout is top-level modules plus the `core` and `managers`
|
|
|
373
386
|
- `fdev cleanup` applies `dart fix`, formats the project, and runs a major-version dependency upgrade.
|
|
374
387
|
- `fdev clear-data` clears app data on Android after confirmation.
|
|
375
388
|
- `fdev uninstall` may fall back to foreground app detection outside a Flutter project root.
|
|
376
|
-
- `fdev`
|
|
389
|
+
- `fdev` with no command opens the desktop dashboard. Use `fdev help` to print command usage.
|
|
390
|
+
- Most command subcommands create build output directories at startup in the current working directory.
|
|
377
391
|
- Some subcommands return failure internally without always mapping that failure to a non-zero process exit code, so automation should inspect command output carefully.
|
|
378
392
|
|
|
379
393
|
## Troubleshooting
|
|
@@ -8,7 +8,7 @@ The package is published on PyPI as `flutter-dev` and installs console commands
|
|
|
8
8
|
pip install flutter-dev
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
Current package metadata declares version `0.1.
|
|
11
|
+
Current package metadata declares version `0.1.5`, Python `>=3.8`, MIT license, and support for macOS, Linux, and Windows.
|
|
12
12
|
|
|
13
13
|
## Key Features
|
|
14
14
|
|
|
@@ -19,6 +19,7 @@ Current package metadata declares version `0.1.4`, Python `>=3.8`, MIT license,
|
|
|
19
19
|
- AI-assisted Git commits using Groq, Mistral, SambaNova, or OpenRouter-compatible chat completion APIs.
|
|
20
20
|
- Git workflow helpers for version tags, branch sync, deployment branch merge, discard, and GitHub account management through `gh`.
|
|
21
21
|
- Feature/page generator that creates a `lib/features/<name>` structure using the project's presenter/base-state pattern.
|
|
22
|
+
- Small Tkinter desktop dashboard opened by running `fdev` with no command, with live colored command logs.
|
|
22
23
|
- Environment diagnostics through `fdev doctor`.
|
|
23
24
|
- Small utilities for Homebrew cleanup, file merging from `paths.txt`, AI provider switching, and git diff prompt export.
|
|
24
25
|
|
|
@@ -38,6 +39,8 @@ fdev doctor
|
|
|
38
39
|
|
|
39
40
|
`pip install flutter-dev` installs the Python package, Python dependencies, and console scripts. It does not install Flutter, Dart, Git, Android SDK tools, CocoaPods, Firebase CLI, GitHub CLI, `scrcpy`, Java/JDK, or VS Code CLI.
|
|
40
41
|
|
|
42
|
+
The desktop dashboard uses Python Tkinter. Some Python distributions install this separately; for Homebrew Python, install the matching package such as `brew install python-tk@3.14`.
|
|
43
|
+
|
|
41
44
|
### Install From Source
|
|
42
45
|
|
|
43
46
|
```bash
|
|
@@ -85,12 +88,19 @@ Feature-specific external tools:
|
|
|
85
88
|
| GitHub CLI / `gh` | `fdev git ...` |
|
|
86
89
|
| VS Code CLI / `code` | Conflict helper in branch sync |
|
|
87
90
|
| Homebrew | `brew`, macOS `install-scrcpy` flow |
|
|
91
|
+
| Python Tkinter | Desktop dashboard opened by `fdev` |
|
|
88
92
|
|
|
89
93
|
## Quick Start
|
|
90
94
|
|
|
91
95
|
Run commands from the root of a Flutter project when they need `pubspec.yaml`, Android Gradle files, iOS files, or build outputs.
|
|
92
96
|
|
|
93
97
|
```bash
|
|
98
|
+
# Open the desktop dashboard
|
|
99
|
+
fdev
|
|
100
|
+
|
|
101
|
+
# Show command help
|
|
102
|
+
fdev help
|
|
103
|
+
|
|
94
104
|
# Check environment and optional tools
|
|
95
105
|
fdev doctor
|
|
96
106
|
|
|
@@ -192,6 +202,8 @@ Switching GitHub accounts runs `gh auth switch` and `gh auth setup-git`, but it
|
|
|
192
202
|
|
|
193
203
|
| Command | Description |
|
|
194
204
|
| --- | --- |
|
|
205
|
+
| `fdev` | Opens a small Tkinter desktop dashboard for running `fdev` commands with live colored logs and command input. The terminal process stays running while the window is open. |
|
|
206
|
+
| `fdev help` | Shows command help without opening the dashboard. |
|
|
195
207
|
| `fdev doctor` | Checks platform, Python, Flutter/Dart/Git, Python packages, Android/iOS tools, optional CLIs, `.env`, Flutter project detection, and global `fdev` availability. |
|
|
196
208
|
| `fdev ai` | Shows the current AI provider and opens an interactive provider switch prompt. |
|
|
197
209
|
| `fdev ai <provider>` | Switches `DEFAULT_AI_SERVICE` to `groq`, `mistral`, `sambanova`, or `openrouter`. |
|
|
@@ -340,7 +352,8 @@ The installed Python layout is top-level modules plus the `core` and `managers`
|
|
|
340
352
|
- `fdev cleanup` applies `dart fix`, formats the project, and runs a major-version dependency upgrade.
|
|
341
353
|
- `fdev clear-data` clears app data on Android after confirmation.
|
|
342
354
|
- `fdev uninstall` may fall back to foreground app detection outside a Flutter project root.
|
|
343
|
-
- `fdev`
|
|
355
|
+
- `fdev` with no command opens the desktop dashboard. Use `fdev help` to print command usage.
|
|
356
|
+
- Most command subcommands create build output directories at startup in the current working directory.
|
|
344
357
|
- Some subcommands return failure internally without always mapping that failure to a non-zero process exit code, so automation should inspect command output carefully.
|
|
345
358
|
|
|
346
359
|
## Troubleshooting
|
|
@@ -70,11 +70,15 @@ from managers.brew import (
|
|
|
70
70
|
from managers.git_account import (
|
|
71
71
|
git_account_manager,
|
|
72
72
|
)
|
|
73
|
+
from managers.gui import (
|
|
74
|
+
launch_gui,
|
|
75
|
+
)
|
|
73
76
|
|
|
74
77
|
|
|
75
|
-
def show_usage():
|
|
78
|
+
def show_usage(exit_code=1):
|
|
76
79
|
"""Show usage information"""
|
|
77
80
|
print(f"{YELLOW}Usage: {sys.argv[0]} [command] [options]{NC}")
|
|
81
|
+
print(f"{BLUE}Run {GREEN}fdev{NC}{BLUE} without a command to open the desktop dashboard.{NC}")
|
|
78
82
|
print(f"\n{BLUE}Build Commands:{NC}")
|
|
79
83
|
print(" apk Build release APK (Full Process)")
|
|
80
84
|
print(" apk-split Build APK with --split-per-abi")
|
|
@@ -114,6 +118,7 @@ def show_usage():
|
|
|
114
118
|
print(f"\n{BLUE}Utility Commands:{NC}")
|
|
115
119
|
print(" doctor Check environment health (tools, deps, config)")
|
|
116
120
|
print(" brew Interactive Homebrew package manager")
|
|
121
|
+
print(" help Show this help")
|
|
117
122
|
print(" merge Merge files from paths.txt into single output file")
|
|
118
123
|
print(" ai Show current AI service or switch to another")
|
|
119
124
|
print(" Usage: fdev ai [groq|mistral|sambanova|openrouter]")
|
|
@@ -137,10 +142,11 @@ def show_usage():
|
|
|
137
142
|
print(f" {GREEN}fdev git config{NC} # Update git user.name / user.email")
|
|
138
143
|
print(f" {GREEN}fdev arb-to-csv{NC} # Export lib/l10n ARB files to translations.csv")
|
|
139
144
|
print(f" {GREEN}fdev csv-to-arb{NC} # Generate lib/l10n ARB files from translations.csv")
|
|
145
|
+
print(f" {GREEN}fdev{NC} # Open small desktop dashboard")
|
|
140
146
|
|
|
141
147
|
print(f"\n{BLUE}Note:{NC}")
|
|
142
148
|
print(f" {YELLOW}Multiple devices detected?{NC} Tool will prompt you to select one.")
|
|
143
|
-
|
|
149
|
+
return exit_code
|
|
144
150
|
|
|
145
151
|
|
|
146
152
|
def main():
|
|
@@ -151,10 +157,13 @@ def main():
|
|
|
151
157
|
os.makedirs("build/ios/ipa", exist_ok=True)
|
|
152
158
|
|
|
153
159
|
if len(sys.argv) < 2:
|
|
154
|
-
|
|
160
|
+
return launch_gui()
|
|
155
161
|
|
|
156
162
|
command = sys.argv[1].lower()
|
|
157
163
|
|
|
164
|
+
if command in ("help", "--help", "-h"):
|
|
165
|
+
return show_usage(exit_code=0)
|
|
166
|
+
|
|
158
167
|
# Build commands
|
|
159
168
|
if command == "apk":
|
|
160
169
|
build_apk()
|
|
@@ -262,7 +271,7 @@ def main():
|
|
|
262
271
|
git_account_manager(sys.argv[2:])
|
|
263
272
|
|
|
264
273
|
else:
|
|
265
|
-
show_usage()
|
|
274
|
+
sys.exit(show_usage())
|
|
266
275
|
|
|
267
276
|
|
|
268
277
|
if __name__ == "__main__":
|
|
@@ -272,4 +281,4 @@ if __name__ == "__main__":
|
|
|
272
281
|
sys.exit(0)
|
|
273
282
|
|
|
274
283
|
signal.signal(signal.SIGINT, signal_handler)
|
|
275
|
-
main()
|
|
284
|
+
sys.exit(main())
|
|
@@ -26,9 +26,12 @@ managers/device.py
|
|
|
26
26
|
managers/doctor.py
|
|
27
27
|
managers/git.py
|
|
28
28
|
managers/git_account.py
|
|
29
|
+
managers/gui.py
|
|
29
30
|
managers/localization.py
|
|
30
31
|
managers/merge.py
|
|
31
32
|
managers/mirror.py
|
|
32
33
|
managers/project.py
|
|
33
34
|
managers/web_deploy.py
|
|
35
|
+
tests/test_fdev_cli.py
|
|
36
|
+
tests/test_gui.py
|
|
34
37
|
tests/test_localization.py
|
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Small Tkinter GUI for running fdev commands with live logs.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import shlex
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
import threading
|
|
12
|
+
import re
|
|
13
|
+
from collections import OrderedDict
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Callable, Iterable, List, Optional
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
ANSI_PATTERN = re.compile(r"\033\[([0-9;]*)m")
|
|
20
|
+
|
|
21
|
+
ANSI_COLORS = {
|
|
22
|
+
30: "black",
|
|
23
|
+
31: "red",
|
|
24
|
+
32: "green",
|
|
25
|
+
33: "yellow",
|
|
26
|
+
34: "blue",
|
|
27
|
+
35: "magenta",
|
|
28
|
+
36: "cyan",
|
|
29
|
+
37: "white",
|
|
30
|
+
90: "bright_black",
|
|
31
|
+
91: "bright_red",
|
|
32
|
+
92: "bright_green",
|
|
33
|
+
93: "bright_yellow",
|
|
34
|
+
94: "bright_blue",
|
|
35
|
+
95: "bright_magenta",
|
|
36
|
+
96: "bright_cyan",
|
|
37
|
+
97: "bright_white",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _apply_ansi_codes(codes: List[int], current_tag: Optional[str]) -> Optional[str]:
|
|
42
|
+
if not codes:
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
tag = current_tag
|
|
46
|
+
bright = False
|
|
47
|
+
|
|
48
|
+
for code in codes:
|
|
49
|
+
if code == 0:
|
|
50
|
+
tag = None
|
|
51
|
+
bright = False
|
|
52
|
+
elif code == 1:
|
|
53
|
+
bright = True
|
|
54
|
+
if tag and not tag.startswith("bright_"):
|
|
55
|
+
tag = f"bright_{tag}"
|
|
56
|
+
elif code in ANSI_COLORS:
|
|
57
|
+
tag = ANSI_COLORS[code]
|
|
58
|
+
if bright and not tag.startswith("bright_"):
|
|
59
|
+
tag = f"bright_{tag}"
|
|
60
|
+
|
|
61
|
+
return tag
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def parse_ansi_segments(text: str, initial_tag: Optional[str] = None):
|
|
65
|
+
"""Split ANSI-colored terminal text into Tkinter tag segments."""
|
|
66
|
+
segments = []
|
|
67
|
+
current_tag = initial_tag
|
|
68
|
+
position = 0
|
|
69
|
+
|
|
70
|
+
for match in ANSI_PATTERN.finditer(text):
|
|
71
|
+
if match.start() > position:
|
|
72
|
+
segments.append((text[position:match.start()], current_tag))
|
|
73
|
+
|
|
74
|
+
raw_codes = match.group(1)
|
|
75
|
+
codes = [int(code) for code in raw_codes.split(";") if code] if raw_codes else [0]
|
|
76
|
+
current_tag = _apply_ansi_codes(codes, current_tag)
|
|
77
|
+
position = match.end()
|
|
78
|
+
|
|
79
|
+
if position < len(text):
|
|
80
|
+
segments.append((text[position:], current_tag))
|
|
81
|
+
|
|
82
|
+
return segments, current_tag
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass(frozen=True)
|
|
86
|
+
class GuiCommand:
|
|
87
|
+
"""Command metadata displayed by the GUI."""
|
|
88
|
+
|
|
89
|
+
label: str
|
|
90
|
+
args: List[str]
|
|
91
|
+
description: str
|
|
92
|
+
argument_prompt: Optional[str] = None
|
|
93
|
+
confirm: bool = False
|
|
94
|
+
|
|
95
|
+
def resolve_args(self, extra_args_text: Optional[str] = None) -> List[str]:
|
|
96
|
+
"""Return the full fdev argument list for this command."""
|
|
97
|
+
resolved = list(self.args)
|
|
98
|
+
if extra_args_text:
|
|
99
|
+
resolved.extend(shlex.split(extra_args_text))
|
|
100
|
+
return resolved
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def get_gui_sections():
|
|
104
|
+
"""Return GUI sections and their fdev commands."""
|
|
105
|
+
return OrderedDict(
|
|
106
|
+
[
|
|
107
|
+
(
|
|
108
|
+
"Dashboard",
|
|
109
|
+
[
|
|
110
|
+
GuiCommand("Doctor", ["doctor"], "Check environment, tools, devices, and config."),
|
|
111
|
+
GuiCommand("AI Status", ["ai"], "Show and switch the active AI provider."),
|
|
112
|
+
GuiCommand("GitHub Accounts", ["git", "status"], "Show GitHub CLI account status."),
|
|
113
|
+
],
|
|
114
|
+
),
|
|
115
|
+
(
|
|
116
|
+
"Build",
|
|
117
|
+
[
|
|
118
|
+
GuiCommand("APK", ["apk"], "Build release APK."),
|
|
119
|
+
GuiCommand("Split APK", ["apk-split"], "Build release APKs split per ABI."),
|
|
120
|
+
GuiCommand("AAB", ["aab"], "Build Android App Bundle."),
|
|
121
|
+
GuiCommand("IPA", ["ipa"], "Build iOS IPA on supported systems."),
|
|
122
|
+
GuiCommand("Release Run", ["release-run"], "Build, install, and launch release APK."),
|
|
123
|
+
GuiCommand("Web Deploy", ["web-deploy"], "Build Flutter web and deploy Firebase.", confirm=True),
|
|
124
|
+
],
|
|
125
|
+
),
|
|
126
|
+
(
|
|
127
|
+
"Project",
|
|
128
|
+
[
|
|
129
|
+
GuiCommand("Setup", ["setup"], "Run full Flutter project setup.", confirm=True),
|
|
130
|
+
GuiCommand("Cleanup", ["cleanup"], "Clean, fix, format, and upgrade dependencies.", confirm=True),
|
|
131
|
+
GuiCommand("Generate L10n", ["lang"], "Run flutter gen-l10n."),
|
|
132
|
+
GuiCommand("Build Runner", ["db"], "Run build_runner."),
|
|
133
|
+
GuiCommand("Cache Repair", ["cache-repair"], "Repair Flutter pub cache.", confirm=True),
|
|
134
|
+
GuiCommand("Pods", ["pod"], "Update iOS pods."),
|
|
135
|
+
GuiCommand(
|
|
136
|
+
"Create Page",
|
|
137
|
+
["page"],
|
|
138
|
+
"Generate a Flutter feature/page structure.",
|
|
139
|
+
argument_prompt="Page name, for example: user_profile",
|
|
140
|
+
confirm=True,
|
|
141
|
+
),
|
|
142
|
+
GuiCommand("ARB to CSV", ["arb-to-csv"], "Export lib/l10n ARB files to translations.csv."),
|
|
143
|
+
GuiCommand("CSV to ARB", ["csv-to-arb"], "Generate ARB files from translations.csv.", confirm=True),
|
|
144
|
+
],
|
|
145
|
+
),
|
|
146
|
+
(
|
|
147
|
+
"Device",
|
|
148
|
+
[
|
|
149
|
+
GuiCommand("Install APK", ["install"], "Install built APK on selected device."),
|
|
150
|
+
GuiCommand("Uninstall", ["uninstall"], "Uninstall app from device.", confirm=True),
|
|
151
|
+
GuiCommand("Clear Data", ["clear-data"], "Clear foreground app data.", confirm=True),
|
|
152
|
+
GuiCommand("Install scrcpy", ["install-scrcpy"], "Install scrcpy if supported."),
|
|
153
|
+
GuiCommand("Mirror", ["mirror"], "Launch Android screen mirror."),
|
|
154
|
+
GuiCommand("Mirror Wireless", ["mirror", "--wireless"], "Set up wireless ADB mirroring."),
|
|
155
|
+
GuiCommand("Mirror No Top", ["mirror", "--no-top"], "Mirror without always-on-top."),
|
|
156
|
+
GuiCommand("Date Time", ["datetime"], "Open Date and Time settings on device."),
|
|
157
|
+
],
|
|
158
|
+
),
|
|
159
|
+
(
|
|
160
|
+
"Git",
|
|
161
|
+
[
|
|
162
|
+
GuiCommand("Commit", ["commit"], "Generate AI commit message and commit.", confirm=True),
|
|
163
|
+
GuiCommand("Tag", ["tag"], "Bump version, commit, tag, and push.", confirm=True),
|
|
164
|
+
GuiCommand("Discard", ["discard"], "Discard tracked and untracked changes.", confirm=True),
|
|
165
|
+
GuiCommand(
|
|
166
|
+
"Sync Branches",
|
|
167
|
+
["sync"],
|
|
168
|
+
"Sync current branch with named branches.",
|
|
169
|
+
argument_prompt="Branch names separated by spaces, for example: dev main",
|
|
170
|
+
confirm=True,
|
|
171
|
+
),
|
|
172
|
+
GuiCommand("Deploy", ["deploy"], "Merge current branch into deployment.", confirm=True),
|
|
173
|
+
GuiCommand("Git Manager", ["git"], "Open interactive GitHub account manager."),
|
|
174
|
+
GuiCommand("Git Status", ["git", "status"], "Show GitHub CLI account status."),
|
|
175
|
+
GuiCommand("Git List", ["git", "list"], "List GitHub CLI accounts."),
|
|
176
|
+
GuiCommand(
|
|
177
|
+
"Git Switch",
|
|
178
|
+
["git", "switch"],
|
|
179
|
+
"Switch to a GitHub CLI account.",
|
|
180
|
+
argument_prompt="GitHub username, for example: royalcourtbd",
|
|
181
|
+
),
|
|
182
|
+
GuiCommand("Git Add", ["git", "add"], "Add a GitHub CLI account.", confirm=True),
|
|
183
|
+
GuiCommand(
|
|
184
|
+
"Git Remove",
|
|
185
|
+
["git", "remove"],
|
|
186
|
+
"Remove a GitHub CLI account.",
|
|
187
|
+
argument_prompt="GitHub username to remove",
|
|
188
|
+
confirm=True,
|
|
189
|
+
),
|
|
190
|
+
GuiCommand("Git Config", ["git", "config"], "Update git identity."),
|
|
191
|
+
],
|
|
192
|
+
),
|
|
193
|
+
(
|
|
194
|
+
"Tools",
|
|
195
|
+
[
|
|
196
|
+
GuiCommand("Brew", ["brew"], "Open Homebrew package manager."),
|
|
197
|
+
GuiCommand("Merge Files", ["merge"], "Merge paths listed in paths.txt."),
|
|
198
|
+
GuiCommand(
|
|
199
|
+
"AI Switch",
|
|
200
|
+
["ai"],
|
|
201
|
+
"Switch AI provider.",
|
|
202
|
+
argument_prompt="Provider: groq, mistral, sambanova, or openrouter",
|
|
203
|
+
),
|
|
204
|
+
GuiCommand("AI Groq", ["ai", "groq"], "Switch AI provider to Groq."),
|
|
205
|
+
GuiCommand("AI Mistral", ["ai", "mistral"], "Switch AI provider to Mistral."),
|
|
206
|
+
GuiCommand("AI SambaNova", ["ai", "sambanova"], "Switch AI provider to SambaNova."),
|
|
207
|
+
GuiCommand("AI OpenRouter", ["ai", "openrouter"], "Switch AI provider to OpenRouter."),
|
|
208
|
+
],
|
|
209
|
+
),
|
|
210
|
+
]
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def build_fdev_command(args: Iterable[str]) -> List[str]:
|
|
215
|
+
"""Build the subprocess command for invoking this repo's fdev entrypoint."""
|
|
216
|
+
repo_root = Path(__file__).resolve().parent.parent
|
|
217
|
+
return [sys.executable, str(repo_root / "fdev.py"), *list(args)]
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class GuiProcessRunner:
|
|
221
|
+
"""Run one fdev subprocess and stream output to callbacks."""
|
|
222
|
+
|
|
223
|
+
def __init__(self):
|
|
224
|
+
self._process: Optional[subprocess.Popen] = None
|
|
225
|
+
self._reader_thread: Optional[threading.Thread] = None
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def is_running(self) -> bool:
|
|
229
|
+
return self._process is not None and self._process.poll() is None
|
|
230
|
+
|
|
231
|
+
def start(
|
|
232
|
+
self,
|
|
233
|
+
command: List[str],
|
|
234
|
+
on_output: Callable[[str], None],
|
|
235
|
+
on_exit: Callable[[int], None],
|
|
236
|
+
) -> bool:
|
|
237
|
+
if self.is_running:
|
|
238
|
+
return False
|
|
239
|
+
|
|
240
|
+
self._process = subprocess.Popen(
|
|
241
|
+
command,
|
|
242
|
+
stdin=subprocess.PIPE,
|
|
243
|
+
stdout=subprocess.PIPE,
|
|
244
|
+
stderr=subprocess.STDOUT,
|
|
245
|
+
text=True,
|
|
246
|
+
encoding="utf-8",
|
|
247
|
+
errors="replace",
|
|
248
|
+
bufsize=1,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
def read_output():
|
|
252
|
+
assert self._process is not None
|
|
253
|
+
assert self._process.stdout is not None
|
|
254
|
+
while True:
|
|
255
|
+
chunk = self._process.stdout.read(1)
|
|
256
|
+
if not chunk:
|
|
257
|
+
break
|
|
258
|
+
on_output(chunk)
|
|
259
|
+
exit_code = self._process.wait()
|
|
260
|
+
on_exit(exit_code)
|
|
261
|
+
|
|
262
|
+
self._reader_thread = threading.Thread(target=read_output, daemon=True)
|
|
263
|
+
self._reader_thread.start()
|
|
264
|
+
return True
|
|
265
|
+
|
|
266
|
+
def send_input(self, text: str) -> bool:
|
|
267
|
+
if not self.is_running or self._process is None or self._process.stdin is None:
|
|
268
|
+
return False
|
|
269
|
+
try:
|
|
270
|
+
self._process.stdin.write(text + "\n")
|
|
271
|
+
self._process.stdin.flush()
|
|
272
|
+
return True
|
|
273
|
+
except (BrokenPipeError, OSError):
|
|
274
|
+
return False
|
|
275
|
+
|
|
276
|
+
def terminate(self) -> bool:
|
|
277
|
+
if not self.is_running or self._process is None:
|
|
278
|
+
return False
|
|
279
|
+
self._process.terminate()
|
|
280
|
+
return True
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class FdevGui:
|
|
284
|
+
"""Tkinter mini dashboard for fdev commands."""
|
|
285
|
+
|
|
286
|
+
def __init__(self, root):
|
|
287
|
+
self.root = root
|
|
288
|
+
self.sections = get_gui_sections()
|
|
289
|
+
self.runner = GuiProcessRunner()
|
|
290
|
+
self.current_section = next(iter(self.sections))
|
|
291
|
+
self._ansi_buffer = ""
|
|
292
|
+
self._ansi_tag = None
|
|
293
|
+
|
|
294
|
+
self.root.title("fdev")
|
|
295
|
+
self.root.geometry("820x540")
|
|
296
|
+
self.root.minsize(720, 460)
|
|
297
|
+
self.root.protocol("WM_DELETE_WINDOW", self._on_close)
|
|
298
|
+
|
|
299
|
+
self._build_widgets()
|
|
300
|
+
self._show_section(self.current_section)
|
|
301
|
+
|
|
302
|
+
def _build_widgets(self):
|
|
303
|
+
import tkinter as tk
|
|
304
|
+
from tkinter import ttk
|
|
305
|
+
|
|
306
|
+
self.root.columnconfigure(1, weight=1)
|
|
307
|
+
self.root.rowconfigure(0, weight=1)
|
|
308
|
+
|
|
309
|
+
sidebar = ttk.Frame(self.root, padding=(10, 10))
|
|
310
|
+
sidebar.grid(row=0, column=0, sticky="ns")
|
|
311
|
+
|
|
312
|
+
title = ttk.Label(sidebar, text="fdev", font=("TkDefaultFont", 16, "bold"))
|
|
313
|
+
title.grid(row=0, column=0, sticky="w", pady=(0, 10))
|
|
314
|
+
|
|
315
|
+
for index, section_name in enumerate(self.sections, start=1):
|
|
316
|
+
button = ttk.Button(
|
|
317
|
+
sidebar,
|
|
318
|
+
text=section_name,
|
|
319
|
+
command=lambda name=section_name: self._show_section(name),
|
|
320
|
+
)
|
|
321
|
+
button.grid(row=index, column=0, sticky="ew", pady=3)
|
|
322
|
+
|
|
323
|
+
content = ttk.Frame(self.root, padding=(8, 10, 10, 10))
|
|
324
|
+
content.grid(row=0, column=1, sticky="nsew")
|
|
325
|
+
content.columnconfigure(0, weight=1)
|
|
326
|
+
content.rowconfigure(1, weight=1)
|
|
327
|
+
|
|
328
|
+
header = ttk.Frame(content)
|
|
329
|
+
header.grid(row=0, column=0, sticky="ew", pady=(0, 8))
|
|
330
|
+
header.columnconfigure(0, weight=1)
|
|
331
|
+
|
|
332
|
+
self.section_label = ttk.Label(header, text="", font=("TkDefaultFont", 14, "bold"))
|
|
333
|
+
self.section_label.grid(row=0, column=0, sticky="w")
|
|
334
|
+
|
|
335
|
+
self.status_label = ttk.Label(header, text="Ready")
|
|
336
|
+
self.status_label.grid(row=0, column=1, sticky="e")
|
|
337
|
+
|
|
338
|
+
body = ttk.PanedWindow(content, orient=tk.VERTICAL)
|
|
339
|
+
body.grid(row=1, column=0, sticky="nsew")
|
|
340
|
+
|
|
341
|
+
self.command_frame = ttk.Frame(body, padding=(0, 0, 0, 6))
|
|
342
|
+
self.command_frame.columnconfigure(0, weight=1)
|
|
343
|
+
self.command_frame.columnconfigure(1, weight=1)
|
|
344
|
+
body.add(self.command_frame, weight=2)
|
|
345
|
+
|
|
346
|
+
log_frame = ttk.Frame(body)
|
|
347
|
+
log_frame.columnconfigure(0, weight=1)
|
|
348
|
+
log_frame.rowconfigure(0, weight=1)
|
|
349
|
+
body.add(log_frame, weight=3)
|
|
350
|
+
|
|
351
|
+
self.log_text = tk.Text(
|
|
352
|
+
log_frame,
|
|
353
|
+
height=12,
|
|
354
|
+
wrap="word",
|
|
355
|
+
state="disabled",
|
|
356
|
+
bg="#111827",
|
|
357
|
+
fg="#e5e7eb",
|
|
358
|
+
insertbackground="#e5e7eb",
|
|
359
|
+
)
|
|
360
|
+
self.log_text.grid(row=0, column=0, sticky="nsew")
|
|
361
|
+
|
|
362
|
+
scrollbar = ttk.Scrollbar(log_frame, orient="vertical", command=self.log_text.yview)
|
|
363
|
+
scrollbar.grid(row=0, column=1, sticky="ns")
|
|
364
|
+
self.log_text.configure(yscrollcommand=scrollbar.set)
|
|
365
|
+
self._configure_log_tags()
|
|
366
|
+
|
|
367
|
+
input_bar = ttk.Frame(content)
|
|
368
|
+
input_bar.grid(row=2, column=0, sticky="ew", pady=(8, 0))
|
|
369
|
+
input_bar.columnconfigure(0, weight=1)
|
|
370
|
+
|
|
371
|
+
self.input_entry = ttk.Entry(input_bar)
|
|
372
|
+
self.input_entry.grid(row=0, column=0, sticky="ew", padx=(0, 6))
|
|
373
|
+
self.input_entry.bind("<Return>", lambda _event: self._send_input())
|
|
374
|
+
|
|
375
|
+
ttk.Button(input_bar, text="Send Input", command=self._send_input).grid(row=0, column=1, padx=(0, 6))
|
|
376
|
+
ttk.Button(input_bar, text="Stop", command=self._stop_process).grid(row=0, column=2, padx=(0, 6))
|
|
377
|
+
ttk.Button(input_bar, text="Clear", command=self._clear_log).grid(row=0, column=3)
|
|
378
|
+
|
|
379
|
+
def _show_section(self, section_name: str):
|
|
380
|
+
from tkinter import ttk
|
|
381
|
+
|
|
382
|
+
self.current_section = section_name
|
|
383
|
+
self.section_label.configure(text=section_name)
|
|
384
|
+
|
|
385
|
+
for child in self.command_frame.winfo_children():
|
|
386
|
+
child.destroy()
|
|
387
|
+
|
|
388
|
+
for index, command in enumerate(self.sections[section_name]):
|
|
389
|
+
row = index // 2
|
|
390
|
+
column = index % 2
|
|
391
|
+
|
|
392
|
+
container = ttk.Frame(self.command_frame, padding=6)
|
|
393
|
+
container.grid(row=row, column=column, sticky="nsew", padx=4, pady=4)
|
|
394
|
+
container.columnconfigure(0, weight=1)
|
|
395
|
+
|
|
396
|
+
button = ttk.Button(
|
|
397
|
+
container,
|
|
398
|
+
text=command.label,
|
|
399
|
+
command=lambda item=command: self._run_command(item),
|
|
400
|
+
)
|
|
401
|
+
button.grid(row=0, column=0, sticky="ew")
|
|
402
|
+
|
|
403
|
+
description = ttk.Label(container, text=command.description, wraplength=260)
|
|
404
|
+
description.grid(row=1, column=0, sticky="w", pady=(4, 0))
|
|
405
|
+
|
|
406
|
+
def _run_command(self, command: GuiCommand):
|
|
407
|
+
from tkinter import messagebox, simpledialog
|
|
408
|
+
|
|
409
|
+
if self.runner.is_running:
|
|
410
|
+
messagebox.showwarning("fdev", "A command is already running.")
|
|
411
|
+
return
|
|
412
|
+
|
|
413
|
+
extra_args_text = None
|
|
414
|
+
if command.argument_prompt:
|
|
415
|
+
extra_args_text = simpledialog.askstring(command.label, command.argument_prompt, parent=self.root)
|
|
416
|
+
if extra_args_text is None:
|
|
417
|
+
return
|
|
418
|
+
|
|
419
|
+
args = command.resolve_args(extra_args_text)
|
|
420
|
+
|
|
421
|
+
if command.confirm:
|
|
422
|
+
display = "fdev " + " ".join(args)
|
|
423
|
+
if not messagebox.askyesno("Confirm command", f"Run this command?\n\n{display}"):
|
|
424
|
+
return
|
|
425
|
+
|
|
426
|
+
subprocess_command = build_fdev_command(args)
|
|
427
|
+
self._append_log("\n$ fdev " + " ".join(args) + "\n")
|
|
428
|
+
self.status_label.configure(text="Running: " + command.label)
|
|
429
|
+
|
|
430
|
+
started = self.runner.start(
|
|
431
|
+
subprocess_command,
|
|
432
|
+
on_output=lambda text: self.root.after(0, self._append_log, text),
|
|
433
|
+
on_exit=lambda code: self.root.after(0, self._command_finished, code),
|
|
434
|
+
)
|
|
435
|
+
if not started:
|
|
436
|
+
messagebox.showwarning("fdev", "Could not start command.")
|
|
437
|
+
|
|
438
|
+
def _append_log(self, text: str):
|
|
439
|
+
self._ansi_buffer += text
|
|
440
|
+
safe_text = self._ansi_buffer
|
|
441
|
+
|
|
442
|
+
last_escape = safe_text.rfind("\033")
|
|
443
|
+
if last_escape != -1 and "m" not in safe_text[last_escape:]:
|
|
444
|
+
self._ansi_buffer = safe_text[last_escape:]
|
|
445
|
+
safe_text = safe_text[:last_escape]
|
|
446
|
+
else:
|
|
447
|
+
self._ansi_buffer = ""
|
|
448
|
+
|
|
449
|
+
if not safe_text:
|
|
450
|
+
return
|
|
451
|
+
|
|
452
|
+
segments, self._ansi_tag = parse_ansi_segments(safe_text, self._ansi_tag)
|
|
453
|
+
|
|
454
|
+
self.log_text.configure(state="normal")
|
|
455
|
+
for segment, tag in segments:
|
|
456
|
+
if tag:
|
|
457
|
+
self.log_text.insert("end", segment, tag)
|
|
458
|
+
else:
|
|
459
|
+
self.log_text.insert("end", segment)
|
|
460
|
+
self.log_text.see("end")
|
|
461
|
+
self.log_text.configure(state="disabled")
|
|
462
|
+
|
|
463
|
+
def _configure_log_tags(self):
|
|
464
|
+
self.log_text.tag_configure("black", foreground="#111827")
|
|
465
|
+
self.log_text.tag_configure("red", foreground="#ff6b6b")
|
|
466
|
+
self.log_text.tag_configure("green", foreground="#35d07f")
|
|
467
|
+
self.log_text.tag_configure("yellow", foreground="#f2d14f")
|
|
468
|
+
self.log_text.tag_configure("blue", foreground="#4da3ff")
|
|
469
|
+
self.log_text.tag_configure("magenta", foreground="#d38cff")
|
|
470
|
+
self.log_text.tag_configure("cyan", foreground="#4dd0e1")
|
|
471
|
+
self.log_text.tag_configure("white", foreground="#f8fafc")
|
|
472
|
+
self.log_text.tag_configure("bright_black", foreground="#94a3b8")
|
|
473
|
+
self.log_text.tag_configure("bright_red", foreground="#ff8787")
|
|
474
|
+
self.log_text.tag_configure("bright_green", foreground="#69e6a3")
|
|
475
|
+
self.log_text.tag_configure("bright_yellow", foreground="#f4e409")
|
|
476
|
+
self.log_text.tag_configure("bright_blue", foreground="#74b9ff")
|
|
477
|
+
self.log_text.tag_configure("bright_magenta", foreground="#e0aaff")
|
|
478
|
+
self.log_text.tag_configure("bright_cyan", foreground="#67e8f9")
|
|
479
|
+
self.log_text.tag_configure("bright_white", foreground="#ffffff")
|
|
480
|
+
|
|
481
|
+
def _clear_log(self):
|
|
482
|
+
self._ansi_buffer = ""
|
|
483
|
+
self._ansi_tag = None
|
|
484
|
+
self.log_text.configure(state="normal")
|
|
485
|
+
self.log_text.delete("1.0", "end")
|
|
486
|
+
self.log_text.configure(state="disabled")
|
|
487
|
+
|
|
488
|
+
def _send_input(self):
|
|
489
|
+
text = self.input_entry.get()
|
|
490
|
+
if not text:
|
|
491
|
+
return
|
|
492
|
+
if self.runner.send_input(text):
|
|
493
|
+
self._append_log("> " + text + "\n")
|
|
494
|
+
self.input_entry.delete(0, "end")
|
|
495
|
+
else:
|
|
496
|
+
self._append_log("[fdev] No running command can receive input.\n")
|
|
497
|
+
|
|
498
|
+
def _stop_process(self):
|
|
499
|
+
if self.runner.terminate():
|
|
500
|
+
self._append_log("[fdev] Stop requested.\n")
|
|
501
|
+
|
|
502
|
+
def _command_finished(self, exit_code: int):
|
|
503
|
+
self.status_label.configure(text=f"Finished: exit {exit_code}")
|
|
504
|
+
self._append_log(f"[fdev] Command finished with exit code {exit_code}.\n")
|
|
505
|
+
|
|
506
|
+
def _on_close(self):
|
|
507
|
+
from tkinter import messagebox
|
|
508
|
+
|
|
509
|
+
if self.runner.is_running:
|
|
510
|
+
if not messagebox.askyesno("fdev", "A command is still running. Stop it and close?"):
|
|
511
|
+
return
|
|
512
|
+
self.runner.terminate()
|
|
513
|
+
self.root.destroy()
|
|
514
|
+
|
|
515
|
+
def run(self):
|
|
516
|
+
self.root.mainloop()
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def launch_gui() -> int:
|
|
520
|
+
"""Launch the fdev Tkinter dashboard."""
|
|
521
|
+
try:
|
|
522
|
+
import tkinter as tk
|
|
523
|
+
except ImportError:
|
|
524
|
+
print("Tkinter is not available in this Python installation.")
|
|
525
|
+
print("Install a Python build that includes Tkinter, then run: fdev")
|
|
526
|
+
return 1
|
|
527
|
+
|
|
528
|
+
root = tk.Tk()
|
|
529
|
+
app = FdevGui(root)
|
|
530
|
+
app.run()
|
|
531
|
+
return 0
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "flutter-dev"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.5"
|
|
8
8
|
description = "A cross-platform CLI toolkit for Flutter development workflows."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
@@ -28,6 +28,7 @@ classifiers = [
|
|
|
28
28
|
"Programming Language :: Python :: 3.11",
|
|
29
29
|
"Programming Language :: Python :: 3.12",
|
|
30
30
|
"Programming Language :: Python :: 3.13",
|
|
31
|
+
"Programming Language :: Python :: 3.14",
|
|
31
32
|
"Topic :: Software Development :: Build Tools",
|
|
32
33
|
"Topic :: Utilities"
|
|
33
34
|
]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import unittest
|
|
3
|
+
from unittest.mock import patch
|
|
4
|
+
|
|
5
|
+
import fdev
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FdevCliTests(unittest.TestCase):
|
|
9
|
+
def test_no_command_launches_gui(self):
|
|
10
|
+
with patch.object(sys, "argv", ["fdev"]), patch.object(fdev, "launch_gui", return_value=0) as launch_gui:
|
|
11
|
+
result = fdev.main()
|
|
12
|
+
|
|
13
|
+
self.assertEqual(result, 0)
|
|
14
|
+
launch_gui.assert_called_once_with()
|
|
15
|
+
|
|
16
|
+
def test_help_command_prints_usage_without_launching_gui(self):
|
|
17
|
+
with (
|
|
18
|
+
patch.object(sys, "argv", ["fdev", "help"]),
|
|
19
|
+
patch.object(fdev, "launch_gui", return_value=0) as launch_gui,
|
|
20
|
+
patch.object(fdev, "show_usage", return_value=0) as show_usage,
|
|
21
|
+
):
|
|
22
|
+
result = fdev.main()
|
|
23
|
+
|
|
24
|
+
self.assertEqual(result, 0)
|
|
25
|
+
show_usage.assert_called_once_with(exit_code=0)
|
|
26
|
+
launch_gui.assert_not_called()
|
|
27
|
+
|
|
28
|
+
def test_gui_subcommand_is_not_the_dashboard_entrypoint(self):
|
|
29
|
+
with (
|
|
30
|
+
patch.object(sys, "argv", ["fdev", "gui"]),
|
|
31
|
+
patch.object(fdev, "launch_gui", return_value=0) as launch_gui,
|
|
32
|
+
patch.object(fdev, "show_usage", return_value=1) as show_usage,
|
|
33
|
+
):
|
|
34
|
+
with self.assertRaises(SystemExit) as raised:
|
|
35
|
+
fdev.main()
|
|
36
|
+
|
|
37
|
+
self.assertEqual(raised.exception.code, 1)
|
|
38
|
+
show_usage.assert_called_once_with()
|
|
39
|
+
launch_gui.assert_not_called()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
if __name__ == "__main__":
|
|
43
|
+
unittest.main()
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import unittest
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from managers.gui import (
|
|
6
|
+
GuiCommand,
|
|
7
|
+
parse_ansi_segments,
|
|
8
|
+
build_fdev_command,
|
|
9
|
+
get_gui_sections,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class GuiCatalogTests(unittest.TestCase):
|
|
14
|
+
def test_gui_sections_include_core_fdev_workflows(self):
|
|
15
|
+
sections = get_gui_sections()
|
|
16
|
+
|
|
17
|
+
self.assertIn("Dashboard", sections)
|
|
18
|
+
self.assertIn("Build", sections)
|
|
19
|
+
self.assertIn("Project", sections)
|
|
20
|
+
self.assertIn("Device", sections)
|
|
21
|
+
self.assertIn("Git", sections)
|
|
22
|
+
self.assertIn("Tools", sections)
|
|
23
|
+
|
|
24
|
+
dashboard_commands = [command.args for command in sections["Dashboard"]]
|
|
25
|
+
build_commands = [command.args for command in sections["Build"]]
|
|
26
|
+
device_commands = [command.args for command in sections["Device"]]
|
|
27
|
+
|
|
28
|
+
self.assertIn(["doctor"], dashboard_commands)
|
|
29
|
+
self.assertIn(["apk"], build_commands)
|
|
30
|
+
self.assertIn(["aab"], build_commands)
|
|
31
|
+
self.assertIn(["mirror"], device_commands)
|
|
32
|
+
self.assertIn(["clear-data"], device_commands)
|
|
33
|
+
|
|
34
|
+
def test_commands_that_need_user_arguments_are_marked(self):
|
|
35
|
+
sections = get_gui_sections()
|
|
36
|
+
all_commands = [
|
|
37
|
+
command
|
|
38
|
+
for commands in sections.values()
|
|
39
|
+
for command in commands
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
commands_by_label = {command.label: command for command in all_commands}
|
|
43
|
+
|
|
44
|
+
self.assertTrue(commands_by_label["Create Page"].argument_prompt)
|
|
45
|
+
self.assertTrue(commands_by_label["Sync Branches"].argument_prompt)
|
|
46
|
+
self.assertTrue(commands_by_label["AI Switch"].argument_prompt)
|
|
47
|
+
self.assertTrue(commands_by_label["Git Switch"].argument_prompt)
|
|
48
|
+
self.assertTrue(commands_by_label["Git Remove"].argument_prompt)
|
|
49
|
+
|
|
50
|
+
def test_gui_catalog_covers_usage_commands_and_common_variants(self):
|
|
51
|
+
sections = get_gui_sections()
|
|
52
|
+
actual_args = {
|
|
53
|
+
tuple(command.args)
|
|
54
|
+
for commands in sections.values()
|
|
55
|
+
for command in commands
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
expected_args = {
|
|
59
|
+
("apk",),
|
|
60
|
+
("apk-split",),
|
|
61
|
+
("aab",),
|
|
62
|
+
("ipa",),
|
|
63
|
+
("release-run",),
|
|
64
|
+
("web-deploy",),
|
|
65
|
+
("lang",),
|
|
66
|
+
("db",),
|
|
67
|
+
("setup",),
|
|
68
|
+
("cleanup",),
|
|
69
|
+
("cache-repair",),
|
|
70
|
+
("page",),
|
|
71
|
+
("arb-to-csv",),
|
|
72
|
+
("csv-to-arb",),
|
|
73
|
+
("install",),
|
|
74
|
+
("uninstall",),
|
|
75
|
+
("clear-data",),
|
|
76
|
+
("install-scrcpy",),
|
|
77
|
+
("mirror",),
|
|
78
|
+
("mirror", "--wireless"),
|
|
79
|
+
("mirror", "--no-top"),
|
|
80
|
+
("datetime",),
|
|
81
|
+
("pod",),
|
|
82
|
+
("tag",),
|
|
83
|
+
("commit",),
|
|
84
|
+
("discard",),
|
|
85
|
+
("sync",),
|
|
86
|
+
("deploy",),
|
|
87
|
+
("doctor",),
|
|
88
|
+
("brew",),
|
|
89
|
+
("merge",),
|
|
90
|
+
("ai",),
|
|
91
|
+
("ai", "groq"),
|
|
92
|
+
("ai", "mistral"),
|
|
93
|
+
("ai", "sambanova"),
|
|
94
|
+
("ai", "openrouter"),
|
|
95
|
+
("git",),
|
|
96
|
+
("git", "status"),
|
|
97
|
+
("git", "list"),
|
|
98
|
+
("git", "switch"),
|
|
99
|
+
("git", "add"),
|
|
100
|
+
("git", "remove"),
|
|
101
|
+
("git", "config"),
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
self.assertTrue(expected_args.issubset(actual_args), expected_args - actual_args)
|
|
105
|
+
|
|
106
|
+
def test_ansi_segments_convert_terminal_colors_to_gui_tags(self):
|
|
107
|
+
segments, final_tag = parse_ansi_segments(
|
|
108
|
+
"\033[0;34mTitle\033[0m plain \033[1;33mwarn\033[0m \033[0;31merror\033[0m"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
self.assertEqual(
|
|
112
|
+
segments,
|
|
113
|
+
[
|
|
114
|
+
("Title", "blue"),
|
|
115
|
+
(" plain ", None),
|
|
116
|
+
("warn", "bright_yellow"),
|
|
117
|
+
(" ", None),
|
|
118
|
+
("error", "red"),
|
|
119
|
+
],
|
|
120
|
+
)
|
|
121
|
+
self.assertIsNone(final_tag)
|
|
122
|
+
|
|
123
|
+
def test_build_fdev_command_uses_current_python_and_repo_fdev_entrypoint(self):
|
|
124
|
+
command = build_fdev_command(["doctor"])
|
|
125
|
+
|
|
126
|
+
self.assertEqual(command[0], sys.executable)
|
|
127
|
+
self.assertEqual(Path(command[1]).name, "fdev.py")
|
|
128
|
+
self.assertEqual(command[2:], ["doctor"])
|
|
129
|
+
|
|
130
|
+
def test_gui_command_combines_base_args_and_extra_args(self):
|
|
131
|
+
gui_command = GuiCommand(
|
|
132
|
+
label="Sync Branches",
|
|
133
|
+
args=["sync"],
|
|
134
|
+
description="Sync branches",
|
|
135
|
+
argument_prompt="Branch names",
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
self.assertEqual(
|
|
139
|
+
gui_command.resolve_args("dev main"),
|
|
140
|
+
["sync", "dev", "main"],
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
if __name__ == "__main__":
|
|
145
|
+
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|