From f06c2dae236d5b9232de702e65a7391470c2545f Mon Sep 17 00:00:00 2001 From: Nokse22 <44558032+Nokse22@users.noreply.github.com> Date: Thu, 1 Aug 2024 21:44:56 +0200 Subject: [PATCH] Added tables (#179) --- src/meson.build | 3 +- src/table_widget.py | 126 ++++++++++++++++++++++++++++++++++++++++++++ src/window.py | 17 +++++- 3 files changed, 143 insertions(+), 3 deletions(-) create mode 100644 src/table_widget.py diff --git a/src/meson.build b/src/meson.build index 28fe075..1781b7a 100644 --- a/src/meson.build +++ b/src/meson.build @@ -43,7 +43,8 @@ alpaca_sources = [ 'dialogs.py', 'local_instance.py', 'available_models.json', - 'available_models_descriptions.py' + 'available_models_descriptions.py', + 'table_widget.py' ] install_data(alpaca_sources, install_dir: moduledir) diff --git a/src/table_widget.py b/src/table_widget.py new file mode 100644 index 0000000..20da25a --- /dev/null +++ b/src/table_widget.py @@ -0,0 +1,126 @@ +import gi +from gi.repository import Adw +from gi.repository import Gtk, GObject, Gio + +import re + +class MarkdownTable: + def __init__(self): + self.headers = [] + self.rows = Gio.ListStore() + self.alignments = [] + + def __repr__(self): + table_repr = 'Headers: {}\n'.format(self.headers) + table_repr += 'Alignments: {}\n'.format(self.alignments) + table_repr += 'Rows:\n' + for row in self.rows: + table_repr += ' | '.join(row) + '\n' + return table_repr + +class Row(GObject.GObject): + def __init__(self, _values): + super().__init__() + + self.values = _values + + def get_column_value(self, index): + return self.values[index] + +class TableWidget(Gtk.Frame): + __gtype_name__ = 'TableWidget' + + def __init__(self, markdown): + super().__init__() + + self.table = MarkdownTable() + + self.set_halign(Gtk.Align.START) + + self.table_widget = Gtk.ColumnView( + show_column_separators=True, + show_row_separators=True, + reorderable=False, + ) + scrolled_window = Gtk.ScrolledWindow( + vscrollbar_policy=Gtk.PolicyType.NEVER, + propagate_natural_width=True + ) + self.set_child(scrolled_window) + + try: + self.parse_markdown_table(markdown) + self.make_table() + scrolled_window.set_child(self.table_widget) + except: + label = Gtk.Label( + label=markdown.lstrip('\n').rstrip('\n'), + selectable=True, + margin_top=6, + margin_bottom=6, + margin_start=6, + margin_end=6 + ) + scrolled_window.set_child(label) + + def parse_markdown_table(self, markdown_text): + # Define regex patterns for matching the table components + header_pattern = r'^\|(.+?)\|$' + separator_pattern = r'^\|(\s*[:-]+:?\s*\|)+$' + row_pattern = r'^\|(.+?)\|$' + + # Split the text into lines + lines = markdown_text.strip().split('\n') + + # Extract headers + header_match = re.match(header_pattern, lines[0], re.MULTILINE) + if header_match: + headers = [header.strip() for header in header_match.group(1).replace("*", "").split('|') if header.strip()] + self.table.headers = headers + + # Extract alignments + separator_match = re.match(separator_pattern, lines[1], re.MULTILINE) + if separator_match: + alignments = [] + separator_columns = lines[1].replace(" ", "").split('|')[1:-1] + for sep in separator_columns: + if ':' in sep: + if sep.startswith('-') and sep.endswith(':'): + alignments.append(1) + elif sep.startswith(':') and sep.endswith('-'): + alignments.append(0) + else: + alignments.append(0.5) + else: + alignments.append(0) # Default alignment is start + self.table.alignments = alignments + + # Extract rows + for line in lines[2:]: + row_match = re.match(row_pattern, line, re.MULTILINE) + if row_match: + rows = line.split('|')[1:-1] + row = Row(rows) + self.table.rows.append(row) + + def make_table(self): + + def _on_factory_setup(_factory, list_item, align): + label = Gtk.Label(xalign=align, ellipsize=3, selectable=True) + list_item.set_child(label) + + def _on_factory_bind(_factory, list_item, index): + label_widget = list_item.get_child() + row = list_item.get_item() + label_widget.set_label(row.get_column_value(index)) + + for index, column_name in enumerate(self.table.headers): + column = Gtk.ColumnViewColumn(title=column_name, expand=True) + factory = Gtk.SignalListItemFactory() + factory.connect("setup", _on_factory_setup, self.table.alignments[index]) + factory.connect("bind", _on_factory_bind, index) + column.set_factory(factory) + self.table_widget.append_column(column) + + selection = Gtk.NoSelection.new(model=self.table.rows) + self.table_widget.set_model(model=selection) diff --git a/src/window.py b/src/window.py index 15bb50b..d843bdd 100644 --- a/src/window.py +++ b/src/window.py @@ -28,7 +28,7 @@ from PIL import Image from pypdf import PdfReader from datetime import datetime from . import dialogs, local_instance, connection_handler, available_models_descriptions - +from .table_widget import TableWidget logger = logging.getLogger(__name__) @@ -820,6 +820,16 @@ Generate a title following these rules: code_text = match.group(1) parts.append({"type": "code", "text": code_text, "language": None}) pos = end + # Match tables + table_pattern = re.compile(r'((\r?\n){2}|^)([^\r\n]*\|[^\r\n]*(\r?\n)?)+(?=(\r?\n){2}|$)', re.MULTILINE) + for match in table_pattern.finditer(text): + start, end = match.span() + if pos < start: + normal_text = text[pos:start] + parts.append({"type": "normal", "text": normal_text.strip()}) + table_text = match.group(0) + parts.append({"type": "table", "text": table_text}) + pos = end # Extract any remaining normal text after the last code block if pos < len(text): normal_text = text[pos:] @@ -869,7 +879,7 @@ Generate a title following these rules: if footer: message_buffer.insert_markup(message_buffer.get_end_iter(), footer, len(footer.encode('utf-8'))) self.bot_message_box.append(message_text) - else: + elif part['type'] == 'code': language = None if part['language']: language = GtkSource.LanguageManager.get_default().get_language(part['language']) @@ -899,6 +909,9 @@ Generate a title following these rules: code_block_box.append(source_view) self.bot_message_box.append(code_block_box) self.style_manager.connect("notify::dark", self.on_theme_changed, buffer) + elif part['type'] == 'table': + table = TableWidget(part['text']) + self.bot_message_box.append(table) vadjustment = self.chat_window.get_vadjustment() vadjustment.set_value(vadjustment.get_upper()) self.bot_message = None