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.
- bash_script_maker-1.1.0.dist-info/METADATA +408 -0
- bash_script_maker-1.1.0.dist-info/RECORD +8 -0
- bash_script_maker-1.1.0.dist-info/WHEEL +5 -0
- bash_script_maker-1.1.0.dist-info/entry_points.txt +2 -0
- bash_script_maker-1.1.0.dist-info/licenses/LICENSE +39 -0
- bash_script_maker-1.1.0.dist-info/top_level.txt +2 -0
- bash_script_maker.py +822 -0
- syntax_highlighter.py +1026 -0
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
|