tklive 0.1.0__tar.gz

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.
tklive-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
tklive-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,58 @@
1
+ Metadata-Version: 2.4
2
+ Name: tklive
3
+ Version: 0.1.0
4
+ Summary: Live hot-reload for tkinter and customtkinter apps
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/yourusername/tklive
7
+ Requires-Python: >=3.8
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: watchdog>=3.0.0
11
+ Dynamic: license-file
12
+
13
+ # tklive
14
+
15
+ Live hot-reload für tkinter und customtkinter Apps.
16
+ Speichere deine Datei → UI updated sich sofort ohne das Fenster zu schließen.
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ pip install tklive
22
+ ```
23
+
24
+ ## Verwendung
25
+
26
+ Füge **zwei Zeilen** zu deinem Projekt hinzu:
27
+
28
+ ```python
29
+ import tkinter as tk
30
+ from tklive import live
31
+
32
+ root = tk.Tk()
33
+
34
+ # ... dein gesamter normaler Code bleibt unverändert ...
35
+
36
+ live(root, __file__) # ← eine Zeile ans Ende
37
+ root.mainloop()
38
+ ```
39
+
40
+ Speichere die Datei → UI reloaded live. Oder **Ctrl+R** für manuellen Reload.
41
+
42
+ ## Features
43
+
44
+ - ✅ Fenster bleibt offen – kein Neustart
45
+ - ✅ State-Erhalt (Entry, Text, Tabs, Scrollposition, Fenstergröße)
46
+ - ✅ customtkinter Support (CTkEntry, CTkTextbox, CTkSlider, CTkTabview ...)
47
+ - ✅ ttk Support (Notebook, Treeview, Combobox ...)
48
+ - ✅ Multi-File Support
49
+
50
+ ## Mehrere Dateien beobachten
51
+
52
+ ```python
53
+ live(root, __file__, watch_files=["ui.py", "components.py"])
54
+ ```
55
+
56
+ ## Lizenz
57
+
58
+ MIT
tklive-0.1.0/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # tklive
2
+
3
+ Live hot-reload für tkinter und customtkinter Apps.
4
+ Speichere deine Datei → UI updated sich sofort ohne das Fenster zu schließen.
5
+
6
+ ## Installation
7
+
8
+ ```bash
9
+ pip install tklive
10
+ ```
11
+
12
+ ## Verwendung
13
+
14
+ Füge **zwei Zeilen** zu deinem Projekt hinzu:
15
+
16
+ ```python
17
+ import tkinter as tk
18
+ from tklive import live
19
+
20
+ root = tk.Tk()
21
+
22
+ # ... dein gesamter normaler Code bleibt unverändert ...
23
+
24
+ live(root, __file__) # ← eine Zeile ans Ende
25
+ root.mainloop()
26
+ ```
27
+
28
+ Speichere die Datei → UI reloaded live. Oder **Ctrl+R** für manuellen Reload.
29
+
30
+ ## Features
31
+
32
+ - ✅ Fenster bleibt offen – kein Neustart
33
+ - ✅ State-Erhalt (Entry, Text, Tabs, Scrollposition, Fenstergröße)
34
+ - ✅ customtkinter Support (CTkEntry, CTkTextbox, CTkSlider, CTkTabview ...)
35
+ - ✅ ttk Support (Notebook, Treeview, Combobox ...)
36
+ - ✅ Multi-File Support
37
+
38
+ ## Mehrere Dateien beobachten
39
+
40
+ ```python
41
+ live(root, __file__, watch_files=["ui.py", "components.py"])
42
+ ```
43
+
44
+ ## Lizenz
45
+
46
+ MIT
@@ -0,0 +1,15 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "tklive"
7
+ version = "0.1.0"
8
+ description = "Live hot-reload for tkinter and customtkinter apps"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.8"
12
+ dependencies = ["watchdog>=3.0.0"]
13
+
14
+ [project.urls]
15
+ Homepage = "https://github.com/yourusername/tklive"
tklive-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,4 @@
1
+ from .core import live
2
+
3
+ __version__ = "0.1.0"
4
+ __all__ = ["live"]
@@ -0,0 +1,151 @@
1
+ import os
2
+ import sys
3
+ import time
4
+ import traceback
5
+ import tkinter as tk
6
+
7
+ from .state import save_state, restore_state
8
+
9
+ try:
10
+ from watchdog.observers import Observer
11
+ from watchdog.events import FileSystemEventHandler
12
+ HAS_WATCHDOG = True
13
+ except ImportError:
14
+ HAS_WATCHDOG = False
15
+
16
+
17
+ class _FileHandler(FileSystemEventHandler):
18
+ def __init__(self, paths, callback):
19
+ self._paths = [os.path.abspath(p) for p in paths]
20
+ self._callback = callback
21
+ self._last = 0
22
+
23
+ def on_modified(self, event):
24
+ if not event.is_directory and event.src_path.endswith(".py"):
25
+ if os.path.abspath(event.src_path) in self._paths or not self._paths:
26
+ now = time.time()
27
+ if now - self._last > 0.5:
28
+ self._last = now
29
+ self._callback()
30
+
31
+
32
+ def _clear_widgets(root):
33
+ for w in root.winfo_children():
34
+ w.destroy()
35
+
36
+
37
+ def live(root: tk.Tk, script_path: str, watch_files: list = None, auto: bool = True):
38
+ """
39
+ Aktiviert live-reload für ein tkinter Script.
40
+
41
+ Verwendung:
42
+ live(root, __file__)
43
+
44
+ Mehrere Dateien beobachten:
45
+ live(root, __file__, watch_files=["ui.py", "components.py"])
46
+
47
+ Parameter:
48
+ root tkinter Root-Fenster
49
+ script_path Pfad zum Hauptscript (__file__ übergeben)
50
+ watch_files Weitere Dateien die beobachtet werden sollen (optional)
51
+ auto Automatisch neu laden wenn Datei gespeichert wird
52
+ """
53
+ abs_path = os.path.abspath(script_path)
54
+ folder = os.path.dirname(abs_path)
55
+
56
+ # Alle zu beobachtenden Dateien sammeln
57
+ all_files = [abs_path]
58
+ if watch_files:
59
+ for f in watch_files:
60
+ p = os.path.abspath(f)
61
+ if p not in all_files:
62
+ all_files.append(p)
63
+
64
+ def do_reload():
65
+ try:
66
+ state = save_state(root)
67
+ root.after(0, lambda: _do_rebuild(state))
68
+ except Exception:
69
+ traceback.print_exc()
70
+
71
+ def _do_rebuild(state):
72
+ try:
73
+ _clear_widgets(root)
74
+
75
+ script_globals = {
76
+ "__file__": abs_path,
77
+ "__name__": "__tkinter_reload__",
78
+ "__builtins__": __builtins__,
79
+ }
80
+
81
+ import tkinter as _tk
82
+ original_tk = _tk.Tk
83
+
84
+ class _FakeTk:
85
+ def __new__(cls, *args, **kwargs):
86
+ return root
87
+
88
+ _tk.Tk = _FakeTk
89
+
90
+ # customtkinter CTk ebenfalls patchen
91
+ try:
92
+ import customtkinter as _ctk
93
+ original_ctk = _ctk.CTk
94
+ class _FakeCTk:
95
+ def __new__(cls, *args, **kwargs):
96
+ return root
97
+ _ctk.CTk = _FakeCTk
98
+ except ImportError:
99
+ original_ctk = None
100
+ _ctk = None
101
+
102
+ original_mainloop = root.mainloop
103
+ root.mainloop = lambda: None
104
+
105
+ try:
106
+ with open(abs_path, "r", encoding="utf-8") as f:
107
+ source = f.read()
108
+ exec(compile(source, abs_path, "exec"), script_globals)
109
+ finally:
110
+ _tk.Tk = original_tk
111
+ root.mainloop = original_mainloop
112
+ if _ctk and original_ctk:
113
+ _ctk.CTk = original_ctk
114
+
115
+ restore_state(root, state)
116
+
117
+ except Exception:
118
+ _clear_widgets(root)
119
+ tb = traceback.format_exc()
120
+ tk.Label(root, text="Fehler beim Reload", fg="red",
121
+ font=("Arial", 14, "bold")).pack(pady=20)
122
+ t = tk.Text(root, width=80, height=20, bg="#1e1e1e", fg="#ff6b6b",
123
+ font=("Courier", 9))
124
+ t.pack(padx=10)
125
+ t.insert("1.0", tb)
126
+
127
+ root.bind_all("<Control-r>", lambda e: do_reload())
128
+
129
+ if auto:
130
+ if not HAS_WATCHDOG:
131
+ return
132
+
133
+ handler = _FileHandler(all_files, do_reload)
134
+ observer = Observer()
135
+
136
+ # Alle relevanten Ordner beobachten
137
+ watched_folders = set()
138
+ for f in all_files:
139
+ watched_folders.add(os.path.dirname(f))
140
+
141
+ for watched_folder in watched_folders:
142
+ observer.schedule(handler, watched_folder, recursive=False)
143
+
144
+ observer.daemon = True
145
+ observer.start()
146
+
147
+ def on_close():
148
+ observer.stop()
149
+ root.destroy()
150
+
151
+ root.protocol("WM_DELETE_WINDOW", on_close)
@@ -0,0 +1,257 @@
1
+ """
2
+ Speichert und stellt Widget-Zustände wieder her.
3
+ Unterstützt: tkinter, ttk, customtkinter, ttkbootstrap
4
+ """
5
+
6
+ import tkinter as tk
7
+
8
+ try:
9
+ from tkinter import ttk
10
+ HAS_TTK = True
11
+ except ImportError:
12
+ HAS_TTK = False
13
+
14
+ try:
15
+ import customtkinter as ctk
16
+ HAS_CTK = True
17
+ except ImportError:
18
+ HAS_CTK = False
19
+
20
+
21
+ def save_state(root: tk.Tk) -> dict:
22
+ state = {
23
+ "widgets": {},
24
+ "window": {
25
+ "geometry": root.geometry(),
26
+ "title": root.title(),
27
+ }
28
+ }
29
+ _collect(root, state["widgets"])
30
+ return state
31
+
32
+
33
+ def restore_state(root: tk.Tk, state: dict):
34
+ if not state:
35
+ return
36
+
37
+ # Fenstergröße und Position wiederherstellen
38
+ if "window" in state:
39
+ try:
40
+ root.geometry(state["window"]["geometry"])
41
+ root.title(state["window"]["title"])
42
+ except Exception:
43
+ pass
44
+
45
+ if "widgets" in state:
46
+ _apply(root, state["widgets"])
47
+
48
+
49
+ def _widget_key(widget) -> str:
50
+ name = str(widget).split(".")[-1]
51
+ return str(widget) if not name else f"{type(widget).__name__}:{str(widget)}"
52
+
53
+
54
+ def _collect(widget, state: dict):
55
+ key = _widget_key(widget)
56
+
57
+ try:
58
+ # ── tkinter Standard ──────────────────────────────
59
+ if isinstance(widget, tk.Entry):
60
+ state[key] = ("entry", widget.get())
61
+
62
+ elif isinstance(widget, tk.Text):
63
+ state[key] = ("text", widget.get("1.0", tk.END), widget.yview()[0])
64
+
65
+ elif isinstance(widget, tk.Scale):
66
+ state[key] = ("scale", widget.get())
67
+
68
+ elif isinstance(widget, tk.Spinbox):
69
+ state[key] = ("spinbox", widget.get())
70
+
71
+ elif isinstance(widget, tk.Listbox):
72
+ selected = list(widget.curselection())
73
+ scroll = widget.yview()[0]
74
+ state[key] = ("listbox", selected, scroll)
75
+
76
+ elif isinstance(widget, tk.BooleanVar):
77
+ state[key] = ("boolvar", widget.get())
78
+
79
+ elif isinstance(widget, (tk.Checkbutton, tk.Radiobutton)):
80
+ try:
81
+ var = widget.cget("variable")
82
+ if var:
83
+ state[key] = ("varname", str(var))
84
+ except Exception:
85
+ pass
86
+
87
+ # ── ttk ──────────────────────────────────────────
88
+ if HAS_TTK:
89
+ if isinstance(widget, ttk.Combobox):
90
+ state[key] = ("combobox", widget.get())
91
+
92
+ elif isinstance(widget, ttk.Entry):
93
+ state[key] = ("entry", widget.get())
94
+
95
+ elif isinstance(widget, ttk.Notebook):
96
+ state[key] = ("notebook", widget.index("current"))
97
+
98
+ elif isinstance(widget, ttk.Treeview):
99
+ selected = widget.selection()
100
+ state[key] = ("treeview", selected)
101
+
102
+ elif isinstance(widget, ttk.Scale):
103
+ state[key] = ("scale", widget.get())
104
+
105
+ elif isinstance(widget, ttk.Spinbox):
106
+ state[key] = ("spinbox", widget.get())
107
+
108
+ # ── customtkinter ─────────────────────────────────
109
+ if HAS_CTK:
110
+ if isinstance(widget, ctk.CTkEntry):
111
+ state[key] = ("ctk_entry", widget.get())
112
+
113
+ elif isinstance(widget, ctk.CTkTextbox):
114
+ state[key] = ("ctk_text", widget.get("1.0", tk.END))
115
+
116
+ elif isinstance(widget, ctk.CTkComboBox):
117
+ state[key] = ("ctk_combo", widget.get())
118
+
119
+ elif isinstance(widget, ctk.CTkSlider):
120
+ state[key] = ("ctk_slider", widget.get())
121
+
122
+ elif isinstance(widget, ctk.CTkCheckBox):
123
+ state[key] = ("ctk_check", widget.get())
124
+
125
+ elif isinstance(widget, ctk.CTkSwitch):
126
+ state[key] = ("ctk_switch", widget.get())
127
+
128
+ elif isinstance(widget, ctk.CTkSegmentedButton):
129
+ state[key] = ("ctk_segmented", widget.get())
130
+
131
+ elif isinstance(widget, ctk.CTkTabview):
132
+ try:
133
+ state[key] = ("ctk_tab", widget.get())
134
+ except Exception:
135
+ pass
136
+
137
+ except Exception:
138
+ pass
139
+
140
+ # Rekursiv durch alle Kinder
141
+ try:
142
+ children = widget.winfo_children()
143
+ except Exception:
144
+ children = []
145
+
146
+ for child in children:
147
+ _collect(child, state)
148
+
149
+
150
+ def _apply(widget, state: dict):
151
+ key = _widget_key(widget)
152
+
153
+ try:
154
+ if key in state:
155
+ entry = state[key]
156
+ kind = entry[0]
157
+
158
+ # ── tkinter Standard ──────────────────────────
159
+ if kind == "entry" and isinstance(widget, tk.Entry):
160
+ widget.delete(0, tk.END)
161
+ widget.insert(0, entry[1])
162
+
163
+ elif kind == "text" and isinstance(widget, tk.Text):
164
+ widget.delete("1.0", tk.END)
165
+ widget.insert("1.0", entry[1].rstrip("\n"))
166
+ widget.yview_moveto(entry[2])
167
+
168
+ elif kind == "scale" and isinstance(widget, tk.Scale):
169
+ widget.set(entry[1])
170
+
171
+ elif kind == "spinbox" and isinstance(widget, tk.Spinbox):
172
+ widget.delete(0, tk.END)
173
+ widget.insert(0, entry[1])
174
+
175
+ elif kind == "listbox" and isinstance(widget, tk.Listbox):
176
+ widget.selection_clear(0, tk.END)
177
+ for i in entry[1]:
178
+ widget.selection_set(i)
179
+ widget.yview_moveto(entry[2])
180
+
181
+ # ── ttk ───────────────────────────────────────
182
+ elif kind == "combobox" and HAS_TTK and isinstance(widget, ttk.Combobox):
183
+ widget.set(entry[1])
184
+
185
+ elif kind == "entry" and HAS_TTK and isinstance(widget, ttk.Entry):
186
+ widget.delete(0, tk.END)
187
+ widget.insert(0, entry[1])
188
+
189
+ elif kind == "notebook" and HAS_TTK and isinstance(widget, ttk.Notebook):
190
+ try:
191
+ widget.select(entry[1])
192
+ except Exception:
193
+ pass
194
+
195
+ elif kind == "treeview" and HAS_TTK and isinstance(widget, ttk.Treeview):
196
+ try:
197
+ widget.selection_set(entry[1])
198
+ except Exception:
199
+ pass
200
+
201
+ elif kind == "scale" and HAS_TTK and isinstance(widget, ttk.Scale):
202
+ widget.set(entry[1])
203
+
204
+ elif kind == "spinbox" and HAS_TTK and isinstance(widget, ttk.Spinbox):
205
+ widget.delete(0, tk.END)
206
+ widget.insert(0, entry[1])
207
+
208
+ # ── customtkinter ─────────────────────────────
209
+ elif kind == "ctk_entry" and HAS_CTK and isinstance(widget, ctk.CTkEntry):
210
+ widget.delete(0, tk.END)
211
+ widget.insert(0, entry[1])
212
+
213
+ elif kind == "ctk_text" and HAS_CTK and isinstance(widget, ctk.CTkTextbox):
214
+ widget.delete("1.0", tk.END)
215
+ widget.insert("1.0", entry[1].rstrip("\n"))
216
+
217
+ elif kind == "ctk_combo" and HAS_CTK and isinstance(widget, ctk.CTkComboBox):
218
+ widget.set(entry[1])
219
+
220
+ elif kind == "ctk_slider" and HAS_CTK and isinstance(widget, ctk.CTkSlider):
221
+ widget.set(entry[1])
222
+
223
+ elif kind == "ctk_check" and HAS_CTK and isinstance(widget, ctk.CTkCheckBox):
224
+ if entry[1]:
225
+ widget.select()
226
+ else:
227
+ widget.deselect()
228
+
229
+ elif kind == "ctk_switch" and HAS_CTK and isinstance(widget, ctk.CTkSwitch):
230
+ if entry[1]:
231
+ widget.select()
232
+ else:
233
+ widget.deselect()
234
+
235
+ elif kind == "ctk_segmented" and HAS_CTK and isinstance(widget, ctk.CTkSegmentedButton):
236
+ try:
237
+ widget.set(entry[1])
238
+ except Exception:
239
+ pass
240
+
241
+ elif kind == "ctk_tab" and HAS_CTK and isinstance(widget, ctk.CTkTabview):
242
+ try:
243
+ widget.set(entry[1])
244
+ except Exception:
245
+ pass
246
+
247
+ except Exception:
248
+ pass
249
+
250
+ # Rekursiv durch alle Kinder
251
+ try:
252
+ children = widget.winfo_children()
253
+ except Exception:
254
+ children = []
255
+
256
+ for child in children:
257
+ _apply(child, state)
@@ -0,0 +1,58 @@
1
+ Metadata-Version: 2.4
2
+ Name: tklive
3
+ Version: 0.1.0
4
+ Summary: Live hot-reload for tkinter and customtkinter apps
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/yourusername/tklive
7
+ Requires-Python: >=3.8
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: watchdog>=3.0.0
11
+ Dynamic: license-file
12
+
13
+ # tklive
14
+
15
+ Live hot-reload für tkinter und customtkinter Apps.
16
+ Speichere deine Datei → UI updated sich sofort ohne das Fenster zu schließen.
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ pip install tklive
22
+ ```
23
+
24
+ ## Verwendung
25
+
26
+ Füge **zwei Zeilen** zu deinem Projekt hinzu:
27
+
28
+ ```python
29
+ import tkinter as tk
30
+ from tklive import live
31
+
32
+ root = tk.Tk()
33
+
34
+ # ... dein gesamter normaler Code bleibt unverändert ...
35
+
36
+ live(root, __file__) # ← eine Zeile ans Ende
37
+ root.mainloop()
38
+ ```
39
+
40
+ Speichere die Datei → UI reloaded live. Oder **Ctrl+R** für manuellen Reload.
41
+
42
+ ## Features
43
+
44
+ - ✅ Fenster bleibt offen – kein Neustart
45
+ - ✅ State-Erhalt (Entry, Text, Tabs, Scrollposition, Fenstergröße)
46
+ - ✅ customtkinter Support (CTkEntry, CTkTextbox, CTkSlider, CTkTabview ...)
47
+ - ✅ ttk Support (Notebook, Treeview, Combobox ...)
48
+ - ✅ Multi-File Support
49
+
50
+ ## Mehrere Dateien beobachten
51
+
52
+ ```python
53
+ live(root, __file__, watch_files=["ui.py", "components.py"])
54
+ ```
55
+
56
+ ## Lizenz
57
+
58
+ MIT
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ tklive/__init__.py
5
+ tklive/core.py
6
+ tklive/state.py
7
+ tklive.egg-info/PKG-INFO
8
+ tklive.egg-info/SOURCES.txt
9
+ tklive.egg-info/dependency_links.txt
10
+ tklive.egg-info/requires.txt
11
+ tklive.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ watchdog>=3.0.0
@@ -0,0 +1 @@
1
+ tklive