From 943c2cdef7b03b3dc59559389e5f0727d75c9a8b Mon Sep 17 00:00:00 2001 From: jubnl Date: Sat, 30 Mar 2024 18:44:13 +0100 Subject: [PATCH 1/7] WIP --- debug/debug_console_v2.py | 349 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 349 insertions(+) create mode 100644 debug/debug_console_v2.py diff --git a/debug/debug_console_v2.py b/debug/debug_console_v2.py new file mode 100644 index 00000000..66ede85d --- /dev/null +++ b/debug/debug_console_v2.py @@ -0,0 +1,349 @@ +import socket +import threading +import tkinter as tk +from datetime import datetime + +log_levels = { + "TRACE": 0, + "DEBUG": 1, + "INFO ": 2, + "WARN ": 3, + "ERROR": 4, + "FATAL": 5 +} + + +class Log: + def __init__(self, log: str): + self.timestamp_str = datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f') + self.log_level = "DEBUG" + self.logger = "DefaultLogger" + self.log_str = "" + self.parse_error = False + log_parts = log.split(" :: ") + if len(log_parts) == 3: + self.log_level = log_parts[0] + self.logger = log_parts[1] + self.log_str = log_parts[2] + else: + self.parse_error = True + self.log_str = log + + def __str__(self): + if not self.parse_error: + return f"{self.timestamp_str} :: {self.log_level} :: {self.logger} :: {self.log_str}\n" + return f"{self.timestamp_str} :: {self.log_str}\n" + + +class PlaceholderEntry(tk.Entry): + def __init__(self, *args, placeholder="", **kwargs): + super().__init__(*args, **kwargs) + self.placeholder = placeholder + self.user_has_interacted = False + self.insert(0, self.placeholder) + self.config(fg='grey') + self.bind('', self.on_focus_in) + self.bind('', self.on_focus_out) + self.bind('', self.handle_ctrl_backspace) + self.bind('', self.on_key_press) # Bind key press event + + def on_focus_in(self, event): + if not self.user_has_interacted and self.get() == self.placeholder: + self.delete(0, 'end') + self.config(fg='black') + + def on_focus_out(self, event): + if not self.get(): + self.insert(0, self.placeholder) + self.config(fg='grey') + self.user_has_interacted = False # Reset flag if entry is empty + + def on_key_press(self, event): + self.user_has_interacted = True # User has interacted when any key is pressed + + def reset_interaction_flag(self): + self.user_has_interacted = False + + def handle_ctrl_backspace(self, event: tk.Event): + # Get the current content of the entry and the cursor position + content = self.get() + cursor_pos = self.index(tk.INSERT) + # Find the start of the word to the left of the cursor + pre_cursor = content[:cursor_pos] + + # If the last character before the cursor is a space, delete it + if len(pre_cursor) > 0 and pre_cursor[-1] == ' ': + self.delete(cursor_pos - 1, tk.INSERT) + return "break" # Prevent default behavior + + word_start = pre_cursor.rfind(' ') + 1 if ' ' in pre_cursor else 0 + # Delete the word + self.delete(f"{word_start}", tk.INSERT) + return "break" # Prevent default behavior + + +class OptionsFrame(tk.Frame): + def __init__(self, parent): + super().__init__(parent) + self.global_search_frame = GlobalSearchFrame(self, parent) + self.specific_search_frame = SpecificSearchFrame(self, parent) + self.create_widgets() + + def inject_console(self, console): + self.global_search_frame.inject_console(console) + self.specific_search_frame.inject_console(console) + + def create_widgets(self): + self.global_search_frame.pack() + self.specific_search_frame.pack() + + +class GlobalSearchFrame(tk.Frame): + def __init__(self, parent, root): + super().__init__(parent) + + self.after_id = None + self.root = root + self.console = None + + # Global search entry + self.search_entry_placeholder = "Search" + self.search_entry_var = tk.StringVar() + self.search_entry = PlaceholderEntry( + self, + placeholder=self.search_entry_placeholder, + textvariable=self.search_entry_var + ) + self.search_entry_var.trace("w", self.on_entry_changed) + self.search_entry.bind('', lambda event: self.console.text_widget.focus()) + self.search_entry.config(fg='grey') + + self.create_widgets() + + def inject_console(self, console): + self.console = console + + def create_widgets(self): + self.search_entry.pack() + + def on_entry_changed(self, *args): + if self.after_id: + self.root.after_cancel(self.after_id) + self.after_id = self.root.after(300, self.apply_search_entry_var) + + def apply_search_entry_var(self): + self.console.set_filter(global_search_str=self.search_entry_var.get()) + self.after_id = None + + +class Console(tk.Frame): + def __init__(self, parent, option_frame: OptionsFrame): + super().__init__(parent) + self.all_logs = [] + self.shown_logs = [] + self.option_frame = option_frame + self.text_widget = tk.Text(self) + self.scrollbar = tk.Scrollbar(self, command=self.text_widget.yview) + self.global_search_str = "" + self.logger_name = "" + self.log_level = "TRACE" + self.and_above = True + self.create_widgets() + + def create_widgets(self): + self.text_widget.pack(side=tk.LEFT, expand=True, fill='both') + self.scrollbar.pack(side=tk.RIGHT, fill='y') + self.text_widget.config(yscrollcommand=self.scrollbar.set) + + def set_filter( + self, + global_search_str: str = None, + logger_name: str = None, + log_level: str = None, + and_above: bool = None + ): + if global_search_str is not None and self.option_frame.global_search_frame.search_entry.user_has_interacted: + self.global_search_str = global_search_str + elif global_search_str is None or not self.option_frame.global_search_frame.search_entry.user_has_interacted: + self.global_search_str = "" + + if logger_name is not None and self.option_frame.specific_search_frame.logger_entry.user_has_interacted: + self.logger_name = logger_name + elif logger_name is None or not self.option_frame.specific_search_frame.logger_entry.user_has_interacted: + self.logger_name = "" + + if log_level is not None: + self.log_level = log_level + + if and_above is not None: + self.and_above = and_above + + self.apply_filters() + + def append_log(self, log: str): + log_obj = Log(log) + self.all_logs.append(log_obj) + if self.filter_log(log_obj): + self.shown_logs.append(log_obj) + # Check if the user is at the end before appending + at_end = self.text_widget.yview()[1] == 1.0 + self.text_widget.insert(tk.END, str(log_obj)) + if at_end: + self.text_widget.see(tk.END) + + def clear_logs(self): + self.text_widget.delete('1.0', tk.END) + self.shown_logs.clear() + self.all_logs.clear() + self.apply_filters() + + def apply_filters(self): + # Re-filter all logs and update the text widget only if necessary + filtered_logs = [log for log in self.all_logs if self.filter_log(log)] + if filtered_logs != self.shown_logs: + self.shown_logs = filtered_logs + self.update_text_widget() + + def filter_log(self, log): + if self.and_above: + flag = log_levels[log.log_level] >= log_levels[self.log_level] + else: + flag = log.log_level == self.log_level + + if self.logger_name: + flag = flag and self.logger_name in log.logger + + return flag + + def update_text_widget(self): + # Preserve the current view position unless at the end + at_end = self.text_widget.yview()[1] == 1.0 + self.text_widget.delete('1.0', tk.END) + for log in self.shown_logs: + self.text_widget.insert(tk.END, str(log)) + if at_end: + self.text_widget.see(tk.END) + + +class SpecificSearchFrame(tk.Frame): + def __init__(self, parent, root): + super().__init__(parent) + self.root = root + self.after_id = None + self.console = None + + # Logger name entry + self.logger_entry_placeholder = "Logger Name" + self.logger_entry_var = tk.StringVar() + self.logger_entry = PlaceholderEntry( + self, + placeholder=self.logger_entry_placeholder, + textvariable=self.logger_entry_var + ) + self.logger_entry_var.trace("w", self.on_entry_changed) + self.logger_entry.bind('', lambda event: self.console.text_widget.focus()) + self.logger_entry.config(fg='grey') + + # Log level dropdown + self.log_level_dropdown_var = tk.StringVar() + self.log_level_dropdown_var.set("TRACE") + self.log_level_dropdown = tk.OptionMenu( + self, + self.log_level_dropdown_var, + *log_levels.keys() + ) + self.log_level_dropdown_var.trace( + "w", + lambda *args: self.console.set_filter(log_level=self.log_level_dropdown_var.get()) + ) + + # And above checkbox + self.and_above_var = tk.BooleanVar() + self.and_above_var.set(True) + self.and_above_checkbox = tk.Checkbutton( + self, + text="And above", + variable=self.and_above_var, + onvalue=True, + offvalue=False, + command=lambda: self.console.set_filter(and_above=self.and_above_var.get()) + ) + + self.clear_log_button: tk.Button | None = None + + self.create_widgets() + + def inject_console(self, console): + self.console = console + self.clear_log_button = tk.Button( + self, + text="Clear Logs", + command=self.console.clear_logs + ) + self.clear_log_button.pack() + + def create_widgets(self): + self.logger_entry.pack() + self.log_level_dropdown.pack() + self.and_above_checkbox.pack() + + def on_entry_changed(self, *args): + if self.after_id: + self.root.after_cancel(self.after_id) + self.after_id = self.root.after(250, self.apply_logger_entry_var) + + def apply_logger_entry_var(self): + self.console.set_filter(logger_name=self.logger_entry_var.get()) + self.after_id = None + + +class MainWindow(tk.Tk): + def __init__(self): + super().__init__() + self.title("Steamodded Debug Console") + self.options_frame = OptionsFrame(self) + self.console = Console(self, self.options_frame) + self.options_frame.inject_console(self.console) + self.create_widgets() + + self.bind('', self.focus_search) + self.bind('', self.focus_search) + + def create_widgets(self): + self.console.pack(expand=True, fill='both') + self.options_frame.pack() + + def get_console(self): + return self.console + + def focus_search(self, event): + self.options_frame.global_search_frame.search_entry.focus() + + +def client_handler(client_socket, console: Console): + while True: + # Traceback can fit in a single log now + data = client_socket.recv(8192) + if not data: + break + + decoded_data = data.decode() + logs = decoded_data.split("ENDOFLOG") + for log in logs: + if log: + console.append_log(log) + + +def listen_for_clients(console: Console): + server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server_socket.bind(('localhost', 12345)) + server_socket.listen() + while True: + client, addr = server_socket.accept() + threading.Thread(target=client_handler, args=(client, console)).start() + + +if __name__ == "__main__": + root = MainWindow() + threading.Thread(target=listen_for_clients, daemon=True, args=(root.get_console(),)).start() + root.mainloop() From b8b3034ee331999b606348d2098597fc52d1b797 Mon Sep 17 00:00:00 2001 From: jubnl Date: Sat, 30 Mar 2024 19:13:00 +0100 Subject: [PATCH 2/7] reformat the buttons at the bottom of the screen --- debug/debug_console_v2.py | 40 ++++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/debug/debug_console_v2.py b/debug/debug_console_v2.py index 66ede85d..71725788 100644 --- a/debug/debug_console_v2.py +++ b/debug/debug_console_v2.py @@ -94,8 +94,8 @@ def inject_console(self, console): self.specific_search_frame.inject_console(console) def create_widgets(self): - self.global_search_frame.pack() - self.specific_search_frame.pack() + self.global_search_frame.pack(side=tk.TOP, fill='x', expand=True) + self.specific_search_frame.pack(side=tk.BOTTOM, fill='x', expand=True) class GlobalSearchFrame(tk.Frame): @@ -118,13 +118,24 @@ def __init__(self, parent, root): self.search_entry.bind('', lambda event: self.console.text_widget.focus()) self.search_entry.config(fg='grey') + self.search_modes = [] + self.search_mode_var = tk.StringVar(value='normal') + self.search_mode_var.trace("w", self.apply_search_mode) + for mode, text in [('normal', 'normal'), ('match_case', 'match case'), ('regex', 'regex')]: + self.search_modes.append(tk.Radiobutton(self, text=text, variable=self.search_mode_var, value=mode)) + self.create_widgets() + def apply_search_mode(self, *args): + self.console.set_filter(global_search_mode=self.search_mode_var.get()) + def inject_console(self, console): self.console = console def create_widgets(self): - self.search_entry.pack() + self.search_entry.pack(side=tk.LEFT, fill='x', expand=True, padx=(5, 0)) + for mode in self.search_modes: + mode.pack(side=tk.LEFT, padx=(5, 0)) def on_entry_changed(self, *args): if self.after_id: @@ -139,6 +150,7 @@ def apply_search_entry_var(self): class Console(tk.Frame): def __init__(self, parent, option_frame: OptionsFrame): super().__init__(parent) + self.global_search_mode = "normal" self.all_logs = [] self.shown_logs = [] self.option_frame = option_frame @@ -158,6 +170,7 @@ def create_widgets(self): def set_filter( self, global_search_str: str = None, + global_search_mode: str = None, logger_name: str = None, log_level: str = None, and_above: bool = None @@ -172,6 +185,9 @@ def set_filter( elif logger_name is None or not self.option_frame.specific_search_frame.logger_entry.user_has_interacted: self.logger_name = "" + if global_search_mode is not None: + self.global_search_mode = global_search_mode + if log_level is not None: self.log_level = log_level @@ -224,6 +240,12 @@ def update_text_widget(self): if at_end: self.text_widget.see(tk.END) + if self.global_search_str: + self.search_text() + + def search_text(self): + pass + class SpecificSearchFrame(tk.Frame): def __init__(self, parent, root): @@ -280,12 +302,12 @@ def inject_console(self, console): text="Clear Logs", command=self.console.clear_logs ) - self.clear_log_button.pack() + self.clear_log_button.pack(side=tk.RIGHT, padx=(5, 0), fill='x', expand=True) def create_widgets(self): - self.logger_entry.pack() - self.log_level_dropdown.pack() - self.and_above_checkbox.pack() + self.logger_entry.pack(side=tk.LEFT, fill='x', expand=True, padx=(5, 0)) + self.log_level_dropdown.pack(side=tk.LEFT, padx=(5, 0), fill='x', expand=True) + self.and_above_checkbox.pack(side=tk.LEFT, padx=(5, 0), fill='x', expand=True) def on_entry_changed(self, *args): if self.after_id: @@ -310,8 +332,8 @@ def __init__(self): self.bind('', self.focus_search) def create_widgets(self): - self.console.pack(expand=True, fill='both') - self.options_frame.pack() + self.console.pack(side=tk.TOP,expand=True, fill='both') + self.options_frame.pack(side=tk.BOTTOM, fill='x', expand=False) def get_console(self): return self.console From dc660d1726119e7f9839ff9ac559300793be8f89 Mon Sep 17 00:00:00 2001 From: jubnl Date: Sat, 30 Mar 2024 20:59:34 +0100 Subject: [PATCH 3/7] implement search functionnality --- debug/debug_console_v2.py | 76 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 71 insertions(+), 5 deletions(-) diff --git a/debug/debug_console_v2.py b/debug/debug_console_v2.py index 71725788..f05b4fc9 100644 --- a/debug/debug_console_v2.py +++ b/debug/debug_console_v2.py @@ -1,3 +1,4 @@ +import re import socket import threading import tkinter as tk @@ -136,6 +137,7 @@ def create_widgets(self): self.search_entry.pack(side=tk.LEFT, fill='x', expand=True, padx=(5, 0)) for mode in self.search_modes: mode.pack(side=tk.LEFT, padx=(5, 0)) + self.search_entry.bind('', lambda event: self.console.next_occurrence()) def on_entry_changed(self, *args): if self.after_id: @@ -216,9 +218,8 @@ def clear_logs(self): def apply_filters(self): # Re-filter all logs and update the text widget only if necessary filtered_logs = [log for log in self.all_logs if self.filter_log(log)] - if filtered_logs != self.shown_logs: - self.shown_logs = filtered_logs - self.update_text_widget() + self.shown_logs = filtered_logs + self.update_text_widget() def filter_log(self, log): if self.and_above: @@ -244,7 +245,72 @@ def update_text_widget(self): self.search_text() def search_text(self): - pass + self.text_widget.tag_remove('found', '1.0', tk.END) + search_query = self.global_search_str.strip() + if not search_query: + return + + if self.global_search_mode == 'match_case': + pattern = re.escape(search_query) + elif self.global_search_mode == 'regex': + # Directly use the user input for regex, but be cautious of Tkinter's limited regex support + pattern = search_query + else: # normal mode, make it case-insensitive + pattern = '(?i)' + re.escape(search_query) # Add (?i) for case-insensitive search in Tkinter + + start = '1.0' + while True: + match_start = self.text_widget.search(pattern, start, tk.END, regexp=True) + if not match_start: + break + match_end = f"{match_start}+{len(search_query)}c" + self.text_widget.tag_add('found', match_start, match_end) + start = match_end + + self.text_widget.tag_config('found', background='yellow') + if at_end := self.text_widget.yview()[1] == 1.0: + first_occurrence = self.text_widget.tag_ranges('found') + if first_occurrence: + self.text_widget.see(first_occurrence[0]) + self.next_occurrence() + + def next_occurrence(self): + current_tags = self.text_widget.tag_ranges('found') + if not current_tags: + return + + # Ensure the 'current_found' tag exists with a blue background. + self.text_widget.tag_config('current_found', background='#ADD8E6') + + # Get the current position of the cursor in the text widget. + cursor_index = self.text_widget.index(tk.INSERT) + + # Remove the 'current_found' tag from the entire text widget. + self.text_widget.tag_remove('current_found', '1.0', tk.END) + + # Convert the current cursor index to a comparable value. + cursor_line, cursor_char = map(int, cursor_index.split('.')) + + for i in range(0, len(current_tags), 2): + tag_start = current_tags[i] + tag_end = current_tags[i + 1] + + # Convert tag start index to comparable values. + tag_start_line, tag_start_char = map(int, str(tag_start).split('.')) + + # Check if the tag start is greater than the cursor position. + if tag_start_line > cursor_line or (tag_start_line == cursor_line and tag_start_char > cursor_char): + self.text_widget.mark_set(tk.INSERT, tag_start) + self.text_widget.see(tag_start) + + # Apply the 'current_found' tag to the current occurrence. + self.text_widget.tag_add('current_found', tag_start, tag_end) + break + else: + # Wrap to the first tag if no next tag is found. + self.text_widget.mark_set(tk.INSERT, str(current_tags[0])) + self.text_widget.see(str(current_tags[0])) + self.text_widget.tag_add('current_found', current_tags[0], current_tags[1]) class SpecificSearchFrame(tk.Frame): @@ -332,7 +398,7 @@ def __init__(self): self.bind('', self.focus_search) def create_widgets(self): - self.console.pack(side=tk.TOP,expand=True, fill='both') + self.console.pack(side=tk.TOP, expand=True, fill='both') self.options_frame.pack(side=tk.BOTTOM, fill='x', expand=False) def get_console(self): From cc1822cf5f95bffaf9f9c17653b06220a6cba8cc Mon Sep 17 00:00:00 2001 From: jubnl Date: Sat, 30 Mar 2024 21:00:14 +0100 Subject: [PATCH 4/7] transfer debug console to proper file --- debug/debug_console_v2.py | 437 --------------------------------- debug/tk_debug_window.py | 505 ++++++++++++++++++++++++++++++-------- 2 files changed, 409 insertions(+), 533 deletions(-) delete mode 100644 debug/debug_console_v2.py diff --git a/debug/debug_console_v2.py b/debug/debug_console_v2.py deleted file mode 100644 index f05b4fc9..00000000 --- a/debug/debug_console_v2.py +++ /dev/null @@ -1,437 +0,0 @@ -import re -import socket -import threading -import tkinter as tk -from datetime import datetime - -log_levels = { - "TRACE": 0, - "DEBUG": 1, - "INFO ": 2, - "WARN ": 3, - "ERROR": 4, - "FATAL": 5 -} - - -class Log: - def __init__(self, log: str): - self.timestamp_str = datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f') - self.log_level = "DEBUG" - self.logger = "DefaultLogger" - self.log_str = "" - self.parse_error = False - log_parts = log.split(" :: ") - if len(log_parts) == 3: - self.log_level = log_parts[0] - self.logger = log_parts[1] - self.log_str = log_parts[2] - else: - self.parse_error = True - self.log_str = log - - def __str__(self): - if not self.parse_error: - return f"{self.timestamp_str} :: {self.log_level} :: {self.logger} :: {self.log_str}\n" - return f"{self.timestamp_str} :: {self.log_str}\n" - - -class PlaceholderEntry(tk.Entry): - def __init__(self, *args, placeholder="", **kwargs): - super().__init__(*args, **kwargs) - self.placeholder = placeholder - self.user_has_interacted = False - self.insert(0, self.placeholder) - self.config(fg='grey') - self.bind('', self.on_focus_in) - self.bind('', self.on_focus_out) - self.bind('', self.handle_ctrl_backspace) - self.bind('', self.on_key_press) # Bind key press event - - def on_focus_in(self, event): - if not self.user_has_interacted and self.get() == self.placeholder: - self.delete(0, 'end') - self.config(fg='black') - - def on_focus_out(self, event): - if not self.get(): - self.insert(0, self.placeholder) - self.config(fg='grey') - self.user_has_interacted = False # Reset flag if entry is empty - - def on_key_press(self, event): - self.user_has_interacted = True # User has interacted when any key is pressed - - def reset_interaction_flag(self): - self.user_has_interacted = False - - def handle_ctrl_backspace(self, event: tk.Event): - # Get the current content of the entry and the cursor position - content = self.get() - cursor_pos = self.index(tk.INSERT) - # Find the start of the word to the left of the cursor - pre_cursor = content[:cursor_pos] - - # If the last character before the cursor is a space, delete it - if len(pre_cursor) > 0 and pre_cursor[-1] == ' ': - self.delete(cursor_pos - 1, tk.INSERT) - return "break" # Prevent default behavior - - word_start = pre_cursor.rfind(' ') + 1 if ' ' in pre_cursor else 0 - # Delete the word - self.delete(f"{word_start}", tk.INSERT) - return "break" # Prevent default behavior - - -class OptionsFrame(tk.Frame): - def __init__(self, parent): - super().__init__(parent) - self.global_search_frame = GlobalSearchFrame(self, parent) - self.specific_search_frame = SpecificSearchFrame(self, parent) - self.create_widgets() - - def inject_console(self, console): - self.global_search_frame.inject_console(console) - self.specific_search_frame.inject_console(console) - - def create_widgets(self): - self.global_search_frame.pack(side=tk.TOP, fill='x', expand=True) - self.specific_search_frame.pack(side=tk.BOTTOM, fill='x', expand=True) - - -class GlobalSearchFrame(tk.Frame): - def __init__(self, parent, root): - super().__init__(parent) - - self.after_id = None - self.root = root - self.console = None - - # Global search entry - self.search_entry_placeholder = "Search" - self.search_entry_var = tk.StringVar() - self.search_entry = PlaceholderEntry( - self, - placeholder=self.search_entry_placeholder, - textvariable=self.search_entry_var - ) - self.search_entry_var.trace("w", self.on_entry_changed) - self.search_entry.bind('', lambda event: self.console.text_widget.focus()) - self.search_entry.config(fg='grey') - - self.search_modes = [] - self.search_mode_var = tk.StringVar(value='normal') - self.search_mode_var.trace("w", self.apply_search_mode) - for mode, text in [('normal', 'normal'), ('match_case', 'match case'), ('regex', 'regex')]: - self.search_modes.append(tk.Radiobutton(self, text=text, variable=self.search_mode_var, value=mode)) - - self.create_widgets() - - def apply_search_mode(self, *args): - self.console.set_filter(global_search_mode=self.search_mode_var.get()) - - def inject_console(self, console): - self.console = console - - def create_widgets(self): - self.search_entry.pack(side=tk.LEFT, fill='x', expand=True, padx=(5, 0)) - for mode in self.search_modes: - mode.pack(side=tk.LEFT, padx=(5, 0)) - self.search_entry.bind('', lambda event: self.console.next_occurrence()) - - def on_entry_changed(self, *args): - if self.after_id: - self.root.after_cancel(self.after_id) - self.after_id = self.root.after(300, self.apply_search_entry_var) - - def apply_search_entry_var(self): - self.console.set_filter(global_search_str=self.search_entry_var.get()) - self.after_id = None - - -class Console(tk.Frame): - def __init__(self, parent, option_frame: OptionsFrame): - super().__init__(parent) - self.global_search_mode = "normal" - self.all_logs = [] - self.shown_logs = [] - self.option_frame = option_frame - self.text_widget = tk.Text(self) - self.scrollbar = tk.Scrollbar(self, command=self.text_widget.yview) - self.global_search_str = "" - self.logger_name = "" - self.log_level = "TRACE" - self.and_above = True - self.create_widgets() - - def create_widgets(self): - self.text_widget.pack(side=tk.LEFT, expand=True, fill='both') - self.scrollbar.pack(side=tk.RIGHT, fill='y') - self.text_widget.config(yscrollcommand=self.scrollbar.set) - - def set_filter( - self, - global_search_str: str = None, - global_search_mode: str = None, - logger_name: str = None, - log_level: str = None, - and_above: bool = None - ): - if global_search_str is not None and self.option_frame.global_search_frame.search_entry.user_has_interacted: - self.global_search_str = global_search_str - elif global_search_str is None or not self.option_frame.global_search_frame.search_entry.user_has_interacted: - self.global_search_str = "" - - if logger_name is not None and self.option_frame.specific_search_frame.logger_entry.user_has_interacted: - self.logger_name = logger_name - elif logger_name is None or not self.option_frame.specific_search_frame.logger_entry.user_has_interacted: - self.logger_name = "" - - if global_search_mode is not None: - self.global_search_mode = global_search_mode - - if log_level is not None: - self.log_level = log_level - - if and_above is not None: - self.and_above = and_above - - self.apply_filters() - - def append_log(self, log: str): - log_obj = Log(log) - self.all_logs.append(log_obj) - if self.filter_log(log_obj): - self.shown_logs.append(log_obj) - # Check if the user is at the end before appending - at_end = self.text_widget.yview()[1] == 1.0 - self.text_widget.insert(tk.END, str(log_obj)) - if at_end: - self.text_widget.see(tk.END) - - def clear_logs(self): - self.text_widget.delete('1.0', tk.END) - self.shown_logs.clear() - self.all_logs.clear() - self.apply_filters() - - def apply_filters(self): - # Re-filter all logs and update the text widget only if necessary - filtered_logs = [log for log in self.all_logs if self.filter_log(log)] - self.shown_logs = filtered_logs - self.update_text_widget() - - def filter_log(self, log): - if self.and_above: - flag = log_levels[log.log_level] >= log_levels[self.log_level] - else: - flag = log.log_level == self.log_level - - if self.logger_name: - flag = flag and self.logger_name in log.logger - - return flag - - def update_text_widget(self): - # Preserve the current view position unless at the end - at_end = self.text_widget.yview()[1] == 1.0 - self.text_widget.delete('1.0', tk.END) - for log in self.shown_logs: - self.text_widget.insert(tk.END, str(log)) - if at_end: - self.text_widget.see(tk.END) - - if self.global_search_str: - self.search_text() - - def search_text(self): - self.text_widget.tag_remove('found', '1.0', tk.END) - search_query = self.global_search_str.strip() - if not search_query: - return - - if self.global_search_mode == 'match_case': - pattern = re.escape(search_query) - elif self.global_search_mode == 'regex': - # Directly use the user input for regex, but be cautious of Tkinter's limited regex support - pattern = search_query - else: # normal mode, make it case-insensitive - pattern = '(?i)' + re.escape(search_query) # Add (?i) for case-insensitive search in Tkinter - - start = '1.0' - while True: - match_start = self.text_widget.search(pattern, start, tk.END, regexp=True) - if not match_start: - break - match_end = f"{match_start}+{len(search_query)}c" - self.text_widget.tag_add('found', match_start, match_end) - start = match_end - - self.text_widget.tag_config('found', background='yellow') - if at_end := self.text_widget.yview()[1] == 1.0: - first_occurrence = self.text_widget.tag_ranges('found') - if first_occurrence: - self.text_widget.see(first_occurrence[0]) - self.next_occurrence() - - def next_occurrence(self): - current_tags = self.text_widget.tag_ranges('found') - if not current_tags: - return - - # Ensure the 'current_found' tag exists with a blue background. - self.text_widget.tag_config('current_found', background='#ADD8E6') - - # Get the current position of the cursor in the text widget. - cursor_index = self.text_widget.index(tk.INSERT) - - # Remove the 'current_found' tag from the entire text widget. - self.text_widget.tag_remove('current_found', '1.0', tk.END) - - # Convert the current cursor index to a comparable value. - cursor_line, cursor_char = map(int, cursor_index.split('.')) - - for i in range(0, len(current_tags), 2): - tag_start = current_tags[i] - tag_end = current_tags[i + 1] - - # Convert tag start index to comparable values. - tag_start_line, tag_start_char = map(int, str(tag_start).split('.')) - - # Check if the tag start is greater than the cursor position. - if tag_start_line > cursor_line or (tag_start_line == cursor_line and tag_start_char > cursor_char): - self.text_widget.mark_set(tk.INSERT, tag_start) - self.text_widget.see(tag_start) - - # Apply the 'current_found' tag to the current occurrence. - self.text_widget.tag_add('current_found', tag_start, tag_end) - break - else: - # Wrap to the first tag if no next tag is found. - self.text_widget.mark_set(tk.INSERT, str(current_tags[0])) - self.text_widget.see(str(current_tags[0])) - self.text_widget.tag_add('current_found', current_tags[0], current_tags[1]) - - -class SpecificSearchFrame(tk.Frame): - def __init__(self, parent, root): - super().__init__(parent) - self.root = root - self.after_id = None - self.console = None - - # Logger name entry - self.logger_entry_placeholder = "Logger Name" - self.logger_entry_var = tk.StringVar() - self.logger_entry = PlaceholderEntry( - self, - placeholder=self.logger_entry_placeholder, - textvariable=self.logger_entry_var - ) - self.logger_entry_var.trace("w", self.on_entry_changed) - self.logger_entry.bind('', lambda event: self.console.text_widget.focus()) - self.logger_entry.config(fg='grey') - - # Log level dropdown - self.log_level_dropdown_var = tk.StringVar() - self.log_level_dropdown_var.set("TRACE") - self.log_level_dropdown = tk.OptionMenu( - self, - self.log_level_dropdown_var, - *log_levels.keys() - ) - self.log_level_dropdown_var.trace( - "w", - lambda *args: self.console.set_filter(log_level=self.log_level_dropdown_var.get()) - ) - - # And above checkbox - self.and_above_var = tk.BooleanVar() - self.and_above_var.set(True) - self.and_above_checkbox = tk.Checkbutton( - self, - text="And above", - variable=self.and_above_var, - onvalue=True, - offvalue=False, - command=lambda: self.console.set_filter(and_above=self.and_above_var.get()) - ) - - self.clear_log_button: tk.Button | None = None - - self.create_widgets() - - def inject_console(self, console): - self.console = console - self.clear_log_button = tk.Button( - self, - text="Clear Logs", - command=self.console.clear_logs - ) - self.clear_log_button.pack(side=tk.RIGHT, padx=(5, 0), fill='x', expand=True) - - def create_widgets(self): - self.logger_entry.pack(side=tk.LEFT, fill='x', expand=True, padx=(5, 0)) - self.log_level_dropdown.pack(side=tk.LEFT, padx=(5, 0), fill='x', expand=True) - self.and_above_checkbox.pack(side=tk.LEFT, padx=(5, 0), fill='x', expand=True) - - def on_entry_changed(self, *args): - if self.after_id: - self.root.after_cancel(self.after_id) - self.after_id = self.root.after(250, self.apply_logger_entry_var) - - def apply_logger_entry_var(self): - self.console.set_filter(logger_name=self.logger_entry_var.get()) - self.after_id = None - - -class MainWindow(tk.Tk): - def __init__(self): - super().__init__() - self.title("Steamodded Debug Console") - self.options_frame = OptionsFrame(self) - self.console = Console(self, self.options_frame) - self.options_frame.inject_console(self.console) - self.create_widgets() - - self.bind('', self.focus_search) - self.bind('', self.focus_search) - - def create_widgets(self): - self.console.pack(side=tk.TOP, expand=True, fill='both') - self.options_frame.pack(side=tk.BOTTOM, fill='x', expand=False) - - def get_console(self): - return self.console - - def focus_search(self, event): - self.options_frame.global_search_frame.search_entry.focus() - - -def client_handler(client_socket, console: Console): - while True: - # Traceback can fit in a single log now - data = client_socket.recv(8192) - if not data: - break - - decoded_data = data.decode() - logs = decoded_data.split("ENDOFLOG") - for log in logs: - if log: - console.append_log(log) - - -def listen_for_clients(console: Console): - server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - server_socket.bind(('localhost', 12345)) - server_socket.listen() - while True: - client, addr = server_socket.accept() - threading.Thread(target=client_handler, args=(client, console)).start() - - -if __name__ == "__main__": - root = MainWindow() - threading.Thread(target=listen_for_clients, daemon=True, args=(root.get_console(),)).start() - root.mainloop() diff --git a/debug/tk_debug_window.py b/debug/tk_debug_window.py index 9197e72c..f05b4fc9 100644 --- a/debug/tk_debug_window.py +++ b/debug/tk_debug_window.py @@ -1,124 +1,437 @@ -import tkinter as tk +import re import socket import threading +import tkinter as tk from datetime import datetime +log_levels = { + "TRACE": 0, + "DEBUG": 1, + "INFO ": 2, + "WARN ": 3, + "ERROR": 4, + "FATAL": 5 +} -def client_handler(client_socket): - while True: - data = client_socket.recv(1024) - if not data: - break - decoded_data = data.decode() - logs = decoded_data.split("ENDOFLOG") - for log in logs: - if log: - text_widget.insert(tk.END, datetime.now().strftime("%Y-%m-%d %H:%M:%S") + " :: " + log + '\n') - text_widget.see(tk.END) +class Log: + def __init__(self, log: str): + self.timestamp_str = datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f') + self.log_level = "DEBUG" + self.logger = "DefaultLogger" + self.log_str = "" + self.parse_error = False + log_parts = log.split(" :: ") + if len(log_parts) == 3: + self.log_level = log_parts[0] + self.logger = log_parts[1] + self.log_str = log_parts[2] + else: + self.parse_error = True + self.log_str = log + def __str__(self): + if not self.parse_error: + return f"{self.timestamp_str} :: {self.log_level} :: {self.logger} :: {self.log_str}\n" + return f"{self.timestamp_str} :: {self.log_str}\n" -def listen_for_clients(): - server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - server_socket.bind(('localhost', 12345)) - server_socket.listen() - while True: - client, addr = server_socket.accept() - threading.Thread(target=client_handler, args=(client,)).start() - - -def on_search_entry_change(var_name, index, mode): - global search_after_id - if search_after_id: - root.after_cancel(search_after_id) - search_after_id = root.after(300, search_text) # 300 ms delay before searching - - -def clear_logs(): - text_widget.delete('1.0', tk.END) - - -def search_text(event=None): # Allow for binding to events - global last_search_start, last_search_end, search_after_id - search_after_id = None - text_widget.tag_remove('found', '1.0', tk.END) - query = search_var.get() - if query: - start_idx = last_search_end if last_search_start and last_search_end else '1.0' - idx = text_widget.search(query, start_idx, nocase=search_mode.get() != 'match_case', - regexp=search_mode.get() == 'regex', stopindex=tk.END) - if idx: - last_search_start = idx - lastidx = f"{idx}+{len(query)}c" if search_mode.get() != 'regex' else f"{idx}+{len(text_widget.get(idx, tk.END).split(None, 1)[0])}c" - text_widget.tag_add('found', idx, lastidx) - text_widget.tag_config('found', foreground='white', background='blue') - text_widget.see(idx) - last_search_end = f"{lastidx}+1c" + +class PlaceholderEntry(tk.Entry): + def __init__(self, *args, placeholder="", **kwargs): + super().__init__(*args, **kwargs) + self.placeholder = placeholder + self.user_has_interacted = False + self.insert(0, self.placeholder) + self.config(fg='grey') + self.bind('', self.on_focus_in) + self.bind('', self.on_focus_out) + self.bind('', self.handle_ctrl_backspace) + self.bind('', self.on_key_press) # Bind key press event + + def on_focus_in(self, event): + if not self.user_has_interacted and self.get() == self.placeholder: + self.delete(0, 'end') + self.config(fg='black') + + def on_focus_out(self, event): + if not self.get(): + self.insert(0, self.placeholder) + self.config(fg='grey') + self.user_has_interacted = False # Reset flag if entry is empty + + def on_key_press(self, event): + self.user_has_interacted = True # User has interacted when any key is pressed + + def reset_interaction_flag(self): + self.user_has_interacted = False + + def handle_ctrl_backspace(self, event: tk.Event): + # Get the current content of the entry and the cursor position + content = self.get() + cursor_pos = self.index(tk.INSERT) + # Find the start of the word to the left of the cursor + pre_cursor = content[:cursor_pos] + + # If the last character before the cursor is a space, delete it + if len(pre_cursor) > 0 and pre_cursor[-1] == ' ': + self.delete(cursor_pos - 1, tk.INSERT) + return "break" # Prevent default behavior + + word_start = pre_cursor.rfind(' ') + 1 if ' ' in pre_cursor else 0 + # Delete the word + self.delete(f"{word_start}", tk.INSERT) + return "break" # Prevent default behavior + + +class OptionsFrame(tk.Frame): + def __init__(self, parent): + super().__init__(parent) + self.global_search_frame = GlobalSearchFrame(self, parent) + self.specific_search_frame = SpecificSearchFrame(self, parent) + self.create_widgets() + + def inject_console(self, console): + self.global_search_frame.inject_console(console) + self.specific_search_frame.inject_console(console) + + def create_widgets(self): + self.global_search_frame.pack(side=tk.TOP, fill='x', expand=True) + self.specific_search_frame.pack(side=tk.BOTTOM, fill='x', expand=True) + + +class GlobalSearchFrame(tk.Frame): + def __init__(self, parent, root): + super().__init__(parent) + + self.after_id = None + self.root = root + self.console = None + + # Global search entry + self.search_entry_placeholder = "Search" + self.search_entry_var = tk.StringVar() + self.search_entry = PlaceholderEntry( + self, + placeholder=self.search_entry_placeholder, + textvariable=self.search_entry_var + ) + self.search_entry_var.trace("w", self.on_entry_changed) + self.search_entry.bind('', lambda event: self.console.text_widget.focus()) + self.search_entry.config(fg='grey') + + self.search_modes = [] + self.search_mode_var = tk.StringVar(value='normal') + self.search_mode_var.trace("w", self.apply_search_mode) + for mode, text in [('normal', 'normal'), ('match_case', 'match case'), ('regex', 'regex')]: + self.search_modes.append(tk.Radiobutton(self, text=text, variable=self.search_mode_var, value=mode)) + + self.create_widgets() + + def apply_search_mode(self, *args): + self.console.set_filter(global_search_mode=self.search_mode_var.get()) + + def inject_console(self, console): + self.console = console + + def create_widgets(self): + self.search_entry.pack(side=tk.LEFT, fill='x', expand=True, padx=(5, 0)) + for mode in self.search_modes: + mode.pack(side=tk.LEFT, padx=(5, 0)) + self.search_entry.bind('', lambda event: self.console.next_occurrence()) + + def on_entry_changed(self, *args): + if self.after_id: + self.root.after_cancel(self.after_id) + self.after_id = self.root.after(300, self.apply_search_entry_var) + + def apply_search_entry_var(self): + self.console.set_filter(global_search_str=self.search_entry_var.get()) + self.after_id = None + + +class Console(tk.Frame): + def __init__(self, parent, option_frame: OptionsFrame): + super().__init__(parent) + self.global_search_mode = "normal" + self.all_logs = [] + self.shown_logs = [] + self.option_frame = option_frame + self.text_widget = tk.Text(self) + self.scrollbar = tk.Scrollbar(self, command=self.text_widget.yview) + self.global_search_str = "" + self.logger_name = "" + self.log_level = "TRACE" + self.and_above = True + self.create_widgets() + + def create_widgets(self): + self.text_widget.pack(side=tk.LEFT, expand=True, fill='both') + self.scrollbar.pack(side=tk.RIGHT, fill='y') + self.text_widget.config(yscrollcommand=self.scrollbar.set) + + def set_filter( + self, + global_search_str: str = None, + global_search_mode: str = None, + logger_name: str = None, + log_level: str = None, + and_above: bool = None + ): + if global_search_str is not None and self.option_frame.global_search_frame.search_entry.user_has_interacted: + self.global_search_str = global_search_str + elif global_search_str is None or not self.option_frame.global_search_frame.search_entry.user_has_interacted: + self.global_search_str = "" + + if logger_name is not None and self.option_frame.specific_search_frame.logger_entry.user_has_interacted: + self.logger_name = logger_name + elif logger_name is None or not self.option_frame.specific_search_frame.logger_entry.user_has_interacted: + self.logger_name = "" + + if global_search_mode is not None: + self.global_search_mode = global_search_mode + + if log_level is not None: + self.log_level = log_level + + if and_above is not None: + self.and_above = and_above + + self.apply_filters() + + def append_log(self, log: str): + log_obj = Log(log) + self.all_logs.append(log_obj) + if self.filter_log(log_obj): + self.shown_logs.append(log_obj) + # Check if the user is at the end before appending + at_end = self.text_widget.yview()[1] == 1.0 + self.text_widget.insert(tk.END, str(log_obj)) + if at_end: + self.text_widget.see(tk.END) + + def clear_logs(self): + self.text_widget.delete('1.0', tk.END) + self.shown_logs.clear() + self.all_logs.clear() + self.apply_filters() + + def apply_filters(self): + # Re-filter all logs and update the text widget only if necessary + filtered_logs = [log for log in self.all_logs if self.filter_log(log)] + self.shown_logs = filtered_logs + self.update_text_widget() + + def filter_log(self, log): + if self.and_above: + flag = log_levels[log.log_level] >= log_levels[self.log_level] else: - last_search_start = None - last_search_end = None + flag = log.log_level == self.log_level + if self.logger_name: + flag = flag and self.logger_name in log.logger -def focus_search(event=None): - search_entry.focus_set() + return flag + def update_text_widget(self): + # Preserve the current view position unless at the end + at_end = self.text_widget.yview()[1] == 1.0 + self.text_widget.delete('1.0', tk.END) + for log in self.shown_logs: + self.text_widget.insert(tk.END, str(log)) + if at_end: + self.text_widget.see(tk.END) -def handle_ctrl_backspace(event): - # Delete the word left of the cursor in the search entry - content = search_entry.get() - cursor_pos = search_entry.index(tk.INSERT) - pre_cursor = content[:cursor_pos] - word_start = pre_cursor.rfind(' ') + 1 if ' ' in pre_cursor else 0 - search_entry.delete(f"{word_start}", tk.INSERT) - return "break" # Prevent default behavior + if self.global_search_str: + self.search_text() + def search_text(self): + self.text_widget.tag_remove('found', '1.0', tk.END) + search_query = self.global_search_str.strip() + if not search_query: + return -if __name__ == "__main__": - root = tk.Tk() + if self.global_search_mode == 'match_case': + pattern = re.escape(search_query) + elif self.global_search_mode == 'regex': + # Directly use the user input for regex, but be cautious of Tkinter's limited regex support + pattern = search_query + else: # normal mode, make it case-insensitive + pattern = '(?i)' + re.escape(search_query) # Add (?i) for case-insensitive search in Tkinter + + start = '1.0' + while True: + match_start = self.text_widget.search(pattern, start, tk.END, regexp=True) + if not match_start: + break + match_end = f"{match_start}+{len(search_query)}c" + self.text_widget.tag_add('found', match_start, match_end) + start = match_end + + self.text_widget.tag_config('found', background='yellow') + if at_end := self.text_widget.yview()[1] == 1.0: + first_occurrence = self.text_widget.tag_ranges('found') + if first_occurrence: + self.text_widget.see(first_occurrence[0]) + self.next_occurrence() + + def next_occurrence(self): + current_tags = self.text_widget.tag_ranges('found') + if not current_tags: + return + + # Ensure the 'current_found' tag exists with a blue background. + self.text_widget.tag_config('current_found', background='#ADD8E6') + + # Get the current position of the cursor in the text widget. + cursor_index = self.text_widget.index(tk.INSERT) + + # Remove the 'current_found' tag from the entire text widget. + self.text_widget.tag_remove('current_found', '1.0', tk.END) + + # Convert the current cursor index to a comparable value. + cursor_line, cursor_char = map(int, cursor_index.split('.')) + + for i in range(0, len(current_tags), 2): + tag_start = current_tags[i] + tag_end = current_tags[i + 1] + + # Convert tag start index to comparable values. + tag_start_line, tag_start_char = map(int, str(tag_start).split('.')) + + # Check if the tag start is greater than the cursor position. + if tag_start_line > cursor_line or (tag_start_line == cursor_line and tag_start_char > cursor_char): + self.text_widget.mark_set(tk.INSERT, tag_start) + self.text_widget.see(tag_start) + + # Apply the 'current_found' tag to the current occurrence. + self.text_widget.tag_add('current_found', tag_start, tag_end) + break + else: + # Wrap to the first tag if no next tag is found. + self.text_widget.mark_set(tk.INSERT, str(current_tags[0])) + self.text_widget.see(str(current_tags[0])) + self.text_widget.tag_add('current_found', current_tags[0], current_tags[1]) + + +class SpecificSearchFrame(tk.Frame): + def __init__(self, parent, root): + super().__init__(parent) + self.root = root + self.after_id = None + self.console = None + + # Logger name entry + self.logger_entry_placeholder = "Logger Name" + self.logger_entry_var = tk.StringVar() + self.logger_entry = PlaceholderEntry( + self, + placeholder=self.logger_entry_placeholder, + textvariable=self.logger_entry_var + ) + self.logger_entry_var.trace("w", self.on_entry_changed) + self.logger_entry.bind('', lambda event: self.console.text_widget.focus()) + self.logger_entry.config(fg='grey') - last_search_start = None - last_search_end = None - search_after_id = None + # Log level dropdown + self.log_level_dropdown_var = tk.StringVar() + self.log_level_dropdown_var.set("TRACE") + self.log_level_dropdown = tk.OptionMenu( + self, + self.log_level_dropdown_var, + *log_levels.keys() + ) + self.log_level_dropdown_var.trace( + "w", + lambda *args: self.console.set_filter(log_level=self.log_level_dropdown_var.get()) + ) - # Frame for text widget and scrollbar - text_frame = tk.Frame(root) - text_frame.pack(expand=True, fill='both') + # And above checkbox + self.and_above_var = tk.BooleanVar() + self.and_above_var.set(True) + self.and_above_checkbox = tk.Checkbutton( + self, + text="And above", + variable=self.and_above_var, + onvalue=True, + offvalue=False, + command=lambda: self.console.set_filter(and_above=self.and_above_var.get()) + ) - text_widget = tk.Text(text_frame) - text_widget.pack(side=tk.LEFT, expand=True, fill='both') + self.clear_log_button: tk.Button | None = None - scrollbar = tk.Scrollbar(text_frame, command=text_widget.yview) - scrollbar.pack(side=tk.RIGHT, fill='y') - text_widget.config(yscrollcommand=scrollbar.set) + self.create_widgets() - # Frame for search functionality - search_frame = tk.Frame(root) - search_frame.pack(fill='x') + def inject_console(self, console): + self.console = console + self.clear_log_button = tk.Button( + self, + text="Clear Logs", + command=self.console.clear_logs + ) + self.clear_log_button.pack(side=tk.RIGHT, padx=(5, 0), fill='x', expand=True) - search_var = tk.StringVar() - search_var.trace("w", lambda name, index, mode, sv=search_var: on_search_entry_change(name, index, mode)) - search_entry = tk.Entry(search_frame, textvariable=search_var) - search_entry.pack(side=tk.LEFT, fill='x', expand=True) - search_entry.bind('', search_text) # Bind the Enter key to the search function - search_entry.bind('', handle_ctrl_backspace) + def create_widgets(self): + self.logger_entry.pack(side=tk.LEFT, fill='x', expand=True, padx=(5, 0)) + self.log_level_dropdown.pack(side=tk.LEFT, padx=(5, 0), fill='x', expand=True) + self.and_above_checkbox.pack(side=tk.LEFT, padx=(5, 0), fill='x', expand=True) - # Bind Ctrl+F to focus on the search bar - root.bind('', focus_search) - root.bind('', focus_search) # For capital 'F' if Caps Lock is on or Shift is pressed + def on_entry_changed(self, *args): + if self.after_id: + self.root.after_cancel(self.after_id) + self.after_id = self.root.after(250, self.apply_logger_entry_var) - # Frame for search options - options_frame = tk.Frame(root) - options_frame.pack(fill='x') + def apply_logger_entry_var(self): + self.console.set_filter(logger_name=self.logger_entry_var.get()) + self.after_id = None - search_mode = tk.StringVar(value='normal') - modes = [('normal', 'normal'), ('match_case', 'match case'), ('regex', 'regex')] - for mode, text in modes: - b = tk.Radiobutton(options_frame, text=text, variable=search_mode, value=mode) - b.pack(side=tk.LEFT) - clear_button = tk.Button(root, text="Clear Logs", command=clear_logs) - clear_button.pack(side=tk.BOTTOM, fill='x') +class MainWindow(tk.Tk): + def __init__(self): + super().__init__() + self.title("Steamodded Debug Console") + self.options_frame = OptionsFrame(self) + self.console = Console(self, self.options_frame) + self.options_frame.inject_console(self.console) + self.create_widgets() - threading.Thread(target=listen_for_clients, daemon=True).start() + self.bind('', self.focus_search) + self.bind('', self.focus_search) + def create_widgets(self): + self.console.pack(side=tk.TOP, expand=True, fill='both') + self.options_frame.pack(side=tk.BOTTOM, fill='x', expand=False) + + def get_console(self): + return self.console + + def focus_search(self, event): + self.options_frame.global_search_frame.search_entry.focus() + + +def client_handler(client_socket, console: Console): + while True: + # Traceback can fit in a single log now + data = client_socket.recv(8192) + if not data: + break + + decoded_data = data.decode() + logs = decoded_data.split("ENDOFLOG") + for log in logs: + if log: + console.append_log(log) + + +def listen_for_clients(console: Console): + server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server_socket.bind(('localhost', 12345)) + server_socket.listen() + while True: + client, addr = server_socket.accept() + threading.Thread(target=client_handler, args=(client, console)).start() + + +if __name__ == "__main__": + root = MainWindow() + threading.Thread(target=listen_for_clients, daemon=True, args=(root.get_console(),)).start() root.mainloop() From 0b5a8507f43e5956b54ccd9c268e516436539a1e Mon Sep 17 00:00:00 2001 From: jubnl Date: Sun, 31 Mar 2024 00:38:05 +0100 Subject: [PATCH 5/7] Add line number to the left and ensure filters are applied when receiving a new log --- debug/tk_debug_window.py | 69 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 3 deletions(-) diff --git a/debug/tk_debug_window.py b/debug/tk_debug_window.py index f05b4fc9..005c9c94 100644 --- a/debug/tk_debug_window.py +++ b/debug/tk_debug_window.py @@ -14,6 +14,59 @@ } +# might or might not be a copy paste from https://stackoverflow.com/a/16375233 +class TextLineNumbers(tk.Canvas): + def __init__(self, *args, **kwargs): + tk.Canvas.__init__(self, *args, **kwargs, highlightthickness=0) + self.textwidget = None + + def attach(self, text_widget): + self.textwidget = text_widget + + def redraw(self, *args): + '''redraw line numbers''' + self.delete("all") + + i = self.textwidget.index("@0,0") + while True: + dline = self.textwidget.dlineinfo(i) + if dline is None: + break + y = dline[1] + linenum = str(i).split(".")[0] + self.create_text(2, y, anchor="nw", text=linenum, fill="#606366") + i = self.textwidget.index("%s+1line" % i) + + +class CustomText(tk.Text): + def __init__(self, *args, **kwargs): + tk.Text.__init__(self, *args, **kwargs) + + # create a proxy for the underlying widget + self._orig = self._w + "_orig" + self.tk.call("rename", self._w, self._orig) + self.tk.createcommand(self._w, self._proxy) + + def _proxy(self, *args): + # let the actual widget perform the requested action + cmd = (self._orig,) + args + result = self.tk.call(cmd) + + # generate an event if something was added or deleted, + # or the cursor position changed + if (args[0] in ("insert", "replace", "delete") or + args[0:3] == ("mark", "set", "insert") or + args[0:2] == ("xview", "moveto") or + args[0:2] == ("xview", "scroll") or + args[0:2] == ("yview", "moveto") or + args[0:2] == ("yview", "scroll") + ): + self.event_generate("<>", when="tail") + + # return what the actual widget returned + return result + + class Log: def __init__(self, log: str): self.timestamp_str = datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f') @@ -156,7 +209,11 @@ def __init__(self, parent, option_frame: OptionsFrame): self.all_logs = [] self.shown_logs = [] self.option_frame = option_frame - self.text_widget = tk.Text(self) + self.text_widget = CustomText(self) + self.linenumbers = TextLineNumbers(self, width=30) + self.linenumbers.attach(self.text_widget) + self.text_widget.bind("<>", self._on_change) + self.text_widget.bind("", self._on_change) self.scrollbar = tk.Scrollbar(self, command=self.text_widget.yview) self.global_search_str = "" self.logger_name = "" @@ -164,9 +221,13 @@ def __init__(self, parent, option_frame: OptionsFrame): self.and_above = True self.create_widgets() + def _on_change(self, event): + self.linenumbers.redraw() + def create_widgets(self): - self.text_widget.pack(side=tk.LEFT, expand=True, fill='both') self.scrollbar.pack(side=tk.RIGHT, fill='y') + self.linenumbers.pack(side=tk.LEFT, fill="y") + self.text_widget.pack(side=tk.LEFT, expand=True, fill='both') self.text_widget.config(yscrollcommand=self.scrollbar.set) def set_filter( @@ -208,6 +269,7 @@ def append_log(self, log: str): self.text_widget.insert(tk.END, str(log_obj)) if at_end: self.text_widget.see(tk.END) + self.apply_filters() def clear_logs(self): self.text_widget.delete('1.0', tk.END) @@ -268,7 +330,8 @@ def search_text(self): start = match_end self.text_widget.tag_config('found', background='yellow') - if at_end := self.text_widget.yview()[1] == 1.0: + at_end = self.text_widget.yview()[1] == 1.0 + if at_end: first_occurrence = self.text_widget.tag_ranges('found') if first_occurrence: self.text_widget.see(first_occurrence[0]) From f7a55e162e53aa4364fb9d4e08889cc33e93ffdc Mon Sep 17 00:00:00 2001 From: jubnl Date: Sun, 31 Mar 2024 00:39:43 +0100 Subject: [PATCH 6/7] better, ensure that the search is applied again when a new log comes --- debug/tk_debug_window.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/debug/tk_debug_window.py b/debug/tk_debug_window.py index 005c9c94..a4707caf 100644 --- a/debug/tk_debug_window.py +++ b/debug/tk_debug_window.py @@ -269,7 +269,8 @@ def append_log(self, log: str): self.text_widget.insert(tk.END, str(log_obj)) if at_end: self.text_widget.see(tk.END) - self.apply_filters() + if self.global_search_str: + self.search_text() def clear_logs(self): self.text_widget.delete('1.0', tk.END) From 4ee50ca45d5af64c7bdf2c9086cf6c43ecca30e3 Mon Sep 17 00:00:00 2001 From: jubnl Date: Sun, 31 Mar 2024 00:51:37 +0100 Subject: [PATCH 7/7] make the console readonly --- debug/tk_debug_window.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/debug/tk_debug_window.py b/debug/tk_debug_window.py index a4707caf..4bae4c43 100644 --- a/debug/tk_debug_window.py +++ b/debug/tk_debug_window.py @@ -229,6 +229,7 @@ def create_widgets(self): self.linenumbers.pack(side=tk.LEFT, fill="y") self.text_widget.pack(side=tk.LEFT, expand=True, fill='both') self.text_widget.config(yscrollcommand=self.scrollbar.set) + self.text_widget.config(state=tk.DISABLED) def set_filter( self, @@ -266,14 +267,18 @@ def append_log(self, log: str): self.shown_logs.append(log_obj) # Check if the user is at the end before appending at_end = self.text_widget.yview()[1] == 1.0 + self.text_widget.config(state=tk.NORMAL) self.text_widget.insert(tk.END, str(log_obj)) + self.text_widget.config(state=tk.DISABLED) if at_end: self.text_widget.see(tk.END) if self.global_search_str: self.search_text() def clear_logs(self): + self.text_widget.config(state=tk.NORMAL) self.text_widget.delete('1.0', tk.END) + self.text_widget.config(state=tk.DISABLED) self.shown_logs.clear() self.all_logs.clear() self.apply_filters() @@ -298,9 +303,13 @@ def filter_log(self, log): def update_text_widget(self): # Preserve the current view position unless at the end at_end = self.text_widget.yview()[1] == 1.0 + self.text_widget.config(state=tk.NORMAL) self.text_widget.delete('1.0', tk.END) + self.text_widget.config(state=tk.DISABLED) for log in self.shown_logs: + self.text_widget.config(state=tk.NORMAL) self.text_widget.insert(tk.END, str(log)) + self.text_widget.config(state=tk.DISABLED) if at_end: self.text_widget.see(tk.END)