From f5edaa37218d4b00f8ff0a44e872d8850750eb5a Mon Sep 17 00:00:00 2001 From: Colin Brown Date: Tue, 20 Jan 2026 10:33:23 +0000 Subject: [PATCH] feat: add output export functionality to CLI with --export flag --- README.md | 28 ++++++++++++++++++++++++++++ src/tickle/cli.py | 16 +++++++++++++++- src/tickle/output.py | 13 +++++++++---- tests/test_cli.py | 3 ++- 4 files changed, 54 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 3fdd625..e74f9fb 100644 --- a/README.md +++ b/README.md @@ -192,6 +192,34 @@ Combine options: ticklе /path/to/repo --markers TODO,FIXME --ignore "tests,venv" --sort marker --reverse --tree-collapse ``` +## Exporting Output for Printing + +You can export tickle's output to a plain text file for easy printing or sharing. Use the `--export` flag: + +```bash +tickle [options] --export output.txt +``` + +- The output will be saved to `output.txt` in plain text format. +- If the file already exists, it will be overwritten (a warning will be shown). +- After export, tickle will display a confirmation message with the file path. + +**Example:** + +```bash +tickle --markers TODO,FIXME --export my_tasks.txt +``` + +You can then print the file using your operating system's tools. For example, on Windows: + +```bash +notepad /p my_tasks.txt +``` + +Or open and print with any text editor. + +--- + ## Configuration Tired of typing the same flags every time? Configuration files let you set persistent defaults for ignore patterns, markers, output format, and more. Share configuration across your team via `pyproject.toml`, or maintain personal preferences in your user config. diff --git a/src/tickle/cli.py b/src/tickle/cli.py index 22051ec..6bee82b 100644 --- a/src/tickle/cli.py +++ b/src/tickle/cli.py @@ -86,9 +86,18 @@ def handle_scan_command(args): git_verbose=merged["git_verbose"], tree_collapse=merged["tree_collapse"], scan_directory=merged["path"], + no_color=bool(getattr(args, "export", None)), ) output = formatter.format(tasks) - print(output) + if hasattr(args, 'export') and args.export: + export_path = Path(args.export) + overwrite = export_path.exists() + export_path.write_text(output, encoding="utf-8") + if overwrite: + print(f"[tickle] Warning: Overwrote existing file: {export_path}") + print(f"[tickle] Output exported to: {export_path}") + else: + print(output) def main(): @@ -205,6 +214,11 @@ def main(): parser.add_argument( "--verbose", action="store_true", help="Show which config file is being used" ) + parser.add_argument( + "--export", + type=str, + help="Export output to a plain text file (overwrites if exists)" + ) parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}") args = parser.parse_args() diff --git a/src/tickle/output.py b/src/tickle/output.py index e34afd4..54dac36 100644 --- a/src/tickle/output.py +++ b/src/tickle/output.py @@ -136,17 +136,19 @@ class TreeFormatter(Formatter): "CHECKBOX": "green", } - def __init__(self, git_verbose: bool = False, show_details: bool = True, scan_directory: str = "."): + def __init__(self, git_verbose: bool = False, show_details: bool = True, scan_directory: str = ".", no_color: bool = False): """Initialize the tree formatter. Args: git_verbose: Whether to show full git commit info (hash and message) show_details: Whether to show individual task details (False shows counts only) scan_directory: Directory being scanned (used for root label) + no_color: Disable color/ANSI codes in output (for export) """ self.git_verbose = git_verbose self.show_details = show_details self.scan_directory = scan_directory + self.no_color = no_color def format(self, tasks: list[Task]) -> str: """Format tasks as a hierarchical tree.""" @@ -157,7 +159,7 @@ def format(self, tasks: list[Task]) -> str: tree = self._build_tree(tasks) # Render to string using Console - console = Console(legacy_windows=False) + console = Console(legacy_windows=False, color_system=None, force_terminal=not self.no_color, no_color=self.no_color) with console.capture() as capture: console.print(tree) @@ -354,7 +356,8 @@ def get_formatter( format_type: str, git_verbose: bool = False, tree_collapse: bool = False, - scan_directory: str = "." + scan_directory: str = ".", + no_color: bool = False, ) -> Formatter: """Factory function to get the appropriate formatter. @@ -363,6 +366,7 @@ def get_formatter( git_verbose: Whether to show full git commit info (hash and message) tree_collapse: Show only directory structure with counts (tree format only) scan_directory: Directory being scanned (tree format only) + no_color: Disable color/ANSI codes in output (for export) Returns: An instance of the appropriate Formatter subclass. @@ -378,7 +382,8 @@ def get_formatter( return TreeFormatter( git_verbose=git_verbose, show_details=not tree_collapse, - scan_directory=scan_directory + scan_directory=scan_directory, + no_color=no_color ) else: raise ValueError(f"Unknown format type: {format_type}") diff --git a/tests/test_cli.py b/tests/test_cli.py index b82b5a1..f662575 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -127,13 +127,14 @@ def test_main_combined_flags(self, sample_repo, capsys): def test_version_flag(self, capsys): """Test --version flag displays version.""" + from tickle import __version__ with mock.patch("sys.argv", ["tickle", "--version"]): with pytest.raises(SystemExit) as exc_info: main() assert exc_info.value.code == 0 captured = capsys.readouterr() - assert "0.1.0" in captured.out + assert __version__ in captured.out def test_sort_by_marker(self, capsys): """Test --sort marker flag."""