add time diff plugin
This commit is contained in:
12
icons/ionicons/stopwatch-outline.svg
Normal file
12
icons/ionicons/stopwatch-outline.svg
Normal 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 |
5
icons/ionicons/stopwatch.svg
Normal file
5
icons/ionicons/stopwatch.svg
Normal 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 |
1
main.py
1
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()
|
||||
|
||||
@@ -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()))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
11
src/plugins/logfile/preprocesslineshook.py
Normal file
11
src/plugins/logfile/preprocesslineshook.py
Normal 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
|
||||
@@ -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
|
||||
|
||||
|
||||
0
src/plugins/timediff/__init__.py
Normal file
0
src/plugins/timediff/__init__.py
Normal file
75
src/plugins/timediff/time_diff_pre_process_lines_hook.py
Normal file
75
src/plugins/timediff/time_diff_pre_process_lines_hook.py
Normal 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())
|
||||
34
src/plugins/timediffplugin.py
Normal file
34
src/plugins/timediffplugin.py
Normal 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)
|
||||
18
src/ui/bigtext/preProcessLinesModel.py
Normal file
18
src/ui/bigtext/preProcessLinesModel.py
Normal 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()
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user