cd-browser 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cd_browser-0.1.1/LICENSE +21 -0
- cd_browser-0.1.1/PKG-INFO +173 -0
- cd_browser-0.1.1/README.md +146 -0
- cd_browser-0.1.1/pyproject.toml +69 -0
- cd_browser-0.1.1/setup.cfg +4 -0
- cd_browser-0.1.1/src/app/__init__.py +0 -0
- cd_browser-0.1.1/src/app/cli.py +15 -0
- cd_browser-0.1.1/src/app/config.py +9 -0
- cd_browser-0.1.1/src/app/domain/__init__.py +0 -0
- cd_browser-0.1.1/src/app/filesystem.py +53 -0
- cd_browser-0.1.1/src/app/history.py +68 -0
- cd_browser-0.1.1/src/app/infrastructure/__init__.py +0 -0
- cd_browser-0.1.1/src/app/logging_config.py +11 -0
- cd_browser-0.1.1/src/app/main.py +16 -0
- cd_browser-0.1.1/src/app/navigator.py +212 -0
- cd_browser-0.1.1/src/app/services/__init__.py +0 -0
- cd_browser-0.1.1/src/app/ui.py +344 -0
- cd_browser-0.1.1/src/app/utils/__init__.py +0 -0
- cd_browser-0.1.1/src/cd_browser.egg-info/PKG-INFO +173 -0
- cd_browser-0.1.1/src/cd_browser.egg-info/SOURCES.txt +28 -0
- cd_browser-0.1.1/src/cd_browser.egg-info/dependency_links.txt +1 -0
- cd_browser-0.1.1/src/cd_browser.egg-info/entry_points.txt +2 -0
- cd_browser-0.1.1/src/cd_browser.egg-info/requires.txt +7 -0
- cd_browser-0.1.1/src/cd_browser.egg-info/top_level.txt +1 -0
- cd_browser-0.1.1/tests/test_cli.py +44 -0
- cd_browser-0.1.1/tests/test_filesystem.py +46 -0
- cd_browser-0.1.1/tests/test_history.py +84 -0
- cd_browser-0.1.1/tests/test_navigator.py +207 -0
- cd_browser-0.1.1/tests/test_smoke.py +18 -0
- cd_browser-0.1.1/tests/test_ui.py +236 -0
cd_browser-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Saky
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cd-browser
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: A fast keyboard-driven directory navigator for the terminal.
|
|
5
|
+
Author: Saky
|
|
6
|
+
Classifier: Development Status :: 3 - Alpha
|
|
7
|
+
Classifier: Intended Audience :: Developers
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Utilities
|
|
17
|
+
Requires-Python: >=3.8
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
22
|
+
Requires-Dist: black>=24.0; extra == "dev"
|
|
23
|
+
Requires-Dist: ruff>=0.3.0; extra == "dev"
|
|
24
|
+
Requires-Dist: mypy>=1.8.0; extra == "dev"
|
|
25
|
+
Requires-Dist: pre-commit>=3.7.0; extra == "dev"
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# cd-browser
|
|
29
|
+
|
|
30
|
+
**Stop typing paths. Browse them.**
|
|
31
|
+
|
|
32
|
+
`cd-browser` is a fast keyboard-driven directory navigator for the terminal.
|
|
33
|
+
It lets you explore directory trees visually and jump to any folder instantly.
|
|
34
|
+
|
|
35
|
+

|
|
36
|
+
|
|
37
|
+
## Why cd-browser?
|
|
38
|
+
|
|
39
|
+
Working in the terminal often means:
|
|
40
|
+
|
|
41
|
+
- typing long directory paths
|
|
42
|
+
- navigating deep folder trees
|
|
43
|
+
- repeating `cd ..` multiple times
|
|
44
|
+
|
|
45
|
+
`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.
|
|
46
|
+
|
|
47
|
+
## Features
|
|
48
|
+
|
|
49
|
+
- 🚀 Fast visual navigation of directory trees
|
|
50
|
+
- ⌨️ Fully keyboard-driven workflow
|
|
51
|
+
- 🌲 Expand and collapse directories
|
|
52
|
+
- 📜 Navigation history inside the session
|
|
53
|
+
- 🖥 Native terminal interface
|
|
54
|
+
- 🔁 Works with Bash, Zsh and other shells
|
|
55
|
+
- ⚡ Returns the selected path to the shell
|
|
56
|
+
- 🔎 Press `.` to toggle hidden directories
|
|
57
|
+
|
|
58
|
+
## Quick Demo
|
|
59
|
+
|
|
60
|
+
Run:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
cd_
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Browse directories using the arrow keys and press **Enter** to jump directly to the selected folder.
|
|
67
|
+
|
|
68
|
+
## Documentation
|
|
69
|
+
|
|
70
|
+
See the documentation index:
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
docs/index.md
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Installation
|
|
77
|
+
|
|
78
|
+
Install the project in user mode:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
pip install .
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
For editable local development:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
pip install -e '.[dev]'
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
After installation, the application command is:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
cd_browser
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
When the interactive session exits, the program prints the final selected directory path.
|
|
97
|
+
|
|
98
|
+
## Shell Integration For `cd_`
|
|
99
|
+
|
|
100
|
+
Because a Python subprocess cannot directly change the parent shell directory, use a shell wrapper that captures the final path printed by `cd_browser`.
|
|
101
|
+
|
|
102
|
+
Bash or Zsh example:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
cd_() {
|
|
106
|
+
local target
|
|
107
|
+
target="$(cd_browser)" || return
|
|
108
|
+
|
|
109
|
+
if [ -n "$target" ] && [ -d "$target" ]; then
|
|
110
|
+
cd "$target"
|
|
111
|
+
fi
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
After adding the function to your shell profile, reload it:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
source ~/.bashrc
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Or:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
source ~/.zshrc
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Then use:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
cd_
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
- Dentro de `cd_browser`, presiona `.` para alternar la visualización de carpetas ocultas.
|
|
134
|
+
|
|
135
|
+
## Uninstall
|
|
136
|
+
|
|
137
|
+
If the project was installed with `pip`, remove it with:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
pip uninstall cd-browser
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
If you also added the `cd_` shell wrapper, remove that function from your shell profile and reload the shell configuration.
|
|
144
|
+
|
|
145
|
+
## Developer Setup
|
|
146
|
+
|
|
147
|
+
Recommended setup:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
make dev
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Run the application in development mode:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
python -m app.main
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Useful development commands:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
make fix
|
|
163
|
+
make quality
|
|
164
|
+
make run
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Template Origin
|
|
168
|
+
|
|
169
|
+
This project was created from the Python AI Dev Template.
|
|
170
|
+
|
|
171
|
+
The original template documentation has been preserved in `README_TEMPLATE.md` so the project-specific README can focus on `cd-browser` usage and development.
|
|
172
|
+
|
|
173
|
+
For the original template setup, conventions, and generic workflow notes, see `README_TEMPLATE.md`.
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# cd-browser
|
|
2
|
+
|
|
3
|
+
**Stop typing paths. Browse them.**
|
|
4
|
+
|
|
5
|
+
`cd-browser` is a fast keyboard-driven directory navigator for the terminal.
|
|
6
|
+
It lets you explore directory trees visually and jump to any folder instantly.
|
|
7
|
+
|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
## Why cd-browser?
|
|
11
|
+
|
|
12
|
+
Working in the terminal often means:
|
|
13
|
+
|
|
14
|
+
- typing long directory paths
|
|
15
|
+
- navigating deep folder trees
|
|
16
|
+
- repeating `cd ..` multiple times
|
|
17
|
+
|
|
18
|
+
`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.
|
|
19
|
+
|
|
20
|
+
## Features
|
|
21
|
+
|
|
22
|
+
- 🚀 Fast visual navigation of directory trees
|
|
23
|
+
- ⌨️ Fully keyboard-driven workflow
|
|
24
|
+
- 🌲 Expand and collapse directories
|
|
25
|
+
- 📜 Navigation history inside the session
|
|
26
|
+
- 🖥 Native terminal interface
|
|
27
|
+
- 🔁 Works with Bash, Zsh and other shells
|
|
28
|
+
- ⚡ Returns the selected path to the shell
|
|
29
|
+
- 🔎 Press `.` to toggle hidden directories
|
|
30
|
+
|
|
31
|
+
## Quick Demo
|
|
32
|
+
|
|
33
|
+
Run:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
cd_
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Browse directories using the arrow keys and press **Enter** to jump directly to the selected folder.
|
|
40
|
+
|
|
41
|
+
## Documentation
|
|
42
|
+
|
|
43
|
+
See the documentation index:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
docs/index.md
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Installation
|
|
50
|
+
|
|
51
|
+
Install the project in user mode:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install .
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
For editable local development:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install -e '.[dev]'
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
After installation, the application command is:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
cd_browser
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
When the interactive session exits, the program prints the final selected directory path.
|
|
70
|
+
|
|
71
|
+
## Shell Integration For `cd_`
|
|
72
|
+
|
|
73
|
+
Because a Python subprocess cannot directly change the parent shell directory, use a shell wrapper that captures the final path printed by `cd_browser`.
|
|
74
|
+
|
|
75
|
+
Bash or Zsh example:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
cd_() {
|
|
79
|
+
local target
|
|
80
|
+
target="$(cd_browser)" || return
|
|
81
|
+
|
|
82
|
+
if [ -n "$target" ] && [ -d "$target" ]; then
|
|
83
|
+
cd "$target"
|
|
84
|
+
fi
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
After adding the function to your shell profile, reload it:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
source ~/.bashrc
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Or:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
source ~/.zshrc
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Then use:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
cd_
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
- Dentro de `cd_browser`, presiona `.` para alternar la visualización de carpetas ocultas.
|
|
107
|
+
|
|
108
|
+
## Uninstall
|
|
109
|
+
|
|
110
|
+
If the project was installed with `pip`, remove it with:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
pip uninstall cd-browser
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
If you also added the `cd_` shell wrapper, remove that function from your shell profile and reload the shell configuration.
|
|
117
|
+
|
|
118
|
+
## Developer Setup
|
|
119
|
+
|
|
120
|
+
Recommended setup:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
make dev
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Run the application in development mode:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
python -m app.main
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Useful development commands:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
make fix
|
|
136
|
+
make quality
|
|
137
|
+
make run
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Template Origin
|
|
141
|
+
|
|
142
|
+
This project was created from the Python AI Dev Template.
|
|
143
|
+
|
|
144
|
+
The original template documentation has been preserved in `README_TEMPLATE.md` so the project-specific README can focus on `cd-browser` usage and development.
|
|
145
|
+
|
|
146
|
+
For the original template setup, conventions, and generic workflow notes, see `README_TEMPLATE.md`.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "cd-browser"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "A fast keyboard-driven directory navigator for the terminal."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Saky" }
|
|
13
|
+
]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.8",
|
|
21
|
+
"Programming Language :: Python :: 3.9",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Topic :: Utilities",
|
|
26
|
+
]
|
|
27
|
+
dependencies = []
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
cd_browser = "app.main:main"
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
dev = [
|
|
34
|
+
"pytest>=8.0",
|
|
35
|
+
"black>=24.0",
|
|
36
|
+
"ruff>=0.3.0",
|
|
37
|
+
"mypy>=1.8.0",
|
|
38
|
+
"pre-commit>=3.7.0"
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[tool.setuptools]
|
|
42
|
+
package-dir = {"" = "src"}
|
|
43
|
+
|
|
44
|
+
[tool.setuptools.packages.find]
|
|
45
|
+
where = ["src"]
|
|
46
|
+
|
|
47
|
+
[tool.black]
|
|
48
|
+
line-length = 88
|
|
49
|
+
target-version = ["py311"]
|
|
50
|
+
|
|
51
|
+
[tool.ruff]
|
|
52
|
+
line-length = 88
|
|
53
|
+
target-version = "py311"
|
|
54
|
+
|
|
55
|
+
[tool.ruff.lint]
|
|
56
|
+
select = ["E", "F", "I", "B", "UP"]
|
|
57
|
+
ignore = []
|
|
58
|
+
|
|
59
|
+
[tool.mypy]
|
|
60
|
+
python_version = "3.11"
|
|
61
|
+
strict = true
|
|
62
|
+
warn_unused_configs = true
|
|
63
|
+
disallow_untyped_defs = true
|
|
64
|
+
ignore_missing_imports = true
|
|
65
|
+
|
|
66
|
+
[tool.pytest.ini_options]
|
|
67
|
+
testpaths = ["tests"]
|
|
68
|
+
pythonpath = ["src"]
|
|
69
|
+
addopts = "-q"
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from app.navigator import Navigator
|
|
6
|
+
from app.ui import TerminalUI
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def run_cli(start_path: Path | None = None, ui: TerminalUI | None = None) -> Path:
|
|
10
|
+
"""Run the interactive directory browser and return the selected path."""
|
|
11
|
+
|
|
12
|
+
initial_path = (start_path or Path.cwd()).expanduser().resolve()
|
|
13
|
+
navigator = Navigator(initial_path)
|
|
14
|
+
terminal_ui = ui or TerminalUI()
|
|
15
|
+
return terminal_ui.run(navigator)
|
|
File without changes
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True, slots=True)
|
|
8
|
+
class DirectoryEntry:
|
|
9
|
+
"""Directory metadata used by the navigator layer."""
|
|
10
|
+
|
|
11
|
+
name: str
|
|
12
|
+
path: Path
|
|
13
|
+
has_children: bool
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def list_directories(path: Path, show_hidden: bool = False) -> list[DirectoryEntry]:
|
|
17
|
+
"""Return direct child directories sorted by name."""
|
|
18
|
+
|
|
19
|
+
directory_path = path.expanduser().resolve()
|
|
20
|
+
entries: list[DirectoryEntry] = []
|
|
21
|
+
|
|
22
|
+
for child in directory_path.iterdir():
|
|
23
|
+
if not child.is_dir():
|
|
24
|
+
continue
|
|
25
|
+
if not show_hidden and child.name.startswith("."):
|
|
26
|
+
continue
|
|
27
|
+
|
|
28
|
+
entries.append(
|
|
29
|
+
DirectoryEntry(
|
|
30
|
+
name=child.name,
|
|
31
|
+
path=child,
|
|
32
|
+
has_children=has_subdirectories(child, show_hidden=show_hidden),
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
return sorted(entries, key=lambda entry: entry.name.casefold())
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def has_subdirectories(path: Path, show_hidden: bool = False) -> bool:
|
|
40
|
+
"""Return whether the directory contains at least one subdirectory."""
|
|
41
|
+
|
|
42
|
+
directory_path = path.expanduser().resolve()
|
|
43
|
+
|
|
44
|
+
for child in directory_path.iterdir():
|
|
45
|
+
if not child.is_dir():
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
if not show_hidden and child.name.startswith("."):
|
|
49
|
+
continue
|
|
50
|
+
|
|
51
|
+
return True
|
|
52
|
+
|
|
53
|
+
return False
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(slots=True)
|
|
8
|
+
class SessionHistory:
|
|
9
|
+
"""In-memory session history for visited directories."""
|
|
10
|
+
|
|
11
|
+
_entries: list[Path] = field(default_factory=list)
|
|
12
|
+
_index: int = -1
|
|
13
|
+
|
|
14
|
+
def visit(self, path: Path) -> Path:
|
|
15
|
+
"""Record a directory visit and discard forward history when needed."""
|
|
16
|
+
|
|
17
|
+
resolved_path = path.expanduser().resolve()
|
|
18
|
+
|
|
19
|
+
if self.current == resolved_path:
|
|
20
|
+
return resolved_path
|
|
21
|
+
|
|
22
|
+
if self._index < len(self._entries) - 1:
|
|
23
|
+
self._entries = self._entries[: self._index + 1]
|
|
24
|
+
|
|
25
|
+
self._entries.append(resolved_path)
|
|
26
|
+
self._index = len(self._entries) - 1
|
|
27
|
+
return resolved_path
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def current(self) -> Path | None:
|
|
31
|
+
"""Return the current history entry."""
|
|
32
|
+
|
|
33
|
+
if self._index == -1:
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
return self._entries[self._index]
|
|
37
|
+
|
|
38
|
+
def back(self) -> Path | None:
|
|
39
|
+
"""Move backward in history when possible."""
|
|
40
|
+
|
|
41
|
+
if self._index <= 0:
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
self._index -= 1
|
|
45
|
+
return self._entries[self._index]
|
|
46
|
+
|
|
47
|
+
def forward(self) -> Path | None:
|
|
48
|
+
"""Move forward in history when possible."""
|
|
49
|
+
|
|
50
|
+
if self._index == -1 or self._index >= len(self._entries) - 1:
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
self._index += 1
|
|
54
|
+
return self._entries[self._index]
|
|
55
|
+
|
|
56
|
+
def select(self, index: int) -> Path:
|
|
57
|
+
"""Jump to a previously visited directory by history index."""
|
|
58
|
+
|
|
59
|
+
if index < 0 or index >= len(self._entries):
|
|
60
|
+
raise IndexError("History index out of range")
|
|
61
|
+
|
|
62
|
+
self._index = index
|
|
63
|
+
return self._entries[self._index]
|
|
64
|
+
|
|
65
|
+
def entries(self) -> tuple[Path, ...]:
|
|
66
|
+
"""Return the full history as an immutable sequence."""
|
|
67
|
+
|
|
68
|
+
return tuple(self._entries)
|
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
from app.cli import run_cli
|
|
4
|
+
from app.logging_config import setup_logging
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def main() -> None:
|
|
8
|
+
"""Run the application entry point."""
|
|
9
|
+
setup_logging()
|
|
10
|
+
|
|
11
|
+
final_path = run_cli()
|
|
12
|
+
sys.stdout.write(f"{final_path}\n")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
if __name__ == "__main__":
|
|
16
|
+
main()
|