Skip to content

Commit 9a1d8b4

Browse files
committed
implement Markdown formatting and code syntax highlighting
1 parent d407dc8 commit 9a1d8b4

8 files changed

Lines changed: 1293 additions & 66 deletions

File tree

Cargo.lock

Lines changed: 1226 additions & 31 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "sofos"
3-
version = "0.1.13"
3+
version = "0.1.14"
44
edition = "2021"
55

66
authors = ["Alexander Alexandrov"]
@@ -25,6 +25,9 @@ toml = "0.9"
2525
thiserror = "2"
2626
colored = "3"
2727
syntect = "5"
28+
# pulldown-cmark-mdcat v2.7 needs pulldown-cmark v0.12
29+
pulldown-cmark = "0.12"
30+
pulldown-cmark-mdcat = "2.7"
2831
similar = "2"
2932
crossterm = "0.29"
3033
reedline = "0.44"

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ A blazingly fast, interactive AI coding assistant powered by Claude or GPT, impl
3232
## Features
3333

3434
- **Interactive REPL** - Multi-turn conversations with Claude or GPT
35+
- **Markdown Formatting** - AI responses with syntax highlighting for code blocks
3536
- **Image Vision** - Analyze local or web images
3637
- **Session History** - Auto-save and resume conversations
3738
- **Custom Instructions** - Project and personal context files

assets/sofos_code.png

-85.9 KB
Loading

src/repl.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -584,7 +584,7 @@ impl Repl {
584584
);
585585
println!();
586586

587-
self.ui.display_session(&session);
587+
self.ui.display_session(&session)?;
588588

589589
Ok(())
590590
}

src/response_handler.rs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,8 @@ impl ResponseHandler {
8585
if !text_output.is_empty() {
8686
println!("{}", "Assistant:".bright_blue().bold());
8787
for text in &text_output {
88-
self.ui.print_assistant_text(text);
88+
self.ui.print_assistant_text(text)?;
8989
}
90-
println!();
9190

9291
let combined_text = text_output.join("\n");
9392
display_messages.push(DisplayMessage::AssistantMessage {
@@ -288,8 +287,8 @@ impl ResponseHandler {
288287
UI::create_tool_display_message(tool_name, tool_input, &output);
289288

290289
if !display_output.is_empty() {
291-
println!("{}", display_output.dimmed());
292-
println!();
290+
let ui = UI::new();
291+
ui.print_tool_output(&display_output);
293292
}
294293

295294
display_messages.push(DisplayMessage::ToolExecution {
@@ -475,8 +474,7 @@ impl ResponseHandler {
475474
if let ContentBlock::Text { text } = block {
476475
if !text.trim().is_empty() {
477476
println!("{}", "Assistant:".bright_blue().bold());
478-
self.ui.print_assistant_text(text);
479-
println!();
477+
self.ui.print_assistant_text(text)?;
480478

481479
display_messages.push(DisplayMessage::AssistantMessage {
482480
content: text.clone(),

src/syntax.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use syntect::highlighting::{Style, ThemeSet};
44
use syntect::parsing::SyntaxSet;
55
use syntect::util::as_24_bit_terminal_escaped;
66

7+
#[allow(dead_code)]
78
pub struct SyntaxHighlighter {
89
syntax_set: SyntaxSet,
910
theme_set: ThemeSet,
@@ -17,6 +18,7 @@ impl SyntaxHighlighter {
1718
}
1819
}
1920

21+
#[allow(dead_code)]
2022
pub fn highlight_text(&self, text: &str) -> String {
2123
let mut result = String::new();
2224
let mut in_code_block = false;

src/ui.rs

Lines changed: 55 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@ use std::sync::Arc;
1010
use std::thread;
1111
use std::time::Duration;
1212

13+
use pulldown_cmark::{Options, Parser};
14+
use pulldown_cmark_mdcat::{
15+
push_tty,
16+
resources::NoopResourceHandler,
17+
terminal::{TerminalProgram, TerminalSize},
18+
Environment, Settings, Theme,
19+
};
20+
use syntect::parsing::SyntaxSet;
21+
1322
/// Message severity levels for consistent UI feedback
1423
#[derive(Debug, Clone, Copy, PartialEq)]
1524
pub enum MessageSeverity {
@@ -130,6 +139,7 @@ impl Drop for RawModeGuard {
130139
}
131140

132141
/// UI utilities for displaying messages, animations, and formatting
142+
#[allow(dead_code)]
133143
pub struct UI {
134144
highlighter: SyntaxHighlighter,
135145
}
@@ -258,14 +268,14 @@ impl UI {
258268
println!();
259269
}
260270

261-
pub fn display_session(&self, session: &crate::history::Session) {
271+
pub fn display_session(&self, session: &crate::history::Session) -> io::Result<()> {
262272
if session.display_messages.is_empty() {
263273
println!(
264274
"{}",
265275
"Note: No display history available for this session.".dimmed()
266276
);
267277
println!();
268-
return;
278+
return Ok(());
269279
}
270280

271281
println!("{}", "═".repeat(80).bright_cyan());
@@ -281,9 +291,7 @@ impl UI {
281291
}
282292
DisplayMessage::AssistantMessage { content } => {
283293
println!("{}", "Assistant:".bright_blue().bold());
284-
let highlighted = self.highlighter.highlight_text(content);
285-
println!("{}", highlighted);
286-
println!();
294+
self.print_assistant_text(content)?;
287295
}
288296
DisplayMessage::ToolExecution {
289297
tool_name,
@@ -296,33 +304,25 @@ impl UI {
296304
) {
297305
if let Some(command) = input_val.get("command").and_then(|v| v.as_str())
298306
{
299-
println!(
300-
"{} {}",
301-
"Executing:".bright_green().bold(),
302-
command.bright_cyan()
303-
);
307+
self.print_tool_header(tool_name, Some(command));
304308
}
305309
}
306310
} else {
307-
println!(
308-
"{} {}",
309-
"Using tool:".bright_yellow().bold(),
310-
tool_name.bright_yellow()
311-
);
311+
self.print_tool_header(tool_name, None);
312312
}
313-
println!("{}", tool_output.dimmed());
314-
println!();
313+
self.print_tool_output(tool_output);
315314
}
316315
}
317316
}
318317

319318
println!("{}", "═".repeat(80).bright_cyan());
320319
println!();
320+
Ok(())
321321
}
322322

323-
pub fn print_assistant_text(&self, text: &str) {
324-
let highlighted = self.highlighter.highlight_text(text);
325-
println!("{}", highlighted);
323+
pub fn print_assistant_text(&self, text: &str) -> io::Result<()> {
324+
self.print_markdown_highlighted(text)?;
325+
Ok(())
326326
}
327327

328328
pub fn print_thinking(&self, thinking: &str) {
@@ -331,8 +331,7 @@ impl UI {
331331
"\n{}",
332332
"Thinking:".truecolor(0x77, 0x00, 0xFF).bold().dimmed()
333333
);
334-
println!("{}", thinking.dimmed().italic());
335-
println!();
334+
println!("{}\n", thinking.dimmed().italic());
336335
}
337336
}
338337

@@ -344,7 +343,7 @@ impl UI {
344343
"Executing:".bright_green().bold(),
345344
cmd.bright_cyan()
346345
);
347-
let _ = io::stdout().flush();
346+
let _ = stdout().flush();
348347
}
349348
} else {
350349
println!(
@@ -355,6 +354,35 @@ impl UI {
355354
}
356355
}
357356

357+
pub fn print_tool_output(&self, tool_output: &str) {
358+
println!("{}\n", tool_output.dimmed());
359+
}
360+
361+
/// Render Markdown to the current terminal with ANSI styling + syntax-highlighted fenced code blocks.
362+
pub fn print_markdown_highlighted(&self, md: &str) -> io::Result<()> {
363+
let parser = Parser::new_ext(md, Options::all());
364+
365+
// Required by the API; current_dir is just a convenient absolute base.
366+
let environment = Environment::for_local_directory(&std::env::current_dir()?)?;
367+
368+
let settings = Settings {
369+
terminal_capabilities: TerminalProgram::detect().capabilities(),
370+
terminal_size: TerminalSize::detect().unwrap_or_default(),
371+
syntax_set: &SyntaxSet::load_defaults_newlines(),
372+
theme: Theme::default(),
373+
};
374+
375+
let mut out = stdout().lock();
376+
push_tty(
377+
&settings,
378+
&environment,
379+
&NoopResourceHandler,
380+
&mut out,
381+
parser,
382+
)?;
383+
out.flush()
384+
}
385+
358386
pub fn run_animation_with_interrupt(
359387
action_message: String,
360388
interrupt_message: String,
@@ -371,7 +399,7 @@ impl UI {
371399
let mut frame_idx = 0;
372400

373401
print!("\n\x1B[?25l");
374-
let _ = io::stdout().flush();
402+
let _ = stdout().flush();
375403

376404
while running_anim.load(Ordering::SeqCst) {
377405
print!(
@@ -380,15 +408,15 @@ impl UI {
380408
action_message.truecolor(0xFF, 0x99, 0x33),
381409
interrupt_message.dimmed(),
382410
);
383-
let _ = io::stdout().flush();
411+
let _ = stdout().flush();
384412
frame_idx = (frame_idx + 1) % frames.len();
385413
thread::sleep(Duration::from_millis(80));
386414
}
387415

388416
print!("\r{}\r", " ".repeat(70));
389417
print!("\x1B[?25h");
390418
println!(); // Move to new line so next output doesn't conflict
391-
let _ = io::stdout().flush();
419+
let _ = stdout().flush();
392420
});
393421

394422
let key_handle = thread::spawn(move || {
@@ -419,7 +447,7 @@ impl UI {
419447
// Final cleanup - ensure terminal is in a known good state
420448
let _ = crossterm::terminal::disable_raw_mode();
421449
print!("\x1B[?25h");
422-
let _ = io::stdout().flush();
450+
let _ = stdout().flush();
423451
}
424452

425453
pub fn create_tool_display_message(

0 commit comments

Comments
 (0)