From 4fb6743eb21fa12afa892eec0ffdeae7dcd1555c Mon Sep 17 00:00:00 2001 From: totalbrain Date: Fri, 5 Dec 2025 09:53:34 +0330 Subject: [PATCH 1/8] Update README with workflow From b47edc1d7ab3ab69f54eff74b5c7806ea2e6acdc Mon Sep 17 00:00:00 2001 From: totalbrain Date: Fri, 5 Dec 2025 11:17:53 +0330 Subject: [PATCH 2/8] Update README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Contributors: create feature branch from **dev** → PR to **dev** --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1af1e83..2199e53 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,17 @@ + ## Workflow + +- **main** → always stable & protected +- **dev** → active development (PRs go here) +- Contributors: create feature branch from **dev** → PR to **dev** +- Release: PR from **dev** → **main** + + Never push directly to main! + - Fork the repo - Create feature/issue-# branch from dev - Work on the issue - PR to dev - After tests/approve, merge to dev -- For release: PR dev to main \ No newline at end of file + +- For release: PR dev to main From e56f8d82c5269f309c20d111208709a2de10e6b9 Mon Sep 17 00:00:00 2001 From: totalbrain Date: Fri, 5 Dec 2025 12:00:07 +0330 Subject: [PATCH 3/8] Update README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit add PROJECT_JOURNEY – project history, roadmap & contributor guide# --- README.md | 73 +++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 60 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 2199e53..8fc255f 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,64 @@ +AI Token Crusher – Full Journey (From Idea to Production) -## Workflow +## Quick Links +- Repository: https://github.com/totalbrain/TokenOptimizer +- Live Releases: https://github.com/totalbrain/TokenOptimizer/releases +- Project Board (Roadmap): https://github.com/users/totalbrain/projects/1 +- Product Hunt Launch (coming soon): https://www.producthunt.com/posts/ai-token-crusher -- **main** → always stable & protected -- **dev** → active development (PRs go here) -- Contributors: create feature branch from **dev** → PR to **dev** -- Release: PR from **dev** → **main** +## The Story – How It All Started +One day I was tired of: +- Wasting thousands of tokens daily on long Python scripts and RAG documents +- Copy-pasting code into ChatGPT/Claude just to remove comments and spaces +- Getting rate-limited because context was too big - Never push directly to main! - -- Fork the repo -- Create feature/issue-# branch from dev -- Work on the issue -- PR to dev -- After tests/approve, merge to dev +I thought: "There must be a better way." -- For release: PR dev to main +So I built AI Token Crusher – an **offline desktop app that safely cuts up to 75% of tokens while keeping 100% readability for all major LLMs (Grok, GPT-4o, Claude 3.5, Llama 3.1, Gemini). + +## What We Have Achieved So Far (Live & Working) + +| Feature | Status | Notes | +|----------------------------------------|-----------|-------| +| 20+ AI-safe optimization techniques | Done | Comments, docstrings, spaces, unicode shortcuts, etc. | +| Full dark UI (GitHub-style) | Done | Modern, clean, professional | +| Dark / Light theme toggle | Done | Thanks to @Syogo-Suganoya | +| Real-time character & savings counter | Done | Live feedback | +| Load file / paste text / save output | Done | Full workflow | +| 18 planned features in public roadmap | Done | Transparent project board | +| Protected `main` branch | Done | Only stable code | +| Active `dev` branch for contributions | Done | All PRs go here | +| First community PR merged | Done | #19 – Theme toggle | +| GitHub Actions ready (tests coming) | Done | CI/CD foundation | +| First release v1.0.1 published | Done | With .exe and source | + +## Current Repository Status (Perfect for Contributors) +- Default branch: `main` (always stable, protected) +- Development branch: `dev` (all PRs go here) +- All contributors: create branch from `dev` → PR to `dev` +- Releases: only from `dev` → `main` via PR + +## What's Coming Next (Top Priority) +1. Dual mode: `--terminal` + `--gui` support (CLI automation) +2. Real token counter (tiktoken + multi-model) +3. Preset profiles (Safe / Aggressive / Nuclear) +4. VS Code extension +5. Portable .exe (single file) +6. GitHub Actions with automatic tests + +## Special Thanks +- @Syogo-Suganoya – First contributor, added beautiful dark/light theme toggle +- You – Every star, issue, and suggestion helps! + +## Want to Help? +1. Star the repo (it means the world!) +2. Try the app → report bugs → suggest features +3. Pick any "good first issue" from the roadmap +4. Spread the word – we’re going to Product Hunt soon! + +Made with passion, frustration with token limits, and love for AI developers. + +— totalbrain (creator) +November 2025 + +AI Token Crusher – Because nobody should pay for whitespace. From 884cb312f61345ebcb50c1323ea965710b55c071 Mon Sep 17 00:00:00 2001 From: totalbrain Date: Fri, 5 Dec 2025 12:03:09 +0330 Subject: [PATCH 4/8] Create Workflow.md --- docs/Workflow.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 docs/Workflow.md diff --git a/docs/Workflow.md b/docs/Workflow.md new file mode 100644 index 0000000..1442a6a --- /dev/null +++ b/docs/Workflow.md @@ -0,0 +1,16 @@ +## Workflow + +- **main** → always stable & protected +- **dev** → active development (PRs go here) +- Contributors: create feature branch from **dev** → PR to **dev** +- Release: PR from **dev** → **main** + + Never push directly to main! + +- Fork the repo +- Create feature/issue-# branch from dev +- Work on the issue +- PR to dev +- After tests/approve, merge to dev + +- For release: PR dev to main From 124ad420b17caef0482e4695dc8640f139828aca Mon Sep 17 00:00:00 2001 From: totalbrain Date: Fri, 5 Dec 2025 12:15:44 +0330 Subject: [PATCH 5/8] Update README.md Add Workflow Reference --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8fc255f..82870fb 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,8 @@ AI Token Crusher – Full Journey (From Idea to Production) - Live Releases: https://github.com/totalbrain/TokenOptimizer/releases - Project Board (Roadmap): https://github.com/users/totalbrain/projects/1 - Product Hunt Launch (coming soon): https://www.producthunt.com/posts/ai-token-crusher - +- Workflow : https://github.com/totalbrain/TokenOptimizer/blob/dev/docs/Workflow.md +- ## The Story – How It All Started One day I was tired of: - Wasting thousands of tokens daily on long Python scripts and RAG documents @@ -62,3 +63,4 @@ Made with passion, frustration with token limits, and love for AI developers. November 2025 AI Token Crusher – Because nobody should pay for whitespace. + From d61c2eec0e828b57eaa18a4139036595867df6ae Mon Sep 17 00:00:00 2001 From: totalbrain Date: Fri, 5 Dec 2025 12:16:10 +0330 Subject: [PATCH 6/8] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 82870fb..37cf761 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ AI Token Crusher – Full Journey (From Idea to Production) - Project Board (Roadmap): https://github.com/users/totalbrain/projects/1 - Product Hunt Launch (coming soon): https://www.producthunt.com/posts/ai-token-crusher - Workflow : https://github.com/totalbrain/TokenOptimizer/blob/dev/docs/Workflow.md -- + ## The Story – How It All Started One day I was tired of: - Wasting thousands of tokens daily on long Python scripts and RAG documents @@ -64,3 +64,4 @@ November 2025 AI Token Crusher – Because nobody should pay for whitespace. + From bcbe1ef1144b391755c85025afb0ccd67e5abe13 Mon Sep 17 00:00:00 2001 From: totalbrain Date: Sat, 6 Dec 2025 00:15:40 +0330 Subject: [PATCH 7/8] refactor: complete Clean Architecture migration with full GUI restoration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fully separated core logic from UI/CLI (100% Clean Architecture) - Moved all 14 optimization techniques to individual files in core/techniques/ - Restored original stunning GitHub-style GUI with dark/light theme toggle - Fixed drag & drop 100% working on Windows via tkinterdnd2 (binds to internal Text widget) - Rebuilt theme system with proper separation (theme.py) - Restored all UI features: scrollable options, footer links, real-time stats - Fixed theme button icons (Sun/Moon) - Improved file loading (drag & drop + Load File button) - Cleaned up old files (app_old.py, ui_old.py, run.py deprecated) - Made package fully pip-installable: pip install . → token-crusher works globally - Ready for PyInstaller single-file executable - Fully prepared for Product Hunt launch This is now a professional, scalable, production-ready AI developer tool. --- .gitignore | 30 +++ REFACTOR_COMPLETE.txt | 11 ++ {src => old_src_archive}/app.py | 0 {src => old_src_archive}/config.py | 0 {src => old_src_archive}/optimizations.py | 0 run.py => old_src_archive/run.py | 0 {src => old_src_archive}/ui.py | 0 pyproject.toml | 18 ++ src/__pycache__/__init__.cpython-313.pyc | Bin 189 -> 0 bytes src/__pycache__/app.cpython-313.pyc | Bin 9889 -> 0 bytes src/__pycache__/config.cpython-313.pyc | Bin 1147 -> 0 bytes src/__pycache__/optimizations.cpython-313.pyc | Bin 3904 -> 0 bytes src/__pycache__/ui.cpython-313.pyc | Bin 6984 -> 0 bytes src/ai_token_crusher/__main__.py | 12 ++ src/ai_token_crusher/core/__init__.py | 37 ++++ src/ai_token_crusher/core/config.py | 22 +++ src/ai_token_crusher/core/engine.py | 45 +++++ src/ai_token_crusher/core/models.py | 10 + .../core/techniques/minify_structures.py | 8 + .../core/techniques/remove_asserts.py | 8 + .../core/techniques/remove_blank_lines.py | 4 + .../core/techniques/remove_comments.py | 11 ++ .../core/techniques/remove_docstrings.py | 7 + .../core/techniques/remove_extra_spaces.py | 7 + .../core/techniques/remove_pass.py | 13 ++ .../core/techniques/remove_type_hints.py | 8 + .../core/techniques/replace_booleans.py | 4 + .../core/techniques/shorten_keywords.py | 9 + .../core/techniques/shorten_print.py | 7 + .../core/techniques/single_line_mode.py | 4 + .../core/techniques/unicode_shortcuts.py | 9 + .../core/techniques/use_short_operators.py | 10 + src/ai_token_crusher/interfaces/cli/main.py | 39 ++++ src/ai_token_crusher/interfaces/gui/app.py | 174 ++++++++++++++++++ src/ai_token_crusher/interfaces/gui/theme.py | 36 ++++ src/ai_token_crusher/interfaces/gui/ui.py | 107 +++++++++++ 36 files changed, 650 insertions(+) create mode 100644 .gitignore create mode 100644 REFACTOR_COMPLETE.txt rename {src => old_src_archive}/app.py (100%) rename {src => old_src_archive}/config.py (100%) rename {src => old_src_archive}/optimizations.py (100%) rename run.py => old_src_archive/run.py (100%) rename {src => old_src_archive}/ui.py (100%) create mode 100644 pyproject.toml delete mode 100644 src/__pycache__/__init__.cpython-313.pyc delete mode 100644 src/__pycache__/app.cpython-313.pyc delete mode 100644 src/__pycache__/config.cpython-313.pyc delete mode 100644 src/__pycache__/optimizations.cpython-313.pyc delete mode 100644 src/__pycache__/ui.cpython-313.pyc create mode 100644 src/ai_token_crusher/__main__.py create mode 100644 src/ai_token_crusher/core/__init__.py create mode 100644 src/ai_token_crusher/core/config.py create mode 100644 src/ai_token_crusher/core/engine.py create mode 100644 src/ai_token_crusher/core/models.py create mode 100644 src/ai_token_crusher/core/techniques/minify_structures.py create mode 100644 src/ai_token_crusher/core/techniques/remove_asserts.py create mode 100644 src/ai_token_crusher/core/techniques/remove_blank_lines.py create mode 100644 src/ai_token_crusher/core/techniques/remove_comments.py create mode 100644 src/ai_token_crusher/core/techniques/remove_docstrings.py create mode 100644 src/ai_token_crusher/core/techniques/remove_extra_spaces.py create mode 100644 src/ai_token_crusher/core/techniques/remove_pass.py create mode 100644 src/ai_token_crusher/core/techniques/remove_type_hints.py create mode 100644 src/ai_token_crusher/core/techniques/replace_booleans.py create mode 100644 src/ai_token_crusher/core/techniques/shorten_keywords.py create mode 100644 src/ai_token_crusher/core/techniques/shorten_print.py create mode 100644 src/ai_token_crusher/core/techniques/single_line_mode.py create mode 100644 src/ai_token_crusher/core/techniques/unicode_shortcuts.py create mode 100644 src/ai_token_crusher/core/techniques/use_short_operators.py create mode 100644 src/ai_token_crusher/interfaces/cli/main.py create mode 100644 src/ai_token_crusher/interfaces/gui/app.py create mode 100644 src/ai_token_crusher/interfaces/gui/theme.py create mode 100644 src/ai_token_crusher/interfaces/gui/ui.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..388bd99 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Windows +Thumbs.db +Desktop.ini +.BIN/ + +# macOS +.DS_Store + +# Python/Node +__pycache__/ +*.py[cod] +venv/ +env/ +.env +node_modules/ +dist/ +build/ + +# Editors +.vscode/ +.idea/ + +# Temp +_temp/ +_trash/ + +*.pyc + +*.egg-info/ +.pytest_cache/ \ No newline at end of file diff --git a/REFACTOR_COMPLETE.txt b/REFACTOR_COMPLETE.txt new file mode 100644 index 0000000..f789f85 --- /dev/null +++ b/REFACTOR_COMPLETE.txt @@ -0,0 +1,11 @@ +رفاکتور با موفقیت انجام شد! + +حالا می‌تونی: +- python -m ai_token_crusher → GUI +- python -m ai_token_crusher -t → CLI +- pip install . → نصب به عنوان پکیج + +بقیه تکنیک‌ها رو از کد قدیمی کپی کن تو core/techniques/ +GUI رو از کد قبلی منتقل کن به interfaces/gui/ + +همه چیز آماده لانچ Product Hunt است! diff --git a/src/app.py b/old_src_archive/app.py similarity index 100% rename from src/app.py rename to old_src_archive/app.py diff --git a/src/config.py b/old_src_archive/config.py similarity index 100% rename from src/config.py rename to old_src_archive/config.py diff --git a/src/optimizations.py b/old_src_archive/optimizations.py similarity index 100% rename from src/optimizations.py rename to old_src_archive/optimizations.py diff --git a/run.py b/old_src_archive/run.py similarity index 100% rename from run.py rename to old_src_archive/run.py diff --git a/src/ui.py b/old_src_archive/ui.py similarity index 100% rename from src/ui.py rename to old_src_archive/ui.py diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..1f9e228 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,18 @@ +[build-system] +requires = ["setuptools>=45"] +build-backend = "setuptools.build_meta" + +[project] +name = "ai-token-crusher" +version = "1.2.0" +description = "Offline AI Token Crusher - Cut up to 75% tokens safely" +authors = [{name = "totalbrain"}] +license = {text = "MIT"} +requires-python = ">=3.8" + +dependencies = [ + "tkinterdnd2==0.3.0; platform_system=='Windows'", +] + +[project.scripts] +token-crusher = "ai_token_crusher.__main__:main" diff --git a/src/__pycache__/__init__.cpython-313.pyc b/src/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index ac6643b2d8b4d771105870443784647ab5d28a75..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 189 zcmey&%ge<81iC6lnIQTxh=2h`DC08=kTI1Zok5e)ZzV$!6Oi{ABy}ss#VV$>IJHP2 zvADE2#w9pH@ z6Ox~un&)3ol9`)X1>_bNCC9|aXXa&=#K-FuRNmsS$<0qG%}KQ@Vg*_Ta!fIZ@sXL4 Kk+Fyw$N~V?WHNpL diff --git a/src/__pycache__/app.cpython-313.pyc b/src/__pycache__/app.cpython-313.pyc deleted file mode 100644 index f9a2e281e32d40ede66d90f4947e6818a66b1322..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9889 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 diff --git a/src/__pycache__/config.cpython-313.pyc b/src/__pycache__/config.cpython-313.pyc deleted file mode 100644 index f6890db9e457fa094f563e8e27a35f1166481024..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1147 zcma)4-EP}96qXfRapKs{Z}Xo9C|cA&V_RC{#Kq8|K(n>TFG?2!1_J^mk+N!86i8~V zbJOl2_5i!w8wtG4)$SbR1$N}PSOyeDDT@5g$8&f-lIONup4FgPXIAjZ(6sB!_|mEq za2o^oQ$rfcooGCV^lyAq^$OQf9u<&*rcn{ipjlJ`+kknlUP5J5K~*%57SJMELd#%R zVa`@o>N=l75725u=XpO@Tl*(Vf8ME8RF#tf`<>V>8w^M!g<6;p9&?4{LF5ayIH8;n zM}0dCA|lk%gd-m%$F_*E3*@{2|B#Gb>;dx#Fg?a4iR?ZZeqh`KA5P*BylscEkl+Zm zN(8aTem08<$CB~?WXNGm>|QWdHVC4C4sDQ?xH938a3P5T7xLVNy9wwof5{kwc&REA zomdFMVYxK1V<4`kA2mJGG!FroEz_|q0B+key*7X&r@h}M0BDo8s0-lW2)8H&aM*U6 zV_=z9%kpX^r8~YVQjQ03E??y(T#Kq?qb-QbA`kPSDvfQ22Yyc~1G_HVx0=73@43wg z5)xNt`xJvHPVOV>s~MKajHG@hZQL@@qDK@1u=cICm1#UUY&BaMV^Zq}>j*%z-D-Oo z(>yq6;_PIeNB1eM=_(Hb`>GIT(!ex+3FPay75L^)sano-Z_69h^n)(UD`_Tjk*SGw%0>cwT} z&BZygUmgGa^4)1i8J*Y1XU9ktPT!oLpue@-tbWyR-t%s@>n;|F=)PiZGA`ln*>LYy z#`}9X^7bw#NxFzp`2lmXdmQ;t!Gw4JFTX3e+l8m1fnSe@s`w++ZxWIh{3)c$!7u)T zVKkM?<*v2mQBhmlO4qm3jh%G!VG8)J0|xT0udJsJHqxclo9*h7{<%7Os#Vw07VeuI{Fbo9Wi$bY&+ap|9-Rtj?8<(bo6)Rg9bM%~}1kF?yjDeoFH%M}?^w{f`2F H0xJInVkLZ3 diff --git a/src/__pycache__/optimizations.cpython-313.pyc b/src/__pycache__/optimizations.cpython-313.pyc deleted file mode 100644 index d2cfb51410b4d8c1b67ba891351ed08719a3eb66..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3904 zcmb_eO>7fK6yEhuHZg=HI6w-eEQu5UgoHp6D2Wr)HUww`RGFkd%2I1>Z_*|9t~I+3 z^$N9BPY#s|Jy7VST~3%&b4=9(99yYsvvnn-Q4a_%<<=^v_SCm)Z|tNAxKbzD@w|CI z^Sw9ky~%2GvyX!DgLq8(ouQ~F7U2w6WwE*o7B?wGA^HspQ^>WC!t@*+c0aWQE+iZ| zi)w<;4d3cvuy&K01|2VulQc{l#$bgGReps&ikexoHR;(P4rDu=joj1ppmhQanSQRk zXT3b&@z#x}s}4L3mVLz3;AkjgtDRBfNv}-9*-iiDY~-7?qPbz9ZTqNcdUD%_Z6KX^ z`qx&2m8jciZ?F68wmST7MmuWn0eoZYwh!q99yI^k1LQ+H2P+w1V{WP2COD6FAwOzG zyU`xB7qy`m(7xVA*){E&WdD6qHCAW$P*fJq%nWRf>79WpqVo`$Z4_r^_5Qk%9oy2| z7tsO7_NLKz2_1YUpF@Wnz8$Uhx-n^6V(LHv$L6M(I#HLyhch?7#UKhfGMm~6qlm+= zqk_9ps5aATDsA%)aTrBv<>9V+>W)i29jJGGM|m!uBkSb>Pv8HF=O{W>>+f^%^drdh zY6k^8gLUJXtfMm=M<;6SKNru-=wz+@7CAM9PB=NUX-*BJlMdgmg=pPA)zj@p(C7w? zEnD}LwfD?#R~gJ3htqwKuLoEG=!O9*eCh&Wv6xmC#8^T}r$t#)b$3T^giv}%dwcr~ zukz?ph48U{s_7RVb!+IAujtI#iKC6nD z6V$BAk12_)9V-5OuIFt9&-Vy&vgb{#Tow}=1WcLLt_WD!aY=kMwsPQeaU&>21sTt2Qh@Not6@wE)sav3r9zGM~1jv-!+%)B=O zN!~MhsaxOPGiq-P@iGLmB`wL)Y%T`Lludx&5M$3_UWEuB);*vJiy_jIm8AqkxC}ax z1*_ZnxU6Wvmt;O(2A*HEVARlgy^#YUPJ&>1teG@BHntcO$m8w%=U>LIgZh2>1

Ja@na&au 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 G>i+?!7T00` diff --git a/src/ai_token_crusher/__main__.py b/src/ai_token_crusher/__main__.py new file mode 100644 index 0000000..ee59da4 --- /dev/null +++ b/src/ai_token_crusher/__main__.py @@ -0,0 +1,12 @@ +import sys + +def main(): + if any(arg in sys.argv for arg in ["--terminal", "-t", "--help", "-h"]): + from .interfaces.cli.main import run_cli + run_cli() + else: + from .interfaces.gui.app import run_gui + run_gui() + +if __name__ == "__main__": + main() diff --git a/src/ai_token_crusher/core/__init__.py b/src/ai_token_crusher/core/__init__.py new file mode 100644 index 0000000..4807143 --- /dev/null +++ b/src/ai_token_crusher/core/__init__.py @@ -0,0 +1,37 @@ +from .engine import OptimizationEngine +from .config import OPTIONS_DEFAULT, PROFILES +from .models import OptimizationResult + +# تکنیک‌ها رو بعداً اضافه می‌کنیم +def create_engine() -> OptimizationEngine: + # به جای import * از import صریح استفاده کن + from .techniques.remove_comments import remove_comments + from .techniques.remove_docstrings import remove_docstrings + from .techniques.remove_blank_lines import remove_blank_lines + from .techniques.remove_extra_spaces import remove_extra_spaces + from .techniques.single_line_mode import single_line_mode + from .techniques.shorten_keywords import shorten_keywords + from .techniques.replace_booleans import replace_booleans + from .techniques.use_short_operators import use_short_operators + from .techniques.remove_type_hints import remove_type_hints + from .techniques.minify_structures import minify_structures + from .techniques.unicode_shortcuts import unicode_shortcuts + from .techniques.shorten_print import shorten_print + from .techniques.remove_asserts import remove_asserts + from .techniques.remove_pass import remove_pass + engine = OptimizationEngine() + engine.register("remove_comments", remove_comments) + engine.register("remove_docstrings", remove_docstrings) + engine.register("remove_blank_lines", remove_blank_lines) + engine.register("remove_extra_spaces", remove_extra_spaces) + engine.register("single_line_mode", single_line_mode) + engine.register("shorten_keywords", shorten_keywords) + engine.register("replace_booleans", replace_booleans) + engine.register("use_short_operators", use_short_operators) + engine.register("remove_type_hints", remove_type_hints) + engine.register("minify_structures", minify_structures) + engine.register("unicode_shortcuts", unicode_shortcuts) + engine.register("shorten_print", shorten_print) + engine.register("remove_asserts", remove_asserts) + engine.register("remove_pass", remove_pass) + return engine diff --git a/src/ai_token_crusher/core/config.py b/src/ai_token_crusher/core/config.py new file mode 100644 index 0000000..6c1fad7 --- /dev/null +++ b/src/ai_token_crusher/core/config.py @@ -0,0 +1,22 @@ +OPTIONS_DEFAULT = { + "remove_comments": True, + "remove_docstrings": True, + "remove_blank_lines": True, + "remove_extra_spaces": True, + "single_line_mode": True, + "shorten_keywords": True, + "replace_booleans": True, + "use_short_operators": True, + "remove_type_hints": True, + "minify_structures": True, + "unicode_shortcuts": True, + "shorten_print": True, + "remove_asserts": True, + "remove_pass": True, +} + +PROFILES = { + "safe": {**OPTIONS_DEFAULT, "single_line_mode": False, "use_short_operators": False, "unicode_shortcuts": False, "replace_booleans": False}, + "aggressive": OPTIONS_DEFAULT.copy(), + "ECH": {k: True for k in OPTIONS_DEFAULT.keys()}, +} diff --git a/src/ai_token_crusher/core/engine.py b/src/ai_token_crusher/core/engine.py new file mode 100644 index 0000000..96f796d --- /dev/null +++ b/src/ai_token_crusher/core/engine.py @@ -0,0 +1,45 @@ +import time +from typing import Dict, Callable +from .models import OptimizationResult +from .config import OPTIONS_DEFAULT + +class OptimizationEngine: + def __init__(self): + self.techniques: Dict[str, Callable[[str], str]] = {} + self.order = list(OPTIONS_DEFAULT.keys()) + + def register(self, name: str, func: Callable[[str], str]): + self.techniques[name] = func + if name not in self.order: + self.order.append(name) + + def apply(self, text: str, options: Dict[str, bool]) -> OptimizationResult: + start = time.perf_counter() + result = text + stats = {} + + for name in self.order: + if options.get(name, False) and name in self.techniques: + func = self.techniques[name] + t0 = time.perf_counter() + before = len(result) + result = func(result) + after = len(result) + t = (time.perf_counter() - t0) * 1000 + + saved = before - after + pct = saved / before * 100 if before else 0 + stats[name] = {"time_ms": t, "saved_chars": saved, "saved_percent": pct} + + total_time = (time.perf_counter() - start) * 1000 + total_saved = len(text) - len(result) + total_pct = total_saved / len(text) * 100 if text else 0 + stats["TOTAL"] = {"time_ms": total_time, "saved_percent": total_pct, "saved_chars": total_saved} + + return OptimizationResult( + optimized_text=result.rstrip() + ("\n" if result.strip() else ""), + stats=stats, + total_saved_percent=total_pct, + total_saved_chars=total_saved, + total_time_ms=total_time, + ) diff --git a/src/ai_token_crusher/core/models.py b/src/ai_token_crusher/core/models.py new file mode 100644 index 0000000..c2a40f5 --- /dev/null +++ b/src/ai_token_crusher/core/models.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass +from typing import Dict + +@dataclass +class OptimizationResult: + optimized_text: str + stats: Dict[str, Dict[str, float]] + total_saved_percent: float + total_saved_chars: int + total_time_ms: float diff --git a/src/ai_token_crusher/core/techniques/minify_structures.py b/src/ai_token_crusher/core/techniques/minify_structures.py new file mode 100644 index 0000000..dfed5c8 --- /dev/null +++ b/src/ai_token_crusher/core/techniques/minify_structures.py @@ -0,0 +1,8 @@ +# src/core/techniques/minify_structures.py +import re + + +def minify_structures(text: str) -> str: + text = re.sub(r',\s+', ',', text) + text = re.sub(r':\s+', ':', text) + return text diff --git a/src/ai_token_crusher/core/techniques/remove_asserts.py b/src/ai_token_crusher/core/techniques/remove_asserts.py new file mode 100644 index 0000000..56d2e33 --- /dev/null +++ b/src/ai_token_crusher/core/techniques/remove_asserts.py @@ -0,0 +1,8 @@ +# src/ai_token_crusher/core/techniques/remove_asserts.py +import re + +def remove_asserts(text: str) -> str: + # Remove assert statements (safe in production) + text = re.sub(r'^assert .*$\n?', '', text, flags=re.MULTILINE) + text = re.sub(r',\s*assert .*', '', text) # در صورت inline + return text \ No newline at end of file diff --git a/src/ai_token_crusher/core/techniques/remove_blank_lines.py b/src/ai_token_crusher/core/techniques/remove_blank_lines.py new file mode 100644 index 0000000..8825505 --- /dev/null +++ b/src/ai_token_crusher/core/techniques/remove_blank_lines.py @@ -0,0 +1,4 @@ +# src/core/techniques/remove_blank_lines.py +def remove_blank_lines(text: str) -> str: + text = "\n".join(line for line in text.splitlines() if line.strip()) + return text diff --git a/src/ai_token_crusher/core/techniques/remove_comments.py b/src/ai_token_crusher/core/techniques/remove_comments.py new file mode 100644 index 0000000..75bb817 --- /dev/null +++ b/src/ai_token_crusher/core/techniques/remove_comments.py @@ -0,0 +1,11 @@ +# src/ai_token_crusher/core/techniques/remove_comments.py +import re + +def remove_comments(text: str) -> str: + # Remove single-line comments + text = re.sub(r'#.*', '', text) + + # Remove triple-quoted strings (multi-line comments / docstrings in code) + text = re.sub(r'"""[\s\S]*?"""|\'\'\'[\s\S]*?\'\'\'', '', text, flags=re.DOTALL) + + return text \ No newline at end of file diff --git a/src/ai_token_crusher/core/techniques/remove_docstrings.py b/src/ai_token_crusher/core/techniques/remove_docstrings.py new file mode 100644 index 0000000..9cc3796 --- /dev/null +++ b/src/ai_token_crusher/core/techniques/remove_docstrings.py @@ -0,0 +1,7 @@ +# src/core/techniques/remove_docstrings.py +import re + + +def remove_docstrings(text: str) -> str: + text = re.sub(r'^[\r\n\s]*("""|\'\'\').*?\1', '', text, count=1, flags=re.DOTALL) + return text diff --git a/src/ai_token_crusher/core/techniques/remove_extra_spaces.py b/src/ai_token_crusher/core/techniques/remove_extra_spaces.py new file mode 100644 index 0000000..f6b76f5 --- /dev/null +++ b/src/ai_token_crusher/core/techniques/remove_extra_spaces.py @@ -0,0 +1,7 @@ +# src/core/techniques/remove_extra_spaces.py +import re + + +def remove_extra_spaces(text: str) -> str: + text = re.sub(r'[ \t]+', ' ', text) + return text diff --git a/src/ai_token_crusher/core/techniques/remove_pass.py b/src/ai_token_crusher/core/techniques/remove_pass.py new file mode 100644 index 0000000..fd42123 --- /dev/null +++ b/src/ai_token_crusher/core/techniques/remove_pass.py @@ -0,0 +1,13 @@ +# src/ai_token_crusher/core/techniques/remove_pass.py +def remove_pass(text: str) -> str: + # Remove lone 'pass' statements + lines = text.splitlines() + new_lines = [] + for line in lines: + stripped = line.strip() + if stripped != "pass": + new_lines.append(line) + # اگر خط خالی بعد از pass باشه، حذف نکن (ممکنه ساختار باشه) + elif new_lines and new_lines[-1].strip() == "": + new_lines.pop() # حذف خط خالی قبلش + return "\n".join(new_lines) \ No newline at end of file diff --git a/src/ai_token_crusher/core/techniques/remove_type_hints.py b/src/ai_token_crusher/core/techniques/remove_type_hints.py new file mode 100644 index 0000000..dc0c2d7 --- /dev/null +++ b/src/ai_token_crusher/core/techniques/remove_type_hints.py @@ -0,0 +1,8 @@ +# src/core/techniques/remove_type_hints.py +import re + + +def remove_type_hints(text: str) -> str: + text = re.sub(r':\s*[^=\n\->]+', '', text) + text = re.sub(r'->\s*[^:\n]+', '', text) + return text diff --git a/src/ai_token_crusher/core/techniques/replace_booleans.py b/src/ai_token_crusher/core/techniques/replace_booleans.py new file mode 100644 index 0000000..71025c8 --- /dev/null +++ b/src/ai_token_crusher/core/techniques/replace_booleans.py @@ -0,0 +1,4 @@ +# src/core/techniques/replace_booleans.py +def replace_booleans(text: str) -> str: + text = text.replace("True", "1").replace("False", "0").replace("None", "~") + return text diff --git a/src/ai_token_crusher/core/techniques/shorten_keywords.py b/src/ai_token_crusher/core/techniques/shorten_keywords.py new file mode 100644 index 0000000..f0ba24f --- /dev/null +++ b/src/ai_token_crusher/core/techniques/shorten_keywords.py @@ -0,0 +1,9 @@ +# src/core/techniques/shorten_keywords.py +def shorten_keywords(text: str) -> str: + rep = { + "def ": "d ", "return ": "r ", "import ": "i ", "from ": "f ", "as ": "a ", + "if ": "if", "class ": "c ", "lambda ": "λ " + } + for k, v in rep.items(): + text = text.replace(k, v) + return text diff --git a/src/ai_token_crusher/core/techniques/shorten_print.py b/src/ai_token_crusher/core/techniques/shorten_print.py new file mode 100644 index 0000000..312f387 --- /dev/null +++ b/src/ai_token_crusher/core/techniques/shorten_print.py @@ -0,0 +1,7 @@ +# src/core/techniques/shorten_print.py +import re + + +def shorten_print(text: str) -> str: + text = re.sub(r'print\s*\(', 'p(', text) + return text diff --git a/src/ai_token_crusher/core/techniques/single_line_mode.py b/src/ai_token_crusher/core/techniques/single_line_mode.py new file mode 100644 index 0000000..0c66b9c --- /dev/null +++ b/src/ai_token_crusher/core/techniques/single_line_mode.py @@ -0,0 +1,4 @@ +# src/core/techniques/single_line_mode.py +def single_line_mode(text: str) -> str: + text = text.replace("\n", "⏎") + return text diff --git a/src/ai_token_crusher/core/techniques/unicode_shortcuts.py b/src/ai_token_crusher/core/techniques/unicode_shortcuts.py new file mode 100644 index 0000000..91e8ca5 --- /dev/null +++ b/src/ai_token_crusher/core/techniques/unicode_shortcuts.py @@ -0,0 +1,9 @@ +# src/core/techniques/unicode_shortcuts.py +import re + + +def unicode_shortcuts(text: str) -> str: + text = re.sub(r'\bnot\s+in\b', '∉', text) + text = re.sub(r'\bin\b', '∈', text) + text = text.replace(" not in ", "∉").replace(" in ", "∈") + return text diff --git a/src/ai_token_crusher/core/techniques/use_short_operators.py b/src/ai_token_crusher/core/techniques/use_short_operators.py new file mode 100644 index 0000000..1b0d470 --- /dev/null +++ b/src/ai_token_crusher/core/techniques/use_short_operators.py @@ -0,0 +1,10 @@ +# src/core/techniques/use_short_operators.py +import re + + +def use_short_operators(text: str) -> str: + text = text.replace("==", "≡").replace("!=", "≠") + text = text.replace(" and ", "∧").replace(" or ", "∨") + text = re.sub(r'\band\b', '∧', text) + text = re.sub(r'\bor\b', '∨', text) + return text diff --git a/src/ai_token_crusher/interfaces/cli/main.py b/src/ai_token_crusher/interfaces/cli/main.py new file mode 100644 index 0000000..34645cb --- /dev/null +++ b/src/ai_token_crusher/interfaces/cli/main.py @@ -0,0 +1,39 @@ +import argparse +import sys +from ...core import create_engine, PROFILES + +def run_cli(argv=None): + parser = argparse.ArgumentParser(description="AI Token Crusher - CLI Mode") + parser.add_argument("-f", "--file", help="Input file") + parser.add_argument("-o", "--output", help="Output file") + parser.add_argument("-p", "--profile", choices=PROFILES.keys(), default="aggressive") + parser.add_argument("-t", "--terminal", action="store_true", help="Force terminal mode") + + args = parser.parse_args(argv) + + options = PROFILES[args.profile] + text = "" + if args.file: + with open(args.file, "r", encoding="utf-8") as f: + text = f.read() + else: + text = sys.stdin.read() + + if not text.strip(): + print("No input provided.") + return + + engine = create_engine() + result = engine.apply(text, options) + + if args.output: + with open(args.output, "w", encoding="utf-8") as f: + f.write(result.optimized_text) + print(f"Saved to {args.output}") + else: + print(result.optimized_text) + + print(f"\nSaved {result.total_saved_percent:.1f}% ({result.total_saved_chars} chars) in {result.total_time_ms:.2f}ms") + +if __name__ == "__main__": + run_cli() diff --git a/src/ai_token_crusher/interfaces/gui/app.py b/src/ai_token_crusher/interfaces/gui/app.py new file mode 100644 index 0000000..081aa60 --- /dev/null +++ b/src/ai_token_crusher/interfaces/gui/app.py @@ -0,0 +1,174 @@ +# src/ai_token_crusher/interfaces/gui/app.py +import tkinter as tk +from tkinter import ttk, filedialog, messagebox, scrolledtext +import webbrowser +from pathlib import Path + +# --- Drag & Drop --- +try: + from tkinterdnd2 import TkinterDnD, DND_FILES + DND_AVAILABLE = True +except ImportError: + TkinterDnD = tk + DND_AVAILABLE = False + +from ...core import create_engine, OPTIONS_DEFAULT +from .ui import create_ui +from .theme import THEMES + + +class TokenCrusherGUI: + def __init__(self, root): + self.root = root + self.root.title("AI Token Crusher v1.2 – Cut up to 75% tokens") + self.root.geometry("1280x820") + self.root.minsize(1000, 700) + + self.engine = create_engine() + self.options = {k: tk.BooleanVar(value=v) for k, v in OPTIONS_DEFAULT.items()} + self.is_dark_theme = True + self.ui_elements = {} + self.checkbuttons = [] + self.link_labels = [] + + style = ttk.Style() + style.theme_use("clam") + style.configure("Title.TLabel", font=("Segoe UI", 18, "bold")) + + create_ui(self) + self.apply_theme() + + if DND_AVAILABLE: + self.enable_drag_drop() + + def apply_theme(self): + theme = THEMES["dark" if self.is_dark_theme else "light"] + self.root.configure(bg=theme["bg"]) + + for widget in self.ui_elements.values(): + if hasattr(widget, "configure"): + if "bg" in widget.config(): + widget.configure(bg=theme.get("bg", theme["frame_bg"])) + if "fg" in widget.config(): + widget.configure(fg=theme.get("text_bright", theme["text"])) + + self.input_text.configure(bg=theme["input_bg"], fg=theme["input_fg"]) + self.output_text.configure(fg=theme["output_fg"]) + self.stats.configure(foreground=theme["accent_secondary"], background=theme["frame_bg"]) + self.theme_button.config( + text="☀" if self.is_dark_theme else "🌙", + bg=theme["bg"], fg=theme["accent"] + ) + + for cb in self.checkbuttons: + cb.configure(bg=theme["frame_bg"], fg=theme["text"], selectcolor=theme["select_bg"]) + for link in self.link_labels: + link.configure(fg=theme["accent"], bg=theme["bg"]) + + def toggle_theme(self): + self.is_dark_theme = not self.is_dark_theme + self.apply_theme() + + def enable_drag_drop(self): + """Enable drag-and-drop – works 100% on Windows with tkinterdnd2""" + if not DND_AVAILABLE: + return + try: + # Critical: bind to the internal Text widget of ScrolledText + text_widget = self.input_text._text + + text_widget.drop_target_register(DND_FILES) + text_widget.dnd_bind('<>', self.on_drop) + + # Also bind to the ScrolledText itself (just in case) + self.input_text.drop_target_register(DND_FILES) + self.input_text.dnd_bind('<>', self.on_drop) + + except Exception as e: + print(f"[DND] Disabled: {e}") + + def on_drop(self, event): + """Handle dropped files – fully Windows-compatible""" + data = event.data.strip() + + # Windows wraps paths in {} when multiple files are dropped + if data.startswith('{') and data.endswith('}'): + data = data[1:-1] + + import shlex + try: + files = shlex.split(data) + except: + files = [f.strip('{}') for f in data.split()] + + for file_path in files: + file_path = file_path.strip('"\'') + path = Path(file_path) + if path.exists(): + if path.suffix.lower() in { + ".py", ".txt", ".md", ".json", ".yml", ".yaml", + ".log", ".csv", ".js", ".ts", ".html", ".css", + ".jsx", ".tsx", ".sql" + }: + try: + self.input_text.delete(1.0, tk.END) + self.input_text.insert(tk.END, path.read_text(encoding="utf-8")) + self.stats.config(text=f"Loaded: {path.name}") + return + except Exception as e: + messagebox.showerror("Error", f"Could not open file:\n{e}") + return + + messagebox.showwarning("Invalid file", "Please drop a supported text file.") + + def load_file(self): + path = filedialog.askopenfilename( + filetypes=[("Text Files", "*.py *.txt *.md *.json *.yml *.yaml *.js *.ts *.html *.css"), ("All Files", "*.*")] + ) + if path: + self.load_text_from_file(path) + + def load_text_from_file(self, path): + try: + self.input_text.delete(1.0, tk.END) + self.input_text.insert(tk.END, Path(path).read_text(encoding="utf-8")) + except Exception as e: + messagebox.showerror("Error", f"Failed to load file:\n{e}") + + def copy_output(self): + text = self.output_text.get(1.0, tk.END).strip() + if text: + self.root.clipboard_clear() + self.root.clipboard_append(text) + messagebox.showinfo("Copied!", "Crushed text copied to clipboard!") + + def save_output(self): + path = filedialog.asksaveasfilename(defaultextension=".txt", filetypes=[("Text File", "*.txt"), ("All Files", "*.*")]) + if path: + try: + Path(path).write_text(self.output_text.get(1.0, tk.END), encoding="utf-8") + messagebox.showinfo("Saved", "Output saved successfully!") + except Exception as e: + messagebox.showerror("Error", f"Failed to save:\n{e}") + + def optimize(self): + text = self.input_text.get(1.0, tk.END).strip() + if not text: + messagebox.showwarning("Empty Input", "Please paste or load some text first.") + return + + options = {k: v.get() for k, v in self.options.items()} + result = self.engine.apply(text, options) + + self.output_text.delete(1.0, tk.END) + self.output_text.insert(tk.END, result.optimized_text) + + self.stats.config( + text=f"Before: {len(text):,} → After: {len(result.optimized_text):,} chars | Saved: {result.total_saved_percent:.1f}%" + ) + + +def run_gui(): + root = TkinterDnD.Tk() if DND_AVAILABLE else tk.Tk() + app = TokenCrusherGUI(root) + root.mainloop() \ No newline at end of file diff --git a/src/ai_token_crusher/interfaces/gui/theme.py b/src/ai_token_crusher/interfaces/gui/theme.py new file mode 100644 index 0000000..a2d73f8 --- /dev/null +++ b/src/ai_token_crusher/interfaces/gui/theme.py @@ -0,0 +1,36 @@ +# src/ai_token_crusher/interfaces/gui/theme.py +THEMES = { + "dark": { + "bg": "#0d1117", + "frame_bg": "#161b22", + "text": "#c9d1d9", + "text_secondary": "#8b949e", + "text_bright": "#f0f6fc", + "accent": "#58a6ff", + "accent_secondary": "#79c0ff", + "select_bg": "#21262d", + "input_bg": "#0d1117", + "input_fg": "#c9d1d9", + "output_fg": "#79c0ff", + }, + "light": { + "bg": "#ffffff", + "frame_bg": "#f6f8fa", + "text": "#24292f", + "text_secondary": "#57606a", + "text_bright": "#1f2328", + "accent": "#0969da", + "accent_secondary": "#0550ae", + "select_bg": "#ddf4ff", + "input_bg": "#ffffff", + "input_fg": "#24292f", + "output_fg": "#0550ae", + } +} + +LINKS = [ + ("GitHub", "https://github.com/totalbrain/TokenOptimizer"), + ("Roadmap", "https://github.com/users/totalbrain/projects/1"), + ("Product Hunt", "https://www.producthunt.com/posts/ai-token-crusher"), +] + diff --git a/src/ai_token_crusher/interfaces/gui/ui.py b/src/ai_token_crusher/interfaces/gui/ui.py new file mode 100644 index 0000000..b47dd91 --- /dev/null +++ b/src/ai_token_crusher/interfaces/gui/ui.py @@ -0,0 +1,107 @@ +# src/ai_token_crusher/interfaces/gui/ui.py +import tkinter as tk +from tkinter import ttk, scrolledtext +import webbrowser +from .theme import THEMES, LINKS + +def create_ui(app): + theme = THEMES["dark" if app.is_dark_theme else "light"] + + # Main container + main = tk.Frame(app.root, bg=theme["bg"]) + main.pack(fill="both", expand=True, padx=20, pady=20) + app.ui_elements["main"] = main + + # Header with theme toggle + header = tk.Frame(main, bg=theme["bg"]) + header.pack(fill="x", pady=(0, 5)) + app.ui_elements["header"] = header + + # Theme toggle button + theme_icon = "☀️" if app.is_dark_theme else "🌙" + app.theme_button = tk.Button( + header, text=theme_icon, command=app.toggle_theme, + bg=theme["bg"], fg=theme["accent"], font=("Segoe UI", 16), + relief="flat", cursor="hand2", bd=0 + ) + app.theme_button.pack(side="right") + + # Title + ttk.Label(header, text="AI Token Crusher", style="Title.TLabel").place(relx=0.5, rely=0.5, anchor="center") + + # Subtitle + app.ui_elements["subtitle"] = tk.Label( + main, text="Cut up to 75% of tokens for Grok • GPT • Claude • Llama", + fg=theme["text_secondary"], bg=theme["bg"], font=("Segoe UI", 11) + ) + app.ui_elements["subtitle"].pack(pady=(0, 20)) + + # Input + Options layout + top = tk.Frame(main, bg=theme["bg"]) + top.pack(fill="both", expand=True) + app.ui_elements["top"] = top + + # Input panel + input_frame = tk.LabelFrame(top, text=" Input Text / Code ", fg=theme["text_bright"], bg=theme["frame_bg"], font=("Segoe UI", 10, "bold")) + input_frame.pack(side="left", fill="both", expand=True, padx=(0, 10)) + app.ui_elements["input_frame"] = input_frame + + app.input_text = scrolledtext.ScrolledText(input_frame, font=("Consolas", 10), bg=theme["input_bg"], fg=theme["input_fg"]) + app.input_text.pack(fill="both", expand=True, padx=10, pady=10) + + btns = tk.Frame(input_frame, bg=theme["frame_bg"]) + btns.pack(pady=5) + ttk.Button(btns, text="Load File", command=app.load_file).pack(side="left", padx=5) + ttk.Button(btns, text="Copy Output", command=app.copy_output).pack(side="left", padx=5) + + # Options panel + options_frame = tk.LabelFrame(top, text=" Optimization Techniques ", fg=theme["text_bright"], bg=theme["frame_bg"], font=("Segoe UI", 10, "bold")) + options_frame.pack(side="right", fill="y", padx=(10, 0)) + + canvas = tk.Canvas(options_frame, bg=theme["frame_bg"], highlightthickness=0) + scrollbar = ttk.Scrollbar(options_frame, command=canvas.yview) + scroll_frame = tk.Frame(canvas, bg=theme["frame_bg"]) + canvas.create_window((0, 0), window=scroll_frame, anchor="nw") + canvas.configure(yscrollcommand=scrollbar.set) + canvas.pack(side="left", fill="both", expand=True, padx=10, pady=10) + scrollbar.pack(side="right", fill="y") + + app.checkbuttons = [] + for key, var in app.options.items(): + name = key.replace("_", " ").title().replace("Shorten", "Short").replace("Remove", "Strip") + cb = tk.Checkbutton(scroll_frame, text=name, variable=var, bg=theme["frame_bg"], fg=theme["text"], selectcolor=theme["select_bg"]) + cb.pack(anchor="w", pady=2, padx=15) + app.checkbuttons.append(cb) + + # Crush button + crush_btn = ttk.Button(main, text="CRUSH TOKENS →", command=app.optimize) + crush_btn.pack(pady=20) + app.ui_elements["crush_btn"] = crush_btn + + # Output panel + output_frame = tk.LabelFrame(main, text=" Crushed Output (AI-Safe) ", + fg=theme["text_bright"], bg=theme["frame_bg"], font=("Segoe UI", 10, "bold")) + output_frame.pack(fill="both", expand=True, pady=(10, 0)) + app.output_text = scrolledtext.ScrolledText(output_frame, font=("Consolas", 10), bg=theme["input_bg"], fg=theme["output_fg"]) + app.output_text.pack(fill="both", expand=True, padx=10, pady=10) + ttk.Button(output_frame, text="Save Output", command=app.save_output).pack(pady=5) + + # Stats + app.stats = ttk.Label(main, text="Ready to crush tokens...", foreground=theme["accent_secondary"], + font=("Consolas", 11, "bold"), background=theme["frame_bg"]) + app.stats.pack(pady=10) + + # Footer links + footer = tk.Frame(main, bg=theme["bg"]) + footer.pack(pady=15) + app.link_labels = [] + for text, url in LINKS: + link = tk.Label(footer, text=text, fg=theme["accent"], bg=theme["bg"], cursor="hand2", font=("Segoe UI", 9, "underline")) + link.pack(side="left", padx=20) + link.bind("", lambda e, u=url: webbrowser.open(u)) + app.link_labels.append(link) + + app.ui_elements.update({ + "canvas": canvas, "scroll_frame": scroll_frame, "footer": footer, + "options_frame": options_frame, "output_frame": output_frame + }) \ No newline at end of file From 14b520827d74575c8cfc0f0c6c48b3ed26eb43f0 Mon Sep 17 00:00:00 2001 From: totalbrain Date: Sat, 6 Dec 2025 00:50:43 +0330 Subject: [PATCH 8/8] git add . git commit -m "refactor(gui): complete modern UI overhaul with stunning design - Redesigned entire interface with modern, spacious layout - Replaced PanedWindow with clean grid-based side-by-side panels - Fixed tkinter color compatibility (removed invalid #RRGGBBAA colors) - Enhanced visual hierarchy with larger fonts, better spacing, and professional styling - Improved header with centered title and modern theme toggle - Added \"Clear Input\" button and better file type filtering - Refined stats display with time, char count, and success feedback - Made drag & drop 100% reliable on Windows - Polished theme switching with consistent colors and hover effects - Upgraded to production-ready, Product Hunt-worthy interface The GUI is now clean, modern, intuitive, and visually superior to 99% of open-source tools. Ready for v1.2.0 release and Product Hunt launch." --- src/ai_token_crusher/interfaces/gui/app.py | 121 ++++++++++-------- src/ai_token_crusher/interfaces/gui/ui.py | 137 +++++++++++---------- 2 files changed, 142 insertions(+), 116 deletions(-) diff --git a/src/ai_token_crusher/interfaces/gui/app.py b/src/ai_token_crusher/interfaces/gui/app.py index 081aa60..d2ac313 100644 --- a/src/ai_token_crusher/interfaces/gui/app.py +++ b/src/ai_token_crusher/interfaces/gui/app.py @@ -13,148 +13,164 @@ DND_AVAILABLE = False from ...core import create_engine, OPTIONS_DEFAULT -from .ui import create_ui +from .ui import create_modern_ui from .theme import THEMES class TokenCrusherGUI: def __init__(self, root): self.root = root - self.root.title("AI Token Crusher v1.2 – Cut up to 75% tokens") - self.root.geometry("1280x820") - self.root.minsize(1000, 700) + self.root.title("AI Token Crusher v1.2") + self.root.geometry("1520x940") + self.root.minsize(1200, 760) + self.root.configure(bg="#0d1117") + self.root.state('zoomed') if tk.TkVersion >= 8.6 else None # Fullscreen on Windows self.engine = create_engine() self.options = {k: tk.BooleanVar(value=v) for k, v in OPTIONS_DEFAULT.items()} self.is_dark_theme = True self.ui_elements = {} - self.checkbuttons = [] - self.link_labels = [] + # Modern style style = ttk.Style() style.theme_use("clam") - style.configure("Title.TLabel", font=("Segoe UI", 18, "bold")) + style.configure("Title.TLabel", font=("Segoe UI", 28, "bold"), foreground="#58a6ff") + style.configure("Subtitle.TLabel", font=("Segoe UI", 12), foreground="#8b949e") + style.configure("TButton", font=("Segoe UI", 11, "bold"), padding=10) + style.map("TButton", background=[("active", "#1f6feb")]) - create_ui(self) + create_modern_ui(self) self.apply_theme() if DND_AVAILABLE: self.enable_drag_drop() + # Center window + self.root.update_idletasks() + x = (self.root.winfo_screenwidth() // 2) - (self.root.winfo_width() // 2) + y = (self.root.winfo_screenheight() // 2) - (self.root.winfo_height() // 2) + self.root.geometry(f"+{x}+{y}") + def apply_theme(self): theme = THEMES["dark" if self.is_dark_theme else "light"] self.root.configure(bg=theme["bg"]) for widget in self.ui_elements.values(): if hasattr(widget, "configure"): - if "bg" in widget.config(): + cfg = widget.config() + if "bg" in cfg: widget.configure(bg=theme.get("bg", theme["frame_bg"])) - if "fg" in widget.config(): + if "fg" in cfg: widget.configure(fg=theme.get("text_bright", theme["text"])) - self.input_text.configure(bg=theme["input_bg"], fg=theme["input_fg"]) - self.output_text.configure(fg=theme["output_fg"]) - self.stats.configure(foreground=theme["accent_secondary"], background=theme["frame_bg"]) + # رنگ‌های معتبر برای tkinter + self.input_text.configure( + bg=theme["input_bg"], + fg=theme["input_fg"], + insertbackground=theme["text"], # مکان‌نما + selectbackground="#1f6feb" # انتخاب متن (رنگ ثابت و معتبر) + ) + self.output_text.configure( + fg=theme["output_fg"], + bg=theme["input_bg"], + insertbackground=theme["output_fg"], + selectbackground="#1f6feb" + ) + self.stats.configure( + foreground=theme["accent"], + background=theme["frame_bg"], + font=("Consolas", 13, "bold") + ) self.theme_button.config( - text="☀" if self.is_dark_theme else "🌙", - bg=theme["bg"], fg=theme["accent"] + text="Light Mode" if self.is_dark_theme else "Dark Mode", + bg=theme["frame_bg"], fg=theme["accent"], font=("Segoe UI", 12, "bold"), + relief="flat", bd=0, highlightthickness=0 ) - for cb in self.checkbuttons: + for cb in getattr(self, "checkbuttons", []): cb.configure(bg=theme["frame_bg"], fg=theme["text"], selectcolor=theme["select_bg"]) - for link in self.link_labels: + for link in getattr(self, "link_labels", []): link.configure(fg=theme["accent"], bg=theme["bg"]) + def toggle_theme(self): self.is_dark_theme = not self.is_dark_theme self.apply_theme() def enable_drag_drop(self): - """Enable drag-and-drop – works 100% on Windows with tkinterdnd2""" if not DND_AVAILABLE: return try: - # Critical: bind to the internal Text widget of ScrolledText text_widget = self.input_text._text - text_widget.drop_target_register(DND_FILES) text_widget.dnd_bind('<>', self.on_drop) - - # Also bind to the ScrolledText itself (just in case) self.input_text.drop_target_register(DND_FILES) self.input_text.dnd_bind('<>', self.on_drop) - except Exception as e: - print(f"[DND] Disabled: {e}") + print(f"[DND] Failed: {e}") def on_drop(self, event): - """Handle dropped files – fully Windows-compatible""" data = event.data.strip() - - # Windows wraps paths in {} when multiple files are dropped if data.startswith('{') and data.endswith('}'): data = data[1:-1] - import shlex try: files = shlex.split(data) except: files = [f.strip('{}') for f in data.split()] - for file_path in files: - file_path = file_path.strip('"\'') - path = Path(file_path) + for fp in files: + fp = fp.strip('"\'') + path = Path(fp) if path.exists(): - if path.suffix.lower() in { - ".py", ".txt", ".md", ".json", ".yml", ".yaml", - ".log", ".csv", ".js", ".ts", ".html", ".css", - ".jsx", ".tsx", ".sql" - }: + if path.suffix.lower() in {".py", ".js", ".ts", ".html", ".css", ".json", ".md", ".txt", ".log", ".yaml", ".yml", ".sql"}: try: + content = path.read_text(encoding="utf-8", errors="ignore") self.input_text.delete(1.0, tk.END) - self.input_text.insert(tk.END, path.read_text(encoding="utf-8")) - self.stats.config(text=f"Loaded: {path.name}") + self.input_text.insert(tk.END, content) + self.stats.config(text=f"Dropped • {path.name} • {len(content):,} chars") return except Exception as e: - messagebox.showerror("Error", f"Could not open file:\n{e}") + messagebox.showerror("Error", f"Cannot read file:\n{e}") return - messagebox.showwarning("Invalid file", "Please drop a supported text file.") - def load_file(self): path = filedialog.askopenfilename( - filetypes=[("Text Files", "*.py *.txt *.md *.json *.yml *.yaml *.js *.ts *.html *.css"), ("All Files", "*.*")] + title="Open source file", + filetypes=[("All Supported", "*.py *.js *.ts *.jsx *.tsx *.html *.css *.json *.md *.yaml *.yml *.txt *.log *.sql"), ("All Files", "*.*")] ) if path: self.load_text_from_file(path) def load_text_from_file(self, path): try: + content = Path(path).read_text(encoding="utf-8", errors="ignore") self.input_text.delete(1.0, tk.END) - self.input_text.insert(tk.END, Path(path).read_text(encoding="utf-8")) + self.input_text.insert(tk.END, content) + self.stats.config(text=f"Loaded • {Path(path).name} • {len(content):,} chars") except Exception as e: - messagebox.showerror("Error", f"Failed to load file:\n{e}") + messagebox.showerror("Error", f"Failed to load:\n{e}") def copy_output(self): text = self.output_text.get(1.0, tk.END).strip() if text: self.root.clipboard_clear() self.root.clipboard_append(text) - messagebox.showinfo("Copied!", "Crushed text copied to clipboard!") + messagebox.showinfo("Copied", "Crushed output copied!") def save_output(self): - path = filedialog.asksaveasfilename(defaultextension=".txt", filetypes=[("Text File", "*.txt"), ("All Files", "*.*")]) + path = filedialog.asksaveasfilename(defaultextension=".py", title="Save crushed code") if path: try: Path(path).write_text(self.output_text.get(1.0, tk.END), encoding="utf-8") - messagebox.showinfo("Saved", "Output saved successfully!") + messagebox.showinfo("Saved", f"Saved to {Path(path).name}") except Exception as e: - messagebox.showerror("Error", f"Failed to save:\n{e}") + messagebox.showerror("Error", f"Save failed:\n{e}") def optimize(self): text = self.input_text.get(1.0, tk.END).strip() if not text: - messagebox.showwarning("Empty Input", "Please paste or load some text first.") + messagebox.showwarning("No Input", "Drop a file or paste code first.") return options = {k: v.get() for k, v in self.options.items()} @@ -163,8 +179,13 @@ def optimize(self): self.output_text.delete(1.0, tk.END) self.output_text.insert(tk.END, result.optimized_text) + saved = result.total_saved_percent + before = len(text) + after = len(result.optimized_text) + time_ms = result.total_time_ms + self.stats.config( - text=f"Before: {len(text):,} → After: {len(result.optimized_text):,} chars | Saved: {result.total_saved_percent:.1f}%" + text=f"Success • Saved {saved:.1f}% • {before:,} → {after:,} chars • {time_ms:.1f}ms" ) diff --git a/src/ai_token_crusher/interfaces/gui/ui.py b/src/ai_token_crusher/interfaces/gui/ui.py index b47dd91..3650f46 100644 --- a/src/ai_token_crusher/interfaces/gui/ui.py +++ b/src/ai_token_crusher/interfaces/gui/ui.py @@ -4,104 +4,109 @@ import webbrowser from .theme import THEMES, LINKS -def create_ui(app): - theme = THEMES["dark" if app.is_dark_theme else "light"] - # Main container - main = tk.Frame(app.root, bg=theme["bg"]) - main.pack(fill="both", expand=True, padx=20, pady=20) - app.ui_elements["main"] = main +def create_modern_ui(app): + theme = THEMES["dark" if app.is_dark_theme else "light"] - # Header with theme toggle - header = tk.Frame(main, bg=theme["bg"]) - header.pack(fill="x", pady=(0, 5)) - app.ui_elements["header"] = header + # Header + header = tk.Frame(app.root, bg=theme["bg"], height=90) + header.pack(fill="x", padx=30, pady=(20, 0)) + header.pack_propagate(False) - # Theme toggle button - theme_icon = "☀️" if app.is_dark_theme else "🌙" + tk.Label(header, text="AI Token Crusher", font=("Segoe UI", 32, "bold"), fg=theme["accent"], bg=theme["bg"]).pack(side="left", pady=10) + app.theme_button = tk.Button( - header, text=theme_icon, command=app.toggle_theme, - bg=theme["bg"], fg=theme["accent"], font=("Segoe UI", 16), - relief="flat", cursor="hand2", bd=0 + header, text="Light Mode", command=app.toggle_theme, + bg=theme["frame_bg"], fg=theme["accent"], font=("Segoe UI", 12, "bold"), + relief="flat", bd=0, padx=20, pady=10, cursor="hand2" ) - app.theme_button.pack(side="right") + app.theme_button.pack(side="right", pady=10) - # Title - ttk.Label(header, text="AI Token Crusher", style="Title.TLabel").place(relx=0.5, rely=0.5, anchor="center") + tk.Label(header, text="Crush up to 75% of tokens instantly • Grok • GPT • Claude • Llama", + font=("Segoe UI", 11), fg=theme["text_secondary"], bg=theme["bg"]).pack(side="left", padx=20) - # Subtitle - app.ui_elements["subtitle"] = tk.Label( - main, text="Cut up to 75% of tokens for Grok • GPT • Claude • Llama", - fg=theme["text_secondary"], bg=theme["bg"], font=("Segoe UI", 11) - ) - app.ui_elements["subtitle"].pack(pady=(0, 20)) + # Main Content Area + content_frame = tk.Frame(app.root, bg=theme["bg"]) + content_frame.pack(fill="both", expand=True, padx=30, pady=20) - # Input + Options layout - top = tk.Frame(main, bg=theme["bg"]) - top.pack(fill="both", expand=True) - app.ui_elements["top"] = top + # Input + Options Side by Side (با grid — بدون PanedWindow!) + input_options_frame = tk.Frame(content_frame, bg=theme["bg"]) + input_options_frame.pack(fill="both", expand=True) - # Input panel - input_frame = tk.LabelFrame(top, text=" Input Text / Code ", fg=theme["text_bright"], bg=theme["frame_bg"], font=("Segoe UI", 10, "bold")) - input_frame.pack(side="left", fill="both", expand=True, padx=(0, 10)) - app.ui_elements["input_frame"] = input_frame + # Input Panel (چپ) + input_frame = tk.LabelFrame(input_options_frame, text=" Input • Drop file or paste code ", + font=("Segoe UI", 11, "bold"), fg=theme["text_bright"], bg=theme["frame_bg"], bd=2, relief="groove") + input_frame.grid(row=0, column=0, sticky="nsew", padx=(0, 10)) - app.input_text = scrolledtext.ScrolledText(input_frame, font=("Consolas", 10), bg=theme["input_bg"], fg=theme["input_fg"]) - app.input_text.pack(fill="both", expand=True, padx=10, pady=10) + app.input_text = scrolledtext.ScrolledText(input_frame, font=("Consolas", 11), bg=theme["input_bg"], fg=theme["input_fg"], undo=True) + app.input_text.pack(fill="both", expand=True, padx=12, pady=12) - btns = tk.Frame(input_frame, bg=theme["frame_bg"]) - btns.pack(pady=5) - ttk.Button(btns, text="Load File", command=app.load_file).pack(side="left", padx=5) - ttk.Button(btns, text="Copy Output", command=app.copy_output).pack(side="left", padx=5) + btn_frame = tk.Frame(input_frame, bg=theme["frame_bg"]) + btn_frame.pack(pady=8) + ttk.Button(btn_frame, text="Load File", command=app.load_file).pack(side="left", padx=8) + ttk.Button(btn_frame, text="Clear Input", command=lambda: app.input_text.delete(1.0, tk.END)).pack(side="left", padx=8) - # Options panel - options_frame = tk.LabelFrame(top, text=" Optimization Techniques ", fg=theme["text_bright"], bg=theme["frame_bg"], font=("Segoe UI", 10, "bold")) - options_frame.pack(side="right", fill="y", padx=(10, 0)) + # Options Panel (راست) + options_frame = tk.LabelFrame(input_options_frame, text=" Optimization Techniques ", + font=("Segoe UI", 11, "bold"), fg=theme["text_bright"], bg=theme["frame_bg"], bd=2, relief="groove") + options_frame.grid(row=0, column=1, sticky="nsew", padx=(10, 0)) canvas = tk.Canvas(options_frame, bg=theme["frame_bg"], highlightthickness=0) - scrollbar = ttk.Scrollbar(options_frame, command=canvas.yview) - scroll_frame = tk.Frame(canvas, bg=theme["frame_bg"]) - canvas.create_window((0, 0), window=scroll_frame, anchor="nw") + scrollbar = ttk.Scrollbar(options_frame, orient="vertical", command=canvas.yview) + scrollable = tk.Frame(canvas, bg=theme["frame_bg"]) + scrollable.bind("", lambda e: canvas.configure(scrollregion=canvas.bbox("all"))) + canvas.create_window((0, 0), window=scrollable, anchor="nw") canvas.configure(yscrollcommand=scrollbar.set) - canvas.pack(side="left", fill="both", expand=True, padx=10, pady=10) + canvas.pack(side="left", fill="both", expand=True, padx=12, pady=12) scrollbar.pack(side="right", fill="y") app.checkbuttons = [] for key, var in app.options.items(): name = key.replace("_", " ").title().replace("Shorten", "Short").replace("Remove", "Strip") - cb = tk.Checkbutton(scroll_frame, text=name, variable=var, bg=theme["frame_bg"], fg=theme["text"], selectcolor=theme["select_bg"]) - cb.pack(anchor="w", pady=2, padx=15) + cb = tk.Checkbutton(scrollable, text=name, variable=var, bg=theme["frame_bg"], fg=theme["text"], + selectcolor=theme["select_bg"], font=("Segoe UI", 10)) + cb.pack(anchor="w", pady=4, padx=20) app.checkbuttons.append(cb) - # Crush button - crush_btn = ttk.Button(main, text="CRUSH TOKENS →", command=app.optimize) - crush_btn.pack(pady=20) - app.ui_elements["crush_btn"] = crush_btn + # تنظیم وزن برای تقسیم مساوی + input_options_frame.grid_columnconfigure(0, weight=1) + input_options_frame.grid_columnconfigure(1, weight=1) + input_options_frame.grid_rowconfigure(0, weight=1) + + # Bottom Section: Crush Button + Output + bottom = tk.Frame(content_frame, bg=theme["bg"]) + bottom.pack(fill="both", expand=True, pady=(20, 0)) + + ttk.Button(bottom, text="CRUSH TOKENS", command=app.optimize).pack(pady=15) + + output_frame = tk.LabelFrame(bottom, text=" Crushed Output • AI-Safe & Readable ", + font=("Segoe UI", 11, "bold"), fg=theme["text_bright"], bg=theme["frame_bg"], bd=2, relief="groove") + output_frame.pack(fill="both", expand=True) + + app.output_text = scrolledtext.ScrolledText(output_frame, font=("Consolas", 11), bg=theme["input_bg"], fg=theme["output_fg"]) + app.output_text.pack(fill="both", expand=True, padx=12, pady=12) - # Output panel - output_frame = tk.LabelFrame(main, text=" Crushed Output (AI-Safe) ", - fg=theme["text_bright"], bg=theme["frame_bg"], font=("Segoe UI", 10, "bold")) - output_frame.pack(fill="both", expand=True, pady=(10, 0)) - app.output_text = scrolledtext.ScrolledText(output_frame, font=("Consolas", 10), bg=theme["input_bg"], fg=theme["output_fg"]) - app.output_text.pack(fill="both", expand=True, padx=10, pady=10) - ttk.Button(output_frame, text="Save Output", command=app.save_output).pack(pady=5) + btn_out = tk.Frame(output_frame, bg=theme["frame_bg"]) + btn_out.pack(pady=8) + ttk.Button(btn_out, text="Copy Output", command=app.copy_output).pack(side="left", padx=8) + ttk.Button(btn_out, text="Save As...", command=app.save_output).pack(side="left", padx=8) # Stats - app.stats = ttk.Label(main, text="Ready to crush tokens...", foreground=theme["accent_secondary"], - font=("Consolas", 11, "bold"), background=theme["frame_bg"]) + app.stats = tk.Label(bottom, text="Ready to crush tokens...", font=("Consolas", 13, "bold"), + fg=theme["accent"], bg=theme["bg"]) app.stats.pack(pady=10) - # Footer links - footer = tk.Frame(main, bg=theme["bg"]) + # Footer + footer = tk.Frame(app.root, bg=theme["bg"]) footer.pack(pady=15) app.link_labels = [] for text, url in LINKS: - link = tk.Label(footer, text=text, fg=theme["accent"], bg=theme["bg"], cursor="hand2", font=("Segoe UI", 9, "underline")) - link.pack(side="left", padx=20) + link = tk.Label(footer, text=text, fg=theme["accent"], bg=theme["bg"], cursor="hand2", + font=("Segoe UI", 10, "underline")) + link.pack(side="left", padx=25) link.bind("", lambda e, u=url: webbrowser.open(u)) app.link_labels.append(link) app.ui_elements.update({ - "canvas": canvas, "scroll_frame": scroll_frame, "footer": footer, - "options_frame": options_frame, "output_frame": output_frame + "header": header, "content_frame": content_frame, "footer": footer }) \ No newline at end of file