gitflow-analytics 1.0.0__py3-none-any.whl → 1.0.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. gitflow_analytics/__init__.py +11 -9
  2. gitflow_analytics/_version.py +2 -2
  3. gitflow_analytics/cli.py +691 -243
  4. gitflow_analytics/cli_rich.py +353 -0
  5. gitflow_analytics/config.py +389 -96
  6. gitflow_analytics/core/analyzer.py +175 -78
  7. gitflow_analytics/core/branch_mapper.py +132 -132
  8. gitflow_analytics/core/cache.py +242 -173
  9. gitflow_analytics/core/identity.py +214 -178
  10. gitflow_analytics/extractors/base.py +13 -11
  11. gitflow_analytics/extractors/story_points.py +70 -59
  12. gitflow_analytics/extractors/tickets.py +111 -88
  13. gitflow_analytics/integrations/github_integration.py +91 -77
  14. gitflow_analytics/integrations/jira_integration.py +284 -0
  15. gitflow_analytics/integrations/orchestrator.py +99 -72
  16. gitflow_analytics/metrics/dora.py +183 -179
  17. gitflow_analytics/models/database.py +191 -54
  18. gitflow_analytics/qualitative/__init__.py +30 -0
  19. gitflow_analytics/qualitative/classifiers/__init__.py +13 -0
  20. gitflow_analytics/qualitative/classifiers/change_type.py +468 -0
  21. gitflow_analytics/qualitative/classifiers/domain_classifier.py +399 -0
  22. gitflow_analytics/qualitative/classifiers/intent_analyzer.py +436 -0
  23. gitflow_analytics/qualitative/classifiers/risk_analyzer.py +412 -0
  24. gitflow_analytics/qualitative/core/__init__.py +13 -0
  25. gitflow_analytics/qualitative/core/llm_fallback.py +653 -0
  26. gitflow_analytics/qualitative/core/nlp_engine.py +373 -0
  27. gitflow_analytics/qualitative/core/pattern_cache.py +457 -0
  28. gitflow_analytics/qualitative/core/processor.py +540 -0
  29. gitflow_analytics/qualitative/models/__init__.py +25 -0
  30. gitflow_analytics/qualitative/models/schemas.py +272 -0
  31. gitflow_analytics/qualitative/utils/__init__.py +13 -0
  32. gitflow_analytics/qualitative/utils/batch_processor.py +326 -0
  33. gitflow_analytics/qualitative/utils/cost_tracker.py +343 -0
  34. gitflow_analytics/qualitative/utils/metrics.py +347 -0
  35. gitflow_analytics/qualitative/utils/text_processing.py +243 -0
  36. gitflow_analytics/reports/analytics_writer.py +25 -8
  37. gitflow_analytics/reports/csv_writer.py +60 -32
  38. gitflow_analytics/reports/narrative_writer.py +21 -15
  39. gitflow_analytics/tui/__init__.py +5 -0
  40. gitflow_analytics/tui/app.py +721 -0
  41. gitflow_analytics/tui/screens/__init__.py +8 -0
  42. gitflow_analytics/tui/screens/analysis_progress_screen.py +487 -0
  43. gitflow_analytics/tui/screens/configuration_screen.py +547 -0
  44. gitflow_analytics/tui/screens/loading_screen.py +358 -0
  45. gitflow_analytics/tui/screens/main_screen.py +304 -0
  46. gitflow_analytics/tui/screens/results_screen.py +698 -0
  47. gitflow_analytics/tui/widgets/__init__.py +7 -0
  48. gitflow_analytics/tui/widgets/data_table.py +257 -0
  49. gitflow_analytics/tui/widgets/export_modal.py +301 -0
  50. gitflow_analytics/tui/widgets/progress_widget.py +192 -0
  51. gitflow_analytics-1.0.3.dist-info/METADATA +490 -0
  52. gitflow_analytics-1.0.3.dist-info/RECORD +62 -0
  53. gitflow_analytics-1.0.0.dist-info/METADATA +0 -201
  54. gitflow_analytics-1.0.0.dist-info/RECORD +0 -30
  55. {gitflow_analytics-1.0.0.dist-info → gitflow_analytics-1.0.3.dist-info}/WHEEL +0 -0
  56. {gitflow_analytics-1.0.0.dist-info → gitflow_analytics-1.0.3.dist-info}/entry_points.txt +0 -0
  57. {gitflow_analytics-1.0.0.dist-info → gitflow_analytics-1.0.3.dist-info}/licenses/LICENSE +0 -0
  58. {gitflow_analytics-1.0.0.dist-info → gitflow_analytics-1.0.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,721 @@
1
+ """Main TUI application for GitFlow Analytics."""
2
+
3
+ import asyncio
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ from textual.app import App, ComposeResult
8
+ from textual.binding import Binding
9
+
10
+ from .screens.main_screen import MainScreen
11
+ from .screens.configuration_screen import ConfigurationScreen
12
+ from .screens.analysis_progress_screen import AnalysisProgressScreen
13
+ from .screens.results_screen import ResultsScreen
14
+ from .screens.loading_screen import InitializationLoadingScreen
15
+ from gitflow_analytics.config import ConfigLoader, Config
16
+
17
+
18
+ class GitFlowAnalyticsApp(App):
19
+ """
20
+ Main Terminal User Interface application for GitFlow Analytics.
21
+
22
+ WHY: Provides a comprehensive TUI that guides users through the entire
23
+ analytics workflow from configuration to results analysis. Designed to
24
+ be more user-friendly than command-line interface while maintaining
25
+ the power and flexibility of the core analysis engine.
26
+
27
+ DESIGN DECISION: Uses a screen-based navigation model where each major
28
+ workflow step (configuration, analysis, results) has its own dedicated
29
+ screen. This provides clear context separation and allows for complex
30
+ interactions within each workflow step.
31
+ """
32
+
33
+ TITLE = "GitFlow Analytics"
34
+ SUB_TITLE = "Developer Productivity Analysis"
35
+
36
+ CSS = """
37
+ /* Global styles */
38
+ .screen-title {
39
+ text-align: center;
40
+ text-style: bold;
41
+ color: $accent;
42
+ margin: 1;
43
+ padding: 1;
44
+ }
45
+
46
+ .section-title {
47
+ text-style: bold;
48
+ color: $secondary;
49
+ margin: 1 0;
50
+ }
51
+
52
+ .subsection-title {
53
+ text-style: bold;
54
+ color: $warning;
55
+ margin: 1 0 0 0;
56
+ }
57
+
58
+ .help-text {
59
+ color: $text-muted;
60
+ text-style: italic;
61
+ margin: 0 0 1 0;
62
+ }
63
+
64
+ .info-message {
65
+ color: $warning;
66
+ text-style: italic;
67
+ margin: 1 0;
68
+ }
69
+
70
+ /* Panel styles */
71
+ .status-panel {
72
+ border: solid $primary;
73
+ margin: 1;
74
+ padding: 1;
75
+ height: auto;
76
+ }
77
+
78
+ .actions-panel {
79
+ border: solid $secondary;
80
+ margin: 1;
81
+ padding: 1;
82
+ height: auto;
83
+ }
84
+
85
+ .stats-panel {
86
+ border: solid $accent;
87
+ margin: 1;
88
+ padding: 1;
89
+ height: auto;
90
+ }
91
+
92
+ .progress-panel {
93
+ height: 8;
94
+ border: solid $primary;
95
+ margin: 1;
96
+ padding: 1;
97
+ }
98
+
99
+ .log-panel {
100
+ border: solid $accent;
101
+ margin: 1;
102
+ padding: 1;
103
+ min-height: 10;
104
+ }
105
+
106
+ .panel-title {
107
+ text-style: bold;
108
+ color: $primary;
109
+ margin-bottom: 1;
110
+ }
111
+
112
+ /* Form styles */
113
+ .form-row {
114
+ height: 3;
115
+ margin: 1 0;
116
+ }
117
+
118
+ .form-label {
119
+ width: 25;
120
+ padding: 1 0;
121
+ }
122
+
123
+ .form-input {
124
+ width: 1fr;
125
+ }
126
+
127
+ /* Button styles */
128
+ .button-bar {
129
+ dock: bottom;
130
+ height: 3;
131
+ align: center middle;
132
+ margin: 1;
133
+ }
134
+
135
+ .action-bar {
136
+ height: 3;
137
+ align: center middle;
138
+ margin: 1 0;
139
+ }
140
+
141
+ /* Modal styles */
142
+ #config-modal {
143
+ background: $surface;
144
+ border: thick $primary;
145
+ width: 90%;
146
+ height: 85%;
147
+ padding: 1;
148
+ }
149
+
150
+ .modal-title {
151
+ text-align: center;
152
+ text-style: bold;
153
+ color: $primary;
154
+ margin-bottom: 1;
155
+ }
156
+
157
+ /* Validation styles */
158
+ .validation-error {
159
+ color: $error;
160
+ text-style: bold;
161
+ }
162
+
163
+ .validation-success {
164
+ color: $success;
165
+ text-style: bold;
166
+ }
167
+
168
+ /* Table styles */
169
+ EnhancedDataTable {
170
+ height: auto;
171
+ min-height: 20;
172
+ }
173
+
174
+ EnhancedDataTable > .datatable--header {
175
+ background: $primary 10%;
176
+ color: $primary;
177
+ text-style: bold;
178
+ }
179
+
180
+ EnhancedDataTable > .datatable--row-hover {
181
+ background: $accent 20%;
182
+ }
183
+
184
+ EnhancedDataTable > .datatable--row-cursor {
185
+ background: $primary 30%;
186
+ }
187
+
188
+ /* Progress widget styles */
189
+ AnalysisProgressWidget {
190
+ height: auto;
191
+ border: solid $primary;
192
+ margin: 1;
193
+ padding: 1;
194
+ }
195
+
196
+ .progress-title {
197
+ text-style: bold;
198
+ color: $primary;
199
+ margin-bottom: 1;
200
+ }
201
+
202
+ .progress-status {
203
+ color: $text;
204
+ margin-top: 1;
205
+ }
206
+
207
+ .progress-eta {
208
+ color: $accent;
209
+ text-style: italic;
210
+ }
211
+
212
+ /* Loading screen styles */
213
+ #loading-container {
214
+ height: 100%;
215
+ align: center middle;
216
+ }
217
+
218
+ #loading-content {
219
+ width: 80%;
220
+ max-width: 100;
221
+ align: center middle;
222
+ margin: 2;
223
+ }
224
+
225
+ #main-spinner {
226
+ margin: 1;
227
+ align: center middle;
228
+ }
229
+
230
+ #loading-steps {
231
+ margin-top: 1;
232
+ }
233
+
234
+ #loading-steps > Static {
235
+ margin: 0 0 0 2;
236
+ padding: 0;
237
+ }
238
+
239
+ /* Utility classes */
240
+ .hidden {
241
+ display: none;
242
+ }
243
+
244
+ .center {
245
+ text-align: center;
246
+ }
247
+
248
+ .bold {
249
+ text-style: bold;
250
+ }
251
+
252
+ .italic {
253
+ text-style: italic;
254
+ }
255
+
256
+ .error {
257
+ color: $error;
258
+ }
259
+
260
+ .success {
261
+ color: $success;
262
+ }
263
+
264
+ .warning {
265
+ color: $warning;
266
+ }
267
+ """
268
+
269
+ BINDINGS = [
270
+ Binding("ctrl+q", "quit", "Quit", priority=True),
271
+ Binding("ctrl+c", "quit", "Quit", priority=True),
272
+ Binding("f1", "help", "Help"),
273
+ Binding("ctrl+d", "toggle_dark", "Toggle Dark Mode"),
274
+ ]
275
+
276
+ def __init__(self) -> None:
277
+ super().__init__()
278
+ self.config: Optional[Config] = None
279
+ self.config_path: Optional[Path] = None
280
+ self.initialization_complete = False
281
+ self._nlp_engine = None
282
+
283
+ def compose(self) -> ComposeResult:
284
+ """
285
+ Compose the main application. We don't yield screens here during startup
286
+ to avoid screen stack management issues.
287
+
288
+ WHY: Instead of yielding screens in compose, we handle the initial screen
289
+ setup in on_mount to ensure proper screen stack management and avoid
290
+ IndexError when transitioning between screens.
291
+ """
292
+ # Don't yield screens here - handle in on_mount for proper screen management
293
+ # Return empty to avoid NoneType iteration error
294
+ return iter([])
295
+
296
+ def on_mount(self) -> None:
297
+ """
298
+ Handle application startup with loading screen.
299
+
300
+ WHY: Shows loading screen immediately while heavy initialization
301
+ happens in the background, providing better user experience.
302
+ Using push_screen instead of compose to avoid screen stack issues.
303
+ """
304
+ # Set up application title
305
+ self.title = "GitFlow Analytics"
306
+ self.sub_title = "Developer Productivity Analysis"
307
+
308
+ # Start with loading screen during initialization
309
+ if not self.initialization_complete:
310
+ loading_screen = InitializationLoadingScreen(
311
+ config_loader_func=self._load_default_config,
312
+ nlp_init_func=self._initialize_nlp_engine,
313
+ loading_message="Initializing GitFlow Analytics...",
314
+ id="loading-screen"
315
+ )
316
+ self.push_screen(loading_screen)
317
+ else:
318
+ # Show main screen if already initialized
319
+ main_screen = MainScreen(self.config, self.config_path, id="main-screen")
320
+ self.push_screen(main_screen)
321
+
322
+ def _load_default_config(self) -> Optional[tuple]:
323
+ """
324
+ Attempt to load configuration from default locations.
325
+
326
+ WHY: Provides automatic configuration discovery to reduce setup
327
+ friction for users who have configurations in standard locations.
328
+ This version is designed to be called from the loading screen.
329
+
330
+ @return: Tuple of (config, config_path) if found, None otherwise
331
+ """
332
+ default_config_paths = [
333
+ Path("config.yaml"),
334
+ Path("gitflow.yaml"),
335
+ Path(".gitflow/config.yaml"),
336
+ Path("~/.gitflow/config.yaml").expanduser(),
337
+ ]
338
+
339
+ for config_path in default_config_paths:
340
+ if config_path.exists():
341
+ try:
342
+ config = ConfigLoader.load(config_path)
343
+ return (config, config_path)
344
+
345
+ except Exception as e:
346
+ # Log error but continue trying other paths
347
+ continue
348
+
349
+ return None
350
+
351
+ def _initialize_nlp_engine(self, config: Optional[Config] = None) -> Optional[object]:
352
+ """
353
+ Initialize the NLP engine for qualitative analysis.
354
+
355
+ WHY: spaCy model loading can be slow and should happen during
356
+ the loading screen to provide user feedback. This method handles
357
+ the heavy NLP initialization work.
358
+
359
+ @param config: Configuration object with qualitative settings
360
+ @return: Initialized NLP engine object or None if not needed/failed
361
+ """
362
+ if not config or not getattr(config, 'qualitative', None):
363
+ return None
364
+
365
+ if not config.qualitative.enabled:
366
+ return None
367
+
368
+ try:
369
+ # Import here to avoid slow imports at module level
370
+ from gitflow_analytics.qualitative.core.nlp_engine import NLPEngine
371
+
372
+ # Initialize the NLP engine (this loads spaCy models)
373
+ nlp_engine = NLPEngine(config.qualitative.nlp)
374
+
375
+ # Validate the setup
376
+ is_valid, issues = nlp_engine.validate_setup()
377
+ if not is_valid:
378
+ # Return None if validation fails, but don't raise exception
379
+ return None
380
+
381
+ return nlp_engine
382
+
383
+ except ImportError:
384
+ # Qualitative analysis dependencies not available
385
+ return None
386
+ except Exception:
387
+ # Other initialization errors
388
+ return None
389
+
390
+ def on_initialization_loading_screen_initialization_complete(
391
+ self, message: InitializationLoadingScreen.InitializationComplete
392
+ ) -> None:
393
+ """
394
+ Handle completion of initialization loading.
395
+
396
+ WHY: When the loading screen completes initialization, we need to
397
+ update the app state and transition to the main screen. Using pop_screen
398
+ and push_screen for proper screen stack management.
399
+ """
400
+ initialization_data = message.data
401
+
402
+ # Update app state with loaded configuration
403
+ config_result = initialization_data.get('config')
404
+ if config_result:
405
+ self.config, self.config_path = config_result
406
+
407
+ # Store NLP engine for later use
408
+ self._nlp_engine = initialization_data.get('nlp')
409
+
410
+ # Mark initialization as complete
411
+ self.initialization_complete = True
412
+
413
+ # Transition to main screen by popping loading screen and pushing main screen
414
+ try:
415
+ self.pop_screen() # Remove loading screen
416
+ except:
417
+ pass # Ignore if no screen to pop
418
+
419
+ main_screen = MainScreen(self.config, self.config_path, id="main-screen")
420
+ self.push_screen(main_screen)
421
+
422
+ # Show success notification if config was loaded
423
+ if self.config and self.config_path:
424
+ self.notify(f"Loaded configuration from {self.config_path}", severity="info")
425
+
426
+ def on_initialization_loading_screen_loading_cancelled(
427
+ self, message: InitializationLoadingScreen.LoadingCancelled
428
+ ) -> None:
429
+ """
430
+ Handle cancellation of initialization loading.
431
+
432
+ WHY: If user cancels loading, we should still show the main screen
433
+ but without the loaded configuration. Using pop_screen and push_screen
434
+ for proper screen stack management.
435
+ """
436
+ # Mark initialization as complete (even if cancelled)
437
+ self.initialization_complete = True
438
+
439
+ # Transition to main screen without configuration
440
+ try:
441
+ self.pop_screen() # Remove loading screen
442
+ except:
443
+ pass # Ignore if no screen to pop
444
+
445
+ main_screen = MainScreen(None, None, id="main-screen")
446
+ self.push_screen(main_screen)
447
+
448
+ self.notify("Initialization cancelled. You can load configuration manually.", severity="warning")
449
+
450
+ def on_main_screen_new_analysis_requested(self, message: MainScreen.NewAnalysisRequested) -> None:
451
+ """Handle new analysis request from main screen."""
452
+ if not self.config:
453
+ self.notify("Please load or create a configuration first", severity="error")
454
+ return
455
+
456
+ # Launch analysis progress screen
457
+ analysis_screen = AnalysisProgressScreen(
458
+ config=self.config,
459
+ weeks=12, # TODO: Get from config or user preference
460
+ enable_qualitative=getattr(self.config, 'qualitative', None) and self.config.qualitative.enabled
461
+ )
462
+
463
+ self.push_screen(analysis_screen)
464
+
465
+ def on_main_screen_configuration_requested(self, message: MainScreen.ConfigurationRequested) -> None:
466
+ """Handle configuration request from main screen."""
467
+ config_screen = ConfigurationScreen(
468
+ config_path=self.config_path,
469
+ config=self.config
470
+ )
471
+
472
+ def handle_config_result(config: Optional[Config]) -> None:
473
+ if config:
474
+ self.config = config
475
+ # TODO: Set config_path if provided
476
+
477
+ # Update main screen
478
+ main_screen = self.query_one("#main-screen", MainScreen)
479
+ main_screen.update_config(self.config, self.config_path)
480
+
481
+ self.notify("Configuration updated successfully", severity="success")
482
+
483
+ self.push_screen(config_screen, handle_config_result)
484
+
485
+ def on_main_screen_cache_status_requested(self, message: MainScreen.CacheStatusRequested) -> None:
486
+ """Handle cache status request from main screen."""
487
+ if not self.config:
488
+ self.notify("No configuration loaded", severity="error")
489
+ return
490
+
491
+ try:
492
+ from gitflow_analytics.core.cache import GitAnalysisCache
493
+
494
+ cache = GitAnalysisCache(self.config.cache.directory)
495
+ stats = cache.get_cache_stats()
496
+
497
+ # Calculate cache size
498
+ import os
499
+ cache_size = 0
500
+ try:
501
+ for root, _dirs, files in os.walk(self.config.cache.directory):
502
+ for f in files:
503
+ cache_size += os.path.getsize(os.path.join(root, f))
504
+ cache_size_mb = cache_size / 1024 / 1024
505
+ except:
506
+ cache_size_mb = 0
507
+
508
+ message_text = f"""Cache Statistics:
509
+ • Location: {self.config.cache.directory}
510
+ • Cached commits: {stats['cached_commits']:,}
511
+ • Cached PRs: {stats['cached_prs']:,}
512
+ • Cached issues: {stats['cached_issues']:,}
513
+ • Stale entries: {stats['stale_commits']:,}
514
+ • Cache size: {cache_size_mb:.1f} MB
515
+ • TTL: {self.config.cache.ttl_hours} hours"""
516
+
517
+ self.notify(message_text, severity="info")
518
+
519
+ except Exception as e:
520
+ self.notify(f"Failed to get cache statistics: {e}", severity="error")
521
+
522
+ def on_main_screen_identity_management_requested(self, message: MainScreen.IdentityManagementRequested) -> None:
523
+ """Handle identity management request from main screen."""
524
+ if not self.config:
525
+ self.notify("No configuration loaded", severity="error")
526
+ return
527
+
528
+ try:
529
+ from gitflow_analytics.core.identity import DeveloperIdentityResolver
530
+
531
+ identity_resolver = DeveloperIdentityResolver(
532
+ self.config.cache.directory / "identities.db"
533
+ )
534
+
535
+ developers = identity_resolver.get_developer_stats()
536
+
537
+ if not developers:
538
+ self.notify("No developer identities found. Run analysis first.", severity="info")
539
+ return
540
+
541
+ # Show top developers
542
+ top_devs = sorted(developers, key=lambda d: d['total_commits'], reverse=True)[:10]
543
+
544
+ dev_list = []
545
+ for dev in top_devs:
546
+ dev_list.append(f"• {dev['primary_name']}: {dev['total_commits']} commits, {dev['alias_count']} aliases")
547
+
548
+ message_text = f"""Developer Identity Statistics:
549
+ • Total unique developers: {len(developers)}
550
+ • Manual mappings: {len(self.config.analysis.manual_identity_mappings) if self.config.analysis.manual_identity_mappings else 0}
551
+
552
+ Top Contributors:
553
+ {chr(10).join(dev_list)}
554
+
555
+ Use the CLI 'merge-identity' command to merge duplicate identities."""
556
+
557
+ self.notify(message_text, severity="info")
558
+
559
+ except Exception as e:
560
+ self.notify(f"Failed to get identity information: {e}", severity="error")
561
+
562
+ def on_main_screen_help_requested(self, message: MainScreen.HelpRequested) -> None:
563
+ """Handle help request from main screen."""
564
+ help_text = """GitFlow Analytics - Terminal UI Help
565
+
566
+ 🚀 Getting Started:
567
+ 1. Load or create a configuration file (API keys, repositories)
568
+ 2. Configure analysis settings (time period, options)
569
+ 3. Run analysis to process your repositories
570
+ 4. Explore results and export reports
571
+
572
+ ⌨️ Key Bindings:
573
+ • Ctrl+Q / Ctrl+C: Quit application
574
+ • F1: Show this help
575
+ • Ctrl+D: Toggle dark/light mode
576
+ • Escape: Go back/cancel current action
577
+
578
+ 📁 Configuration:
579
+ • GitHub Personal Access Token required
580
+ • OpenRouter API key for qualitative analysis
581
+ • JIRA optional for enhanced ticket tracking
582
+
583
+ 🔧 Analysis Features:
584
+ • Git commit analysis with developer identity resolution
585
+ • Pull request metrics and timing analysis
586
+ • Qualitative analysis using AI for commit categorization
587
+ • DORA metrics calculation
588
+ • Story point tracking from commit messages
589
+
590
+ 📊 Export Options:
591
+ • CSV reports for spreadsheet analysis
592
+ • JSON export for API integration
593
+ • Markdown reports for documentation
594
+
595
+ 💡 Tips:
596
+ • Use organization auto-discovery for multiple repositories
597
+ • Enable qualitative analysis for deeper insights
598
+ • Review identity mappings for accurate attribution
599
+
600
+ For more information: https://github.com/bobmatnyc/gitflow-analytics"""
601
+
602
+ self.notify(help_text, severity="info")
603
+
604
+ def action_quit(self) -> None:
605
+ """
606
+ Quit the application with confirmation if analysis is running.
607
+
608
+ WHY: Provides safe exit that checks for running operations to
609
+ prevent data loss or corruption from incomplete analysis.
610
+ """
611
+ # Check if analysis is running
612
+ try:
613
+ analysis_screen = self.query("AnalysisProgressScreen")
614
+ if analysis_screen:
615
+ # TODO: Show confirmation dialog
616
+ pass
617
+ except:
618
+ pass
619
+
620
+ self.exit()
621
+
622
+ def action_help(self) -> None:
623
+ """Show application help."""
624
+ # Trigger help from main screen if available
625
+ try:
626
+ main_screen = self.query_one("#main-screen", MainScreen)
627
+ main_screen.action_help()
628
+ except:
629
+ # Fallback to direct help
630
+ self.on_main_screen_help_requested(MainScreen.HelpRequested())
631
+
632
+ def action_toggle_dark(self) -> None:
633
+ """
634
+ Toggle between dark and light themes.
635
+
636
+ WHY: Provides theme flexibility for different user preferences
637
+ and working environments (bright vs dim lighting conditions).
638
+ """
639
+ self.dark = not self.dark
640
+ theme = "dark" if self.dark else "light"
641
+ self.notify(f"Switched to {theme} theme", severity="info")
642
+
643
+ def get_current_config(self) -> Optional[Config]:
644
+ """Get the currently loaded configuration."""
645
+ return self.config
646
+
647
+ def get_current_config_path(self) -> Optional[Path]:
648
+ """Get the path of the currently loaded configuration."""
649
+ return self.config_path
650
+
651
+ def get_nlp_engine(self) -> Optional[object]:
652
+ """
653
+ Get the initialized NLP engine for qualitative analysis.
654
+
655
+ WHY: Provides access to the pre-loaded NLP engine to avoid
656
+ re-initialization overhead during analysis operations.
657
+
658
+ @return: Initialized NLP engine or None if not available
659
+ """
660
+ return self._nlp_engine
661
+
662
+ def update_config(self, config: Config, config_path: Optional[Path] = None) -> None:
663
+ """
664
+ Update the application configuration and refresh relevant screens.
665
+
666
+ WHY: Provides centralized configuration updates that can be called
667
+ from any screen to ensure all parts of the application stay in sync.
668
+ """
669
+ self.config = config
670
+ self.config_path = config_path
671
+
672
+ # Update main screen if visible
673
+ try:
674
+ main_screen = self.query_one("#main-screen", MainScreen)
675
+ main_screen.update_config(config, config_path)
676
+ except:
677
+ pass
678
+
679
+ async def run_analysis_async(
680
+ self,
681
+ weeks: int = 12,
682
+ enable_qualitative: bool = True
683
+ ) -> Optional[dict]:
684
+ """
685
+ Run analysis asynchronously and return results.
686
+
687
+ WHY: Provides programmatic access to analysis functionality
688
+ for integration with other systems or automated workflows.
689
+
690
+ @param weeks: Number of weeks to analyze
691
+ @param enable_qualitative: Whether to enable qualitative analysis
692
+ @return: Analysis results dictionary or None if failed
693
+ """
694
+ if not self.config:
695
+ raise ValueError("No configuration loaded")
696
+
697
+ # Create analysis screen
698
+ analysis_screen = AnalysisProgressScreen(
699
+ config=self.config,
700
+ weeks=weeks,
701
+ enable_qualitative=enable_qualitative
702
+ )
703
+
704
+ # Run analysis (this would need to be implemented properly)
705
+ # For now, return None to indicate not implemented
706
+ return None
707
+
708
+
709
+ def main() -> None:
710
+ """
711
+ Main entry point for the TUI application.
712
+
713
+ WHY: Provides a clean entry point that can be called from the CLI
714
+ or used as a standalone application launcher.
715
+ """
716
+ app = GitFlowAnalyticsApp()
717
+ app.run()
718
+
719
+
720
+ if __name__ == "__main__":
721
+ main()