Skip to content

Commit 308f32d

Browse files
committed
Refactor TokenOptimizer into modular structure
- Deleted main.py and pyproject.toml as part of restructuring. - Created run.py as the new entry point for the application. - Introduced src/__init__.py to define the package structure. - Moved AITokenCrusher class to src/app.py and refactored for modularity. - Added src/config.py for configuration data including options and themes. - Implemented src/optimizations.py for optimization logic. - Created src/ui.py to handle UI creation and management. - Added pytest.ini for test configuration. - Created conftest.py and multiple test files for unit testing.
1 parent b87776b commit 308f32d

File tree

14 files changed

+477
-494
lines changed

14 files changed

+477
-494
lines changed

main.py

Lines changed: 0 additions & 376 deletions
This file was deleted.

pyproject.toml

Lines changed: 0 additions & 3 deletions
This file was deleted.

pytest.ini

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[pytest]
2+
pythonpath = src
3+
testpaths = tests
4+
addopts = -ra -q

run.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# run.py - Entry point for AI Token Crusher
2+
import sys
3+
import os
4+
5+
# Add src to path so imports work when running from root
6+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
7+
8+
# Optional: Better error if tkdnd2 not installed
9+
try:
10+
from tkinterdnd2 import TkinterDnD
11+
RootClass = TkinterDnD.Tk
12+
except ImportError:
13+
from tkinter import Tk
14+
RootClass = Tk
15+
16+
from src.app import AITokenCrusher
17+
18+
if __name__ == "__main__":
19+
root = RootClass()
20+
app = AITokenCrusher(root)
21+
root.mainloop()

src/__init__.py

Whitespace-only changes.

src/app.py

Lines changed: 117 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,94 +1,122 @@
1-
# main.py - AI Token Crusher v1.0
1+
# src/app.py - Main application class (fully modular, no early Tk errors)
22
import tkinter as tk
3-
from tkinter import ttk, filedialog, messagebox, scrolledtext
3+
from tkinter import ttk, filedialog, messagebox
44
import webbrowser
5-
import re
5+
import os
6+
7+
# Drag-and-drop support (optional)
8+
try:
9+
from tkinterdnd2 import DND_FILES, TkinterDnD
10+
TKDND_AVAILABLE = True
11+
except ImportError:
12+
DND_FILES = None
13+
TkinterDnD = tk.Tk
14+
TKDND_AVAILABLE = False
15+
16+
from .config import OPTIONS_DEFAULT, THEMES, LINKS
17+
from .ui import create_ui
18+
from .optimizations import apply_optimizations
19+
620

721
class AITokenCrusher:
822
def __init__(self, root):
923
self.root = root
1024
self.root.title("AI Token Crusher - Cut up to 75% tokens")
1125
self.root.geometry("1280x820")
1226
self.root.minsize(1000, 700)
13-
self.root.configure(bg="#0d1117")
1427

28+
# --- CRITICAL FIX: Create BooleanVars AFTER root exists ---
29+
self.options = {}
30+
for key, default in OPTIONS_DEFAULT.items():
31+
self.options[key] = tk.BooleanVar(value=default)
32+
33+
self.is_dark_theme = True
34+
self.ui_elements = {}
35+
self.checkbuttons = []
36+
self.link_labels = []
37+
38+
# Initial theme setup
39+
self.root.configure(bg=THEMES["dark"]["bg"])
1540
style = ttk.Style()
1641
style.theme_use("clam")
17-
style.configure("Title.TLabel", foreground="#58a6ff", font=("Segoe UI", 18, "bold"), background="#0d1117")
18-
style.configure("TButton", padding=10, font=("Segoe UI", 9, "bold"))
19-
20-
self.options = {
21-
"remove_comments": tk.BooleanVar(value=True),
22-
"remove_docstrings": tk.BooleanVar(value=True),
23-
"remove_blank_lines": tk.BooleanVar(value=True),
24-
"remove_extra_spaces": tk.BooleanVar(value=True),
25-
"single_line_mode": tk.BooleanVar(value=True),
26-
"shorten_keywords": tk.BooleanVar(value=True),
27-
"replace_booleans": tk.BooleanVar(value=True),
28-
"use_short_operators": tk.BooleanVar(value=True),
29-
"remove_type_hints": tk.BooleanVar(value=True),
30-
"minify_structures": tk.BooleanVar(value=True),
31-
"unicode_shortcuts": tk.BooleanVar(value=True),
32-
"shorten_print": tk.BooleanVar(value=True),
33-
"remove_asserts": tk.BooleanVar(value=True),
34-
"remove_pass": tk.BooleanVar(value=True),
35-
}
36-
37-
self.create_ui()
38-
39-
def create_ui(self):
40-
main = tk.Frame(self.root, bg="#0d1117")
41-
main.pack(fill="both", expand=True, padx=20, pady=20)
42-
43-
ttk.Label(main, text="AI Token Crusher", style="Title.TLabel").pack(pady=(0, 5))
44-
ttk.Label(main, text="Cut up to 75% of tokens for Grok • GPT • Claude • Llama",
45-
foreground="#8b949e", font=("Segoe UI", 11), background="#0d1117").pack(pady=(0, 20))
46-
47-
top = tk.Frame(main, bg="#0d1117")
48-
top.pack(fill="both", expand=True)
49-
50-
input_frame = tk.LabelFrame(top, text=" Input Text / Code ", fg="#f0f6fc", bg="#161b22", font=("Segoe UI", 10, "bold"))
51-
input_frame.pack(side="left", fill="both", expand=True, padx=(0, 10))
52-
self.input_text = scrolledtext.ScrolledText(input_frame, font=("Consolas", 10), bg="#0d1117", fg="#c9d1d9")
53-
self.input_text.pack(fill="both", expand=True, padx=10, pady=10)
54-
55-
btns = tk.Frame(input_frame, bg="#161b22")
56-
btns.pack(pady=5)
57-
ttk.Button(btns, text="Load File", command=self.load_file).pack(side="left", padx=5)
58-
ttk.Button(btns, text="Copy Output", command=self.copy_output).pack(side="left", padx=5)
59-
60-
options_frame = tk.LabelFrame(top, text=" Optimization Techniques ", fg="#f0f6fc", bg="#161b22", font=("Segoe UI", 10, "bold"))
61-
options_frame.pack(side="right", fill="y", padx=(10, 0))
62-
canvas = tk.Canvas(options_frame, bg="#161b22", highlightthickness=0)
63-
scrollbar = ttk.Scrollbar(options_frame, command=canvas.yview)
64-
scroll_frame = tk.Frame(canvas, bg="#161b22")
65-
canvas.create_window((0, 0), window=scroll_frame, anchor="nw")
66-
canvas.configure(yscrollcommand=scrollbar.set)
67-
canvas.pack(side="left", fill="both", expand=True, padx=10, pady=10)
68-
scrollbar.pack(side="right", fill="y")
69-
70-
for key, var in self.options.items():
71-
name = key.replace("_", " ").title().replace("Shorten", "Short").replace("Remove", "Strip")
72-
tk.Checkbutton(scroll_frame, text=name, variable=var, bg="#161b22", fg="#c9d1d9", selectcolor="#21262d").pack(anchor="w", pady=2, padx=15)
73-
74-
ttk.Button(main, text="CRUSH TOKENS →", command=self.optimize).pack(pady=20)
75-
76-
output_frame = tk.LabelFrame(main, text=" Crushed Output (AI-Safe) ", fg="#f0f6fc", bg="#161b22", font=("Segoe UI", 10, "bold"))
77-
output_frame.pack(fill="both", expand=True, pady=(10, 0))
78-
self.output_text = scrolledtext.ScrolledText(output_frame, font=("Consolas", 10), bg="#0d1117", fg="#79c0ff")
79-
self.output_text.pack(fill="both", expand=True, padx=10, pady=10)
80-
ttk.Button(output_frame, text="Save Output", command=self.save_output).pack(pady=5)
81-
82-
self.stats = ttk.Label(main, text="Ready to crush tokens...", foreground="#79c0ff", font=("Consolas", 11, "bold"), background="#161b22")
83-
self.stats.pack(pady=10)
84-
85-
footer = tk.Frame(main, bg="#0d1117")
86-
footer.pack(pady=15)
87-
links = [("GitHub", "https://github.com/totalbrain/TokenOptimizer"), ("Roadmap", "https://github.com/users/totalbrain/projects/1")]
88-
for text, url in links:
89-
link = tk.Label(footer, text=text, fg="#58a6ff", bg="#0d1117", cursor="hand2", font=("Segoe UI", 9, "underline"))
90-
link.pack(side="left", padx=20)
91-
link.bind("<Button-1>", lambda e, u=url: webbrowser.open(u))
42+
style.configure(
43+
"Title.TLabel",
44+
foreground="#58a6ff",
45+
font=("Segoe UI", 18, "bold"),
46+
background=THEMES["dark"]["bg"]
47+
)
48+
49+
# Build UI
50+
create_ui(self)
51+
52+
# Enable drag & drop if available
53+
self.enable_drag_and_drop()
54+
55+
def toggle_theme(self):
56+
self.is_dark_theme = not self.is_dark_theme
57+
self.apply_theme()
58+
59+
def apply_theme(self):
60+
theme = THEMES["dark" if self.is_dark_theme else "light"]
61+
62+
# Update root
63+
self.root.configure(bg=theme["bg"])
64+
65+
# Update ttk style
66+
style = ttk.Style()
67+
style.configure("Title.TLabel", foreground=theme["accent"], background=theme["bg"])
68+
69+
# Update theme button
70+
self.theme_button.config(
71+
text="☀️" if self.is_dark_theme else "🌙",
72+
bg=theme["bg"],
73+
fg=theme["accent"]
74+
)
75+
76+
# Update stored UI elements
77+
for elem, widget in self.ui_elements.items():
78+
if hasattr(widget, "configure"):
79+
if "bg" in widget.config():
80+
widget.configure(bg=theme.get("bg", theme["frame_bg"]))
81+
if "fg" in widget.config():
82+
widget.configure(fg=theme.get("text_bright", theme["text"]))
83+
84+
# Special widgets
85+
self.input_text.configure(bg=theme["input_bg"], fg=theme["input_fg"])
86+
self.output_text.configure(fg=theme["output_fg"])
87+
self.stats.configure(foreground=theme["accent_secondary"], background=theme["frame_bg"])
88+
89+
# Checkbuttons
90+
for cb in self.checkbuttons:
91+
cb.configure(bg=theme["frame_bg"], fg=theme["text"], selectcolor=theme["select_bg"])
92+
93+
# Links
94+
for link in self.link_labels:
95+
link.configure(bg=theme["bg"], fg=theme["accent"])
96+
97+
def enable_drag_and_drop(self):
98+
if not TKDND_AVAILABLE or DND_FILES is None:
99+
return
100+
try:
101+
self.input_text.drop_target_register(DND_FILES)
102+
self.input_text.dnd_bind("<<Drop>>", self.on_drop_files)
103+
except Exception:
104+
pass # Silently skip if DnD setup fails
105+
106+
def on_drop_files(self, event):
107+
file_paths = self.root.splitlist(event.data)
108+
allowed = {".py", ".txt", ".md", ".json"}
109+
for path in file_paths:
110+
if os.path.splitext(path)[1].lower() in allowed:
111+
try:
112+
with open(path, "r", encoding="utf-8") as f:
113+
content = f.read()
114+
self.input_text.delete(1.0, tk.END)
115+
self.input_text.insert(tk.END, content)
116+
return
117+
except Exception as e:
118+
messagebox.showerror("Read Error", f"Failed to open file:\n{e}")
119+
messagebox.showwarning("Invalid File", "Only .py, .txt, .md, .json files are supported.")
92120

93121
def load_file(self):
94122
path = filedialog.askopenfilename(filetypes=[("All Files", "*.*")])
@@ -114,51 +142,25 @@ def save_output(self):
114142
def optimize(self):
115143
text = self.input_text.get(1.0, tk.END).strip()
116144
if not text:
117-
messagebox.showwarning("Empty", "Paste or load text first!")
145+
messagebox.showwarning("Empty Input", "Please paste or load some text first.")
118146
return
119147

120-
optimized = self.apply_optimizations(text)
148+
optimized = apply_optimizations(self.options, text)
149+
121150
self.output_text.delete(1.0, tk.END)
122151
self.output_text.insert(tk.END, optimized)
123152

124153
before = len(text)
125154
after = len(optimized)
126155
saved = 100 * (before - after) / before if before else 0
127-
self.stats.config(text=f"Before: {before:,} → After: {after:,} chars | Saved: {saved:.1f}%")
128-
129-
def apply_optimizations(self, text):
130-
import re
131-
if self.options["remove_comments"].get():
132-
text = re.sub(r'#.*', '', text)
133-
text = re.sub(r'"""[\s\S]*?"""|\'\'\'[\s\S]*?\'\'\'', '', text)
134-
if self.options["remove_docstrings"].get():
135-
text = re.sub(r'^[\r\n\s]*("""|\'\'\').*?\1', '', text, count=1, flags=re.DOTALL)
136-
if self.options["remove_blank_lines"].get():
137-
text = "\n".join(line for line in text.splitlines() if line.strip())
138-
if self.options["remove_extra_spaces"].get():
139-
text = re.sub(r'[ \t]+', ' ', text)
140-
if self.options["single_line_mode"].get():
141-
text = text.replace("\n", "⏎")
142-
if self.options["shorten_keywords"].get():
143-
rep = {"def ": "d ", "return ": "r ", "import ": "i ", "from ": "f ", "as ": "a ", "if ": "if", "class ": "c ", "lambda ": "λ "}
144-
for k, v in rep.items(): text = text.replace(k, v)
145-
if self.options["replace_booleans"].get():
146-
text = text.replace("True", "1").replace("False", "0").replace("None", "~")
147-
if self.options["use_short_operators"].get():
148-
text = text.replace("==", "≡").replace("!=", "≠").replace(" and ", "∧").replace(" or ", "∨")
149-
if self.options["remove_type_hints"].get():
150-
text = re.sub(r':\s*[^=\n\->]+', '', text)
151-
text = re.sub(r'->\s*[^:\n]+', '', text)
152-
if self.options["minify_structures"].get():
153-
text = re.sub(r',\s+', ',', text)
154-
text = re.sub(r':\s+', ':', text)
155-
if self.options["unicode_shortcuts"].get():
156-
text = text.replace(" in ", "∈").replace(" not in ", "∉")
157-
if self.options["shorten_print"].get():
158-
text = re.sub(r'print\s*\(', 'p(', text)
159-
return text.strip() + "\n"
160156

157+
self.stats.config(
158+
text=f"Before: {before:,} → After: {after:,} chars | Saved: {saved:.1f}%"
159+
)
160+
161+
162+
# Entry point when running directly (python -m src.app)
161163
if __name__ == "__main__":
162-
root = tk.Tk()
164+
root = TkinterDnD.Tk() if TKDND_AVAILABLE else tk.Tk()
163165
app = AITokenCrusher(root)
164166
root.mainloop()

src/config.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# src/config.py - Only pure data, no Tkinter variables
2+
import tkinter as tk # فقط برای type hint و استفاده در جاهای دیگه
3+
4+
# Default option states (will be converted to BooleanVar in app.py)
5+
OPTIONS_DEFAULT = {
6+
"remove_comments": True,
7+
"remove_docstrings": True,
8+
"remove_blank_lines": True,
9+
"remove_extra_spaces": True,
10+
"single_line_mode": True,
11+
"shorten_keywords": True,
12+
"replace_booleans": True,
13+
"use_short_operators": True,
14+
"remove_type_hints": True,
15+
"minify_structures": True,
16+
"unicode_shortcuts": True,
17+
"shorten_print": True,
18+
"remove_asserts": True,
19+
"remove_pass": True,
20+
}
21+
22+
# Pure theme data (no Tkinter objects)
23+
THEMES = {
24+
"dark": {
25+
"bg": "#0d1117",
26+
"frame_bg": "#161b22",
27+
"text": "#c9d1d9",
28+
"text_secondary": "#8b949e",
29+
"text_bright": "#f0f6fc",
30+
"accent": "#58a6ff",
31+
"accent_secondary": "#79c0ff",
32+
"select_bg": "#21262d",
33+
"input_bg": "#0d1117",
34+
"input_fg": "#c9d1d9",
35+
"output_fg": "#79c0ff",
36+
},
37+
"light": {
38+
"bg": "#ffffff",
39+
"frame_bg": "#f6f8fa",
40+
"text": "#24292f",
41+
"text_secondary": "#57606a",
42+
"text_bright": "#1f2328",
43+
"accent": "#0969da",
44+
"accent_secondary": "#0550ae",
45+
"select_bg": "#ddf4ff",
46+
"input_bg": "#ffffff",
47+
"input_fg": "#24292f",
48+
"output_fg": "#0550ae",
49+
}
50+
}
51+
52+
# Footer links
53+
LINKS = [
54+
("GitHub", "https://github.com/totalbrain/TokenOptimizer"),
55+
("Roadmap", "https://github.com/users/totalbrain/projects/1")
56+
]

src/optimizations.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# src/optimizations.py
2+
import re
3+
4+
def apply_optimizations(options, text):
5+
if options["remove_comments"].get():
6+
text = re.sub(r'#.*', '', text)
7+
text = re.sub(r'"""[\s\S]*?"""|\'\'\'[\s\S]*?\'\'\'', '', text)
8+
if options["remove_docstrings"].get():
9+
text = re.sub(r'^[\r\n\s]*("""|\'\'\').*?\1', '', text, count=1, flags=re.DOTALL)
10+
if options["remove_blank_lines"].get():
11+
text = "\n".join(line for line in text.splitlines() if line.strip())
12+
if options["remove_extra_spaces"].get():
13+
text = re.sub(r'[ \t]+', ' ', text)
14+
if options["single_line_mode"].get():
15+
text = text.replace("\n", "⏎")
16+
if options["shorten_keywords"].get():
17+
rep = {"def ": "d ", "return ": "r ", "import ": "i ", "from ": "f ", "as ": "a ", "if ": "if", "class ": "c ", "lambda ": "λ "}
18+
for k, v in rep.items():
19+
text = text.replace(k, v)
20+
if options["replace_booleans"].get():
21+
text = text.replace("True", "1").replace("False", "0").replace("None", "~")
22+
if options["use_short_operators"].get():
23+
text = text.replace("==", "≡").replace("!=", "≠").replace(" and ", "∧").replace(" or ", "∨")
24+
if options["remove_type_hints"].get():
25+
text = re.sub(r':\s*[^=\n\->]+', '', text)
26+
text = re.sub(r'->\s*[^:\n]+', '', text)
27+
if options["minify_structures"].get():
28+
text = re.sub(r',\s+', ',', text)
29+
text = re.sub(r':\s+', ':', text)
30+
if options["unicode_shortcuts"].get():
31+
text = text.replace(" in ", "∈").replace(" not in ", "∉")
32+
if options["shorten_print"].get():
33+
text = re.sub(r'print\s*\(', 'p(', text)
34+
return text.strip() + "\n"

0 commit comments

Comments
 (0)