""" MIT License — Copyright (c) 2026 Prof. Dr. Joern Fischer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ import tkinter as tk from tkinter import filedialog, messagebox, Menu, ttk import os import sys import subprocess import threading import queue import platform import re import webbrowser import shutil from tkinterdnd2 import DND_FILES, TkinterDnD # ── Minimalistic Monokai Syntax Highlighters ────────────────────────────────── MONOKAI = { "keyword": "#f92672", "string": "#e6db74", "comment": "#75715e", "number": "#ae81ff", "function": "#a6e22e", "class": "#a6e22e", "operator": "#f92672", "tag": "#f92672", "attr": "#a6e22e", "special": "#66d9ef", "heading": "#a6e22e", "bold": "#f92672", "link": "#66d9ef", "default": "#f8f8f2", } def _highlight_python(text_widget): code = text_widget.get("1.0", "end-1c") rules = [ ("comment", r"#[^\n]*"), ("string", r'"""[\s\S]*?"""|\'\'\'[\s\S]*?\'\'\'|"(?:\\.|[^"\\])*"|\'(?:\\.|[^\'\\])*\''), ("keyword", r"\b(False|None|True|and|as|assert|async|await|break|class|continue|" r"def|del|elif|else|except|finally|for|from|global|if|import|in|is|" r"lambda|nonlocal|not|or|pass|raise|return|try|while|with|yield)\b"), ("function", r"\bdef\s+(\w+)"), ("class", r"\bclass\s+(\w+)"), ("number", r"\b(0x[0-9a-fA-F]+|0b[01]+|0o[0-7]+|\d+\.?\d*([eE][+-]?\d+)?)\b"), ("special", r"\b(self|cls|__\w+__)\b"), ] _apply_rules(text_widget, code, rules) def _highlight_c_cpp(text_widget): code = text_widget.get("1.0", "end-1c") rules = [ ("comment", r"//[^\n]*|/\*[\s\S]*?\*/"), ("string", r'"(?:\\.|[^"\\])*"|\'(?:\\.|[^\'\\])*\''), ("keyword", r"\b(auto|break|case|char|const|continue|default|do|double|else|enum|" r"extern|float|for|goto|if|inline|int|long|register|restrict|return|" r"short|signed|sizeof|static|struct|switch|typedef|union|unsigned|void|" r"volatile|while|class|namespace|new|delete|public|private|protected|" r"virtual|override|template|typename|bool|nullptr|true|false|try|catch|throw)\b"), ("function", r"\b(\w+)\s*(?=\()"), ("number", r"\b(0x[0-9a-fA-F]+[uUlL]*|\d+\.?\d*([eE][+-]?\d+)?[fFlL]?)\b"), ("special", r"#\s*(include|define|ifdef|ifndef|endif|pragma|undef|if|else|elif)\b"), ] _apply_rules(text_widget, code, rules) def _highlight_java(text_widget): code = text_widget.get("1.0", "end-1c") rules = [ ("comment", r"//[^\n]*|/\*[\s\S]*?\*/"), ("string", r'"(?:\\.|[^"\\])*"|\'(?:\\.|[^\'\\])*\''), ("keyword", r"\b(abstract|assert|boolean|break|byte|case|catch|char|class|const|" r"continue|default|do|double|else|enum|extends|final|finally|float|for|" r"goto|if|implements|import|instanceof|int|interface|long|native|new|" r"package|private|protected|public|return|short|static|strictfp|super|" r"switch|synchronized|this|throw|throws|transient|try|void|volatile|while|" r"true|false|null|var|record|sealed|permits)\b"), ("function", r"\b(\w+)\s*(?=\()"), ("class", r"\bclass\s+(\w+)"), ("number", r"\b(\d+\.?\d*([eE][+-]?\d+)?[fFdDlL]?)\b"), ("special", r"@\w+"), ] _apply_rules(text_widget, code, rules) def _highlight_html(text_widget): code = text_widget.get("1.0", "end-1c") rules = [ ("comment", r""), ("string", r'"[^"]*"|\'[^\']*\''), ("tag", r".*", re.MULTILINE), ] rules_norm = [(t, p, f if len(r) > 2 else 0) for r in rules for t, p, f in [(r[0], r[1], r[2] if len(r) > 2 else 0)]] _apply_rules(text_widget, code, rules_norm) def _highlight_latex(text_widget): code = text_widget.get("1.0", "end-1c") rules = [ # Comments: % to end of line (blocks everything inside) ("comment", r"%[^\n]*"), # Verbatim environment contents (block inner highlighting) ("string", r"\\begin\{verbatim\}[\s\S]*?\\end\{verbatim\}"), # Math environments: $…$ $$…$$ \(…\) \[…\] ("special", r"\$\$[\s\S]*?\$\$|\$[^$\n]*?\$" r"|\\[\(\[][\s\S]*?\\[\)\]]"), # Document-structure and formatting commands (keyword = pink) ("keyword", r"\\(begin|end|documentclass|usepackage|newcommand|renewcommand|" r"def|let|newenvironment|renewenvironment|input|include|bibliography|" r"bibliographystyle|label|ref|eqref|cite|footnote|" r"part|chapter|section|subsection|subsubsection|paragraph|subparagraph|" r"title|author|date|maketitle|tableofcontents|" r"textbf|textit|texttt|emph|underline|textrm|textsf|" r"Large|large|small|tiny|huge|Huge|normalsize|" r"item|enumerate|itemize|description)\b"), # Any other backslash-command (function = green) ("function", r"\\[a-zA-Z@]+\*?"), # Curly-brace groups {…} — yellow, but do NOT block inner rules ("string", r"\{[^{}]*\}"), # Numbers ("number", r"\b\d+\.?\d*\b"), ] # Only comments and verbatim block inner rules; {…} does not. _apply_rules(text_widget, code, rules, block_tags={"comment", "string"}) def _apply_rules(text_widget, code, rules, block_tags=None): """Apply regex-based highlight rules to text_widget. block_tags – set of tag names whose matched ranges block all later rules. Defaults to {"comment", "string"}. Pass a smaller set (e.g. {"comment"}) for languages like LaTeX where string-tagged spans should NOT suppress inner commands. """ if block_tags is None: block_tags = {"comment", "string"} # Remove old highlight tags for tag in MONOKAI: text_widget.tag_remove(tag, "1.0", tk.END) # Track character ranges already claimed by a block-tag rule. covered = [] # list of (start, end) int pairs def _is_covered(start, end): for cs, ce in covered: if start < ce and end > cs: return True return False for rule in rules: if len(rule) == 3: tag, pattern, flags = rule else: tag, pattern = rule flags = 0 color = MONOKAI.get(tag, MONOKAI["default"]) if tag not in text_widget.tag_names(): text_widget.tag_configure(tag, foreground=color) text_widget.tag_raise(tag, "sel") for m in re.finditer(pattern, code, flags): s, e = m.start(), m.end() if tag not in block_tags and _is_covered(s, e): continue # skip: inside a protected region start_idx = f"1.0+{s}c" end_idx = f"1.0+{e}c" text_widget.tag_add(tag, start_idx, end_idx) if tag in block_tags: covered.append((s, e)) HIGHLIGHTER_MAP = { ".py": _highlight_python, ".c": _highlight_c_cpp, ".cpp": _highlight_c_cpp, ".cc": _highlight_c_cpp, ".h": _highlight_c_cpp, ".hpp": _highlight_c_cpp, ".java": _highlight_java, ".html": _highlight_html, ".htm": _highlight_html, ".php": _highlight_php, ".md": _highlight_markdown, ".markdown": _highlight_markdown, ".tex": _highlight_latex, ".ltx": _highlight_latex, ".sty": _highlight_latex, ".cls": _highlight_latex, } def _tokenize_for_sidebar(code: str, filename: str): """ Return a list of (hex_color, text_segment) tuples for the entire code, using the same rules as the syntax highlighter but working purely on strings (no tk widget needed). Uncovered characters get the default colour. """ ext = os.path.splitext(filename)[1].lower() if filename else "" # Pick the right rule set DEFAULT_COLOR = MONOKAI["default"] rule_sets = { ".py": [ ("comment", r"#[^\n]*"), ("string", r'"""[\s\S]*?"""|\'\'\'[\s\S]*?\'\'\'|"(?:\\.|[^"\\])*"|\'(?:\\.|[^\'\\])*\''), ("keyword", r"\b(False|None|True|and|as|assert|async|await|break|class|continue|" r"def|del|elif|else|except|finally|for|from|global|if|import|in|is|" r"lambda|nonlocal|not|or|pass|raise|return|try|while|with|yield)\b"), ("function",r"\bdef\s+(\w+)"), ("class", r"\bclass\s+(\w+)"), ("number", r"\b(0x[0-9a-fA-F]+|0b[01]+|0o[0-7]+|\d+\.?\d*([eE][+-]?\d+)?)\b"), ("special", r"\b(self|cls|__\w+__)\b"), ], ".c": [ ("comment", r"//[^\n]*|/\*[\s\S]*?\*/"), ("string", r'"(?:\\.|[^"\\])*"|\'(?:\\.|[^\'\\])*\''), ("keyword", r"\b(auto|break|case|char|const|continue|default|do|double|else|enum|" r"extern|float|for|goto|if|inline|int|long|register|restrict|return|" r"short|signed|sizeof|static|struct|switch|typedef|union|unsigned|void|" r"volatile|while|bool|nullptr|true|false)\b"), ("function",r"\b(\w+)\s*(?=\()"), ("number", r"\b(0x[0-9a-fA-F]+[uUlL]*|\d+\.?\d*([eE][+-]?\d+)?[fFlL]?)\b"), ("special", r"#\s*(include|define|ifdef|ifndef|endif|pragma|undef|if|else|elif)\b"), ], ".java":[ ("comment", r"//[^\n]*|/\*[\s\S]*?\*/"), ("string", r'"(?:\\.|[^"\\])*"|\'(?:\\.|[^\'\\])*\''), ("keyword", r"\b(abstract|assert|boolean|break|byte|case|catch|char|class|const|" r"continue|default|do|double|else|enum|extends|final|finally|float|for|" r"goto|if|implements|import|instanceof|int|interface|long|native|new|" r"package|private|protected|public|return|short|static|super|switch|" r"synchronized|this|throw|throws|transient|try|void|volatile|while|" r"true|false|null|var)\b"), ("function",r"\b(\w+)\s*(?=\()"), ("number", r"\b(\d+\.?\d*([eE][+-]?\d+)?[fFdDlL]?)\b"), ("special", r"@\w+"), ], ".html":[ ("comment", r""), ("string", r'"[^"]*"|\'[^\']*\''), ("tag", r".*", re.MULTILINE), ], ".tex": [ ("comment", r"%[^\n]*"), ("string", r"\\begin\{verbatim\}[\s\S]*?\\end\{verbatim\}|\{[^{}]*\}"), ("keyword", r"\\(begin|end|documentclass|usepackage|newcommand|renewcommand|" r"def|let|newenvironment|renewenvironment|input|include|bibliography|" r"bibliographystyle|label|ref|eqref|cite|footnote|" r"part|chapter|section|subsection|subsubsection|paragraph|subparagraph|" r"title|author|date|maketitle|tableofcontents|" r"textbf|textit|texttt|emph|underline|" r"Large|large|small|tiny|huge|Huge|normalsize|item)\b"), ("function",r"\\[a-zA-Z@]+\*?"), ("special", r"\$\$[\s\S]*?\$\$|\$[^$\n]*?\$|\\[\(\[][\s\S]*?\\[\)\]]"), ("number", r"\b\d+\.?\d*\b"), ], } # map aliases for alias in (".cpp", ".cc", ".hpp", ".h"): rule_sets[alias] = rule_sets[".c"] rule_sets[".htm"] = rule_sets[".html"] rule_sets[".markdown"] = rule_sets[".md"] rule_sets[".ltx"] = rule_sets[".tex"] rule_sets[".sty"] = rule_sets[".tex"] rule_sets[".cls"] = rule_sets[".tex"] rules = rule_sets.get(ext) if not rules: return [(DEFAULT_COLOR, code)] BLOCK_TAGS = {"comment", "string"} # build color array: one color per character index n = len(code) colors = [DEFAULT_COLOR] * n covered = [] # (start, end) def is_covered(s, e): for cs, ce in covered: if s < ce and e > cs: return True return False for rule in rules: if len(rule) == 3: tag, pattern, flags = rule else: tag, pattern = rule[0], rule[1] flags = 0 color = MONOKAI.get(tag, DEFAULT_COLOR) for m in re.finditer(pattern, code, flags): s, e = m.start(), m.end() if tag not in BLOCK_TAGS and is_covered(s, e): continue for i in range(s, e): colors[i] = color if tag in BLOCK_TAGS: covered.append((s, e)) # compress into (color, segment) runs if n == 0: return [] runs = [] cur_color = colors[0] buf = [] for i, ch in enumerate(code): c = colors[i] if c != cur_color: runs.append((cur_color, "".join(buf))) buf = [ch] cur_color = c else: buf.append(ch) if buf: runs.append((cur_color, "".join(buf))) return runs # ── Syntax-Highlighter ──────────────────────────────────────────────────────── class SyntaxHighlighter: def __init__(self, text_widget): self.text = text_widget self.root = text_widget.winfo_toplevel() self.update_delay_ms = 300 self._highlight_job = None self._setup_tags() def _setup_tags(self): for tag, color in MONOKAI.items(): if tag not in self.text.tag_names(): self.text.tag_configure(tag, foreground=color) self.text.tag_raise(tag, "sel") def highlight(self, filename=""): if self._highlight_job: self.root.after_cancel(self._highlight_job) self._highlight_job = self.root.after( self.update_delay_ms, lambda: self._do_highlight(filename)) def _do_highlight(self, filename): ext = os.path.splitext(filename)[1].lower() if filename else "" highlighter_fn = HIGHLIGHTER_MAP.get(ext) if highlighter_fn: try: highlighter_fn(self.text) except Exception as e: print(f"Highlighting-Fehler: {e}") else: # No highlighter for this extension – clear all highlight tags for tag in MONOKAI: self.text.tag_remove(tag, "1.0", tk.END) def on_text_change(self, event=None): self.highlight() # ── Find Dialog ─────────────────────────────────────────────────────────────── class FindDialog(tk.Toplevel): def __init__(self, parent, text_widget): super().__init__(parent) self.text = text_widget self.title("Suchen") self.resizable(False, False) self.configure(bg="gray20") self.transient(parent) self._last_search = "" self._matches = [] self._current_match = -1 self._build_ui() self.bind("", lambda e: self.destroy()) self.entry_search.focus_set() def _build_ui(self): frame = tk.Frame(self, bg="gray20", padx=10, pady=10) frame.pack(fill=tk.BOTH, expand=True) tk.Label(frame, text="Suchen:", bg="gray20", fg="white").grid(row=0, column=0, sticky="w") self.entry_search = tk.Entry(frame, width=30, bg="gray30", fg="white", insertbackground="white") self.entry_search.grid(row=0, column=1, padx=5, pady=3) self.entry_search.bind("", lambda e: self.find_next()) btn_frame = tk.Frame(frame, bg="gray20") btn_frame.grid(row=1, column=0, columnspan=2, pady=5) tk.Button(btn_frame, text="◀ Vorheriges", command=self.find_prev, bg="gray35", fg="black").pack(side=tk.LEFT, padx=3) tk.Button(btn_frame, text="Nächstes ▶", command=self.find_next, bg="gray35", fg="black").pack(side=tk.LEFT, padx=3) self.status_label = tk.Label(frame, text="", bg="gray20", fg="#75715e") self.status_label.grid(row=2, column=0, columnspan=2) def _search(self): query = self.entry_search.get() if not query: return self.text.tag_remove("search_highlight", "1.0", tk.END) self._matches = [] self._current_match = -1 self._last_search = query start = "1.0" while True: pos = self.text.search(query, start, stopindex=tk.END, nocase=True) if not pos: break end = f"{pos}+{len(query)}c" self._matches.append((pos, end)) self.text.tag_add("search_highlight", pos, end) start = end self.text.tag_configure("search_highlight", background="#f92672", foreground="white") self.status_label.config( text=f"0 / {len(self._matches)} Treffer" if self._matches else "Keine Treffer") def find_next(self): if self.entry_search.get() != self._last_search or not self._matches: self._search() if not self._matches: return self._current_match = (self._current_match + 1) % len(self._matches) self._jump_to_current() def find_prev(self): if self.entry_search.get() != self._last_search or not self._matches: self._search() if not self._matches: return self._current_match = (self._current_match - 1) % len(self._matches) self._jump_to_current() def _jump_to_current(self): pos, end = self._matches[self._current_match] self.text.tag_remove("current_match", "1.0", tk.END) self.text.tag_add("current_match", pos, end) self.text.tag_configure("current_match", background="#fd971f", foreground="black") self.text.tag_raise("current_match", "search_highlight") self.text.see(pos) self.status_label.config( text=f"{self._current_match + 1} / {len(self._matches)} Treffer") def destroy(self): self.text.tag_remove("search_highlight", "1.0", tk.END) self.text.tag_remove("current_match", "1.0", tk.END) super().destroy() class FindReplaceDialog(FindDialog): def __init__(self, parent, text_widget): tk.Toplevel.__init__(self, parent) self.text = text_widget self.title("Suchen & Ersetzen") self.resizable(False, False) self.configure(bg="gray20") self.transient(parent) self._last_search = "" self._matches = [] self._current_match = -1 self._build_ui() self.bind("", lambda e: self.destroy()) self.entry_search.focus_set() def _build_ui(self): frame = tk.Frame(self, bg="gray20", padx=10, pady=10) frame.pack(fill=tk.BOTH, expand=True) tk.Label(frame, text="Suchen:", bg="gray20", fg="white").grid(row=0, column=0, sticky="w") self.entry_search = tk.Entry(frame, width=30, bg="gray30", fg="white", insertbackground="white") self.entry_search.grid(row=0, column=1, padx=5, pady=3) self.entry_search.bind("", lambda e: self.find_next()) tk.Label(frame, text="Ersetzen:", bg="gray20", fg="white").grid(row=1, column=0, sticky="w") self.entry_replace = tk.Entry(frame, width=30, bg="gray30", fg="white", insertbackground="white") self.entry_replace.grid(row=1, column=1, padx=5, pady=3) btn_frame = tk.Frame(frame, bg="gray20") btn_frame.grid(row=2, column=0, columnspan=2, pady=5) tk.Button(btn_frame, text="◀", command=self.find_prev, bg="gray35", fg="black").pack(side=tk.LEFT, padx=2) tk.Button(btn_frame, text="▶", command=self.find_next, bg="gray35", fg="black").pack(side=tk.LEFT, padx=2) tk.Button(btn_frame, text="Ersetzen", command=self.replace_current, bg="gray35", fg="black").pack(side=tk.LEFT, padx=2) tk.Button(btn_frame, text="Alle ersetzen", command=self.replace_all, bg="#f92672", fg="black").pack(side=tk.LEFT, padx=2) self.status_label = tk.Label(frame, text="", bg="gray20", fg="#75715e") self.status_label.grid(row=3, column=0, columnspan=2) def replace_current(self): if not self._matches or self._current_match < 0: self.find_next() return pos, end = self._matches[self._current_match] self.text.delete(pos, end) self.text.insert(pos, self.entry_replace.get()) self._last_search = "" self.find_next() def replace_all(self): query = self.entry_search.get() if not query: return self._search() for pos, end in reversed(self._matches): self.text.delete(pos, end) self.text.insert(pos, self.entry_replace.get()) count = len(self._matches) self._matches = [] self._current_match = -1 self.status_label.config(text=f"{count} Ersetzungen vorgenommen") # ── Eingebettetes Terminal-Panel ────────────────────────────────────────────── class TerminalPanel(tk.Frame): """ Eingebettetes Ausgabe-Panel. Führt Python via subprocess aus und leitet stdout/stderr thread-sicher über eine Queue in das Text-Widget. """ def __init__(self, parent, **kwargs): super().__init__(parent, bg="black", **kwargs) self._process = None self._queue = queue.Queue() self.on_close = None self._build_ui() self._poll() def _build_ui(self): # Header header = tk.Frame(self, bg="gray15", height=28) header.pack(fill=tk.X, side=tk.TOP) header.pack_propagate(False) tk.Label(header, text="▶ Ausgabe", bg="gray15", fg="#a6e22e", font=("Menlo", 12, "bold")).pack(side=tk.LEFT, padx=8, pady=4) for label, cmd, color in [ ("✕ Schließen", self._request_close, "black"), ("⏹ Abbrechen", self.kill_process, "#f92672"), ("- Leeren", self.clear, "black"), ]: tk.Button(header, text=label, command=cmd, bg="gray15", fg=color, relief=tk.FLAT, font=("Menlo", 12), cursor="hand2").pack(side=tk.RIGHT, padx=6) # Ausgabe out_frame = tk.Frame(self, bg="black") out_frame.pack(fill=tk.BOTH, expand=True) self.output = tk.Text( out_frame, bg="#0d0d0d", fg="#f8f8f2", font=("Menlo", 11), wrap="word", state="disabled", relief=tk.FLAT, ) sb = tk.Scrollbar(out_frame, command=self.output.yview, bg="gray20") self.output.config(yscrollcommand=sb.set) sb.pack(side=tk.RIGHT, fill=tk.Y) self.output.pack(fill=tk.BOTH, expand=True) self.output.tag_configure("info", foreground="#75715e") self.output.tag_configure("stdout", foreground="#f8f8f2") self.output.tag_configure("error", foreground="#f92672") self.output.tag_configure("done", foreground="#a6e22e") def _request_close(self): self.kill_process() if self.on_close: self.on_close() def clear(self): self.output.config(state="normal") self.output.delete("1.0", tk.END) self.output.config(state="disabled") def kill_process(self): if self._process and self._process.poll() is None: try: self._process.kill() except Exception: pass def run_python(self, file_path): self.kill_process() self.clear() python = sys.executable self._append(f"$ {python} {os.path.basename(file_path)}\n\n", "info") try: self._process = subprocess.Popen( [python, "-u", file_path], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1, cwd=os.path.dirname(file_path) or ".", ) except Exception as e: self._append(f"Fehler beim Starten: {e}\n", "error") return threading.Thread(target=self._read_output, daemon=True).start() def run_c_cpp(self, file_path, ext): self.kill_process() self.clear() compiler = "g++" if ext in (".cpp", ".cc") else "gcc" if shutil.which(compiler) is None: self._append( f"⚠ '{compiler}' nicht gefunden.\n" "Bitte GCC/G++ installieren und im PATH verfügbar machen.\n", "error") return src_dir = os.path.abspath(os.path.dirname(file_path)) or os.getcwd() src_file = os.path.basename(file_path) bin_name = os.path.splitext(src_file)[0] if platform.system() == "Windows": bin_name += ".exe" launch_cmd = [bin_name] else: launch_cmd = [f"./{bin_name}"] # Compile: run from src_dir so relative #includes work compile_cmd = [compiler, src_file, "-o", bin_name] self._append(f"$ cd {src_dir}\n$ {' '.join(compile_cmd)}\n", "info") try: result = subprocess.run( compile_cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, cwd=src_dir, ) except Exception as e: self._append(f"Compiler-Fehler: {e}\n", "error") return if result.stdout: self._append(result.stdout, "error" if result.returncode != 0 else "stdout") if result.returncode != 0: self._append(f"\n─── Kompilierung fehlgeschlagen (Exit {result.returncode}) ───\n", "error") return self._append("─── Kompilierung erfolgreich ───\n\n", "done") self._append(f"$ {' '.join(launch_cmd)}\n\n", "info") try: self._process = subprocess.Popen( launch_cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1, cwd=src_dir, ) except Exception as e: self._append(f"Fehler beim Starten: {e}\n", "error") return threading.Thread(target=self._read_output, daemon=True).start() def run_java(self, file_path): self.kill_process() self.clear() if shutil.which("javac") is None: self._append( "⚠ 'javac' nicht gefunden.\n" "Bitte das JDK installieren und im PATH verfügbar machen.\n", "error") return src_dir = os.path.abspath(os.path.dirname(file_path)) or os.getcwd() src_file = os.path.basename(file_path) class_name = os.path.splitext(src_file)[0] # "Foo", never "Foo.class" # Compile: javac Foo.java (run from src_dir) compile_cmd = ["javac", src_file] self._append(f"$ cd {src_dir}\n$ {' '.join(compile_cmd)}\n", "info") try: result = subprocess.run( compile_cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, cwd=src_dir, ) except Exception as e: self._append(f"Compiler-Fehler: {e}\n", "error") return if result.stdout: self._append(result.stdout, "error" if result.returncode != 0 else "stdout") if result.returncode != 0: self._append(f"\n─── Kompilierung fehlgeschlagen (Exit {result.returncode}) ───\n", "error") return self._append("─── Kompilierung erfolgreich ───\n\n", "done") # Run: java -cp . Foo (. = current dir = src_dir, already set as cwd) run_cmd = ["java", "-cp", ".", class_name] self._append(f"$ {' '.join(run_cmd)}\n\n", "info") try: self._process = subprocess.Popen( run_cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1, cwd=src_dir, ) except Exception as e: self._append(f"Fehler beim Starten: {e}\n", "error") return threading.Thread(target=self._read_output, daemon=True).start() def run_latex(self, file_path): """Compile with pdflatex (twice for refs) then open/reload in Skim.""" self.kill_process() self.clear() if shutil.which("pdflatex") is None: self._append( "⚠ 'pdflatex' nicht gefunden.\n" "Bitte eine LaTeX-Distribution (TeX Live, MiKTeX) installieren.\n", "error") return src_dir = os.path.dirname(file_path) or "." basename = os.path.basename(file_path) pdf_path = os.path.splitext(file_path)[0] + ".pdf" # Run pdflatex twice so cross-references are resolved for pass_num in (1, 2): cmd = ["pdflatex", "-interaction=nonstopmode", basename] self._append(f"─── Durchlauf {pass_num}/2: {' '.join(cmd)}\n", "info") try: result = subprocess.run( cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, cwd=src_dir, ) except Exception as e: self._append(f"Fehler: {e}\n", "error") return # Show only warnings/errors to keep output readable for line in result.stdout.splitlines(): low = line.lower() if any(k in low for k in ("error", "warning", "!", "undefined")): self._append(line + "\n", "error" if ("error" in low or line.startswith("!")) else "stdout") if result.returncode != 0: self._append( f"\n─── pdflatex fehlgeschlagen (Exit {result.returncode}) ───\n", "error") self._append("Überprüfe die .log-Datei für Details.\n", "info") return self._append( f"\n─── Kompilierung erfolgreich → {os.path.basename(pdf_path)} ───\n", "done") # Open / reload in Skim # "reloadFileAtPath:" tells an already-open Skim window to refresh. skim = "/Applications/Skim.app" skim_bin = os.path.join(skim, "Contents/SharedSupport/displayline") abs_pdf = os.path.abspath(pdf_path) try: if os.path.isfile(skim_bin): # displayline — opens at current line; # using line 1 so it always works even without SyncTeX. subprocess.Popen([skim_bin, "1", abs_pdf, os.path.abspath(file_path)]) self._append("PDF wird in Skim angezeigt …\n", "info") elif os.path.isdir(skim): subprocess.Popen(["open", "-a", "Skim", abs_pdf]) self._append("PDF wird in Skim geöffnet …\n", "info") else: # Skim not installed — fall back to system default and warn self._append("⚠ Skim nicht gefunden (/Applications/Skim.app).\n" "PDF wird mit dem Standard-Viewer geöffnet …\n", "error") subprocess.Popen(["open", abs_pdf]) except Exception as e: self._append(f"PDF öffnen fehlgeschlagen: {e}\n", "error") def _read_output(self): """Read stdout/stderr from self._process line by line into the queue.""" try: for line in self._process.stdout: self._queue.put(("out", line)) except Exception: pass finally: rc = self._process.wait() self._queue.put(("done", rc)) def _poll(self): try: while True: kind, data = self._queue.get_nowait() if kind == "out": tag = "error" if ("Error" in data or "Traceback" in data) else "stdout" self._append(data, tag) elif kind == "done": color = "done" if data == 0 else "error" self._append(f"\n─── Exit-Code {data} ───\n", color) except queue.Empty: pass self.after(50, self._poll) def _append(self, text, tag="stdout"): self.output.config(state="normal") self.output.insert(tk.END, text, tag) self.output.see(tk.END) self.output.config(state="disabled") # ── Haupteditor ─────────────────────────────────────────────────────────────── class SimpleTextEditor: def __init__(self, root): self.root = root self.root.title("Slime text") self.root.geometry("1100x750") self._find_dialog = None self._find_replace_dialog = None self.root.drop_target_register(DND_FILES) self.root.dnd_bind('<>', self._on_file_drop) # Haupt-Layout: Editor oben, Terminal unten (ein-/ausblendbar) self.main_pane = tk.PanedWindow( self.root, orient=tk.VERTICAL, bg="gray20", sashwidth=5, sashrelief=tk.FLAT) self.main_pane.pack(fill=tk.BOTH, expand=True) self.notebook = ttk.Notebook(self.main_pane) self.main_pane.add(self.notebook, stretch="always") self.notebook.bind("", self._show_tab_menu) self.terminal_panel = TerminalPanel(self.main_pane) self.terminal_panel.on_close = self._hide_terminal self._terminal_visible = False self.tabs = {} self.font_size = tk.IntVar(value=12) self.create_menu() self.bind_shortcuts() self.add_new_tab() # ── Terminal ────────────────────────────────────────────────────────────── def _show_terminal(self): if not self._terminal_visible: self.main_pane.add(self.terminal_panel, height=220, stretch="never") self._terminal_visible = True def _hide_terminal(self): if self._terminal_visible: self.main_pane.remove(self.terminal_panel) self._terminal_visible = False # ── Tab erstellen ───────────────────────────────────────────────────────── def add_new_tab(self, file_path=None, content=""): tab_frame = tk.Frame(self.notebook, bg="gray20") # Sidebar-Miniaturansicht canvas = tk.Canvas(tab_frame, width=120, bg="gray20", highlightthickness=0) canvas.pack(side=tk.RIGHT, fill=tk.Y) # Linker Bereich: Zeilennummern-Canvas + Textbereich text_container = tk.Frame(tab_frame, bg="gray20") text_container.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) font = ("Menlo", self.font_size.get()) # Zeilennummern-Canvas (Canvas statt Text-Widget → pixelgenaue Platzierung) ln_canvas = tk.Canvas(text_container, width=50, bg="gray25", highlightthickness=0) ln_canvas.pack(side=tk.LEFT, fill=tk.Y) # Horizontale Scrollbar h_scroll = tk.Scrollbar(text_container, orient=tk.HORIZONTAL, bg="gray30") h_scroll.pack(side=tk.BOTTOM, fill=tk.X) # Haupttext-Widget text_area = tk.Text( text_container, undo=True, wrap='none', font=font, bg="gray20", fg="#f8f8f2", insertbackground="white", padx=6, pady=5, height=1, width=1, xscrollcommand=h_scroll.set, ) text_area.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) if content: text_area.insert(tk.END, content) h_scroll.config(command=text_area.xview) text_area.config( yscrollcommand=lambda f, l, c=canvas: self._on_tab_scroll(f, l, c)) title = os.path.basename(file_path) if file_path else "Unbenannt" self.notebook.add(tab_frame, text=title) self.notebook.select(tab_frame) tab_id = str(tab_frame) self.tabs[tab_id] = { "text": text_area, "ln_canvas": ln_canvas, "canvas": canvas, "file": file_path, "font": font, "dragging": False, "drag_offset_y": 0, "sidebar_text_id": None, } highlighter = SyntaxHighlighter(text_area) self.tabs[tab_id]["highlighter"] = highlighter # Sidebar-Events canvas.bind("", lambda e, tid=tab_id: self._update_sidebar_content(tid)) canvas.bind("", lambda e, tid=tab_id: self._on_mouse_down(e, tid)) canvas.bind("", lambda e, tid=tab_id: self._on_mouse_drag(e, tid)) canvas.bind("", lambda e, tid=tab_id: self._on_mouse_up(e, tid)) # Text-Events text_area.bind("", lambda e, tid=tab_id: self._on_text_change(tid)) text_area.bind("", lambda e, tid=tab_id: self._on_text_change(tid)) text_area.bind("<>", lambda e, tid=tab_id: self.root.after(50, lambda: self._on_paste(tid))) # Mausrad im Textbereich → Zeilennummern nachziehen for seq in ("", "", ""): text_area.bind(seq, lambda e, tid=tab_id: self.root.after(10, lambda: self._redraw_line_numbers(tid))) # Mausrad auf Zeilennummern-Canvas → an Text weiterleiten ln_canvas.bind("", lambda e, tid=tab_id: self._forward_scroll(e, tid)) ln_canvas.bind("", lambda e, tid=tab_id: self._forward_scroll(e, tid)) ln_canvas.bind("", lambda e, tid=tab_id: self._forward_scroll(e, tid)) # Canvas-Größenänderung → Nummern neu zeichnen ln_canvas.bind("", lambda e, tid=tab_id: self._redraw_line_numbers(tid)) self.root.after_idle(lambda tid=tab_id: ( self._redraw_line_numbers(tid), self._update_sidebar_content(tid), self.tabs[tid]["highlighter"].highlight(file_path or ""), )) # ── Zeilennummern (pixelgenau via dlineinfo) ────────────────────────────── def _redraw_line_numbers(self, tid): if tid not in self.tabs: return tab = self.tabs[tid] text = tab["text"] canvas = tab["ln_canvas"] font = tab["font"] canvas.delete("all") first_index = text.index("@0,0") line_num = int(first_index.split(".")[0]) while True: info = text.dlineinfo(f"{line_num}.0") if info is None: break x, y, w, h, baseline = info canvas.create_text( canvas.winfo_width() - 6, y + h // 2, text=str(line_num), anchor="e", font=font, fill="gray80", ) line_num += 1 def _forward_scroll(self, event, tid): if tid not in self.tabs: return "break" text = self.tabs[tid]["text"] if event.num == 4: text.yview_scroll(-1, "units") elif event.num == 5: text.yview_scroll(1, "units") else: text.yview_scroll(int(-1 * (event.delta / 120)), "units") self._redraw_line_numbers(tid) return "break" # ── Änderungs-Callbacks ─────────────────────────────────────────────────── def _on_paste(self, tid): if tid not in self.tabs: return tab = self.tabs[tid] tab["highlighter"].highlight(tab["file"] or "") self._redraw_line_numbers(tid) self._update_sidebar_content(tid) def _on_text_change(self, tid): if tid not in self.tabs: return tab = self.tabs[tid] tab["highlighter"].highlight(tab["file"] or "") self._redraw_line_numbers(tid) self._update_sidebar_content(tid) def _on_tab_scroll(self, first, last, canvas): tid = self._get_current_tab_id() if tid in self.tabs and not self.tabs[tid]["dragging"]: self._update_viewport_position(tid) self._redraw_line_numbers(tid) # ── Hilfsmethoden ───────────────────────────────────────────────────────── def _get_current_tab_id(self): return self.notebook.select() def _get_current_text_widget(self): tid = self._get_current_tab_id() return self.tabs[tid]["text"] if tid in self.tabs else None def close_current_tab(self): if len(self.notebook.tabs()) > 1: tid = self._get_current_tab_id() self.notebook.forget(tid) del self.tabs[tid] else: tid = self._get_current_tab_id() self.tabs[tid]["text"].delete(1.0, tk.END) self.tabs[tid]["file"] = None self.notebook.tab(tid, text="Unbenannt") self._redraw_line_numbers(tid) self._update_sidebar_content(tid) def _on_file_drop(self, event): for f_path in self.root.tk.splitlist(event.data): try: with open(f_path, 'r', encoding='utf-8') as f: content = f.read() self.add_new_tab(file_path=f_path, content=content) except Exception as e: messagebox.showerror("Fehler", f"Konnte {f_path} nicht öffnen:\n{e}") # ── Sidebar-Miniaturansicht ──────────────────────────────────────────────── def _update_sidebar_content(self, tid): if tid not in self.tabs: return tab = self.tabs[tid] canvas = tab["canvas"] filename = tab["file"] or "" code = tab["text"].get("1.0", tk.END).rstrip('\n') canvas.delete("all") tab["sidebar_text_id"] = None # ── build colored token list ────────────────────────────────────────── # Reuse the same highlighter rules to produce (color, text) segments. tokens = _tokenize_for_sidebar(code, filename) # ── render token spans line by line onto the canvas ─────────────────── FONT = ("Menlo", 2) LINE_H = 3 # px per line in the minimap X_START = 4 X_MAX = 112 # wrap at this x CHAR_W = 1.18 # approximate px width per character at font size 2 x, y = X_START, 4 for color, segment in tokens: for ch in segment: if ch == '\n': x = X_START y += LINE_H continue if x >= X_MAX: # don't render characters beyond right margin (no visual wrap) continue canvas.create_text(x, y, text=ch, anchor="nw", font=FONT, fill=color) x += CHAR_W # update scroll region total_y = y + LINE_H + 4 canvas_h = max(total_y, canvas.winfo_height()) canvas.config(scrollregion=(0, 0, 116, canvas_h)) # remember any item as anchor for tag_lower in viewport drawing all_items = canvas.find_all() tab["sidebar_text_id"] = all_items[0] if all_items else None self._update_viewport_position(tid) def _update_viewport_position(self, tid): tab = self.tabs[tid] canvas = tab["canvas"] text = tab["text"] canvas.delete("viewport") try: first, last = text.yview() sr = canvas.cget("scrollregion").split() total_h = float(sr[3]) - float(sr[1]) v_top = float(sr[1]) + first * total_h v_bottom = v_top + (last - first) * total_h canvas.create_rectangle(0, v_top, 116, v_bottom, fill="gray30", outline="", stipple="gray50", tags="viewport") # keep viewport rectangle below text items for item in canvas.find_withtag("viewport"): canvas.tag_lower(item) c_height = canvas.winfo_height() curr_start, curr_end = canvas.yview() if total_h <= 0: return if v_bottom > curr_end * total_h: canvas.yview_moveto((v_bottom - c_height) / total_h) elif v_top < curr_start * total_h: canvas.yview_moveto(v_top / total_h) except Exception: pass def _on_mouse_down(self, event, tid): tab = self.tabs[tid] cy = tab["canvas"].canvasy(event.y) items = tab["canvas"].find_withtag("viewport") if items: coords = tab["canvas"].coords(items[0]) if len(coords) >= 4 and coords[1] <= cy <= coords[3]: tab["dragging"] = True tab["drag_offset_y"] = cy - coords[1] tab["canvas"].config(cursor="hand2") def _on_mouse_drag(self, event, tid): tab = self.tabs[tid] if not tab["dragging"]: return try: sr = tab["canvas"].cget("scrollregion").split() total_h = float(sr[3]) - float(sr[1]) cy = tab["canvas"].canvasy(event.y) new_top = (cy - tab["drag_offset_y"] - float(sr[1])) / total_h new_top = max(0.0, min( new_top, 1.0 - (tab["text"].yview()[1] - tab["text"].yview()[0]))) tab["text"].yview_moveto(new_top) self._update_viewport_position(tid) self._redraw_line_numbers(tid) except Exception: pass def _on_mouse_up(self, event, tid): self.tabs[tid]["dragging"] = False self.tabs[tid]["canvas"].config(cursor="") # ── Ausführen (universell) ──────────────────────────────────────────────── def run_file(self): tid = self._get_current_tab_id() if tid not in self.tabs: return tab = self.tabs[tid] file_path = tab["file"] if not file_path: messagebox.showwarning("Nicht gespeichert", "Bitte zuerst speichern (Ctrl+S).") return ext = os.path.splitext(file_path)[1].lower() if ext == ".py": self._show_terminal() self.terminal_panel.run_python(file_path) elif ext in (".c", ".cpp", ".cc"): self._show_terminal() self.terminal_panel.run_c_cpp(file_path, ext) elif ext == ".java": self._show_terminal() self.terminal_panel.run_java(file_path) elif ext in (".html", ".htm"): import webbrowser webbrowser.open(f"file://{os.path.abspath(file_path)}") elif ext in (".tex", ".ltx"): self._show_terminal() self.terminal_panel.run_latex(file_path) else: messagebox.showinfo("Nicht unterstützt", f"Keine Ausführung für '{ext}' konfiguriert.") # keep old name as alias for shortcut compatibility def run_python(self): self.run_file() # ── Word Count ──────────────────────────────────────────────────────────── def show_word_count(self): tid = self._get_current_tab_id() if tid not in self.tabs: return text = self.tabs[tid]["text"].get("1.0", "end-1c") lines = text.splitlines() words = text.split() chars = len(text) chars_no_space = len(text.replace(" ", "").replace("\n", "").replace("\t", "")) win = tk.Toplevel(self.root) win.title("Wörter zählen") win.configure(bg="gray20") win.resizable(False, False) win.transient(self.root) frame = tk.Frame(win, bg="gray20", padx=20, pady=16) frame.pack() for i, (label, value) in enumerate([ ("Zeilen", len(lines)), ("Wörter", len(words)), ("Zeichen (gesamt)", chars), ("Zeichen (ohne Leerz.)", chars_no_space), ]): tk.Label(frame, text=label + ":", bg="gray20", fg="#75715e", font=("Menlo", 11), anchor="w", width=24 ).grid(row=i, column=0, sticky="w", pady=3) tk.Label(frame, text=str(value), bg="gray20", fg="#f8f8f2", font=("Menlo", 11, "bold"), anchor="e", width=8 ).grid(row=i, column=1, sticky="e", pady=3) tk.Button(frame, text="Schließen", command=win.destroy, bg="gray35", fg="white", relief=tk.FLAT, font=("Menlo", 10)).grid(row=4, column=0, columnspan=2, pady=(12, 0)) win.bind("", lambda e: win.destroy()) # ── Suche ───────────────────────────────────────────────────────────────── def open_find_dialog(self): text = self._get_current_text_widget() if not text: return if self._find_dialog and self._find_dialog.winfo_exists(): self._find_dialog.lift() self._find_dialog.entry_search.focus_set() return self._find_dialog = FindDialog(self.root, text) def open_find_replace_dialog(self): text = self._get_current_text_widget() if not text: return if self._find_replace_dialog and self._find_replace_dialog.winfo_exists(): self._find_replace_dialog.lift() self._find_replace_dialog.entry_search.focus_set() return self._find_replace_dialog = FindReplaceDialog(self.root, text) # ── Menü ────────────────────────────────────────────────────────────────── def create_menu(self): menubar = Menu(self.root) self.root.config(menu=menubar) file_menu = Menu(menubar, tearoff=0) menubar.add_cascade(label="Datei", menu=file_menu) file_menu.add_command(label="Neuer Tab", command=self.add_new_tab, accelerator="Ctrl+N") file_menu.add_command(label="Öffnen...", command=self.open_file, accelerator="Ctrl+O") file_menu.add_command(label="Speichern", command=self.save_file, accelerator="Ctrl+S") file_menu.add_command(label="Speichern unter...", command=self.save_as_file, accelerator="Ctrl+Shift+S") file_menu.add_separator() file_menu.add_command(label="Tab schließen", command=self.close_current_tab, accelerator="Ctrl+W") file_menu.add_command(label="Beenden", command=self.root.quit) search_menu = Menu(menubar, tearoff=0) menubar.add_cascade(label="Suche", menu=search_menu) search_menu.add_command(label="Suchen...", command=self.open_find_dialog, accelerator="Cmd+F") search_menu.add_command(label="Suchen & Ersetzen...", command=self.open_find_replace_dialog, accelerator="Cmd+Shift+F") run_menu = Menu(menubar, tearoff=0) menubar.add_cascade(label="Ausführen", menu=run_menu) run_menu.add_command(label="Ausführen (Py / C / C++ / Java / HTML / LaTeX)", command=self.run_file, accelerator="Cmd+B") tools_menu = Menu(menubar, tearoff=0) menubar.add_cascade(label="Tools", menu=tools_menu) tools_menu.add_command(label="Wörter zählen...", command=self.show_word_count) font_menu = Menu(menubar, tearoff=0) menubar.add_cascade(label="Schriftgröße", menu=font_menu) font_menu.add_command(label="Vergrößern", command=lambda: self.change_font_size(1), accelerator="Ctrl++") font_menu.add_command(label="Verkleinern", command=lambda: self.change_font_size(-1), accelerator="Ctrl+-") font_menu.add_separator() font_menu.add_command(label="Standard (12)", command=lambda: self.set_font_size(12)) # ── Shortcuts ───────────────────────────────────────────────────────────── def bind_shortcuts(self): for mod in ("", ""): self.root.bind(mod, lambda e: self.add_new_tab()) for mod in ("", ""): self.root.bind(mod, lambda e: self.open_file()) for mod in ("", ""): self.root.bind(mod, lambda e: self.save_file()) for mod in ("", ""): self.root.bind(mod, lambda e: self.save_as_file()) for mod in ("", ""): self.root.bind(mod, lambda e: self.close_current_tab()) for mod in ("", ""): self.root.bind(mod, lambda e: self.change_font_size(1)) for mod in ("", ""): self.root.bind(mod, lambda e: self.change_font_size(-1)) for mod in ("", ""): self.root.bind(mod, lambda e: self.open_find_dialog()) for mod in ("", ""): self.root.bind(mod, lambda e: self.open_find_replace_dialog()) for mod in ("", ""): self.root.bind(mod, lambda e: self.run_file()) # ── Schriftgröße ────────────────────────────────────────────────────────── def change_font_size(self, delta): self.set_font_size(max(8, min(self.font_size.get() + delta, 32))) def set_font_size(self, size): self.font_size.set(size) font = ("Menlo", size) for tid, tab_data in self.tabs.items(): tab_data["font"] = font tab_data["text"].configure(font=font) self._redraw_line_numbers(tid) current_tid = self._get_current_tab_id() if current_tid in self.tabs: self._update_sidebar_content(current_tid) # ── Datei-Operationen ───────────────────────────────────────────────────── def open_file(self): f_path = filedialog.askopenfilename( title="Datei öffnen", filetypes=[("Alle Dateien", "*.*")] #,("Textdateien", "*.txt"), ("Python", "*.py"),("C-Files", "*.c"),("C++ Files", "*.cpp") ) if f_path: try: with open(f_path, 'r', encoding='utf-8') as f: content = f.read() self.add_new_tab(file_path=f_path, content=content) except Exception as e: messagebox.showerror("Fehler beim Öffnen", f"Datei konnte nicht gelesen werden:\n{e}") def save_file(self): tid = self._get_current_tab_id() tab = self.tabs[tid] if not tab["file"]: self.save_as_file() else: try: content = tab["text"].get(1.0, tk.END) enc, errors = self._encoding_for(tab["file"]) with open(tab["file"], 'w', encoding=enc, errors=errors) as f: f.write(content) except Exception as e: messagebox.showerror("Fehler beim Speichern", f"Datei konnte nicht gespeichert werden:\n{e}") def save_as_file(self): tid = self._get_current_tab_id() tab = self.tabs[tid] f_path = filedialog.asksaveasfilename( title="Speichern unter", defaultextension=".txt", filetypes=[("Alle Dateien", "*.*"), ("Python", "*.py"), ("LaTeX", "*.tex"), ("Textdateien", "*.txt")] ) if not f_path: return try: tab["file"] = f_path self.notebook.tab(tid, text=os.path.basename(f_path)) content = tab["text"].get(1.0, tk.END) enc, errors = self._encoding_for(f_path) with open(tab["file"], 'w', encoding=enc, errors=errors) as f: f.write(content) except Exception as e: messagebox.showerror("Fehler beim Speichern unter", f"Datei konnte nicht gespeichert werden:\n{e}") @staticmethod def _encoding_for(file_path): """Return (encoding, errors) tuple for writing the given file. LaTeX files are saved as ASCII with \\uXXXX escaping for any character whose code point is > 127, so pdflatex never trips over unexpected byte sequences. All other files use UTF-8. """ ext = os.path.splitext(file_path)[1].lower() if ext in (".tex", ".ltx", ".sty", ".cls"): return ("ascii", "backslashreplace") return ("utf-8", "strict") def _show_tab_menu(self, event): menu = Menu(self.root, tearoff=0) menu.add_command(label="Tab schließen", command=self.close_current_tab) menu.post(event.x_root, event.y_root) # ── Start ───────────────────────────────────────────────────────────────────── if __name__ == "__main__": root = TkinterDnD.Tk() editor = SimpleTextEditor(root) root.mainloop()