diff --git a/bigtext.py b/bigtext.py
index 0c7d5fa..5c75fa9 100644
--- a/bigtext.py
+++ b/bigtext.py
@@ -228,15 +228,19 @@ class InnerBigText(QWidget):
if self.selection_highlight.start_byte != self.selection_highlight.end_byte:
start = min(self.selection_highlight.start_byte, self.selection_highlight.end_byte)
end = max(self.selection_highlight.start_byte, self.selection_highlight.end_byte)
+ bytes_human_readable = humanbytes(end - start)
if end - start > (1024 ** 2) * 5:
you_sure = QMessageBox(
QMessageBox.Icon.Warning,
- self.tr("copy to clipboard"),
+ self.tr("data selection"),
self.tr(
- "You are about to copy %s to the clipboard. Are you sure you want to do that?" % humanbytes(
- end - start)))
- you_sure.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
- you_sure.setDefaultButton(QMessageBox.StandardButton.No)
+ "You have selected {0} of data.").format(bytes_human_readable))
+ you_sure.setStandardButtons(QMessageBox.StandardButton.Cancel)
+ you_sure.addButton(QPushButton(self.tr("Copy {0} to Clipboard").format(bytes_human_readable)),
+ QMessageBox.ButtonRole.AcceptRole)
+ # TODO add save dialog
+ # you_sure.addButton(QPushButton(self.tr("Write to File")), QMessageBox.ButtonRole.YesRole)
+ you_sure.setDefaultButton(QMessageBox.StandardButton.Cancel)
result = you_sure.exec()
if result != QMessageBox.StandardButton.Yes:
# abort
@@ -267,7 +271,7 @@ class InnerBigText(QWidget):
for l in self.lines:
self.update_longest_line(len(l.line()))
- highlighters = self.model.highlights
+ highlighters = self.model.highlighters()
if self.model.get_query_highlight():
highlighters = highlighters + [self.model.get_query_highlight()]
highlighters = highlighters + [self.selection_highlight] # selection highlight should be last
diff --git a/colorbutton.py b/colorbutton.py
new file mode 100644
index 0000000..4b66692
--- /dev/null
+++ b/colorbutton.py
@@ -0,0 +1,47 @@
+import re
+
+from PyQt6.QtGui import QColor
+from PyQt6.QtWidgets import QWidget, QHBoxLayout, QPushButton, QColorDialog, QSizePolicy
+
+
+class ColorButton(QPushButton):
+ def __init__(self, color: str):
+ super(QPushButton, self).__init__()
+ self.color = color
+ self.setStyleSheet("background-color: #%s" % color)
+ self.pressed.connect(self._update_color)
+ self.setSizePolicy(QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed))
+
+ def set_color(self, color: str):
+ if self._is_hex_color(color):
+ self.setStyleSheet("background-color: #%s" % color)
+ self.setText(color)
+ self.color = color
+ else:
+ self.setStyleSheet("background-color: none")
+ self.setText(self.tr("not set"))
+ self.color = "None"
+
+ def _update_color(self):
+ new_color = QColorDialog.getColor(self._to_qcolor(self.color))
+ if new_color.isValid():
+ color = self._to_hex(new_color)
+ self.set_color(color)
+
+ @staticmethod
+ def _is_hex_color(color: str):
+ return re.match("[0-9a-f]{6}", color, flags=re.IGNORECASE)
+
+ def _to_qcolor(self, color: str):
+ if self._is_hex_color(color):
+ red = int(color[0:2], 16)
+ green = int(color[2:4], 16)
+ blue = int(color[4:6], 16)
+ return QColor(red, green, blue)
+ return QColor(255, 255, 255)
+
+ def _to_hex(self, color: QColor) -> str:
+ red = "{0:0{1}x}".format(color.red(), 2)
+ green = "{0:0{1}x}".format(color.green(), 2)
+ blue = "{0:0{1}x}".format(color.blue(), 2)
+ return red + green + blue
diff --git a/filterwidget.py b/filterwidget.py
index e3051fa..ff30d57 100644
--- a/filterwidget.py
+++ b/filterwidget.py
@@ -129,6 +129,7 @@ class FilterWidget(QWidget):
def filter_changed(self):
query = self.query_field.text()
ignore_case = self.ignore_case.isChecked()
+ is_regex = self.is_regex.isChecked()
if len(query) == 0:
self.reset_filter()
return
@@ -138,7 +139,7 @@ class FilterWidget(QWidget):
try:
flags = re.IGNORECASE if ignore_case else 0
- if self.is_regex.isChecked():
+ if is_regex:
regex = re.compile(query, flags=flags)
else:
regex = re.compile(re.escape(query), flags=flags)
@@ -147,8 +148,8 @@ class FilterWidget(QWidget):
self.filter_model.truncate()
return
- self.source_model.set_query_highlight(regex)
- self.filter_model.set_query_highlight(regex)
+ self.source_model.set_query_highlight(query, ignore_case, is_regex)
+ self.filter_model.set_query_highlight(query, ignore_case, is_regex)
self.filter_task = FilterTask(
self.source_model,
diff --git a/hbox.py b/hbox.py
new file mode 100644
index 0000000..17e91e6
--- /dev/null
+++ b/hbox.py
@@ -0,0 +1,9 @@
+from PyQt6.QtWidgets import QWidget, QHBoxLayout
+
+
+class HBox(QWidget):
+ def __init__(self, *widgets: QWidget):
+ super(HBox, self).__init__()
+ self.layout = QHBoxLayout(self)
+ for widget in widgets:
+ self.layout.addWidget(widget)
diff --git a/highlight_regex.py b/highlight_regex.py
index cc2e4dd..a609cce 100644
--- a/highlight_regex.py
+++ b/highlight_regex.py
@@ -14,28 +14,48 @@ import re
class HighlightRegex(Highlight):
- def __init__(self, regex: re.Pattern, brush: QBrush = QBrush(), pen: QPen = Qt.PenStyle.NoPen,
- brush_full_line=QBrush()):
- self.regex = regex
- self.brush = brush
- self.pen = pen
- self.brush_full_line = brush_full_line
+ def __init__(self, query: str, ignore_case: bool, is_regex: bool, hit_background_color: str = "None",
+ line_background_color: str = "None"):
+ self.query = query
+ self.ignore_case = ignore_case
+ self.is_regex = is_regex
+ self.regex = self._get_regex()
+ self.hit_background_color = hit_background_color
+ self.line_background_color = line_background_color
+
+ def _get_regex(self):
+ flags = re.IGNORECASE if self.ignore_case else 0
+ if self.is_regex:
+ return re.compile(self.query, flags=flags)
+ else:
+ return re.compile(re.escape(self.query), flags=flags)
def compute_highlight(self, line: Line) -> Optional[List[HighlightedRange]]:
result = []
- #print("execute regex: %s in %s" % (self.regex, line.line()))
+ # print("execute regex: %s in %s" % (self.regex, line.line()))
match_iter = re.finditer(self.regex, line.line())
for match in match_iter:
- #print("%s" % match)
+ # print("%s" % match)
start = match.start(0)
end = match.end(0)
result.append(HighlightedRange(
start,
- end-start,
+ end - start,
highlight_full_line=True,
- brush=self.brush,
- pen=self.pen,
- brush_full_line=self.brush_full_line
+ brush=self.brush(self.hit_background_color),
+ brush_full_line=self.brush(self.line_background_color)
))
- return result
\ No newline at end of file
+ return result
+
+ def hit_background_brush(self):
+ return self.brush(self.hit_background_color)
+
+ @staticmethod
+ def brush(color: str) -> QBrush:
+ if re.match("[0-9a-f]{6}", color, flags=re.IGNORECASE):
+ red = int(color[0:2], 16)
+ green = int(color[2:4], 16)
+ blue = int(color[4:6], 16)
+ return QBrush(QColor(red, green, blue))
+ return QBrush()
diff --git a/highlighting.py b/highlighting.py
index 02ad35c..2e2e9f3 100644
--- a/highlighting.py
+++ b/highlighting.py
@@ -1,5 +1,6 @@
import logging
import re
+import uuid
from PyQt6.QtGui import QBrush, QColor
@@ -9,11 +10,10 @@ from settings import Settings
log = logging.getLogger("highlighting")
-
class Highlighting:
@staticmethod
- def read_config(settings: Settings) -> [Highlight]:
+ def read_config(settings: Settings) -> [HighlightRegex]:
result = []
config = settings.config
@@ -26,32 +26,42 @@ class Highlighting:
continue
ignore_case = config.getboolean(section, "ignore-case", fallback=True)
is_regex = config.getboolean(section, "is-regex", fallback=False)
- line_background_color = Highlighting.brush(config.get(section, "line.background.color", fallback="None"))
- hit_background_color = Highlighting.brush(config.get(section, "hit.background.color", fallback="None"))
+ line_background_color = config.get(section, "line.background.color", fallback="None")
+ hit_background_color = config.get(section, "hit.background.color", fallback="None")
+
try:
- flags = re.IGNORECASE if ignore_case else 0
- if is_regex:
- regex = re.compile(query, flags=flags)
- else:
- regex = re.compile(re.escape(query), flags=flags)
+ highlight = HighlightRegex(
+ query=query,
+ ignore_case=ignore_case,
+ is_regex=is_regex,
+ hit_background_color=hit_background_color,
+ line_background_color=line_background_color
+ )
+ result.append(highlight)
except:
log.exception("failed to parse query for highlighter: %s" % section)
continue
- highlight = HighlightRegex(
- regex=regex,
- brush=hit_background_color,
- brush_full_line=line_background_color
- )
- result.append(highlight)
-
return result
@staticmethod
- def brush(color: str) -> QBrush:
- if re.match("[0-9a-f]{6}", color, flags=re.IGNORECASE):
- red = int(color[0:2], 16)
- green = int(color[2:4], 16)
- blue = int(color[4:6], 16)
- return QBrush(QColor(red, green, blue))
- return QBrush()
+ def write_config(settings: Settings, highlighters: [HighlightRegex]):
+ Highlighting.remove_highlighting_sections(settings)
+ section_counter = 0
+ for highlighter in highlighters:
+ highlighter: HighlightRegex = highlighter
+ section = "highlighting.%d" % section_counter
+ section_counter = section_counter + 1
+ settings.config.add_section(section)
+ settings.config.set(section, "query", highlighter.query)
+ settings.config.set(section, "ignore-case", str(highlighter.ignore_case))
+ settings.config.set(section, "is-regex", str(highlighter.is_regex))
+ settings.config.set(section, "line.background.color", highlighter.line_background_color)
+ settings.config.set(section, "hit.background.color", highlighter.hit_background_color)
+
+ @staticmethod
+ def remove_highlighting_sections(settings: Settings):
+ for section in settings.config.sections():
+ if not section.startswith("highlighting."):
+ continue
+ settings.config.remove_section(section)
diff --git a/highlightingdialog.py b/highlightingdialog.py
new file mode 100644
index 0000000..eb79afa
--- /dev/null
+++ b/highlightingdialog.py
@@ -0,0 +1,170 @@
+import re
+
+from PyQt6.QtWidgets import QDialog, QLineEdit, QLabel, QGridLayout, QCheckBox, QListWidget, QListWidgetItem, \
+ QPushButton, QDialogButtonBox, QMessageBox
+
+from colorbutton import ColorButton
+from hbox import HBox
+from highlight_regex import HighlightRegex
+from highlighting import Highlighting
+from settings import Settings
+
+
+class PayloadItem(QListWidgetItem):
+ def __init__(self, text: str, payload=None):
+ super(PayloadItem, self).__init__(text)
+ self.payload = payload
+
+
+class HighlightingDialog(QDialog):
+ def __init__(self, settings: Settings):
+ super(HighlightingDialog, self).__init__()
+ self.setWindowTitle(self.tr("Manage Highlighting"))
+ self.setModal(True)
+ self._settings = settings
+
+ form_grid = QGridLayout(self)
+ self.layout = form_grid
+
+ row = 0
+ self.list = QListWidget(self)
+ form_grid.addWidget(self.list, row, 0, 1, 2)
+
+ row = row + 1
+ self.btn_add = QPushButton(self.tr("Add"))
+ self.btn_add.pressed.connect(self._add)
+ self.btn_update = QPushButton(self.tr("Update"))
+ self.btn_update.pressed.connect(self._update)
+ self.btn_delete = QPushButton(self.tr("Delete"))
+ self.btn_delete.pressed.connect(self._delete)
+ self.btn_move_up = QPushButton(self.tr("Move Up"))
+ self.btn_move_up.pressed.connect(self._move_up)
+ self.btn_move_down = QPushButton(self.tr("Move Down"))
+ self.btn_move_down.pressed.connect(self._move_down)
+ form_grid.addWidget(HBox(self.btn_add, self.btn_update, self.btn_delete, self.btn_move_up, self.btn_move_down),
+ row, 0, 1, 2)
+
+ row = row + 1
+ self.query = QLineEdit(self)
+ form_grid.addWidget(QLabel(self.tr("Query:")), row, 0)
+ form_grid.addWidget(self.query, row, 1)
+
+ row = row + 1
+ self.ignore_case = QCheckBox(self.tr("Ignore Case"))
+ self.ignore_case.setChecked(True)
+ form_grid.addWidget(self.ignore_case, row, 0, 1, 2)
+
+ row = row + 1
+ self.is_regex = QCheckBox(self.tr("Regular Expression"))
+ self.is_regex.setChecked(True)
+ form_grid.addWidget(self.is_regex, row, 0, 1, 2)
+
+ row = row + 1
+ form_grid.addWidget(QLabel(self.tr("Hit Background:")), row, 0)
+ self.hit_background_color = ColorButton("ff00ff")
+ form_grid.addWidget(self.hit_background_color, row, 1)
+
+ row = row + 1
+ form_grid.addWidget(QLabel(self.tr("Line Background:")), row, 0)
+ self.line_background_color = ColorButton("ff00ff0d")
+ form_grid.addWidget(self.line_background_color, row, 1)
+
+ row = row + 1
+ self.buttons = QDialogButtonBox()
+ self.buttons.setStandardButtons(QDialogButtonBox.StandardButton.Cancel | QDialogButtonBox.StandardButton.Save)
+ self.buttons.accepted.connect(self._save)
+ self.buttons.rejected.connect(self.close)
+ form_grid.addWidget(self.buttons, row, 0, 1, 2)
+
+ self.list.currentItemChanged.connect(self._item_changed)
+ self.list.itemSelectionChanged.connect(self._selection_changed)
+ self._load_existing_hightlighters()
+
+ def _add(self):
+ highlighter = HighlightRegex(
+ self.query.text(),
+ self.ignore_case.isChecked(),
+ self.is_regex.isChecked(),
+ self.hit_background_color.color,
+ self.line_background_color.color
+ )
+ item = PayloadItem(self.query.text(), highlighter)
+ item.setBackground(highlighter.hit_background_brush())
+ self.list.addItem(item)
+ self.list.setCurrentItem(item)
+
+ def _update(self):
+ item: PayloadItem = self.list.currentItem()
+ highlighter: HighlightRegex = item.payload
+
+ highlighter.query = self.query.text()
+ highlighter.ignore_case = self.ignore_case.isChecked()
+ highlighter.is_regex = self.is_regex.isChecked()
+ highlighter.hit_background_color = self.hit_background_color.color
+ highlighter.line_background_color = self.line_background_color.color
+
+ item.setText(self.query.text())
+ item.setBackground(highlighter.hit_background_brush())
+
+ def _delete(self):
+ index = self.list.selectedIndexes()[0]
+ selected_index = index.row()
+ self.list.takeItem(selected_index)
+
+ def _move_up(self):
+ index = self.list.currentIndex()
+ selected_index = index.row()
+ item = self.list.takeItem(selected_index)
+ self.list.insertItem(selected_index - 1, item)
+ self.list.setCurrentIndex(index.siblingAtRow(selected_index - 1))
+
+ def _move_down(self):
+ index = self.list.selectedIndexes()[0]
+ selected_index = index.row()
+ item = self.list.takeItem(selected_index)
+ self.list.insertItem(selected_index + 1, item)
+ self.list.setCurrentIndex(index.sibling(selected_index + 1, 0))
+
+ def _save(self):
+ highlighters = []
+ for index in range(0, self.list.count()):
+ item: PayloadItem = self.list.item(index)
+ highlighters.append(item.payload)
+ Highlighting.write_config(self._settings, highlighters)
+ self.close()
+
+ def _selection_changed(self):
+ if len(self.list.selectedIndexes()) == 0:
+ self.btn_update.setDisabled(True)
+ self.btn_delete.setDisabled(True)
+ self.btn_move_up.setDisabled(True)
+ self.btn_move_down.setDisabled(True)
+ if len(self.list.selectedIndexes()) == 1:
+ selected_index = self.list.selectedIndexes()[0].row()
+ self.btn_update.setDisabled(False)
+ self.btn_delete.setDisabled(False)
+ self.btn_move_up.setDisabled(selected_index == 0)
+ self.btn_move_down.setDisabled(selected_index + 1 >= self.list.count())
+
+ def _item_changed(self, current, _previous):
+ item: PayloadItem = current
+ if item:
+ highlighter: HighlightRegex = item.payload
+ self.query.setText(highlighter.query)
+ self.ignore_case.setChecked(highlighter.ignore_case)
+ self.is_regex.setChecked(highlighter.is_regex)
+ self.hit_background_color.set_color(highlighter.hit_background_color)
+ self.line_background_color.set_color(highlighter.line_background_color)
+
+ def _load_existing_hightlighters(self):
+ highlighters: [HighlightRegex] = Highlighting.read_config(self._settings)
+ first_item = None
+ for highlighter in highlighters:
+ item = PayloadItem(str(highlighter.query))
+ item.payload = highlighter
+ item.setBackground(highlighter.hit_background_brush())
+ self.list.addItem(item)
+ if not first_item:
+ first_item = item
+ if first_item:
+ self.list.setCurrentItem(first_item)
diff --git a/logFileModel.py b/logFileModel.py
index f69b424..b195605 100644
--- a/logFileModel.py
+++ b/logFileModel.py
@@ -21,15 +21,8 @@ class LogFileModel:
self._file = os.path.realpath(file)
self._lock = threading.RLock()
- self.highlights = Highlighting.read_config(settings)
- # [
- # HighlightRegex(
- # re.compile("ERROR"),
- # brush=QBrush(QColor(220, 112, 122)),
- # pen=QPen(QColor(0, 0, 0)),
- # brush_full_line=QBrush(QColor(255, 112, 122))
- # )
- # ]
+ def highlighters(self):
+ return Highlighting.read_config(self.settings)
def get_file(self):
return self._file
@@ -40,14 +33,15 @@ class LogFileModel:
def get_query_highlight(self):
return self._query_highlight
- def set_query_highlight(self, regex: Optional[re.Pattern] = None):
- if regex:
- self._query_highlight = HighlightRegex(
- regex,
- brush=QBrush(QColor(255, 255, 0))
- )
- else:
- self._query_highlight = None
+ def clear_query_highlight(self):
+ self._query_highlight = None
+
+ def set_query_highlight(self, query: str, ignore_case: bool, is_regex: bool):
+ self._query_highlight = HighlightRegex(
+ query=query,
+ ignore_case=ignore_case,
+ is_regex=is_regex,
+ hit_background_color="ffff00")
def get_tab_name(self):
file_name = os.path.basename(self._file)
diff --git a/main.py b/main.py
index 691d397..9f469bd 100644
--- a/main.py
+++ b/main.py
@@ -12,6 +12,7 @@ import urlutils
from aboutdialog import AboutDialog
from ravenui import RavenUI
from settingsstore import SettingsStore
+from highlightingdialog import HighlightingDialog
from tabs import Tabs
from urlutils import url_is_file
@@ -64,6 +65,13 @@ class MainWindow(QMainWindow):
file_menu.addAction(close_action)
return file_menu
+ def highlight_menu(self) -> QMenu:
+ result = QMenu(self.tr("&Highlighting"), self)
+ manage = QAction(self.tr("&Manage"), self)
+ manage.triggered.connect(lambda: HighlightingDialog(self.settings).exec())
+ result.addAction(manage)
+ return result
+
def help_menu(self) -> QMenu:
help_menu = QMenu(self.tr("&Help", "name of the help menu"), self)