From 0fa1ae215ac1434591968818f8f75ad5b35d52ec Mon Sep 17 00:00:00 2001 From: sigoden Date: Sat, 4 Mar 2023 09:02:57 +0800 Subject: feat: support highlight reply markdown (#3) * feat: support highlight reply markdown * migrate markdown highlighter from termimad to mdcat * optimize render No need to clear screen when there is no newline in reply token * handle ctrl-c when rendering stream * update readme * fix ctrlc don't abort acquire_stream when establish connection * ensure render_stream's dtect_ctrlc is exit before next readline This will make reedline don't throw err 'The cursor position could not be read within a normal duration' --- src/render.rs | 189 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 src/render.rs (limited to 'src/render.rs') diff --git a/src/render.rs b/src/render.rs new file mode 100644 index 0000000..6a052e2 --- /dev/null +++ b/src/render.rs @@ -0,0 +1,189 @@ +use anyhow::Result; +use crossbeam::sync::WaitGroup; +use crossterm::{ + cursor, + event::{self, Event, KeyCode, KeyEvent, KeyModifiers}, + execute, queue, style, + terminal::{ + self, disable_raw_mode, enable_raw_mode, size, ClearType, EnterAlternateScreen, + LeaveAlternateScreen, + }, +}; +use mdcat::{ + push_tty, + terminal::{TerminalProgram, TerminalSize}, + Environment, ResourceAccess, Settings, +}; +use pulldown_cmark::Parser; +use std::{ + io::{self, Write}, + sync::{ + atomic::{AtomicBool, Ordering}, + mpsc::Receiver, + Arc, + }, + thread, + time::Duration, +}; +use syntect::parsing::SyntaxSet; + +use crate::repl::{dump, ReplyEvent}; + +pub fn render_stream( + rx: Receiver, + ctrlc: Arc, + markdown_render: Arc, +) -> Result<()> { + let wg = WaitGroup::new(); + let ctrlc_clone = ctrlc.clone(); + let stream_done = Arc::new(AtomicBool::new(false)); + let stream_done_clone = stream_done.clone(); + let wg_clone = wg.clone(); + thread::spawn(move || { + let _ = detect_ctrlc(ctrlc_clone, stream_done_clone); + drop(wg_clone); + }); + let ret = render_stream_inner(rx, ctrlc, markdown_render); + stream_done.store(true, Ordering::SeqCst); + wg.wait(); + ret +} + +fn detect_ctrlc(ctrlc: Arc, stream_done: Arc) -> Result<()> { + loop { + if ctrlc.load(Ordering::SeqCst) || stream_done.load(Ordering::SeqCst) { + return Ok(()); + } + if event::poll(Duration::from_millis(100))? { + if let Event::Key(KeyEvent { + code: KeyCode::Char('c'), + modifiers: KeyModifiers::CONTROL, + .. + }) = event::read()? + { + ctrlc.store(true, Ordering::SeqCst); + break; + } + } + } + Ok(()) +} + +fn render_stream_inner( + rx: Receiver, + ctrlc: Arc, + markdown_render: Arc, +) -> Result<()> { + // setup terminal + enable_raw_mode()?; + let mut output = String::new(); + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen)?; + + fn clear(stdout: &mut impl Write) -> io::Result<()> { + queue!( + stdout, + style::ResetColor, + terminal::Clear(ClearType::All), + cursor::Hide, + cursor::MoveTo(0, 0) + ) + } + + clear(&mut stdout)?; + + while let Ok(ev) = rx.recv() { + if ctrlc.load(Ordering::SeqCst) { + break; + } + match ev { + ReplyEvent::Text(text) => { + output.push_str(&text); + let rows = size()?.1 as usize; + let lines: Vec<&str> = output.split('\n').collect(); + let len = lines.len(); + let skip = if len > rows { len - rows } else { 0 }; + let mut selected_lines = vec![]; + let mut count_begin_code = 0; + let mut code = None; + for (index, line) in lines.iter().enumerate() { + if index < skip { + if line.starts_with("```") { + count_begin_code += 1; + code = Some(*line); + } + } else { + selected_lines.push(*line); + } + } + if count_begin_code % 2 == 1 { + if let Some(code) = code { + selected_lines[0] = code + } + }; + let content = selected_lines.join("\n"); + let markdown = markdown_render.render(&content)?; + if text.contains('\n') { + clear(&mut stdout)?; + for line in markdown.split('\n') { + queue!(stdout, style::Print(line), cursor::MoveToNextLine(1))?; + } + } else if let Some(line) = markdown.split('\n').last() { + queue!( + stdout, + style::ResetColor, + terminal::Clear(ClearType::CurrentLine), + cursor::MoveToColumn(0), + style::Print(line) + )?; + } + + stdout.flush()?; + } + ReplyEvent::Done => { + break; + } + } + } + + execute!(stdout, style::ResetColor, cursor::Show)?; + + // restore terminal + disable_raw_mode()?; + execute!(stdout, LeaveAlternateScreen)?; + + Ok(()) +} + +pub struct MarkdownRender { + env: Environment, + settings: Settings, +} + +impl MarkdownRender { + pub fn init() -> Result { + let terminal = TerminalProgram::detect(); + let env = + Environment::for_local_directory(&std::env::current_dir().expect("Working directory"))?; + let settings = Settings { + resource_access: ResourceAccess::LocalOnly, + syntax_set: SyntaxSet::load_defaults_newlines(), + terminal_capabilities: terminal.capabilities(), + terminal_size: TerminalSize::default(), + }; + Ok(Self { env, settings }) + } + + pub fn print(&self, input: &str) -> Result<()> { + let markdown = self.render(input)?; + dump(markdown, 0); + Ok(()) + } + + pub fn render(&self, input: &str) -> Result { + let source = Parser::new(input); + let mut sink = Vec::new(); + push_tty(&self.settings, &self.env, &mut sink, source)?; + Ok(String::from_utf8_lossy(&sink).into()) + } +} -- cgit v1.2.3