Skip to content
Merged
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
3 changes: 2 additions & 1 deletion .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ max-line-length = 88
# W503: Line break before binary operator
# W504: line break after binary operator
# E203: Whitespace before ':'
ignore = E501, W503, W504, E203
# W291/W293: trailing/blank-line whitespace
ignore = E501, W503, W504, E203, W291, W293

# A list of files and directories to exclude from linting.
exclude =
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ Codecat is a lightning-fast, Python-powered CLI tool that **aggregates your enti
- **Dynamic fence handling** for code blocks containing backticks
- **Glob pattern support** for flexible file inclusion/exclusion

### 🌐 **Interactive Web UI**

- **Visual Dashboard** provides a browser-based interface to manage everything
- **Fully Offline** operation with bundled assets—no internet required
- **Seamless Integration** perfectly reflects all CLI capabilities

### ⚙️ **Highly Configurable**

- **JSON configuration** with sensible defaults
Expand Down Expand Up @@ -123,6 +129,7 @@ codecat stats .
| Command | Description | Example |
| ------------------------- | ----------------------------------------- | -------------------------- |
| `codecat run <path>` | Scan directory and create Markdown output | `codecat run ./my-project` |
| `codecat web` | Launch the interactive Web UI | `codecat web` |
| `codecat stats <path>` | Show project statistics without output | `codecat stats .` |
| `codecat generate-config` | Create configuration template | `codecat generate-config` |

Expand Down Expand Up @@ -155,6 +162,9 @@ codecat run .
# Simple scan of current directory
codecat run .

# Launch the interactive web interface
codecat web

# Scan specific directory with custom output
codecat run ./my-project --output-file "project-complete.md"

Expand Down
19 changes: 10 additions & 9 deletions codecat.spec
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
# -*- mode: python ; coding: utf-8 -*-

block_cipher = None

a = Analysis(
['src/codecat/__main__.py'],
pathex=['src'],
pathex=[],
binaries=[],
datas=[('assets/favicon.ico', '.')],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)

pyz = PYZ(a.pure, cipher=block_cipher)
pyz = PYZ(a.pure)

exe = EXE(
pyz,
Expand All @@ -31,7 +30,9 @@ exe = EXE(
upx_exclude=[],
runtime_tmpdir=None,
console=True,
icon='assets/favicon.ico',
include_binaries=True,
version='file_version_info.txt',
)
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
# --- Project Metadata ---
[project]
name = "codecat"
version = "1.0.2"
version = "1.1.0"
description = "A powerful, feature-rich command-line tool to aggregate source code into a single Markdown file."
readme = "README.md"
requires-python = ">=3.10"
Expand All @@ -19,7 +19,7 @@ classifiers = [
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"License :: OSI Approved :: GPL-3.0-only",
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
"Operating System :: OS Independent",
"Topic :: Software Development :: Documentation",
"Topic :: Text Processing",
Expand Down
2 changes: 1 addition & 1 deletion src/codecat/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@
This package contains the core logic and metadata for the Codecat application.
"""

__version__ = "1.0.2"
__version__ = "1.1.0"
11 changes: 9 additions & 2 deletions src/codecat/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
and also serves as the entry for PyInstaller-built executables.
"""

import logging
import os
import sys

Expand Down Expand Up @@ -39,7 +40,7 @@ def main():
"Example Usage:\n"
" [cyan]codecat --help[/cyan]\n\n"
"For more information, visit the GitHub README:\n"
" [cyan][link=https://github.com/exonymos/codecat?tab=readme-ov-file#-codecat]https://github.com/exonymos/codecat[/link][/cyan]",
" [cyan][link=https://github.com/exonymos/codecat]https://github.com/exonymos/codecat[/link][/cyan]",
title="Usage Error",
border_style="red",
padding=(1, 2),
Expand All @@ -50,8 +51,14 @@ def main():
os.system("pause")
sys.exit(1)

logging.basicConfig(
level=logging.WARNING,
stream=sys.stderr,
format="%(message)s",
)

# If it's a valid terminal session or has arguments, run the main Typer app.
app()
app(prog_name="codecat")


if __name__ == "__main__":
Expand Down
54 changes: 42 additions & 12 deletions src/codecat/cli_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from concurrent.futures import ThreadPoolExecutor, as_completed
from contextlib import nullcontext
from pathlib import Path
from typing import List, Optional
from typing import Optional

import typer
from rich.console import Console
Expand All @@ -30,6 +30,7 @@
from codecat.file_processor import ProcessedFileData, process_file
from codecat.file_scanner import scan_project
from codecat.markdown_generator import generate_markdown
from codecat.web_ui import start_web_app

# --- Initialize Rich Console for output ---
console = Console(stderr=True, highlight=False)
Expand Down Expand Up @@ -78,7 +79,7 @@ def version_callback(value: bool):
]

IncludePatterns = Annotated[
Optional[List[str]],
Optional[list[str]],
typer.Option(
"--include",
"-i",
Expand All @@ -87,7 +88,7 @@ def version_callback(value: bool):
]

ExcludePatterns = Annotated[
Optional[List[str]],
Optional[list[str]],
typer.Option(
"--exclude",
"-e",
Expand All @@ -98,7 +99,7 @@ def version_callback(value: bool):

# --- Helper Functions for Rich UI and Output ---
def _create_summary_table(
processed_results: List[ProcessedFileData], project_path: Path
processed_results: list[ProcessedFileData], project_path: Path
) -> Table:
"""Creates a Rich Table summarizing the results of a scan."""
summary = Table(
Expand Down Expand Up @@ -156,7 +157,7 @@ def _log_initial_info(

def _scan_project_files(
project_path: Path, effective_config: dict, show_ui: bool
) -> List[Path]:
) -> list[Path]:
"""Scans the project for files to process, handling UI and errors."""
scan_status_text = f"Scanning files in [cyan]'{project_path.name}'[/cyan]..."
scan_context = (
Expand Down Expand Up @@ -189,17 +190,17 @@ def _scan_project_files(


def _process_files_parallel(
files_to_scan: List[Path],
files_to_scan: list[Path],
project_path: Path,
effective_config: dict,
show_ui: bool,
max_workers: Optional[int],
) -> List[ProcessedFileData]:
) -> list[ProcessedFileData]:
"""
Processes a list of files in parallel, showing a static message and handling errors.
Returns a sorted list of ProcessedFileData objects.
"""
processed_results: List[ProcessedFileData] = []
processed_results: list[ProcessedFileData] = []
is_verbose = effective_config.get("verbose", False)
stop_on_error = effective_config.get("stop_on_error", False)

Expand Down Expand Up @@ -263,7 +264,7 @@ def _orchestrate_scan(
effective_config: dict,
show_ui: bool,
max_workers: Optional[int],
) -> List[ProcessedFileData]:
) -> list[ProcessedFileData]:
"""
Handles the shared logic of scanning and processing files for any command.

Expand Down Expand Up @@ -340,6 +341,8 @@ def run(
Scans a project, aggregates files, and compiles them into a single Markdown file.
"""
is_verbose = verbose and not silent
# NOTE: Checking for pytest in sys.modules avoids the Rich Status spinner conflicting
# with the test runner's output capture. This is a deliberate practical trade-off;
is_testing = "pytest" in sys.modules
show_ui = not is_verbose and not silent and not is_testing

Expand Down Expand Up @@ -431,7 +434,8 @@ def stats(
]

for file_data in text_files:
assert file_data.content is not None
if file_data.content is None:
continue
lang = lang_map.get(file_data.path.suffix.lower(), "text")
lang_counts[lang] += 1
num_lines = len(file_data.content.splitlines())
Expand Down Expand Up @@ -515,8 +519,9 @@ def generate_config(
raise typer.Exit(code=1)

try:
with open(config_file_path, "w", encoding="utf-8") as f:
json.dump(DEFAULT_CONFIG, f, indent=4)
config_file_path.write_text(
json.dumps(DEFAULT_CONFIG, indent=4), encoding="utf-8"
)
console.print(
f"Successfully generated config file: [green]{config_file_path.resolve()}[/green]"
)
Expand All @@ -527,6 +532,31 @@ def generate_config(
raise typer.Exit(code=1)


@app.command(name="web")
def web(
project_path: ProjectPath = Path("."),
port: Annotated[
int,
typer.Option(
"--port",
"-p",
help="The port to bind the web server to. Defaults to 8080. If in use, it will find the next available port.",
),
] = 8080,
):
"""
Launch the optional Codecat Web Interface.
"""
console.print(
Panel(
f"🌐 [bold]Starting Codecat Web Interface[/bold]\n"
f"Target Directory: [cyan]{project_path.resolve()}[/cyan]",
border_style="magenta",
)
)
start_web_app(port=port, project_path=project_path)


@app.callback()
def main_callback(
version: Annotated[
Expand Down
12 changes: 5 additions & 7 deletions src/codecat/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,10 @@

import copy
import json
import logging
from pathlib import Path
from typing import Any, Optional

import typer
from typer import colors as typer_colors

# Import constants for default config file names.
from codecat.constants import DEFAULT_CONFIG_FILENAME, DEFAULT_OUTPUT_FILENAME

Expand Down Expand Up @@ -354,10 +352,10 @@ def _load_user_config_from_file(
}
return user_config, True
except (json.JSONDecodeError, IOError) as e:
typer.secho(
f"Notice: Could not load or parse config '{config_path.resolve()}'. Error: {e}.",
fg=typer_colors.YELLOW,
err=True,
logging.warning(
"Notice: Could not load or parse config '%s'. Error: %s.",
config_path.resolve(),
e,
)
return None, False
return None, False
Expand Down
4 changes: 1 addition & 3 deletions src/codecat/file_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,7 @@ def _is_likely_binary_by_nulls(chunk: bytes) -> bool:
if not chunk:
return False
null_bytes = chunk.count(b"\x00")
return (
len(chunk) > 0 and (null_bytes / len(chunk)) * 100 > NULL_BYTE_THRESHOLD_PERCENT
)
return (null_bytes / len(chunk)) * 100 > NULL_BYTE_THRESHOLD_PERCENT


def _try_decode_bytes(
Expand Down
Loading