From ea4e80f0a8e16b977c23fb2902b4bb1b5724c81e Mon Sep 17 00:00:00 2001 From: Tobia Rigon <42972324+TobiaRigon@users.noreply.github.com> Date: Wed, 16 Jul 2025 15:23:03 +0200 Subject: [PATCH 1/2] Implement requested improvements --- README.md | 8 +- promptpack/cli.py | 2 +- promptpack/gui/file_selector.py | 66 ++++++---- promptpack/gui/main_window.py | 1 + promptpack/gui/settings_dialog.py | 9 +- promptpack/i18n.py | 21 +++- promptpack/locales/eng.json | 3 +- promptpack/locales/it.json | 3 +- promptpack/settings.py | 4 + promptpack/utils.py | 196 ++++++++++++++++++++---------- 10 files changed, 213 insertions(+), 100 deletions(-) diff --git a/README.md b/README.md index 7e572dd..8cffda5 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 @@ -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": [] @@ -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) | diff --git a/promptpack/cli.py b/promptpack/cli.py index 64db4da..c540bc8 100644 --- a/promptpack/cli.py +++ b/promptpack/cli.py @@ -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") diff --git a/promptpack/gui/file_selector.py b/promptpack/gui/file_selector.py index 7e9ec08..c289c24 100644 --- a/promptpack/gui/file_selector.py +++ b/promptpack/gui/file_selector.py @@ -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 @@ -116,33 +117,14 @@ def select_files(app): 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 refresh_tree(*_args): tree.delete(*tree.get_children()) - def insert_items(parent, path: Path): + def insert_node(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) - for child in sorted(path.iterdir()): - insert_items(node, child) + 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 @@ -164,8 +146,30 @@ def insert_items(parent, path: Path): item = tree.insert(parent, 'end', text=f"[{'x' if var.get() else ' '}] {path.name}", values=(str(path), 'file')) checkbox_items[str(path)] = item - insert_items('', Path(folder)) - update_token_label() + 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()): + selector.after(0, lambda c=child: insert_node(node, c)) + + threading.Thread(target=worker, daemon=True).start() + + tree.bind("<>", populate_node) def update_token_label(): @@ -180,7 +184,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): diff --git a/promptpack/gui/main_window.py b/promptpack/gui/main_window.py index ec0bd73..3e137c8 100644 --- a/promptpack/gui/main_window.py +++ b/promptpack/gui/main_window.py @@ -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))]) diff --git a/promptpack/gui/settings_dialog.py b/promptpack/gui/settings_dialog.py index 2983b02..eaad699 100644 --- a/promptpack/gui/settings_dialog.py +++ b/promptpack/gui/settings_dialog.py @@ -95,6 +95,8 @@ def prompt_list(title, key): 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="HTML", variable=app.export_format, value="html").pack(pady=2) + ttk.Radiobutton(scrollable, text="PDF", variable=app.export_format, value="pdf").pack(pady=2) ttk.Radiobutton(scrollable, text="JSON", variable=app.export_format, value="json").pack(pady=2) 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) @@ -142,7 +144,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 = { diff --git a/promptpack/i18n.py b/promptpack/i18n.py index 0d58a8b..7f88ad4 100644 --- a/promptpack/i18n.py +++ b/promptpack/i18n.py @@ -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] diff --git a/promptpack/locales/eng.json b/promptpack/locales/eng.json index 85889cd..2c301b1 100644 --- a/promptpack/locales/eng.json +++ b/promptpack/locales/eng.json @@ -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)" } diff --git a/promptpack/locales/it.json b/promptpack/locales/it.json index 52591af..c2e7e79 100644 --- a/promptpack/locales/it.json +++ b/promptpack/locales/it.json @@ -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)" } diff --git a/promptpack/settings.py b/promptpack/settings.py index 6262316..64cd11d 100644 --- a/promptpack/settings.py +++ b/promptpack/settings.py @@ -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": [], @@ -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() diff --git a/promptpack/utils.py b/promptpack/utils.py index d176e8a..056430e 100644 --- a/promptpack/utils.py +++ b/promptpack/utils.py @@ -5,6 +5,12 @@ from datetime import datetime from tempfile import NamedTemporaryFile import webbrowser +import markdown + +try: + import weasyprint # type: ignore +except Exception: # pragma: no cover - optional dependency + weasyprint = None from .tokenizer import estimate_token_count @@ -35,6 +41,56 @@ ".en": "", } +PREVIEW_CSS_DARK = """ + +""" + +PREVIEW_CSS_LIGHT = """ + +""" + def apply_icon(window): try: @@ -66,6 +122,49 @@ def sanitize_sensitive_data(text: str) -> str: return text +def _init_export(start_folder: str, export_format: str): + project_name = Path(start_folder).name + date_str = datetime.now().strftime("%Y%m%d") + if export_format == "json": + header = "" + container: list[str] | dict = {"project": project_name, "date": date_str, "files": []} + else: + header = f"Project: {project_name} - {date_str}\n\n" + container = [header] + tokens = estimate_token_count(header) + return project_name, date_str, container, tokens + + +def _write_part(dest_folder: str, export_format: str, project_name: str, date_str: str, part: int, container, theme: str): + if export_format == "json": + output_file = Path(dest_folder) / f"{project_name}-{date_str}-part{part}.json" + output_file.write_text(json.dumps(container, indent=2), encoding="utf-8") + elif export_format in {"md", "txt", "html", "pdf"}: + suffix = "md" if export_format == "md" else export_format + output_file = Path(dest_folder) / f"{project_name}-{date_str}-part{part}.{suffix}" + text = "".join(container) + if export_format in {"html", "pdf"}: + html = markdown.markdown(text, extensions=["fenced_code", "codehilite"]) + style = PREVIEW_CSS_DARK if theme == "dark" else PREVIEW_CSS_LIGHT + html = f"{style}{html}" + if export_format == "pdf" and weasyprint is not None: + weasyprint.HTML(string=html).write_pdf(str(output_file)) + else: + output_file.write_text(html, encoding="utf-8") + else: + output_file.write_text(text, encoding="utf-8") + else: + raise ValueError(f"Unsupported format: {export_format}") + return output_file + + +def _add_block(container, export_format: str, block): + if export_format == "json": + container["files"].append(block) + else: + container.append(block) + + def generate_output( start_folder: str, dest_folder: str, @@ -76,71 +175,40 @@ def generate_output( use_code_block: bool, max_tokens: int = 200000, progress_callback=None, + theme: str = "light", ): - """Export selected files in the chosen format. + """Export selected files in the chosen format.""" + + project_name, date_str, container, token_count = _init_export(start_folder, export_format if export_format not in {"html", "pdf"} else "md") - The output is split into multiple parts if the number of tokens exceeds - ``max_tokens``. Each part is saved sequentially in the destination folder. - """ - project_name = Path(start_folder).name - date_str = datetime.now().strftime('%Y%m%d') - token_count = 0 part = 1 output_files = [] - if export_format == "json": - data = {"project": project_name, "date": date_str, "files": []} - lines = [] - header = "" - token_count = 0 - else: - lines = [] - header = f"Project: {project_name} - {date_str}\n\n" - lines.append(header) - token_count = estimate_token_count(header) - - def _flush(): - nonlocal part, data, lines - if export_format == "json": - output_file = Path(dest_folder) / f"{project_name}-{date_str}-part{part}.json" - output_file.write_text(json.dumps(data, indent=2), encoding="utf-8") - else: - suffix = "md" if export_format == "md" else "txt" - output_file = Path(dest_folder) / f"{project_name}-{date_str}-part{part}.{suffix}" - output_file.write_text("".join(lines), encoding="utf-8") - part += 1 - return output_file - total_files = len(included_files) processed = 0 + for path in included_files: rel_path = path.relative_to(start_folder) if tree_only: + line = rel_path.as_posix() + "\n" + block_tokens = estimate_token_count(line) + if token_count + block_tokens > max_tokens: + output_files.append(_write_part(dest_folder, export_format, project_name, date_str, part, container, theme)) + part += 1 + container = _init_export(start_folder, export_format if export_format not in {"html", "pdf"} else "md")[2] + token_count = estimate_token_count(container[0] if isinstance(container, list) else "") if export_format == "json": - line_data = {"path": rel_path.as_posix()} - block_tokens = estimate_token_count(rel_path.as_posix()) - if token_count + block_tokens > max_tokens: - output_files.append(_flush()) - token_count = estimate_token_count(header) - data = {"project": project_name, "date": date_str, "files": []} - data["files"].append(line_data) - token_count += block_tokens + _add_block(container, export_format, {"path": rel_path.as_posix()}) else: - line = f"{rel_path.as_posix()}\n" - block_tokens = estimate_token_count(line) - if token_count + block_tokens > max_tokens: - output_files.append(_flush()) - token_count = estimate_token_count(header) - lines = [header] - lines.append(line) - token_count += block_tokens + _add_block(container, export_format, line) + token_count += block_tokens processed += 1 if progress_callback: progress_callback(processed, total_files) continue try: - content = path.read_text(encoding='utf-8', errors='ignore') + content = path.read_text(encoding="utf-8", errors="ignore") except Exception: continue content = sanitize_sensitive_data(content) @@ -148,33 +216,35 @@ def _flush(): if export_format == "json": block_tokens = estimate_token_count(content) if token_count + block_tokens > max_tokens: - output_files.append(_flush()) - token_count = estimate_token_count(header) - data = {"project": project_name, "date": date_str, "files": []} - data["files"].append({"path": rel_path.as_posix(), "content": content}) + output_files.append(_write_part(dest_folder, export_format, project_name, date_str, part, container, theme)) + part += 1 + container = _init_export(start_folder, export_format)[2] + token_count = estimate_token_count("") + _add_block(container, export_format, {"path": rel_path.as_posix(), "content": content}) token_count += block_tokens else: - new_lines = [] + block_lines = [] if include_heading: - new_lines.append(f"## {rel_path.as_posix()}\n") - if export_format == "md" and use_code_block: - lang = LANG_MAP.get(path.suffix, '') - new_lines.append(f"```{lang}\n{content}\n```\n\n") + block_lines.append(f"## {rel_path.as_posix()}\n") + if use_code_block: + lang = LANG_MAP.get(path.suffix, "") + block_lines.append(f"```{lang}\n{content}\n```\n\n") else: - new_lines.append(f"{content}\n\n") - block = ''.join(new_lines) + block_lines.append(f"{content}\n\n") + block = "".join(block_lines) block_tokens = estimate_token_count(block) if token_count + block_tokens > max_tokens: - output_files.append(_flush()) - token_count = estimate_token_count(header) - lines = [header] - lines.append(block) + output_files.append(_write_part(dest_folder, export_format, project_name, date_str, part, container, theme)) + part += 1 + container = _init_export(start_folder, export_format if export_format not in {"html", "pdf"} else "md")[2] + token_count = estimate_token_count(container[0] if isinstance(container, list) else "") + _add_block(container, export_format, block) token_count += block_tokens processed += 1 if progress_callback: progress_callback(processed, total_files) - output_files.append(_flush()) + output_files.append(_write_part(dest_folder, export_format, project_name, date_str, part, container, theme)) return output_files From 58df4c9848de678660946dc38682feab320fcc69 Mon Sep 17 00:00:00 2001 From: Tobia Rigon <42972324+TobiaRigon@users.noreply.github.com> Date: Wed, 16 Jul 2025 15:31:42 +0200 Subject: [PATCH 2/2] Fix lazy file tree and update export format UI --- promptpack/gui/file_selector.py | 62 ++++++++++++++++++------------- promptpack/gui/settings_dialog.py | 13 ++++--- 2 files changed, 44 insertions(+), 31 deletions(-) diff --git a/promptpack/gui/file_selector.py b/promptpack/gui/file_selector.py index c289c24..fdb5b34 100644 --- a/promptpack/gui/file_selector.py +++ b/promptpack/gui/file_selector.py @@ -110,6 +110,11 @@ def select_files(app): vsb.grid(row=0, column=1, sticky="ns") hsb.grid(row=1, column=0, sticky="ew") + tree.bind("", lambda e: tree.focus_set()) + tree.bind("", lambda e: tree.yview_scroll(int(-1 * (e.delta / 120)), "units")) + tree.bind("", lambda e: tree.yview_scroll(-1, "units")) + tree.bind("", lambda e: tree.yview_scroll(1, "units")) + token_label = ttk.Label(selector, text="") token_label.pack(pady=2) @@ -117,38 +122,43 @@ def select_files(app): checkbox_items = {} all_state = tk.BooleanVar(value=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_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 worker(): for child in sorted(Path(folder).iterdir()): - selector.after(0, lambda c=child: insert_node('', c)) + selector.after(0, lambda c=child: insert_node("", c)) selector.after(0, update_token_label) threading.Thread(target=worker, daemon=True).start() diff --git a/promptpack/gui/settings_dialog.py b/promptpack/gui/settings_dialog.py index eaad699..ccd8803 100644 --- a/promptpack/gui/settings_dialog.py +++ b/promptpack/gui/settings_dialog.py @@ -93,11 +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="HTML", variable=app.export_format, value="html").pack(pady=2) - ttk.Radiobutton(scrollable, text="PDF", variable=app.export_format, value="pdf").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)