bash-script-maker 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
syntax_highlighter.py ADDED
@@ -0,0 +1,1026 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Syntax-Highlighter für Bash-Scripts
5
+ """
6
+
7
+ import tkinter as tk
8
+ from tkinter import scrolledtext, Listbox, Toplevel
9
+ import ttkbootstrap as ttk
10
+ from ttkbootstrap.scrolled import ScrolledText
11
+ import re
12
+ import os
13
+ import glob
14
+
15
+
16
+ class BashAutocomplete:
17
+ """Autovervollständigung für Bash-Scripts"""
18
+
19
+ def __init__(self, text_widget):
20
+ self.text_widget = text_widget
21
+ self.suggestions_window = None
22
+ self.suggestions_listbox = None
23
+ self.current_suggestions = []
24
+ self.current_word_start = None
25
+ self.current_word_end = None
26
+
27
+ # Bash-Befehle und Schlüsselwörter
28
+ self.bash_commands = {
29
+ # Grundlegende Befehle
30
+ "echo",
31
+ "printf",
32
+ "read",
33
+ "exit",
34
+ "return",
35
+ "cd",
36
+ "pwd",
37
+ "ls",
38
+ "cp",
39
+ "mv",
40
+ "rm",
41
+ "mkdir",
42
+ "rmdir",
43
+ "touch",
44
+ "cat",
45
+ "grep",
46
+ "sed",
47
+ "awk",
48
+ "find",
49
+ "chmod",
50
+ "chown",
51
+ "ps",
52
+ "kill",
53
+ "killall",
54
+ "top",
55
+ "df",
56
+ "du",
57
+ "free",
58
+ "uname",
59
+ "whoami",
60
+ "id",
61
+ "groups",
62
+ "passwd",
63
+ "su",
64
+ "sudo",
65
+ "which",
66
+ "whereis",
67
+ "locate",
68
+ "updatedb",
69
+ "tar",
70
+ "gzip",
71
+ "gunzip",
72
+ "bzip2",
73
+ "bunzip2",
74
+ "zip",
75
+ "unzip",
76
+ "wget",
77
+ "curl",
78
+ # Erweiterte Befehle
79
+ "head",
80
+ "tail",
81
+ "sort",
82
+ "uniq",
83
+ "wc",
84
+ "cut",
85
+ "paste",
86
+ "tr",
87
+ "diff",
88
+ "patch",
89
+ "comm",
90
+ "join",
91
+ "xargs",
92
+ "tee",
93
+ "yes",
94
+ "seq",
95
+ "bc",
96
+ "dc",
97
+ "expr",
98
+ "let",
99
+ "declare",
100
+ "export",
101
+ "unset",
102
+ "set",
103
+ "shift",
104
+ "getopts",
105
+ "source",
106
+ "eval",
107
+ "exec",
108
+ "trap",
109
+ "wait",
110
+ "jobs",
111
+ "fg",
112
+ "bg",
113
+ "disown",
114
+ # Systembefehle
115
+ "mount",
116
+ "umount",
117
+ "fdisk",
118
+ "mkfs",
119
+ "fsck",
120
+ "dd",
121
+ "mkswap",
122
+ "swapon",
123
+ "swapoff",
124
+ "sysctl",
125
+ "modprobe",
126
+ "lsmod",
127
+ "insmod",
128
+ "rmmod",
129
+ "lspci",
130
+ "lsusb",
131
+ "ifconfig",
132
+ "ip",
133
+ "route",
134
+ "netstat",
135
+ "ss",
136
+ "ping",
137
+ "traceroute",
138
+ "nslookup",
139
+ "dig",
140
+ "host",
141
+ "hostname",
142
+ "date",
143
+ "cal",
144
+ "uptime",
145
+ # Paketverwaltung
146
+ "apt",
147
+ "apt-get",
148
+ "dpkg",
149
+ "rpm",
150
+ "yum",
151
+ "dnf",
152
+ "pacman",
153
+ "zypper",
154
+ "snap",
155
+ "flatpak",
156
+ # Text-Editoren
157
+ "vi",
158
+ "vim",
159
+ "nano",
160
+ "emacs",
161
+ "gedit",
162
+ "kate",
163
+ "code",
164
+ "subl",
165
+ # Entwicklungstools
166
+ "gcc",
167
+ "g++",
168
+ "make",
169
+ "cmake",
170
+ "git",
171
+ "svn",
172
+ "hg",
173
+ "docker",
174
+ "podman",
175
+ "kubectl",
176
+ }
177
+
178
+ # Bash-Schlüsselwörter
179
+ self.bash_keywords = {
180
+ "if",
181
+ "then",
182
+ "else",
183
+ "elif",
184
+ "fi",
185
+ "for",
186
+ "while",
187
+ "until",
188
+ "do",
189
+ "done",
190
+ "case",
191
+ "esac",
192
+ "select",
193
+ "function",
194
+ "local",
195
+ "readonly",
196
+ "declare",
197
+ "typeset",
198
+ "export",
199
+ "unset",
200
+ "break",
201
+ "continue",
202
+ "return",
203
+ "exit",
204
+ "trap",
205
+ "eval",
206
+ "exec",
207
+ "source",
208
+ "alias",
209
+ "unalias",
210
+ "builtin",
211
+ "command",
212
+ "type",
213
+ "hash",
214
+ "help",
215
+ "history",
216
+ "fc",
217
+ "bind",
218
+ "set",
219
+ "shopt",
220
+ "caller",
221
+ "false",
222
+ "true",
223
+ }
224
+
225
+ # Häufige Optionen für Befehle
226
+ self.command_options = {
227
+ "ls": ["-l", "-a", "-h", "-la", "-lh", "-1", "-R", "-t", "-S", "-X"],
228
+ "cp": ["-r", "-i", "-v", "-p", "-u", "-n", "--backup"],
229
+ "mv": ["-i", "-v", "-u", "-n", "--backup"],
230
+ "rm": ["-i", "-r", "-f", "-v", "--interactive=never"],
231
+ "mkdir": ["-p", "-v", "-m"],
232
+ "chmod": ["-R", "-v", "+x", "+r", "+w", "755", "644"],
233
+ "chown": ["-R", "-v", "--reference"],
234
+ "grep": ["-i", "-v", "-r", "-n", "-l", "-c", "--color"],
235
+ "find": ["-name", "-type", "-exec", "-delete", "-mtime", "-size"],
236
+ "ps": ["aux", "ef", "-p", "-u", "-C"],
237
+ "tar": ["-xzf", "-czf", "-tzf", "-xvzf", "-cvzf"],
238
+ "git": [
239
+ "status",
240
+ "add",
241
+ "commit",
242
+ "push",
243
+ "pull",
244
+ "clone",
245
+ "branch",
246
+ "checkout",
247
+ "merge",
248
+ "log",
249
+ ],
250
+ }
251
+
252
+ # Event-Bindings für Autocomplete
253
+ self.bind_events()
254
+
255
+ def bind_events(self):
256
+ """Bindet Events für Autocomplete"""
257
+ # Strg+Space für Autocomplete
258
+ self.text_widget.bind("<Control-space>", self.show_suggestions)
259
+ # Tab für Autocomplete (alternative zu Tab für Einrückung)
260
+ self.text_widget.bind("<Control-Tab>", self.show_suggestions)
261
+ # Escape zum Schließen der Vorschlagsliste
262
+ self.text_widget.bind("<Escape>", self.hide_suggestions)
263
+
264
+ def get_current_word_bounds(self):
265
+ """Ermittelt die Grenzen des aktuellen Wortes unter dem Cursor"""
266
+ cursor_pos = self.text_widget.index(tk.INSERT)
267
+ line, col = cursor_pos.split(".")
268
+
269
+ # Hole die aktuelle Zeile
270
+ line_content = self.text_widget.get(f"{line}.0", f"{line}.end")
271
+
272
+ # Finde Wortgrenzen
273
+ word_start = col
274
+ word_end = col
275
+
276
+ # Gehe rückwärts zum Wortanfang
277
+ while word_start > 0 and (
278
+ line_content[int(word_start) - 1].isalnum()
279
+ or line_content[int(word_start) - 1] in "_-$/"
280
+ ):
281
+ word_start = str(int(word_start) - 1)
282
+
283
+ # Gehe vorwärts zum Wortende
284
+ while int(word_end) < len(line_content) and (
285
+ line_content[int(word_end)].isalnum()
286
+ or line_content[int(word_end)] in "_-$/"
287
+ ):
288
+ word_end = str(int(word_end) + 1)
289
+
290
+ return f"{line}.{word_start}", f"{line}.{word_end}"
291
+
292
+ def get_current_word(self):
293
+ """Gibt das aktuelle Wort unter dem Cursor zurück"""
294
+ start, end = self.get_current_word_bounds()
295
+ return self.text_widget.get(start, end).strip()
296
+
297
+ def get_context_aware_suggestions(self, partial_word, line_content, cursor_pos):
298
+ """Generiert kontextabhängige Vorschläge"""
299
+ suggestions = set()
300
+
301
+ # Position in der Zeile
302
+ line_num, col = cursor_pos.split(".")
303
+ prefix = line_content[: int(col)].strip()
304
+
305
+ # Prüfe Kontext
306
+ if not partial_word:
307
+ # Am Zeilenanfang - zeige alle Befehle
308
+ suggestions.update(self.bash_commands)
309
+ suggestions.update(self.bash_keywords)
310
+ elif partial_word.startswith("$"):
311
+ # Variablen
312
+ suggestions.update(self.get_variable_suggestions())
313
+ elif partial_word.startswith("/"):
314
+ # Pfadvervollständigung
315
+ suggestions.update(self.get_path_suggestions(partial_word))
316
+ elif partial_word.startswith("./") or partial_word.startswith("../"):
317
+ # Relativer Pfad
318
+ suggestions.update(self.get_path_suggestions(partial_word))
319
+ elif partial_word in self.command_options:
320
+ # Optionen für bekannten Befehl
321
+ suggestions.update(self.command_options[partial_word])
322
+ else:
323
+ # Normale Befehle und Schlüsselwörter
324
+ # Filtere basierend auf Eingabe
325
+ for cmd in self.bash_commands:
326
+ if cmd.startswith(partial_word):
327
+ suggestions.add(cmd)
328
+
329
+ for keyword in self.bash_keywords:
330
+ if keyword.startswith(partial_word):
331
+ suggestions.add(keyword)
332
+
333
+ # Wenn keine direkten Übereinstimmungen, zeige ähnliche
334
+ if not suggestions:
335
+ suggestions.update(self.get_similar_suggestions(partial_word))
336
+
337
+ return sorted(list(suggestions))
338
+
339
+ def get_variable_suggestions(self):
340
+ """Sammelt alle Variablen aus dem Script"""
341
+ variables = set()
342
+
343
+ # Häufige Bash-Variablen
344
+ variables.update(
345
+ [
346
+ "$HOME",
347
+ "$PATH",
348
+ "$PWD",
349
+ "$USER",
350
+ "$SHELL",
351
+ "$0",
352
+ "$1",
353
+ "$2",
354
+ "$3",
355
+ "$?",
356
+ "$$",
357
+ "$!",
358
+ ]
359
+ )
360
+
361
+ # Extrahiere benutzerdefinierte Variablen aus dem Text
362
+ text_content = self.text_widget.get("1.0", tk.END)
363
+ var_pattern = r"\b([A-Za-z_][A-Za-z0-9_]*)\s*="
364
+ for match in re.finditer(var_pattern, text_content):
365
+ var_name = match.group(1)
366
+ variables.add(f"${var_name}")
367
+
368
+ return variables
369
+
370
+ def get_path_suggestions(self, partial_path):
371
+ """Generiert Pfadvorschläge"""
372
+ suggestions = set()
373
+
374
+ try:
375
+ # Expandiere ~ zu Home-Verzeichnis
376
+ expanded_path = os.path.expanduser(partial_path)
377
+
378
+ # Finde das Verzeichnis und den Präfix
379
+ if os.path.isdir(expanded_path):
380
+ base_dir = expanded_path
381
+ prefix = ""
382
+ else:
383
+ base_dir = os.path.dirname(expanded_path) or "."
384
+ prefix = os.path.basename(expanded_path)
385
+
386
+ # Liste Dateien/Verzeichnisse auf
387
+ if os.path.exists(base_dir):
388
+ for item in os.listdir(base_dir):
389
+ if item.startswith(prefix):
390
+ full_path = os.path.join(base_dir, item)
391
+ if os.path.isdir(full_path):
392
+ suggestions.add(
393
+ os.path.join(os.path.dirname(partial_path), item) + "/"
394
+ )
395
+ else:
396
+ suggestions.add(
397
+ os.path.join(os.path.dirname(partial_path), item)
398
+ )
399
+
400
+ except (OSError, ValueError):
401
+ pass
402
+
403
+ return suggestions
404
+
405
+ def get_similar_suggestions(self, partial_word):
406
+ """Findet ähnliche Befehle/Schlüsselwörter"""
407
+ suggestions = set()
408
+ partial_lower = partial_word.lower()
409
+
410
+ # Fuzzy-Matching für Befehle
411
+ for cmd in self.bash_commands:
412
+ if partial_lower in cmd.lower():
413
+ suggestions.add(cmd)
414
+
415
+ # Fuzzy-Matching für Schlüsselwörter
416
+ for keyword in self.bash_keywords:
417
+ if partial_lower in keyword.lower():
418
+ suggestions.add(keyword)
419
+
420
+ return suggestions
421
+
422
+ def show_suggestions(self, event=None):
423
+ """Zeigt Vorschlagsliste an"""
424
+ # Verstecke bestehende Vorschläge
425
+ self.hide_suggestions()
426
+
427
+ # Hole aktuelles Wort und Kontext
428
+ current_word = self.get_current_word()
429
+ cursor_pos = self.text_widget.index(tk.INSERT)
430
+ line_num, col = cursor_pos.split(".")
431
+ line_content = self.text_widget.get(f"{line_num}.0", f"{line_num}.end")
432
+
433
+ # Generiere Vorschläge
434
+ suggestions = self.get_context_aware_suggestions(
435
+ current_word, line_content, cursor_pos
436
+ )
437
+
438
+ if not suggestions:
439
+ return "break"
440
+
441
+ # Erstelle Vorschlagsfenster
442
+ self.current_word_start, self.current_word_end = self.get_current_word_bounds()
443
+ self.current_suggestions = suggestions
444
+
445
+ # Position für Vorschlagsfenster berechnen
446
+ bbox = self.text_widget.bbox(self.current_word_end)
447
+ if bbox:
448
+ x, y, width, height = bbox
449
+ x += self.text_widget.winfo_rootx()
450
+ y += self.text_widget.winfo_rooty() + height
451
+ else:
452
+ # Fallback-Position
453
+ x = self.text_widget.winfo_rootx() + 50
454
+ y = self.text_widget.winfo_rooty() + 100
455
+
456
+ # Erstelle Toplevel-Fenster
457
+ self.suggestions_window = Toplevel(self.text_widget)
458
+ self.suggestions_window.wm_overrideredirect(True)
459
+ self.suggestions_window.wm_geometry(f"+{x}+{y}")
460
+
461
+ # Erstelle Listbox
462
+ self.suggestions_listbox = Listbox(
463
+ self.suggestions_window,
464
+ height=min(len(suggestions), 10),
465
+ width=30,
466
+ font=("Courier", 10),
467
+ )
468
+
469
+ # Füge Vorschläge hinzu
470
+ for suggestion in suggestions:
471
+ self.suggestions_listbox.insert(tk.END, suggestion)
472
+
473
+ self.suggestions_listbox.pack()
474
+
475
+ # Selektion des ersten Elements
476
+ if suggestions:
477
+ self.suggestions_listbox.selection_set(0)
478
+ self.suggestions_listbox.activate(0)
479
+
480
+ # Event-Bindings für Listbox
481
+ self.suggestions_listbox.bind("<Return>", self.apply_suggestion)
482
+ self.suggestions_listbox.bind("<Tab>", self.apply_suggestion)
483
+ self.suggestions_listbox.bind("<Escape>", self.hide_suggestions)
484
+ self.suggestions_listbox.bind("<Up>", lambda e: self.navigate_suggestions(-1))
485
+ self.suggestions_listbox.bind("<Down>", lambda e: self.navigate_suggestions(1))
486
+ self.suggestions_listbox.bind("<Button-1>", self.on_listbox_click)
487
+
488
+ # Fokussiere Listbox
489
+ self.suggestions_listbox.focus_set()
490
+
491
+ return "break"
492
+
493
+ def navigate_suggestions(self, direction):
494
+ """Navigiert in der Vorschlagsliste"""
495
+ if not self.suggestions_listbox:
496
+ return
497
+
498
+ current_selection = self.suggestions_listbox.curselection()
499
+ if not current_selection:
500
+ return
501
+
502
+ current_index = current_selection[0]
503
+ new_index = current_index + direction
504
+
505
+ if 0 <= new_index < self.suggestions_listbox.size():
506
+ self.suggestions_listbox.selection_clear(0, tk.END)
507
+ self.suggestions_listbox.selection_set(new_index)
508
+ self.suggestions_listbox.activate(new_index)
509
+ self.suggestions_listbox.see(new_index)
510
+
511
+ def on_listbox_click(self, event):
512
+ """Behandelt Klicks in der Listbox"""
513
+ if self.suggestions_listbox:
514
+ self.apply_suggestion()
515
+
516
+ def apply_suggestion(self, event=None):
517
+ """Wendet den ausgewählten Vorschlag an"""
518
+ if not self.suggestions_listbox:
519
+ return
520
+
521
+ selection = self.suggestions_listbox.curselection()
522
+ if selection:
523
+ selected_suggestion = self.suggestions_listbox.get(selection[0])
524
+
525
+ # Ersetze das aktuelle Wort
526
+ self.text_widget.delete(self.current_word_start, self.current_word_end)
527
+ self.text_widget.insert(self.current_word_start, selected_suggestion)
528
+
529
+ # Setze Cursor an das Ende
530
+ self.text_widget.mark_set(
531
+ tk.INSERT, f"{self.current_word_start} + {len(selected_suggestion)}c"
532
+ )
533
+
534
+ self.hide_suggestions()
535
+ return "break"
536
+
537
+ def hide_suggestions(self, event=None):
538
+ """Versteckt die Vorschlagsliste"""
539
+ if self.suggestions_window:
540
+ self.suggestions_window.destroy()
541
+ self.suggestions_window = None
542
+ self.suggestions_listbox = None
543
+ self.current_suggestions = []
544
+
545
+ # Fokussiere zurück zum Text-Widget
546
+ self.text_widget.focus_set()
547
+
548
+ return "break"
549
+
550
+
551
+ class BashSyntaxHighlighter:
552
+ """Syntax-Highlighter für Bash-Scripts"""
553
+
554
+ def __init__(self, text_widget):
555
+ self.text_widget = text_widget
556
+ self.highlighting_active = True
557
+ self.tag_configs = {} # Hält die Konfigurationen für die Tags
558
+
559
+ # Syntax-Muster für Bash
560
+ self.patterns = {
561
+ "comments": r"#.*$", # Kommentare
562
+ "shebang": r"^#!/.*bash", # Shebang
563
+ "strings": r'(["\'])(?:(?=(\\?))\2.)*?\1', # Strings
564
+ "variables": r"\$[A-Za-z_][A-Za-z0-9_]*|\$\{[^}]+\}", # Variablen
565
+ "commands": r"\b(?:echo|read|if|then|else|elif|fi|for|while|do|done|case|esac|function|return|exit|cd|ls|pwd|mkdir|rm|cp|mv|chmod|chown|grep|sed|awk|cat|head|tail|sort|uniq|wc|find|ps|kill|sudo|apt|yum|dnf|pacman)\b", # Häufige Befehle
566
+ "operators": r"\b(?:-eq|-ne|-lt|-le|-gt|-ge|-f|-d|-e|-r|-w|-x|&&|\|\||==|!=|=~)\b", # Operatoren
567
+ "numbers": r"\b\d+\b", # Zahlen
568
+ "brackets": r"[(){}[\]]", # Klammern
569
+ }
570
+
571
+ # Tag-Konfigurationen
572
+ self.configure_tags()
573
+
574
+ # Event-Binding für Live-Highlighting
575
+ self.text_widget.bind("<KeyRelease>", self.highlight_syntax)
576
+ self.text_widget.bind("<ButtonRelease>", self.highlight_syntax)
577
+
578
+ def configure_tags(self):
579
+ """Konfiguriert die Text-Tags für das Solarized Dark Theme"""
580
+ # Solarized Dark Palette
581
+ sol_base01 = "#586e75" # Comments
582
+ sol_cyan = "#2aa198" # Shebang
583
+ sol_orange = "#cb4b16" # Strings, Numbers
584
+ sol_blue = "#268bd2" # Variables
585
+ sol_green = "#859900" # Commands, Keywords
586
+ sol_magenta = "#d33682" # Operators
587
+ sol_base0 = "#839496" # Brackets
588
+
589
+ # Kommentare
590
+ self.tag_configs["comments"] = {
591
+ "foreground": sol_base01,
592
+ "font": ("Courier", 10, "italic bold"),
593
+ }
594
+ self.text_widget.tag_configure("comments", **self.tag_configs["comments"])
595
+
596
+ # Shebang
597
+ self.tag_configs["shebang"] = {
598
+ "foreground": sol_cyan,
599
+ "font": ("Courier", 10, "bold"),
600
+ }
601
+ self.text_widget.tag_configure("shebang", **self.tag_configs["shebang"])
602
+
603
+ # Strings
604
+ self.tag_configs["strings"] = {
605
+ "foreground": sol_orange,
606
+ "font": ("Courier", 10, "bold"),
607
+ }
608
+ self.text_widget.tag_configure("strings", **self.tag_configs["strings"])
609
+
610
+ # Variablen
611
+ self.tag_configs["variables"] = {
612
+ "foreground": sol_blue,
613
+ "font": ("Courier", 10, "bold"),
614
+ }
615
+ self.text_widget.tag_configure("variables", **self.tag_configs["variables"])
616
+
617
+ # Befehle & Schlüsselwörter
618
+ self.tag_configs["commands"] = {
619
+ "foreground": sol_green,
620
+ "font": ("Courier", 10, "bold"),
621
+ }
622
+ self.text_widget.tag_configure("commands", **self.tag_configs["commands"])
623
+
624
+ # Operatoren
625
+ self.tag_configs["operators"] = {
626
+ "foreground": sol_magenta,
627
+ "font": ("Courier", 10, "bold"),
628
+ }
629
+ self.text_widget.tag_configure("operators", **self.tag_configs["operators"])
630
+
631
+ # Zahlen
632
+ self.tag_configs["numbers"] = {
633
+ "foreground": sol_orange,
634
+ "font": ("Courier", 10, "bold"),
635
+ }
636
+ self.text_widget.tag_configure("numbers", **self.tag_configs["numbers"])
637
+
638
+ # Klammern
639
+ self.tag_configs["brackets"] = {
640
+ "foreground": sol_base0,
641
+ "font": ("Courier", 10, "bold"),
642
+ }
643
+ self.text_widget.tag_configure("brackets", **self.tag_configs["brackets"])
644
+
645
+ def highlight_syntax(self, event=None):
646
+ """Hebt die Syntax im Text hervor"""
647
+ if not self.highlighting_active:
648
+ return
649
+
650
+ # Entferne alle vorhandenen Tags
651
+ for tag in self.patterns.keys():
652
+ self.text_widget.tag_remove(tag, "1.0", tk.END)
653
+
654
+ # Hole den gesamten Text
655
+ text_content = self.text_widget.get("1.0", tk.END)
656
+
657
+ # Hebe jedes Muster hervor
658
+ for tag_name, pattern in self.patterns.items():
659
+ self._highlight_pattern(tag_name, pattern, text_content)
660
+
661
+ def _highlight_pattern(self, tag_name, pattern, text_content):
662
+ """Hebt ein bestimmtes Muster hervor"""
663
+ try:
664
+ for match in re.finditer(pattern, text_content, re.MULTILINE):
665
+ start = f"1.0 + {match.start()} chars"
666
+ end = f"1.0 + {match.end()} chars"
667
+
668
+ self.text_widget.tag_add(tag_name, start, end)
669
+ except re.error:
670
+ # Überspringe ungültige Regex-Muster
671
+ pass
672
+
673
+ def toggle_highlighting(self):
674
+ """Schaltet Syntax-Highlighting ein/aus"""
675
+ self.highlighting_active = not self.highlighting_active
676
+ if self.highlighting_active:
677
+ self.highlight_syntax()
678
+ else:
679
+ # Entferne alle Tags
680
+ for tag in self.patterns.keys():
681
+ self.text_widget.tag_remove(tag, "1.0", tk.END)
682
+
683
+
684
+ class BashScriptEditor(ScrolledText):
685
+ """Bash-Script-Editor mit Syntax-Highlighting und Tab-Unterstützung"""
686
+
687
+ def __init__(self, parent, **kwargs):
688
+ # Tab-Konfiguration muss vor dem super().__init__ Aufruf stehen
689
+ self.tab_size = 4 # 4 Leerzeichen pro Tab
690
+
691
+ # Konfigurationen an das zugrundeliegende Text-Widget via kwargs weitergeben
692
+ kwargs.setdefault("font", ("Courier", 10, "bold"))
693
+ self.base_font = kwargs.get("font")
694
+ kwargs.setdefault("tabs", (f"{self.tab_size}c",))
695
+
696
+ super().__init__(parent, **kwargs)
697
+
698
+ # Einrückungs-Schlüsselwörter
699
+ self.indent_keywords = {
700
+ "if",
701
+ "then",
702
+ "else",
703
+ "elif",
704
+ "for",
705
+ "while",
706
+ "until",
707
+ "case",
708
+ "function",
709
+ "do",
710
+ }
711
+ self.dedent_keywords = {"fi", "done", "esac", "else", "elif"}
712
+
713
+ # Syntax-Highlighter initialisieren
714
+ self.highlighter = BashSyntaxHighlighter(self.text)
715
+
716
+ # Autocomplete initialisieren
717
+ self.autocomplete = BashAutocomplete(self.text)
718
+
719
+ # Veraltete .config Aufrufe entfernt, da sie über kwargs an den Konstruktor übergeben werden
720
+ # und teilweise mit dem Theme "superhero" in Konflikt stehen.
721
+
722
+ # Tab-Event-Bindings
723
+ self.text.bind("<Tab>", self.handle_tab)
724
+ self.text.bind("<Shift-Tab>", self.handle_shift_tab)
725
+ self.text.bind("<Return>", self.handle_return)
726
+ self.text.bind("<BackSpace>", self.handle_backspace)
727
+
728
+ # Rechtsklick-Menü
729
+ self.create_context_menu()
730
+
731
+ def create_context_menu(self):
732
+ """Erstellt ein Rechtsklick-Kontextmenü"""
733
+ self.context_menu = tk.Menu(self.text, tearoff=0)
734
+ self.context_menu.add_command(
735
+ label="Ausschneiden", command=lambda: self.event_generate("<<Cut>>")
736
+ )
737
+ self.context_menu.add_command(
738
+ label="Kopieren", command=lambda: self.event_generate("<<Copy>>")
739
+ )
740
+ self.context_menu.add_command(
741
+ label="Einfügen", command=lambda: self.event_generate("<<Paste>>")
742
+ )
743
+ self.context_menu.add_separator()
744
+ self.context_menu.add_command(
745
+ label="Alles auswählen", command=self.select_all, accelerator="Ctrl+A"
746
+ )
747
+ self.context_menu.add_separator()
748
+ self.context_menu.add_command(
749
+ label="Einrücken", command=self.insert_indent, accelerator="Tab"
750
+ )
751
+ self.context_menu.add_command(
752
+ label="Ausrücken", command=self.remove_indent, accelerator="Shift+Tab"
753
+ )
754
+ self.context_menu.add_command(
755
+ label="Zeile duplizieren", command=self.duplicate_line, accelerator="Ctrl+D"
756
+ )
757
+ self.context_menu.add_command(
758
+ label="Kommentar umschalten",
759
+ command=self.comment_uncomment_selection,
760
+ accelerator="Ctrl+/",
761
+ )
762
+ self.context_menu.add_separator()
763
+ self.context_menu.add_command(
764
+ label="Autovervollständigung",
765
+ command=self.autocomplete.show_suggestions,
766
+ accelerator="Ctrl+Space",
767
+ )
768
+ self.context_menu.add_separator()
769
+ self.context_menu.add_command(
770
+ label="Syntax-Highlighting umschalten",
771
+ command=self.highlighter.toggle_highlighting,
772
+ )
773
+
774
+ # Rechtsklick-Event binden
775
+ self.text.bind("<Button-3>", self.show_context_menu)
776
+
777
+ # Zusätzliche Tastenkombinationen
778
+ self.text.bind("<Control-a>", lambda e: self.select_all())
779
+ self.text.bind("<Control-d>", lambda e: self.duplicate_line())
780
+ self.text.bind("<Control-slash>", lambda e: self.comment_uncomment_selection())
781
+
782
+ def update_font(self, font_family, font_size):
783
+ """Aktualisiert die Schriftart des Editors."""
784
+ new_font = (font_family, font_size, "bold")
785
+ self.text.config(font=new_font)
786
+ # Sorge dafür, dass die Syntax-Tags die neue Schriftgröße übernehmen, aber ihre Stile beibehalten
787
+ for tag_name, config in self.highlighter.tag_configs.items():
788
+ font_config = config.get("font")
789
+ if (
790
+ font_config
791
+ and isinstance(font_config, (list, tuple))
792
+ and len(font_config) == 3
793
+ ):
794
+ # Behält den Stil (italic, bold) bei
795
+ style = font_config[2]
796
+ self.text.tag_configure(tag_name, font=(font_family, font_size, style))
797
+ else:
798
+ # Standard-Schriftart für andere Tags
799
+ self.text.tag_configure(tag_name, font=new_font)
800
+
801
+ def show_context_menu(self, event):
802
+ """Zeigt das Kontextmenü an"""
803
+ try:
804
+ self.context_menu.tk_popup(event.x_root, event.y_root)
805
+ finally:
806
+ self.context_menu.grab_release()
807
+
808
+ def select_all(self):
809
+ """Wählt den gesamten Text aus"""
810
+ self.tag_add(tk.SEL, "1.0", tk.END)
811
+ self.mark_set(tk.INSERT, tk.END)
812
+ self.see(tk.INSERT)
813
+
814
+ def insert_command_at_cursor(self, command):
815
+ """Fügt einen Befehl an der Cursor-Position ein"""
816
+ current_pos = self.index(tk.INSERT)
817
+ self.insert(current_pos, command + "\n")
818
+ self.see(current_pos)
819
+
820
+ def get_selected_text(self):
821
+ """Gibt den ausgewählten Text zurück"""
822
+ try:
823
+ return self.get(tk.SEL_FIRST, tk.SEL_LAST)
824
+ except tk.TclError:
825
+ return ""
826
+
827
+ def replace_selected_text(self, new_text):
828
+ """Ersetzt den ausgewählten Text"""
829
+ try:
830
+ self.delete(tk.SEL_FIRST, tk.SEL_LAST)
831
+ self.insert(tk.INSERT, new_text)
832
+ except tk.TclError:
833
+ pass
834
+
835
+ def duplicate_line(self):
836
+ """Dupliziert die aktuelle Zeile"""
837
+ current_line = self.index(tk.INSERT).split(".")[0]
838
+ line_content = self.get(f"{current_line}.0", f"{current_line}.end")
839
+
840
+ # Füge die Zeile nach der aktuellen ein
841
+ self.insert(f"{current_line}.end", "\n" + line_content)
842
+
843
+ def comment_uncomment_selection(self):
844
+ """Kommentiert/entfernt Kommentar von ausgewähltem Text"""
845
+ selected_text = self.get_selected_text()
846
+ if not selected_text:
847
+ return
848
+
849
+ lines = selected_text.split("\n")
850
+ commented_lines = []
851
+
852
+ for line in lines:
853
+ if line.strip().startswith("#"):
854
+ # Entferne Kommentar
855
+ commented_lines.append(line.replace("#", "", 1).lstrip())
856
+ else:
857
+ # Füge Kommentar hinzu
858
+ commented_lines.append("# " + line)
859
+
860
+ new_text = "\n".join(commented_lines)
861
+ self.replace_selected_text(new_text)
862
+
863
+ def handle_tab(self, event):
864
+ """Behandelt Tab-Taste für Einrückung"""
865
+ # Verhindere Standard-Tab-Verhalten
866
+ self.after_idle(lambda: self.insert_indent())
867
+ return "break"
868
+
869
+ def handle_shift_tab(self, event):
870
+ """Behandelt Shift+Tab für Ausrückung"""
871
+ self.after_idle(lambda: self.remove_indent())
872
+ return "break"
873
+
874
+ def handle_return(self, event):
875
+ """Behandelt Enter-Taste mit automatischer Einrückung"""
876
+ # Hole die aktuelle Zeile
877
+ current_line = self.index(tk.INSERT).split(".")[0]
878
+ line_content = self.get(f"{current_line}.0", f"{current_line}.end")
879
+
880
+ # Berechne Einrückung für die nächste Zeile
881
+ indent_level = self._calculate_indent_level(line_content)
882
+
883
+ # Füge Zeilenumbruch und Einrückung ein
884
+ self.insert(tk.INSERT, "\n" + " " * (indent_level * self.tab_size))
885
+
886
+ # Stelle sicher, dass die neue Zeile sichtbar ist
887
+ self.see(tk.INSERT)
888
+ return "break"
889
+
890
+ def handle_backspace(self, event):
891
+ """Behandelt Backspace mit intelligenter Ausrückung"""
892
+ # Prüfe, ob wir am Anfang einer eingerückten Zeile sind
893
+ current_pos = self.index(tk.INSERT)
894
+ line_start = current_pos.split(".")[0] + ".0"
895
+ line_content = self.get(line_start, current_pos)
896
+
897
+ # Wenn die Zeile nur aus Leerzeichen besteht und wir am Ende sind
898
+ if line_content.strip() == "" and len(line_content) > 0:
899
+ # Lösche bis zum nächsten Tab-Stop
900
+ spaces_to_remove = len(line_content) % self.tab_size
901
+ if spaces_to_remove == 0:
902
+ spaces_to_remove = self.tab_size
903
+
904
+ # Lösche die Leerzeichen
905
+ start_pos = f"{line_start} + {len(line_content) - spaces_to_remove} chars"
906
+ self.delete(start_pos, current_pos)
907
+ return "break"
908
+
909
+ # Normales Backspace-Verhalten
910
+ return None
911
+
912
+ def insert_indent(self):
913
+ """Fügt Einrückung an der aktuellen Position oder für ausgewählten Text ein"""
914
+ try:
915
+ # Prüfe, ob Text ausgewählt ist
916
+ selected_text = self.get(tk.SEL_FIRST, tk.SEL_LAST)
917
+ if selected_text:
918
+ self._indent_selection()
919
+ else:
920
+ self._indent_current_line()
921
+ except tk.TclError:
922
+ # Kein ausgewählter Text
923
+ self._indent_current_line()
924
+
925
+ def remove_indent(self):
926
+ """Entfernt Einrückung von der aktuellen Position oder ausgewähltem Text"""
927
+ try:
928
+ # Prüfe, ob Text ausgewählt ist
929
+ selected_text = self.get(tk.SEL_FIRST, tk.SEL_LAST)
930
+ if selected_text:
931
+ self._dedent_selection()
932
+ else:
933
+ self._dedent_current_line()
934
+ except tk.TclError:
935
+ # Kein ausgewählter Text
936
+ self._dedent_current_line()
937
+
938
+ def _indent_current_line(self):
939
+ """Rückt die aktuelle Zeile ein"""
940
+ current_line = self.index(tk.INSERT).split(".")[0]
941
+ line_start = f"{current_line}.0"
942
+ line_content = self.get(line_start, f"{current_line}.end")
943
+
944
+ # Füge Tab am Anfang der Zeile ein
945
+ self.insert(line_start, " " * self.tab_size)
946
+
947
+ def _dedent_current_line(self):
948
+ """Rückt die aktuelle Zeile aus"""
949
+ current_line = self.index(tk.INSERT).split(".")[0]
950
+ line_start = f"{current_line}.0"
951
+ line_content = self.get(line_start, f"{current_line}.end")
952
+
953
+ # Entferne Leerzeichen vom Anfang der Zeile
954
+ leading_spaces = len(line_content) - len(line_content.lstrip())
955
+ spaces_to_remove = min(leading_spaces, self.tab_size)
956
+
957
+ if spaces_to_remove > 0:
958
+ end_pos = f"{line_start} + {spaces_to_remove} chars"
959
+ self.delete(line_start, end_pos)
960
+
961
+ def _indent_selection(self):
962
+ """Rückt alle ausgewählten Zeilen ein"""
963
+ start_line = self.index(tk.SEL_FIRST).split(".")[0]
964
+ end_line = self.index(tk.SEL_LAST).split(".")[0]
965
+
966
+ for line_num in range(int(start_line), int(end_line) + 1):
967
+ line_start = f"{line_num}.0"
968
+ self.insert(line_start, " " * self.tab_size)
969
+
970
+ # Aktualisiere Auswahl
971
+ new_start = f"{start_line}.0 + {self.tab_size} chars"
972
+ new_end = f"{end_line}.end + {self.tab_size} chars"
973
+ self.tag_remove(tk.SEL, "1.0", tk.END)
974
+ self.tag_add(tk.SEL, new_start, new_end)
975
+
976
+ def _dedent_selection(self):
977
+ """Rückt alle ausgewählten Zeilen aus"""
978
+ start_line = self.index(tk.SEL_FIRST).split(".")[0]
979
+ end_line = self.index(tk.SEL_LAST).split(".")[0]
980
+
981
+ total_removed = 0
982
+ for line_num in range(int(start_line), int(end_line) + 1):
983
+ line_start = f"{line_num}.0"
984
+ line_content = self.get(line_start, f"{line_num}.end")
985
+
986
+ leading_spaces = len(line_content) - len(line_content.lstrip())
987
+ spaces_to_remove = min(leading_spaces, self.tab_size)
988
+
989
+ if spaces_to_remove > 0:
990
+ end_pos = f"{line_start} + {spaces_to_remove} chars"
991
+ self.delete(line_start, end_pos)
992
+ total_removed += spaces_to_remove
993
+
994
+ # Aktualisiere Auswahl
995
+ if total_removed > 0:
996
+ new_end = f"{end_line}.end - {total_removed} chars"
997
+ self.tag_remove(tk.SEL, "1.0", tk.END)
998
+ self.tag_add(tk.SEL, tk.SEL_FIRST, new_end)
999
+
1000
+ def _calculate_indent_level(self, line_content):
1001
+ """Berechnet die Einrückungsebene für die nächste Zeile"""
1002
+ stripped_line = line_content.strip()
1003
+
1004
+ # Prüfe auf Einrückung-erhöhende Schlüsselwörter
1005
+ if any(keyword in stripped_line for keyword in self.indent_keywords):
1006
+ return (len(line_content) - len(line_content.lstrip())) // self.tab_size + 1
1007
+
1008
+ # Prüfe auf Einrückung-verringernde Schlüsselwörter
1009
+ if any(keyword in stripped_line for keyword in self.dedent_keywords):
1010
+ current_indent = (
1011
+ len(line_content) - len(line_content.lstrip())
1012
+ ) // self.tab_size
1013
+ return max(0, current_indent - 1)
1014
+
1015
+ # Behalte aktuelle Einrückungsebene bei
1016
+ return (len(line_content) - len(line_content.lstrip())) // self.tab_size
1017
+
1018
+ def get_current_indent_level(self):
1019
+ """Gibt die aktuelle Einrückungsebene zurück"""
1020
+ current_line = self.index(tk.INSERT).split(".")[0]
1021
+ line_content = self.get(f"{current_line}.0", f"{current_line}.end")
1022
+ return (len(line_content) - len(line_content.lstrip())) // self.tab_size
1023
+
1024
+
1025
+ # Alias für Abwärtskompatibilität
1026
+ EnhancedScriptEditor = BashScriptEditor