Skip to content

Commit 20fa148

Browse files
committed
Implement CI workflow, add feature request template, and update project files
1 parent c4b7e53 commit 20fa148

File tree

6 files changed

+233
-56
lines changed

6 files changed

+233
-56
lines changed

.github/copilot-instructions.md

Lines changed: 40 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -2,68 +2,52 @@
22
Guidance for AI coding agents working on TokenOptimizer
33
Keep this short, concrete, and code-focused. Update when `main.py` changes.
44
-->
5-
6-
# Copilot / Agent Instructions — TokenOptimizer
7-
5+
# Copilot / Agent Instructions — TokenOptimizer (concise)
86
Summary
9-
- This repository is a small single-file Python desktop app: `main.py` implements a Tkinter GUI and the token-optimization logic in the `AITokenCrusher` class.
10-
- Primary goal: conservative, offline token minimization for text/code using deterministic string and regex transforms.
11-
12-
Where to look
13-
- Core app and logic: `main.py` (UI, options, `apply_optimizations`).
14-
- Project description / releases: `README.md`.
15-
- Static assets: `assets/` (images, screenshots).
16-
- Contribution workflow: `feature_request.md` describes feature requests.
17-
18-
Big-picture architecture
19-
- Single-process GUI: `AITokenCrusher` constructs the UI and stores state in `self.options` (a dict of `tk.BooleanVar`) and `self.ui_elements`.
20-
- The optimization pipeline is implemented in `apply_optimizations(self, text)` and applied from `optimize()`.
21-
- UI <-> logic boundary: do not embed heavy business logic in widget callbacks; extend or refactor `apply_optimizations` when adding new transforms.
22-
23-
Project-specific patterns & conventions
24-
- Option keys are snake_case strings in `self.options` (e.g. `remove_comments`, `single_line_mode`). Follow this pattern when adding flags.
25-
- Checkbuttons are created by iterating `self.options`; adding a new option requires:
26-
1) add a `tk.BooleanVar` entry to `self.options` in `__init__`,
27-
2) reference it in `apply_optimizations`, and
28-
3) let the existing loop auto-create the checkbox.
29-
- UI state containers: use `self.ui_elements` to store references (avoid creating loose globals).
30-
- Theme handling: themes live in `self.themes` and switching occurs via `toggle_theme()` + `apply_theme()` — prefer updating these maps rather than hardcoding colors.
31-
32-
Optimization details agents must preserve
33-
- `apply_optimizations` mixes regex and string replacements. Important examples to preserve exactly:
34-
- Newline single-line mode uses the literal character `` (replaces `\n`).
35-
- Boolean/text replacements: `True``1`, `False``0`, `None``~`.
36-
- Shortened keywords: `def ``d `, `return ``r `, `import ``i `, `lambda ``λ `.
37-
- Operator replacements: `==```, `!=```, ` and ```, ` or ```.
38-
- Short print: `print(``p(` (note the removed whitespace pattern).
39-
- These substitutions are intentionally non-standard/unicode; preserve their literal characters and avoid accidentally normalizing or escaping them.
40-
41-
Run / build / debug
42-
- Run locally: `python main.py` (uses system Python with Tkinter). On Windows PowerShell:
7+
- Small, single-process Tkinter desktop app. Primary logic lives in `main.py` (`AITokenCrusher`) and an identical copy in `src/app.py`.
8+
- Purpose: deterministic, offline token minimization for text/code via string + regex transforms (keep substitutions literal).
9+
Where to look (high-value files)
10+
- `main.py` — canonical UI + optimization logic. Edit here for behavior changes.
11+
- `src/app.py` — duplicate of `main.py` (check both when refactoring).
12+
- `README.md` — user-facing description and release link.
13+
- `assets/` — screenshots and static images used in README.
14+
Big-picture architecture (quick)
15+
- UI and logic are co-located in `AITokenCrusher`:
16+
- `self.options` (dict of `tk.BooleanVar`) drives which transforms run.
17+
- `apply_optimizations(self, text)` contains the pipeline of regex/string transforms.
18+
- `optimize()` reads input, calls `apply_optimizations`, updates output and stats.
19+
- The UI auto-creates checkboxes by iterating `self.options` — adding an option is three small steps (see below).
20+
Project-specific conventions (do these exactly)
21+
- Option lifecycle: add a `tk.BooleanVar` key in `self.options` (snake_case), implement the transform in `apply_optimizations`, and the checkbox appears automatically.
22+
- Keep UI references in `self.ui_elements` (avoid top-level globals).
23+
- Theme configuration lives in `self.themes` and is applied with `toggle_theme()` / `apply_theme()`; prefer updating theme maps over hardcoding colors.
24+
Critical transforms and literals to preserve
25+
- `single_line_mode` replaces newlines with the literal character `` (not a word). Do not normalize or escape it.
26+
- Exact mappings (examples taken from `apply_optimizations`):
27+
- Keywords: `def ``d `, `return ``r `, `import ``i `, `lambda ``λ `
28+
- Booleans: `True``1`, `False``0`, `None``~`
29+
- Operators: `==```, `!=```, ` and ```, ` or ```
30+
- Print: `print(``p(`
31+
- Regex-based removals (comments, docstrings, type hints) are implemented with `re.sub` — preserve the intent and the approximate patterns when editing.
32+
Developer workflows / commands
33+
- Run app locally (PowerShell):
4334
```pwsh
4435
python .\main.py
4536
```
46-
- Packaging (not present in repo): releases show a Windows `.exe`. If creating builds, use a one-file GUI build (example):
37+
- Build a one-file Windows executable (only if requested):
4738
```pwsh
4839
pyinstaller --onefile --windowed main.py
4940
```
50-
Only add packaging config if requested; keep runtime behaviour deterministic.
51-
5241
Testing and verification
53-
- There are no automated tests. Changes that affect behavior require manual testing: launch the GUI, paste sample code, toggle options, and verify `apply_optimizations` output.
54-
- When adding new transforms, include small unit-testable helper functions and a minimal manual verification harness that calls `apply_optimizations(text)`.
55-
56-
Safe editing guidance for agents
57-
- Be conservative: prefer adding small helper functions over large refactors.
58-
- Keep public names and option keys stable (changing keys requires UI and saved-state updates).
59-
- When introducing new dependencies, document why and prefer pure-Python stdlib solutions (this repo intentionally targets offline usage).
60-
42+
- There are no automated tests. When changing transforms: add a small, importable helper function and unit tests under `tests/`.
43+
- Manual verification: run the GUI, paste input, toggle the relevant option(s), press *CRUSH TOKENS →*, and confirm output and the saved `%` metric.
44+
Small-change checklist for agents (use before committing)
45+
1. Add or rename an option: update `self.options` (tk.BooleanVar), add logic in `apply_optimizations`, keep key stable.
46+
2. Avoid changing existing substitution characters (⏎, ≡, ≠, ∧, ∨, λ, ~) unless maintainer OK.
47+
3. For UI changes, reuse `self.ui_elements` and the checkbox auto-creation loop.
48+
4. When refactoring, update both `main.py` and `src/app.py` (one is a copy).
6149
Files to reference in PRs
62-
- `main.py` — implementation and starting point for most changes.
63-
- `README.md` — user-facing description, check for mismatch with behaviour.
64-
- `assets/` — update assets if UI text or screenshots change.
65-
66-
If something is unclear
67-
- Ask the maintainer which transforms are allowed to change semantics; otherwise assume existing substitutions must remain literal and reversible for humans.
68-
69-
— End —
50+
- `main.py`, `src/app.py`, `README.md`, and `assets/`.
51+
If unclear
52+
- Ask the maintainer whether a transform may change semantics. Default: preserve existing mappings and prefer small, testable helpers.
53+
— End

.github/workflows/ci.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: CI - AI Token Crusher Tests
2+
3+
on:
4+
push:
5+
branches: [ "main" ]
6+
pull_request:
7+
branches: [ "main" ]
8+
schedule:
9+
- cron: '0 0 * * *' # Nightly at midnight UTC
10+
11+
jobs:
12+
test:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
- name: Set up Python 3.11
17+
uses: actions/setup-python@v5
18+
with:
19+
python-version: '3.11'
20+
- name: Install dependencies
21+
run: pip install -r requirements.txt pytest
22+
- name: Run tests
23+
run: pytest tests/ -v
File renamed without changes.

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[build-system]
2+
requires = ["setuptools>=61.0"]
3+
build-backend = "setuptools.build_meta"

requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
tkinter
2+
pytest
3+
pyinstaller

src/app.py

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
# main.py - AI Token Crusher v1.0
2+
import tkinter as tk
3+
from tkinter import ttk, filedialog, messagebox, scrolledtext
4+
import webbrowser
5+
import re
6+
7+
class AITokenCrusher:
8+
def __init__(self, root):
9+
self.root = root
10+
self.root.title("AI Token Crusher - Cut up to 75% tokens")
11+
self.root.geometry("1280x820")
12+
self.root.minsize(1000, 700)
13+
self.root.configure(bg="#0d1117")
14+
15+
style = ttk.Style()
16+
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))
92+
93+
def load_file(self):
94+
path = filedialog.askopenfilename(filetypes=[("All Files", "*.*")])
95+
if path:
96+
with open(path, "r", encoding="utf-8") as f:
97+
self.input_text.delete(1.0, tk.END)
98+
self.input_text.insert(tk.END, f.read())
99+
100+
def copy_output(self):
101+
output = self.output_text.get(1.0, tk.END).strip()
102+
if output:
103+
self.root.clipboard_clear()
104+
self.root.clipboard_append(output)
105+
messagebox.showinfo("Copied!", "Crushed text copied to clipboard!")
106+
107+
def save_output(self):
108+
path = filedialog.asksaveasfilename(defaultextension=".txt")
109+
if path:
110+
with open(path, "w", encoding="utf-8") as f:
111+
f.write(self.output_text.get(1.0, tk.END))
112+
messagebox.showinfo("Saved", "Output saved successfully!")
113+
114+
def optimize(self):
115+
text = self.input_text.get(1.0, tk.END).strip()
116+
if not text:
117+
messagebox.showwarning("Empty", "Paste or load text first!")
118+
return
119+
120+
optimized = self.apply_optimizations(text)
121+
self.output_text.delete(1.0, tk.END)
122+
self.output_text.insert(tk.END, optimized)
123+
124+
before = len(text)
125+
after = len(optimized)
126+
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"
160+
161+
if __name__ == "__main__":
162+
root = tk.Tk()
163+
app = AITokenCrusher(root)
164+
root.mainloop()

0 commit comments

Comments
 (0)