diff --git a/README.md b/README.md index 8d108438ee9bb92b2e85b01e6917344a5d76dc07..5841602bc890de1008bfda4da24e9e40ee060d3a 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [](https://pypi.python.org/pypi/cutelog) This is a graphical log viewer for Python's standard logging module. -It can be targeted as a SocketHandler with no additional setup (see [Usage](#usage)). +It can be targeted with a SocketHandler with no additional setup (see [Usage](#usage)). The program is in beta: it's lacking some features and may be unstable, but it works. cutelog is cross-platform, although it's mainly written and optimized for Linux. @@ -45,14 +45,16 @@ $ pip install git+https://github.com/busimus/cutelog.git 2. Put the following into your code: ```python import logging -import logging.handlers +from logging.handlers import SocketHandler -log = logging.getLogger('MyLogger') +log = logging.getLogger('Root logger') log.setLevel(1) # to send all messages to cutelog -socket_handler = logging.handlers.SocketHandler('127.0.0.1', 19996) # default listening address +socket_handler = SocketHandler('127.0.0.1', 19996) # default listening address log.addHandler(socket_handler) log.info('Hello world!') ``` +Afterwards it's recommended to designate different loggers for different parts of your program with `log_2 = log.getChild("Child logger")`. +This will create "log namespaces" which allow you to filter out messages from various subsystems of your program. ## Planned features * [ ] Indication that the connection has been closed diff --git a/README.rst b/README.rst index fafc8f0c7a01de9c91c07be90de63e387318c256..bba07ec006fbec8d818b8a6b4bcabb2beaa1f9e2 100644 --- a/README.rst +++ b/README.rst @@ -6,7 +6,7 @@ cutelog ======= This is a graphical log viewer for Python's standard logging module. -It can be targeted as a SocketHandler with no additional setup (see Usage_). +It can be targeted with a SocketHandler with no additional setup (see Usage_). The program is in beta: it's lacking some features and may be unstable, but it works. cutelog is cross-platform, although it's mainly written and optimized for Linux. @@ -52,12 +52,15 @@ Usage import logging from logging.handlers import SocketHandler - log = logging.getLogger('MyLogger') + log = logging.getLogger('Root logger') log.setLevel(1) # to send all messages to cutelog socket_handler = SocketHandler('127.0.0.1', 19996) # default listening address log.addHandler(socket_handler) log.info('Hello world!') +Afterwards it's recommended to designate different loggers for different parts of your program with `log_2 = log.getChild("Child logger")`. +This will create "log namespaces" which allow you to filter out messages from various subsystems of your program. + Code, issues, changelog ======================= Visit the project's `GitHub page <https://github.com/busimus/cutelog>`_. diff --git a/cutelog/config.py b/cutelog/config.py index 603d884d3d5b9e81147ae21cb4f5fffe8ce455bc..462aa1aa70428ee6f74c5c42d017634837d05f69 100644 --- a/cutelog/config.py +++ b/cutelog/config.py @@ -28,12 +28,12 @@ else: Option = namedtuple('Option', ['name', 'type', 'default']) OPTION_SPEC = ( # Appearance - ('dark_theme_default', bool, False), - ('logger_table_font', str, DEFAULT_FONT), - ('logger_table_font_size', int, 9), - ('text_view_dialog_font', str, 'Courier New'), - ('text_view_dialog_font_size', int, 12), - ('logger_row_height', int, 20), + ('dark_theme_default', bool, False), + ('logger_table_font', str, DEFAULT_FONT), + ('logger_table_font_size', int, 9), + ('text_view_dialog_font', str, 'Courier New'), + ('text_view_dialog_font_size', int, 12), + ('logger_row_height', int, 20), # Search ('search_open_default', bool, False), @@ -42,16 +42,16 @@ OPTION_SPEC = ( ('search_wildcard_default', bool, False), # Server - ('listen_host', str, '0.0.0.0'), - ('listen_port', int, 19996), - ('one_tab_mode', bool, False), + ('listen_host', str, '0.0.0.0'), + ('listen_port', int, 19996), + ('single_tab_mode_default', bool, False), # Advanced - ('console_logging_level', int, 30), - ('loop_event_delay', float, 0.005), - ('benchmark', bool, False), - ('benchmark_interval', float, 0.0005), - ('light_theme_is_native', bool, False), + ('console_logging_level', int, 30), + ('loop_event_delay', float, 0.005), + ('benchmark', bool, False), + ('benchmark_interval', float, 0.0005), + ('light_theme_is_native', bool, False), ) @@ -96,6 +96,15 @@ class Config(QObject): # self.log.debug('Returning "{}"'.format(value)) return value + def __setitem__(self, name, value): + # self.log.debug('Setting "{}"'.format(name)) + if name not in self.options: + raise Exception('No option with name "{}"'.format(name)) + self.options[name] = value + + def set_option(self, name, value): + self[name] = value + @staticmethod def get_resource_path(name, directory='ui'): data_dir = resource_filename('cutelog', directory) diff --git a/cutelog/listener.py b/cutelog/listener.py index 4638ee702bad5101033f8e24f2ba990169df96c4..00ebb8d24777ccb1a307cff3c68d8c859cd61bdd 100644 --- a/cutelog/listener.py +++ b/cutelog/listener.py @@ -33,6 +33,8 @@ class LogServer(QTcpServer): if self.benchmark: self.log.debug('Starting a benchmark connection') new_conn = BenchmarkConnection(self, None, "benchmark", self.stop_signal, self.log) + new_conn.finished.connect(new_conn.deleteLater) + new_conn.connection_finished.connect(self.cleanup_connection) self.on_connection(new_conn, "benchmark") self.threads.append(new_conn) new_conn.start() @@ -100,8 +102,9 @@ class LogConnection(QThread): self.tab_closed = False # used to stop the connection from a "parent" logger def __repr__(self): - return "{}(name={}, socketDescriptor={})".format(self.__class__.__name__, self.name, - self.socketDescriptor) + # return "{}(name={}, socketDescriptor={})".format(self.__class__.__name__, self.name, + # self.socketDescriptor) + return "{}(name={})".format(self.__class__.__name__, self.name) def run(self): self.log.debug('Connection "{}" is starting'.format(self.name)) @@ -188,3 +191,4 @@ class BenchmarkConnection(LogConnection): c += 1 time.sleep(CONFIG.benchmark_interval) self.connection_finished.emit(self) + self.log.debug('Connection "{}" has stopped'.format(self.name)) diff --git a/cutelog/main_window.py b/cutelog/main_window.py index 73646b80d36624b8c5261f01c765076bf0f9eaec..017136dc80134aab0dccb22b41aa430aee3cd6ef 100644 --- a/cutelog/main_window.py +++ b/cutelog/main_window.py @@ -1,4 +1,5 @@ import asyncio +from functools import partial from PyQt5 import uic from PyQt5.QtCore import Qt, QFile, QTextStream @@ -30,6 +31,7 @@ class MainWindow(*MainWindowBase): self.stop_signal = asyncio.Event() self.finished = asyncio.Event() self.dark_theme = CONFIG['dark_theme_default'] + self.single_tab_mode = CONFIG['single_tab_mode_default'] self.loggers_by_name = {} # name -> LoggerTab @@ -44,34 +46,15 @@ class MainWindow(*MainWindowBase): def setupUi(self): super().setupUi(self) + self.setWindowTitle('cutelog') + self.setup_menubar() + self.setup_action_triggers() self.setup_shortcuts() self.connectionTabWidget.tabCloseRequested.connect(self.close_tab) - self.setWindowTitle('cutelog') - - self.actionQuit.triggered.connect(self.shutdown) - - self.actionRenameTab.triggered.connect(self.rename_tab) - self.actionCloseTab.triggered.connect(self.close_current_tab) - - self.actionPopOut.triggered.connect(self.pop_out_tab) - self.actionPopIn.triggered.connect(self.pop_in_tabs_dialog) - self.actionDarkTheme.toggled.connect(self.toggle_dark_theme) - - # self.actionReloadStyle.triggered.connect(self.reload_stylesheet) - self.actionRestartServer.triggered.connect(self.restart_server) - self.actionStartStopServer.triggered.connect(self.start_or_stop_server) - - self.actionAbout.triggered.connect(self.about_dialog) - self.actionSettings.triggered.connect(self.settings_dialog) - self.actionMergeTabs.triggered.connect(self.merge_tabs_dialog) - self.actionTrimTabRecords.triggered.connect(self.trim_records_dialog) - self.actionSetMaxCapacity.triggered.connect(self.max_capacity_dialog) - self.reload_stylesheet() - self.restore_geometry() self.show() @@ -83,9 +66,12 @@ class MainWindow(*MainWindowBase): # File menu self.menuFile = self.menubar.addMenu("File") - self.actionDarkTheme = self.menuFile.addAction('Dark Theme') + self.actionDarkTheme = self.menuFile.addAction('Dark theme') self.actionDarkTheme.setCheckable(True) self.actionDarkTheme.setChecked(self.dark_theme) + self.actionSingleTab = self.menuFile.addAction('Single tab mode') + self.actionSingleTab.setCheckable(True) + self.actionSingleTab.setChecked(self.single_tab_mode) # self.actionReloadStyle = self.menuFile.addAction('Reload style') self.actionSettings = self.menuFile.addAction('Settings') self.menuFile.addSeparator() @@ -113,6 +99,27 @@ class MainWindow(*MainWindowBase): self.menuHelp = self.menubar.addMenu("Help") self.actionAbout = self.menuHelp.addAction("About cutelog") + def setup_action_triggers(self): + self.actionQuit.triggered.connect(self.shutdown) + self.actionSingleTab.triggered.connect(partial(setattr, self, 'single_tab_mode')) + + self.actionRenameTab.triggered.connect(self.rename_tab_dialog) + self.actionCloseTab.triggered.connect(self.close_current_tab) + + self.actionPopOut.triggered.connect(self.pop_out_tab) + self.actionPopIn.triggered.connect(self.pop_in_tabs_dialog) + self.actionDarkTheme.toggled.connect(self.toggle_dark_theme) + + # self.actionReloadStyle.triggered.connect(self.reload_stylesheet) + self.actionRestartServer.triggered.connect(self.restart_server) + self.actionStartStopServer.triggered.connect(self.start_or_stop_server) + + self.actionAbout.triggered.connect(self.about_dialog) + self.actionSettings.triggered.connect(self.settings_dialog) + self.actionMergeTabs.triggered.connect(self.merge_tabs_dialog) + self.actionTrimTabRecords.triggered.connect(self.trim_records_dialog) + self.actionSetMaxCapacity.triggered.connect(self.max_capacity_dialog) + def setup_shortcuts(self): self.actionQuit.setShortcut('Ctrl+Q') self.actionDarkTheme.setShortcut('Ctrl+S') @@ -202,9 +209,11 @@ class MainWindow(*MainWindowBase): def on_connection(self, conn, name): self.log.debug('New connection: "{}"'.format(name)) - one_tab_mode = CONFIG['one_tab_mode'] and len(self.loggers_by_name) > 0 - if one_tab_mode: + # self.single_tab_mode is ignored if there are 0 tabs currently + single_tab_mode = self.single_tab_mode and len(self.loggers_by_name) > 0 + + if single_tab_mode: new_logger = list(self.loggers_by_name.values())[0] new_logger.add_connection(conn) else: @@ -215,7 +224,7 @@ class MainWindow(*MainWindowBase): conn.new_record.connect(new_logger.on_record) conn.connection_finished.connect(new_logger.remove_connection) - if not one_tab_mode: + if not single_tab_mode: self.connectionTabWidget.addTab(new_logger, name) self.loggers_by_name[name] = new_logger @@ -243,19 +252,6 @@ class MainWindow(*MainWindowBase): self.stop_reason = 'restart' self.stop_signal.set() - # async def wait_server_closed(self): - # self.log.debug('Waiting for the server to close') - # self.actionRestartServer.setText('Stopping the server...') - # self.actionRestartServer.setEnabled(False) - # try: - # await asyncio.wait_for(self.server.wait_server_closed(), timeout=10) - # except asyncio.TimeoutError as e: - # self.log.error('Waiting for the server to close timed out after 10 seconds') - # else: - # self.log.debug('Waiting for server to close finished') - # self.actionRestartServer.setText('Restart server') - # self.actionRestartServer.setEnabled(True) - def start_or_stop_server(self): if self.server_running: self.stop_reason = 'pause' @@ -268,7 +264,7 @@ class MainWindow(*MainWindowBase): def set_status(self, string): self.statusBar().showMessage(string) - def rename_tab(self): + def rename_tab_dialog(self): logger, index = self.current_logger_and_index() if not logger: return @@ -276,10 +272,10 @@ class MainWindow(*MainWindowBase): d = QInputDialog(self) d.setLabelText('Enter the new name for the "{}" tab:'.format(logger.name)) d.setWindowTitle('Rename the "{}" tab'.format(logger.name)) - d.textValueSelected.connect(self.change_current_tab_name) + d.textValueSelected.connect(self.rename_current_tab) d.open() - def change_current_tab_name(self, new_name): + def rename_current_tab(self, new_name): logger, index = self.current_logger_and_index() if new_name in self.loggers_by_name and new_name != logger.name: show_warning_dialog(self, "Rename error", diff --git a/cutelog/resources/ui/settings_dialog.ui b/cutelog/resources/ui/settings_dialog.ui index 5e230b9280d5f9505a9b29d86f6fcb5d869094b4..6fbd967de045e5d386b20a8dbcadf67e5f7f8f39 100644 --- a/cutelog/resources/ui/settings_dialog.ui +++ b/cutelog/resources/ui/settings_dialog.ui @@ -293,12 +293,12 @@ </widget> </item> <item row="2" column="0"> - <widget class="QLabel" name="oneTabLabel"> + <widget class="QLabel" name="singleTabLabel"> <property name="text"> - <string>One &tab for all connections</string> + <string>Single &tab mode by default</string> </property> <property name="buddy"> - <cstring>oneTabCheckBox</cstring> + <cstring>singleTabCheckBox</cstring> </property> </widget> </item> @@ -316,7 +316,7 @@ </spacer> </item> <item row="2" column="1"> - <widget class="QCheckBox" name="oneTabCheckBox"> + <widget class="QCheckBox" name="singleTabCheckBox"> <property name="text"> <string/> </property> diff --git a/cutelog/settings_dialog.py b/cutelog/settings_dialog.py index 5aa2aade34d19a7531c43703c71e722e3a4ead68..0c007638e88e56c24334393ae38a346cb4ee83ad 100644 --- a/cutelog/settings_dialog.py +++ b/cutelog/settings_dialog.py @@ -36,10 +36,10 @@ class SettingsDialog(*SettingsDialogBase): self.benchmarkCheckBox.setToolTip('Has effect after restarting the server, ' '<b>for testing purposes only</b>.') - self.oneTabCheckBox.setToolTip("Forces all connections into one tab. " + self.singleTabCheckBox.setToolTip("Forces all connections into one tab. " "Useful for when you're restarting one " "program very often.") - self.oneTabLabel.setBuddy(self.oneTabCheckBox) # @Hmmm: why doesn't this work? + self.singleTabLabel.setBuddy(self.singleTabCheckBox) # @Hmmm: why doesn't this work? def load_from_config(self): # Appearance page @@ -61,7 +61,7 @@ class SettingsDialog(*SettingsDialogBase): self.listenHostLine.setText(CONFIG['listen_host']) self.listenPortLine.setValidator(QIntValidator(0, 65535, self)) self.listenPortLine.setText(str(CONFIG['listen_port'])) - self.oneTabCheckBox.setChecked(CONFIG['one_tab_mode']) + self.singleTabCheckBox.setChecked(CONFIG['single_tab_mode_default']) # Advanced page self.logLevelLine.setValidator(QIntValidator(0, 1000, self)) @@ -99,7 +99,7 @@ class SettingsDialog(*SettingsDialogBase): o['listen_host'] = self.listenHostLine.text() o['listen_port'] = int(self.listenPortLine.text()) o['console_logging_level'] = int(self.logLevelLine.text()) - o['one_tab_mode'] = self.oneTabCheckBox.isChecked() + o['single_tab_mode_default'] = self.singleTabCheckBox.isChecked() # Advanced o['loop_event_delay'] = float(self.loopEventDelayLine.text())