Skip to content

Commit 77aece4

Browse files
committed
Bump version 0.6: Show results in panels... like on searchcode.com :)
1 parent ef0e109 commit 77aece4

8 files changed

Lines changed: 177 additions & 142 deletions

File tree

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,5 @@ requires = ["poetry-core"]
3434
build-backend = "poetry.core.masonry.api"
3535

3636
[tool.poetry.scripts]
37-
sc = "searchcode.__cli.app:cli"
38-
searchcode = "searchcode.__cli.app:cli"
37+
sc = "searchcode._cli.app:cli"
38+
searchcode = "searchcode._cli.app:cli"

src/searchcode/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
__version__ = "0.6.0"
2424
__author__ = "Ritchie Mwewa"
2525

26+
__all__ = ["Searchcode"]
27+
2628

2729
class License:
2830
terms_and_conditions: str = """
Lines changed: 25 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,14 @@
1919
from types import SimpleNamespace
2020

2121
import rich_click as click
22-
from rich.syntax import Syntax
2322

24-
from .lib import (
25-
console,
26-
update_window_title,
23+
from .panels import console, print_panels
24+
from .. import __pkg__, __version__, License
25+
from .._lib import (
2726
clear_screen,
28-
print_jsonp,
29-
print_panels,
3027
namespace_to_dict,
28+
update_window_title,
3129
)
32-
from .. import __pkg__, __version__, License
3330
from ..api import Searchcode
3431

3532
__all__ = ["cli"]
@@ -149,12 +146,12 @@ def search(
149146
lines_of_code_gt=lines_of_code_gt,
150147
callback=callback,
151148
)
152-
print_jsonp(jsonp=response)
149+
print_panels(data=response)
153150
return
154151

155152
# normal paginated search
156-
with console.status(f"Querying: [green]{query}[/]") as status:
157-
results, total = fetch_paginated_results(
153+
with console.status(f"Querying code index with [green]{query}[/]...") as status:
154+
results, total = _fetch_paginated_results(
158155
query=query,
159156
start_page=page,
160157
per_page=per_page,
@@ -166,12 +163,20 @@ def search(
166163
status=status,
167164
)
168165

169-
print_results(
170-
results=results, total=total, query=query, callback=callback, pretty=pretty
171-
)
166+
if results:
167+
if not callback and not pretty:
168+
console.log(f"Showing {len(results)} of {total} results for '{query}'")
169+
if pretty:
170+
console.print(namespace_to_dict(obj=results))
171+
else:
172+
print_panels(data=results)
173+
else:
174+
console.log(
175+
f"[bold yellow]✘[/bold yellow] No results found for [bold yellow]{query}[/bold yellow]."
176+
)
172177

173178

174-
def fetch_paginated_results(
179+
def _fetch_paginated_results(
175180
query: str,
176181
start_page: int,
177182
per_page: int,
@@ -192,11 +197,6 @@ def fetch_paginated_results(
192197
total_results = 0
193198

194199
for current_iteration in range(1, pages + 1):
195-
status.update(
196-
f"Fetching page [cyan]{current_iteration}[/]/[cyan]{pages}[/] "
197-
f"([cyan]{len(all_results)}[/] results collected)"
198-
)
199-
200200
response = sc.search(
201201
query=query,
202202
page=current_page,
@@ -207,6 +207,10 @@ def fetch_paginated_results(
207207
lines_of_code_gt=lines_of_code_gt,
208208
callback=None,
209209
)
210+
status.update(
211+
f"Getting page results on page [cyan]{current_iteration}[/] of [cyan]{pages}[/] "
212+
f"([cyan]{len(all_results)}[/] results collected)..."
213+
)
210214

211215
if isinstance(response, str):
212216
break
@@ -224,35 +228,6 @@ def fetch_paginated_results(
224228
return all_results, total_results
225229

226230

227-
def print_results(
228-
results: t.List, total: int, query: str, callback: t.Optional[str], pretty: bool
229-
):
230-
"""
231-
Print search results.
232-
233-
:param results: List of code records.
234-
:param total: Total result count.
235-
:param query: Search query.
236-
:param callback: JSONP callback (should be None).
237-
:param pretty: Whether to print raw JSON.
238-
"""
239-
if results:
240-
if pretty:
241-
console.print(namespace_to_dict(obj=results))
242-
else:
243-
print_panels(data=results)
244-
245-
if not callback and not pretty:
246-
console.log(
247-
f"[bold green]✔[/bold green] Returned {len(results)} of {total} "
248-
f"results for [bold green]{query}[/bold green]."
249-
)
250-
else:
251-
console.log(
252-
f"[bold yellow]✘[/bold yellow] No results for [bold yellow]{query}[/bold yellow]."
253-
)
254-
255-
256231
@cli.command()
257232
@click.argument("id", type=int)
258233
def code(id: int):
@@ -263,16 +238,6 @@ def code(id: int):
263238
"""
264239
clear_screen()
265240
update_window_title(text=str(id))
266-
with console.status(f"Fetching code file: [cyan]{id}[/]"):
241+
with console.status(f"Getting code file [cyan]{id}[/]..."):
267242
data = sc.code(id)
268-
lines = data.code
269-
language = data.language
270-
if lines:
271-
syntax = Syntax(
272-
code=lines, lexer=language, line_numbers=True, theme="dracula"
273-
)
274-
console.print(syntax)
275-
else:
276-
console.log(
277-
f"[bold yellow]✘[/bold yellow] No matching file found: [bold yellow]{id}[/bold yellow]."
278-
)
243+
print_panels(data=data, id=id)

src/searchcode/_cli/panels.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import typing as t
2+
from types import SimpleNamespace
3+
4+
from rich.console import Group, Console
5+
from rich.panel import Panel
6+
from rich.rule import Rule
7+
from rich.syntax import Syntax
8+
from rich.text import Text
9+
10+
console = Console(highlight=True, log_time=False)
11+
12+
13+
def _extract_code_string_with_linenumbers(lines_dict: t.Dict[str, str]) -> str:
14+
"""
15+
Convert a dictionary of line_number: code_line into a single
16+
multiline string sorted by line number.
17+
18+
Each line is right-aligned to maintain visual alignment in output.
19+
20+
:param lines_dict: Dictionary where keys are line numbers (as strings) and values are lines of code.
21+
:return: Multiline string with original line numbers included.
22+
"""
23+
sorted_lines = sorted(lines_dict.items(), key=lambda x: int(x[0]))
24+
numbered_lines = [
25+
f"{line_no.rjust(4)} {line.rstrip()}" for line_no, line in sorted_lines
26+
]
27+
return "\n".join(numbered_lines)
28+
29+
30+
def _make_syntax(code: str, language: str, **syntax_kwargs) -> Syntax:
31+
"""
32+
Create a Syntax object with consistent settings.
33+
34+
:param code: The source code to render.
35+
:type code: str
36+
:param language: The programming language lexer to use.
37+
:type language: str
38+
:param syntax_kwargs: Additional keyword arguments for Syntax.
39+
:type syntax_kwargs: Any
40+
:return: A rich Syntax object for displaying code.
41+
:rtype: Syntax
42+
"""
43+
return Syntax(code=code, lexer=language, theme="dracula", **syntax_kwargs)
44+
45+
46+
def _make_syntax_panel(
47+
syntax: Syntax, header_text: t.Optional[str] = None, add_divider: bool = False
48+
) -> Panel:
49+
"""
50+
Wrap a Syntax (or any renderable) in a styled Panel. Optionally include a header and divider.
51+
52+
:param syntax: The Syntax object to display inside the Panel.
53+
:type syntax: Syntax
54+
:param header_text: Optional markup string for the header above the syntax.
55+
:type header_text: Optional[str]
56+
:param add_divider: Whether to include a horizontal rule between header and syntax.
57+
:type add_divider: bool
58+
:return: A rich Panel containing the syntax (and optional header/divider).
59+
:rtype: Panel
60+
"""
61+
if header_text:
62+
header = Text.from_markup(header_text, justify="left", overflow="ellipsis")
63+
divider = Rule(style="#444444") if add_divider else None
64+
content_items = [header, divider, syntax] if divider else [header, syntax]
65+
content = Group(*content_items)
66+
else:
67+
content = syntax
68+
69+
return Panel(renderable=content, border_style="#444444", title_align="left")
70+
71+
72+
def print_panels(
73+
data: t.Union[t.List[SimpleNamespace], SimpleNamespace, str], **kwargs
74+
) -> None:
75+
"""
76+
Print panels for displaying code or structured file information.
77+
78+
Accepts either:
79+
- a single SimpleNamespace with fields `code`, `language`
80+
- a string of raw code
81+
- a list of SimpleNamespace objects with fields `filename`, `repo`, `language`, `linescount`, `lines`
82+
83+
:param data: The input data to display as panels.
84+
:type data: Union[List[SimpleNamespace], SimpleNamespace, str]
85+
:param kwargs: Additional optional keyword arguments (e.g., id for logging).
86+
:type kwargs: Any
87+
:return: None
88+
:rtype: None
89+
"""
90+
panels: t.List[Panel] = []
91+
92+
if isinstance(data, SimpleNamespace):
93+
code = data.code
94+
language = data.language
95+
if code:
96+
syntax = _make_syntax(code, language, line_numbers=True)
97+
panel = _make_syntax_panel(syntax)
98+
panels.append(panel)
99+
else:
100+
console.log(
101+
f"[bold yellow]✘[/bold yellow] No matching file found: [bold yellow]{kwargs.get('id')}[/bold yellow]."
102+
)
103+
return
104+
elif isinstance(data, str):
105+
syntax = _make_syntax(data, "text", line_numbers=True)
106+
panel = _make_syntax_panel(syntax)
107+
panels.append(panel)
108+
else:
109+
for item in data:
110+
filename = item.filename
111+
repo = item.repo
112+
language = item.language
113+
lines_count = item.linescount
114+
lines = item.lines
115+
116+
code_string = _extract_code_string_with_linenumbers(
117+
lines_dict=lines.__dict__
118+
)
119+
120+
syntax = _make_syntax(
121+
code=code_string, language=language, word_wrap=False, indent_guides=True
122+
)
123+
124+
header_text = (
125+
f"[bold]{filename}[/] ([blue]{repo}[/]) "
126+
f"{language} · [cyan]{lines_count}[/] lines"
127+
)
128+
129+
panel = _make_syntax_panel(
130+
syntax=syntax, header_text=header_text, add_divider=True
131+
)
132+
133+
panels.append(panel)
134+
135+
console.print(*panels)
Lines changed: 2 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,6 @@
33
import typing as t
44
from types import SimpleNamespace
55

6-
from rich import box
7-
from rich.console import Console
8-
from rich.panel import Panel
9-
from rich.syntax import Syntax
10-
11-
console = Console(highlight=True, log_time=False)
12-
136

147
def namespace_to_dict(
158
obj: t.Union[SimpleNamespace, t.List[SimpleNamespace]],
@@ -54,74 +47,14 @@ def dict_to_namespace(
5447
return obj
5548

5649

57-
def print_jsonp(jsonp: str) -> None:
58-
"""
59-
Pretty-prints a raw JSONP string.
60-
61-
:param jsonp: A complete JSONP string.
62-
"""
63-
syntax = Syntax(jsonp, "text", line_numbers=True)
64-
console.print(syntax)
65-
66-
67-
def print_panels(data: t.List[SimpleNamespace]):
68-
"""
69-
Render a list of code records as rich panels with syntax highlighting.
70-
Line numbers are preserved and displayed alongside code content.
71-
72-
:param data: A list of dictionaries, where each dictionary represents a code record
73-
"""
74-
75-
def extract_code_string_with_linenumbers(lines_dict: t.Dict[str, str]) -> str:
76-
"""
77-
Convert a dictionary of line_number: code_line into a single
78-
multiline string sorted by line number.
79-
80-
Each line is right-aligned to maintain visual alignment in output.
81-
82-
:param lines_dict: Dictionary where keys are line numbers (as strings) and values are lines of code.
83-
:return: Multiline string with original line numbers included.
84-
"""
85-
sorted_lines = sorted(lines_dict.items(), key=lambda x: int(x[0]))
86-
numbered_lines = [
87-
f"{line_no.rjust(4)} {line.rstrip()}" for line_no, line in sorted_lines
88-
]
89-
return "\n".join(numbered_lines)
90-
91-
for item in data:
92-
filename = item.filename
93-
repo = item.repo
94-
language = item.language
95-
lines_count = item.linescount
96-
lines = item.lines
97-
98-
code_string = extract_code_string_with_linenumbers(lines_dict=lines.__dict__)
99-
100-
syntax = Syntax(
101-
code=code_string,
102-
lexer=language,
103-
word_wrap=False,
104-
indent_guides=True,
105-
theme="dracula",
106-
)
107-
108-
panel = Panel(
109-
renderable=syntax,
110-
box=box.ROUNDED,
111-
title=f"[bold]{filename}[/] ([blue]{repo}[/]) {language} ⸱ [cyan]{lines_count}[/] lines",
112-
highlight=True,
113-
)
114-
115-
console.print(panel)
116-
117-
11850
def update_window_title(text: str):
11951
"""
12052
Update the current window title with the specified text.
12153
12254
:param text: Text to update the window with.
12355
"""
124-
from .. import __pkg__, __version__
56+
from . import __pkg__, __version__
57+
from ._cli.panels import console
12558

12659
console.set_window_title(f"{__pkg__.capitalize()} v{__version__} - {text}")
12760

0 commit comments

Comments
 (0)