janito 0.6.0__py3-none-any.whl → 0.8.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 (116) hide show
  1. janito/__main__.py +127 -134
  2. janito/agents/__init__.py +22 -16
  3. janito/agents/agent.py +24 -20
  4. janito/agents/claudeai.py +41 -55
  5. janito/agents/deepseekai.py +47 -0
  6. janito/change/applied_blocks.py +34 -0
  7. janito/change/applier.py +167 -0
  8. janito/change/edit_blocks.py +148 -0
  9. janito/change/finder.py +72 -0
  10. janito/change/request.py +144 -0
  11. janito/change/validator.py +87 -251
  12. janito/change/view/content.py +63 -0
  13. janito/change/{viewer → view}/diff.py +44 -43
  14. janito/change/view/panels.py +201 -0
  15. janito/change/view/sections.py +69 -0
  16. janito/change/view/styling.py +140 -0
  17. janito/change/view/summary.py +37 -0
  18. janito/change/{viewer → view}/themes.py +62 -55
  19. janito/change/view/viewer.py +59 -0
  20. janito/cli/__init__.py +1 -1
  21. janito/cli/commands.py +68 -45
  22. janito/cli/functions.py +66 -111
  23. janito/common.py +132 -53
  24. janito/config.py +99 -101
  25. janito/data/change_prompt.txt +81 -0
  26. janito/data/system_prompt.txt +3 -0
  27. janito/qa.py +56 -66
  28. janito/version.py +22 -22
  29. janito/workspace/__init__.py +8 -7
  30. janito/workspace/analysis.py +120 -120
  31. janito/workspace/models.py +97 -0
  32. janito/workspace/show.py +115 -0
  33. janito/workspace/stats.py +42 -0
  34. janito/workspace/workset.py +135 -0
  35. janito/workspace/workspace.py +335 -0
  36. janito-0.8.0.dist-info/METADATA +106 -0
  37. janito-0.8.0.dist-info/RECORD +40 -0
  38. {janito-0.6.0.dist-info → janito-0.8.0.dist-info}/licenses/LICENSE +20 -20
  39. janito/__init__.py +0 -2
  40. janito/agents/openai.py +0 -53
  41. janito/agents/test.py +0 -34
  42. janito/change/__init__.py +0 -32
  43. janito/change/__main__.py +0 -0
  44. janito/change/analysis/__init__.py +0 -23
  45. janito/change/analysis/__main__.py +0 -7
  46. janito/change/analysis/analyze.py +0 -61
  47. janito/change/analysis/formatting.py +0 -78
  48. janito/change/analysis/options.py +0 -81
  49. janito/change/analysis/prompts.py +0 -98
  50. janito/change/analysis/view/__init__.py +0 -9
  51. janito/change/analysis/view/terminal.py +0 -171
  52. janito/change/applier/__init__.py +0 -5
  53. janito/change/applier/file.py +0 -58
  54. janito/change/applier/main.py +0 -156
  55. janito/change/applier/text.py +0 -245
  56. janito/change/applier/workspace_dir.py +0 -58
  57. janito/change/core.py +0 -131
  58. janito/change/history.py +0 -44
  59. janito/change/operations.py +0 -7
  60. janito/change/parser.py +0 -289
  61. janito/change/play.py +0 -54
  62. janito/change/preview.py +0 -82
  63. janito/change/prompts.py +0 -126
  64. janito/change/test.py +0 -0
  65. janito/change/viewer/__init__.py +0 -11
  66. janito/change/viewer/content.py +0 -66
  67. janito/change/viewer/pager.py +0 -56
  68. janito/change/viewer/panels.py +0 -555
  69. janito/change/viewer/styling.py +0 -103
  70. janito/clear_statement_parser/clear_statement_format.txt +0 -328
  71. janito/clear_statement_parser/examples.txt +0 -326
  72. janito/clear_statement_parser/models.py +0 -104
  73. janito/clear_statement_parser/parser.py +0 -496
  74. janito/cli/base.py +0 -30
  75. janito/cli/handlers/ask.py +0 -22
  76. janito/cli/handlers/demo.py +0 -22
  77. janito/cli/handlers/request.py +0 -24
  78. janito/cli/handlers/scan.py +0 -9
  79. janito/cli/history.py +0 -61
  80. janito/cli/registry.py +0 -26
  81. janito/demo/__init__.py +0 -4
  82. janito/demo/data.py +0 -13
  83. janito/demo/mock_data.py +0 -20
  84. janito/demo/operations.py +0 -45
  85. janito/demo/runner.py +0 -59
  86. janito/demo/scenarios.py +0 -32
  87. janito/prompts.py +0 -2
  88. janito/review.py +0 -13
  89. janito/search_replace/README.md +0 -146
  90. janito/search_replace/__init__.py +0 -6
  91. janito/search_replace/__main__.py +0 -21
  92. janito/search_replace/core.py +0 -119
  93. janito/search_replace/parser.py +0 -52
  94. janito/search_replace/play.py +0 -61
  95. janito/search_replace/replacer.py +0 -36
  96. janito/search_replace/searcher.py +0 -299
  97. janito/shell/__init__.py +0 -39
  98. janito/shell/bus.py +0 -31
  99. janito/shell/commands.py +0 -195
  100. janito/shell/handlers.py +0 -122
  101. janito/shell/history.py +0 -20
  102. janito/shell/processor.py +0 -52
  103. janito/tui/__init__.py +0 -21
  104. janito/tui/base.py +0 -22
  105. janito/tui/flows/__init__.py +0 -5
  106. janito/tui/flows/changes.py +0 -65
  107. janito/tui/flows/content.py +0 -128
  108. janito/tui/flows/selection.py +0 -117
  109. janito/tui/screens/__init__.py +0 -3
  110. janito/tui/screens/app.py +0 -1
  111. janito/workspace/manager.py +0 -48
  112. janito/workspace/scan.py +0 -232
  113. janito-0.6.0.dist-info/METADATA +0 -185
  114. janito-0.6.0.dist-info/RECORD +0 -95
  115. {janito-0.6.0.dist-info → janito-0.8.0.dist-info}/WHEEL +0 -0
  116. {janito-0.6.0.dist-info → janito-0.8.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,201 @@
1
+ from rich.console import Console
2
+ from typing import List, Tuple
3
+ from rich.panel import Panel
4
+ from rich.syntax import Syntax
5
+ from pathlib import Path
6
+ from rich.columns import Columns
7
+ from rich.text import Text
8
+ from rich.layout import Layout
9
+ from ..edit_blocks import EditType, CodeChange
10
+ from .styling import format_content
11
+ from .sections import find_modified_sections
12
+
13
+ # Constants for panel layout
14
+ PANEL_MIN_WIDTH = 40
15
+ PANEL_MAX_WIDTH = 120
16
+ PANEL_PADDING = 4
17
+ COLUMN_SPACING = 4
18
+
19
+ def create_diff_columns(
20
+ original_section: List[str],
21
+ modified_section: List[str],
22
+ filename: str,
23
+ start: int,
24
+ term_width: int,
25
+ context_lines: int = 3,
26
+ current_change: int = 1,
27
+ total_changes: int = 1,
28
+ operation: str = "Edit",
29
+ reason: str = None,
30
+ is_removal: bool = False
31
+ ) -> Tuple[Text, Columns]: # Changed return type to return header and content separately
32
+ """Create side-by-side diff view with consistent styling and context."""
33
+ # Create header with progress info and rule
34
+ header = Text()
35
+ header_text, header_style = create_progress_header(
36
+ operation=operation,
37
+ filename=filename,
38
+ current=current_change,
39
+ total=total_changes,
40
+ term_width=term_width,
41
+ reason=reason
42
+ )
43
+
44
+ header.append(header_text)
45
+ header.append("\n")
46
+ header.append("─" * term_width, style="dim")
47
+
48
+ # Find sections that have changed
49
+ sections = find_modified_sections(
50
+ original_section,
51
+ modified_section,
52
+ context_lines=context_lines
53
+ )
54
+
55
+ if not sections:
56
+ # If no differences, show full content
57
+ diff_columns = _create_single_section_columns(
58
+ original_section,
59
+ modified_section,
60
+ filename,
61
+ start,
62
+ term_width,
63
+ is_removal=is_removal
64
+ )
65
+ else:
66
+ # Create columns for each modified section
67
+ rendered_sections = []
68
+ for i, (orig, mod) in enumerate(sections):
69
+ if i > 0:
70
+ rendered_sections.append(Text("...\n", style="dim"))
71
+
72
+ section_columns = _create_single_section_columns(
73
+ orig, mod, filename, start, term_width, is_removal=is_removal
74
+ )
75
+ rendered_sections.append(section_columns)
76
+
77
+ # Create single column containing all sections
78
+ diff_columns = Columns(
79
+ rendered_sections,
80
+ equal=False,
81
+ expand=False,
82
+ padding=(0, 0)
83
+ )
84
+
85
+ return header, diff_columns
86
+
87
+ def _create_single_section_columns(
88
+ original: List[str],
89
+ modified: List[str],
90
+ filename: str,
91
+ start: int,
92
+ term_width: int,
93
+ is_removal: bool = False # Add parameter with default value
94
+ ) -> Columns:
95
+ """Create columns for a single diff section."""
96
+ left_width, right_width = calculate_panel_widths(
97
+ '\n'.join(original),
98
+ '\n'.join(modified),
99
+ term_width
100
+ )
101
+
102
+ # Format content with correct parameters
103
+ left_content = format_content(
104
+ original,
105
+ search_lines=original,
106
+ replace_lines=modified,
107
+ is_search=True, # This indicates it's the original/search content
108
+ width=left_width,
109
+ is_removal=is_removal # Pass the parameter
110
+ )
111
+ right_content = format_content(
112
+ modified,
113
+ search_lines=original,
114
+ replace_lines=modified,
115
+ is_search=False, # This indicates it's the modified/replace content
116
+ width=right_width
117
+ )
118
+
119
+ left_text = create_panel_text("Original", left_content, left_width)
120
+ right_text = create_panel_text("Modified", right_content, right_width)
121
+
122
+ # Create columns without manual padding
123
+ columns = Columns(
124
+ [
125
+ left_text,
126
+ Text(" " * COLUMN_SPACING),
127
+ right_text
128
+ ],
129
+ equal=False,
130
+ expand=False,
131
+ padding=(0, 0)
132
+ )
133
+
134
+ return columns
135
+
136
+ def calculate_panel_widths(left_content: str, right_content: str, term_width: int) -> Tuple[int, int]:
137
+ """Calculate optimal widths for side-by-side panels with overflow protection."""
138
+ available_width = term_width - PANEL_PADDING - COLUMN_SPACING
139
+
140
+ left_max = max((len(line) for line in left_content.splitlines()), default=0)
141
+ right_max = max((len(line) for line in right_content.splitlines()), default=0)
142
+
143
+ left_max = max(PANEL_MIN_WIDTH, min(left_max, PANEL_MAX_WIDTH))
144
+ right_max = max(PANEL_MIN_WIDTH, min(right_max, PANEL_MAX_WIDTH))
145
+
146
+ if (left_max + right_max) <= available_width:
147
+ return left_max, right_max
148
+
149
+ ratio = left_max / (left_max + right_max)
150
+ left_width = min(
151
+ PANEL_MAX_WIDTH,
152
+ max(PANEL_MIN_WIDTH, int(available_width * ratio))
153
+ )
154
+ right_width = min(
155
+ PANEL_MAX_WIDTH,
156
+ max(PANEL_MIN_WIDTH, available_width - left_width)
157
+ )
158
+
159
+ return left_width, right_width
160
+
161
+ def create_panel_text(title: str, content: str, width: int) -> Text:
162
+ """Create a text container with centered title."""
163
+ text = Text()
164
+ title_padding = (width - len(title)) // 2
165
+ color = "red" if title == "Original" else "green"
166
+ text.append(" " * title_padding + title + " " * title_padding, style=f"{color} bold")
167
+ text.append("\n")
168
+ text.append(content)
169
+ return text
170
+
171
+ def create_progress_header(operation: str, filename: str, current: int, total: int,
172
+ term_width: int, reason: str = None, style: str = "cyan") -> Tuple[Text, str]:
173
+ """Create a header showing filename and global change counter.
174
+
175
+ Args:
176
+ operation: Type of operation being performed
177
+ filename: Name of the file being modified
178
+ current: Current global change number
179
+ total: Total number of changes
180
+ term_width: Width of the terminal
181
+ reason: Optional reason for the change
182
+ style: Color style for the header
183
+
184
+ Returns:
185
+ Tuple of (Rich Text object, style)
186
+ """
187
+ text = Text()
188
+ header = f"{operation}: {filename} | Progress {current}/{total}"
189
+ if reason:
190
+ header += f" | {reason}"
191
+
192
+ # Calculate padding for centering
193
+ padding = (term_width - len(header)) // 2
194
+
195
+ # Create full-width background by padding both sides
196
+ full_line = " " * padding + header + " " * (term_width - len(header) - padding)
197
+
198
+ # Apply background color to entire line with better contrast
199
+ text.append(full_line, style=f"white on dark_blue")
200
+
201
+ return text, style
@@ -0,0 +1,69 @@
1
+ from typing import List, Tuple, Set
2
+
3
+ def find_modified_sections(original: list[str], modified: list[str], context_lines: int = 3) -> list[tuple[list[str], list[str]]]:
4
+ """
5
+ Find modified sections between original and modified text with surrounding context.
6
+ Merges sections with separator lines.
7
+
8
+ Args:
9
+ original: List of original lines
10
+ modified: List of modified linesn
11
+ context_lines: Number of unchanged context lines to include
12
+
13
+ Returns:
14
+ List of tuples containing (original_section, modified_section) line pairs
15
+ """
16
+ # Find different lines
17
+ different_lines = get_different_lines(original, modified)
18
+ if not different_lines:
19
+ return []
20
+
21
+ return create_sections(original, modified, different_lines, context_lines)
22
+
23
+ def get_different_lines(original: List[str], modified: List[str]) -> Set[int]:
24
+ """Find lines that differ between original and modified content"""
25
+ different_lines = set()
26
+ for i in range(max(len(original), len(modified))):
27
+ if i >= len(original) or i >= len(modified):
28
+ different_lines.add(i)
29
+ elif original[i] != modified[i]:
30
+ different_lines.add(i)
31
+ return different_lines
32
+
33
+ def create_sections(original: List[str], modified: List[str],
34
+ different_lines: Set[int], context_lines: int) -> List[Tuple[List[str], List[str]]]:
35
+ """Create sections from different lines with context"""
36
+ current_section = set()
37
+ orig_content = []
38
+ mod_content = []
39
+
40
+ for line_num in sorted(different_lines):
41
+ if not current_section or line_num <= max(current_section) + context_lines * 2:
42
+ current_section.add(line_num)
43
+ else:
44
+ process_section(original, modified, current_section, orig_content,
45
+ mod_content, context_lines)
46
+ current_section = {line_num}
47
+
48
+ if current_section:
49
+ process_section(original, modified, current_section, orig_content,
50
+ mod_content, context_lines)
51
+
52
+ return [(orig_content, mod_content)] if orig_content else []
53
+
54
+ def process_section(original: List[str], modified: List[str],
55
+ current_section: Set[int], orig_content: List[str],
56
+ mod_content: List[str], context_lines: int) -> None:
57
+ """Process a section and add it to the content lists"""
58
+ start = max(0, min(current_section) - context_lines)
59
+ end = min(max(len(original), len(modified)),
60
+ max(current_section) + context_lines + 1)
61
+
62
+ # Add separator if needed
63
+ if orig_content:
64
+ orig_content.append("...")
65
+ mod_content.append("...")
66
+
67
+ # Add section content
68
+ orig_content.extend(original[start:end])
69
+ mod_content.extend(modified[start:end])
@@ -0,0 +1,140 @@
1
+ from rich.text import Text
2
+ from rich.console import Console
3
+ from typing import List
4
+ from .diff import find_similar_lines
5
+ from .themes import DEFAULT_THEME, ColorTheme, ThemeType, get_theme_by_type
6
+
7
+ current_theme = DEFAULT_THEME
8
+
9
+ def set_theme(theme: ColorTheme) -> None:
10
+ """Set the current color theme"""
11
+ global current_theme
12
+ current_theme = theme
13
+
14
+ def get_min_indent(lines: List[str]) -> int:
15
+ """Calculate the minimum indentation level across all non-empty lines."""
16
+ non_empty_lines = [line for line in lines if line.strip()]
17
+ if not non_empty_lines:
18
+ return 0
19
+ return min(len(line) - len(line.lstrip()) for line in non_empty_lines)
20
+
21
+ def apply_line_style(line: str, style: str, width: int, full_width: bool = False) -> Text:
22
+ """Apply consistent styling to a single line with optional full width background"""
23
+ text = Text()
24
+ if full_width:
25
+ # For full width, pad the entire line
26
+ padded_line = line.ljust(width)
27
+ text.append(padded_line, style=style)
28
+ else:
29
+ # Left align the content and pad to width
30
+ text.append(line, style=style)
31
+ padding = " " * max(0, width - len(line))
32
+ text.append(padding, style=style)
33
+
34
+ text.append("\n", style=style)
35
+ return text
36
+
37
+ from textwrap import wrap
38
+
39
+ def format_content(lines: List[str], search_lines: List[str] = None, replace_lines: List[str] = None,
40
+ is_search: bool = False, width: int = 80, is_delete: bool = False,
41
+ is_removal: bool = False, syntax_type: str = None, is_append: bool = False) -> Text:
42
+ """Format content with appropriate styling based on operation type.
43
+
44
+ Handles different content types:
45
+ - Regular content with diff highlighting
46
+ - Delete operations (red background)
47
+ - Append operations (green background)
48
+ - Text wrapping for long lines
49
+ """
50
+ text = Text()
51
+
52
+ # For delete operations, show all content with red background in a single column
53
+ if is_delete or is_removal:
54
+ bg_color = current_theme.line_backgrounds['removed' if is_removal else 'deleted']
55
+ style = f"{current_theme.text_color} on {bg_color}"
56
+ min_indent = get_min_indent(lines)
57
+ for line in lines:
58
+ processed_line = line[min_indent:] if line.strip() else line
59
+ text.append(apply_line_style(processed_line, style, width, full_width=True))
60
+ return text
61
+
62
+ # For append operations, show all lines as added
63
+ if is_append:
64
+ bg_color = current_theme.line_backgrounds['added']
65
+ style = f"{current_theme.text_color} on {bg_color}"
66
+ min_indent = get_min_indent(lines)
67
+ for line in lines:
68
+ if line.strip():
69
+ line = line[min_indent:]
70
+ text.append(apply_line_style(line, style, width, full_width=True))
71
+ return text
72
+
73
+ # Regular diff formatting
74
+ if search_lines and replace_lines:
75
+ # Find similar lines for better diff visualization
76
+ similar_pairs = find_similar_lines(search_lines, replace_lines)
77
+ similar_added = {j for _, j, _ in similar_pairs}
78
+ similar_deleted = {i for i, _, _ in similar_pairs}
79
+
80
+ # Create sets for comparison
81
+ search_set = set(search_lines)
82
+ replace_set = set(replace_lines)
83
+ common_lines = search_set & replace_set
84
+
85
+ def add_line(line: str, line_type: str = 'unchanged'):
86
+ bg_color = current_theme.line_backgrounds.get(line_type, current_theme.line_backgrounds['unchanged'])
87
+ style = f"{current_theme.text_color} on {bg_color}"
88
+
89
+ if syntax_type == 'python':
90
+ # Just use the background color without syntax highlighting
91
+ text.append(apply_line_style(line, style, width))
92
+ return
93
+
94
+ # Wrap long lines
95
+ if len(line) > width:
96
+ wrapped_lines = wrap(line, width=width, break_long_words=True, break_on_hyphens=False)
97
+ for wrapped in wrapped_lines:
98
+ text.append(apply_line_style(wrapped, style, width))
99
+ else:
100
+ text.append(apply_line_style(line, style, width))
101
+
102
+ for i, line in enumerate(lines):
103
+ if not line.strip(): # Handle empty lines
104
+ add_line("", 'unchanged')
105
+ elif line in common_lines:
106
+ add_line(line, 'unchanged')
107
+ elif not is_search:
108
+ add_line(line, 'added')
109
+ else:
110
+ add_line(line, 'deleted')
111
+
112
+ return text
113
+
114
+ from rich.panel import Panel
115
+ from rich.columns import Columns
116
+
117
+ def create_legend_items(console: Console) -> Panel:
118
+ """Create a compact legend panel with color blocks
119
+
120
+ Args:
121
+ console: Console instance for width calculation
122
+ """
123
+ text = Text()
124
+ term_width = console.width or 120
125
+
126
+ # Add color blocks for each type
127
+ for label, bg_type in [("Unchanged", "unchanged"),
128
+ ("Deleted", "deleted"),
129
+ ("Added", "added")]:
130
+ style = f"{current_theme.text_color} on {current_theme.line_backgrounds[bg_type]}"
131
+ text.append(" ", style=style) # Color block
132
+ text.append(" " + label + " ") # Label with spacing
133
+
134
+ return Panel(
135
+ text,
136
+ padding=(0, 1),
137
+ expand=False,
138
+ title="Legend",
139
+ title_align="center"
140
+ )
@@ -0,0 +1,37 @@
1
+ from rich.table import Table
2
+ from rich.console import Console
3
+ from typing import List, Dict
4
+
5
+ def show_changes_summary(changes_summary: List[Dict], console: Console) -> None:
6
+ """Show summary table of all changes."""
7
+ table = Table(title="Changes Summary")
8
+ table.add_column("File", style="cyan")
9
+ table.add_column("Type", style="magenta")
10
+ table.add_column("Lines Changed", justify="right", style="yellow")
11
+ table.add_column("Delta", justify="right", style="green")
12
+ table.add_column("Block", style="blue", justify="center")
13
+ table.add_column("Reason")
14
+
15
+ for change in changes_summary:
16
+ delta = change['lines_modified'] - change['lines_original']
17
+ delta_str = f"{'+' if delta > 0 else ''}{delta}"
18
+
19
+ if change['type'] == "CREATE":
20
+ lines_info = f"+{change['lines_modified']} lines"
21
+ elif change['type'] in ("DELETE", "CLEAN"):
22
+ lines_info = f"-{change['lines_original']} lines"
23
+ else:
24
+ lines_info = f"{change['lines_original']} → {change['lines_modified']} lines"
25
+
26
+ table.add_row(
27
+ str(change['file']),
28
+ change['type'],
29
+ lines_info,
30
+ delta_str,
31
+ change['block_marker'] or '-',
32
+ change['reason']
33
+ )
34
+
35
+ console.print("\n")
36
+ console.print(table, justify="center")
37
+ console.print("\n")
@@ -1,55 +1,62 @@
1
- from dataclasses import dataclass
2
- from typing import Dict
3
- from enum import Enum
4
-
5
- class ThemeType(Enum):
6
- DARK = "dark"
7
- LIGHT = "light"
8
-
9
- # Dark theme colors
10
- DARK_LINE_BACKGROUNDS = {
11
- 'unchanged': '#1A1A1A',
12
- 'added': '#003300',
13
- 'deleted': '#660000'
14
- }
15
-
16
- # Light theme colors
17
- LIGHT_LINE_BACKGROUNDS = {
18
- 'unchanged': 'grey93', # Light gray for unchanged
19
- 'added': 'light_green', # Semantic light green for additions
20
- 'deleted': 'light_red' # Semantic light red for deletions
21
- }
22
-
23
- @dataclass
24
- class ColorTheme:
25
- text_color: str
26
- line_backgrounds: Dict[str, str]
27
- theme_type: ThemeType
28
-
29
- # Predefined themes
30
- DARK_THEME = ColorTheme(
31
- text_color="white",
32
- line_backgrounds=DARK_LINE_BACKGROUNDS,
33
- theme_type=ThemeType.DARK
34
- )
35
-
36
- LIGHT_THEME = ColorTheme(
37
- text_color="black",
38
- line_backgrounds=LIGHT_LINE_BACKGROUNDS,
39
- theme_type=ThemeType.LIGHT
40
- )
41
-
42
- DEFAULT_THEME = DARK_THEME
43
-
44
- def validate_theme(theme: ColorTheme) -> bool:
45
- """Validate that a theme contains all required colors"""
46
- required_line_backgrounds = {'unchanged', 'added', 'deleted'}
47
-
48
- if not isinstance(theme, ColorTheme):
49
- return False
50
-
51
- return all(bg in theme.line_backgrounds for bg in required_line_backgrounds)
52
-
53
- def get_theme_by_type(theme_type: ThemeType) -> ColorTheme:
54
- """Get a predefined theme by type"""
55
- return LIGHT_THEME if theme_type == ThemeType.LIGHT else DARK_THEME
1
+ from dataclasses import dataclass
2
+ from typing import Dict
3
+ from enum import Enum
4
+
5
+ class ThemeType(Enum):
6
+ DARK = "dark"
7
+ LIGHT = "light"
8
+
9
+ # Dark theme colors
10
+ DARK_LINE_BACKGROUNDS = {
11
+ 'unchanged': '#1A1A1A',
12
+ 'added': '#003300',
13
+ 'deleted': '#330000',
14
+ 'removed': '#330000',
15
+ 'success': '#004400' # Dark green for success messages
16
+ }
17
+
18
+ # Light theme colors
19
+ LIGHT_LINE_BACKGROUNDS = {
20
+ 'unchanged': 'grey93', # Light gray for unchanged
21
+ 'added': 'light_green', # Semantic light green for additions
22
+ 'deleted': '#FFE6E6', # Soft light red for deletions
23
+ 'removed': '#FFE6E6',
24
+ 'success': '#E6FFE6' # Light green for success messages
25
+ }
26
+
27
+ @dataclass
28
+ class ColorTheme:
29
+ text_color: str
30
+ line_backgrounds: Dict[str, str]
31
+ theme_type: ThemeType
32
+ success_color: str = "green"
33
+
34
+ # Predefined themes
35
+ DARK_THEME = ColorTheme(
36
+ text_color="white",
37
+ line_backgrounds=DARK_LINE_BACKGROUNDS,
38
+ theme_type=ThemeType.DARK,
39
+ success_color="green"
40
+ )
41
+
42
+ LIGHT_THEME = ColorTheme(
43
+ text_color="black",
44
+ line_backgrounds=LIGHT_LINE_BACKGROUNDS,
45
+ theme_type=ThemeType.LIGHT,
46
+ success_color="dark_green"
47
+ )
48
+
49
+ DEFAULT_THEME = DARK_THEME
50
+
51
+ def validate_theme(theme: ColorTheme) -> bool:
52
+ """Validate that a theme contains all required colors"""
53
+ required_line_backgrounds = {'unchanged', 'added', 'deleted'}
54
+
55
+ if not isinstance(theme, ColorTheme):
56
+ return False
57
+
58
+ return all(bg in theme.line_backgrounds for bg in required_line_backgrounds)
59
+
60
+ def get_theme_by_type(theme_type: ThemeType) -> ColorTheme:
61
+ """Get a predefined theme by type"""
62
+ return LIGHT_THEME if theme_type == ThemeType.LIGHT else DARK_THEME
@@ -0,0 +1,59 @@
1
+ from rich.console import Console
2
+ from ..applied_blocks import AppliedBlocks
3
+ from .panels import create_diff_columns
4
+ from .summary import show_changes_summary
5
+
6
+ class ChangeViewer:
7
+ def __init__(self):
8
+ self.console = Console()
9
+
10
+ def show_changes(self, applied_blocks: AppliedBlocks):
11
+ """Show all changes with diffs and summary"""
12
+ for block in applied_blocks.blocks:
13
+ self._show_block(block)
14
+
15
+ # Show summary table
16
+ show_changes_summary(applied_blocks.get_changes_summary(), self.console)
17
+
18
+ def _show_block(self, block):
19
+ """Show a single change block with appropriate visualization"""
20
+ if block.edit_type.name == "CREATE":
21
+ header, columns = create_diff_columns(
22
+ [], # No original content
23
+ block.modified_content,
24
+ str(block.filename),
25
+ 0,
26
+ self.console.width
27
+ )
28
+ elif block.edit_type.name == "DELETE":
29
+ header, columns = create_diff_columns(
30
+ block.original_content,
31
+ [], # No modified content
32
+ str(block.filename),
33
+ 0,
34
+ self.console.width,
35
+ is_removal=True
36
+ )
37
+ elif block.edit_type.name == "CLEAN":
38
+ header, columns = create_diff_columns(
39
+ block.original_content,
40
+ [], # No modified content for clean
41
+ str(block.filename),
42
+ block.range_start - 1,
43
+ self.console.width,
44
+ operation="Clean",
45
+ reason=block.reason,
46
+ is_removal=True
47
+ )
48
+ else: # EDIT operation
49
+ header, columns = create_diff_columns(
50
+ block.original_content,
51
+ block.modified_content,
52
+ str(block.filename),
53
+ block.range_start - 1,
54
+ self.console.width,
55
+ reason=block.reason
56
+ )
57
+
58
+ self.console.print(header)
59
+ self.console.print(columns)
janito/cli/__init__.py CHANGED
@@ -1,2 +1,2 @@
1
-
1
+
2
2
  # This file is intentionally left empty to make the cli directory a package.