diff --git a/markdown_parser.py b/markdown_parser.py index eba27db..bb830f8 100644 --- a/markdown_parser.py +++ b/markdown_parser.py @@ -1,62 +1,128 @@ -""" -Module: markdown_parser.py ----------------------------- -Jednoduch Markdown parser pro curses. -Rozpozn zkladn znaky: - - **tun** curses.A_BOLD - - *kurzva* curses.A_UNDERLINE - - `kd` curses.A_REVERSE (pro zvraznn kdu) -Vrac seznam segment ve tvaru (text, atribut), kter lze postupn vypsat pomoc curses. -""" - -import re -import curses - -def parse_markdown(text): - """ - Rozparsuje zadan text obsahujc jednoduch Markdown znaky a vrt seznam - dvojic (segment_text, attribute). Nepodporuje vnoen znaky. - - Podporovan znaky: - **text** tun (A_BOLD) - *text* kurzva (A_UNDERLINE) - `text` kd (A_REVERSE) - - Pokud se znaky nepruj, vrac zbytek textu s atributem 0. - """ - segments = [] - pos = 0 - pattern = re.compile(r'(\*\*.*?\*\*|\*.*?\*|`.*?`)') - for match in pattern.finditer(text): - start, end = match.span() - # Normaln text ped znakou - if start > pos: - segments.append((text[pos:start], 0)) - token = match.group(0) - # Rozpoznn typu tokenu - if token.startswith("**") and token.endswith("**"): - # Bold odstranme dvojit hvzdiky - content = token[2:-2] - segments.append((content, curses.A_BOLD)) - elif token.startswith("*") and token.endswith("*"): - # Italic odstranme hvzdiky - content = token[1:-1] - segments.append((content, curses.A_UNDERLINE)) - elif token.startswith("`") and token.endswith("`"): - # Inline kd odstranme zptn apostrofy a pouijeme reverzn barvu - content = token[1:-1] - segments.append((content, curses.A_REVERSE)) - else: - segments.append((token, 0)) - pos = end - # Zbytek textu - if pos < len(text): - segments.append((text[pos:], 0)) - return segments - -if __name__ == "__main__": - # Jednoduch test - sample = "Toto je **tun text**, toto je *kurzva* a toto je `kd`." - segments = parse_markdown(sample) - for seg, attr in segments: - print(f"Segment: '{seg}', Attr: {attr}") +""" +Module: markdown_parser.py +---------------------------- +Rozšířený Markdown parser pro curses. + +Podporované funkce: +1. Nadpisy (řádky začínající #) -> zobrazeny tučně a podtrženě +2. Odrážky (řádky začínající - a *) -> zobrazeny s tučným textem a symbolickou tečkou +3. Víceřádkové bloky kódu (```): + vše mezi trojitými backticky je zobrazeno s curses.A_REVERSE +4. Inline formátování: + **text** -> curses.A_BOLD + *text* -> curses.A_UNDERLINE + `text` -> curses.A_REVERSE + +Poznámka: nepodporujeme vnořené značky, a parser je pouze zjednodušený. +""" + +import re +import curses + +def parse_markdown(text): + """ + Hlavní funkce pro rozparsování zadaného textu na seznam (segment_text, attribute). + Respektuje víceřádkové bloky kódu (```), nadpisy, odrážky a volá parse_inline pro + zpracování běžných řádků. + """ + segments = [] + lines = text.splitlines() + in_code_block = False # Indikátor, zda se nacházíme uvnitř bloku ```. + + for line in lines: + stripped = line.strip() + + # Kontrola začátku/konce bloku kódu (```). + if stripped.startswith("```"): + # Přepínáme stav + in_code_block = not in_code_block + if in_code_block: + # Řádek obsahující samotné ``` + # Pokud tam je něco navíc, ořežeme + code_block_marker = stripped[3:].strip() + # Můžeme sem zařadit logiku pro detekci jazyka, pokud by to bylo potřeba + else: + # Konec bloku kódu + pass + continue + + if in_code_block: + # Ve víceřádkovém kódu každou linku zobrazíme s curses.A_REVERSE + segments.append((line + "\n", curses.A_REVERSE)) + continue + + # Pokud nejsme v bloku kódu, zpracujeme nadpisy, odrážky a inline formát. + if stripped.startswith("#"): + # Nadpis: zjistíme, kolik # tam je + hash_count = 0 + for ch in line: + if ch == '#': + hash_count += 1 + else: + break + # Obsah nadpisu za #... + content = line[hash_count:].strip() + # Atribut: tučný a podtržený + segments.append((content + "\n", curses.A_BOLD | curses.A_UNDERLINE)) + elif stripped.startswith("-") or stripped.startswith("*"): + # Odrážka + content = line[1:].strip() + segments.append((" • " + content + "\n", curses.A_BOLD)) + else: + # Běžný řádek -> inline formát + inline_segments = parse_inline(line) + for seg_text, seg_attr in inline_segments: + segments.append((seg_text, seg_attr)) + segments.append(("\n", 0)) + + return segments + + +def parse_inline(text): + """ + Rozparsuje inline Markdown značky (tučný, kurzíva, inline kód) v jednom řádku, + vrací seznam (text, attr). + """ + segments = [] + pos = 0 + + # Regulární výraz pro zachycení **text**, *text*, `text` + pattern = re.compile(r'(\*\*.*?\*\*|\*.*?\*|`.*?`)') + + for match in pattern.finditer(text): + start, end = match.span() + if start > pos: + segments.append((text[pos:start], 0)) + token = match.group(0) + if token.startswith("**") and token.endswith("**"): + content = token[2:-2] + segments.append((content, curses.A_BOLD)) + elif token.startswith("*") and token.endswith("*"): + content = token[1:-1] + segments.append((content, curses.A_UNDERLINE)) + elif token.startswith("`") and token.endswith("`"): + content = token[1:-1] + segments.append((content, curses.A_REVERSE)) + else: + segments.append((token, 0)) + pos = end + + if pos < len(text): + segments.append((text[pos:], 0)) + return segments + +if __name__ == "__main__": + # Krátký test + sample = """# Nadpis 1 +Toto je normální text s **tučným** a *kurzívou*. +- Odrážka 1 +* Odrážka 2 +A teď blok kódu: +ls -la echo "Hello" +Další text s `inline kódem` a ## menším nadpisem? +## Nadpis 2 +""" + segs = parse_markdown(sample) + for seg, attr in segs: + # Ukázkové vypsání v terminálu (bez curses) pro debug + print(f"{repr(seg)} attr={attr}") diff --git a/ui.py b/ui.py index 73629fd..9cf72f5 100644 --- a/ui.py +++ b/ui.py @@ -2,6 +2,8 @@ Module: ui.py ------------- Obsahuje interaktivní rozhraní Robovojtíka založené na knihovně curses. +Implementuje barevné oddělení, trvalou hlavičku, historii chatu s formátováním Markdown, +spinner pro indikaci čekání a vstupní oblast. """ import curses @@ -19,14 +21,15 @@ logger = logging.getLogger("robovojtik.ui") automode = False def main_curses(stdscr): + # Inicializace barev curses.start_color() curses.use_default_colors() curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE) # Hlavička - curses.init_pair(2, curses.COLOR_WHITE, -1) # Chat - curses.init_pair(3, curses.COLOR_GREEN, -1) # Výstup - curses.init_pair(4, curses.COLOR_YELLOW, -1) # Vstup - curses.init_pair(5, curses.COLOR_CYAN, -1) # Spinner - curses.init_pair(6, curses.COLOR_BLACK, curses.COLOR_WHITE) # Oddělovač + curses.init_pair(2, curses.COLOR_WHITE, -1) # Chat + curses.init_pair(3, curses.COLOR_GREEN, -1) # Výstup + curses.init_pair(4, curses.COLOR_YELLOW, -1) # Vstup + curses.init_pair(5, curses.COLOR_CYAN, -1) # Spinner + curses.init_pair(6, curses.COLOR_BLACK, curses.COLOR_WHITE) # Oddělovač curses.curs_set(1) stdscr.nodelay(False) @@ -36,7 +39,7 @@ def main_curses(stdscr): header_height = 3 prompt_height = 2 left_width = width // 2 - right_width = width - left_width - 1 # oddělovač = 1 sloupec + right_width = width - left_width - 1 # oddělovač chat_height = height - header_height - prompt_height header_win = curses.newwin(header_height, left_width, 0, 0) @@ -83,16 +86,20 @@ def main_curses(stdscr): def spinner_func(ch): mid_x = left_width // 2 - 5 - header_win.addnstr(1, mid_x, f"Čekám... {ch}", left_width - mid_x - 1, curses.color_pair(5) | curses.A_BOLD) - header_win.refresh() + # Vypíšeme spinner do hlavičky + try: + header_win.addnstr(1, mid_x, f"Čekám... {ch}", left_width - mid_x - 1, curses.color_pair(5) | curses.A_BOLD) + header_win.refresh() + except curses.error: + pass chat_history = [] + def add_to_chat(text): - # Použijeme markdown_parser pro formátování + # Nové volání parseru parse_markdown z markdown_parser segments = markdown_parser.parse_markdown(text) - for seg_text, attr in segments: - chat_win.addstr(seg_text, attr) - chat_win.addstr("\n") + for seg_text, seg_attr in segments: + chat_win.addstr(seg_text, seg_attr) chat_win.refresh() add_to_chat("Historie chatu:") @@ -163,13 +170,14 @@ def main_curses(stdscr): add_to_chat("Robovojtík odpovídá:\n" + response) continue + # Ostatní dotazy assistant_response = api_interface.volani_asistenta(user_input, spinner_func=spinner_func) add_to_chat("Robovojtík odpovídá:\n" + assistant_response) if assistant_response.strip().lower().startswith("navrhovaný příkaz:"): lines = assistant_response.splitlines() proposal_line = lines[0] - navrhovany_prikaz = proposal_line[len("Navrhovaný příkaz:"):].strip() + navrhovany_prikaz = proposal_line[len("navrhovaný příkaz:"):].strip() add_to_chat(f"Navrhovaný příkaz: {navrhovany_prikaz}") if not automode: prompt_win.erase()