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.
Files changed (40) hide show
  1. {flutter_dev-0.1.4 → flutter_dev-0.1.5}/CHANGELOG.md +14 -0
  2. {flutter_dev-0.1.4 → flutter_dev-0.1.5}/PKG-INFO +17 -3
  3. {flutter_dev-0.1.4 → flutter_dev-0.1.5}/README.md +15 -2
  4. {flutter_dev-0.1.4 → flutter_dev-0.1.5}/fdev.py +14 -5
  5. {flutter_dev-0.1.4 → flutter_dev-0.1.5}/flutter_dev.egg-info/SOURCES.txt +3 -0
  6. flutter_dev-0.1.5/managers/gui.py +531 -0
  7. {flutter_dev-0.1.4 → flutter_dev-0.1.5}/pyproject.toml +2 -1
  8. flutter_dev-0.1.5/tests/test_fdev_cli.py +43 -0
  9. flutter_dev-0.1.5/tests/test_gui.py +145 -0
  10. {flutter_dev-0.1.4 → flutter_dev-0.1.5}/.env.example +0 -0
  11. {flutter_dev-0.1.4 → flutter_dev-0.1.5}/LICENSE +0 -0
  12. {flutter_dev-0.1.4 → flutter_dev-0.1.5}/MANIFEST.in +0 -0
  13. {flutter_dev-0.1.4 → flutter_dev-0.1.5}/common_utils.py +0 -0
  14. {flutter_dev-0.1.4 → flutter_dev-0.1.5}/core/__init__.py +0 -0
  15. {flutter_dev-0.1.4 → flutter_dev-0.1.5}/core/constants.py +0 -0
  16. {flutter_dev-0.1.4 → flutter_dev-0.1.5}/core/state.py +0 -0
  17. {flutter_dev-0.1.4 → flutter_dev-0.1.5}/create_page.py +0 -0
  18. {flutter_dev-0.1.4 → flutter_dev-0.1.5}/gemini_api.py +0 -0
  19. {flutter_dev-0.1.4 → flutter_dev-0.1.5}/git_diff_output_editor.py +0 -0
  20. {flutter_dev-0.1.4 → flutter_dev-0.1.5}/install_legacy.py +0 -0
  21. {flutter_dev-0.1.4 → flutter_dev-0.1.5}/managers/__init__.py +0 -0
  22. {flutter_dev-0.1.4 → flutter_dev-0.1.5}/managers/ai.py +0 -0
  23. {flutter_dev-0.1.4 → flutter_dev-0.1.5}/managers/app.py +0 -0
  24. {flutter_dev-0.1.4 → flutter_dev-0.1.5}/managers/brew.py +0 -0
  25. {flutter_dev-0.1.4 → flutter_dev-0.1.5}/managers/build.py +0 -0
  26. {flutter_dev-0.1.4 → flutter_dev-0.1.5}/managers/datetime.py +0 -0
  27. {flutter_dev-0.1.4 → flutter_dev-0.1.5}/managers/device.py +0 -0
  28. {flutter_dev-0.1.4 → flutter_dev-0.1.5}/managers/doctor.py +0 -0
  29. {flutter_dev-0.1.4 → flutter_dev-0.1.5}/managers/git.py +0 -0
  30. {flutter_dev-0.1.4 → flutter_dev-0.1.5}/managers/git_account.py +0 -0
  31. {flutter_dev-0.1.4 → flutter_dev-0.1.5}/managers/localization.py +0 -0
  32. {flutter_dev-0.1.4 → flutter_dev-0.1.5}/managers/merge.py +0 -0
  33. {flutter_dev-0.1.4 → flutter_dev-0.1.5}/managers/mirror.py +0 -0
  34. {flutter_dev-0.1.4 → flutter_dev-0.1.5}/managers/project.py +0 -0
  35. {flutter_dev-0.1.4 → flutter_dev-0.1.5}/managers/web_deploy.py +0 -0
  36. {flutter_dev-0.1.4 → flutter_dev-0.1.5}/requirements.txt +0 -0
  37. {flutter_dev-0.1.4 → flutter_dev-0.1.5}/setup.cfg +0 -0
  38. {flutter_dev-0.1.4 → flutter_dev-0.1.5}/setup.py +0 -0
  39. {flutter_dev-0.1.4 → flutter_dev-0.1.5}/switch_ai.py +0 -0
  40. {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.4
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.4`, Python `>=3.8`, MIT license, and support for macOS, Linux, and Windows.
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` creates build output directories at startup in the current working directory.
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.4`, Python `>=3.8`, MIT license, and support for macOS, Linux, and Windows.
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` creates build output directories at startup in the current working directory.
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
- sys.exit(1)
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
- show_usage()
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.4"
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