diff --git a/icons/ionicons/stopwatch-outline.svg b/icons/ionicons/stopwatch-outline.svg
new file mode 100644
index 0000000..16196a7
--- /dev/null
+++ b/icons/ionicons/stopwatch-outline.svg
@@ -0,0 +1,12 @@
+
\ No newline at end of file
diff --git a/icons/ionicons/stopwatch.svg b/icons/ionicons/stopwatch.svg
new file mode 100644
index 0000000..1a62773
--- /dev/null
+++ b/icons/ionicons/stopwatch.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/main.py b/main.py
index 3836579..98686cb 100644
--- a/main.py
+++ b/main.py
@@ -54,6 +54,7 @@ if __name__ == "__main__":
PluginRegistry.load_plugin("OpenFilePlugin")
PluginRegistry.load_plugin("LogFilePlugin")
PluginRegistry.load_plugin("NotesPlugin")
+ PluginRegistry.load_plugin("TimeDiffPlugin")
window = PluginRegistry.execute_single("create_main_window")
window.show()
diff --git a/src/mainwindow.py b/src/mainwindow.py
index 19451c6..d87553e 100644
--- a/src/mainwindow.py
+++ b/src/mainwindow.py
@@ -24,11 +24,6 @@ MAX_LINE_LENGTH = 4096
logging.basicConfig(level=logging.INFO)
log = logging.getLogger("main")
-
-def flat_map(array: List[List]) -> List:
- return reduce(list.__add__, array)
-
-
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
@@ -55,7 +50,7 @@ class MainWindow(QMainWindow):
def create_dynamic_menu_bar(self) -> QMenuBar:
menu_bar = QMenuBar()
- menu_contributions: [MenuContribution] = flat_map(PluginRegistry.execute("get_menu_contributions"))
+ menu_contributions: [MenuContribution] = PluginRegistry.execute_flat_map("get_menu_contributions")
menu_contributions.append(MenuContribution("settings", action=self._action_highlighter()))
menu_contributions.append(MenuContribution("settings", action=self._action_highlight_search_terms()))
menu_contributions.append(MenuContribution("settings", action=self._action_new_tab()))
diff --git a/src/plugins/domain/menucontribution.py b/src/plugins/domain/menucontribution.py
index 6164c5c..5496d12 100644
--- a/src/plugins/domain/menucontribution.py
+++ b/src/plugins/domain/menucontribution.py
@@ -33,7 +33,7 @@ def sort_menu_contributions(menu_contributions: [MenuContribution]) -> [MenuCont
result = []
items = _sort_by_action_id(menu_contributions[:])
- _recursive_half_order_adder(result, items, None)
+ _recursive_half_order_adder(result, items)
# add remaining items to the end (ordered by their action_id)
# This resolves cycles.
@@ -42,10 +42,10 @@ def sort_menu_contributions(menu_contributions: [MenuContribution]) -> [MenuCont
return result
-def _recursive_half_order_adder(result: [MenuContribution], items: [MenuContribution], parent):
+def _recursive_half_order_adder(result: [MenuContribution], items: [MenuContribution]):
for item in items:
mc: MenuContribution = item
if not mc.after:
result.append(mc)
items.remove(mc)
- _recursive_half_order_adder(result, items, mc.action_id)
+ _recursive_half_order_adder(result, items)
diff --git a/src/plugins/domain/raction.py b/src/plugins/domain/raction.py
index ee403b0..1d3fbdc 100644
--- a/src/plugins/domain/raction.py
+++ b/src/plugins/domain/raction.py
@@ -1,7 +1,7 @@
from typing import Callable
from PySide6.QtGui import QAction, QIcon
-from PySide6.QtWidgets import QMenu
+from PySide6.QtWidgets import QMenu, QPushButton, QWidget
class RAction():
@@ -86,3 +86,20 @@ class RAction():
self._update_check_state()
return action
+
+ def to_qpushbutton(self, parent: QWidget) -> QPushButton:
+ button = QPushButton(parent)
+ if self.label:
+ button.setText(self.label)
+ if self.icon_from_theme:
+ button.setIcon(QIcon.fromTheme(self.icon_from_theme))
+ if self.icon_file:
+ button.setIcon(QIcon(self.icon_file))
+ if self.shortcut:
+ button.setShortcut(self.shortcut)
+ if self.action:
+ button.pressed.connect(self.action)
+ if self.checkable:
+ button.setChecked(self.checked)
+ button.setCheckable(self.checkable)
+ return button
diff --git a/src/plugins/logfile/filterwidget.py b/src/plugins/logfile/filterwidget.py
index 00a1481..dec0f84 100644
--- a/src/plugins/logfile/filterwidget.py
+++ b/src/plugins/logfile/filterwidget.py
@@ -8,13 +8,17 @@ from typing import Optional, Callable
from PySide6.QtCore import QRunnable, QThreadPool, Signal
from PySide6.QtGui import QIcon
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QCheckBox, QPushButton, QComboBox, \
- QSizePolicy, QProgressBar
+ QSizePolicy, QProgressBar, QMenu, QMenuBar
+from src.plugins.domain.raction import RAction
+from src.plugins.logfile.preprocesslineshook import PreProcessLinesHook
from src.ui.bigtext.bigtext import BigText
from src.ui.bigtext.logFileModel import LogFileModel
from src.i18n import _
from src.pluginregistry import PluginRegistry
+from src.ui.hbox import HBox
+from src.zonedpluginregistry import ZonedPluginRegistry
class FilterTask(QRunnable):
@@ -27,6 +31,7 @@ class FilterTask(QRunnable):
regex: re.Pattern,
lock: threading.RLock,
filter_match_found_listeners: Callable[[int], None],
+ pre_process_lines_hooks: [PreProcessLinesHook],
progress_handler: Callable[[float], None],
on_before: Callable[[], None],
on_finish: Callable[[], None]
@@ -36,6 +41,7 @@ class FilterTask(QRunnable):
self.filter_model = filter_model
self.regex = regex
self.progress_handler = progress_handler
+ self.pre_process_lines_hooks = pre_process_lines_hooks
self.on_before = on_before
self.on_finish = on_finish
self.lock = lock
@@ -71,7 +77,12 @@ class FilterTask(QRunnable):
target_line_offset = target.tell()
for listener in self.filter_match_found_listeners:
listener(target_line_offset, source_line_offset)
- target.write(l)
+
+ for hook in self.pre_process_lines_hooks:
+ h: PreProcessLinesHook = hook
+ line = h.pre_process_line(line, target)
+
+ target.write(line.encode("utf8"))
# sometime buffering can hide results for a while
# We force a flush periodically.
@@ -100,9 +111,10 @@ class FilterWidget(QWidget):
search_is_running = Signal(bool)
signal_update_progress = Signal(float)
- def __init__(self, source_model: LogFileModel):
+ def __init__(self, source_model: LogFileModel, zoned_plugin_registry: ZonedPluginRegistry):
super(FilterWidget, self).__init__()
self.source_model = source_model
+ self._zoned_plugin_registry = zoned_plugin_registry
self._lock = threading.RLock()
@@ -131,6 +143,8 @@ class FilterWidget(QWidget):
self.btn_bookmark.setToolTip(_("save query"))
self.btn_bookmark.pressed.connect(self._save_query)
+ self.menu = self._create_stuff_menu()
+
self.ignore_case = QCheckBox(_("ignore case"))
self.ignore_case.setChecked(True)
self.ignore_case.stateChanged.connect(self.filter_changed)
@@ -146,6 +160,7 @@ class FilterWidget(QWidget):
filter_bar.layout.addWidget(self.progress_bar)
filter_bar.layout.addWidget(self.btn_cancel_search)
filter_bar.layout.addWidget(self.btn_bookmark)
+ filter_bar.layout.addWidget(self.menu)
filter_bar.layout.addWidget(self.ignore_case)
filter_bar.layout.addWidget(self.is_regex)
@@ -162,6 +177,16 @@ class FilterWidget(QWidget):
def on_reveal(self):
self._reload_save_queries()
+ def _create_stuff_menu(self):
+ menu = HBox()
+ actions = self._zoned_plugin_registry.execute_flat_map("get_filter_widget_actions")
+
+ for action in actions:
+ raction: RAction = action;
+ button = raction.to_qpushbutton(menu)
+ menu.addWidget(button)
+ return menu
+
def _reload_save_queries(self):
saved_queries = PluginRegistry.execute_single("saved_queries")
for saved_query in saved_queries:
@@ -238,12 +263,15 @@ class FilterWidget(QWidget):
self.source_model.set_query_highlight(query, ignore_case, is_regex)
self.filter_model.set_query_highlight(query, ignore_case, is_regex)
+ pre_process_lines_hooks = self._zoned_plugin_registry.execute_remove_falsy("get_pre_process_lines_hook")
+
self.filter_task = FilterTask(
self.source_model,
self.filter_model,
regex,
self._lock,
self.filter_match_found_listeners,
+ pre_process_lines_hooks,
self.progress_handler,
lambda: self.search_is_running.emit(True),
lambda: self.search_is_running.emit(False)
diff --git a/src/plugins/logfile/fulltabwidget.py b/src/plugins/logfile/fulltabwidget.py
index 08fff5d..a855a16 100644
--- a/src/plugins/logfile/fulltabwidget.py
+++ b/src/plugins/logfile/fulltabwidget.py
@@ -7,15 +7,17 @@ from src.plugins.logfile.filterwidget import FilterWidget
from src.ui.bigtext.logFileModel import LogFileModel
from src.plugins.krowlog.Tab import Tab
from src.util.conversion import humanbytes
+from src.zonedpluginregistry import ZonedPluginRegistry
class FullTabWidget(Tab):
- def __init__(self, model: LogFileModel, unique_id: str, title: str):
+ def __init__(self, model: LogFileModel, unique_id: str, title: str, zoned_plugin_registry: ZonedPluginRegistry):
super(FullTabWidget, self).__init__(unique_id, title)
self._model = model
+ self._zoned_plugin_registry = zoned_plugin_registry
self.file_view = BigText(model)
- self.filter_hit_view = FilterWidget(self._model)
+ self.filter_hit_view = FilterWidget(self._model, self._zoned_plugin_registry)
self.filter_view_syncer = FilterViewSyncer(self.file_view)
self.filter_hit_view.add_line_click_listener(self.filter_view_syncer.click_listener)
self.filter_hit_view.add_filter_match_found_listener(self.filter_view_syncer.match_found)
diff --git a/src/plugins/logfile/preprocesslineshook.py b/src/plugins/logfile/preprocesslineshook.py
new file mode 100644
index 0000000..ce2c8da
--- /dev/null
+++ b/src/plugins/logfile/preprocesslineshook.py
@@ -0,0 +1,11 @@
+from abc import abstractmethod
+from typing import List, BinaryIO
+
+from src.ui.bigtext.line import Line
+
+
+class PreProcessLinesHook:
+
+ @abstractmethod
+ def pre_process_line(self, line: str, file_io: BinaryIO) -> str:
+ return line
diff --git a/src/plugins/logfileplugin.py b/src/plugins/logfileplugin.py
index 1694d95..192b54b 100644
--- a/src/plugins/logfileplugin.py
+++ b/src/plugins/logfileplugin.py
@@ -3,6 +3,7 @@ from typing import Optional
from PySide6.QtWidgets import QMessageBox
+from src.pluginregistry import PluginRegistry
from src.plugins.logfile.fulltabwidget import FullTabWidget
from src.ui.bigtext.logFileModel import LogFileModel
from src.pluginbase import PluginBase
@@ -31,7 +32,8 @@ class LogFilePlugin(PluginBase):
filename = os.path.basename(realpath)
model = LogFileModel(file, self.settings)
- tab = FullTabWidget(model, unique_id=realpath, title=filename)
+ tab = FullTabWidget(model, unique_id=realpath, title=filename,
+ zoned_plugin_registry=PluginRegistry.create_zoned_plugin_registry())
return tab
diff --git a/src/plugins/timediff/__init__.py b/src/plugins/timediff/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/plugins/timediff/time_diff_pre_process_lines_hook.py b/src/plugins/timediff/time_diff_pre_process_lines_hook.py
new file mode 100644
index 0000000..ffd7fec
--- /dev/null
+++ b/src/plugins/timediff/time_diff_pre_process_lines_hook.py
@@ -0,0 +1,75 @@
+from typing import List, Optional, BinaryIO
+
+from src.plugins.logfile.preprocesslineshook import PreProcessLinesHook
+from src.ui.bigtext.line import Line
+from re import compile
+from datetime import datetime, timedelta
+
+
+class TimeDiffPreProcessLinesHook(PreProcessLinesHook):
+ def __init__(self):
+ super(TimeDiffPreProcessLinesHook, self).__init__()
+ self.date_pattern = compile(r"\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}([,.]\d{3})")
+ self.prev_time: Optional[datetime] = None
+
+ def pre_process_line(self, line: str, file_io: BinaryIO) -> str:
+ time = self._parse_time(line)
+ if time:
+ if self.prev_time:
+ time_diff = time - self.prev_time
+ if time_diff.total_seconds() > 0:
+ line_ending = self.parse_line_ending(line)
+ time_diff_str = self.time_diff_to_str(time_diff)
+
+ time_diff_line = f"⏱ {time_diff_str}{line_ending}"
+ file_io.write(time_diff_line.encode("utf8"))
+ self.prev_time = time
+ return line
+
+ def time_diff_to_str(self, time_diff: timedelta) -> str:
+ total_seconds = time_diff.total_seconds()
+ hours = int(total_seconds / 3600)
+ minutes = int((total_seconds % 3600) / 60)
+ seconds = int(total_seconds % 60)
+ milliseconds = int((total_seconds * 1000) % 1000)
+ if hours > 0:
+ result = f"{hours:3}:{minutes:02}:{seconds:02}.{milliseconds:03}"
+ elif minutes > 0:
+ result = f" {minutes:2}:{seconds:02}.{milliseconds:03}"
+ elif seconds:
+ result = f" {seconds:2}.{milliseconds:03}"
+ else:
+ result = f" 0.{milliseconds:03}"
+
+ return result
+
+ def parse_line_ending(self, line):
+ if line[-2:] == "\r\n":
+ return "\r\n"
+ return line[-1]
+
+ def _parse_time(self, line: str) -> Optional[datetime]:
+
+ m = self.date_pattern.match(line)
+ if m:
+ date_string = m.group(0).replace(" ", "T").replace(",", ".")
+ time = datetime.strptime(date_string, "%Y-%m-%dT%H:%M:%S.%f")
+ return time
+ return None
+
+
+if __name__ == "__main__":
+ t = TimeDiffPreProcessLinesHook()
+ texts = ["2022-02-22T12:00:00,123 foo bar", "2022-02-22T12:00:00.124 baz",
+ "should not match 2022-02-22T12:00:00,130 other",
+ "2022-02-22 12:00:00,130 yet another"]
+
+ lines = []
+ byte_offset = 0
+ for text in texts:
+ lines.append(Line(byte_offset=byte_offset, byte_end=byte_offset + len(text.encode("utf8")), line=text))
+ byte_offset = byte_offset + len(text.encode("utf8"))
+
+ ls = t.pre_process_lines(lines)
+ for l in ls:
+ print(l.line())
diff --git a/src/plugins/timediffplugin.py b/src/plugins/timediffplugin.py
new file mode 100644
index 0000000..89acaa1
--- /dev/null
+++ b/src/plugins/timediffplugin.py
@@ -0,0 +1,34 @@
+from typing import Optional
+
+from PySide6.QtGui import QIcon
+
+from src.pluginbase import PluginBase
+from src.plugins.domain.raction import RAction
+from src.plugins.logfile.preprocesslineshook import PreProcessLinesHook
+from src.plugins.timediff.time_diff_pre_process_lines_hook import TimeDiffPreProcessLinesHook
+from src.i18n import _
+
+
+class TimeDiffPlugin(PluginBase):
+ def __init__(self):
+ super(TimeDiffPlugin, self).__init__()
+ self.time_diff_state = False
+ self.time_diff_action = RAction(_(""), lambda: self._toggle_time_diff(),
+ icon_file="icons/ionicons/stopwatch-outline.svg", checkable=True)
+
+ def copy(self):
+ return TimeDiffPlugin()
+
+ def get_filter_widget_actions(self) -> [RAction]:
+ return [
+ self.time_diff_action
+ ]
+
+ def get_pre_process_lines_hook(self) -> Optional[PreProcessLinesHook]:
+ if self.time_diff_state:
+ return TimeDiffPreProcessLinesHook()
+ return None
+
+ def _toggle_time_diff(self):
+ self.time_diff_state = not self.time_diff_state
+ self.time_diff_action.set_checked(self.time_diff_state)
diff --git a/src/ui/bigtext/preProcessLinesModel.py b/src/ui/bigtext/preProcessLinesModel.py
new file mode 100644
index 0000000..b4e1bb9
--- /dev/null
+++ b/src/ui/bigtext/preProcessLinesModel.py
@@ -0,0 +1,18 @@
+from typing import List
+
+from src.plugins.logfile.preprocesslineshook import PreProcessLinesHook
+
+
+class PreProcessLinesModel:
+ def __init__(self):
+ self._pre_process_lines_hooks: List[PreProcessLinesHook] = []
+
+ def add_hook(self, hook: PreProcessLinesHook):
+ self._pre_process_lines_hooks.append(hook)
+
+ def add_hooks(self, hooks: List[PreProcessLinesHook]):
+ for hook in hooks:
+ self.add_hook(hook)
+
+ def get_hooks(self) -> List[PreProcessLinesHook]:
+ return self._pre_process_lines_hooks.copy()
diff --git a/src/ui/hbox.py b/src/ui/hbox.py
index 71a4ea1..3b17ae4 100644
--- a/src/ui/hbox.py
+++ b/src/ui/hbox.py
@@ -5,5 +5,9 @@ class HBox(QWidget):
def __init__(self, *widgets: QWidget):
super(HBox, self).__init__()
self.layout = QHBoxLayout(self)
+ self.layout.setContentsMargins(0, 0, 0, 0)
for widget in widgets:
self.layout.addWidget(widget)
+
+ def addWidget(self, widget: QWidget):
+ self.layout.addWidget(widget)
diff --git a/testbed/scribble.py b/testbed/scribble.py
index cfe81f7..e3dcdd3 100644
--- a/testbed/scribble.py
+++ b/testbed/scribble.py
@@ -1,10 +1,24 @@
# extract icons from dll on windows
# https://mail.python.org/pipermail/python-win32/2009-April/009078.html
+import sys
-import PySide6.QtCore
+from PySide6.QtWidgets import QApplication, QWidget, QHBoxLayout, QComboBox, QMainWindow
-# Prints PySide6 version
-print(PySide6.__version__)
-# Prints the Qt version used to compile PySide6
-print(PySide6.QtCore.__version__)
+def text_changed():
+ print("text changed")
+
+
+if __name__ == "__main__":
+ app = QApplication(sys.argv)
+ window = QMainWindow()
+
+ query_field = QComboBox()
+ query_field.setEditable(True)
+ query_field.setInsertPolicy(QComboBox.InsertAtTop)
+ query_field.lineEdit().textChanged.connect(text_changed)
+ window.setCentralWidget(query_field)
+
+ window.show()
+
+ app.exec()