API Reference¶
Recursivist is organized as a set of focused modules that can be used directly from Python. This page documents the public API (generated from the source docstrings) and shows how to compose it.
Module Overview¶
| Module | Responsibility |
|---|---|
recursivist.scanner | Walk a directory into the nested structure dict |
recursivist.tree | Render a structure as a Rich tree in the terminal |
recursivist.exporters | Exporter registry and per-format exporters |
recursivist.compare | Compare and render two structures |
recursivist.filtering | Ignore-file, glob, and regex exclusion logic |
recursivist.flags | Resolve sort/display flags into a DisplayOptions |
recursivist.sorting | File ordering (by type, metric, or name similarity) |
recursivist.metrics | Lines of code, size, mtime, and metric formatting |
recursivist.colors | Deterministic per-extension colors |
recursivist.icons | Emoji and Nerd Font icon lookup |
recursivist.git_status | Git status lookup |
recursivist.github | Materialize a GitHub repository for scanning |
recursivist.config | User-configuration persistence |
The Structure Dictionary¶
Most of the API revolves around the nested dictionary produced by get_directory_structure. Each subdirectory is a nested dict under its own name; a directory's own files and aggregate metrics live under reserved keys:
_files: a list ofFileEntryobjects for the directory's files_loc,_size,_mtime: aggregate totals, present only when the matching metric is requested_max_depth_reached: present when traversal stopped at the depth limit_hidden_contents: present alongside_max_depth_reachedwhen the untraversed directory is not empty, so renderers can tell it apart from one that holds nothing_git_markers: a{filename: status}map, present only with Git status enabled
DisplayOptions¶
Sorting and annotation are driven by a single resolved value, recursivist.flags.DisplayOptions, which the renderers and exporters consult. It separates ordering (sort_key) from annotation (metrics and show_git_status):
from recursivist.flags import DisplayOptions
# Sort by lines of code; annotate each file with LOC then size
spec = DisplayOptions(sort_key="loc", metrics=("loc", "size"))
sort_key is one of "loc", "size", "mtime", "git_status", "similarity", or None (the default extension/name order). metrics is the ordered tuple of numeric metrics to display, and show_git_status toggles the Git-status marker. To build a DisplayOptions from raw CLI flags — recovering their left-to-right order from argv — use recursivist.flags.resolve_display_options.
FileEntry¶
recursivist._models.FileEntry ¶
Bases: NamedTuple
A single file within a scanned directory structure.
Attributes:
| Name | Type | Description |
|---|---|---|
name | str | Bare filename (e.g. |
path | str | The string to display for this file — an absolute, forward-slash path when full-path display is enabled, otherwise just |
loc | int | Lines of code. Populated only when LOC counting is enabled during scanning; |
size | int | File size in bytes. Populated only when size tracking is enabled; |
mtime | float | Modification time (seconds since epoch). Populated only when mtime tracking is enabled; |
Source code in recursivist/_models.py
coerce(item) classmethod ¶
Normalize any raw _files entry to a :class:FileEntry by position.
A :class:FileEntry is returned unchanged; a bare string becomes a name-only entry; and a tuple is read by its canonical (name, path, loc, size, mtime) slots by index, defaulting any missing trailing field. This matches how the scanner always emits entries (as full :class:FileEntry values), and is the single normalization boundary the sorting layer routes file lists through before the metric values are read.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
item | Union[FileEntry, tuple[Any, ...], str] | A :class: | required |
Returns:
| Type | Description |
|---|---|
FileEntry | The equivalent :class: |
Source code in recursivist/_models.py
Scanner¶
recursivist.scanner ¶
Directory traversal.
Recursively walks a directory, applies the exclusion rules from :mod:recursivist.filtering, collects optional per-file metrics from :mod:recursivist.metrics, and returns the nested structure dict consumed by the renderers and exporters.
get_directory_structure(root_dir, exclude_dirs=None, ignore_file=None, exclude_extensions=None, parent_ignore_patterns=None, exclude_patterns=None, include_patterns=None, max_depth=0, current_depth=0, current_path='', show_full_path=False, sort_by_loc=False, sort_by_size=False, sort_by_mtime=False, show_git_status=False, git_status_map=None, ancestor_ids=None) ¶
Build a nested dictionary representing a directory structure.
Recursively traverses root_dir, applying the exclusion rules and optionally collecting per-file metrics, and returns the nested mapping consumed by the renderers and exporters. Each subdirectory becomes a nested dict under its own name; a directory's files and aggregate metrics are stored under reserved keys.
Reserved keys in the returned structure:
"_files": list of :class:FileEntryfor the directory's files."_loc": total lines of code (when sort_by_loc is set)."_size": total size in bytes (when sort_by_size is set)."_mtime": latest modification time (when sort_by_mtime is set)."_max_depth_reached": present when traversal stopped at max_depth."_hidden_contents": present (andTrue) alongside"_max_depth_reached"when the untraversed directory is not empty, so renderers can still tell it apart from one that holds nothing."_symlink_loop": present (andTrue) when a directory was not recursed into because it resolves to one of its own ancestors, i.e. a symlink (or other) cycle back up the tree."_git_markers":{filename: status_char}(when show_git_status is set).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
root_dir | str | Directory to scan. | required |
exclude_dirs | Sequence[str] | None | Directory names to skip entirely. | None |
ignore_file | str | None | Name of an ignore file to honor within each directory (e.g. | None |
exclude_extensions | set[str] | None | Lowercase, dot-prefixed extensions to exclude. | None |
parent_ignore_patterns | Sequence[tuple[str, tuple[str, ...]]] | None | Ignore files inherited from parent directories as a shallowest-first stack of | None |
exclude_patterns | Sequence[str | Pattern[str]] | None | Glob or compiled-regex patterns to exclude. | None |
include_patterns | Sequence[str | Pattern[str]] | None | Glob or compiled-regex patterns to include, which override the exclusions. | None |
max_depth | int | Maximum depth to traverse, or | 0 |
current_depth | int | Current recursion depth. Set internally. | 0 |
current_path | str | Path of the current directory relative to the scan root. Set internally. | '' |
show_full_path | bool | Whether to store absolute paths instead of bare filenames. | False |
sort_by_loc | bool | Whether to count and total lines of code. | False |
sort_by_size | bool | Whether to measure and total file sizes. | False |
sort_by_mtime | bool | Whether to record file modification times. | False |
show_git_status | bool | Whether to annotate files with Git status markers. | False |
git_status_map | dict[str, str] | None | Pre-computed | None |
ancestor_ids | frozenset[tuple[int, int]] | None |
| None |
Returns:
| Type | Description |
|---|---|
dict[str, Any] | A |
set[str] | directory mapping and extensions is the set of lowercase file |
tuple[dict[str, Any], set[str]] | extensions encountered. |
Source code in recursivist/scanner.py
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 | |
has_contents(structure) ¶
Return whether a directory-structure entry holds anything to display.
A directory counts as non-empty when it has files, has subdirectories, or was cut short by the depth limit with contents left unexplored. Renderers use this to pick between the open and closed folder icons.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
structure | Any | A subtree of a structure dict as produced by :func: | required |
Returns:
| Type | Description |
|---|---|
bool |
|
Source code in recursivist/scanner.py
iter_subdirectories(structure) ¶
Yield (name, content) for each real subdirectory in structure.
Reserved bookkeeping keys (see :data:RESERVED_KEYS) are skipped, and entries are yielded in case-sensitive name order.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
structure | dict[str, Any] | A directory-structure dict as produced by :func: | required |
Yields:
| Type | Description |
|---|---|
tuple[str, Any] |
|
Source code in recursivist/scanner.py
Tree Rendering¶
recursivist.tree ¶
Terminal tree rendering.
Builds and prints a rich tree from a scanned structure, with extension colors, optional metric annotations, and Git status markers. This is the top of the dependency stack, composing the scanner, filtering, colors, metrics, sorting, and icon modules.
build_tree(structure, tree, color_map, spec, show_full_path=False, icon_style='emoji') ¶
Populate a rich tree from a scanned directory structure.
Recursively adds each file and subdirectory of structure to tree, with filenames colored by extension. Files are ordered by spec.sort_key via :func:recursivist.sorting.sort_files_by_type. A subtree that hit the depth limit is simply left unexpanded; its folder icon still shows whether anything was cut off.
The resolved spec controls the annotations appended to each entry:
spec.metrics: the ordered lines-of-code, size, and modification-time metrics to append (in the exact order requested).spec.show_git_status: append a colored marker to each file —[U]untracked (grey),[M]modified (yellow),[A]added (green),[D]deleted (red). The marker always trails the metric parenthetical, and deleted files no longer on disk are also struck through.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
structure | dict[str, Any] | Directory-structure dict to render. | required |
tree | Tree |
| required |
color_map | dict[str, str] | Mapping of lowercase file extension to hex color. | required |
spec | DisplayOptions | Resolved sorting and annotation directives. | required |
show_full_path | bool | Whether to display absolute paths instead of bare filenames. | False |
icon_style | str | Icon style to use, either | 'emoji' |
Source code in recursivist/tree.py
display_tree(root_dir, exclude_dirs=None, ignore_file=None, exclude_extensions=None, exclude_patterns=None, include_patterns=None, use_regex=False, max_depth=0, show_full_path=False, spec=None, icon_style='emoji', structure=None, extensions=None, root_name=None) ¶
Scan a directory and render it as a tree in the terminal.
Runs the full pipeline — optionally fetching Git status, scanning the directory, building a color map, and printing a rich tree — unless a pre-computed structure and extensions are supplied, in which case the scan is skipped and those are rendered directly.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
root_dir | str | Directory to display. | required |
exclude_dirs | list[str] | None | Directory names to skip entirely. | None |
ignore_file | str | None | Name of an ignore file to honor (e.g. | None |
exclude_extensions | set[str] | None | File extensions to exclude. Normalized to a lowercase, dot-prefixed form before scanning. | None |
exclude_patterns | list[str] | None | Glob or regex patterns to exclude. | None |
include_patterns | list[str] | None | Glob or regex patterns to include, which override the exclusions. | None |
use_regex | bool | Whether to treat the patterns as regular expressions instead of glob patterns. | False |
max_depth | int | Maximum depth to display, or | 0 |
show_full_path | bool | Whether to display absolute paths instead of bare filenames. | False |
spec | DisplayOptions | None | Resolved sorting and annotation directives. Defaults to a plain :class: | None |
icon_style | str | Icon style to use, either | 'emoji' |
structure | dict[str, Any] | None | Pre-computed directory structure. When given together with extensions, the directory is not re-scanned. | None |
extensions | set[str] | None | Pre-computed set of file extensions matching structure. | None |
root_name | str | None | Display name for the root node. Defaults to the basename of root_dir; supply this to label the tree with something other than the scanned path (e.g. a repository name for a GitHub input). | None |
Source code in recursivist/tree.py
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 | |
Exporters¶
Exports go through the get_exporter factory, which returns a BaseExporter subclass for the requested format. Call its export method with an output path.
recursivist.exporters ¶
Exporter registry.
Maps each supported export format to its exporter class and exposes :func:get_exporter, the factory used to construct the right exporter for a requested format.
canonical_extension(format_type) ¶
Return the canonical output file extension for a format identifier.
The extension is read from the exporter class, so aliases that share an exporter collapse to a single extension.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
format_type | str | Export format identifier (e.g. | required |
Returns:
| Type | Description |
|---|---|
str | The exporter's canonical file extension, without a leading dot (e.g. |
str |
|
str | lowercased format_type for unknown formats, mirroring |
str | func: |
Source code in recursivist/exporters/__init__.py
get_exporter(format_type, **kwargs) ¶
Construct the exporter for a given format.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
format_type | str | Export format identifier (e.g. | required |
**kwargs | Any | Keyword arguments forwarded to the exporter's constructor (see :class: | {} |
Returns:
| Type | Description |
|---|---|
BaseExporter | A ready-to-use exporter instance; call its |
BaseExporter | the output file. |
Raises:
| Type | Description |
|---|---|
ValueError | If format_type is not a supported format. |
Source code in recursivist/exporters/__init__.py
supported_formats() ¶
Return the export format identifiers accepted by :func:get_exporter.
This is the single source of truth for which formats are valid; callers (e.g. the CLI) should derive their validation from it rather than hard-coding a list.
Returns:
| Type | Description |
|---|---|
list[str] | The supported format identifiers, sorted for stable presentation. |
list[str] | Alias keys are included, so both |
Source code in recursivist/exporters/__init__.py
recursivist.exporters.base ¶
Shared base class for directory-structure exporters.
Defines :class:BaseExporter, which stores the scanned structure and the resolved display options common to every output format. Concrete exporters subclass it and implement :meth:BaseExporter.export.
BaseExporter ¶
Common base for the per-format exporters.
Holds the scanned structure and the resolved :class:DisplayOptions; the actual output is produced by each subclass's :meth:export. For convenience, the individual pieces of the spec are also exposed as plain attributes (metrics, sort_key, show_loc/show_size/ show_mtime/show_git_status) so exporters can read them directly.
Attributes:
| Name | Type | Description |
|---|---|---|
extension | str | Canonical file extension for this format, without a leading dot (e.g. |
Source code in recursivist/exporters/base.py
__init__(structure, root_name, base_path=None, spec=None, icon_style='emoji') ¶
Store the structure and display options for an export.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
structure | dict[str, Any] | Scanned directory structure to export. | required |
root_name | str | Display name of the root directory. | required |
base_path | str | None | Base path for full-path display. When provided (not | None |
spec | DisplayOptions | None | Resolved sorting and annotation directives. Defaults to a plain :class: | None |
icon_style | str | Icon style to use, either | 'emoji' |
Source code in recursivist/exporters/base.py
export(output_path) ¶
Write the export to output_path.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
output_path | str | Path the output file is written to. | required |
Raises:
| Type | Description |
|---|---|
NotImplementedError | Always; subclasses must override this method. |
Source code in recursivist/exporters/base.py
Compare¶
recursivist.compare ¶
Side-by-side directory comparison.
Builds the structures for two directories with identical filtering and renders them next to each other, highlighting entries unique to either side. Supports the same filtering and metric options as the single-tree renderer, with terminal output for interactive use and HTML export for sharing.
build_comparison_tree(structure, other_structure, tree, spec, show_full_path=False, icon_style='emoji', identity_spec=None, *, this_is_remote=False, other_is_remote=False) ¶
Populate a rich tree, highlighting differences against another tree.
Recursively adds the entries of structure to tree, comparing each against other_structure: items present in both are shown normally, items unique to structure are highlighted in green, and items unique to other_structure are highlighted in red. File names are rendered without file-type-specific colors so the green/red difference highlighting stands out. Files are ordered by spec.sort_key and metric annotations are appended in spec.metrics order.
When spec.show_git_status is set, each file is followed by a plain Git-status badge — [U] untracked, [M] modified, [A] added, [D] deleted — read from the _git_markers stored on structure (and on other_structure for entries unique to it). The badge is not color-coded; it trails the metric parenthetical, and deleted files are struck through.
Two identically named files count as the same entry only when their displayed annotations also match (see :func:_comparison_identity), so a differing metric or Git status marks them as unique to their side. identity_spec controls which annotations that match considers: it defaults to spec, but a caller comparing a local directory against a hosted repository passes spec.without_remote_unsupported() so that annotations a remote side cannot provide (modification time, Git status) are excluded from the identity — those are still displayed per spec, they just no longer split otherwise-matching files across the two sides.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
structure | dict[str, Any] | Structure of the directory being rendered. | required |
other_structure | dict[str, Any] | Structure of the directory being compared against. | required |
tree | Tree |
| required |
spec | DisplayOptions | Resolved sorting and annotation directives. | required |
show_full_path | bool | Whether to display absolute paths instead of bare filenames. | False |
icon_style | str | Icon style to use, either | 'emoji' |
identity_spec | DisplayOptions | None | Directives governing which annotations contribute to cross-side file identity. Defaults to spec. | None |
this_is_remote | bool | Whether the primary structure originates from a hosted repository. | False |
other_is_remote | bool | Whether the compared structure originates from a hosted repository. | False |
Source code in recursivist/compare.py
241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 | |
compare_directory_structures(dir1, dir2, exclude_dirs=None, ignore_file=None, exclude_extensions=None, exclude_patterns=None, include_patterns=None, max_depth=0, show_full_path=False, spec=None) ¶
Scan two inputs for comparison, each a local directory or GitHub URL.
Each side is scanned with the same filtering and metric settings. A side may be a local directory or a GitHub repository URL; a GitHub side is downloaded and extracted to a temporary directory (removed before this function returns), scanned there, and — when show_full_path is set — has its file paths rewritten to GitHub blob URLs.
The --ignore-file option, Git-status annotations, and modification-time annotations only apply to local directories, so they are skipped for any GitHub side (its spec is adjusted via :meth:~recursivist.flags.DisplayOptions.without_remote_unsupported) while still being honored for a local side. When both sides are GitHub repositories the caller is expected to have already cleared these from spec as well.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dir1 | str | First input — a local directory path or a GitHub repository URL. | required |
dir2 | str | Second input — a local directory path or a GitHub repository URL. | required |
exclude_dirs | Sequence[str] | None | Directory names to skip entirely. | None |
ignore_file | str | None | Name of an ignore file to honor for local sides (e.g. | None |
exclude_extensions | set[str] | None | Lowercase, dot-prefixed extensions to exclude. | None |
exclude_patterns | Sequence[str | Pattern[str]] | None | Glob or compiled-regex patterns to exclude. | None |
include_patterns | Sequence[str | Pattern[str]] | None | Glob or compiled-regex patterns to include, which override the exclusions. | None |
max_depth | int | Maximum depth to scan, or | 0 |
show_full_path | bool | Whether to store absolute paths (local sides) or GitHub blob URLs (GitHub sides) instead of bare filenames. | False |
spec | DisplayOptions | None | Resolved sorting and annotation directives. When Git status is requested it is looked up independently for each local side. Defaults to a plain :class: | None |
Returns:
| Type | Description |
|---|---|
tuple[dict[str, Any], dict[str, Any]] | A |
Source code in recursivist/compare.py
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 | |
display_comparison(dir1, dir2, exclude_dirs=None, ignore_file=None, exclude_extensions=None, exclude_patterns=None, include_patterns=None, use_regex=False, max_depth=0, show_full_path=False, spec=None, icon_style='emoji') ¶
Render two directory trees side by side in the terminal.
Scans both directories with identical options and prints them as two labeled, color-highlighted panels: entries unique to dir1 and dir2 are highlighted in contrasting colors, shared entries are shown normally, and a legend explains the scheme.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dir1 | str | Path to the first directory. | required |
dir2 | str | Path to the second directory. | required |
exclude_dirs | list[str] | None | Directory names to skip entirely. | None |
ignore_file | str | None | Name of an ignore file to honor (e.g. | None |
exclude_extensions | set[str] | None | File extensions to exclude. Normalized to a lowercase, dot-prefixed form before scanning. | None |
exclude_patterns | list[str] | None | Glob or regex patterns to exclude. | None |
include_patterns | list[str] | None | Glob or regex patterns to include, which override the exclusions. | None |
use_regex | bool | Whether to treat the patterns as regular expressions instead of glob patterns. | False |
max_depth | int | Maximum depth to display, or | 0 |
show_full_path | bool | Whether to display absolute paths instead of bare filenames. | False |
spec | DisplayOptions | None | Resolved sorting and annotation directives. Defaults to a plain :class: | None |
icon_style | str | Icon style to use, either | 'emoji' |
Source code in recursivist/compare.py
517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 | |
export_comparison(dir1, dir2, format_type, output_path, exclude_dirs=None, ignore_file=None, exclude_extensions=None, exclude_patterns=None, include_patterns=None, use_regex=False, max_depth=0, show_full_path=False, spec=None, icon_style='emoji') ¶
Export a side-by-side directory comparison to an HTML file.
Scans both directories with identical options and writes a standalone, responsive HTML document containing the highlighted comparison, a legend, and a summary of the settings used. Only HTML output is supported.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dir1 | str | Path to the first directory. | required |
dir2 | str | Path to the second directory. | required |
format_type | str | Export format. Only | required |
output_path | str | Path the HTML file is written to. | required |
exclude_dirs | list[str] | None | Directory names to skip entirely. | None |
ignore_file | str | None | Name of an ignore file to honor (e.g. | None |
exclude_extensions | set[str] | None | File extensions to exclude. Normalized to a lowercase, dot-prefixed form before scanning. | None |
exclude_patterns | list[str] | None | Glob or regex patterns to exclude. | None |
include_patterns | list[str] | None | Glob or regex patterns to include, which override the exclusions. | None |
use_regex | bool | Whether to treat the patterns as regular expressions instead of glob patterns. | False |
max_depth | int | Maximum depth to include, or | 0 |
show_full_path | bool | Whether to write absolute paths instead of bare filenames. | False |
spec | DisplayOptions | None | Resolved sorting and annotation directives. Defaults to a plain :class: | None |
icon_style | str | Icon style to use, either | 'emoji' |
Raises:
| Type | Description |
|---|---|
ValueError | If format_type is not |
Source code in recursivist/compare.py
723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 | |
Filtering¶
recursivist.filtering ¶
File and directory filtering: ignore files, glob/regex patterns, and gitignore-style exclusion rules.
Provides the predicate :func:should_exclude used by the scanner. Git-style ignore matching is delegated to :mod:pathspec (its gitignore matcher), which implements the full gitignore specification: anchoring, ** wildcards, directory-only (trailing /) patterns, ! negation with last-match-wins, character classes, backslash escapes, and trailing-whitespace handling. The glob and regex matching used by --exclude-pattern/--include-pattern is unrelated and remains pure standard library.
Like Git, each ignore file is evaluated relative to the directory that contains it rather than relative to the scan root. The active ignore files are kept as a stack (shallowest first); a path is tested against every level with its own anchoring, and a deeper file's verdict overrides a shallower one, so an anchored pattern such as /build in a nested .gitignore matches only within that subdirectory and does not leak up to the scan root or down past the anchor.
compile_regex_patterns(patterns, is_regex=False) ¶
Compile patterns to regex objects when regex matching is requested.
When is_regex is False the patterns are returned unchanged for glob matching. When True each pattern is compiled to a :class:re.Pattern; any pattern that fails to compile is kept as a string and a warning is logged.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
patterns | Sequence[str] | Patterns to process. | required |
is_regex | bool | Whether to treat the patterns as regular expressions ( | False |
Returns:
| Type | Description |
|---|---|
list[str | Pattern[str]] | A list whose items are plain strings for glob patterns or compiled |
list[str | Pattern[str]] | class: |
Source code in recursivist/filtering.py
parse_ignore_file(ignore_file_path) ¶
Read an ignore file and return its lines as gitignore patterns.
Lines are returned verbatim with only their terminators removed, preserving order and every character that is significant to the gitignore grammar (comments, blank lines, backslash escapes, and escaped trailing whitespace). Interpretation is left entirely to the gitignore matcher, so callers must not strip or filter the returned lines.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ignore_file_path | str | Path to the ignore file (e.g. | required |
Returns:
| Type | Description |
|---|---|
list[str] | The list of pattern lines, or an empty list when the file does not |
list[str] | exist. |
Source code in recursivist/filtering.py
should_exclude(path, ignore_context, exclude_extensions=None, exclude_patterns=None, include_patterns=None) ¶
Decide whether a path should be excluded from the scan.
The filtering rules are applied in priority order:
- If include_patterns are given and none match a file, exclude it.
- If any exclude_patterns match, exclude the path (this overrides include patterns).
- If the file's extension is in exclude_extensions, exclude it.
- If an include pattern matched, include the path (this overrides the gitignore-style patterns below).
- Otherwise apply the gitignore-style rules from ignore_context via :mod:
pathspec, honoring anchoring,**wildcards, directory-only (trailing/) patterns,!negation, character classes and escapes per the gitignore specification. Each ignore file in the stack is matched relative to its own directory and deeper files override shallower ones, so a nested file's anchored patterns stay scoped to its subtree.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path | str | Filesystem path to test. | required |
ignore_context | dict[str, Any] | Mapping describing the active ignore rules. Recognized keys are | required |
exclude_extensions | set[str] | None | Lowercase, dot-prefixed extensions to exclude. | None |
exclude_patterns | Sequence[str | Pattern[str]] | None | Glob or compiled-regex patterns to exclude. | None |
include_patterns | Sequence[str | Pattern[str]] | None | Glob or compiled-regex patterns to include, which override the gitignore-style exclusions. | None |
Returns:
| Type | Description |
|---|---|
bool |
|
Source code in recursivist/filtering.py
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 | |
Flags¶
recursivist.flags ¶
Command-line flag resolution for file sorting and annotation.
Recursivist exposes three families of file-annotation flags:
- Sorting-only —
--sort-by-similaritygroups similarly named files together but adds no annotation of its own. - Combined —
--sort-by-loc,--sort-by-size,--sort-by-mtimeand--sort-by-git-statuseach sort files by a metric and annotate every file with that metric. - Display-only —
--loc,--size,--mtimeand--git-statusannotate files with a metric without influencing the ordering.
When several of these flags are combined, they are resolved strictly by their left-to-right order on the command line rather than by any fixed internal precedence. The rules, implemented by :func:resolve_flags, are:
- Only the first sorting flag (sorting-only or combined) is honored; every later sorting flag is discarded completely — it contributes neither ordering nor annotation.
- Display-only flags always annotate. Their annotations appear in the exact order the flags were given.
- When the winning sort is a combined numeric metric (LOC, size or mtime), that metric's annotation is shown first, ahead of any display-only ones.
- When the winning sort is a combined Git-status flag, its badge trails at the very end, after every display-only annotation.
The resolution is expressed as a :class:DisplayOptions, the single value the renderers and exporters consult to decide how to sort and what to annotate.
DisplayOptions dataclass ¶
Resolved sorting and annotation directives for a single run.
This is the value produced by :func:resolve_flags and threaded through the renderers and exporters. It cleanly separates the two concerns the flags used to conflate: how files are ordered (:attr:sort_key) and what is annotated, and in what order (:attr:metrics plus :attr:show_git_status).
Attributes:
| Name | Type | Description |
|---|---|---|
sort_key | str | None | The single metric files are ordered by — one of :data: |
metrics | tuple[str, ...] | The numeric metrics (subset of :data: |
show_git_status | bool | Whether to append the Git-status badge to each file. The badge always trails the numeric-metric parenthetical. |
Source code in recursivist/flags.py
show_loc property ¶
Whether the lines-of-code annotation is shown.
show_mtime property ¶
Whether the modification-time annotation is shown.
show_size property ¶
Whether the file-size annotation is shown.
sorts_by_metric property ¶
Whether ordering is driven by a numeric metric (LOC, size, mtime).
without_remote_unsupported() ¶
Return a copy with annotations that don't apply to a hosted repo.
A GitHub checkout has no meaningful per-file Git status or modification time — every file effectively shares the tip commit's status and timestamp — so the Git-status badge and the modification-time metric are dropped, and a sort keyed on either falls back to the default ordering. The lines-of-code and size metrics are retained, since those are computed from the file contents themselves.
Returns:
| Name | Type | Description |
|---|---|---|
A | DisplayOptions | class: |
DisplayOptions | removed from both the sort key and the annotation set. |
Source code in recursivist/flags.py
FlagSpec dataclass ¶
Static description of a single order-sensitive flag.
Attributes:
| Name | Type | Description |
|---|---|---|
id | str | Stable identifier used as a dictionary key by the CLI layer. |
mode | str | One of :data: |
metric | str | The metric the flag relates to (e.g. :data: |
long | str | The long option string, including the leading dashes. |
short | str | None | The single-letter short option (without the dash), or |
Source code in recursivist/flags.py
resolve_display_options(*, sort_loc=False, sort_size=False, sort_mtime=False, sort_similarity=False, sort_git=False, disp_loc=False, disp_size=False, disp_mtime=False, disp_git=False, tokens=None) ¶
Resolve the raw per-flag booleans into :class:DisplayOptions.
The set of active flags comes from the boolean arguments (which the CLI parser has already validated), while their relative order is recovered from tokens — the raw command-line arguments. This split keeps resolution robust: the parser is the source of truth for which flags are present, and the token scan only orders that known set. A flag that cannot be located in tokens falls back to its registry position so ordering stays deterministic.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sort_loc | bool | Whether | False |
sort_size | bool | Whether | False |
sort_mtime | bool | Whether | False |
sort_similarity | bool | Whether | False |
sort_git | bool | Whether | False |
disp_loc | bool | Whether | False |
disp_size | bool | Whether | False |
disp_mtime | bool | Whether | False |
disp_git | bool | Whether | False |
tokens | Sequence[str] | None | The raw command-line tokens used to order the active flags. Defaults to | None |
Returns:
| Type | Description |
|---|---|
DisplayOptions | The resolved :class: |
Source code in recursivist/flags.py
resolve_flags(events) ¶
Resolve an ordered sequence of flag events into :class:DisplayOptions.
Each event is a (mode, metric) pair drawn from the registry, in the left-to-right order the flags appeared on the command line. The resolution rules are described in the module docstring.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
events | Sequence[tuple[str, str]] | The flag events, ordered by command-line position. | required |
Returns:
| Type | Description |
|---|---|
DisplayOptions | The resolved :class: |
Source code in recursivist/flags.py
Sorting¶
recursivist.sorting ¶
File ordering.
Sorts a directory's files by extension/name, by a numeric metric (LOC, size, mtime), or groups them by name similarity. Operates on :class:FileEntry.
sort_files_by_similarity(files) ¶
Order files so that similarly named files sit next to each other.
Unlike the LOC/size/mtime metrics, name similarity is relational: it depends on how a file's name compares to the others rather than on any single measured value, so it cannot be expressed as a sorted key. Instead this builds a greedy nearest-neighbour chain:
- Entries are seeded in case-insensitive name order and the alphabetically-first name becomes the start of the chain. Using a fixed, name-derived anchor makes the result deterministic and stable across runs (the same directory always yields the same order).
- Repeatedly, the not-yet-placed entry whose name is most similar to the most recently placed name is appended. Similarity is the :class:
difflib.SequenceMatcherratio computed case-insensitively on the full filename (extension included), somain.py/main.jsandtest_api.py/test_api.jsnaturally cluster. - Ratio ties are broken by case-insensitive name order: because the candidates are kept alphabetically sorted and the best match is only replaced on a strictly greater ratio, the alphabetically-first of any tied group wins.
This is a heuristic (locally greedy) ordering rather than a globally optimal grouping, which is the appropriate trade-off for a directory listing: each directory holds relatively few files, so the O(n^2) pairwise comparisons are cheap, and the result reliably places obvious name-siblings adjacent to one another.
Inputs may be :class:FileEntry instances, bare filename strings, or positional tuples; every item is normalised to a :class:FileEntry via :meth:FileEntry.coerce before ordering. Since only the name is used, the metric slots do not affect the result.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
files | Sequence[Any] | List of file items ( | required |
Returns:
| Type | Description |
|---|---|
list[FileEntry] | Reordered list of :class: |
Source code in recursivist/sorting.py
sort_files_by_type(files, sort_key=None, git_markers=None) ¶
Order files by a single resolved sort key.
Exactly one ordering is applied, chosen by sort_key:
None: the default — by extension, then case-insensitive name."loc"/"size"/"mtime": by that metric, largest/newest first (a stable sort keeps the pre-existing order for equal values)."git_status": grouped by Git status (modified, added, deleted, untracked, then clean), and by case-insensitive name within each group. Requires git_markers."similarity": by name similarity, via :func:sort_files_by_similarity.
This mirrors the resolution in :mod:recursivist.flags, where only the first sorting flag on the command line takes effect, so there is never more than one active metric to combine.
Inputs may be :class:FileEntry instances, bare filename strings, or positional tuples; every item is normalised to a :class:FileEntry via :meth:FileEntry.coerce before sorting.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
files | Sequence[Any] | List of file items ( | required |
sort_key | str | None | The single metric to order by, or | None |
git_markers | Mapping[str, str] | None |
| None |
Returns:
| Type | Description |
|---|---|
list[FileEntry] | Sorted list of :class: |
Source code in recursivist/sorting.py
Metrics¶
recursivist.metrics ¶
File statistics and metric formatting.
Lines-of-code counting, file size and modification-time retrieval, and the helpers that format those metrics into the annotation suffixes shown next to files and directories. Pure standard library.
count_lines_of_code(file_path) ¶
Count the number of lines in a text file.
Detects the encoding well enough to count lines reliably: UTF-16 files are recognized by their byte-order mark or by a regular pattern of null bytes, while files containing null bytes that are not UTF-16 are treated as binary and skipped. Decoding falls back from strict UTF-8 to UTF-16 and finally to UTF-8 with replacement so that text is never rejected over a stray byte.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
file_path | str | Path to the file. | required |
Returns:
| Type | Description |
|---|---|
int | The number of lines, or |
int | be read. |
Source code in recursivist/metrics.py
format_dir_metrics(content, metrics=()) ¶
Return the space-prefixed metrics suffix for a directory node.
Wraps :func:format_metrics_suffix, reading the totals from a directory's structure dict and keeping only the requested metrics that are actually present on that directory — while preserving the requested display order. Returns "" for a non-dict node.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
content | Any | The directory's structure dict (or any value; non-dicts yield an empty string). | required |
metrics | Sequence[str] | The metrics to display, in order. | () |
Returns:
| Type | Description |
|---|---|
str | The metrics suffix (with a leading space) or an empty string. |
Source code in recursivist/metrics.py
format_metrics(loc=0, size=0, mtime=0.0, metrics=()) ¶
Build the parenthetical metrics annotation for a file or directory.
Includes exactly the metrics named in metrics, in that order — e.g. metrics=("size", "loc") yields "(4.2 KB, 120 lines)". The metric names are those defined in :mod:recursivist.flags: "loc", "size" and "mtime".
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
loc | int | Lines-of-code count. | 0 |
size | int | Size in bytes. | 0 |
mtime | float | Modification time (seconds since epoch). | 0.0 |
metrics | Sequence[str] | The metrics to include, in display order. | () |
Returns:
| Type | Description |
|---|---|
str | The annotation string including the surrounding parentheses, or an |
str | empty string when metrics is empty. |
Source code in recursivist/metrics.py
format_metrics_suffix(loc=0, size=0, mtime=0.0, metrics=()) ¶
Like :func:format_metrics but prefixed with a single space.
Convenient for appending directly after a file or directory name. Returns an empty string (no leading space) when metrics is empty.
Source code in recursivist/metrics.py
format_size(size_in_bytes) ¶
Format a byte count as a human-readable size string.
Scales the value to bytes, KB, MB, or GB and formats it with one decimal place for every unit above bytes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
size_in_bytes | int | Size in bytes. | required |
Returns:
| Type | Description |
|---|---|
str | A human-readable size string (e.g. |
Source code in recursivist/metrics.py
format_timestamp(timestamp) ¶
Format a Unix timestamp as a human-readable, recency-aware string.
The representation becomes coarser as the timestamp gets older:
- Today:
"Today HH:MM" - Yesterday:
"Yesterday HH:MM" - Within the last week: abbreviated weekday and time (e.g.
"Mon 14:30") - Earlier this year: abbreviated month and day (e.g.
"Mar 15") - Older:
"YYYY-MM-DD"
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timestamp | float | Seconds since the epoch. | required |
Returns:
| Type | Description |
|---|---|
str | The formatted date/time string, or |
str | falls outside the representable range. |
Source code in recursivist/metrics.py
get_file_mtime(file_path) ¶
Return a file's modification time in seconds since the epoch.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
file_path | str | Path to the file. | required |
Returns:
| Type | Description |
|---|---|
float | The modification time as a float, or |
float | accessed. |
Source code in recursivist/metrics.py
get_file_size(file_path) ¶
Return the size of a file in bytes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
file_path | str | Path to the file whose size should be retrieved. | required |
Returns:
| Type | Description |
|---|---|
int | Size of the file in bytes, or |
int | accessed (e.g., permission error or the path no longer exists). |
Source code in recursivist/metrics.py
Colors¶
recursivist.colors ¶
Deterministic color assignment for file extensions.
Generates a stable, visually distinct hex color per file extension using a hash of the extension, with a collision-avoidance pass over already-assigned colors. Also provides WCAG 2.1 contrast helpers used by renderers that draw onto a known background (such as the HTML exporter) to guarantee legible text. Pure standard library.
WCAG_AAA_LARGE_TEXT = 4.5 module-attribute ¶
WCAG 2.1 level AAA minimum contrast ratio for large text (>=18pt, or >=14pt bold).
WCAG_AAA_NORMAL_TEXT = 7.0 module-attribute ¶
WCAG 2.1 level AAA minimum contrast ratio for normal-sized body text.
WCAG_AA_LARGE_TEXT = 3.0 module-attribute ¶
WCAG 2.1 level AA minimum contrast ratio for large text (>=18pt, or >=14pt bold).
WCAG_AA_NORMAL_TEXT = 4.5 module-attribute ¶
WCAG 2.1 level AA minimum contrast ratio for normal-sized body text.
color_distance(color1, color2) ¶
Calculate the perceptual distance between two RGB colors.
Uses a weighted Euclidean distance formula that approximates human color perception by emphasising the green channel over red and blue.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
color1 | tuple[int, int, int] | First color as an | required |
color2 | tuple[int, int, int] | Second color as an | required |
Returns:
| Type | Description |
|---|---|
float | A non-negative float representing the perceptual distance; |
float | means the colors are identical and larger values indicate greater |
float | visual difference. |
Source code in recursivist/colors.py
contrast_ratio(color1, color2) ¶
Calculate the WCAG contrast ratio between two colors.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
color1 | tuple[int, int, int] | First color as an | required |
color2 | tuple[int, int, int] | Second color as an | required |
Returns:
| Type | Description |
|---|---|
float | The contrast ratio, from |
float | (black against white). WCAG 2.1 requires at least |
float | body text at level AA and |
Source code in recursivist/colors.py
ensure_contrast(hex_color, background='#ffffff', min_ratio=WCAG_AA_NORMAL_TEXT) cached ¶
Adjust a color until it meets a WCAG contrast ratio against background.
The hue is preserved so extensions stay recognisable and mutually distinguishable; only brightness (and, if brightness alone is not enough, saturation) is changed. Colors that already meet min_ratio are returned unchanged, so this is a no-op for compliant input.
Colors are darkened against light backgrounds and lightened against dark ones, whichever direction can reach the required ratio.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
hex_color | str | Foreground color as a hex string, with or without a leading | required |
background | str | Background color the text is drawn on, as a hex string. | '#ffffff' |
min_ratio | float | Minimum acceptable contrast ratio. Defaults to | WCAG_AA_NORMAL_TEXT |
Returns:
| Type | Description |
|---|---|
str | A CSS hex color string that meets min_ratio against background, |
str | or (if no adjustment of this hue can reach the ratio) the closest |
str | achievable color. |
Source code in recursivist/colors.py
generate_color_for_extension(extension) ¶
Generate a stable, visually distinct color for a file extension.
The color is derived deterministically from a hash of the extension, so a given extension always maps to the same color within a session. Candidate colors are nudged through hue/saturation/value variations until they are far enough from every previously assigned color, keeping distinct extensions visually separable. The leading dot is optional and ignored, so "py" and ".py" share a color. An empty extension maps to white.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
extension | str | File extension, with or without a leading dot. | required |
Returns:
| Type | Description |
|---|---|
str | A CSS hex color string (e.g., |
Source code in recursivist/colors.py
hex_to_rgb(hex_color) ¶
Convert a CSS hex color string to an (r, g, b) tuple.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
hex_color | str | Six-digit hex color string, optionally prefixed with | required |
Returns:
| Type | Description |
|---|---|
int | A three-tuple of integers |
int |
|
Source code in recursivist/colors.py
relative_luminance(color) ¶
Calculate the WCAG relative luminance of an sRGB color.
Implements the definition given in WCAG 2.1: each channel is normalised to 0–1, linearised to remove the sRGB transfer function, and then combined with the standard luminance coefficients.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
color | tuple[int, int, int] | Color as an | required |
Returns:
| Type | Description |
|---|---|
float | The relative luminance, from |
Source code in recursivist/colors.py
rgb_to_hex(color) ¶
Convert an (r, g, b) tuple to a CSS hex color string.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
color | tuple[int, int, int] | Three-tuple of integers | required |
Returns:
| Type | Description |
|---|---|
str | A lowercase six-digit hex color string prefixed with |
Source code in recursivist/colors.py
Icons¶
recursivist.icons ¶
Nerd Font icon mappings for files and directories.
This module provides icon lookup utilities based on Nerd Font glyph codes. Icons are resolved in priority order:
- Exact filename match (e.g.,
Dockerfile,package.json) - File extension match (e.g.,
.py,.ts) - Named folder match (e.g.,
node_modules,.git) - Generic fallback icons for unknown files and directories
get_icon(filename, is_dir=False, style='emoji', is_empty=False) ¶
Return the icon for a file or directory in the requested style.
With the "emoji" style, a single generic file emoji is returned for files and an open or closed folder emoji for directories. With the "nerd" style, a Nerd Font glyph is resolved in priority order.
For files:
- Exact filename match in
EXACT_MATCH_ICONS(case-insensitive). - File-extension match in
EXTENSION_ICONS. - The
DEFAULT_NERD_FILEfallback.
For directories, FOLDER_ICONS is consulted first (so well-known folders keep their distinctive glyph regardless of their contents), falling back to the open or closed generic folder glyph depending on is_empty.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
filename | str | Name of the file or directory (basename only, not a full path). Matched case-insensitively. | required |
is_dir | bool | When | False |
style | str | Icon style to use, either | 'emoji' |
is_empty | bool | Only meaningful when is_dir is set. When | False |
Returns:
| Type | Description |
|---|---|
str | A single Unicode character containing the matching glyph. |
Source code in recursivist/icons.py
Git Status¶
recursivist.git_status ¶
Git status lookup.
Wraps git status --porcelain and maps changed/untracked paths back to a location relative to the directory being visualised. Pure standard library.
get_git_status(directory) ¶
Get Git status for files relative to a given directory.
Runs git status --porcelain -z from the repository root and maps every changed/untracked path back to a path relative to directory, filtering out files that live outside of it.
Status characters returned: - 'U': Untracked (?? in porcelain output) - 'M': Modified (working-tree or staged modification) - 'A': Added / staged for the first time (includes renames) - 'D': Deleted (working-tree or staged deletion)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
directory | str | Absolute path to the directory being visualised. Must be inside a Git repository. | required |
Returns:
| Type | Description |
|---|---|
dict[str, str] |
|
dict[str, str] | slashes regardless of OS, or an empty dict when Git is unavailable or |
dict[str, str] | the directory is not tracked. |
Source code in recursivist/git_status.py
GitHub¶
A GitHub repository URL passed to visualize, export, or compare is resolved here. parse_github_url turns a URL into a GitHubTarget, and checkout_repository downloads the repository's source archive into a temporary directory and yields a RepoCheckout whose local_root is scanned like any other directory. apply_github_urls rewrites file paths to GitHub blob URLs for --full-path output.
recursivist.github ¶
Remote GitHub repository support.
Lets the visualize, export and compare commands accept a GitHub repository URL anywhere they accept a local directory. A repository is materialized by downloading its source archive from codeload.github.com and extracting it into a temporary directory, after which the existing local-directory scanner, renderers and exporters are reused unchanged.
The archive endpoint is used rather than the REST API on purpose: the REST API limits unauthenticated clients to 60 requests/hour (shared per public IP), which is easily exhausted, whereas archive downloads are not subject to that limit. Only the default branch is resolved through a lightweight, unlimited info/refs request when the caller did not pin a ref explicitly.
Because a hosted repository already reflects its ignore rules — files excluded by a .gitignore are simply absent — and because every file in a checkout shares the same Git status and effective modification time (the tip commit's), the --ignore-file, --git-status, --sort-by-git-status, --mtime and --sort-by-mtime options are not meaningful for a GitHub input and are skipped. The lines-of-code and size annotations are retained, since they are derived from the file contents. The --full-path option still applies, but instead of a filesystem path it shows each file's canonical GitHub blob URL.
Only the Python standard library is used. When a token is present in the GITHUB_TOKEN or GH_TOKEN environment variable it is sent with every request, which raises the rate limits and enables access to private repositories.
GitHubError ¶
Bases: Exception
Raised when a GitHub repository cannot be resolved or downloaded.
Carries a human-readable message suitable for surfacing directly to the user (e.g. an invalid URL, a missing repository, a rate-limit response, or a network/extraction failure).
Source code in recursivist/github.py
GitHubTarget dataclass ¶
A parsed reference to a GitHub repository or a subtree within one.
Attributes:
| Name | Type | Description |
|---|---|---|
owner | str | Repository owner (user or organization). |
repo | str | Repository name, without any trailing |
ref | str | None | The branch, tag, or commit the caller pinned via |
subpath | str | A forward-slashed path within the repository to treat as the root of the scan, or |
Source code in recursivist/github.py
display_name property ¶
A short label for the scanned root (subpath basename or repo name).
slug property ¶
The owner/repo identifier.
blob_url(ref, relpath) ¶
Return the canonical GitHub blob URL for a file.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ref | str | The concrete ref (branch, tag, or commit) to embed in the URL. | required |
relpath | str | The file's forward-slashed path relative to the repository root (already including any :attr: | required |
Returns:
| Type | Description |
|---|---|
str | A URL of the form |
str |
|
Source code in recursivist/github.py
RepoCheckout dataclass ¶
A materialized GitHub repository on the local filesystem.
Attributes:
| Name | Type | Description |
|---|---|---|
target | GitHubTarget | The :class: |
local_root | str | Absolute path to the directory to scan — the extracted repository root, or the requested subpath within it. |
ref | str | The concrete ref that was downloaded (the pinned ref, or the resolved default branch). |
root_name | str | The display name for the scanned root (the repository name, or the last segment of the subpath). |
Source code in recursivist/github.py
apply_github_urls(structure, checkout) ¶
Rewrite each file's display path to its GitHub blob URL, in place.
Used when --full-path is requested for a GitHub input: it walks structure and replaces every :class:~recursivist._models.FileEntry path with the file's canonical blob URL, so the unmodified renderers and exporters display GitHub URLs instead of temporary filesystem paths.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
structure | dict[str, Any] | A scanned structure produced by :func: | required |
checkout | RepoCheckout | The checkout the structure was scanned from, supplying the owner, repo, ref, and subpath used to build URLs. | required |
Returns:
| Type | Description |
|---|---|
dict[str, Any] | The same structure object, with file paths rewritten. |
Source code in recursivist/github.py
checkout_repository(target, token=None) ¶
Download and extract a GitHub repository into a temporary directory.
Resolves the ref (using the default branch when the target does not pin one), downloads the source archive, and safely extracts it. The extracted files are removed when the context exits.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target | GitHubTarget | The repository (and optional subtree) to check out. | required |
token | str | None | Optional GitHub token; defaults to :func: | None |
Yields:
| Name | Type | Description |
|---|---|---|
A | RepoCheckout | class: |
Raises:
| Type | Description |
|---|---|
GitHubError | If the repository cannot be resolved, downloaded, or extracted. |
Source code in recursivist/github.py
commit_shas_equal(sha1, sha2) ¶
Return whether two commit SHAs identify the same commit.
Handles abbreviated SHAs (as short as 7 characters, Git's conventional minimum) by treating one as a match for the other when it is a case-insensitive prefix. None never matches.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sha1 | str | None | The first commit SHA, or | required |
sha2 | str | None | The second commit SHA, or | required |
Returns:
| Type | Description |
|---|---|
bool |
|
Source code in recursivist/github.py
get_github_token() ¶
Return a GitHub token from the environment, if configured.
Looks up GITHUB_TOKEN first and then GH_TOKEN. When set, the token is sent with archive and ref requests, raising rate limits and permitting access to private repositories.
Returns:
| Type | Description |
|---|---|
str | None | The token string, or |
Source code in recursivist/github.py
is_github_url(text) ¶
Return whether text looks like a GitHub repository reference.
This is a cheap syntactic check used to decide whether an argument should be treated as a remote repository rather than a local path; it does not contact the network.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
text | str | The raw argument to test. | required |
Returns:
| Type | Description |
|---|---|
bool |
|
Source code in recursivist/github.py
parse_github_url(text) ¶
Parse a GitHub repository URL into a :class:GitHubTarget.
Accepts the common HTTPS forms (with or without scheme, www. or a trailing .git), an optional /tree/<ref>[/<subpath>] or /blob/<ref>/<subpath> selector, and the SSH form git@github.com:owner/repo.git.
When a /tree or /blob selector is present, the segment immediately after it is taken as the ref and everything beyond it as the subpath. Refs that themselves contain slashes (e.g. feature/x) therefore cannot be distinguished from a subpath by URL alone; pass such a repository without a selector, or pin the ref with a plain branch name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
text | str | The raw argument to parse. | required |
Returns:
| Type | Description |
|---|---|
GitHubTarget | None | The parsed :class: |
GitHubTarget | None | recognizable GitHub URL. |
Source code in recursivist/github.py
resolve_commit_shas(target, refs, token=None) ¶
Resolve each ref in refs to a commit SHA using one advertisement fetch.
A single info/refs request is made and reused for every ref, so this is cheap even for several refs on the same repository. Each entry is resolved as follows:
Noneresolves to the commit the default branch (HEAD) points at.- A branch or tag name resolves to its tip commit; annotated tags resolve to the commit they dereference to.
- A value that is not an advertised ref but looks like a commit SHA (7–40 hex characters) is returned as-is, lowercased, so explicit commit pins are supported.
- Anything else resolves to
None.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target | GitHubTarget | The repository to resolve against. | required |
refs | list[str | None] | The refs to resolve, in order. | required |
token | str | None | Optional GitHub token for private repositories. | None |
Returns:
| Type | Description |
|---|---|
list[str | None] | A list the same length as refs, each a lowercase commit SHA or |
list[str | None] |
|
Raises:
| Type | Description |
|---|---|
GitHubError | If the repository is missing, private, or unreachable. |
Source code in recursivist/github.py
resolve_default_branch(target, token=None) ¶
Resolve a repository's default branch without using the REST API.
Reads the symbolic HEAD reference from the repository's Git smart-HTTP info/refs advertisement, which is not subject to the REST API's unauthenticated rate limit.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target | GitHubTarget | The repository whose default branch is wanted. | required |
token | str | None | Optional GitHub token for private repositories. | None |
Returns:
| Type | Description |
|---|---|
str | The default branch name (e.g. |
Raises:
| Type | Description |
|---|---|
GitHubError | If the repository is missing or private without a valid token, or if the default branch cannot be determined. |
Source code in recursivist/github.py
Configuration¶
recursivist.config ¶
User configuration persistence.
Reads and writes recursivist's JSON settings file, resolving its location with Typer's platform-aware application directory. The only stored preference is currently the icon style.
get_config_path() ¶
Return the path to the configuration file, creating its directory.
The location is resolved with :func:typer.get_app_dir, so it follows each platform's convention for application data. The parent directory is created if it does not already exist.
Returns:
| Type | Description |
|---|---|
Path | Path to |
Source code in recursivist/config.py
load_config() ¶
Load the user configuration from disk.
Returns:
| Type | Description |
|---|---|
dict[str, Any] | The parsed configuration mapping, or the default |
dict[str, Any] |
|
dict[str, Any] | does not contain a JSON object. |
Source code in recursivist/config.py
save_config(config) ¶
Write the user configuration to disk as indented JSON.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
config | dict[str, Any] | Configuration mapping to persist. Overwrites any existing file at the configuration path. | required |
Source code in recursivist/config.py
Example: Custom Analysis Script¶
This script scans a directory with metrics enabled, exports two formats, and prints a summary:
import sys
from recursivist.scanner import get_directory_structure
from recursivist.exporters import get_exporter
from recursivist.flags import DisplayOptions
from recursivist._models import FileEntry
def analyze_directory(directory_path: str) -> None:
# Scan with lines-of-code and size tracking enabled
structure, extensions = get_directory_structure(
directory_path,
exclude_dirs=["node_modules", ".git", ".venv"],
exclude_extensions={".pyc", ".log", ".tmp"},
sort_by_loc=True,
sort_by_size=True,
)
# Describe how to sort and annotate, then export via the factory.
# Here: order by lines of code, annotating each file with LOC then size.
spec = DisplayOptions(sort_key="loc", metrics=("loc", "size"))
for fmt, out in (("md", "analysis.md"), ("json", "analysis.json")):
exporter = get_exporter(
fmt,
structure=structure,
root_name=directory_path,
spec=spec,
)
exporter.export(out)
print(f"Directory: {directory_path}")
print(f"Extensions: {sorted(extensions)}")
print(f"Total lines of code: {structure.get('_loc', 0)}")
print(f"Total size (bytes): {structure.get('_size', 0)}")
# Collect every file as a FileEntry and find the largest by LOC.
# FileEntry.coerce reads the canonical (name, path, loc, size, mtime)
# slots positionally, so it needs no knowledge of which flags were set.
def collect(struct: dict, path: str = "") -> list[tuple[str, FileEntry]]:
found: list[tuple[str, FileEntry]] = []
for entry in struct.get("_files", []):
fe = FileEntry.coerce(entry)
found.append((f"{path}/{fe.name}" if path else fe.name, fe))
for name, content in struct.items():
if isinstance(content, dict) and not name.startswith("_"):
found.extend(collect(content, f"{path}/{name}" if path else name))
return found
files = collect(structure)
files.sort(key=lambda item: item[1].loc, reverse=True)
print("\nTop 5 files by lines of code:")
for display_path, fe in files[:5]:
print(f" {fe.loc:>6} {display_path}")
if __name__ == "__main__":
analyze_directory(sys.argv[1] if len(sys.argv) > 1 else ".")
Extending Recursivist¶
The modular layout makes the common extension points clear:
- A new export format: subclass
BaseExporterin a new module underrecursivist/exporters/, implementexport, and register it in the_EXPORTERSmap inrecursivist/exporters/__init__.py. - Custom filtering: extend
should_excludeinrecursivist/filtering.py. - Custom rendering: build on
build_treeanddisplay_treeinrecursivist/tree.py. - A new metric: collect it in
get_directory_structure(recursivist/scanner.py), thread it throughFileEntry, register it inrecursivist/flags.py(so it resolves intoDisplayOptions), and surface it in the renderers, exporters, and CLI.
See the Development Guide for the full workflow.