1+ """
2+ Copyright (C) 2024 Ritchie Mwewa
3+
4+ This program is free software: you can redistribute it and/or modify
5+ it under the terms of the GNU General Public License as published by
6+ the Free Software Foundation, either version 3 of the License, or
7+ (at your option) any later version.
8+
9+ This program is distributed in the hope that it will be useful,
10+ but WITHOUT ANY WARRANTY; without even the implied warranty of
11+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+ GNU General Public License for more details.
13+
14+ You should have received a copy of the GNU General Public License
15+ along with this program. If not, see <https://www.gnu.org/licenses/>.
16+ """
17+
18+ import os
19+ import subprocess
120import typing as t
221
322import rich_click as click
4- from rich import print as rprint , box
23+ from rich import box
24+ from rich .console import Console
25+ from rich .panel import Panel
526from rich .pretty import pprint
627from rich .syntax import Syntax
7- from rich .table import Table
828from whats_that_code .election import guess_language_all_methods
929
30+ from . import License , __pkg__ , __version__
1031from .api import Searchcode
1132
12- sc = Searchcode (user_agent = "searchCode -sdk/cli" )
33+ sc = Searchcode (user_agent = f" { __pkg__ } -sdk/cli" )
1334
1435__all__ = ["cli" ]
1536
37+ console = Console (highlight = True )
38+
1639
1740@click .group ()
41+ @click .version_option (version = __version__ , package_name = __pkg__ )
1842def cli ():
1943 """
2044 Searchcode
2145
2246 Simple, comprehensive code search.
2347 """
24- ...
48+
49+ __update_window_title ("Source code search engine." )
50+
51+
52+ @cli .command ("license" )
53+ @click .option ("--conditions" , help = "License terms and conditions." , is_flag = True )
54+ @click .option ("--warranty" , help = "License warranty." , is_flag = True )
55+ def licence (conditions : t .Optional [bool ], warranty : t .Optional [bool ]):
56+ """
57+ Show license
58+ """
59+ __clear_screen ()
60+ __update_window_title (
61+ text = "Terms and Conditions" if conditions else "Warranty" if warranty else None
62+ )
63+ if conditions :
64+ console .print (
65+ License .terms_and_conditions ,
66+ justify = "center" ,
67+ style = "on #272822" , # monokai themed background :)
68+ )
69+ if warranty :
70+ console .print (
71+ License .warranty ,
72+ justify = "center" ,
73+ style = "on #272822" ,
74+ )
2575
2676
2777@cli .command ()
@@ -31,7 +81,7 @@ def cli():
3181 "--page" ,
3282 type = int ,
3383 default = 0 ,
34- help = "Start page number (defaults to 0)." ,
84+ help = "Start page (defaults to 0)." ,
3585)
3686@click .option (
3787 "--per-page" ,
@@ -76,33 +126,37 @@ def search(
76126 callback : t .Optional [str ] = None ,
77127):
78128 """
79- Query the code index and (returns 100 results by default).
129+ Query the code index (returns 100 results by default).
80130
81- e.g., searchcode search "import module"
131+ e.g., sc search "import module"
82132 """
83- languages = languages . split ( "," ) if languages else None
84- sources = sources . split ( "," ) if sources else None
133+ __clear_screen ()
134+ __update_window_title ( text = query )
85135
86- results = sc .search (
87- query = query ,
88- page = page ,
89- per_page = per_page ,
90- languages = languages ,
91- sources = sources ,
92- lines_of_code_lt = lines_of_code_lt ,
93- lines_of_code_gt = lines_of_code_gt ,
94- callback = callback ,
95- )
136+ with console .status (f"Querying code index with [green]{ query } [/]" ):
137+ languages = languages .split ("," ) if languages else None
138+ sources = sources .split ("," ) if sources else None
96139
97- (
98- __print_jsonp (jsonp = results )
99- if callback
100- else (
101- pprint (results )
102- if pretty
103- else __print_table (records = results ["results" ], ignore_keys = ["lines" ])
140+ results = sc .search (
141+ query = query ,
142+ page = page ,
143+ per_page = per_page ,
144+ languages = languages ,
145+ sources = sources ,
146+ lines_of_code_lt = lines_of_code_lt ,
147+ lines_of_code_gt = lines_of_code_gt ,
148+ callback = callback ,
149+ )
150+
151+ (
152+ __print_jsonp (jsonp = results )
153+ if callback
154+ else (
155+ pprint (results )
156+ if pretty
157+ else __print_panels (data = results .get ("results" ))
158+ )
104159 )
105- )
106160
107161
108162@cli .command ()
@@ -111,55 +165,94 @@ def code(id: int):
111165 """
112166 Get the raw data from a code file.
113167
114- e.g., searchcode code 4061576
168+ e.g., sc code 4061576
169+ """
170+ __clear_screen ()
171+ __update_window_title (text = str (id ))
172+ with console .status (f"Retrieving code data for [cyan]{ id } [/]" ) as status :
173+ code_data = sc .code (id )
174+ if code_data :
175+ status .update ("Determining code language" )
176+ language = guess_language_all_methods (code = code_data )
177+ syntax = Syntax (code = code_data , lexer = language , line_numbers = True )
178+ console .print (syntax )
179+
180+
181+ def __print_jsonp (jsonp : str ) -> None :
182+ """
183+ Pretty-prints a raw JSONP string.
184+
185+ :param jsonp: A complete JSONP string.
115186 """
116- code_data = sc .code (id )
117- if code_data :
118- language = guess_language_all_methods (code = code_data )
119- syntax = Syntax (code = code_data , lexer = language , line_numbers = True )
120- rprint (syntax )
187+ syntax = Syntax (jsonp , "text" , line_numbers = True )
188+ console .print (syntax )
121189
122190
123- def __print_table ( records : t .List [t .Dict ], ignore_keys : t . List [ str ] = None ) -> None :
191+ def __print_panels ( data : t .List [t .Dict ]) :
124192 """
125- Creates a rich table from a list of dict objects,
126- ignoring specified keys .
193+ Render a list of code records as rich panels with syntax highlighting.
194+ Line numbers are preserved and displayed alongside code content .
127195
128- :param records: List of dict objects.
129- :param ignore_keys: List of keys to exclude from the table.
130- :return: None. Prints the table using rich.
196+ :param data: A list of dictionaries, where each dictionary represents a code record
131197 """
132- if not records :
133- raise ValueError ("Data must be a non-empty list of dict objects." )
134198
135- ignore_keys = ignore_keys or []
199+ def extract_code_string_with_linenumbers (lines_dict : t .Dict [str , str ]) -> str :
200+ """
201+ Convert a dictionary of line_number: code_line into a single
202+ multiline string sorted by line number.
136203
137- # Collect all unique keys across all records, excluding ignored ones
138- all_keys = set ()
139- for record in records :
140- all_keys .update (key for key in record .keys () if key not in ignore_keys )
204+ Each line is right-aligned to maintain visual alignment in output.
141205
142- columns = sorted (all_keys )
206+ :param lines_dict: Dictionary where keys are line numbers (as strings) and values are lines of code.
207+ :return: Multiline string with original line numbers included.
208+ """
209+ sorted_lines = sorted (lines_dict .items (), key = lambda x : int (x [0 ]))
210+ numbered_lines = [
211+ f"{ line_no .rjust (4 )} { line .rstrip ()} " for line_no , line in sorted_lines
212+ ]
213+ return "\n " .join (numbered_lines )
143214
144- table = Table (box = box .ROUNDED , highlight = True , header_style = "bold" )
215+ for item in data :
216+ filename = item .get ("filename" , "Unknown" )
217+ repo = item .get ("repo" , "Unknown" )
218+ language = item .get ("language" , "text" )
219+ lines_count = item .get ("linescount" , "??" )
145220
146- for index , column in enumerate ( columns ):
147- style = "dim" if index == 0 else None
148- table . add_column ( column . capitalize (), style = style )
221+ code_string = extract_code_string_with_linenumbers (
222+ lines_dict = item . get ( "lines" , {})
223+ )
149224
150- for record in records :
151- data = record
152- row = [str (data .get (column , "" )) for column in columns ]
153- table .add_row (* row )
225+ syntax = Syntax (
226+ code = code_string , lexer = language , word_wrap = False , indent_guides = True
227+ )
154228
155- rprint (table )
229+ panel = Panel (
230+ renderable = syntax ,
231+ box = box .ROUNDED ,
232+ title = f"[bold]{ filename } [/] ([blue]{ repo } [/]) { language } ⸱ [cyan]{ lines_count } [/] lines" ,
233+ highlight = True ,
234+ )
156235
236+ console .print (panel )
157237
158- def __print_jsonp (jsonp : str ) -> None :
238+
239+ def __update_window_title (text : str ):
159240 """
160- Pretty-prints a raw JSONP string .
241+ Update the current window title with the specified text .
161242
162- :param jsonp: A complete JSONP string .
243+ :param text: Text to update the window with .
163244 """
164- syntax = Syntax (jsonp , "text" , line_numbers = True )
165- rprint (syntax )
245+ console .set_window_title (f"{ __pkg__ .capitalize ()} - { text } " )
246+
247+
248+ def __clear_screen ():
249+ """
250+ Clear the screen.
251+
252+ Not using console.clear() because it doesn't really clear the screen.
253+ It instead creates a space between the items on top and below,
254+ then moves the cursor to the items on the bottom, thus creating the illusion of a "cleared screen".
255+
256+ Using subprocess might be a bad idea, but I'm yet to see how bad of an idea that is.
257+ """
258+ subprocess .run (["cls" if os .name == "nt" else "clear" ])
0 commit comments