1- # main .py - AI Token Crusher v1.0
1+ # src/app .py - Main application class (fully modular, no early Tk errors)
22import tkinter as tk
3- from tkinter import ttk , filedialog , messagebox , scrolledtext
3+ from tkinter import ttk , filedialog , messagebox
44import 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
721class 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)
161163if __name__ == "__main__" :
162- root = tk .Tk ()
164+ root = TkinterDnD . Tk () if TKDND_AVAILABLE else tk .Tk ()
163165 app = AITokenCrusher (root )
164166 root .mainloop ()
0 commit comments