reveal-cli 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.
- plugins/c-header.yaml +89 -0
- plugins/gdscript.yaml +96 -0
- plugins/python.yaml +94 -0
- plugins/yaml.yaml +87 -0
- reveal/__init__.py +26 -0
- reveal/analyzers/__init__.py +39 -0
- reveal/analyzers/bash.py +44 -0
- reveal/analyzers/dockerfile.py +179 -0
- reveal/analyzers/gdscript.py +176 -0
- reveal/analyzers/go.py +13 -0
- reveal/analyzers/javascript.py +21 -0
- reveal/analyzers/jupyter_analyzer.py +230 -0
- reveal/analyzers/markdown.py +79 -0
- reveal/analyzers/nginx.py +185 -0
- reveal/analyzers/python.py +15 -0
- reveal/analyzers/rust.py +13 -0
- reveal/analyzers/toml.py +96 -0
- reveal/analyzers/typescript.py +24 -0
- reveal/analyzers/yaml_json.py +110 -0
- reveal/base.py +267 -0
- reveal/main.py +355 -0
- reveal/tests/__init__.py +1 -0
- reveal/tests/test_json_yaml_line_numbers.py +238 -0
- reveal/tests/test_line_numbers.py +151 -0
- reveal/tests/test_toml_analyzer.py +220 -0
- reveal/tree_view.py +105 -0
- reveal/treesitter.py +281 -0
- reveal_cli-0.8.0.dist-info/METADATA +352 -0
- reveal_cli-0.8.0.dist-info/RECORD +33 -0
- reveal_cli-0.8.0.dist-info/WHEEL +5 -0
- reveal_cli-0.8.0.dist-info/entry_points.txt +2 -0
- reveal_cli-0.8.0.dist-info/licenses/LICENSE +21 -0
- reveal_cli-0.8.0.dist-info/top_level.txt +2 -0
reveal/base.py
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""Base analyzer class for reveal - clean, simple design."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional, Dict, Any, List
|
|
6
|
+
import hashlib
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FileAnalyzer:
|
|
10
|
+
"""Base class for all file analyzers.
|
|
11
|
+
|
|
12
|
+
Provides automatic functionality:
|
|
13
|
+
- File reading with encoding detection
|
|
14
|
+
- Metadata extraction
|
|
15
|
+
- Line number formatting
|
|
16
|
+
- Source extraction helpers
|
|
17
|
+
|
|
18
|
+
Subclasses only need to implement:
|
|
19
|
+
- get_structure(): Return dict of file elements
|
|
20
|
+
- extract_element(type, name): Extract specific element (optional)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, path: str):
|
|
24
|
+
self.path = Path(path)
|
|
25
|
+
self.lines = self._read_file()
|
|
26
|
+
self.content = '\n'.join(self.lines)
|
|
27
|
+
|
|
28
|
+
def _read_file(self) -> List[str]:
|
|
29
|
+
"""Read file with automatic encoding detection."""
|
|
30
|
+
encodings = ['utf-8', 'utf-8-sig', 'latin-1', 'cp1252']
|
|
31
|
+
|
|
32
|
+
for encoding in encodings:
|
|
33
|
+
try:
|
|
34
|
+
with open(self.path, 'r', encoding=encoding) as f:
|
|
35
|
+
return f.read().splitlines()
|
|
36
|
+
except (UnicodeDecodeError, LookupError):
|
|
37
|
+
continue
|
|
38
|
+
|
|
39
|
+
# Last resort: read as binary and decode with errors='replace'
|
|
40
|
+
with open(self.path, 'rb') as f:
|
|
41
|
+
content = f.read().decode('utf-8', errors='replace')
|
|
42
|
+
return content.splitlines()
|
|
43
|
+
|
|
44
|
+
def get_metadata(self) -> Dict[str, Any]:
|
|
45
|
+
"""Return file metadata.
|
|
46
|
+
|
|
47
|
+
Automatic - works for all file types.
|
|
48
|
+
"""
|
|
49
|
+
stat = os.stat(self.path)
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
'path': str(self.path),
|
|
53
|
+
'name': self.path.name,
|
|
54
|
+
'size': stat.st_size,
|
|
55
|
+
'size_human': self._format_size(stat.st_size),
|
|
56
|
+
'lines': len(self.lines),
|
|
57
|
+
'encoding': self._detect_encoding(),
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
def get_structure(self) -> Dict[str, List[Dict[str, Any]]]:
|
|
61
|
+
"""Return file structure (imports, functions, classes, etc.).
|
|
62
|
+
|
|
63
|
+
Override in subclasses for custom extraction.
|
|
64
|
+
Default: Returns empty structure.
|
|
65
|
+
"""
|
|
66
|
+
return {}
|
|
67
|
+
|
|
68
|
+
def extract_element(self, element_type: str, name: str) -> Optional[Dict[str, Any]]:
|
|
69
|
+
"""Extract a specific element from the file.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
element_type: Type of element ('function', 'class', 'section', etc.)
|
|
73
|
+
name: Name of the element
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Dict with 'line_start', 'line_end', 'source', etc. or None
|
|
77
|
+
|
|
78
|
+
Override in subclasses for semantic extraction.
|
|
79
|
+
Default: Falls back to grep-based search.
|
|
80
|
+
"""
|
|
81
|
+
# Default: simple grep-based extraction
|
|
82
|
+
return self._grep_extract(name)
|
|
83
|
+
|
|
84
|
+
def _grep_extract(self, name: str) -> Optional[Dict[str, Any]]:
|
|
85
|
+
"""Fallback: Extract by grepping for name."""
|
|
86
|
+
for i, line in enumerate(self.lines, 1):
|
|
87
|
+
if name in line:
|
|
88
|
+
# Found it - extract this line and a few after
|
|
89
|
+
line_start = i
|
|
90
|
+
line_end = min(i + 20, len(self.lines)) # Up to 20 lines
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
'name': name,
|
|
94
|
+
'line_start': line_start,
|
|
95
|
+
'line_end': line_end,
|
|
96
|
+
'source': '\n'.join(self.lines[line_start-1:line_end]),
|
|
97
|
+
}
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
def format_with_lines(self, source: str, start_line: int) -> str:
|
|
101
|
+
"""Format source code with line numbers.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
source: Source code to format
|
|
105
|
+
start_line: Starting line number
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Formatted string with line numbers
|
|
109
|
+
"""
|
|
110
|
+
lines = source.split('\n')
|
|
111
|
+
result = []
|
|
112
|
+
|
|
113
|
+
for i, line in enumerate(lines):
|
|
114
|
+
line_num = start_line + i
|
|
115
|
+
result.append(f" {line_num:4d} {line}")
|
|
116
|
+
|
|
117
|
+
return '\n'.join(result)
|
|
118
|
+
|
|
119
|
+
def _format_size(self, size: int) -> str:
|
|
120
|
+
"""Format file size in human-readable form."""
|
|
121
|
+
for unit in ['B', 'KB', 'MB', 'GB']:
|
|
122
|
+
if size < 1024.0:
|
|
123
|
+
return f"{size:.1f} {unit}"
|
|
124
|
+
size /= 1024.0
|
|
125
|
+
return f"{size:.1f} TB"
|
|
126
|
+
|
|
127
|
+
def _detect_encoding(self) -> str:
|
|
128
|
+
"""Detect file encoding."""
|
|
129
|
+
# Simple heuristic for now
|
|
130
|
+
try:
|
|
131
|
+
self.content.encode('ascii')
|
|
132
|
+
return 'ASCII'
|
|
133
|
+
except UnicodeEncodeError:
|
|
134
|
+
return 'UTF-8'
|
|
135
|
+
|
|
136
|
+
def get_directory_entry(self) -> Dict[str, Any]:
|
|
137
|
+
"""Return info for directory listing.
|
|
138
|
+
|
|
139
|
+
Automatic - works for all file types.
|
|
140
|
+
"""
|
|
141
|
+
meta = self.get_metadata()
|
|
142
|
+
file_type = self.__class__.__name__.replace('Analyzer', '')
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
'path': str(self.path),
|
|
146
|
+
'name': self.path.name,
|
|
147
|
+
'size': meta['size_human'],
|
|
148
|
+
'lines': meta['lines'],
|
|
149
|
+
'type': file_type,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# Registry for file type analyzers
|
|
154
|
+
_ANALYZER_REGISTRY: Dict[str, type] = {}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def register(*extensions, name: str = '', icon: str = 'š'):
|
|
158
|
+
"""Decorator to register an analyzer for file extensions.
|
|
159
|
+
|
|
160
|
+
Usage:
|
|
161
|
+
@register('.py', name='Python', icon='š')
|
|
162
|
+
class PythonAnalyzer(FileAnalyzer):
|
|
163
|
+
...
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
extensions: File extensions to register (e.g., '.py', '.rs')
|
|
167
|
+
name: Display name for this file type
|
|
168
|
+
icon: Emoji icon for this file type
|
|
169
|
+
"""
|
|
170
|
+
def decorator(cls):
|
|
171
|
+
for ext in extensions:
|
|
172
|
+
_ANALYZER_REGISTRY[ext.lower()] = cls
|
|
173
|
+
|
|
174
|
+
# Store metadata on class
|
|
175
|
+
cls.type_name = name or cls.__name__.replace('Analyzer', '')
|
|
176
|
+
cls.icon = icon
|
|
177
|
+
|
|
178
|
+
return cls
|
|
179
|
+
|
|
180
|
+
return decorator
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def get_analyzer(path: str) -> Optional[type]:
|
|
184
|
+
"""Get analyzer class for a file path.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
path: File path
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Analyzer class or None if not found
|
|
191
|
+
"""
|
|
192
|
+
file_path = Path(path)
|
|
193
|
+
ext = file_path.suffix.lower()
|
|
194
|
+
|
|
195
|
+
# If we have an extension, use it
|
|
196
|
+
if ext and ext in _ANALYZER_REGISTRY:
|
|
197
|
+
return _ANALYZER_REGISTRY.get(ext)
|
|
198
|
+
|
|
199
|
+
# No extension or not found - check special filenames (Dockerfile, Makefile)
|
|
200
|
+
filename = file_path.name.lower()
|
|
201
|
+
if filename in _ANALYZER_REGISTRY:
|
|
202
|
+
return _ANALYZER_REGISTRY.get(filename)
|
|
203
|
+
|
|
204
|
+
# Still no match - check shebang for extensionless scripts
|
|
205
|
+
if not ext or ext not in _ANALYZER_REGISTRY:
|
|
206
|
+
shebang_ext = _detect_shebang(path)
|
|
207
|
+
if shebang_ext:
|
|
208
|
+
return _ANALYZER_REGISTRY.get(shebang_ext)
|
|
209
|
+
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _detect_shebang(path: str) -> Optional[str]:
|
|
214
|
+
"""Detect file type from shebang line.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
path: File path
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
Extension to use (e.g., '.py', '.sh') or None
|
|
221
|
+
"""
|
|
222
|
+
try:
|
|
223
|
+
with open(path, 'rb') as f:
|
|
224
|
+
first_line = f.readline()
|
|
225
|
+
|
|
226
|
+
# Decode with error handling
|
|
227
|
+
try:
|
|
228
|
+
shebang = first_line.decode('utf-8', errors='ignore').strip()
|
|
229
|
+
except:
|
|
230
|
+
return None
|
|
231
|
+
|
|
232
|
+
if not shebang.startswith('#!'):
|
|
233
|
+
return None
|
|
234
|
+
|
|
235
|
+
# Map shebangs to extensions
|
|
236
|
+
shebang_lower = shebang.lower()
|
|
237
|
+
|
|
238
|
+
# Python
|
|
239
|
+
if 'python' in shebang_lower:
|
|
240
|
+
return '.py'
|
|
241
|
+
|
|
242
|
+
# Shell scripts (bash, sh, zsh)
|
|
243
|
+
if any(shell in shebang_lower for shell in ['bash', '/sh', 'zsh']):
|
|
244
|
+
return '.sh'
|
|
245
|
+
|
|
246
|
+
return None
|
|
247
|
+
|
|
248
|
+
except (IOError, OSError):
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def get_all_analyzers() -> Dict[str, Dict[str, Any]]:
|
|
253
|
+
"""Get all registered analyzers with metadata.
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
Dict mapping extension to analyzer metadata
|
|
257
|
+
e.g., {'.py': {'name': 'Python', 'icon': 'š', 'class': PythonAnalyzer}}
|
|
258
|
+
"""
|
|
259
|
+
result = {}
|
|
260
|
+
for ext, cls in _ANALYZER_REGISTRY.items():
|
|
261
|
+
result[ext] = {
|
|
262
|
+
'extension': ext,
|
|
263
|
+
'name': getattr(cls, 'type_name', cls.__name__.replace('Analyzer', '')),
|
|
264
|
+
'icon': getattr(cls, 'icon', 'š'),
|
|
265
|
+
'class': cls,
|
|
266
|
+
}
|
|
267
|
+
return result
|
reveal/main.py
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
"""Clean, simple CLI for reveal."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import os
|
|
5
|
+
import argparse
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
from datetime import datetime, timedelta
|
|
9
|
+
from .base import get_analyzer, get_all_analyzers, FileAnalyzer
|
|
10
|
+
from .tree_view import show_directory_tree
|
|
11
|
+
from . import __version__
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def check_for_updates():
|
|
15
|
+
"""Check PyPI for newer version (once per day, non-blocking).
|
|
16
|
+
|
|
17
|
+
- Checks at most once per day (cached in ~/.config/reveal/last_update_check)
|
|
18
|
+
- 1-second timeout (doesn't slow down CLI)
|
|
19
|
+
- Fails silently (no errors shown to user)
|
|
20
|
+
- Opt-out: Set REVEAL_NO_UPDATE_CHECK=1 environment variable
|
|
21
|
+
"""
|
|
22
|
+
# Opt-out check
|
|
23
|
+
if os.environ.get('REVEAL_NO_UPDATE_CHECK'):
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
# Setup cache directory
|
|
28
|
+
cache_dir = Path.home() / '.config' / 'reveal'
|
|
29
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
cache_file = cache_dir / 'last_update_check'
|
|
31
|
+
|
|
32
|
+
# Check if we should update (once per day)
|
|
33
|
+
if cache_file.exists():
|
|
34
|
+
last_check_str = cache_file.read_text().strip()
|
|
35
|
+
try:
|
|
36
|
+
last_check = datetime.fromisoformat(last_check_str)
|
|
37
|
+
if datetime.now() - last_check < timedelta(days=1):
|
|
38
|
+
return # Checked recently, skip
|
|
39
|
+
except (ValueError, OSError):
|
|
40
|
+
pass # Invalid cache, continue with check
|
|
41
|
+
|
|
42
|
+
# Check PyPI (using urllib to avoid new dependencies)
|
|
43
|
+
import urllib.request
|
|
44
|
+
import json
|
|
45
|
+
|
|
46
|
+
req = urllib.request.Request(
|
|
47
|
+
'https://pypi.org/pypi/reveal-cli/json',
|
|
48
|
+
headers={'User-Agent': f'reveal-cli/{__version__}'}
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
with urllib.request.urlopen(req, timeout=1) as response:
|
|
52
|
+
data = json.loads(response.read().decode('utf-8'))
|
|
53
|
+
latest_version = data['info']['version']
|
|
54
|
+
|
|
55
|
+
# Update cache file
|
|
56
|
+
cache_file.write_text(datetime.now().isoformat())
|
|
57
|
+
|
|
58
|
+
# Compare versions (simple string comparison works for semver)
|
|
59
|
+
if latest_version != __version__:
|
|
60
|
+
# Parse versions for proper comparison
|
|
61
|
+
def parse_version(v):
|
|
62
|
+
return tuple(map(int, v.split('.')))
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
if parse_version(latest_version) > parse_version(__version__):
|
|
66
|
+
print(f"ā ļø Update available: reveal {latest_version} (you have {__version__})")
|
|
67
|
+
print(f"š” Update: pip install --upgrade reveal-cli\n")
|
|
68
|
+
except (ValueError, AttributeError):
|
|
69
|
+
pass # Version comparison failed, ignore
|
|
70
|
+
|
|
71
|
+
except Exception:
|
|
72
|
+
# Fail silently - don't interrupt user's workflow
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def main():
|
|
77
|
+
"""Main CLI entry point."""
|
|
78
|
+
# Fix Windows console encoding for emoji/unicode support
|
|
79
|
+
if sys.platform == 'win32':
|
|
80
|
+
# Set environment variable for subprocess compatibility
|
|
81
|
+
os.environ.setdefault('PYTHONIOENCODING', 'utf-8')
|
|
82
|
+
# Reconfigure stdout/stderr to use UTF-8 with error handling
|
|
83
|
+
if hasattr(sys.stdout, 'reconfigure'):
|
|
84
|
+
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
85
|
+
if hasattr(sys.stderr, 'reconfigure'):
|
|
86
|
+
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
|
|
87
|
+
|
|
88
|
+
_main_impl()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _main_impl():
|
|
92
|
+
"""Main CLI entry point."""
|
|
93
|
+
parser = argparse.ArgumentParser(
|
|
94
|
+
description='Reveal: Explore code semantically - The simplest way to understand code',
|
|
95
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
96
|
+
epilog='''
|
|
97
|
+
Examples:
|
|
98
|
+
# Directory exploration
|
|
99
|
+
reveal src/ # Show directory tree
|
|
100
|
+
reveal src/ --depth=5 # Deeper tree view
|
|
101
|
+
|
|
102
|
+
# File structure
|
|
103
|
+
reveal app.py # Show structure (imports, functions, classes)
|
|
104
|
+
reveal app.py --meta # Show metadata (size, lines, encoding)
|
|
105
|
+
reveal player.gd # GDScript support (Godot engine)
|
|
106
|
+
|
|
107
|
+
# Element extraction
|
|
108
|
+
reveal app.py load_config # Extract specific function
|
|
109
|
+
reveal app.py Database # Extract class definition
|
|
110
|
+
|
|
111
|
+
# Output formats
|
|
112
|
+
reveal app.py --format=json # JSON output for scripting
|
|
113
|
+
reveal app.py --format=grep # Grep-compatible format
|
|
114
|
+
|
|
115
|
+
# Discovery
|
|
116
|
+
reveal --list-supported # Show all supported file types
|
|
117
|
+
reveal --version # Show version
|
|
118
|
+
|
|
119
|
+
Perfect filename:line integration - works with vim, git, grep, sed, awk!
|
|
120
|
+
'''
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
parser.add_argument('path', nargs='?', help='File or directory to reveal')
|
|
124
|
+
parser.add_argument('element', nargs='?', help='Element to extract (function, class, etc.)')
|
|
125
|
+
|
|
126
|
+
# Optional flags
|
|
127
|
+
parser.add_argument('--version', action='version', version=f'reveal {__version__}')
|
|
128
|
+
parser.add_argument('--list-supported', '-l', action='store_true',
|
|
129
|
+
help='List all supported file types')
|
|
130
|
+
parser.add_argument('--meta', action='store_true', help='Show metadata only')
|
|
131
|
+
parser.add_argument('--format', choices=['text', 'json', 'grep'], default='text',
|
|
132
|
+
help='Output format (text, json, grep)')
|
|
133
|
+
parser.add_argument('--depth', type=int, default=3, help='Directory tree depth (default: 3)')
|
|
134
|
+
|
|
135
|
+
args = parser.parse_args()
|
|
136
|
+
|
|
137
|
+
# Check for updates (once per day, non-blocking, opt-out available)
|
|
138
|
+
check_for_updates()
|
|
139
|
+
|
|
140
|
+
# Handle --list-supported
|
|
141
|
+
if args.list_supported:
|
|
142
|
+
list_supported_types()
|
|
143
|
+
sys.exit(0)
|
|
144
|
+
|
|
145
|
+
# Path is required if not using --list-supported
|
|
146
|
+
if not args.path:
|
|
147
|
+
parser.print_help()
|
|
148
|
+
sys.exit(1)
|
|
149
|
+
|
|
150
|
+
# Check if path exists
|
|
151
|
+
path = Path(args.path)
|
|
152
|
+
if not path.exists():
|
|
153
|
+
print(f"Error: {args.path} not found", file=sys.stderr)
|
|
154
|
+
sys.exit(1)
|
|
155
|
+
|
|
156
|
+
# Route based on path type
|
|
157
|
+
if path.is_dir():
|
|
158
|
+
# Directory ā show tree
|
|
159
|
+
output = show_directory_tree(str(path), depth=args.depth)
|
|
160
|
+
print(output)
|
|
161
|
+
|
|
162
|
+
elif path.is_file():
|
|
163
|
+
# File ā show structure or extract element
|
|
164
|
+
handle_file(str(path), args.element, args.meta, args.format)
|
|
165
|
+
|
|
166
|
+
else:
|
|
167
|
+
print(f"Error: {args.path} is neither file nor directory", file=sys.stderr)
|
|
168
|
+
sys.exit(1)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def list_supported_types():
|
|
172
|
+
"""List all supported file types."""
|
|
173
|
+
analyzers = get_all_analyzers()
|
|
174
|
+
|
|
175
|
+
if not analyzers:
|
|
176
|
+
print("No file types registered")
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
print(f"š Reveal v{__version__} - Supported File Types\n")
|
|
180
|
+
|
|
181
|
+
# Sort by name for nice display
|
|
182
|
+
sorted_analyzers = sorted(analyzers.items(), key=lambda x: x[1]['name'])
|
|
183
|
+
|
|
184
|
+
for ext, info in sorted_analyzers:
|
|
185
|
+
icon = info['icon']
|
|
186
|
+
name = info['name']
|
|
187
|
+
print(f" {icon} {name:15s} ({ext})")
|
|
188
|
+
|
|
189
|
+
print(f"\n⨠Total: {len(analyzers)} file types supported")
|
|
190
|
+
print(f"\nš” Use 'reveal <file>' to explore any supported file")
|
|
191
|
+
print(f"š” Use 'reveal --help' for usage examples")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def handle_file(path: str, element: Optional[str], show_meta: bool, output_format: str):
|
|
195
|
+
"""Handle file analysis.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
path: File path
|
|
199
|
+
element: Optional element to extract
|
|
200
|
+
show_meta: Whether to show metadata only
|
|
201
|
+
output_format: Output format ('text', 'json', 'grep')
|
|
202
|
+
"""
|
|
203
|
+
# Get analyzer
|
|
204
|
+
analyzer_class = get_analyzer(path)
|
|
205
|
+
if not analyzer_class:
|
|
206
|
+
ext = Path(path).suffix or '(no extension)'
|
|
207
|
+
print(f"Error: No analyzer found for {path} ({ext})", file=sys.stderr)
|
|
208
|
+
print(f"\nš” Hint: File type '{ext}' is not supported yet", file=sys.stderr)
|
|
209
|
+
print(f"š” Run 'reveal --list-supported' to see all supported file types", file=sys.stderr)
|
|
210
|
+
print(f"š” Visit https://github.com/scottsen/reveal to request new file types", file=sys.stderr)
|
|
211
|
+
sys.exit(1)
|
|
212
|
+
|
|
213
|
+
analyzer = analyzer_class(path)
|
|
214
|
+
|
|
215
|
+
# Show metadata only?
|
|
216
|
+
if show_meta:
|
|
217
|
+
show_metadata(analyzer, output_format)
|
|
218
|
+
return
|
|
219
|
+
|
|
220
|
+
# Extract specific element?
|
|
221
|
+
if element:
|
|
222
|
+
extract_element(analyzer, element, output_format)
|
|
223
|
+
return
|
|
224
|
+
|
|
225
|
+
# Default: show structure
|
|
226
|
+
show_structure(analyzer, output_format)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def show_metadata(analyzer: FileAnalyzer, output_format: str):
|
|
230
|
+
"""Show file metadata."""
|
|
231
|
+
meta = analyzer.get_metadata()
|
|
232
|
+
|
|
233
|
+
if output_format == 'json':
|
|
234
|
+
import json
|
|
235
|
+
print(json.dumps(meta, indent=2))
|
|
236
|
+
else:
|
|
237
|
+
print(f"š {meta['name']}\n")
|
|
238
|
+
print(f"Path: {meta['path']}")
|
|
239
|
+
print(f"Size: {meta['size_human']}")
|
|
240
|
+
print(f"Lines: {meta['lines']}")
|
|
241
|
+
print(f"Encoding: {meta['encoding']}")
|
|
242
|
+
print(f"\nā reveal {meta['path']}")
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def show_structure(analyzer: FileAnalyzer, output_format: str):
|
|
246
|
+
"""Show file structure."""
|
|
247
|
+
structure = analyzer.get_structure()
|
|
248
|
+
path = analyzer.path
|
|
249
|
+
|
|
250
|
+
if output_format == 'json':
|
|
251
|
+
import json
|
|
252
|
+
print(json.dumps(structure, indent=2))
|
|
253
|
+
return
|
|
254
|
+
|
|
255
|
+
if not structure:
|
|
256
|
+
print(f"š {path.name}\n")
|
|
257
|
+
print("No structure available for this file type")
|
|
258
|
+
return
|
|
259
|
+
|
|
260
|
+
print(f"š {path.name}\n")
|
|
261
|
+
|
|
262
|
+
# Show each category
|
|
263
|
+
for category, items in structure.items():
|
|
264
|
+
if not items:
|
|
265
|
+
continue
|
|
266
|
+
|
|
267
|
+
# Format category name (e.g., 'functions' ā 'Functions')
|
|
268
|
+
category_name = category.capitalize()
|
|
269
|
+
print(f"{category_name} ({len(items)}):")
|
|
270
|
+
|
|
271
|
+
for item in items:
|
|
272
|
+
line = item.get('line', '?')
|
|
273
|
+
name = item.get('name', '')
|
|
274
|
+
signature = item.get('signature', '')
|
|
275
|
+
content = item.get('content', '')
|
|
276
|
+
|
|
277
|
+
# Format based on what's available
|
|
278
|
+
if signature and name:
|
|
279
|
+
# Function with signature
|
|
280
|
+
if output_format == 'grep':
|
|
281
|
+
print(f"{path}:{line}:{name}{signature}")
|
|
282
|
+
else:
|
|
283
|
+
print(f" {path}:{line:<6} {name}{signature}")
|
|
284
|
+
elif name:
|
|
285
|
+
# Just name (class, struct, etc.)
|
|
286
|
+
if output_format == 'grep':
|
|
287
|
+
print(f"{path}:{line}:{name}")
|
|
288
|
+
else:
|
|
289
|
+
print(f" {path}:{line:<6} {name}")
|
|
290
|
+
elif content:
|
|
291
|
+
# Just content (import, etc.)
|
|
292
|
+
if output_format == 'grep':
|
|
293
|
+
print(f"{path}:{line}:{content}")
|
|
294
|
+
else:
|
|
295
|
+
print(f" {path}:{line:<6} {content}")
|
|
296
|
+
|
|
297
|
+
print() # Blank line between categories
|
|
298
|
+
|
|
299
|
+
# Navigation hints
|
|
300
|
+
if output_format == 'text':
|
|
301
|
+
print(f"ā reveal {path} <element>")
|
|
302
|
+
print(f"ā vim {path}:<line>")
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def extract_element(analyzer: FileAnalyzer, element: str, output_format: str):
|
|
306
|
+
"""Extract a specific element.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
analyzer: File analyzer
|
|
310
|
+
element: Element name to extract
|
|
311
|
+
output_format: Output format
|
|
312
|
+
"""
|
|
313
|
+
# Try common element types
|
|
314
|
+
for element_type in ['function', 'class', 'struct', 'section']:
|
|
315
|
+
result = analyzer.extract_element(element_type, element)
|
|
316
|
+
if result:
|
|
317
|
+
break
|
|
318
|
+
else:
|
|
319
|
+
# Not found
|
|
320
|
+
print(f"Error: Element '{element}' not found in {analyzer.path}", file=sys.stderr)
|
|
321
|
+
sys.exit(1)
|
|
322
|
+
|
|
323
|
+
# Format output
|
|
324
|
+
if output_format == 'json':
|
|
325
|
+
import json
|
|
326
|
+
print(json.dumps(result, indent=2))
|
|
327
|
+
return
|
|
328
|
+
|
|
329
|
+
path = analyzer.path
|
|
330
|
+
line_start = result.get('line_start', 1)
|
|
331
|
+
line_end = result.get('line_end', line_start)
|
|
332
|
+
source = result.get('source', '')
|
|
333
|
+
name = result.get('name', element)
|
|
334
|
+
|
|
335
|
+
# Header
|
|
336
|
+
print(f"{path}:{line_start}-{line_end} | {name}\n")
|
|
337
|
+
|
|
338
|
+
# Source with line numbers
|
|
339
|
+
if output_format == 'grep':
|
|
340
|
+
# Grep format: filename:linenum:content
|
|
341
|
+
for i, line in enumerate(source.split('\n')):
|
|
342
|
+
line_num = line_start + i
|
|
343
|
+
print(f"{path}:{line_num}:{line}")
|
|
344
|
+
else:
|
|
345
|
+
# Human-readable format
|
|
346
|
+
formatted = analyzer.format_with_lines(source, line_start)
|
|
347
|
+
print(formatted)
|
|
348
|
+
|
|
349
|
+
# Navigation hints
|
|
350
|
+
print(f"\nā vim {path}:{line_start}")
|
|
351
|
+
print(f"ā reveal {path}")
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
if __name__ == '__main__':
|
|
355
|
+
main()
|
reveal/tests/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Test suite for reveal."""
|