feat UI full redesign
This commit is contained in:
1 parent
ce8e59c8eb
commit
72c06ed465
1 file changed
+323
-37
@@ -6,15 +6,24 @@ from pygments import highlight
|
|||||||
from pygments.formatters import HtmlFormatter
|
from pygments.formatters import HtmlFormatter
|
||||||
from pygments.lexers import TextLexer, get_lexer_by_name, guess_lexer
|
from pygments.lexers import TextLexer, get_lexer_by_name, guess_lexer
|
||||||
from pygments.util import ClassNotFound
|
from pygments.util import ClassNotFound
|
||||||
from PyQt6.QtCore import QThread, QTimer, pyqtSignal
|
from PyQt6.QtCore import Qt, QThread, QTimer, pyqtSignal
|
||||||
from PyQt6.QtGui import (
|
from PyQt6.QtGui import (
|
||||||
QColor,
|
QColor,
|
||||||
QFont,
|
QFont,
|
||||||
|
QIcon,
|
||||||
|
QLinearGradient,
|
||||||
|
QPainter,
|
||||||
|
QPainterPath,
|
||||||
|
QPalette,
|
||||||
|
QPixmap,
|
||||||
QTextBlockFormat,
|
QTextBlockFormat,
|
||||||
QTextCharFormat,
|
QTextCharFormat,
|
||||||
QTextCursor,
|
QTextCursor,
|
||||||
QTextDocument,
|
QTextDocument,
|
||||||
QTextDocumentFragment,
|
QTextDocumentFragment,
|
||||||
|
QTextLength,
|
||||||
|
QTextTableCellFormat,
|
||||||
|
QTextTableFormat,
|
||||||
)
|
)
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QApplication,
|
QApplication,
|
||||||
@@ -33,6 +42,198 @@ import chatbot
|
|||||||
# How often the bottom-right GPU/VRAM readout refreshes.
|
# How often the bottom-right GPU/VRAM readout refreshes.
|
||||||
GPU_STATUS_INTERVAL_MS = 1000
|
GPU_STATUS_INTERVAL_MS = 1000
|
||||||
|
|
||||||
|
# --- Visual theme ------------------------------------------------------
|
||||||
|
# An explicit dark palette rather than relying on the OS theme, so the app
|
||||||
|
# looks the same everywhere instead of "Qt follows Windows' dark app theme".
|
||||||
|
BG_APP = "#0b0d12"
|
||||||
|
BG_PANEL = "#12151c"
|
||||||
|
BG_ELEVATED = "#181c25"
|
||||||
|
BORDER = "#262b36"
|
||||||
|
BORDER_STRONG = "#333949"
|
||||||
|
TEXT_PRIMARY = "#e6e8ee"
|
||||||
|
TEXT_SECONDARY = "#8b92a5"
|
||||||
|
ACCENT = "#6366f1"
|
||||||
|
ACCENT_HOVER = "#7b7ef4"
|
||||||
|
ACCENT_PRESSED = "#4f52d6"
|
||||||
|
|
||||||
|
# (accent, background) per chat role, used for the accent stripe and tinted
|
||||||
|
# card background of each message.
|
||||||
|
ROLE_STYLES = {
|
||||||
|
"user": ("#60a5fa", "#141b2b"),
|
||||||
|
"assistant": ("#34d399", "#0f211c"),
|
||||||
|
"system": ("#9aa1b4", "#161922"),
|
||||||
|
}
|
||||||
|
|
||||||
|
_STYLESHEET = f"""
|
||||||
|
QMainWindow {{
|
||||||
|
background-color: {BG_APP};
|
||||||
|
}}
|
||||||
|
|
||||||
|
QWidget {{
|
||||||
|
color: {TEXT_PRIMARY};
|
||||||
|
font-family: "Segoe UI Variable Text", "Segoe UI", sans-serif;
|
||||||
|
font-size: 10.5pt;
|
||||||
|
}}
|
||||||
|
|
||||||
|
#header {{
|
||||||
|
border-bottom: 1px solid {BORDER};
|
||||||
|
}}
|
||||||
|
|
||||||
|
#titleLabel {{
|
||||||
|
font-size: 15pt;
|
||||||
|
font-weight: 600;
|
||||||
|
color: {TEXT_PRIMARY};
|
||||||
|
}}
|
||||||
|
|
||||||
|
#subtitleLabel {{
|
||||||
|
font-size: 9pt;
|
||||||
|
color: {TEXT_SECONDARY};
|
||||||
|
}}
|
||||||
|
|
||||||
|
#badge {{
|
||||||
|
background-color: {BG_ELEVATED};
|
||||||
|
border: 1px solid {BORDER};
|
||||||
|
border-radius: 11px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
color: {TEXT_SECONDARY};
|
||||||
|
font-size: 9pt;
|
||||||
|
}}
|
||||||
|
|
||||||
|
QTextBrowser#chatView {{
|
||||||
|
background-color: {BG_PANEL};
|
||||||
|
border: 1px solid {BORDER};
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 10px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
QLineEdit#messageInput {{
|
||||||
|
background-color: {BG_ELEVATED};
|
||||||
|
border: 1px solid {BORDER};
|
||||||
|
border-radius: 19px;
|
||||||
|
padding: 0 16px;
|
||||||
|
color: {TEXT_PRIMARY};
|
||||||
|
selection-background-color: {ACCENT};
|
||||||
|
}}
|
||||||
|
|
||||||
|
QLineEdit#messageInput:focus {{
|
||||||
|
border: 1px solid {ACCENT};
|
||||||
|
}}
|
||||||
|
|
||||||
|
QLineEdit#messageInput:disabled {{
|
||||||
|
color: {TEXT_SECONDARY};
|
||||||
|
}}
|
||||||
|
|
||||||
|
QPushButton#sendButton {{
|
||||||
|
background-color: {ACCENT};
|
||||||
|
color: #ffffff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 19px;
|
||||||
|
padding: 0 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
}}
|
||||||
|
|
||||||
|
QPushButton#sendButton:hover {{
|
||||||
|
background-color: {ACCENT_HOVER};
|
||||||
|
}}
|
||||||
|
|
||||||
|
QPushButton#sendButton:pressed {{
|
||||||
|
background-color: {ACCENT_PRESSED};
|
||||||
|
}}
|
||||||
|
|
||||||
|
QPushButton#sendButton:disabled {{
|
||||||
|
background-color: {BG_ELEVATED};
|
||||||
|
color: {TEXT_SECONDARY};
|
||||||
|
border: 1px solid {BORDER};
|
||||||
|
}}
|
||||||
|
|
||||||
|
QStatusBar {{
|
||||||
|
background-color: {BG_APP};
|
||||||
|
border-top: 1px solid {BORDER};
|
||||||
|
color: {TEXT_SECONDARY};
|
||||||
|
font-size: 9pt;
|
||||||
|
}}
|
||||||
|
|
||||||
|
QStatusBar::item {{
|
||||||
|
border: none;
|
||||||
|
}}
|
||||||
|
|
||||||
|
QScrollBar:vertical {{
|
||||||
|
background: transparent;
|
||||||
|
width: 10px;
|
||||||
|
margin: 4px 2px 4px 0;
|
||||||
|
}}
|
||||||
|
|
||||||
|
QScrollBar::handle:vertical {{
|
||||||
|
background: {BORDER_STRONG};
|
||||||
|
border-radius: 5px;
|
||||||
|
min-height: 24px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
QScrollBar::handle:vertical:hover {{
|
||||||
|
background: {TEXT_SECONDARY};
|
||||||
|
}}
|
||||||
|
|
||||||
|
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{
|
||||||
|
height: 0;
|
||||||
|
}}
|
||||||
|
|
||||||
|
QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {{
|
||||||
|
background: transparent;
|
||||||
|
}}
|
||||||
|
|
||||||
|
QToolTip {{
|
||||||
|
background-color: {BG_ELEVATED};
|
||||||
|
color: {TEXT_PRIMARY};
|
||||||
|
border: 1px solid {BORDER};
|
||||||
|
padding: 4px 8px;
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _dark_palette() -> QPalette:
|
||||||
|
"""Baseline QPalette for the Fusion style, so anything the stylesheet
|
||||||
|
doesn't reach (native dialogs, menus) still matches the dark theme."""
|
||||||
|
palette = QPalette()
|
||||||
|
palette.setColor(QPalette.ColorRole.Window, QColor(BG_APP))
|
||||||
|
palette.setColor(QPalette.ColorRole.WindowText, QColor(TEXT_PRIMARY))
|
||||||
|
palette.setColor(QPalette.ColorRole.Base, QColor(BG_ELEVATED))
|
||||||
|
palette.setColor(QPalette.ColorRole.AlternateBase, QColor(BG_PANEL))
|
||||||
|
palette.setColor(QPalette.ColorRole.ToolTipBase, QColor(BG_ELEVATED))
|
||||||
|
palette.setColor(QPalette.ColorRole.ToolTipText, QColor(TEXT_PRIMARY))
|
||||||
|
palette.setColor(QPalette.ColorRole.Text, QColor(TEXT_PRIMARY))
|
||||||
|
palette.setColor(QPalette.ColorRole.PlaceholderText, QColor(TEXT_SECONDARY))
|
||||||
|
palette.setColor(QPalette.ColorRole.Button, QColor(BG_ELEVATED))
|
||||||
|
palette.setColor(QPalette.ColorRole.ButtonText, QColor(TEXT_PRIMARY))
|
||||||
|
palette.setColor(QPalette.ColorRole.Highlight, QColor(ACCENT))
|
||||||
|
palette.setColor(QPalette.ColorRole.HighlightedText, QColor("#ffffff"))
|
||||||
|
return palette
|
||||||
|
|
||||||
|
|
||||||
|
def _make_app_icon() -> QIcon:
|
||||||
|
"""Small generated glyph for the window/taskbar icon, since the project
|
||||||
|
ships no icon asset."""
|
||||||
|
size = 64
|
||||||
|
pixmap = QPixmap(size, size)
|
||||||
|
pixmap.fill(QColor(0, 0, 0, 0))
|
||||||
|
|
||||||
|
painter = QPainter(pixmap)
|
||||||
|
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||||
|
|
||||||
|
gradient = QLinearGradient(0, 0, size, size)
|
||||||
|
gradient.setColorAt(0.0, QColor(ACCENT_HOVER))
|
||||||
|
gradient.setColorAt(1.0, QColor(ACCENT_PRESSED))
|
||||||
|
|
||||||
|
path = QPainterPath()
|
||||||
|
path.addRoundedRect(2, 2, size - 4, size - 4, 16, 16)
|
||||||
|
painter.fillPath(path, gradient)
|
||||||
|
|
||||||
|
painter.setPen(QColor("#ffffff"))
|
||||||
|
painter.setFont(QFont("Segoe UI", 28, QFont.Weight.Bold))
|
||||||
|
painter.drawText(pixmap.rect(), Qt.AlignmentFlag.AlignCenter, "D")
|
||||||
|
painter.end()
|
||||||
|
|
||||||
|
return QIcon(pixmap)
|
||||||
|
|
||||||
# GitHub dialect adds fenced code blocks with language tags, tables, and
|
# GitHub dialect adds fenced code blocks with language tags, tables, and
|
||||||
# strikethrough on top of CommonMark; NoHTML stops raw HTML in the model's
|
# strikethrough on top of CommonMark; NoHTML stops raw HTML in the model's
|
||||||
# output from being interpreted as literal markup in the chat view.
|
# output from being interpreted as literal markup in the chat view.
|
||||||
@@ -103,6 +304,48 @@ def _reset_cursor_format(cursor: QTextCursor) -> None:
|
|||||||
cursor.insertBlock(QTextBlockFormat(), QTextCharFormat())
|
cursor.insertBlock(QTextBlockFormat(), QTextCharFormat())
|
||||||
|
|
||||||
|
|
||||||
|
def _insert_message_card(cursor: QTextCursor, accent: str, bg: str) -> QTextCursor:
|
||||||
|
"""Insert a flat card with a colored accent stripe for one chat message,
|
||||||
|
and return a cursor positioned inside it, ready for content.
|
||||||
|
|
||||||
|
QTextBrowser's HTML subset has no border-radius, so a rounded chat bubble
|
||||||
|
isn't achievable directly; a two-column table (a thin accent-colored
|
||||||
|
strip beside a tinted message body) stands in for one. Each message gets
|
||||||
|
its own table/cell, so - unlike the previous shared-cursor approach -
|
||||||
|
there's no risk of one message's formatting bleeding into the next.
|
||||||
|
"""
|
||||||
|
table_format = QTextTableFormat()
|
||||||
|
table_format.setCellPadding(0)
|
||||||
|
table_format.setCellSpacing(0)
|
||||||
|
table_format.setBorder(0)
|
||||||
|
table_format.setWidth(QTextLength(QTextLength.Type.PercentageLength, 100))
|
||||||
|
table_format.setColumnWidthConstraints(
|
||||||
|
[
|
||||||
|
QTextLength(QTextLength.Type.FixedLength, 3),
|
||||||
|
QTextLength(QTextLength.Type.PercentageLength, 100),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
table_format.setTopMargin(2)
|
||||||
|
table_format.setBottomMargin(10)
|
||||||
|
|
||||||
|
table = cursor.insertTable(1, 2, table_format)
|
||||||
|
|
||||||
|
strip_format = QTextTableCellFormat()
|
||||||
|
strip_format.setBackground(QColor(accent))
|
||||||
|
table.cellAt(0, 0).setFormat(strip_format)
|
||||||
|
|
||||||
|
body_format = QTextTableCellFormat()
|
||||||
|
body_format.setBackground(QColor(bg))
|
||||||
|
body_format.setTopPadding(10)
|
||||||
|
body_format.setBottomPadding(10)
|
||||||
|
body_format.setLeftPadding(14)
|
||||||
|
body_format.setRightPadding(14)
|
||||||
|
body_cell = table.cellAt(0, 1)
|
||||||
|
body_cell.setFormat(body_format)
|
||||||
|
|
||||||
|
return body_cell.firstCursorPosition()
|
||||||
|
|
||||||
|
|
||||||
class ModelLoaderThread(QThread):
|
class ModelLoaderThread(QThread):
|
||||||
loaded = pyqtSignal(object)
|
loaded = pyqtSignal(object)
|
||||||
failed = pyqtSignal(str)
|
failed = pyqtSignal(str)
|
||||||
@@ -155,30 +398,66 @@ class ChatWindow(QMainWindow):
|
|||||||
central = QWidget()
|
central = QWidget()
|
||||||
self.setCentralWidget(central)
|
self.setCentralWidget(central)
|
||||||
layout = QVBoxLayout(central)
|
layout = QVBoxLayout(central)
|
||||||
|
layout.setContentsMargins(20, 16, 20, 16)
|
||||||
|
layout.setSpacing(14)
|
||||||
|
|
||||||
|
header = QWidget()
|
||||||
|
header.setObjectName("header")
|
||||||
|
header_layout = QHBoxLayout(header)
|
||||||
|
header_layout.setContentsMargins(4, 0, 4, 12)
|
||||||
|
header_layout.setSpacing(12)
|
||||||
|
|
||||||
|
title_box = QVBoxLayout()
|
||||||
|
title_box.setSpacing(0)
|
||||||
|
title_label = QLabel("Dev Assist")
|
||||||
|
title_label.setObjectName("titleLabel")
|
||||||
|
subtitle_label = QLabel("Local LLM · tool-augmented")
|
||||||
|
subtitle_label.setObjectName("subtitleLabel")
|
||||||
|
title_box.addWidget(title_label)
|
||||||
|
title_box.addWidget(subtitle_label)
|
||||||
|
header_layout.addLayout(title_box)
|
||||||
|
header_layout.addStretch(1)
|
||||||
|
|
||||||
|
self._n_ctx = 0
|
||||||
|
self.context_status_label = QLabel("Context: --")
|
||||||
|
self.context_status_label.setObjectName("badge")
|
||||||
|
header_layout.addWidget(self.context_status_label)
|
||||||
|
|
||||||
|
self.gpu_status_label = QLabel("GPU: --")
|
||||||
|
self.gpu_status_label.setObjectName("badge")
|
||||||
|
header_layout.addWidget(self.gpu_status_label)
|
||||||
|
|
||||||
|
layout.addWidget(header)
|
||||||
|
|
||||||
self.chat_view = QTextBrowser()
|
self.chat_view = QTextBrowser()
|
||||||
|
self.chat_view.setObjectName("chatView")
|
||||||
self.chat_view.setOpenExternalLinks(True)
|
self.chat_view.setOpenExternalLinks(True)
|
||||||
self.chat_view.setFont(QFont("Segoe UI", 10))
|
self.chat_view.setFont(QFont("Segoe UI Variable Text", 10))
|
||||||
layout.addWidget(self.chat_view)
|
layout.addWidget(self.chat_view)
|
||||||
|
|
||||||
input_row = QHBoxLayout()
|
input_row = QHBoxLayout()
|
||||||
|
input_row.setSpacing(10)
|
||||||
|
|
||||||
self.input_box = QLineEdit()
|
self.input_box = QLineEdit()
|
||||||
|
self.input_box.setObjectName("messageInput")
|
||||||
self.input_box.setPlaceholderText("Ask a development question...")
|
self.input_box.setPlaceholderText("Ask a development question...")
|
||||||
|
self.input_box.setMinimumHeight(40)
|
||||||
|
input_palette = self.input_box.palette()
|
||||||
|
input_palette.setColor(QPalette.ColorRole.PlaceholderText, QColor(TEXT_SECONDARY))
|
||||||
|
self.input_box.setPalette(input_palette)
|
||||||
self.input_box.returnPressed.connect(self._on_send)
|
self.input_box.returnPressed.connect(self._on_send)
|
||||||
input_row.addWidget(self.input_box)
|
input_row.addWidget(self.input_box)
|
||||||
|
|
||||||
self.send_button = QPushButton("Send")
|
self.send_button = QPushButton("Send →")
|
||||||
|
self.send_button.setObjectName("sendButton")
|
||||||
|
self.send_button.setMinimumHeight(40)
|
||||||
|
self.send_button.setMinimumWidth(96)
|
||||||
|
self.send_button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||||
self.send_button.clicked.connect(self._on_send)
|
self.send_button.clicked.connect(self._on_send)
|
||||||
input_row.addWidget(self.send_button)
|
input_row.addWidget(self.send_button)
|
||||||
|
|
||||||
layout.addLayout(input_row)
|
layout.addLayout(input_row)
|
||||||
|
|
||||||
self._n_ctx = 0
|
|
||||||
self.context_status_label = QLabel("Context: --")
|
|
||||||
self.statusBar().addPermanentWidget(self.context_status_label)
|
|
||||||
|
|
||||||
self.gpu_status_label = QLabel()
|
|
||||||
self.statusBar().addPermanentWidget(self.gpu_status_label)
|
|
||||||
self._init_gpu_monitor()
|
self._init_gpu_monitor()
|
||||||
|
|
||||||
self.statusBar().showMessage("Loading model...")
|
self.statusBar().showMessage("Loading model...")
|
||||||
@@ -281,66 +560,69 @@ class ChatWindow(QMainWindow):
|
|||||||
self._set_input_enabled(True)
|
self._set_input_enabled(True)
|
||||||
|
|
||||||
def _append_user(self, text: str) -> None:
|
def _append_user(self, text: str) -> None:
|
||||||
self._append_block("You", text, "#2b6cb0")
|
accent, bg = ROLE_STYLES["user"]
|
||||||
|
self._append_block("You", text, accent, bg)
|
||||||
|
|
||||||
def _append_assistant(self, text: str) -> None:
|
def _append_assistant(self, text: str) -> None:
|
||||||
self._append_markdown_block("Assistant", text, "#2f855a")
|
accent, bg = ROLE_STYLES["assistant"]
|
||||||
|
self._append_markdown_block("Assistant", text, accent, bg)
|
||||||
|
|
||||||
def _append_system(self, text: str) -> None:
|
def _append_system(self, text: str) -> None:
|
||||||
self._append_block("System", text, "#718096")
|
accent, bg = ROLE_STYLES["system"]
|
||||||
|
self._append_block("System", text, accent, bg)
|
||||||
|
|
||||||
def _append_block(self, label: str, text: str, color: str) -> None:
|
def _append_block(self, label: str, text: str, accent: str, bg: str) -> None:
|
||||||
escaped = (
|
escaped = (
|
||||||
text.replace("&", "&")
|
text.replace("&", "&")
|
||||||
.replace("<", "<")
|
.replace("<", "<")
|
||||||
.replace(">", ">")
|
.replace(">", ">")
|
||||||
.replace("\n", "<br>")
|
.replace("\n", "<br>")
|
||||||
)
|
)
|
||||||
self.chat_view.append(f'<p><b style="color:{color};">{label}:</b><br>{escaped}</p>')
|
|
||||||
self.chat_view.moveCursor(QTextCursor.MoveOperation.End)
|
|
||||||
|
|
||||||
def _append_markdown_block(self, label: str, markdown_text: str, color: str) -> None:
|
|
||||||
cursor = self.chat_view.textCursor()
|
cursor = self.chat_view.textCursor()
|
||||||
cursor.movePosition(QTextCursor.MoveOperation.End)
|
cursor.movePosition(QTextCursor.MoveOperation.End)
|
||||||
# Force a fresh, format-reset paragraph before the label - moving to
|
body = _insert_message_card(cursor, accent, bg)
|
||||||
# End only puts the cursor after the previous message's last
|
body.insertHtml(
|
||||||
# character (inheriting its block format, e.g. a Pygments code
|
f'<span style="color:{accent}; font-weight:600; font-size:8.5pt;">{label.upper()}</span>'
|
||||||
# block's background color), not on a clean new line.
|
f'<br><span style="color:{TEXT_PRIMARY};">{escaped}</span>'
|
||||||
if not self.chat_view.document().isEmpty():
|
)
|
||||||
_reset_cursor_format(cursor)
|
self.chat_view.moveCursor(QTextCursor.MoveOperation.End)
|
||||||
cursor.insertHtml(f'<b style="color:{color};">{label}:</b>')
|
|
||||||
|
def _append_markdown_block(self, label: str, markdown_text: str, accent: str, bg: str) -> None:
|
||||||
|
cursor = self.chat_view.textCursor()
|
||||||
|
cursor.movePosition(QTextCursor.MoveOperation.End)
|
||||||
|
# Each message gets its own table cell (see _insert_message_card), so
|
||||||
|
# unlike the previous shared-cursor version, there's no need to reset
|
||||||
|
# formatting before the label - a fresh cell starts with plain format.
|
||||||
|
body = _insert_message_card(cursor, accent, bg)
|
||||||
|
body.insertHtml(f'<span style="color:{accent}; font-weight:600; font-size:8.5pt;">{label.upper()}</span>')
|
||||||
|
|
||||||
pos = 0
|
pos = 0
|
||||||
for match in _CODE_FENCE_RE.finditer(markdown_text):
|
for match in _CODE_FENCE_RE.finditer(markdown_text):
|
||||||
before = markdown_text[pos : match.start()]
|
before = markdown_text[pos : match.start()]
|
||||||
if before.strip():
|
if before.strip():
|
||||||
_reset_cursor_format(cursor)
|
_reset_cursor_format(body)
|
||||||
fragment = QTextDocumentFragment.fromMarkdown(before, _MARKDOWN_FEATURES)
|
fragment = QTextDocumentFragment.fromMarkdown(before, _MARKDOWN_FEATURES)
|
||||||
cursor.insertFragment(fragment)
|
body.insertFragment(fragment)
|
||||||
lang, code = match.group(1), match.group(2)
|
lang, code = match.group(1), match.group(2)
|
||||||
for line in _highlight_code_lines(code, lang):
|
for line in _highlight_code_lines(code, lang):
|
||||||
cursor.insertBlock(_CODE_BLOCK_FORMAT)
|
body.insertBlock(_CODE_BLOCK_FORMAT)
|
||||||
start = cursor.position()
|
start = body.position()
|
||||||
cursor.insertHtml(line)
|
body.insertHtml(line)
|
||||||
# insertHtml() builds its own char formats from the parsed
|
# insertHtml() builds its own char formats from the parsed
|
||||||
# HTML and ignores the cursor's format, so the monospace
|
# HTML and ignores the cursor's format, so the monospace
|
||||||
# font has to be merged on afterwards; merging (rather than
|
# font has to be merged on afterwards; merging (rather than
|
||||||
# replacing) keeps Pygments' per-token colors and bold.
|
# replacing) keeps Pygments' per-token colors and bold.
|
||||||
line_cursor = QTextCursor(cursor)
|
line_cursor = QTextCursor(body)
|
||||||
line_cursor.setPosition(start)
|
line_cursor.setPosition(start)
|
||||||
line_cursor.setPosition(cursor.position(), QTextCursor.MoveMode.KeepAnchor)
|
line_cursor.setPosition(body.position(), QTextCursor.MoveMode.KeepAnchor)
|
||||||
line_cursor.mergeCharFormat(_CODE_CHAR_FORMAT)
|
line_cursor.mergeCharFormat(_CODE_CHAR_FORMAT)
|
||||||
pos = match.end()
|
pos = match.end()
|
||||||
remainder = markdown_text[pos:]
|
remainder = markdown_text[pos:]
|
||||||
if remainder.strip():
|
if remainder.strip():
|
||||||
_reset_cursor_format(cursor)
|
_reset_cursor_format(body)
|
||||||
fragment = QTextDocumentFragment.fromMarkdown(remainder, _MARKDOWN_FEATURES)
|
fragment = QTextDocumentFragment.fromMarkdown(remainder, _MARKDOWN_FEATURES)
|
||||||
cursor.insertFragment(fragment)
|
body.insertFragment(fragment)
|
||||||
|
|
||||||
# Reset the block format one more time so a code block's background/
|
|
||||||
# monospace font doesn't bleed into whatever message gets appended next.
|
|
||||||
_reset_cursor_format(cursor)
|
|
||||||
self.chat_view.setTextCursor(cursor)
|
|
||||||
self.chat_view.moveCursor(QTextCursor.MoveOperation.End)
|
self.chat_view.moveCursor(QTextCursor.MoveOperation.End)
|
||||||
|
|
||||||
def closeEvent(self, event) -> None:
|
def closeEvent(self, event) -> None:
|
||||||
@@ -357,6 +639,10 @@ class ChatWindow(QMainWindow):
|
|||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
|
app.setStyle("Fusion")
|
||||||
|
app.setPalette(_dark_palette())
|
||||||
|
app.setStyleSheet(_STYLESHEET)
|
||||||
|
app.setWindowIcon(_make_app_icon())
|
||||||
window = ChatWindow()
|
window = ChatWindow()
|
||||||
window.show()
|
window.show()
|
||||||
sys.exit(app.exec())
|
sys.exit(app.exec())
|
||||||
|
|||||||
Reference in new issue
Block a user