11use directories:: ProjectDirs ;
2+ use pulldown_cmark:: { Options , Parser } ;
23use std:: fs;
34use std:: path:: { Path , PathBuf } ;
45
@@ -13,14 +14,374 @@ pub fn get_config_dir() -> PathBuf {
1314}
1415
1516pub fn render_response ( text : & str ) -> String {
16- let mut formatted = text. replace ( "```bash" , "\x1b [1;33m" ) ; // Yellow for bash code
17- formatted = formatted. replace ( "```shell" , "\x1b [1;33m" ) ; // Yellow for shell code
18- formatted = formatted. replace ( "```javascript" , "\x1b [1;34m" ) ; // Blue for JavaScript
19- formatted = formatted. replace ( "```python" , "\x1b [1;32m" ) ; // Green for Python
20- formatted = formatted. replace ( "```rust" , "\x1b [1;31m" ) ; // Red for Rust
21- formatted = formatted. replace ( "```" , "\x1b [0m" ) ; // Reset color at end of code block
22-
23- formatted
17+ render_markdown ( text)
18+ }
19+
20+ fn render_markdown ( markdown : & str ) -> String {
21+ let mut options = Options :: empty ( ) ;
22+ options. insert ( Options :: ENABLE_STRIKETHROUGH ) ;
23+ options. insert ( Options :: ENABLE_TABLES ) ;
24+ options. insert ( Options :: ENABLE_FOOTNOTES ) ;
25+ options. insert ( Options :: ENABLE_TASKLISTS ) ;
26+
27+ let parser = Parser :: new_ext ( markdown, options) ;
28+ let mut output = String :: new ( ) ;
29+ let mut in_code_block = false ;
30+ let mut code_language: String = String :: new ( ) ;
31+ let mut code_content = String :: new ( ) ;
32+ let mut list_depth: usize = 0 ;
33+
34+ for event in parser {
35+ match event {
36+ pulldown_cmark:: Event :: Start ( tag) => {
37+ match tag {
38+ pulldown_cmark:: Tag :: Heading ( level, _, _) => {
39+ output. push ( '\n' ) ;
40+ let ( color, symbol) = match level {
41+ pulldown_cmark:: HeadingLevel :: H1 => ( "\x1b [1;96m" , "█" ) , // Bright cyan
42+ pulldown_cmark:: HeadingLevel :: H2 => ( "\x1b [1;95m" , "▓" ) , // Bright magenta
43+ pulldown_cmark:: HeadingLevel :: H3 => ( "\x1b [1;94m" , "▒" ) , // Bright blue
44+ pulldown_cmark:: HeadingLevel :: H4 => ( "\x1b [1;93m" , "░" ) , // Bright yellow
45+ _ => ( "\x1b [1;97m" , "▪" ) , // Bright white
46+ } ;
47+ output. push_str ( & format ! ( "{}{} " , color, symbol) ) ;
48+ }
49+ pulldown_cmark:: Tag :: Strong => {
50+ output. push_str ( "\x1b [1;97m" ) ; // Bright white bold
51+ }
52+ pulldown_cmark:: Tag :: Emphasis => {
53+ output. push_str ( "\x1b [3;96m" ) ; // Italic cyan
54+ }
55+ pulldown_cmark:: Tag :: CodeBlock ( kind) => {
56+ in_code_block = true ;
57+ code_content. clear ( ) ;
58+
59+ if let pulldown_cmark:: CodeBlockKind :: Fenced ( lang) = kind {
60+ code_language = lang. to_string ( ) ;
61+ } else {
62+ code_language. clear ( ) ;
63+ }
64+
65+ output. push ( '\n' ) ;
66+ // Top border
67+ output. push_str ( "\x1b [90m┌" ) ;
68+ if !code_language. is_empty ( ) {
69+ output. push_str ( & format ! ( "─ \x1b [93m{}\x1b [90m " , code_language) ) ;
70+ output. push_str (
71+ & "─" . repeat ( 60_usize . saturating_sub ( code_language. len ( ) + 3 ) ) ,
72+ ) ;
73+ } else {
74+ output. push_str ( & "─" . repeat ( 60 ) ) ;
75+ }
76+ output. push_str ( "┐\x1b [0m\n " ) ;
77+ }
78+ pulldown_cmark:: Tag :: List ( _) => {
79+ if list_depth == 0 {
80+ output. push ( '\n' ) ;
81+ }
82+ list_depth += 1 ;
83+ }
84+ pulldown_cmark:: Tag :: Item => {
85+ let indent = " " . repeat ( list_depth. saturating_sub ( 1 ) ) ;
86+ output. push_str ( & format ! ( "{}∘ " , indent) ) ;
87+ }
88+ pulldown_cmark:: Tag :: BlockQuote => {
89+ output. push_str ( "\x1b [90m▐ \x1b [3;37m" ) ; // Gray bar with italic text
90+ }
91+ pulldown_cmark:: Tag :: Link ( _, _, _) => {
92+ output. push_str ( "\x1b [4;34m" ) ; // Blue underlined
93+ }
94+ pulldown_cmark:: Tag :: Strikethrough => {
95+ output. push_str ( "\x1b [9m" ) ; // Strikethrough
96+ }
97+ pulldown_cmark:: Tag :: Table ( _) => {
98+ output. push ( '\n' ) ;
99+ output. push_str ( "\x1b [90m┌─ Table ─┐\x1b [0m\n " ) ;
100+ }
101+ pulldown_cmark:: Tag :: TableHead => {
102+ output. push_str ( "\x1b [1;93m" ) ; // Bold yellow headers
103+ }
104+ pulldown_cmark:: Tag :: TableCell => {
105+ output. push_str ( "│ " ) ;
106+ }
107+ _ => { }
108+ }
109+ }
110+ pulldown_cmark:: Event :: End ( tag) => match tag {
111+ pulldown_cmark:: Tag :: Heading ( level, _, _) => {
112+ output. push_str ( "\x1b [0m" ) ;
113+ match level {
114+ pulldown_cmark:: HeadingLevel :: H1 => {
115+ output. push ( '\n' ) ;
116+ output. push_str ( "\x1b [90m" ) ;
117+ output. push_str ( & "═" . repeat ( 60 ) ) ;
118+ output. push_str ( "\x1b [0m" ) ;
119+ }
120+ pulldown_cmark:: HeadingLevel :: H2 => {
121+ output. push ( '\n' ) ;
122+ output. push_str ( "\x1b [90m" ) ;
123+ output. push_str ( & "─" . repeat ( 40 ) ) ;
124+ output. push_str ( "\x1b [0m" ) ;
125+ }
126+ _ => { }
127+ }
128+ output. push ( '\n' ) ;
129+ }
130+ pulldown_cmark:: Tag :: Strong
131+ | pulldown_cmark:: Tag :: Emphasis
132+ | pulldown_cmark:: Tag :: Strikethrough
133+ | pulldown_cmark:: Tag :: Link ( _, _, _) => {
134+ output. push_str ( "\x1b [0m" ) ;
135+ }
136+ pulldown_cmark:: Tag :: CodeBlock ( _) => {
137+ in_code_block = false ;
138+
139+ if !code_content. is_empty ( ) {
140+ let highlighted = highlight_code_block ( & code_content, & code_language) ;
141+ for line in highlighted. lines ( ) {
142+ output. push_str ( "\x1b [90m│\x1b [0m " ) ;
143+ output. push_str ( line) ;
144+ output. push ( '\n' ) ;
145+ }
146+ }
147+
148+ output. push_str ( "\x1b [90m└" ) ;
149+ output. push_str ( & "─" . repeat ( 60 ) ) ;
150+ output. push_str ( "┘\x1b [0m\n " ) ;
151+ }
152+ pulldown_cmark:: Tag :: List ( _) => {
153+ list_depth = list_depth. saturating_sub ( 1 ) ;
154+ if list_depth == 0 {
155+ output. push ( '\n' ) ;
156+ }
157+ }
158+ pulldown_cmark:: Tag :: Item => {
159+ output. push ( '\n' ) ;
160+ }
161+ pulldown_cmark:: Tag :: Paragraph => {
162+ if !in_code_block {
163+ output. push ( '\n' ) ;
164+ }
165+ }
166+ pulldown_cmark:: Tag :: BlockQuote => {
167+ output. push_str ( "\x1b [0m\n " ) ;
168+ }
169+ pulldown_cmark:: Tag :: TableHead => {
170+ output. push_str ( "\x1b [0m\n " ) ;
171+ output. push_str ( "\x1b [90m" ) ;
172+ output. push_str ( & "─" . repeat ( 60 ) ) ;
173+ output. push_str ( "\x1b [0m\n " ) ;
174+ }
175+ pulldown_cmark:: Tag :: TableRow => {
176+ output. push ( '\n' ) ;
177+ }
178+ _ => { }
179+ } ,
180+ pulldown_cmark:: Event :: Text ( text) => {
181+ if in_code_block {
182+ code_content. push_str ( & text) ;
183+ } else {
184+ output. push_str ( & text) ;
185+ }
186+ }
187+ pulldown_cmark:: Event :: Code ( code) => {
188+ output. push_str ( "\x1b [48;5;238;38;5;156m" ) ;
189+ output. push_str ( " " ) ;
190+ output. push_str ( & code) ;
191+ output. push_str ( " " ) ;
192+ output. push_str ( "\x1b [0m" ) ;
193+ }
194+ pulldown_cmark:: Event :: SoftBreak => {
195+ if in_code_block {
196+ code_content. push ( '\n' ) ;
197+ } else {
198+ output. push ( ' ' ) ;
199+ }
200+ }
201+ pulldown_cmark:: Event :: HardBreak => {
202+ if in_code_block {
203+ code_content. push ( '\n' ) ;
204+ } else {
205+ output. push ( '\n' ) ;
206+ }
207+ }
208+ pulldown_cmark:: Event :: TaskListMarker ( checked) => {
209+ if checked {
210+ output. push_str ( "\x1b [92m✓\x1b [0m " ) ;
211+ } else {
212+ output. push_str ( "\x1b [90m○\x1b [0m " ) ;
213+ }
214+ }
215+ _ => { }
216+ }
217+ }
218+
219+ output
220+ }
221+
222+ fn highlight_code_block ( code : & str , language : & str ) -> String {
223+ if !language. is_empty ( ) {
224+ if let Ok ( highlighted) = highlight_with_syntect ( code, language) {
225+ return highlighted;
226+ }
227+ }
228+
229+ let mut output = String :: new ( ) ;
230+ for line in code. lines ( ) {
231+ if line. trim ( ) . is_empty ( ) {
232+ output. push ( '\n' ) ;
233+ continue ;
234+ }
235+
236+ let colored_line = match language. to_lowercase ( ) . as_str ( ) {
237+ "rust" | "rs" => highlight_rust_line ( line) ,
238+ "python" | "py" => highlight_python_line ( line) ,
239+ "javascript" | "js" | "typescript" | "ts" => highlight_js_line ( line) ,
240+ "json" => highlight_json_line ( line) ,
241+ "bash" | "sh" | "shell" => highlight_bash_line ( line) ,
242+ _ => format ! ( "\x1b [37m{}\x1b [0m" , line) , // Default white
243+ } ;
244+
245+ output. push_str ( & colored_line) ;
246+ output. push ( '\n' ) ;
247+ }
248+
249+ output
250+ }
251+
252+ fn highlight_with_syntect (
253+ code : & str ,
254+ language : & str ,
255+ ) -> Result < String , Box < dyn std:: error:: Error > > {
256+ use syntect:: easy:: HighlightLines ;
257+ use syntect:: highlighting:: { Style , ThemeSet } ;
258+ use syntect:: parsing:: SyntaxSet ;
259+ use syntect:: util:: { as_24_bit_terminal_escaped, LinesWithEndings } ;
260+
261+ let ss = SyntaxSet :: load_defaults_newlines ( ) ;
262+ let ts = ThemeSet :: load_defaults ( ) ;
263+ let theme = & ts. themes [ "base16-ocean.dark" ] ;
264+
265+ let syntax = ss
266+ . find_syntax_by_extension ( language)
267+ . or_else ( || ss. find_syntax_by_token ( language) )
268+ . unwrap_or_else ( || ss. find_syntax_plain_text ( ) ) ;
269+
270+ let mut h = HighlightLines :: new ( syntax, theme) ;
271+ let mut output = String :: new ( ) ;
272+
273+ for line in LinesWithEndings :: from ( code) {
274+ let ranges: Vec < ( Style , & str ) > = h. highlight_line ( line, & ss) ?;
275+ let escaped = as_24_bit_terminal_escaped ( & ranges[ ..] , false ) ;
276+ output. push_str ( & escaped) ;
277+ }
278+
279+ Ok ( output)
280+ }
281+
282+ fn highlight_rust_line ( line : & str ) -> String {
283+ let mut result = String :: new ( ) ;
284+ let keywords = [
285+ "fn" , "let" , "mut" , "if" , "else" , "for" , "while" , "match" , "struct" , "enum" , "impl" , "use" ,
286+ "pub" ,
287+ ] ;
288+
289+ for word in line. split_whitespace ( ) {
290+ if keywords. contains ( & word) {
291+ result. push_str ( & format ! ( "\x1b [94m{}\x1b [0m " , word) ) ; // Blue
292+ } else if word. starts_with ( "//" ) {
293+ result. push_str ( & format ! ( "\x1b [90m{}\x1b [0m " , word) ) ; // Gray
294+ } else if word. starts_with ( '"' ) && word. ends_with ( '"' ) {
295+ result. push_str ( & format ! ( "\x1b [92m{}\x1b [0m " , word) ) ; // Green
296+ } else {
297+ result. push_str ( & format ! ( "\x1b [37m{}\x1b [0m " , word) ) ; // White
298+ }
299+ }
300+
301+ result
302+ }
303+
304+ fn highlight_python_line ( line : & str ) -> String {
305+ let mut result = String :: new ( ) ;
306+ let keywords = [
307+ "def" , "class" , "if" , "else" , "elif" , "for" , "while" , "import" , "from" , "return" , "try" ,
308+ "except" ,
309+ ] ;
310+
311+ for word in line. split_whitespace ( ) {
312+ if keywords. contains ( & word) {
313+ result. push_str ( & format ! ( "\x1b [94m{}\x1b [0m " , word) ) ; // Blue
314+ } else if word. starts_with ( "#" ) {
315+ result. push_str ( & format ! ( "\x1b [90m{}\x1b [0m " , word) ) ; // Gray
316+ } else if ( word. starts_with ( '"' ) && word. ends_with ( '"' ) )
317+ || ( word. starts_with ( '\'' ) && word. ends_with ( '\'' ) )
318+ {
319+ result. push_str ( & format ! ( "\x1b [92m{}\x1b [0m " , word) ) ; // Green
320+ } else {
321+ result. push_str ( & format ! ( "\x1b [37m{}\x1b [0m " , word) ) ; // White
322+ }
323+ }
324+
325+ result
326+ }
327+
328+ fn highlight_js_line ( line : & str ) -> String {
329+ let mut result = String :: new ( ) ;
330+ let keywords = [
331+ "function" , "const" , "let" , "var" , "if" , "else" , "for" , "while" , "return" , "class" ,
332+ "import" , "export" ,
333+ ] ;
334+
335+ for word in line. split_whitespace ( ) {
336+ if keywords. contains ( & word) {
337+ result. push_str ( & format ! ( "\x1b [94m{}\x1b [0m " , word) ) ; // Blue
338+ } else if word. starts_with ( "//" ) {
339+ result. push_str ( & format ! ( "\x1b [90m{}\x1b [0m " , word) ) ; // Gray
340+ } else if word. starts_with ( '"' ) && word. ends_with ( '"' ) {
341+ result. push_str ( & format ! ( "\x1b [92m{}\x1b [0m " , word) ) ; // Green
342+ } else {
343+ result. push_str ( & format ! ( "\x1b [37m{}\x1b [0m " , word) ) ; // White
344+ }
345+ }
346+
347+ result
348+ }
349+
350+ fn highlight_json_line ( line : & str ) -> String {
351+ let trimmed = line. trim ( ) ;
352+ if trimmed. starts_with ( '"' ) && trimmed. contains ( ':' ) {
353+ format ! ( "\x1b [96m{}\x1b [0m" , line) // Cyan for keys
354+ } else if trimmed. starts_with ( '"' ) {
355+ format ! ( "\x1b [92m{}\x1b [0m" , line) // Green for string values
356+ } else if trimmed
357+ . chars ( )
358+ . all ( |c| c. is_ascii_digit ( ) || c == '.' || c == '-' )
359+ {
360+ format ! ( "\x1b [93m{}\x1b [0m" , line) // Yellow for numbers
361+ } else {
362+ format ! ( "\x1b [37m{}\x1b [0m" , line) // White for everything else
363+ }
364+ }
365+
366+ fn highlight_bash_line ( line : & str ) -> String {
367+ let mut result = String :: new ( ) ;
368+ let keywords = [
369+ "if" , "then" , "else" , "fi" , "for" , "do" , "done" , "while" , "case" , "esac" , "function" ,
370+ ] ;
371+
372+ for word in line. split_whitespace ( ) {
373+ if keywords. contains ( & word) {
374+ result. push_str ( & format ! ( "\x1b [94m{}\x1b [0m " , word) ) ; // Blue
375+ } else if word. starts_with ( "#" ) {
376+ result. push_str ( & format ! ( "\x1b [90m{}\x1b [0m " , word) ) ; // Gray
377+ } else if word. starts_with ( '$' ) {
378+ result. push_str ( & format ! ( "\x1b [95m{}\x1b [0m " , word) ) ; // Magenta for variables
379+ } else {
380+ result. push_str ( & format ! ( "\x1b [37m{}\x1b [0m " , word) ) ; // White
381+ }
382+ }
383+
384+ result
24385}
25386
26387pub fn set_model ( model_name : & str , config_dir : & Path ) -> bool {
0 commit comments