Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,19 @@

- GUI for selecting a source folder and choosing which files to include
- Treeview interface with checkboxes for including/excluding individual files
- Lazy loading of large folders keeps the interface responsive
- Live preview window to see the generated output before exporting
- Default filters for file extensions and folders (e.g., skip `.env`, `node_modules`, `.git`, etc.)
- Saves and loads user settings to/from a JSON file
- Choose export format (TXT, Markdown or JSON) and optionally include code blocks and headings
- Export also to HTML or PDF with the same styling as the browser preview
- Optionally export only the file tree without contents
- Switch between dark and light mode, with buttons and fields adopting dark colors when the theme is set to "dark"
- Files listed in a `.gitignore` file are automatically deselected
- Passwords and API keys in the output are masked for safety
- Exports larger than the chosen token limit are automatically split into multiple files
- Select a token limit preset (ChatGPT, Gemini, Claude) or set a custom value
- Live preview is skipped for files bigger than the configured size
- Token counting usa i tokenizer ufficiali (tiktoken, anthropic, Google) quando disponibili
- Scegli la lingua dell'interfaccia tramite un menu a discesa: le opzioni vengono rilevate automaticamente dai file JSON in `promptpack/locales`
- Le stringhe tradotte sono raccolte in file JSON dentro `promptpack/locales` per facilitare l'aggiunta di nuove lingue
Expand Down Expand Up @@ -59,7 +62,7 @@ The file `promptpack.py` simply launches the application.
2. **Select Files**: Opens an expandable tree of all folders and files. You can include/exclude each item via checkboxes.
- Default selections are based on the current settings.
3. **Settings**: Define default allowed extensions, excluded folders and files. Also choose:
- Export format (txt, md, json)
- Export format (txt, md, html, pdf, json)
- Include file headings
- Use code blocks for each file (Markdown only)
- Export only the file tree
Expand All @@ -81,6 +84,7 @@ User preferences are saved in a file named `promptpack_settings.json` in the sam
"tree_only": false,
"include_heading": true,
"use_code_block": true,
"preview_size_limit": 1000000,
"max_tokens": 200000,
"last_start_folder": "",
"last_selected_files": []
Expand Down Expand Up @@ -138,7 +142,7 @@ python -m promptpack.cli ./input ./out --no-heading --no-code-block
| ----------------- | ------------------------------------------------------------------- |
| `source` | Source folder to analyze |
| `dest` | Destination folder for the generated output |
| `--format` | Export format: `txt`, `md`, `json` |
| `--format` | Export format: `txt`, `md`, `html`, `pdf`, `json` |
| `--tree-only` | Export only the folder structure, without file contents |
| `--no-heading` | Do not include headings for each file |
| `--no-code-block` | Do not wrap contents in code blocks (Markdown only) |
Expand Down
2 changes: 1 addition & 1 deletion promptpack/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def main():
parser = argparse.ArgumentParser(description="PromptPack CLI")
parser.add_argument("source", help="cartella sorgente")
parser.add_argument("dest", help="cartella di destinazione")
parser.add_argument("--format", choices=["txt", "md", "json"], dest="format")
parser.add_argument("--format", choices=["txt", "md", "json", "html", "pdf"], dest="format")
parser.add_argument("--tree-only", action="store_true")
parser.add_argument("--no-heading", action="store_true")
parser.add_argument("--no-code-block", action="store_true")
Expand Down
116 changes: 71 additions & 45 deletions promptpack/gui/file_selector.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import tkinter as tk
from tkinter import Toplevel, ttk, messagebox
import threading
from pathlib import Path
from ..settings import save_settings
from ..utils import apply_icon
Expand Down Expand Up @@ -109,63 +110,76 @@ def select_files(app):
vsb.grid(row=0, column=1, sticky="ns")
hsb.grid(row=1, column=0, sticky="ew")

tree.bind("<Enter>", lambda e: tree.focus_set())
tree.bind("<MouseWheel>", lambda e: tree.yview_scroll(int(-1 * (e.delta / 120)), "units"))
tree.bind("<Button-4>", lambda e: tree.yview_scroll(-1, "units"))
tree.bind("<Button-5>", lambda e: tree.yview_scroll(1, "units"))

token_label = ttk.Label(selector, text="")
token_label.pack(pady=2)

checkbox_vars: dict[str, tk.BooleanVar] = {}
checkbox_items = {}
all_state = tk.BooleanVar(value=False)

def dir_has_visible(path: Path) -> bool:
term = search_var.get().lower()
ext = ext_var.get()
if term and term in path.name.lower():
return True
for p in path.iterdir():
if p.is_dir():
if dir_has_visible(p):
return True
else:
if ext != "All" and p.suffix.lower() != ext.lower():
continue
if term and term not in p.name.lower():
continue
return True
return False
def insert_node(parent, path: Path):
if path.is_dir():
node = tree.insert(parent, "end", text=path.name, values=(str(path), "dir"))
tree.insert(node, "end", values=("dummy", "dummy"))
else:
if ext_var.get() != "All" and path.suffix.lower() != ext_var.get().lower():
return
term = search_var.get().lower()
if term and term not in path.name.lower():
return
var = checkbox_vars.get(str(path))
if var is None:
default_checked = (
is_valid(app, path)
and not any(skip in path.parts for skip in app.settings["excluded_dirs"])
and not (
app.gitignore_spec
and app.gitignore_spec.match_file(str(path.relative_to(folder)))
)
)
var = tk.BooleanVar(value=(path in app.selected_files or default_checked))
checkbox_vars[str(path)] = var
item = tree.insert(
parent,
"end",
text=f"[{'x' if var.get() else ' '}] {path.name}",
values=(str(path), "file"),
)
checkbox_items[str(path)] = item


def refresh_tree(*_args):
tree.delete(*tree.get_children())

def insert_items(parent, path: Path):
if path.is_dir():
if not dir_has_visible(path):
return
node = tree.insert(parent, 'end', text=path.name, values=(str(path), 'dir'), open=False)
def worker():
for child in sorted(Path(folder).iterdir()):
selector.after(0, lambda c=child: insert_node("", c))
selector.after(0, update_token_label)

threading.Thread(target=worker, daemon=True).start()

def populate_node(event):
node = tree.focus()
values = tree.item(node, "values")
if len(values) < 2 or values == ("dummy", "dummy"):
return
path = Path(values[0])
children = tree.get_children(node)
if children and tree.item(children[0], "values") == ("dummy", "dummy"):
tree.delete(children[0])

def worker():
for child in sorted(path.iterdir()):
insert_items(node, child)
else:
if ext_var.get() != 'All' and path.suffix.lower() != ext_var.get().lower():
return
term = search_var.get().lower()
if term and term not in path.name.lower():
return
var = checkbox_vars.get(str(path))
if var is None:
default_checked = (
is_valid(app, path)
and not any(skip in path.parts for skip in app.settings["excluded_dirs"])
and not (
app.gitignore_spec
and app.gitignore_spec.match_file(str(path.relative_to(folder)))
)
)
var = tk.BooleanVar(value=(path in app.selected_files or default_checked))
checkbox_vars[str(path)] = var
item = tree.insert(parent, 'end', text=f"[{'x' if var.get() else ' '}] {path.name}", values=(str(path), 'file'))
checkbox_items[str(path)] = item
selector.after(0, lambda c=child: insert_node(node, c))

insert_items('', Path(folder))
update_token_label()
threading.Thread(target=worker, daemon=True).start()

tree.bind("<<TreeviewOpen>>", populate_node)


def update_token_label():
Expand All @@ -180,7 +194,19 @@ def update_token_label():
def update_preview_live():
if app.enable_preview.get():
files = {Path(p) for p, var in checkbox_vars.items() if var.get()}
app.build_preview_async(files)
limit = app.settings.get("preview_size_limit", 1_000_000)
for f in files:
try:
if f.stat().st_size > limit:
app.enable_preview.set(False)
if app.preview_window and app.preview_window.winfo_exists():
app.preview_window.destroy()
messagebox.showinfo(app.t("preview"), app.t("preview_disabled_large", path=f.name, size=limit))
break
except Exception:
continue
else:
app.build_preview_async(files)
update_token_label()

def toggle_checkbox(event):
Expand Down
1 change: 1 addition & 0 deletions promptpack/gui/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ def worker():
self.use_code_block.get(),
self.settings.get("max_tokens", 200000),
progress_callback=callback,
theme=self.theme.get(),
)
msg = "\n".join(str(p) for p in output_paths)
self.root.after(0, lambda: [self.hide_progress(), messagebox.showinfo(self.t("done"), self.t("files_generated", msg=msg))])
Expand Down
18 changes: 14 additions & 4 deletions promptpack/gui/settings_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,14 @@ def prompt_list(title, key):

ttk.Label(scrollable, text=app.t("output_opts"), style="Heading.TLabel").pack(padx=10, pady=(20, 5))
ttk.Label(scrollable, text=app.t("export_format")).pack(pady=(5, 0))
ttk.Radiobutton(scrollable, text="TXT", variable=app.export_format, value="txt").pack(pady=2)
ttk.Radiobutton(scrollable, text="Markdown", variable=app.export_format, value="md").pack(pady=2)
ttk.Radiobutton(scrollable, text="JSON", variable=app.export_format, value="json").pack(pady=2)
format_combo = ttk.Combobox(
scrollable,
textvariable=app.export_format,
values=["txt", "md", "html", "pdf", "json"],
state="readonly",
)
format_combo.set(app.export_format.get())
format_combo.pack(pady=5)
ttk.Checkbutton(scrollable, text=app.t("include_headings"), variable=app.include_heading).pack(pady=5)
ttk.Checkbutton(scrollable, text=app.t("use_code"), variable=app.use_code_block).pack(pady=5)
ttk.Checkbutton(scrollable, text=app.t("tree_only"), variable=app.tree_only).pack(pady=5)
Expand Down Expand Up @@ -142,7 +147,12 @@ def toggle_entry(*_):

ttk.Label(scrollable, text=app.t("language"), style="Heading.TLabel").pack(padx=10, pady=(20, 5))
language_options = available_languages()
ttk.OptionMenu(scrollable, app.language, app.language.get(), *language_options, command=lambda *_: None).pack(pady=5)

def on_language_change(*_):
app.load_translations()
app.update_texts()

ttk.OptionMenu(scrollable, app.language, app.language.get(), *language_options, command=on_language_change).pack(pady=5)

def save_and_close():
new_settings = {
Expand Down
21 changes: 15 additions & 6 deletions promptpack/i18n.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,23 @@
def load_translations(lang: str) -> dict[str, str]:
"""Carica le stringhe localizzate dal file corrispondente."""
if lang not in _cache:
path = LOCALES_DIR / f"{lang}.json"
if not path.exists():
path = LOCALES_DIR / "eng.json"
# Always start from English strings as fallback
data: dict[str, str] = {}
try:
with path.open("r", encoding="utf-8") as f:
_cache[lang] = json.load(f)
with (LOCALES_DIR / "eng.json").open("r", encoding="utf-8") as f:
data = json.load(f)
except Exception:
_cache[lang] = {}
data = {}

if lang != "eng":
path = LOCALES_DIR / f"{lang}.json"
if path.exists():
try:
with path.open("r", encoding="utf-8") as f:
data.update(json.load(f))
except Exception:
pass
_cache[lang] = data
return _cache[lang]


Expand Down
3 changes: 2 additions & 1 deletion promptpack/locales/eng.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,6 @@
"need_folders": "Please select both source and destination folders",
"token_header": "Token estimate: {count} (remaining {remaining})\n{sep}\n",
"tokens": "Tokens: {tokens} / {max}",
"project_label": "Project: {name} - {date}"
"project_label": "Project: {name} - {date}",
"preview_disabled_large": "Preview disabled for large file {path} (> {size} bytes)"
}
3 changes: 2 additions & 1 deletion promptpack/locales/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,6 @@
"need_folders": "Seleziona sia la cartella sorgente che quella di destinazione",
"token_header": "Stima token: {count} (restano {remaining})\n{sep}\n",
"tokens": "Token: {tokens} / {max}",
"project_label": "Progetto: {name} - {date}"
"project_label": "Progetto: {name} - {date}",
"preview_disabled_large": "Anteprima disabilitata per il file {path} (> {size} byte)"
}
4 changes: 4 additions & 0 deletions promptpack/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
"token_model": "gpt",
# Maximum tokens per preview or export file
"max_tokens": 200_000,
# Disable live preview for files bigger than this size (bytes)
"preview_size_limit": 1_000_000,
# Remember last source folder and selected files
"last_start_folder": "",
"last_selected_files": [],
Expand All @@ -42,6 +44,8 @@ def load_settings():
data["language"] = DEFAULT_SETTINGS["language"]
if "token_model" not in data:
data["token_model"] = DEFAULT_SETTINGS["token_model"]
if "preview_size_limit" not in data:
data["preview_size_limit"] = DEFAULT_SETTINGS["preview_size_limit"]
return {**DEFAULT_SETTINGS, **data}
except Exception:
return DEFAULT_SETTINGS.copy()
Expand Down
Loading