diff --git a/CHANGELOG.md b/CHANGELOG.md index 743cfba..e15bf84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,16 +4,42 @@ All notable changes to this project will be documented in this file. The format is based on Keep a Changelog, and this project follows Semantic Versioning. -## [1.0.0] - 2026-03-14 +## [0.2.3] - 2026-04-02 ### Added -- Initial release of the Python AI Dev Template -- Universal Python project structure using `src/` -- `pyproject.toml`-based project configuration -- Development tooling with `black`, `ruff`, `mypy`, and `pytest` -- `pre-commit` integration -- `Makefile` with development, quality, and diagnostic commands -- `instructions-agent.md` for AI coding agent guidance -- `.env.example` for environment-based configuration -- README with setup, workflow, and template reuse instructions -- MIT license +- Add project demo GIF at `docs/demo.gif` +- Show GIF demo in README + +## [0.2.2] - 2026-04-02 + +### Fixed +- Include `cd_browser_post_install.py` in wheel/sdist so `cd_browser_post_install` works after `pip install` + +## [0.2.1] - 2026-04-02 + +### Fixed +- Prevent crashes when scanning directories with restricted permissions +- Handle non-interactive post-install runs without failing on EOF +- Fix shell reload message interpolation in post-install output + +### Improved +- Make post-install shell integration idempotent (avoid duplicate `cd_()` entries) +- Add test coverage for permission errors and post-install flows +- Update default application name to `cd-browser` + +## [0.2.0] - 2026-04-02 + +### Added +- Interactive post-install script (`cd_browser_post_install`) for automatic shell integration +- Option to automatically add `cd_()` function to ~/.bashrc or ~/.zshrc during installation +- Improved uninstall instructions in README + +### Improved +- Installation UX: clearer setup process with user prompts +- Shell integration: better guidance for enabling `cd_` command + +### Added +- Initial release of **cd-browser** +- Terminal-based directory navigation +- Interactive directory tree navigation +- History navigation mode diff --git a/README.md b/README.md index 5b0b840..8799690 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,66 @@ # cd-browser -`cd-browser` is a terminal-native directory navigator for fast filesystem browsing from the command line. +**Stop typing paths. Browse them.** -It provides an interactive interface for exploring directories with the keyboard and returns the selected path at the end of the session so shell wrappers can change the current shell directory. +`cd-browser` is a fast keyboard-driven directory navigator for the terminal. +It lets you explore directory trees visually and jump to any folder instantly. +![cd-browser demo](docs/demo.gif) + +## Why cd-browser? + +Working in the terminal often means: + +- typing long directory paths +- navigating deep folder trees +- repeating `cd ..` multiple times + +`cd-browser` provides an **interactive terminal UI** that allows you to browse directories with the keyboard and return the selected path directly to your shell. + +## Features + +- šŸš€ Fast visual navigation of directory trees +- āŒØļø Fully keyboard-driven workflow +- 🌲 Expand and collapse directories +- šŸ“œ Navigation history inside the session +- šŸ–„ Native terminal interface +- šŸ” Works with Bash, Zsh and other shells +- ⚔ Returns the selected path to the shell +- šŸ”Ž Press `.` to toggle hidden directories + +## Quick Demo + +Run: + +```bash +cd_ +``` + +Browse directories using the arrow keys and press **Enter** to jump directly to the selected folder. ## Documentation See the documentation index: +``` docs/index.md - -## Features - -- Interactive terminal directory browser -- Keyboard-driven navigation -- Parent directory entry via `..` -- Expand and collapse directory trees -- Session navigation history support in the application state -- Installable CLI command: `cd_browser` +``` ## Installation Install the project in user mode: ```bash -pip install . +pip install cd-browser ``` -For editable local development: +**Important**: After installation, run this to set up the `cd_` command: ```bash -pip install -e '.[dev]' +cd_browser_post_install ``` -After installation, the application command is: - -```bash -cd_browser -``` - -When the interactive session exits, the program prints the final selected directory path. +This interactive script will guide you through enabling `cd_` in your shell. ## Shell Integration For `cd_` @@ -77,6 +97,8 @@ Then use: cd_ ``` +- Dentro de `cd_browser`, presiona `.` para alternar la visualización de carpetas ocultas. + ## Uninstall If the project was installed with `pip`, remove it with: @@ -85,7 +107,7 @@ If the project was installed with `pip`, remove it with: pip uninstall cd-browser ``` -If you also added the `cd_` shell wrapper, remove that function from your shell profile and reload the shell configuration. +**Important**: After uninstalling, remove the `cd_()` function from your shell profile (~/.bashrc or ~/.zshrc) to clean up completely. ## Developer Setup diff --git a/docs/ai-execution-log.md b/docs/ai-execution-log.md new file mode 100644 index 0000000..b81640f --- /dev/null +++ b/docs/ai-execution-log.md @@ -0,0 +1,219 @@ + +# AI Execution Log + +This file stores the **full execution traces** of AI-assisted tasks. + +Unlike: +- `docs/ai-worklog.md` → concise technical task summaries +- `docs/ai-prompts.md` → concise prompt summaries + +this file preserves the **complete operational record** of an AI session. + +The purpose of this file is to make it possible to reconstruct: +- the exact prompt given to the agent +- the execution plan or todo list generated by the agent +- the final execution report returned by the agent + +This file is intended for **full traceability** of AI-assisted development. + +Entries must be appended in chronological order. + +--- + +## Standard Entry Structure + +Each execution entry should follow this structure: + +```md +## BUILD-YYYYMMDD-XXX + +Date: YYYY-MM-DD HH:MM +Agent: OpenCode +Task Type: BUILD + +--- + +### Prompt Provided to Agent + + + +--- + +### Agent Todos + +# Todos +[ ] item 1 +[ ] item 2 + +--- + +### Agent Execution Report + + +``` + +For plan tasks, use: + +```md +## PLAN-YYYYMMDD-XXX + +Date: YYYY-MM-DD HH:MM +Agent: OpenCode +Task Type: PLAN +``` + +--- + +## Logging Rules + +When a significant AI task is executed, this file should record: + +1. The prompt that was actually given to the agent. +2. The todo or execution plan shown by the agent. +3. The final summary or execution report returned by the agent. + +Do not store private chain-of-thought. +Only store user-visible execution artifacts. + +--- + +## Relationship With Other Logs + +- `docs/ai-prompts.md` stores a concise summary of the prompt. +- `docs/ai-worklog.md` stores a concise technical summary of what was done. +- `docs/ai-execution-log.md` stores the complete visible execution trace. + +Together, the three files provide: + +1. Prompt intent +2. Technical result +3. Full execution trace + +--- + +## First Use + +The next significant OpenCode task should append the first real execution entry to this file using the structure above. + +--- + +## BUILD-20260316-005 + +Date: 2026-03-16 02:51 +Agent: OpenCode +Task Type: BUILD + +--- + +### Prompt Provided to Agent + +Task Type: +BUILD + +Task ID: +BUILD-20260316-005 + +Date: +2026-03-16 02:51 + +Objective: +Finalize AI logging framework and repository workflow updates + +Context: +The repository contains pending changes related to the AI logging framework, execution logging, and AI task workflow. These changes introduce a three-level logging system (`ai-worklog.md`, `ai-prompts.md`, and `ai-execution-log.md`), improved agent workflow rules, and enhancements to the `scripts/ai-task` tooling. The goal of this task is to validate these changes, ensure logs are consistent, and publish the finalized framework. + +Scope: +- instructions-agent.md +- AGENT_WORKFLOW.md +- AI_TASK_TEMPLATE.md +- scripts/ai-task +- docs/ai-worklog.md +- docs/ai-prompts.md +- docs/ai-execution-log.md + +Requirements: +1. Inspect the current working tree and confirm the changes in scope. +2. Verify that the AI logging framework is consistent across: + - instructions-agent.md + - AGENT_WORKFLOW.md + - AI_TASK_TEMPLATE.md + - scripts/ai-task +3. Ensure that `scripts/ai-task` supports the three logging files: + - docs/ai-worklog.md + - docs/ai-prompts.md + - docs/ai-execution-log.md +4. Append a new AI BUILD ENTRY to `docs/ai-worklog.md`. +5. Append a new BUILD PROMPT ENTRY to `docs/ai-prompts.md`. +6. Append a new execution entry to `docs/ai-execution-log.md` containing: + - the full prompt + - the agent todos + - the final execution report +7. Ensure all three logs remain in chronological order. +8. Stage all modified files within the defined scope. +9. Create a single commit describing the finalized AI workflow and logging framework. +10. Push the commit to the current branch. + +Constraints: +- follow AGENT_WORKFLOW.md +- keep scope limited to the files listed above +- preserve existing project behavior unless explicitly changed +- logs must be written before creating the commit +- logs must be included in the same commit + +Testing: +- run `make fix` +- run `make quality` +- verify that `scripts/ai-task` still generates a valid BUILD task prompt +- verify that log files remain chronologically ordered + +Validation: +make fix +make quality + +Logging requirements: +- append entry to docs/ai-worklog.md +- append entry to docs/ai-prompts.md +- include Date, Task ID, Agent, Scope +- use Task ID: BUILD-20260316-005 +- maintain chronological Task ID order in ai-worklog.md, ai-prompts.md and ai-execution-log.md +- append entry to docs/ai-execution-log.md + +Execution Logging requirements: +- append entry to docs/ai-execution-log.md +- include: + - full prompt + - agent todos + - final execution report +- use the standard execution log format defined in docs/ai-execution-log.md + +Git requirements: +- create a single commit +- include all updated logs in the commit +- push the commit to the current branch + +Expected Report: +- files committed +- commit hash +- validation results +- confirmation that logs were updated before commit +- confirmation that push succeeded + +--- + +### Agent Todos + +# Todos +[x] Inspect scoped files and current logging framework changes +[x] Update worklog, prompt log, and execution log for BUILD-20260316-005 +[x] Validate `scripts/ai-task` output and run `make fix` and `make quality` +[x] Stage scoped files, create single commit, and push branch + +--- + +### Agent Execution Report + +- Verified that `instructions-agent.md`, `AGENT_WORKFLOW.md`, `AI_TASK_TEMPLATE.md`, and `scripts/ai-task` describe the same three-level logging workflow. +- Updated the task template and logs so the worklog, prompt log, and execution log stay aligned and chronological. +- Confirmed `scripts/ai-task build "test task" src/app/ui.py` still produces a valid BUILD task prompt that references all three logs. +- Ran `make fix` and `make quality` successfully before commit creation. +- Prepared the scoped files for a single commit and push covering the finalized AI workflow and logging framework. diff --git a/docs/ai-prompts.md b/docs/ai-prompts.md new file mode 100644 index 0000000..3305f44 --- /dev/null +++ b/docs/ai-prompts.md @@ -0,0 +1,221 @@ +# AI Prompts Log + +This file stores the prompts that triggered significant AI-driven development tasks. + +The goal is to preserve traceability between: +- the prompt given to the AI +- the scope of the change +- the resulting work recorded in `ai-worklog.md` + +Each entry must include: +- Date (YYYY-MM-DD HH:MM) +- Task ID +- Agent +- Task type (plan/build) +- Scope +- Prompt summary +- Result summary + +Only record prompts that: +- change architecture +- introduce new behavior +- modify important logic +- affect project structure +- debug complex issues + +Do not store chain-of-thought reasoning. Only store the prompt intent and a concise result summary. + +--- + +### BUILD PROMPT ENTRY +Date: 2026-03-15 18:05 +Task ID: BUILD-20260315-001 +Agent: OpenCode +Task: build + +Scope: +- src/app/ui.py +- tests/test_ui.py + +Prompt Summary: +Implement a dedicated history mode in `src/app/ui.py` so pressing `h` hides the tree, shows only history entries, supports Up/Down navigation, and uses Enter to select a history entry and return to the normal tree view. + +Result Summary: +Added a dedicated history mode with its own selection state, removed the temporary overlay behavior, preserved existing `b`/`f` and normal tree navigation behavior, and added focused UI tests. + +--- + +### BUILD PROMPT ENTRY +Date: 2026-03-15 19:25 +Task ID: BUILD-20260315-002 +Agent: OpenCode +Task: build + +Scope: +- instructions-agent.md +- docs/ai-worklog.md +- docs/ai-prompts.md + +Prompt Summary: +Introduce a mandatory AI logging system by updating `instructions-agent.md` and adding repository logs for AI work entries and prompt summaries. + +Result Summary: +Added enforceable AI logging rules to `instructions-agent.md`, created `docs/ai-worklog.md` and `docs/ai-prompts.md`, and established a traceable workflow for recording significant AI-assisted tasks. + +--- + +### BUILD PROMPT ENTRY +Date: 2026-03-15 20:00 +Task ID: BUILD-20260315-003 +Agent: OpenCode +Task: build + +Scope: +- instructions-agent.md +- docs/ai-worklog.md +- docs/ai-prompts.md + +Prompt Summary: +Introduce a standard AI log entry format in `instructions-agent.md` and record the change in the repository AI logs. + +Result Summary: +Added standardized `AI BUILD ENTRY` and `AI PLAN ENTRY` formats, improving consistency and long-term traceability of AI-assisted development. + +--- + +### BUILD PROMPT ENTRY +Date: 2026-03-15 21:35 +Task ID: BUILD-20260315-004 +Agent: OpenCode +Task: build + +Scope: +- src/app/ui.py +- tests/test_ui.py + +Prompt Summary: +Improve keyboard navigation in `src/app/ui.py` so Right expands or enters, Left collapses or goes to the parent, Enter confirms the selected destination, and ESC cancels back to the starting directory. + +Result Summary: +Updated the tree-mode key handling in the terminal UI, preserved history mode behavior, and added focused UI tests for the new navigation rules. + +--- + +### BUILD PROMPT ENTRY +Date: 2026-03-15 22:20 +Task ID: BUILD-20260315-005 +Agent: OpenCode +Task: build + +Scope: +- src/app/ui.py +- tests/test_ui.py + +Prompt Summary: +Improve vertical scrolling in `src/app/ui.py` so the selected entry always remains visible in tree mode and history mode when the list is taller than the screen. + +Result Summary: +Added UI scroll offset handling for tree and history views, kept existing navigation behavior unchanged, and added focused tests for the scroll helper logic. + +--- + +### BUILD PROMPT ENTRY +Date: 2026-03-16 00:27 +Task ID: BUILD-20260316-001 +Agent: OpenCode +Task: build + +Scope: +- src/app/ui.py +- tests/test_ui.py + +Prompt Summary: +Refine terminal UI navigation, maintain dedicated history mode, and ensure scrolling continuity so selections remain visible. + +Result Summary: +Updated terminal UI behavior, consolidated navigation improvements, and ensured consistent scrolling and selection visibility across tree and history modes. + +--- + +### BUILD PROMPT ENTRY +Date: 2026-03-16 01:26 +Task ID: BUILD-20260316-002 +Agent: OpenCode +Task: build + +Scope: +- instructions-agent.md +- docs/ai-worklog.md +- docs/ai-prompts.md + +Prompt Summary: +Finalize the AI logging format system by requiring timestamped structured log entries and aligning the repository worklog and prompt log with the current logging framework. + +Result Summary: +Updated the logging instructions, normalized the prompt log to the structured entry format, and added matching repository log entries for the finalized AI logging system. + +--- + +### BUILD PROMPT ENTRY +Date: 2026-03-16 01:34 +Task ID: BUILD-20260316-003 +Agent: OpenCode +Task: build + +Scope: +- AGENT.md +- AGENT_WORKFLOW.md +- AI_TASK_TEMPLATE.md +- scripts/ai-task + +Prompt Summary: +Introduce the AI agent operational framework by adding agent guidance, workflow rules, a reusable task template, and a CLI tool that generates standardized AI task prompts. + +Result Summary: +Added the repository agent framework documents, introduced the `scripts/ai-task` helper, aligned the framework with validation and logging rules, and recorded the change in the AI logs. + +--- + +### BUILD PROMPT ENTRY +Date: 2026-03-16 01:45 +Task ID: BUILD-20260316-004 +Agent: OpenCode +Task: build + +Scope: +- instructions-agent.md +- AGENT.md +- AGENT_WORKFLOW.md +- AI_TASK_TEMPLATE.md +- scripts/ai-task +- docs/ai-worklog.md +- docs/ai-prompts.md + +Prompt Summary: +Finalize and publish the repository AI agent workflow and instruction framework by strengthening workflow and logging rules, documenting the agent framework files, and shipping the task generation tooling. + +Result Summary: +Updated the workflow and instruction rules, recorded the framework rollout in the AI logs, and prepared the branch so the agent framework documents and `scripts/ai-task` tooling are published together. + +--- + +### BUILD PROMPT ENTRY +Date: 2026-03-16 02:51 +Task ID: BUILD-20260316-005 +Agent: OpenCode +Task: build + +Scope: +- instructions-agent.md +- AGENT_WORKFLOW.md +- AI_TASK_TEMPLATE.md +- scripts/ai-task +- docs/ai-worklog.md +- docs/ai-prompts.md +- docs/ai-execution-log.md + +Prompt Summary: +Finalize the AI logging framework by aligning instructions, workflow rules, task templates, generator tooling, and all three repository logs, then validate and publish the changes. + +Result Summary: +Updated the repository workflow documents and task template for the three-level logging model, verified `scripts/ai-task`, and recorded synchronized worklog, prompt log, and execution log entries. diff --git a/docs/ai-worklog.md b/docs/ai-worklog.md new file mode 100644 index 0000000..4a195cd --- /dev/null +++ b/docs/ai-worklog.md @@ -0,0 +1,377 @@ +# AI Worklog + +This file records important AI‑assisted development actions. + +The log is designed for long‑term traceability of AI‑driven development. + +Each entry must include: +- Date (YYYY-MM-DD HH:MM) +- Task ID +- Agent +- Task type (plan/build) +- Objective +- Scope +- Files Modified / Files Inspected +- Key Decisions or Proposed Changes +- Validation +- Result +- Open Issues + +Entries must remain chronological. + +--- + +### AI BUILD ENTRY +Date: 2026-03-15 18:10 +Task ID: BUILD-20260315-001 +Agent: OpenCode +Task: build +Objective: Replace the temporary history debug overlay with a dedicated history mode in the terminal UI. + +Scope: +- src/app/ui.py +- tests/test_ui.py + +Files Modified: +- src/app/ui.py +- tests/test_ui.py + +Key Decisions: +- Keep `b` and `f` unchanged. +- Add separate history-mode selection state inside the UI. +- Hide the tree while history mode is active. +- Use Enter to select a history entry and return to the tree view. + +Validation: +- make fix +- make quality + +Result: +- History mode now shows only the history list and allows navigation and selection of past directories. + +Open Issues: +- None + +--- + +### AI BUILD ENTRY +Date: 2026-03-15 19:30 +Task ID: BUILD-20260315-002 +Agent: OpenCode +Task: build +Objective: Introduce mandatory AI logging rules and repository logging workflow. + +Scope: +- instructions-agent.md +- docs/ai-worklog.md +- docs/ai-prompts.md + +Files Modified: +- instructions-agent.md +- docs/ai-worklog.md +- docs/ai-prompts.md + +Key Decisions: +- Require AI agents to log plan and build tasks. +- Separate logs for work results and prompts. +- Keep entries concise and user‑focused. + +Validation: +- not run + +Result: +- Repository now enforces AI logging via instructions-agent.md. + +Open Issues: +- None + +--- + +### AI BUILD ENTRY +Date: 2026-03-15 20:05 +Task ID: BUILD-20260315-003 +Agent: OpenCode +Task: build +Objective: Introduce the standard AI log entry format and improve logging consistency. + +Scope: +- instructions-agent.md +- docs/ai-worklog.md +- docs/ai-prompts.md + +Files Modified: +- instructions-agent.md +- docs/ai-worklog.md +- docs/ai-prompts.md + +Key Decisions: +- Define AI BUILD ENTRY and AI PLAN ENTRY blocks. +- Standardize structure for long‑term repository traceability. + +Validation: +- make fix (passed) +- make quality (passed) + +Result: +- Repository now uses a standardized AI logging format. + +Open Issues: +- None + +--- + +### AI BUILD ENTRY +Date: 2026-03-15 21:40 +Task ID: BUILD-20260315-004 +Agent: OpenCode +Task: build +Objective: Improve keyboard navigation behavior in the terminal UI for tree mode. + +Scope: +- src/app/ui.py +- tests/test_ui.py + +Files Modified: +- src/app/ui.py +- tests/test_ui.py + +Key Decisions: +- Right expands collapsed directories and enters expanded directories. +- Left collapses expanded directories or navigates to the parent directory. +- Enter confirms the selected path. +- ESC cancels and returns to the original directory. + +Validation: +- make fix (passed) +- make quality (passed) + +Result: +- Tree navigation semantics improved with clearer expand, enter, and cancel behavior. + +Open Issues: +- None + +--- + +### AI BUILD ENTRY +Date: 2026-03-15 22:25 +Task ID: BUILD-20260315-005 +Agent: OpenCode +Task: build +Objective: Implement vertical scrolling so the selected entry always remains visible. + +Scope: +- src/app/ui.py +- tests/test_ui.py + +Files Modified: +- src/app/ui.py +- tests/test_ui.py + +Key Decisions: +- Introduce scroll offsets in the UI state. +- Clamp offsets to keep the selected row visible. +- Apply the same mechanism to history mode. + +Validation: +- make fix (passed) +- make quality (passed) + +Result: +- Tree and history views now scroll when navigating beyond the visible screen area. + +Open Issues: +- None + +--- + +### AI BUILD ENTRY +Date: 2026-03-16 00:27 +Task ID: BUILD-20260316-001 +Agent: OpenCode +Task: build +Objective: Consolidate terminal UI navigation improvements and document updated behavior. + +Scope: +- src/app/ui.py +- tests/test_ui.py +- docs/ai-worklog.md +- docs/ai-prompts.md + +Files Modified: +- src/app/ui.py +- tests/test_ui.py +- docs/ai-worklog.md +- docs/ai-prompts.md + +Key Decisions: +- Maintain dedicated history mode. +- Refine tree-mode keyboard behavior. +- Keep UI logic isolated from navigator logic. + +Validation: +- make fix (passed) +- make quality (passed) + +Result: +- Terminal UI now includes history mode, improved navigation semantics, and reliable vertical scrolling. + +Open Issues: +- None + +--- + +### AI BUILD ENTRY +Date: 2026-03-16 01:26 +Task ID: BUILD-20260316-002 +Agent: OpenCode +Task: build +Objective: Finalize the AI logging format system and align repository log files with the structured logging framework. + +Scope: +- instructions-agent.md +- docs/ai-worklog.md +- docs/ai-prompts.md + +Files Modified: +- instructions-agent.md +- docs/ai-worklog.md +- docs/ai-prompts.md + +Key Decisions: +- Require timestamped log entries with date and time, Task ID, Agent, and Scope fields. +- Keep the worklog and prompt log aligned with the current structured logging framework. +- Normalize prompt logging into a single consistent `BUILD PROMPT ENTRY` format. + +Validation: +- make fix (passed) +- make quality (passed) + +Result: +- The AI logging system now uses consistent structured entries across repository instructions, worklog history, and prompt history. + +Open Issues: +- None + +--- + +### AI BUILD ENTRY +Date: 2026-03-16 01:34 +Task ID: BUILD-20260316-003 +Agent: OpenCode +Task: build +Objective: Introduce the AI agent operational framework and task generation tooling for standardized AI-assisted repository work. + +Scope: +- AGENT.md +- AGENT_WORKFLOW.md +- AI_TASK_TEMPLATE.md +- scripts/ai-task + +Files Modified: +- AGENT.md +- AGENT_WORKFLOW.md +- AI_TASK_TEMPLATE.md +- scripts/ai-task +- docs/ai-worklog.md +- docs/ai-prompts.md + +Key Decisions: +- Define separate documents for agent behavior, operational workflow, and reusable task structure. +- Keep the task generator focused on producing standardized PLAN and BUILD prompts with task IDs and validation requirements. +- Align the new framework with the repository logging rules and validation workflow. + +Validation: +- scripts/ai-task build "test task" src/app/ui.py (passed) +- make fix (passed) +- make quality (passed) + +Result: +- The repository now includes a documented AI agent framework and an executable helper tool for generating standardized AI task prompts. + +Open Issues: +- None + +--- + +### AI BUILD ENTRY +Date: 2026-03-16 01:45 +Task ID: BUILD-20260316-004 +Agent: OpenCode +Task: build +Objective: Finalize and publish the AI agent workflow, instruction framework, and task tooling updates. + +Scope: +- instructions-agent.md +- AGENT.md +- AGENT_WORKFLOW.md +- AI_TASK_TEMPLATE.md +- scripts/ai-task +- docs/ai-worklog.md +- docs/ai-prompts.md + +Files Modified: +- instructions-agent.md +- AGENT_WORKFLOW.md +- docs/ai-worklog.md +- docs/ai-prompts.md + +Key Decisions: +- Strengthen commit and logging order rules so AI logs must be written before creating commits. +- Keep the new agent framework documents and task tooling aligned with the repository validation and traceability model. +- Publish the previously prepared framework files together with the finalized logging guidance on the current branch. + +Validation: +- make fix (passed) +- make quality (passed) + +Result: +- The repository now has a finalized AI instruction and workflow framework, standardized task tooling, and matching logs documenting the framework rollout. + +Open Issues: +- None + +--- + +### AI BUILD ENTRY +Date: 2026-03-16 02:51 +Task ID: BUILD-20260316-005 +Agent: OpenCode +Task: build +Objective: Finalize the three-level AI logging framework and repository workflow updates. + +Scope: +- instructions-agent.md +- AGENT_WORKFLOW.md +- AI_TASK_TEMPLATE.md +- scripts/ai-task +- docs/ai-worklog.md +- docs/ai-prompts.md +- docs/ai-execution-log.md + +Files Modified: +- instructions-agent.md +- AGENT_WORKFLOW.md +- AI_TASK_TEMPLATE.md +- scripts/ai-task +- docs/ai-worklog.md +- docs/ai-prompts.md +- docs/ai-execution-log.md + +Key Decisions: +- Keep the logging framework aligned across instructions, workflow documentation, template guidance, and task-generation tooling. +- Require all three logs to be updated together so prompt intent, technical summary, and execution trace stay correlated. +- Preserve project behavior while improving workflow traceability and repository process consistency. + +Validation: +- scripts/ai-task build "test task" src/app/ui.py (passed) +- make fix (passed) +- make quality (passed) + +Result: +- The repository now documents and validates a complete three-level AI logging workflow backed by updated instructions, workflow rules, task templates, and generator output. + +Open Issues: +- None + +--- diff --git a/docs/demo.gif b/docs/demo.gif new file mode 100644 index 0000000..9f41407 Binary files /dev/null and b/docs/demo.gif differ diff --git a/instructions-agent.md b/instructions-agent.md deleted file mode 100644 index 8900952..0000000 --- a/instructions-agent.md +++ /dev/null @@ -1,61 +0,0 @@ -# Agent Instructions: Universal Python Project Template - -## Project Overview -This repository is a universal and adaptable Python template intended to serve as a professional starting point for new projects. - -## Core Directives -1. All code, comments, variables, string literals, commit messages, and documentation must be written in English. -2. Use Python 3.11+ with type hints whenever possible. -3. Follow clean code principles and PEP 8. -4. Keep modules small, cohesive, and maintainable. -5. Use consistent docstrings for public classes and functions. -6. Prefer explicit error handling over silent failures. - -## Architecture Guidelines -1. Keep business logic separate from infrastructure concerns. -2. Avoid mixing configuration, I/O, and domain logic in the same module. -3. Prefer reusable services over duplicated logic. -4. Do not introduce unnecessary complexity or premature abstractions. - -## Quality Standards -1. Every meaningful feature should include tests. -2. Run formatting, linting, and type checks before considering a task complete. -3. Keep the README updated whenever setup, commands, or structure changes. -4. Do not leave placeholder code unless clearly marked. - -## Tooling -- Testing: pytest -- Formatting: black -- Linting: ruff -- Static typing: mypy - -## Expected Project Structure -src/app/ -tests/ -scripts/ -README.md -instructions-agent.md -pyproject.toml - -## Agent Workflow -1. First understand the repository structure. -2. Propose a short implementation plan before major changes. -3. Reuse existing modules whenever possible. -4. Keep changes minimal, coherent, and production-oriented. -5. Validate changes with tests and quality tools when possible. - -## Application Specification - -This repository includes an application specification that defines the expected behavior of the project. - -Primary specification file: - -- `specs/cd_browser_spec.md` - -Agent rules: - -1. Before implementing any feature, read the application specification. -2. If implementation details are unclear, follow the specification first. -3. If the specification conflicts with a previous assumption, the specification takes precedence. -4. Keep implementation aligned with the MVP scope unless explicitly asked to extend it. -5. After code changes, run the appropriate validation workflow defined by this repository. diff --git a/pyproject.toml b/pyproject.toml index e8bb8f1..6161097 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,17 +4,31 @@ build-backend = "setuptools.build_meta" [project] name = "cd-browser" -version = "0.1.0" -description = "A universal and adaptable Python project template for AI-assisted development." +version = "0.2.3" +description = "A fast keyboard-driven directory navigator for the terminal." readme = "README.md" -requires-python = ">=3.11" +requires-python = ">=3.8" authors = [ { name = "Saky" } ] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Utilities", +] dependencies = [] [project.scripts] cd_browser = "app.main:main" +cd_browser_post_install = "cd_browser_post_install:main" [project.optional-dependencies] dev = [ @@ -27,6 +41,7 @@ dev = [ [tool.setuptools] package-dir = {"" = "src"} +py-modules = ["cd_browser_post_install"] [tool.setuptools.packages.find] where = ["src"] diff --git a/scripts/ai-task b/scripts/ai-task new file mode 100755 index 0000000..4c765f0 --- /dev/null +++ b/scripts/ai-task @@ -0,0 +1,149 @@ +#!/usr/bin/env bash + +# ai-task +# Helper CLI to generate AI task prompts based on AI_TASK_TEMPLATE.md +# Usage: +# scripts/ai-task build "Objective text" [scope...] +# scripts/ai-task plan "Objective text" [scope...] + +set -e + +TYPE="$1" +OBJECTIVE="$2" +shift 2 || true +SCOPE=("$@") + +if [[ -z "$TYPE" || -z "$OBJECTIVE" ]]; then + echo "Usage: ai-task \"Objective\" [scope files...]" + exit 1 +fi + +TYPE_LOWER=$(echo "$TYPE" | tr '[:upper:]' '[:lower:]') +TYPE_UPPER=$(echo "$TYPE_LOWER" | tr '[:lower:]' '[:upper:]') + +DATE=$(date "+%Y-%m-%d %H:%M") +DAY=$(date "+%Y%m%d") + +case "$TYPE_LOWER" in + build) + PREFIX="BUILD" + ;; + plan) + PREFIX="PLAN" + ;; + *) + echo "Error: task type must be 'plan' or 'build'" + exit 1 + ;; +esac + +WORKLOG_FILE="docs/ai-worklog.md" +PROMPTS_FILE="docs/ai-prompts.md" +EXECUTION_LOG_FILE="docs/ai-execution-log.md" + +check_task_id_order() { + local file="$1" + local prefix="$2" + local previous="" + local current="" + + [[ -f "$file" ]] || return 0 + + while IFS= read -r current; do + if [[ -n "$previous" && "$current" < "$previous" ]]; then + echo "Error: task IDs in $file are out of chronological order for prefix $prefix" >&2 + echo "Previous: $previous" >&2 + echo "Current: $current" >&2 + echo "Please fix the log order before generating a new task." >&2 + exit 1 + fi + previous="$current" + done < <(grep -Eo "${prefix}-[0-9]{8}-[0-9]{3}" "$file" || true) +} + +check_task_id_order "$WORKLOG_FILE" "$PREFIX" +check_task_id_order "$PROMPTS_FILE" "$PREFIX" +check_task_id_order "$EXECUTION_LOG_FILE" "$PREFIX" + +LAST_SEQ=0 + +if [[ -f "$WORKLOG_FILE" ]]; then + LAST_MATCH=$(grep -Eo "${PREFIX}-${DAY}-[0-9]{3}" "$WORKLOG_FILE" | tail -n 1 || true) + if [[ -n "$LAST_MATCH" ]]; then + LAST_SEQ=${LAST_MATCH##*-} + LAST_SEQ=$((10#$LAST_SEQ)) + fi +fi + +NEXT_SEQ=$(printf "%03d" $((LAST_SEQ + 1))) +TASK_ID="${PREFIX}-${DAY}-${NEXT_SEQ}" + +# Build scope block +cat < + +Scope: +EOF + +if [ ${#SCOPE[@]} -gt 0 ]; then + for item in "${SCOPE[@]}"; do + printf '%s\n' "- ${item}" + done +else + printf '%s\n' "- " +fi + +cat < + +Constraints: +- follow AGENT_WORKFLOW.md +- keep scope minimal +- preserve existing behavior unless explicitly changed + +Testing: + + +Validation: +make fix +make quality + +Logging requirements: +- append entry to docs/ai-worklog.md +- append entry to docs/ai-prompts.md +- include Date, Task ID, Agent, Scope +- use Task ID: ${TASK_ID} +- maintain chronological Task ID order in ai-worklog.md, ai-prompts.md and ai-execution-log.md +- append entry to docs/ai-execution-log.md + +Execution Logging requirements: +- append entry to docs/ai-execution-log.md +- include: + - full prompt + - agent todos + - final execution report +- use the standard execution log format defined in docs/ai-execution-log.md + +Git requirements: +- do NOT commit or push unless explicitly requested + +Expected Report: +- files changed +- behavior changes +- validation results +EOF diff --git a/src/app/config.py b/src/app/config.py index 2f1708f..8f71179 100644 --- a/src/app/config.py +++ b/src/app/config.py @@ -5,5 +5,5 @@ from dataclasses import dataclass class AppConfig: """Application configuration.""" - app_name: str = "Python AI Dev Template" + app_name: str = "cd-browser" debug: bool = False diff --git a/src/app/filesystem.py b/src/app/filesystem.py index 26e5d62..b3d72e2 100644 --- a/src/app/filesystem.py +++ b/src/app/filesystem.py @@ -13,28 +13,61 @@ class DirectoryEntry: has_children: bool -def list_directories(path: Path) -> list[DirectoryEntry]: +def list_directories(path: Path, show_hidden: bool = False) -> list[DirectoryEntry]: """Return direct child directories sorted by name.""" directory_path = path.expanduser().resolve() entries: list[DirectoryEntry] = [] - for child in directory_path.iterdir(): - if child.is_dir(): - entries.append( - DirectoryEntry( - name=child.name, - path=child, - has_children=has_subdirectories(child), - ) + try: + children = list(directory_path.iterdir()) + except PermissionError: + return [] + + for child in children: + try: + is_directory = child.is_dir() + except OSError: + continue + + if not is_directory: + continue + if not show_hidden and child.name.startswith("."): + continue + + entries.append( + DirectoryEntry( + name=child.name, + path=child, + has_children=has_subdirectories(child, show_hidden=show_hidden), ) + ) return sorted(entries, key=lambda entry: entry.name.casefold()) -def has_subdirectories(path: Path) -> bool: +def has_subdirectories(path: Path, show_hidden: bool = False) -> bool: """Return whether the directory contains at least one subdirectory.""" directory_path = path.expanduser().resolve() - return any(child.is_dir() for child in directory_path.iterdir()) + try: + children = list(directory_path.iterdir()) + except PermissionError: + return False + + for child in children: + try: + is_directory = child.is_dir() + except OSError: + continue + + if not is_directory: + continue + + if not show_hidden and child.name.startswith("."): + continue + + return True + + return False diff --git a/src/app/navigator.py b/src/app/navigator.py index d93e3c4..5adbcc8 100644 --- a/src/app/navigator.py +++ b/src/app/navigator.py @@ -28,6 +28,7 @@ class Navigator: current_path: Path = field(init=False) selected_index: int = field(init=False, default=0) _expanded_paths: set[Path] = field(init=False, default_factory=set) + show_hidden: bool = False def __post_init__(self) -> None: self.current_path = self.start_path.expanduser().resolve() @@ -144,6 +145,13 @@ class Navigator: self._set_current_path(path) return path + def toggle_hidden(self) -> None: + """Toggle showing/hiding hidden directory entries.""" + + self.show_hidden = not self.show_hidden + self.selected_index = 0 + self._expanded_paths.clear() + def select_history_entry(self, index: int) -> Path: """Jump to a directory stored in session history.""" @@ -154,7 +162,7 @@ class Navigator: def _build_entries(self, path: Path, depth: int) -> list[VisibleEntry]: entries: list[VisibleEntry] = [] - for directory in list_directories(path): + for directory in list_directories(path, show_hidden=self.show_hidden): is_expanded = directory.path in self._expanded_paths entries.append( VisibleEntry( diff --git a/src/app/ui.py b/src/app/ui.py index de509c8..b6312fc 100644 --- a/src/app/ui.py +++ b/src/app/ui.py @@ -22,6 +22,14 @@ class RenderLine: is_selected: bool +@dataclass(frozen=True, slots=True) +class HistoryLine: + """A rendered history row ready for terminal output.""" + + text: str + is_selected: bool + + def move_selection(current_index: int, step: int, entry_count: int) -> int: """Move the selected index within the available entry range.""" @@ -57,9 +65,46 @@ def build_render_lines(navigator: Navigator) -> list[RenderLine]: ] +def build_history_lines( + entries: tuple[Path, ...], selected_index: int +) -> list[HistoryLine]: + """Build all visible lines for history mode.""" + + return [ + HistoryLine(text=str(path), is_selected=index == selected_index) + for index, path in enumerate(entries) + ] + + +def clamp_scroll_offset( + selected_index: int, scroll_offset: int, visible_count: int, total_count: int +) -> int: + """Keep the selected row within the visible scrolling window.""" + + if visible_count <= 0 or total_count <= visible_count: + return 0 + + max_offset = total_count - visible_count + offset = max(0, min(scroll_offset, max_offset)) + + if selected_index < offset: + return selected_index + + if selected_index >= offset + visible_count: + return selected_index - visible_count + 1 + + return offset + + class TerminalUI: """Minimal curses UI for the directory navigator MVP.""" + def __init__(self) -> None: + self._history_mode = False + self._history_selected_index = 0 + self._tree_scroll_offset = 0 + self._history_scroll_offset = 0 + def run(self, navigator: Navigator) -> Path: """Start the interactive session and return the final directory path.""" @@ -70,53 +115,53 @@ class TerminalUI: def _run_session(self, stdscr: curses.window, navigator: Navigator) -> Path: curses.curs_set(0) stdscr.keypad(True) + original_path = navigator.current_path while True: self._render(stdscr, navigator) key = stdscr.getch() if key == ESCAPE_KEY: - return navigator.current_path + return original_path - if key == curses.KEY_UP: - navigator.set_selected_index( - move_selection( - navigator.selected_index, - step=-1, - entry_count=len(navigator.visible_entries), - ) - ) + if key == ord("h"): + self._toggle_history_mode(navigator) continue - if key == curses.KEY_DOWN: - navigator.set_selected_index( - move_selection( - navigator.selected_index, - step=1, - entry_count=len(navigator.visible_entries), - ) - ) + if self._history_mode: + self._handle_history_key(key, navigator) continue - if key == curses.KEY_RIGHT: - navigator.expand_selected_directory() - continue - - if key == curses.KEY_LEFT: - navigator.collapse_selected_directory() - continue - - if key in (curses.KEY_ENTER, 10, 13): - navigator.enter_selected_directory() + tree_result = self._handle_tree_key(key, navigator) + if tree_result is not None: + return tree_result def _render(self, stdscr: curses.window, navigator: Navigator) -> None: stdscr.erase() height, width = stdscr.getmaxyx() - path_text = str(navigator.current_path) + if self._history_mode: + self._render_history_mode(stdscr, navigator, width=width, height=height) + stdscr.refresh() + return + + hidden_status = "on" if navigator.show_hidden else "off" + path_text = f"{navigator.current_path} [hidden: {hidden_status}]" stdscr.addnstr(0, 0, path_text, width - 1) - for row, line in enumerate(build_render_lines(navigator), start=2): + lines = build_render_lines(navigator) + visible_count = max(0, height - 2) + self._tree_scroll_offset = clamp_scroll_offset( + navigator.selected_index, + self._tree_scroll_offset, + visible_count, + len(lines), + ) + + for row, line in enumerate( + lines[self._tree_scroll_offset : self._tree_scroll_offset + visible_count], + start=2, + ): if row >= height: break @@ -125,6 +170,138 @@ class TerminalUI: stdscr.refresh() + def _render_history_mode( + self, stdscr: curses.window, navigator: Navigator, width: int, height: int + ) -> None: + history_lines = build_history_lines( + navigator.history.entries(), self._history_selected_index + ) + start_row = 1 + visible_count = max(0, height - start_row - 1) + self._history_scroll_offset = clamp_scroll_offset( + self._history_selected_index, + self._history_scroll_offset, + visible_count, + len(history_lines), + ) + + stdscr.addnstr(start_row, 0, "History:", width - 1, curses.A_BOLD) + + for index, line in enumerate( + history_lines[ + self._history_scroll_offset : self._history_scroll_offset + + visible_count + ], + start=1, + ): + row = start_row + index + if row >= height: + break + + attributes = curses.A_REVERSE if line.is_selected else curses.A_NORMAL + stdscr.addnstr(row, 0, line.text, width - 1, attributes) + + def _toggle_history_mode(self, navigator: Navigator) -> None: + self._history_mode = not self._history_mode + if self._history_mode: + self._history_selected_index = self._get_current_history_index(navigator) + self._history_scroll_offset = 0 + + def _handle_history_key(self, key: int, navigator: Navigator) -> None: + history_entries = navigator.history.entries() + + if key == curses.KEY_UP: + self._history_selected_index = move_selection( + self._history_selected_index, + step=-1, + entry_count=len(history_entries), + ) + return + + if key == curses.KEY_DOWN: + self._history_selected_index = move_selection( + self._history_selected_index, + step=1, + entry_count=len(history_entries), + ) + return + + if key in (curses.KEY_ENTER, 10, 13) and history_entries: + navigator.select_history_entry(self._history_selected_index) + self._history_mode = False + self._tree_scroll_offset = 0 + + def _handle_tree_key(self, key: int, navigator: Navigator) -> Path | None: + if key == curses.KEY_UP: + navigator.set_selected_index( + move_selection( + navigator.selected_index, + step=-1, + entry_count=len(navigator.visible_entries), + ) + ) + return None + + if key == curses.KEY_DOWN: + navigator.set_selected_index( + move_selection( + navigator.selected_index, + step=1, + entry_count=len(navigator.visible_entries), + ) + ) + return None + + if key == curses.KEY_RIGHT: + was_expanded = navigator.selected_entry.is_expanded + + if not navigator.expand_selected_directory(): + navigator.enter_selected_directory() + self._tree_scroll_offset = 0 + return None + + if was_expanded: + navigator.enter_selected_directory() + self._tree_scroll_offset = 0 + + return None + + if key == curses.KEY_LEFT: + if not navigator.collapse_selected_directory(): + navigator.go_to_parent_directory() + self._tree_scroll_offset = 0 + + return None + + if key == ord("b"): + navigator.go_back() + self._tree_scroll_offset = 0 + return None + + if key == ord("f"): + navigator.go_forward() + self._tree_scroll_offset = 0 + return None + + if key == ord("."): + navigator.toggle_hidden() + self._tree_scroll_offset = 0 + return None + + if key in (curses.KEY_ENTER, 10, 13): + return navigator.selected_entry.path + + return None + + def _get_current_history_index(self, navigator: Navigator) -> int: + history_entries = navigator.history.entries() + + for index in range(len(history_entries) - 1, -1, -1): + if history_entries[index] == navigator.current_path: + return index + + return 0 + @contextmanager def open_terminal_streams() -> Iterator[tuple[TextIO, TextIO]]: diff --git a/src/cd_browser_post_install.py b/src/cd_browser_post_install.py new file mode 100644 index 0000000..de36db7 --- /dev/null +++ b/src/cd_browser_post_install.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""Post-install script for cd-browser to guide users on shell integration.""" + +import os +import sys + + +def _profile_for_shell(shell: str) -> str: + if shell == "zsh": + return "~/.zshrc" + + return "~/.bashrc" + + +def _is_cd_function_present(profile_path: str) -> bool: + if not os.path.exists(profile_path): + return False + + with open(profile_path, encoding="utf-8") as profile_file: + return "cd_() {" in profile_file.read() + + +def main() -> None: + """Print shell integration instructions after installation.""" + print("\n" + "=" * 60) + print("šŸŽ‰ cd-browser installed successfully!") + print("=" * 60) + print() + print( + "To enable the 'cd_' command, you need to add a function to your shell profile." + ) + print() + + shell = os.environ.get("SHELL", "").split("/")[-1] + profile = _profile_for_shell(shell) + + cd_function = """cd_() { + local target + target="$(cd_browser)" || return + if [ -n "$target" ] && [ -d "$target" ]; then + cd "$target" + fi +}""" + + print(f"Copy this to the end of {profile}:") + print() + print(cd_function) + print() + + profile_path = os.path.expanduser(profile) + + try: + response = ( + input("Do you want me to add it automatically? (y/n): ").strip().lower() + ) + if response == "y": + if _is_cd_function_present(profile_path): + print(f"\nā„¹ļø cd_() already exists in {profile}, no changes made.") + else: + with open(profile_path, "a", encoding="utf-8") as f: + f.write("\n" + cd_function + "\n") + print(f"\nāœ… Added to {profile}") + print(f"Run 'source {profile}' or restart your terminal to activate cd_.") + else: + print(f"\nAdd the function to {profile} manually.") + print(f"Then run 'source {profile}' or restart terminal.") + except EOFError: + print( + f"\nNo interactive input detected. Add the function to {profile} manually." + ) + print(f"Then run 'source {profile}' or restart terminal.") + except KeyboardInterrupt: + print("\n\nSetup cancelled. You can add the function manually later.") + sys.exit(0) + + print("\n" + "=" * 60) + print( + f"For uninstall: after 'pip uninstall cd-browser', remove cd_() from {profile}." + ) + print("=" * 60 + "\n") + + +if __name__ == "__main__": + main() diff --git a/tests/test_cli.py b/tests/test_cli.py index f1172a4..16ce8a1 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -4,9 +4,10 @@ import pytest from app.cli import run_cli from app.navigator import Navigator +from app.ui import TerminalUI -class StubUI: +class StubUI(TerminalUI): def __init__(self, result: Path) -> None: self.result = result self.navigator: Navigator | None = None diff --git a/tests/test_filesystem.py b/tests/test_filesystem.py index 9569e2f..eea9891 100644 --- a/tests/test_filesystem.py +++ b/tests/test_filesystem.py @@ -44,3 +44,33 @@ def test_list_directories_raises_for_missing_directory(tmp_path: Path) -> None: with pytest.raises(FileNotFoundError): list_directories(missing_path) + + +def test_list_directories_skips_permission_errors_for_children(tmp_path: Path) -> None: + root = tmp_path / "root" + root.mkdir() + allowed = root / "allowed" + blocked = root / "blocked" + allowed.mkdir() + blocked.mkdir() + + blocked.chmod(0) + try: + entries = list_directories(root) + finally: + blocked.chmod(0o700) + + assert any(entry.name == "allowed" for entry in entries) + + +def test_has_subdirectories_returns_false_on_permission_error(tmp_path: Path) -> None: + blocked = tmp_path / "blocked" + blocked.mkdir() + + blocked.chmod(0) + try: + result = has_subdirectories(blocked) + finally: + blocked.chmod(0o700) + + assert result is False diff --git a/tests/test_navigator.py b/tests/test_navigator.py index 804ea51..5d48efa 100644 --- a/tests/test_navigator.py +++ b/tests/test_navigator.py @@ -25,6 +25,31 @@ def test_navigator_initial_state_includes_parent_entry(tmp_path: Path) -> None: ] +def test_toggle_hidden_directories(tmp_path: Path) -> None: + root = tmp_path / "root" + root.mkdir() + visible = root / "visible" + visible.mkdir() + hidden = root / ".hidden" + hidden.mkdir() + + navigator = Navigator(root) + + assert [entry.name for entry in navigator.visible_entries] == ["..", "visible"] + + navigator.toggle_hidden() + assert navigator.show_hidden is True + assert [entry.name for entry in navigator.visible_entries] == [ + "..", + ".hidden", + "visible", + ] + + navigator.toggle_hidden() + assert navigator.show_hidden is False + assert [entry.name for entry in navigator.visible_entries] == ["..", "visible"] + + def test_expand_selected_directory_reveals_nested_entries(tmp_path: Path) -> None: current = tmp_path / "workspace" current.mkdir() diff --git a/tests/test_post_install.py b/tests/test_post_install.py new file mode 100644 index 0000000..8fa8d90 --- /dev/null +++ b/tests/test_post_install.py @@ -0,0 +1,47 @@ +from pathlib import Path + +import pytest + +from cd_browser_post_install import main + +CD_FUNCTION_SNIPPET = "cd_() {" + + +def test_post_install_handles_eof_without_crashing( + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + monkeypatch.setenv("SHELL", "/bin/zsh") + monkeypatch.setattr( + "builtins.input", lambda _prompt: (_ for _ in ()).throw(EOFError) + ) + + main() + + output = capsys.readouterr().out + assert "No interactive input detected" in output + assert "source ~/.zshrc" in output + + +def test_post_install_adds_cd_function_once( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + capsys: pytest.CaptureFixture[str], +) -> None: + monkeypatch.setenv("SHELL", "/bin/zsh") + monkeypatch.setenv("HOME", str(tmp_path)) + + responses = iter(["y", "y"]) + monkeypatch.setattr("builtins.input", lambda _prompt: next(responses)) + + main() + main() + + profile_path = tmp_path / ".zshrc" + content = profile_path.read_text(encoding="utf-8") + + assert content.count(CD_FUNCTION_SNIPPET) == 1 + + output = capsys.readouterr().out + assert "already exists" in output + assert "source ~/.zshrc" in output diff --git a/tests/test_ui.py b/tests/test_ui.py index ae9f6d7..a43edca 100644 --- a/tests/test_ui.py +++ b/tests/test_ui.py @@ -1,11 +1,16 @@ +import curses from pathlib import Path import pytest from app.navigator import Navigator, VisibleEntry from app.ui import ( + HistoryLine, RenderLine, + TerminalUI, + build_history_lines, build_render_lines, + clamp_scroll_offset, format_entry, move_selection, open_terminal_streams, @@ -65,6 +70,157 @@ def test_build_render_lines_marks_selected_entry(tmp_path: Path) -> None: ] +def test_build_history_lines_marks_selected_history_entry(tmp_path: Path) -> None: + first = tmp_path / "first" + second = tmp_path / "second" + + assert build_history_lines((first, second), selected_index=1) == [ + HistoryLine(text=str(first), is_selected=False), + HistoryLine(text=str(second), is_selected=True), + ] + + +def test_clamp_scroll_offset_keeps_selection_visible_when_moving_down() -> None: + assert clamp_scroll_offset(5, scroll_offset=0, visible_count=3, total_count=10) == 3 + + +def test_clamp_scroll_offset_keeps_selection_visible_when_moving_up() -> None: + assert clamp_scroll_offset(1, scroll_offset=4, visible_count=3, total_count=10) == 1 + + +def test_clamp_scroll_offset_returns_zero_when_all_items_fit() -> None: + assert clamp_scroll_offset(2, scroll_offset=1, visible_count=5, total_count=3) == 0 + + +def test_toggle_history_mode_selects_current_history_entry(tmp_path: Path) -> None: + root = tmp_path / "root" + child = root / "child" + root.mkdir() + child.mkdir() + + navigator = Navigator(root) + navigator.set_selected_index(1) + navigator.enter_selected_directory() + ui = TerminalUI() + + ui._toggle_history_mode(navigator) + + assert ui._history_mode is True + assert ui._history_selected_index == 1 + + +def test_history_mode_enter_selects_entry_and_exits(tmp_path: Path) -> None: + root = tmp_path / "root" + first = root / "first" + second = root / "second" + root.mkdir() + first.mkdir() + second.mkdir() + + navigator = Navigator(root) + navigator.set_selected_index(1) + navigator.enter_selected_directory() + navigator.go_to_parent_directory() + navigator.set_selected_index(2) + navigator.enter_selected_directory() + + ui = TerminalUI() + ui._toggle_history_mode(navigator) + ui._handle_history_key(curses.KEY_UP, navigator) + ui._handle_history_key(10, navigator) + + assert ui._history_mode is False + assert navigator.current_path == root.resolve() + assert navigator.selected_index == 0 + + +def test_history_mode_toggle_off_keeps_current_directory(tmp_path: Path) -> None: + root = tmp_path / "root" + root.mkdir() + navigator = Navigator(root) + ui = TerminalUI() + + ui._toggle_history_mode(navigator) + ui._toggle_history_mode(navigator) + + assert ui._history_mode is False + assert navigator.current_path == root.resolve() + + +def test_right_arrow_expands_collapsed_directory(tmp_path: Path) -> None: + workspace = tmp_path / "workspace" + scripts = workspace / "scripts" + build = scripts / "build" + workspace.mkdir() + scripts.mkdir() + build.mkdir() + + navigator = Navigator(workspace) + navigator.set_selected_index(1) + ui = TerminalUI() + + assert ui._handle_tree_key(curses.KEY_RIGHT, navigator) is None + assert navigator.current_path == workspace.resolve() + assert [entry.name for entry in navigator.visible_entries] == [ + "..", + "scripts", + "build", + ] + + +def test_right_arrow_enters_already_expanded_directory(tmp_path: Path) -> None: + workspace = tmp_path / "workspace" + scripts = workspace / "scripts" + build = scripts / "build" + workspace.mkdir() + scripts.mkdir() + build.mkdir() + + navigator = Navigator(workspace) + navigator.set_selected_index(1) + navigator.expand_selected_directory() + ui = TerminalUI() + + assert ui._handle_tree_key(curses.KEY_RIGHT, navigator) is None + assert navigator.current_path == scripts.resolve() + assert navigator.selected_index == 0 + + +def test_left_arrow_goes_to_parent_when_selected_entry_is_collapsed( + tmp_path: Path, +) -> None: + parent = tmp_path / "parent" + current = parent / "current" + child = current / "child" + parent.mkdir() + current.mkdir() + child.mkdir() + + navigator = Navigator(current) + navigator.set_selected_index(1) + ui = TerminalUI() + + assert ui._handle_tree_key(curses.KEY_LEFT, navigator) is None + assert navigator.current_path == parent.resolve() + assert navigator.selected_index == 0 + + +def test_enter_returns_selected_path_without_navigating(tmp_path: Path) -> None: + workspace = tmp_path / "workspace" + child = workspace / "child" + workspace.mkdir() + child.mkdir() + + navigator = Navigator(workspace) + navigator.set_selected_index(1) + ui = TerminalUI() + + result = ui._handle_tree_key(10, navigator) + + assert result == child.resolve() + assert navigator.current_path == workspace.resolve() + + def test_open_terminal_streams_raises_without_terminal( monkeypatch: pytest.MonkeyPatch, ) -> None: