rename ravenlog to krowlog
There is a database named RavenDB. KrowLog starts with a K, which is a) distinctive and b) has an association to KDE.
This commit is contained in:
0
src/plugins/logfile/__init__.py
Normal file
0
src/plugins/logfile/__init__.py
Normal file
21
src/plugins/logfile/filterviewsyncer.py
Normal file
21
src/plugins/logfile/filterviewsyncer.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from src.ui.bigtext.bigtext import BigText
|
||||
|
||||
|
||||
class FilterViewSyncer:
|
||||
|
||||
def __init__(self, sync_view: BigText):
|
||||
self._matches = {}
|
||||
self._sync_view = sync_view
|
||||
|
||||
def click_listener(self, byte_offset: int):
|
||||
source_byte_offset = self._matches[byte_offset] if byte_offset in self._matches else None
|
||||
# print("click %d -> %d (total hits %d)" % (byte_offset, source_byte_offset, len(self._matches)))
|
||||
if source_byte_offset is not None:
|
||||
self._sync_view.scroll_to_byte(source_byte_offset)
|
||||
|
||||
def match_found(self, match_byte_offset: int, source_byte_offset: int):
|
||||
# print("match %d" % match_byte_offset)
|
||||
if match_byte_offset >= 0:
|
||||
self._matches[match_byte_offset] = source_byte_offset
|
||||
else:
|
||||
self._matches = {}
|
||||
246
src/plugins/logfile/filterwidget.py
Normal file
246
src/plugins/logfile/filterwidget.py
Normal file
@@ -0,0 +1,246 @@
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
from typing import Optional, Callable
|
||||
|
||||
from PySide6.QtCore import QRunnable, QThreadPool, Signal, QThread, QObject
|
||||
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLineEdit, QCheckBox, QPushButton, QLabel, QProgressBar
|
||||
|
||||
from src.ui.bigtext.bigtext import BigText
|
||||
from src.ui.bigtext.logFileModel import LogFileModel
|
||||
|
||||
from src.i18n import _
|
||||
from src.pluginregistry import PluginRegistry
|
||||
|
||||
log = logging.getLogger("filterwidget")
|
||||
|
||||
|
||||
class FilterTask(QThread):
|
||||
aborted = False
|
||||
|
||||
on_before = Signal()
|
||||
on_finish = Signal()
|
||||
filter_progress = Signal(float)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
source_model: LogFileModel,
|
||||
filter_model: LogFileModel,
|
||||
regex: re.Pattern,
|
||||
lock: threading.RLock,
|
||||
filter_match_found_listeners: Callable[[int], None]
|
||||
):
|
||||
super(FilterTask, self).__init__()
|
||||
self.source_model = source_model
|
||||
self.filter_model = filter_model
|
||||
self.regex = regex
|
||||
self.lock = lock
|
||||
self.filter_match_found_listeners = filter_match_found_listeners
|
||||
|
||||
def run(self):
|
||||
# print("writing to tmp file", self.filter_model.get_file())
|
||||
|
||||
# the lock ensures that we only start a new search when the previous search already ended
|
||||
with self.lock:
|
||||
print("starting thread ", threading.current_thread())
|
||||
self.on_before.emit()
|
||||
|
||||
if self.aborted:
|
||||
self.on_finish.emit()
|
||||
|
||||
for listener in self.filter_match_found_listeners:
|
||||
listener(-1, -1) # notify listeners that a new search started
|
||||
|
||||
self.filter_progress.emit(0.0)
|
||||
|
||||
try:
|
||||
source_file = self.source_model.get_file()
|
||||
file_size = os.stat(source_file).st_size
|
||||
start = time.time()
|
||||
with open(source_file, "rb") as source:
|
||||
with open(self.filter_model.get_file(), "w+b") as target:
|
||||
line_count = 0
|
||||
lines_written = 0
|
||||
last_bytes_read = 0
|
||||
bytes_read = 0
|
||||
while l := source.readline():
|
||||
bytes_read = bytes_read + len(l)
|
||||
line_count = line_count + 1
|
||||
line = l.decode("utf8", errors="ignore")
|
||||
|
||||
if self.regex.findall(line):
|
||||
# time.sleep(0.5)
|
||||
lines_written = lines_written + 1
|
||||
source_line_offset = source.tell() - len(l)
|
||||
target_line_offset = target.tell()
|
||||
for listener in self.filter_match_found_listeners:
|
||||
listener(target_line_offset, source_line_offset)
|
||||
target.write(l)
|
||||
|
||||
# sometime buffering can hide results for a while
|
||||
# We force a flush periodically.
|
||||
if line_count % 10000 == 0:
|
||||
now = time.time()
|
||||
time_diff = (now - start)
|
||||
if time_diff > 0.5:
|
||||
read_speed = ((bytes_read - last_bytes_read) / time_diff) / (1024 * 1024)
|
||||
# todo progress disabled because of its detrimental effect on UI responsibility
|
||||
# print("emit %f" % (bytes_read / file_size))
|
||||
self.filter_progress.emit(bytes_read / file_size)
|
||||
# self._progress_updater(bytes_read / file_size, read_speed)
|
||||
last_bytes_read = bytes_read
|
||||
start = time.time()
|
||||
|
||||
if lines_written > 0:
|
||||
target.flush()
|
||||
lines_written = 0
|
||||
|
||||
if self.aborted:
|
||||
print("aborted ", time.time())
|
||||
break
|
||||
finally:
|
||||
self.on_finish.emit()
|
||||
print("dome thread ", threading.current_thread())
|
||||
|
||||
|
||||
class FilterWidget(QWidget):
|
||||
filter_model: LogFileModel
|
||||
filter_task: Optional[FilterTask] = None
|
||||
|
||||
def __init__(self, source_model: LogFileModel):
|
||||
super(FilterWidget, self).__init__()
|
||||
self.source_model = source_model
|
||||
|
||||
self._lock = threading.RLock()
|
||||
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.query_field = QLineEdit()
|
||||
self.query_field.textChanged.connect(self.filter_changed)
|
||||
|
||||
self._lbl_search_progress = QLabel("0%")
|
||||
self._lbl_search_progress.setVisible(False)
|
||||
|
||||
self._progress_bar = QProgressBar();
|
||||
self._progress_bar.setVisible(False)
|
||||
self._progress_bar.setMinimum(0)
|
||||
self._progress_bar.setMaximum(100)
|
||||
self._progress_bar.setMaximumWidth(50)
|
||||
|
||||
self.btn_cancel_search = QPushButton(_("Cancel"))
|
||||
self.btn_cancel_search.setVisible(False)
|
||||
self.btn_cancel_search.pressed.connect(self._cancel_search)
|
||||
|
||||
self.ignore_case = QCheckBox(_("ignore case"))
|
||||
self.ignore_case.setChecked(True)
|
||||
self.ignore_case.stateChanged.connect(self.filter_changed)
|
||||
|
||||
self.is_regex = QCheckBox(_("regex"))
|
||||
self.is_regex.setChecked(True)
|
||||
self.is_regex.stateChanged.connect(self.filter_changed)
|
||||
|
||||
filter_bar = QWidget()
|
||||
filter_bar.layout = QHBoxLayout(filter_bar)
|
||||
filter_bar.layout.setContentsMargins(0, 0, 0, 0)
|
||||
filter_bar.layout.addWidget(self.query_field)
|
||||
filter_bar.layout.addWidget(self._lbl_search_progress)
|
||||
filter_bar.layout.addWidget(self._progress_bar)
|
||||
filter_bar.layout.addWidget(self.btn_cancel_search)
|
||||
filter_bar.layout.addWidget(self.ignore_case)
|
||||
filter_bar.layout.addWidget(self.is_regex)
|
||||
|
||||
(handle, self.tmpfilename) = tempfile.mkstemp()
|
||||
os.close(handle)
|
||||
self.filter_model = LogFileModel(self.tmpfilename, self.source_model.settings)
|
||||
self.hits_view = BigText(self.filter_model)
|
||||
|
||||
self.layout.addWidget(filter_bar)
|
||||
self.layout.addWidget(self.hits_view)
|
||||
|
||||
self.filter_match_found_listeners: [Callable[[int], None]] = []
|
||||
|
||||
def add_line_click_listener(self, listener: Callable[[int], None]):
|
||||
self.hits_view.add_line_click_listener(listener)
|
||||
|
||||
def add_filter_match_found_listener(self, listener: Callable[[int], None]):
|
||||
self.filter_match_found_listeners.append(listener)
|
||||
|
||||
def destruct(self):
|
||||
# print("cleanup: ", self.tmpfilename)
|
||||
self._cancel_search()
|
||||
os.remove(self.tmpfilename)
|
||||
|
||||
def _cancel_search(self):
|
||||
if self.filter_task:
|
||||
# print("cancel started ", time.time())
|
||||
self.filter_task.aborted = True
|
||||
# wait until the previous search is aborted
|
||||
with self._lock:
|
||||
pass
|
||||
|
||||
def reset_filter(self):
|
||||
self.filter_model.truncate()
|
||||
self.source_model.clear_query_highlight()
|
||||
self.filter_model.clear_query_highlight()
|
||||
PluginRegistry.execute("update_ui")
|
||||
|
||||
def filter_changed(self):
|
||||
query = self.query_field.text()
|
||||
ignore_case = self.ignore_case.isChecked()
|
||||
is_regex = self.is_regex.isChecked()
|
||||
|
||||
# cancel previous search
|
||||
self._cancel_search()
|
||||
|
||||
if len(query) == 0:
|
||||
self.reset_filter()
|
||||
return
|
||||
|
||||
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)
|
||||
except:
|
||||
# query was not a valid regex -> clear search hits, then abort
|
||||
self.filter_model.truncate()
|
||||
return
|
||||
|
||||
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,
|
||||
self.filter_model,
|
||||
regex,
|
||||
self._lock,
|
||||
self.filter_match_found_listeners,
|
||||
)
|
||||
self.filter_task.on_before.connect(self._on_before)
|
||||
self.filter_task.on_finish.connect(self._on_finish)
|
||||
self.filter_task.filter_progress.connect(self._update_progress)
|
||||
# self.filter_task.finished.connect(self.filter_task.deleteLater)
|
||||
# super().connect(self.filter_task, FilterTask.filter_progress, self, self._update_progress)
|
||||
# super().connect(self.filter_task, FilterTask.finished, self.filter_task, QObject.deleteLater)
|
||||
self.filter_task.start()
|
||||
# QThreadPool.globalInstance().start(self.filter_task)
|
||||
|
||||
def _on_before(self):
|
||||
print("on_before")
|
||||
self.btn_cancel_search.setVisible(True)
|
||||
self._progress_bar.setVisible(True)
|
||||
|
||||
def _on_finish(self):
|
||||
print("on_finish")
|
||||
self.btn_cancel_search.setVisible(False)
|
||||
self._progress_bar.setVisible(False)
|
||||
self.filter_task.deleteLater()
|
||||
|
||||
def _update_progress(self, progress: float):
|
||||
print("progress %f" % (progress))
|
||||
self._progress_bar.setValue(progress * 100)
|
||||
47
src/plugins/logfile/fulltabwidget.py
Normal file
47
src/plugins/logfile/fulltabwidget.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from PySide6.QtWidgets import *
|
||||
from PySide6.QtCore import *
|
||||
|
||||
from src.ui.bigtext.bigtext import BigText
|
||||
from src.plugins.logfile.filterviewsyncer import FilterViewSyncer
|
||||
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
|
||||
|
||||
|
||||
class FullTabWidget(Tab):
|
||||
|
||||
def __init__(self, model: LogFileModel, unique_id: str, title: str):
|
||||
super(FullTabWidget, self).__init__(unique_id, title)
|
||||
self._model = model
|
||||
self.file_view = BigText(model)
|
||||
self.filter_hit_view = FilterWidget(self._model)
|
||||
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)
|
||||
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
splitter = QSplitter()
|
||||
splitter.setOrientation(Qt.Orientation.Vertical)
|
||||
splitter.setHandleWidth(5)
|
||||
# splitter.setStyleSheet("QSplitter::handle{background: #cccccc;}")
|
||||
splitter.addWidget(self.file_view)
|
||||
splitter.addWidget(self.filter_hit_view)
|
||||
|
||||
self.layout.addWidget(splitter)
|
||||
|
||||
def get_file(self) -> str:
|
||||
return self.file_view.get_file()
|
||||
|
||||
# overriding abstract method
|
||||
def destruct(self):
|
||||
self.file_view.destruct()
|
||||
self.filter_hit_view.destruct()
|
||||
|
||||
# overriding abstract method
|
||||
def get_status_text(self) -> str:
|
||||
file = self._model.get_file()
|
||||
file_size = humanbytes(self._model.byte_count())
|
||||
return "%s - %s" % (file_size, file)
|
||||
Reference in New Issue
Block a user