add time diff plugin

This commit is contained in:
2022-03-12 09:28:33 +01:00
parent b99421e8e7
commit 297f67b9b5
16 changed files with 239 additions and 21 deletions

View File

@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
<title>ionicons-v5-g</title>
<line x1="256" y1="232" x2="256" y2="152"
style="fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/>
<line x1="256" y1="88" x2="256" y2="72"
style="fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:48px"/>
<line x1="132" y1="132" x2="120" y2="120"
style="fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:48px"/>
<circle cx="256" cy="272" r="32" style="fill:none;stroke:#000;stroke-miterlimit:10;stroke-width:32px"/>
<path d="M256,96A176,176,0,1,0,432,272,176,176,0,0,0,256,96Z"
style="fill:none;stroke:#000;stroke-miterlimit:10;stroke-width:32px"/>
</svg>

After

Width:  |  Height:  |  Size: 827 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
<title>ionicons-v5-g</title>
<circle cx="256" cy="272" r="16"/>
<path d="M280,81.5V72a24,24,0,0,0-48,0v9.5a191,191,0,0,0-84.43,32.13L137,103A24,24,0,0,0,103,137l8.6,8.6A191.17,191.17,0,0,0,64,272c0,105.87,86.13,192,192,192s192-86.13,192-192C448,174.26,374.58,93.34,280,81.5ZM256,320a48,48,0,0,1-16-93.25V152a16,16,0,0,1,32,0v74.75A48,48,0,0,1,256,320Z"/>
</svg>

After

Width:  |  Height:  |  Size: 461 B

View File

@@ -54,6 +54,7 @@ if __name__ == "__main__":
PluginRegistry.load_plugin("OpenFilePlugin") PluginRegistry.load_plugin("OpenFilePlugin")
PluginRegistry.load_plugin("LogFilePlugin") PluginRegistry.load_plugin("LogFilePlugin")
PluginRegistry.load_plugin("NotesPlugin") PluginRegistry.load_plugin("NotesPlugin")
PluginRegistry.load_plugin("TimeDiffPlugin")
window = PluginRegistry.execute_single("create_main_window") window = PluginRegistry.execute_single("create_main_window")
window.show() window.show()

View File

@@ -24,11 +24,6 @@ MAX_LINE_LENGTH = 4096
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
log = logging.getLogger("main") log = logging.getLogger("main")
def flat_map(array: List[List]) -> List:
return reduce(list.__add__, array)
class MainWindow(QMainWindow): class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs) super(MainWindow, self).__init__(*args, **kwargs)
@@ -55,7 +50,7 @@ class MainWindow(QMainWindow):
def create_dynamic_menu_bar(self) -> QMenuBar: def create_dynamic_menu_bar(self) -> QMenuBar:
menu_bar = 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_highlighter()))
menu_contributions.append(MenuContribution("settings", action=self._action_highlight_search_terms())) menu_contributions.append(MenuContribution("settings", action=self._action_highlight_search_terms()))
menu_contributions.append(MenuContribution("settings", action=self._action_new_tab())) menu_contributions.append(MenuContribution("settings", action=self._action_new_tab()))

View File

@@ -33,7 +33,7 @@ def sort_menu_contributions(menu_contributions: [MenuContribution]) -> [MenuCont
result = [] result = []
items = _sort_by_action_id(menu_contributions[:]) 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) # add remaining items to the end (ordered by their action_id)
# This resolves cycles. # This resolves cycles.
@@ -42,10 +42,10 @@ def sort_menu_contributions(menu_contributions: [MenuContribution]) -> [MenuCont
return result 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: for item in items:
mc: MenuContribution = item mc: MenuContribution = item
if not mc.after: if not mc.after:
result.append(mc) result.append(mc)
items.remove(mc) items.remove(mc)
_recursive_half_order_adder(result, items, mc.action_id) _recursive_half_order_adder(result, items)

View File

@@ -1,7 +1,7 @@
from typing import Callable from typing import Callable
from PySide6.QtGui import QAction, QIcon from PySide6.QtGui import QAction, QIcon
from PySide6.QtWidgets import QMenu from PySide6.QtWidgets import QMenu, QPushButton, QWidget
class RAction(): class RAction():
@@ -86,3 +86,20 @@ class RAction():
self._update_check_state() self._update_check_state()
return action 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

View File

@@ -8,13 +8,17 @@ from typing import Optional, Callable
from PySide6.QtCore import QRunnable, QThreadPool, Signal from PySide6.QtCore import QRunnable, QThreadPool, Signal
from PySide6.QtGui import QIcon from PySide6.QtGui import QIcon
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QCheckBox, QPushButton, QComboBox, \ 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.bigtext import BigText
from src.ui.bigtext.logFileModel import LogFileModel from src.ui.bigtext.logFileModel import LogFileModel
from src.i18n import _ from src.i18n import _
from src.pluginregistry import PluginRegistry from src.pluginregistry import PluginRegistry
from src.ui.hbox import HBox
from src.zonedpluginregistry import ZonedPluginRegistry
class FilterTask(QRunnable): class FilterTask(QRunnable):
@@ -27,6 +31,7 @@ class FilterTask(QRunnable):
regex: re.Pattern, regex: re.Pattern,
lock: threading.RLock, lock: threading.RLock,
filter_match_found_listeners: Callable[[int], None], filter_match_found_listeners: Callable[[int], None],
pre_process_lines_hooks: [PreProcessLinesHook],
progress_handler: Callable[[float], None], progress_handler: Callable[[float], None],
on_before: Callable[[], None], on_before: Callable[[], None],
on_finish: Callable[[], None] on_finish: Callable[[], None]
@@ -36,6 +41,7 @@ class FilterTask(QRunnable):
self.filter_model = filter_model self.filter_model = filter_model
self.regex = regex self.regex = regex
self.progress_handler = progress_handler self.progress_handler = progress_handler
self.pre_process_lines_hooks = pre_process_lines_hooks
self.on_before = on_before self.on_before = on_before
self.on_finish = on_finish self.on_finish = on_finish
self.lock = lock self.lock = lock
@@ -71,7 +77,12 @@ class FilterTask(QRunnable):
target_line_offset = target.tell() target_line_offset = target.tell()
for listener in self.filter_match_found_listeners: for listener in self.filter_match_found_listeners:
listener(target_line_offset, source_line_offset) 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 # sometime buffering can hide results for a while
# We force a flush periodically. # We force a flush periodically.
@@ -100,9 +111,10 @@ class FilterWidget(QWidget):
search_is_running = Signal(bool) search_is_running = Signal(bool)
signal_update_progress = Signal(float) 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__() super(FilterWidget, self).__init__()
self.source_model = source_model self.source_model = source_model
self._zoned_plugin_registry = zoned_plugin_registry
self._lock = threading.RLock() self._lock = threading.RLock()
@@ -131,6 +143,8 @@ class FilterWidget(QWidget):
self.btn_bookmark.setToolTip(_("save query")) self.btn_bookmark.setToolTip(_("save query"))
self.btn_bookmark.pressed.connect(self._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 = QCheckBox(_("ignore case"))
self.ignore_case.setChecked(True) self.ignore_case.setChecked(True)
self.ignore_case.stateChanged.connect(self.filter_changed) 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.progress_bar)
filter_bar.layout.addWidget(self.btn_cancel_search) filter_bar.layout.addWidget(self.btn_cancel_search)
filter_bar.layout.addWidget(self.btn_bookmark) 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.ignore_case)
filter_bar.layout.addWidget(self.is_regex) filter_bar.layout.addWidget(self.is_regex)
@@ -162,6 +177,16 @@ class FilterWidget(QWidget):
def on_reveal(self): def on_reveal(self):
self._reload_save_queries() 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): def _reload_save_queries(self):
saved_queries = PluginRegistry.execute_single("saved_queries") saved_queries = PluginRegistry.execute_single("saved_queries")
for saved_query in 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.source_model.set_query_highlight(query, ignore_case, is_regex)
self.filter_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.filter_task = FilterTask(
self.source_model, self.source_model,
self.filter_model, self.filter_model,
regex, regex,
self._lock, self._lock,
self.filter_match_found_listeners, self.filter_match_found_listeners,
pre_process_lines_hooks,
self.progress_handler, self.progress_handler,
lambda: self.search_is_running.emit(True), lambda: self.search_is_running.emit(True),
lambda: self.search_is_running.emit(False) lambda: self.search_is_running.emit(False)

View File

@@ -7,15 +7,17 @@ from src.plugins.logfile.filterwidget import FilterWidget
from src.ui.bigtext.logFileModel import LogFileModel from src.ui.bigtext.logFileModel import LogFileModel
from src.plugins.krowlog.Tab import Tab from src.plugins.krowlog.Tab import Tab
from src.util.conversion import humanbytes from src.util.conversion import humanbytes
from src.zonedpluginregistry import ZonedPluginRegistry
class FullTabWidget(Tab): 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) super(FullTabWidget, self).__init__(unique_id, title)
self._model = model self._model = model
self._zoned_plugin_registry = zoned_plugin_registry
self.file_view = BigText(model) 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_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_line_click_listener(self.filter_view_syncer.click_listener)
self.filter_hit_view.add_filter_match_found_listener(self.filter_view_syncer.match_found) self.filter_hit_view.add_filter_match_found_listener(self.filter_view_syncer.match_found)

View File

@@ -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

View File

@@ -3,6 +3,7 @@ from typing import Optional
from PySide6.QtWidgets import QMessageBox from PySide6.QtWidgets import QMessageBox
from src.pluginregistry import PluginRegistry
from src.plugins.logfile.fulltabwidget import FullTabWidget from src.plugins.logfile.fulltabwidget import FullTabWidget
from src.ui.bigtext.logFileModel import LogFileModel from src.ui.bigtext.logFileModel import LogFileModel
from src.pluginbase import PluginBase from src.pluginbase import PluginBase
@@ -31,7 +32,8 @@ class LogFilePlugin(PluginBase):
filename = os.path.basename(realpath) filename = os.path.basename(realpath)
model = LogFileModel(file, self.settings) 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 return tab

View File

View File

@@ -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())

View File

@@ -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)

View File

@@ -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()

View File

@@ -5,5 +5,9 @@ class HBox(QWidget):
def __init__(self, *widgets: QWidget): def __init__(self, *widgets: QWidget):
super(HBox, self).__init__() super(HBox, self).__init__()
self.layout = QHBoxLayout(self) self.layout = QHBoxLayout(self)
self.layout.setContentsMargins(0, 0, 0, 0)
for widget in widgets: for widget in widgets:
self.layout.addWidget(widget) self.layout.addWidget(widget)
def addWidget(self, widget: QWidget):
self.layout.addWidget(widget)

View File

@@ -1,10 +1,24 @@
# extract icons from dll on windows # extract icons from dll on windows
# https://mail.python.org/pipermail/python-win32/2009-April/009078.html # 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 def text_changed():
print(PySide6.QtCore.__version__) 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()