From f7613c903076bfb80db65c3ac9f389d6689c308a Mon Sep 17 00:00:00 2001 From: buenosairesam Date: Fri, 3 Apr 2026 04:56:32 -0300 Subject: [PATCH] markdown --- cht/ui/markdown.py | 145 +++++++++++++++++++++++++++++++++++++++++++++ cht/window.py | 133 +---------------------------------------- 2 files changed, 148 insertions(+), 130 deletions(-) create mode 100644 cht/ui/markdown.py diff --git a/cht/ui/markdown.py b/cht/ui/markdown.py new file mode 100644 index 0000000..38e0bd1 --- /dev/null +++ b/cht/ui/markdown.py @@ -0,0 +1,145 @@ +"""Markdown → Gtk.TextBuffer renderer. + +Pure functions — no app state, just TextBuffer + TextIter operations. +""" + +import re + +from gi.repository import Gtk + + +def setup_tags(buf: Gtk.TextBuffer) -> None: + """Create the standard set of text tags used by render().""" + buf.create_tag("h1", weight=700, scale=1.4) + buf.create_tag("h2", weight=700, scale=1.2) + buf.create_tag("h3", weight=700, scale=1.05) + buf.create_tag("bold", weight=700) + buf.create_tag("italic", style=2) + buf.create_tag("code", family="monospace", background="#2a2a2a", foreground="#e8e8e8") + buf.create_tag("codeblock", family="monospace", background="#1e1e1e", + foreground="#e8e8e8", left_margin=16, right_margin=16, + pixels_above_lines=4, pixels_below_lines=4) + buf.create_tag("bullet", left_margin=16) + + +def render(buf: Gtk.TextBuffer, insert_iter: Gtk.TextIter, text: str) -> None: + """Parse *text* as lightweight Markdown and insert into *buf* at *insert_iter*.""" + lines = text.split("\n") + i = 0 + while i < len(lines): + line = lines[i] + if line.startswith("```"): + lang = line[3:].strip() + block_lines = [] + i += 1 + while i < len(lines) and not lines[i].startswith("```"): + block_lines.append(lines[i]) + i += 1 + code = "\n".join(block_lines) + "\n" + block_mark = buf.create_mark(None, insert_iter, True) + _insert_highlighted_code(buf, insert_iter, code, lang) + buf.apply_tag_by_name("codeblock", buf.get_iter_at_mark(block_mark), insert_iter) + buf.delete_mark(block_mark) + buf.insert(insert_iter, "\n") + i += 1 + continue + header_match = re.match(r"^(#{1,3})\s+(.*)", line) + if header_match: + level = len(header_match.group(1)) + content = header_match.group(2) + tag = f"h{level}" + mark = buf.create_mark(None, insert_iter, True) + buf.insert(insert_iter, content + "\n") + buf.apply_tag_by_name(tag, buf.get_iter_at_mark(mark), insert_iter) + i += 1 + continue + bullet_match = re.match(r"^[\-\*]\s+(.*)", line) + if bullet_match: + _insert_inline(buf, insert_iter, "• " + bullet_match.group(1), "bullet") + buf.insert(insert_iter, "\n") + i += 1 + continue + _insert_inline_line(buf, insert_iter, line) + buf.insert(insert_iter, "\n") + i += 1 + + +# -- private helpers -- + +_INLINE_RE = re.compile( + r"(\*\*(.+?)\*\*|__(.+?)__|" + r"\*(.+?)\*|_(.+?)_|" + r"`([^`]+?)`)" +) + + +def _insert_inline(buf, it, text: str, outer_tag: str | None = None): + mark = buf.create_mark(None, it, True) + _insert_inline_line(buf, it, text) + if outer_tag: + buf.apply_tag_by_name(outer_tag, buf.get_iter_at_mark(mark), it) + + +def _insert_inline_line(buf, it, text: str): + pos = 0 + for m in _INLINE_RE.finditer(text): + if m.start() > pos: + buf.insert(it, text[pos:m.start()]) + full = m.group(0) + if full.startswith("**") or full.startswith("__"): + inner = m.group(2) or m.group(3) + mark = buf.create_mark(None, it, True) + buf.insert(it, inner) + buf.apply_tag_by_name("bold", buf.get_iter_at_mark(mark), it) + elif full.startswith("*") or full.startswith("_"): + inner = m.group(4) or m.group(5) + mark = buf.create_mark(None, it, True) + buf.insert(it, inner) + buf.apply_tag_by_name("italic", buf.get_iter_at_mark(mark), it) + elif full.startswith("`"): + inner = m.group(6) + mark = buf.create_mark(None, it, True) + buf.insert(it, inner) + buf.apply_tag_by_name("code", buf.get_iter_at_mark(mark), it) + pos = m.end() + if pos < len(text): + buf.insert(it, text[pos:]) + + +def _insert_highlighted_code(buf, it, code: str, lang: str): + try: + from pygments.lexers import get_lexer_by_name, guess_lexer + from pygments.styles import get_style_by_name + except ImportError: + buf.insert(it, code) + return + try: + lexer = get_lexer_by_name(lang, stripall=False) if lang else guess_lexer(code) + except Exception: + try: + lexer = guess_lexer(code) + except Exception: + buf.insert(it, code) + return + try: + style = get_style_by_name("monokai") + except Exception: + buf.insert(it, code) + return + tag_table = buf.get_tag_table() + for ttype, value in lexer.get_tokens(code): + tag_name = f"pyg_{ttype}" + tag = tag_table.lookup(tag_name) + if tag is None: + sf = style.style_for_token(ttype) + tag = buf.create_tag(tag_name, family="monospace") + if sf.get("color"): + tag.set_property("foreground", f"#{sf['color']}") + if sf.get("bold"): + tag.set_property("weight", 700) + if sf.get("italic"): + tag.set_property("style", 2) + mark = buf.create_mark(None, it, True) + buf.insert(it, value) + buf.apply_tag(tag, buf.get_iter_at_mark(mark), it) + buf.delete_mark(mark) diff --git a/cht/window.py b/cht/window.py index 821db88..9cf3459 100644 --- a/cht/window.py +++ b/cht/window.py @@ -19,6 +19,7 @@ from cht.ui.waveform import WaveformWidget from cht.ui.frames_panel import FramesPanel from cht.ui.transcript_panel import TranscriptPanel from cht.ui.keyboard import KeyboardManager, KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN, KEY_RETURN, KEY_KP_ENTER, KEY_ESCAPE, KEY_DELETE +from cht.ui import markdown from cht.audio.waveform import WaveformEngine from cht.transcriber.engine import TranscriberEngine, LANGUAGES from cht.stream.manager import StreamManager, list_sessions, delete_sessions @@ -497,7 +498,7 @@ class ChtWindow(Adw.ApplicationWindow): self._agent_output_view.set_cursor_visible(False) self._agent_output_view.set_left_margin(8) self._agent_output_view.set_right_margin(8) - self._setup_md_tags(self._agent_output_view.get_buffer()) + markdown.setup_tags(self._agent_output_view.get_buffer()) scroll = Gtk.ScrolledWindow() scroll.set_vexpand(True) @@ -660,138 +661,10 @@ class ChtWindow(Adw.ApplicationWindow): start = buf.get_iter_at_mark(self._response_start_mark) end = buf.get_end_iter() buf.delete(start, end) - self._render_md(buf, start, "".join(self._response_accum)) + markdown.render(buf, start, "".join(self._response_accum)) buf.delete_mark(self._response_start_mark) self._append_agent_output("\n") - # -- Markdown rendering -- - - def _setup_md_tags(self, buf): - buf.create_tag("h1", weight=700, scale=1.4) - buf.create_tag("h2", weight=700, scale=1.2) - buf.create_tag("h3", weight=700, scale=1.05) - buf.create_tag("bold", weight=700) - buf.create_tag("italic", style=2) - buf.create_tag("code", family="monospace", background="#2a2a2a", foreground="#e8e8e8") - buf.create_tag("codeblock", family="monospace", background="#1e1e1e", - foreground="#e8e8e8", left_margin=16, right_margin=16, - pixels_above_lines=4, pixels_below_lines=4) - buf.create_tag("bullet", left_margin=16) - - def _render_md(self, buf, insert_iter, text: str): - import re - lines = text.split("\n") - i = 0 - while i < len(lines): - line = lines[i] - if line.startswith("```"): - lang = line[3:].strip() - block_lines = [] - i += 1 - while i < len(lines) and not lines[i].startswith("```"): - block_lines.append(lines[i]) - i += 1 - code = "\n".join(block_lines) + "\n" - block_mark = buf.create_mark(None, insert_iter, True) - self._insert_highlighted_code(buf, insert_iter, code, lang) - buf.apply_tag_by_name("codeblock", buf.get_iter_at_mark(block_mark), insert_iter) - buf.delete_mark(block_mark) - buf.insert(insert_iter, "\n") - i += 1 - continue - header_match = re.match(r"^(#{1,3})\s+(.*)", line) - if header_match: - level = len(header_match.group(1)) - content = header_match.group(2) - tag = f"h{level}" - mark = buf.create_mark(None, insert_iter, True) - buf.insert(insert_iter, content + "\n") - buf.apply_tag_by_name(tag, buf.get_iter_at_mark(mark), insert_iter) - i += 1 - continue - bullet_match = re.match(r"^[\-\*]\s+(.*)", line) - if bullet_match: - self._insert_inline(buf, insert_iter, "• " + bullet_match.group(1), "bullet") - buf.insert(insert_iter, "\n") - i += 1 - continue - self._insert_inline_line(buf, insert_iter, line) - buf.insert(insert_iter, "\n") - i += 1 - - def _insert_inline(self, buf, it, text: str, outer_tag: str | None = None): - mark = buf.create_mark(None, it, True) - self._insert_inline_line(buf, it, text) - if outer_tag: - buf.apply_tag_by_name(outer_tag, buf.get_iter_at_mark(mark), it) - - def _insert_inline_line(self, buf, it, text: str): - import re - pattern = re.compile(r"(\*\*(.+?)\*\*|__(.+?)__|" - r"\*(.+?)\*|_(.+?)_|" - r"`([^`]+?)`)") - pos = 0 - for m in pattern.finditer(text): - if m.start() > pos: - buf.insert(it, text[pos:m.start()]) - full = m.group(0) - if full.startswith("**") or full.startswith("__"): - inner = m.group(2) or m.group(3) - mark = buf.create_mark(None, it, True) - buf.insert(it, inner) - buf.apply_tag_by_name("bold", buf.get_iter_at_mark(mark), it) - elif full.startswith("*") or full.startswith("_"): - inner = m.group(4) or m.group(5) - mark = buf.create_mark(None, it, True) - buf.insert(it, inner) - buf.apply_tag_by_name("italic", buf.get_iter_at_mark(mark), it) - elif full.startswith("`"): - inner = m.group(6) - mark = buf.create_mark(None, it, True) - buf.insert(it, inner) - buf.apply_tag_by_name("code", buf.get_iter_at_mark(mark), it) - pos = m.end() - if pos < len(text): - buf.insert(it, text[pos:]) - - def _insert_highlighted_code(self, buf, it, code: str, lang: str): - try: - from pygments.lexers import get_lexer_by_name, guess_lexer - from pygments.styles import get_style_by_name - except ImportError: - buf.insert(it, code) - return - try: - lexer = get_lexer_by_name(lang, stripall=False) if lang else guess_lexer(code) - except Exception: - try: - lexer = guess_lexer(code) - except Exception: - buf.insert(it, code) - return - try: - style = get_style_by_name("monokai") - except Exception: - buf.insert(it, code) - return - tag_table = buf.get_tag_table() - for ttype, value in lexer.get_tokens(code): - tag_name = f"pyg_{ttype}" - tag = tag_table.lookup(tag_name) - if tag is None: - sf = style.style_for_token(ttype) - tag = buf.create_tag(tag_name, family="monospace") - if sf.get("color"): - tag.set_property("foreground", f"#{sf['color']}") - if sf.get("bold"): - tag.set_property("weight", 700) - if sf.get("italic"): - tag.set_property("style", 2) - mark = buf.create_mark(None, it, True) - buf.insert(it, value) - buf.apply_tag(tag, buf.get_iter_at_mark(mark), it) - buf.delete_mark(mark) - # -- Settings callbacks -- def _on_clear_agent_output(self, _button):