igntui 0.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. igntui/__init__.py +57 -0
  2. igntui/__main__.py +10 -0
  3. igntui/app.py +37 -0
  4. igntui/cli/__init__.py +31 -0
  5. igntui/cli/base.py +104 -0
  6. igntui/cli/commands/__init__.py +17 -0
  7. igntui/cli/commands/cache_cmd.py +95 -0
  8. igntui/cli/commands/generate_cmd.py +85 -0
  9. igntui/cli/commands/list_cmd.py +66 -0
  10. igntui/cli/commands/test_cmd.py +64 -0
  11. igntui/cli/commands/tui_cmd.py +34 -0
  12. igntui/cli/parser.py +162 -0
  13. igntui/cli/setup.py +71 -0
  14. igntui/core/__init__.py +8 -0
  15. igntui/core/api/__init__.py +16 -0
  16. igntui/core/api/client.py +222 -0
  17. igntui/core/api/errors.py +29 -0
  18. igntui/core/api/rate_limiter.py +30 -0
  19. igntui/core/api/request_handler.py +124 -0
  20. igntui/core/api/response.py +16 -0
  21. igntui/core/cache.py +284 -0
  22. igntui/core/config.py +174 -0
  23. igntui/core/search.py +354 -0
  24. igntui/main.py +74 -0
  25. igntui/tui/__init__.py +22 -0
  26. igntui/tui/actions.py +254 -0
  27. igntui/tui/app.py +246 -0
  28. igntui/tui/curses_setup.py +88 -0
  29. igntui/tui/event_handler.py +310 -0
  30. igntui/tui/lifecycle.py +102 -0
  31. igntui/tui/renderer.py +108 -0
  32. igntui/tui/state.py +77 -0
  33. igntui/ui/__init__.py +33 -0
  34. igntui/ui/components/__init__.py +31 -0
  35. igntui/ui/components/base_dialog.py +75 -0
  36. igntui/ui/components/base_panel.py +85 -0
  37. igntui/ui/components/confirm_dialog.py +100 -0
  38. igntui/ui/components/content_panel.py +77 -0
  39. igntui/ui/components/export_dialog.py +139 -0
  40. igntui/ui/components/help_dialog.py +161 -0
  41. igntui/ui/components/save_dialog.py +109 -0
  42. igntui/ui/components/search_panel.py +52 -0
  43. igntui/ui/components/selected_panel.py +107 -0
  44. igntui/ui/components/splash.py +236 -0
  45. igntui/ui/components/status_bar.py +78 -0
  46. igntui/ui/components/templates_panel.py +153 -0
  47. igntui/utils/__init__.py +19 -0
  48. igntui/utils/logging.py +288 -0
  49. igntui-0.0.0.dist-info/METADATA +425 -0
  50. igntui-0.0.0.dist-info/RECORD +52 -0
  51. igntui-0.0.0.dist-info/WHEEL +4 -0
  52. igntui-0.0.0.dist-info/entry_points.txt +4 -0
igntui/__init__.py ADDED
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+
5
+ import platform
6
+ import sys
7
+
8
+ __version__ = "0.0.0"
9
+ __author__ = "Mohammad Abu Mattar"
10
+ __email__ = "info@mkabumattar.com"
11
+ __description__ = """A professional TUI application for generating .gitignore files from templates
12
+ provided by gitignore.io with advanced search, caching, and performance features.
13
+ """
14
+
15
+
16
+ def get_version_string() -> str:
17
+ python_version = (
18
+ f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
19
+ )
20
+ os_name = platform.system()
21
+ os_version = platform.release()
22
+ return f"igntui/{__version__} Python/{python_version} {os_name}/{os_version}"
23
+
24
+
25
+ from .core.api import GitIgnoreAPI, APIResponse
26
+ from .core.config import config
27
+ from .core.search import SearchManager, SearchMode
28
+ from .core.cache import CacheManager, TemplateCache
29
+
30
+ from .main import cli_main, tui_main
31
+
32
+ try:
33
+ from .app import run_tui
34
+
35
+ TUI_AVAILABLE = True
36
+ except ImportError:
37
+ run_tui = None
38
+ TUI_AVAILABLE = False
39
+
40
+ __all__ = [
41
+ "__version__",
42
+ "__author__",
43
+ "__email__",
44
+ "__description__",
45
+ "get_version_string",
46
+ "GitIgnoreAPI",
47
+ "APIResponse",
48
+ "config",
49
+ "SearchManager",
50
+ "SearchMode",
51
+ "CacheManager",
52
+ "TemplateCache",
53
+ "cli_main",
54
+ "tui_main",
55
+ "run_tui",
56
+ "TUI_AVAILABLE",
57
+ ]
igntui/__main__.py ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+
5
+ import sys
6
+
7
+ from .main import cli_main
8
+
9
+ if __name__ == "__main__":
10
+ cli_main()
igntui/app.py ADDED
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+
5
+ import curses
6
+ import sys
7
+ from typing import Optional
8
+
9
+
10
+ def run_tui(show_splash: bool = True) -> int:
11
+ try:
12
+ from .tui import GitIgnoreTUI
13
+
14
+ def main(stdscr):
15
+ tui = GitIgnoreTUI(stdscr, show_splash=show_splash)
16
+ return tui.run()
17
+
18
+ exit_code = curses.wrapper(main)
19
+ return exit_code if exit_code is not None else 0
20
+
21
+ except ImportError as e:
22
+ if "curses" in str(e):
23
+ print("Error: curses module not available.")
24
+ print("On Windows, install: pip install windows-curses")
25
+ return 1
26
+ else:
27
+ raise
28
+ except KeyboardInterrupt:
29
+ print("\nOperation cancelled by user")
30
+ return 1
31
+ except Exception as e:
32
+ print(f"Error running TUI: {e}")
33
+ return 1
34
+
35
+
36
+ if __name__ == "__main__":
37
+ sys.exit(run_tui())
igntui/cli/__init__.py ADDED
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+
5
+ from .base import BaseCLI, CLICommand, safe_exit
6
+ from .setup import setup_logging, check_curses_availability, print_curses_error
7
+ from .parser import create_base_parser, create_command_parser, get_command_instance
8
+ from .commands import (
9
+ ListCommand,
10
+ GenerateCommand,
11
+ TUICommand,
12
+ CacheCommand,
13
+ TestCommand,
14
+ )
15
+
16
+ __all__ = [
17
+ "BaseCLI",
18
+ "CLICommand",
19
+ "safe_exit",
20
+ "setup_logging",
21
+ "check_curses_availability",
22
+ "print_curses_error",
23
+ "create_base_parser",
24
+ "create_command_parser",
25
+ "get_command_instance",
26
+ "ListCommand",
27
+ "GenerateCommand",
28
+ "TUICommand",
29
+ "CacheCommand",
30
+ "TestCommand",
31
+ ]
igntui/cli/base.py ADDED
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+
5
+ import argparse
6
+ import logging
7
+ import sys
8
+ from typing import List, Optional
9
+
10
+ from .. import __version__, get_version_string
11
+ from ..core.api import GitIgnoreAPI
12
+ from ..core.config import Config
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class BaseCLI:
18
+ def __init__(self):
19
+ self.config = Config()
20
+ self.api = GitIgnoreAPI()
21
+
22
+ def setup_logging(self, verbose: bool = False):
23
+ level = logging.DEBUG if verbose else logging.INFO
24
+ logging.basicConfig(
25
+ level=level, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
26
+ )
27
+
28
+ def check_terminal_requirements(self) -> bool:
29
+ try:
30
+ import curses
31
+
32
+ stdscr = curses.initscr()
33
+ curses.endwin()
34
+ return True
35
+ except ImportError:
36
+ print("Error: curses module not available.")
37
+ print("On Windows, install: pip install windows-curses")
38
+ return False
39
+ except Exception as e:
40
+ print(f"Error: Terminal not compatible with curses: {e}")
41
+ return False
42
+
43
+ def handle_api_error(self, error: Exception) -> None:
44
+ if "connection" in str(error).lower() or "timeout" in str(error).lower():
45
+ print("Error: Cannot connect to gitignore.io API")
46
+ print("Please check your internet connection and try again.")
47
+ elif "not found" in str(error).lower() or "404" in str(error):
48
+ print("Error: Template not found")
49
+ print("Use 'igntui list' to see available templates.")
50
+ else:
51
+ print(f"Error: {error}")
52
+
53
+ logger.error(f"API error: {error}")
54
+
55
+
56
+ class CLICommand:
57
+ def __init__(self, cli: BaseCLI):
58
+ self.cli = cli
59
+
60
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
61
+ raise NotImplementedError
62
+
63
+ def execute(self, args: argparse.Namespace) -> int:
64
+ raise NotImplementedError
65
+
66
+
67
+ def create_base_parser() -> argparse.ArgumentParser:
68
+ parser = argparse.ArgumentParser(
69
+ prog="igntui",
70
+ description="Interactive TUI for generating .gitignore files from gitignore.io templates",
71
+ formatter_class=argparse.RawDescriptionHelpFormatter,
72
+ epilog="""
73
+ Examples:
74
+ igntui # Launch interactive TUI
75
+ igntui list # List available templates
76
+ igntui generate python # Generate .gitignore for Python
77
+ igntui test # Test API connection
78
+
79
+ For more information, visit: https://github.com/toptal/gitignore.io
80
+ """,
81
+ )
82
+
83
+ parser.add_argument("--version", action="version", version=get_version_string())
84
+
85
+ parser.add_argument(
86
+ "-v", "--verbose", action="store_true", help="Enable verbose output"
87
+ )
88
+
89
+ parser.add_argument(
90
+ "--no-color", action="store_true", help="Disable colored output"
91
+ )
92
+
93
+ return parser
94
+
95
+
96
+ def safe_exit(code: int = 0) -> None:
97
+ try:
98
+ import curses
99
+
100
+ curses.endwin()
101
+ except:
102
+ pass
103
+
104
+ sys.exit(code)
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+
5
+ from .list_cmd import ListCommand
6
+ from .generate_cmd import GenerateCommand
7
+ from .tui_cmd import TUICommand
8
+ from .cache_cmd import CacheCommand
9
+ from .test_cmd import TestCommand
10
+
11
+ __all__ = [
12
+ "ListCommand",
13
+ "GenerateCommand",
14
+ "TUICommand",
15
+ "CacheCommand",
16
+ "TestCommand",
17
+ ]
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+
5
+ import argparse
6
+ from datetime import datetime
7
+
8
+ from ..base import CLICommand
9
+
10
+
11
+ class CacheCommand(CLICommand):
12
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
13
+ subparsers = parser.add_subparsers(dest="cache_action", help="Cache action")
14
+
15
+ clear_parser = subparsers.add_parser("clear", help="Clear cache")
16
+ clear_parser.add_argument(
17
+ "--force", action="store_true", help="Skip confirmation prompt"
18
+ )
19
+
20
+ subparsers.add_parser("stats", help="Show cache statistics")
21
+ subparsers.add_parser("info", help="Show cache information")
22
+
23
+ def execute(self, args: argparse.Namespace) -> int:
24
+ try:
25
+ from ...core.cache import CacheManager
26
+ from ...core.config import config
27
+
28
+ cache_dir = config.get_cache_dir()
29
+ cache = CacheManager(cache_dir=str(cache_dir))
30
+
31
+ if not args.cache_action or args.cache_action == "info":
32
+ return self._show_info(cache)
33
+ elif args.cache_action == "stats":
34
+ return self._show_stats(cache)
35
+ elif args.cache_action == "clear":
36
+ return self._clear_cache(
37
+ cache, args.force if hasattr(args, "force") else False
38
+ )
39
+ else:
40
+ print(f"Unknown cache action: {args.cache_action}")
41
+ return 1
42
+
43
+ except Exception as e:
44
+ print(f"Error managing cache: {e}")
45
+ if hasattr(args, "verbose") and args.verbose:
46
+ import traceback
47
+
48
+ traceback.print_exc()
49
+ return 1
50
+
51
+ def _show_info(self, cache: "CacheManager") -> int:
52
+ print("Cache Information:")
53
+ print(f" Location: {cache.cache_dir}")
54
+ print(f" TTL: {cache.default_ttl} seconds")
55
+
56
+ try:
57
+ from pathlib import Path
58
+
59
+ template_cache_file = Path(cache.cache_dir) / "templates.json"
60
+ if template_cache_file.exists():
61
+ import json
62
+
63
+ with open(template_cache_file, "r") as f:
64
+ data = json.load(f)
65
+ templates = data.get("templates", [])
66
+ print(f" Template cache: Valid")
67
+ print(f" Templates cached: {len(templates)}")
68
+ else:
69
+ print(f" Template cache: Not found")
70
+ except Exception as e:
71
+ print(f" Template cache: Error reading ({e})")
72
+
73
+ return 0
74
+
75
+ def _show_stats(self, cache: "CacheManager") -> int:
76
+ stats = cache.get_stats()
77
+
78
+ print("Cache Statistics:")
79
+ for key, value in stats.items():
80
+ if isinstance(value, datetime):
81
+ value = value.strftime("%Y-%m-%d %H:%M:%S")
82
+ print(f" {key}: {value}")
83
+
84
+ return 0
85
+
86
+ def _clear_cache(self, cache: "CacheManager", force: bool = False) -> int:
87
+ if not force:
88
+ response = input("Clear cache? This will remove all cached data. (y/N): ")
89
+ if response.lower() not in ["y", "yes"]:
90
+ print("Cache clear cancelled")
91
+ return 1
92
+
93
+ cache.clear_all()
94
+ print("Cache cleared successfully")
95
+ return 0
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+
5
+ import argparse
6
+ import os
7
+ from pathlib import Path
8
+
9
+ from ..base import CLICommand
10
+
11
+
12
+ class GenerateCommand(CLICommand):
13
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
14
+ parser.add_argument(
15
+ "templates", nargs="+", help="Template names to generate (space-separated)"
16
+ )
17
+ parser.add_argument(
18
+ "--output",
19
+ "-o",
20
+ type=Path,
21
+ metavar="FILE",
22
+ help="Output file path (default: print to stdout)",
23
+ )
24
+ parser.add_argument(
25
+ "--append",
26
+ "-a",
27
+ action="store_true",
28
+ help="Append to existing file instead of overwriting",
29
+ )
30
+ parser.add_argument(
31
+ "--force",
32
+ "-f",
33
+ action="store_true",
34
+ help="Force overwrite without confirmation",
35
+ )
36
+
37
+ def execute(self, args: argparse.Namespace) -> int:
38
+ try:
39
+ if hasattr(args, "verbose") and args.verbose:
40
+ print(f"Generating .gitignore for: {', '.join(args.templates)}")
41
+
42
+ response = self.cli.api.get_templates(args.templates)
43
+
44
+ if not response.success:
45
+ print(f"Error: {response.error_message}")
46
+ return 1
47
+
48
+ content = response.data
49
+
50
+ if not content.strip():
51
+ print("Warning: Generated content is empty")
52
+ return 1
53
+
54
+ if not args.output:
55
+ print(content)
56
+ return 0
57
+
58
+ output_file = args.output
59
+
60
+ if output_file.exists() and not args.append and not args.force:
61
+ response_text = input(
62
+ f"File '{output_file}' exists. Overwrite? (y/N): "
63
+ )
64
+ if response_text.lower() not in ("y", "yes"):
65
+ print("Operation cancelled")
66
+ return 1
67
+
68
+ mode = "a" if args.append else "w"
69
+ try:
70
+ with open(output_file, mode, encoding="utf-8") as f:
71
+ if args.append and output_file.stat().st_size > 0:
72
+ f.write("\n\n# Generated by igntui\n")
73
+ f.write(content)
74
+
75
+ action = "Appended to" if args.append else "Generated"
76
+ print(f"✓ {action} {output_file}")
77
+ return 0
78
+
79
+ except IOError as e:
80
+ print(f"Error writing file: {e}")
81
+ return 1
82
+
83
+ except Exception as e:
84
+ self.cli.handle_api_error(e)
85
+ return 1
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+
5
+ import argparse
6
+
7
+ from ..base import CLICommand
8
+
9
+
10
+ class ListCommand(CLICommand):
11
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
12
+ parser.add_argument(
13
+ "--filter", "-f", metavar="PATTERN", help="Filter templates by pattern"
14
+ )
15
+ parser.add_argument(
16
+ "--count", "-c", action="store_true", help="Show count only"
17
+ )
18
+
19
+ def execute(self, args: argparse.Namespace) -> int:
20
+ try:
21
+ if hasattr(args, "verbose") and args.verbose:
22
+ print("Fetching templates from gitignore.io...")
23
+
24
+ response = self.cli.api.list_templates()
25
+
26
+ if not response.success:
27
+ print(f"Error: {response.error_message}")
28
+ return 1
29
+
30
+ templates = response.data
31
+
32
+ if hasattr(args, "filter") and args.filter:
33
+ pattern = args.filter.lower()
34
+ templates = [t for t in templates if pattern in t.lower()]
35
+
36
+ if hasattr(args, "count") and args.count:
37
+ print(f"Found {len(templates)} templates")
38
+ return 0
39
+
40
+ if not templates:
41
+ if hasattr(args, "filter") and args.filter:
42
+ print(f"No templates found matching '{args.filter}'")
43
+ else:
44
+ print("No templates found")
45
+ return 1
46
+
47
+ print(f"Available templates ({len(templates)}):")
48
+ print()
49
+
50
+ max_width = 80
51
+ col_width = max(len(t) for t in templates) + 2
52
+ cols = max(1, max_width // col_width)
53
+
54
+ for i, template in enumerate(templates):
55
+ print(f"{template:<{col_width}}", end="")
56
+ if (i + 1) % cols == 0:
57
+ print()
58
+
59
+ if len(templates) % cols != 0:
60
+ print()
61
+
62
+ return 0
63
+
64
+ except Exception as e:
65
+ self.cli.handle_api_error(e)
66
+ return 1
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+
5
+ import argparse
6
+ import sys
7
+
8
+ from ..base import CLICommand
9
+
10
+
11
+ class TestCommand(CLICommand):
12
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
13
+ parser.add_argument(
14
+ "--timeout",
15
+ type=int,
16
+ default=10,
17
+ help="Connection timeout in seconds (default: 10)",
18
+ )
19
+
20
+ def execute(self, args: argparse.Namespace) -> int:
21
+ try:
22
+ from ...core.api import GitIgnoreAPI
23
+
24
+ print("Testing connection to gitignore.io API...")
25
+ print(f"Timeout: {args.timeout} seconds")
26
+ print()
27
+
28
+ api = GitIgnoreAPI()
29
+
30
+ print("Attempting to connect...", end="", flush=True)
31
+ response = api.test_connection()
32
+
33
+ if not response.success:
34
+ print(" FAILED")
35
+ print(f"Error: {response.error_message}")
36
+ return 1
37
+
38
+ print(" SUCCESS")
39
+ print()
40
+
41
+ print("API Response:")
42
+ response_time = (
43
+ response.response_time if response.response_time is not None else 0.0
44
+ )
45
+ print(f" Response time: {response_time:.3f}s")
46
+ print(f" From cache: {'Yes' if response.from_cache else 'No'}")
47
+ print(
48
+ f" Status code: {response.status_code if response.status_code else 'N/A'}"
49
+ )
50
+
51
+ print()
52
+ print("✓ API is working correctly")
53
+ return 0
54
+
55
+ except KeyboardInterrupt:
56
+ print("\n\nOperation cancelled by user")
57
+ return 1
58
+ except Exception as e:
59
+ print(f"\n\nError testing API: {e}")
60
+ if hasattr(args, "verbose") and args.verbose:
61
+ import traceback
62
+
63
+ traceback.print_exc()
64
+ return 1
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+
5
+ import argparse
6
+
7
+ from ..base import CLICommand
8
+
9
+
10
+ class TUICommand(CLICommand):
11
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
12
+ parser.add_argument(
13
+ "--no-splash", action="store_true", help="Skip splash screen on startup"
14
+ )
15
+
16
+ def execute(self, args: argparse.Namespace) -> int:
17
+ if not self.cli.check_terminal_requirements():
18
+ return 1
19
+
20
+ try:
21
+ from ...app import run_tui
22
+
23
+ return run_tui(show_splash=not args.no_splash)
24
+
25
+ except KeyboardInterrupt:
26
+ print("\nOperation cancelled by user")
27
+ return 1
28
+ except Exception as e:
29
+ print(f"Error launching TUI: {e}")
30
+ if hasattr(args, "verbose") and args.verbose:
31
+ import traceback
32
+
33
+ traceback.print_exc()
34
+ return 1