riched 1.3.3__tar.gz → 1.3.4__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.
- {riched-1.3.3 → riched-1.3.4}/AGENTS.md +3 -3
- {riched-1.3.3 → riched-1.3.4}/PKG-INFO +9 -9
- {riched-1.3.3 → riched-1.3.4}/README.md +8 -8
- {riched-1.3.3 → riched-1.3.4}/pyproject.toml +1 -1
- {riched-1.3.3 → riched-1.3.4}/src/rich_editor/app.py +93 -85
- {riched-1.3.3 → riched-1.3.4}/src/rich_editor/bindings.yaml +3 -3
- {riched-1.3.3 → riched-1.3.4}/src/rich_editor/cli.py +6 -1
- {riched-1.3.3 → riched-1.3.4}/src/rich_editor/editor.py +2 -2
- {riched-1.3.3 → riched-1.3.4}/src/rich_editor/screens.py +2 -2
- {riched-1.3.3 → riched-1.3.4}/tests/test_cli.py +1 -1
- {riched-1.3.3 → riched-1.3.4}/tests/test_command_palette.py +3 -2
- {riched-1.3.3 → riched-1.3.4}/tests/test_create_file.py +4 -3
- {riched-1.3.3 → riched-1.3.4}/tests/test_dirty_buffers.py +10 -11
- {riched-1.3.3 → riched-1.3.4}/tests/test_editor_shortcuts.py +5 -5
- {riched-1.3.3 → riched-1.3.4}/tests/test_file_io.py +1 -1
- {riched-1.3.3 → riched-1.3.4}/tests/test_footer.py +2 -2
- {riched-1.3.3 → riched-1.3.4}/tests/test_formatting.py +2 -1
- {riched-1.3.3 → riched-1.3.4}/tests/test_markdown_preview.py +4 -2
- {riched-1.3.3 → riched-1.3.4}/tests/test_refresh.py +10 -6
- riched-1.3.3/tests/test_file_tree.py → riched-1.3.4/tests/test_sidebar.py +18 -16
- {riched-1.3.3 → riched-1.3.4}/uv.lock +1 -1
- {riched-1.3.3 → riched-1.3.4}/.gitignore +0 -0
- {riched-1.3.3 → riched-1.3.4}/.python-version +0 -0
- {riched-1.3.3 → riched-1.3.4}/docs/command-palette.svg +0 -0
- {riched-1.3.3 → riched-1.3.4}/docs/markdown-preview.svg +0 -0
- {riched-1.3.3 → riched-1.3.4}/docs/python-buffer.svg +0 -0
- {riched-1.3.3 → riched-1.3.4}/docs/search-replace.svg +0 -0
- {riched-1.3.3 → riched-1.3.4}/src/rich_editor/__init__.py +0 -0
- {riched-1.3.3 → riched-1.3.4}/src/rich_editor/formatting.py +0 -0
- {riched-1.3.3 → riched-1.3.4}/src/rich_editor/keybindings.py +0 -0
- {riched-1.3.3 → riched-1.3.4}/src/rich_editor/quick_open.py +0 -0
- {riched-1.3.3 → riched-1.3.4}/src/rich_editor/settings.py +0 -0
- {riched-1.3.3 → riched-1.3.4}/src/rich_editor/syntax.py +0 -0
- {riched-1.3.3 → riched-1.3.4}/tests/__init__.py +0 -0
- {riched-1.3.3 → riched-1.3.4}/tests/helpers.py +0 -0
- {riched-1.3.3 → riched-1.3.4}/tests/runner.py +0 -0
- {riched-1.3.3 → riched-1.3.4}/tests/test_quick_open.py +0 -0
- {riched-1.3.3 → riched-1.3.4}/tests/test_replace.py +0 -0
- {riched-1.3.3 → riched-1.3.4}/tests/test_syntax.py +0 -0
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
## Project Structure & Module Organization
|
|
7
7
|
|
|
8
|
-
This repository is a uv-managed Python project for `riched`, a Textual TUI editor. Application code lives in `src/rich_editor/`: `cli.py` owns argument parsing, `app.py` owns the Textual app and
|
|
8
|
+
This repository is a uv-managed Python project for `riched`, a Textual TUI editor. Application code lives in `src/rich_editor/`: `cli.py` owns argument parsing, `app.py` owns the Textual app and sidebar, `editor.py` customizes text editing, `screens.py` contains modal screens, `quick_open.py` indexes quick-open candidates, `keybindings.py` loads shipped shortcut config and popup display rows, `bindings.yaml` defines shipped bindings, `settings.py` persists user settings such as theme, and `syntax.py` handles language detection/highlighting. End-to-end tests live in `tests/`. Avoid committing generated files such as `.venv/`, `__pycache__/`, `keys.log`, `dist/`, or build artifacts.
|
|
9
9
|
|
|
10
10
|
## Build, Test, and Development Commands
|
|
11
11
|
|
|
@@ -26,13 +26,13 @@ When changing app-level key bindings, test both file-open and no-buffer/director
|
|
|
26
26
|
|
|
27
27
|
## Testing Guidelines
|
|
28
28
|
|
|
29
|
-
Only add end-to-end tests in `tests/`; do not create unit tests. Tests use Textual's Pilot harness and should be named `test_<behavior>`. Keep each test focused on user-visible behavior such as file open/save, dirty prompts,
|
|
29
|
+
Only add end-to-end tests in `tests/`; do not create unit tests. Tests use Textual's Pilot harness and should be named `test_<behavior>`. Keep each test focused on user-visible behavior such as file open/save, dirty prompts, sidebar switching, quick-open search/indexing, or syntax highlighting. Use temporary files as existing tests do.
|
|
30
30
|
|
|
31
31
|
For hotkey e2e tests, cover the YAML-declared preferred key, terminal-normalized aliases, and any alternative keys. If the real terminal key name differs from the user-facing label, cover the real Textual name too. Before accepting an alternative, confirm it can be produced distinctly by Ghostty on macOS; Ctrl punctuation is especially risky because some combinations are control bytes with legacy meanings.
|
|
32
32
|
|
|
33
33
|
## Commit & Pull Request Guidelines
|
|
34
34
|
|
|
35
|
-
Use concise imperative commit messages, for example `Add
|
|
35
|
+
Use concise imperative commit messages, for example `Add sidebar` or `Refactor editor package`. Pull requests should include a short summary, manual/e2e verification steps, related issue links if any, and terminal screenshots or recordings for visible TUI changes. When merging pull requests, use rebase and merge.
|
|
36
36
|
|
|
37
37
|
## Agent-Specific Instructions
|
|
38
38
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: riched
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.4
|
|
4
4
|
Summary: Terminal Editor with VS Code Keybindings
|
|
5
5
|
Project-URL: Repository, https://github.com/hoishing/riched
|
|
6
6
|
Requires-Python: >=3.12
|
|
@@ -53,7 +53,7 @@ Terminal Editor with VS Code Keybindings Powered by [Rich](https://github.com/Te
|
|
|
53
53
|
|
|
54
54
|
- vscode text editing key bindings
|
|
55
55
|
- macOS style key bindings work in ssh session to linux host also
|
|
56
|
-
- project
|
|
56
|
+
- project sidebar with create, rename, quick refresh, and move-to-trash support
|
|
57
57
|
- quick file open with fuzzy search
|
|
58
58
|
- in-terminal Markdown preview with table of contents toggle
|
|
59
59
|
- document formatting through external formatter CLIs
|
|
@@ -86,7 +86,7 @@ App shortcuts:
|
|
|
86
86
|
| ------------ | ----------------------- |
|
|
87
87
|
| `⌘S` | Save |
|
|
88
88
|
| `F1` | Command palette |
|
|
89
|
-
| `⌘B` | Toggle
|
|
89
|
+
| `⌘B` | Toggle sidebar |
|
|
90
90
|
| `⌘P` | Quick open |
|
|
91
91
|
| `⌃H` | Replace |
|
|
92
92
|
| `⌥N` | Create file |
|
|
@@ -120,7 +120,7 @@ Editor shortcuts:
|
|
|
120
120
|
| `⌘⇧←` | Select to line start |
|
|
121
121
|
| `⌘⇧→` | Select to line end |
|
|
122
122
|
|
|
123
|
-
|
|
123
|
+
Sidebar shortcuts:
|
|
124
124
|
|
|
125
125
|
| Shortcut | Action |
|
|
126
126
|
| ----------- | ------------------------------------- |
|
|
@@ -171,27 +171,27 @@ Files unsupported by these tools are left unchanged and Riched shows a warning.
|
|
|
171
171
|
|
|
172
172
|
## Refresh
|
|
173
173
|
|
|
174
|
-
Use the `↻` title-bar button or `⌘R` to refresh the workspace. Refresh reloads the
|
|
174
|
+
Use the `↻` title-bar button or `⌘R` to refresh the workspace. Refresh reloads the sidebar and reloads the current buffer from disk. If the buffer has unsaved edits, Riched prompts to save, discard, or cancel before reloading.
|
|
175
175
|
|
|
176
176
|
## Creating Files
|
|
177
177
|
|
|
178
178
|
Use `⌥N` to create a file. Riched prompts for the file name and supports nested relative paths such as `notes/today.md`.
|
|
179
179
|
|
|
180
|
-
When a folder is highlighted in the
|
|
180
|
+
When a folder is highlighted in the sidebar, the new file is created inside that folder. Otherwise, the new file is created beside the currently open file. If no file is open, the new file is created in the project root.
|
|
181
181
|
|
|
182
182
|
If the requested file already exists, Riched opens it without overwriting its contents.
|
|
183
183
|
|
|
184
184
|
## Renaming Files And Folders
|
|
185
185
|
|
|
186
|
-
When the
|
|
186
|
+
When the sidebar is focused, `Enter` opens a rename prompt for the selected file or folder. Renaming keeps the open buffer connected to the renamed path when the current file, or a folder containing it, is renamed.
|
|
187
187
|
|
|
188
188
|
The project root cannot be renamed. Empty names, path separators, and duplicate target names are rejected in the prompt.
|
|
189
189
|
|
|
190
190
|
## Moving Files To Trash
|
|
191
191
|
|
|
192
|
-
When the
|
|
192
|
+
When the sidebar is focused, `⌘⌫` removes the selected file or folder by moving it to Trash. The command palette also shows `Move "<name>" to Trash` for the selected sidebar item. Both paths ask for confirmation before moving the item.
|
|
193
193
|
|
|
194
|
-
On stock Ghostty, `⌘⌫` is delivered to terminal apps as `⌃U`, so Riched binds both forms for the
|
|
194
|
+
On stock Ghostty, `⌘⌫` is delivered to terminal apps as `⌃U`, so Riched binds both forms for the sidebar.
|
|
195
195
|
|
|
196
196
|
## Limitations
|
|
197
197
|
|
|
@@ -38,7 +38,7 @@ Terminal Editor with VS Code Keybindings Powered by [Rich](https://github.com/Te
|
|
|
38
38
|
|
|
39
39
|
- vscode text editing key bindings
|
|
40
40
|
- macOS style key bindings work in ssh session to linux host also
|
|
41
|
-
- project
|
|
41
|
+
- project sidebar with create, rename, quick refresh, and move-to-trash support
|
|
42
42
|
- quick file open with fuzzy search
|
|
43
43
|
- in-terminal Markdown preview with table of contents toggle
|
|
44
44
|
- document formatting through external formatter CLIs
|
|
@@ -71,7 +71,7 @@ App shortcuts:
|
|
|
71
71
|
| ------------ | ----------------------- |
|
|
72
72
|
| `⌘S` | Save |
|
|
73
73
|
| `F1` | Command palette |
|
|
74
|
-
| `⌘B` | Toggle
|
|
74
|
+
| `⌘B` | Toggle sidebar |
|
|
75
75
|
| `⌘P` | Quick open |
|
|
76
76
|
| `⌃H` | Replace |
|
|
77
77
|
| `⌥N` | Create file |
|
|
@@ -105,7 +105,7 @@ Editor shortcuts:
|
|
|
105
105
|
| `⌘⇧←` | Select to line start |
|
|
106
106
|
| `⌘⇧→` | Select to line end |
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
Sidebar shortcuts:
|
|
109
109
|
|
|
110
110
|
| Shortcut | Action |
|
|
111
111
|
| ----------- | ------------------------------------- |
|
|
@@ -156,27 +156,27 @@ Files unsupported by these tools are left unchanged and Riched shows a warning.
|
|
|
156
156
|
|
|
157
157
|
## Refresh
|
|
158
158
|
|
|
159
|
-
Use the `↻` title-bar button or `⌘R` to refresh the workspace. Refresh reloads the
|
|
159
|
+
Use the `↻` title-bar button or `⌘R` to refresh the workspace. Refresh reloads the sidebar and reloads the current buffer from disk. If the buffer has unsaved edits, Riched prompts to save, discard, or cancel before reloading.
|
|
160
160
|
|
|
161
161
|
## Creating Files
|
|
162
162
|
|
|
163
163
|
Use `⌥N` to create a file. Riched prompts for the file name and supports nested relative paths such as `notes/today.md`.
|
|
164
164
|
|
|
165
|
-
When a folder is highlighted in the
|
|
165
|
+
When a folder is highlighted in the sidebar, the new file is created inside that folder. Otherwise, the new file is created beside the currently open file. If no file is open, the new file is created in the project root.
|
|
166
166
|
|
|
167
167
|
If the requested file already exists, Riched opens it without overwriting its contents.
|
|
168
168
|
|
|
169
169
|
## Renaming Files And Folders
|
|
170
170
|
|
|
171
|
-
When the
|
|
171
|
+
When the sidebar is focused, `Enter` opens a rename prompt for the selected file or folder. Renaming keeps the open buffer connected to the renamed path when the current file, or a folder containing it, is renamed.
|
|
172
172
|
|
|
173
173
|
The project root cannot be renamed. Empty names, path separators, and duplicate target names are rejected in the prompt.
|
|
174
174
|
|
|
175
175
|
## Moving Files To Trash
|
|
176
176
|
|
|
177
|
-
When the
|
|
177
|
+
When the sidebar is focused, `⌘⌫` removes the selected file or folder by moving it to Trash. The command palette also shows `Move "<name>" to Trash` for the selected sidebar item. Both paths ask for confirmation before moving the item.
|
|
178
178
|
|
|
179
|
-
On stock Ghostty, `⌘⌫` is delivered to terminal apps as `⌃U`, so Riched binds both forms for the
|
|
179
|
+
On stock Ghostty, `⌘⌫` is delivered to terminal apps as `⌃U`, so Riched binds both forms for the sidebar.
|
|
180
180
|
|
|
181
181
|
## Limitations
|
|
182
182
|
|
|
@@ -71,9 +71,9 @@ from .syntax import (
|
|
|
71
71
|
supported_file_types,
|
|
72
72
|
)
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
SIDEBAR_DEFAULT_WIDTH = 30
|
|
75
|
+
SIDEBAR_MIN_WIDTH = 18
|
|
76
|
+
SIDEBAR_MAX_WIDTH = 44
|
|
77
77
|
MARKDOWN_SUFFIXES = {".md", ".markdown"}
|
|
78
78
|
FILE_TYPE_PICKER_WIDTH = 24
|
|
79
79
|
FILE_TYPE_PICKER_MAX_HEIGHT = 16
|
|
@@ -261,7 +261,7 @@ class RichedDirectoryTree(DirectoryTree):
|
|
|
261
261
|
for binding in DirectoryTree.BINDINGS
|
|
262
262
|
if _binding_key(binding) not in {"enter", "space"}
|
|
263
263
|
),
|
|
264
|
-
*build_static_bindings("
|
|
264
|
+
*build_static_bindings("sidebar"),
|
|
265
265
|
Binding("left", "collapse_cursor", "Collapse folder", show=False),
|
|
266
266
|
Binding("right", "expand_cursor", "Expand folder", show=False),
|
|
267
267
|
Binding("enter", "rename_cursor", "Rename", show=False),
|
|
@@ -297,7 +297,7 @@ class RichedDirectoryTree(DirectoryTree):
|
|
|
297
297
|
def action_quit_check(self) -> None:
|
|
298
298
|
app = self.app
|
|
299
299
|
if isinstance(app, RichedApp):
|
|
300
|
-
app.
|
|
300
|
+
app.action_sidebar_quit_check()
|
|
301
301
|
|
|
302
302
|
def action_rename_cursor(self) -> None:
|
|
303
303
|
if not self.has_focus:
|
|
@@ -307,7 +307,7 @@ class RichedDirectoryTree(DirectoryTree):
|
|
|
307
307
|
return
|
|
308
308
|
app = self.app
|
|
309
309
|
if isinstance(app, RichedApp):
|
|
310
|
-
app.
|
|
310
|
+
app.rename_sidebar_path(Path(node.data.path))
|
|
311
311
|
|
|
312
312
|
def action_trash_selected(self) -> None:
|
|
313
313
|
if not self.has_focus:
|
|
@@ -317,23 +317,23 @@ class RichedDirectoryTree(DirectoryTree):
|
|
|
317
317
|
return
|
|
318
318
|
app = self.app
|
|
319
319
|
if isinstance(app, RichedApp):
|
|
320
|
-
app.
|
|
320
|
+
app.trash_sidebar_path(Path(node.data.path))
|
|
321
321
|
|
|
322
322
|
|
|
323
|
-
class
|
|
324
|
-
"""Mouse handle for resizing the
|
|
323
|
+
class SidebarResizeHandle(Static):
|
|
324
|
+
"""Mouse handle for resizing the sidebar."""
|
|
325
325
|
|
|
326
326
|
can_focus = False
|
|
327
327
|
|
|
328
328
|
def __init__(self) -> None:
|
|
329
|
-
super().__init__("", id="
|
|
329
|
+
super().__init__("", id="sidebar-resize-handle")
|
|
330
330
|
self._drag_start_x: int | None = None
|
|
331
331
|
self._drag_start_width: int | None = None
|
|
332
332
|
|
|
333
333
|
def on_mouse_down(self, event: events.MouseDown) -> None:
|
|
334
334
|
if event.button != 1 or event.screen_x is None:
|
|
335
335
|
return
|
|
336
|
-
tree = self.app.query_one("#
|
|
336
|
+
tree = self.app.query_one("#sidebar", DirectoryTree)
|
|
337
337
|
self._drag_start_x = int(event.screen_x)
|
|
338
338
|
self._drag_start_width = tree.region.width
|
|
339
339
|
self.capture_mouse()
|
|
@@ -349,7 +349,7 @@ class FileTreeResizeHandle(Static):
|
|
|
349
349
|
delta = int(event.screen_x) - self._drag_start_x
|
|
350
350
|
app = self.app
|
|
351
351
|
if isinstance(app, RichedApp):
|
|
352
|
-
app.
|
|
352
|
+
app.set_sidebar_width(self._drag_start_width + delta)
|
|
353
353
|
event.stop()
|
|
354
354
|
event.prevent_default()
|
|
355
355
|
|
|
@@ -369,7 +369,7 @@ class RichedMarkdownViewer(MarkdownViewer):
|
|
|
369
369
|
|
|
370
370
|
BINDINGS = [
|
|
371
371
|
*MarkdownViewer.BINDINGS,
|
|
372
|
-
Binding("escape", "
|
|
372
|
+
Binding("escape", "focus_sidebar", "Focus sidebar", show=False),
|
|
373
373
|
]
|
|
374
374
|
|
|
375
375
|
async def go(self, location: str | Any) -> None:
|
|
@@ -380,14 +380,14 @@ class RichedMarkdownViewer(MarkdownViewer):
|
|
|
380
380
|
return
|
|
381
381
|
await super().go(location)
|
|
382
382
|
|
|
383
|
-
def
|
|
383
|
+
def action_focus_sidebar(self) -> None:
|
|
384
384
|
app = self.app
|
|
385
385
|
if isinstance(app, RichedApp):
|
|
386
|
-
app.
|
|
386
|
+
app.action_focus_sidebar()
|
|
387
387
|
|
|
388
388
|
|
|
389
389
|
class RichedApp(App):
|
|
390
|
-
"""TUI text editor with a
|
|
390
|
+
"""TUI text editor with a sidebar."""
|
|
391
391
|
|
|
392
392
|
COMMAND_PALETTE_BINDING = "f1"
|
|
393
393
|
|
|
@@ -398,20 +398,20 @@ class RichedApp(App):
|
|
|
398
398
|
#workspace {
|
|
399
399
|
height: 1fr;
|
|
400
400
|
}
|
|
401
|
-
#
|
|
401
|
+
#sidebar {
|
|
402
402
|
width: 30;
|
|
403
403
|
min-width: 18;
|
|
404
404
|
max-width: 44;
|
|
405
405
|
}
|
|
406
|
-
#
|
|
406
|
+
#sidebar-resize-handle {
|
|
407
407
|
width: 1;
|
|
408
408
|
min-width: 1;
|
|
409
409
|
max-width: 1;
|
|
410
410
|
height: 1fr;
|
|
411
411
|
background: $panel;
|
|
412
412
|
}
|
|
413
|
-
#
|
|
414
|
-
#
|
|
413
|
+
#sidebar-resize-handle:hover,
|
|
414
|
+
#sidebar-resize-handle.dragging {
|
|
415
415
|
background: $accent;
|
|
416
416
|
}
|
|
417
417
|
#editor {
|
|
@@ -477,6 +477,7 @@ class RichedApp(App):
|
|
|
477
477
|
self,
|
|
478
478
|
path: Path,
|
|
479
479
|
root: Path | None = None,
|
|
480
|
+
show_sidebar: bool = False,
|
|
480
481
|
) -> None:
|
|
481
482
|
super().__init__()
|
|
482
483
|
saved_theme = load_theme()
|
|
@@ -487,6 +488,7 @@ class RichedApp(App):
|
|
|
487
488
|
self._initial_path = None if initial_path.is_dir() else initial_path
|
|
488
489
|
self.path: Path | None = self._initial_path
|
|
489
490
|
self.root = root or Path.cwd()
|
|
491
|
+
self._show_sidebar_on_startup = show_sidebar
|
|
490
492
|
self._saved_text = ""
|
|
491
493
|
self._quick_open_entries: list[QuickOpenEntry] = []
|
|
492
494
|
self._quick_open_complete = False
|
|
@@ -538,13 +540,13 @@ class RichedApp(App):
|
|
|
538
540
|
),
|
|
539
541
|
]
|
|
540
542
|
)
|
|
541
|
-
selected = self.
|
|
543
|
+
selected = self._selected_sidebar_path()
|
|
542
544
|
if selected is not None and not self._is_project_root_path(selected):
|
|
543
545
|
commands.append(
|
|
544
546
|
SystemCommand(
|
|
545
547
|
f'Move "{selected.name}" to Trash',
|
|
546
|
-
"Move the selected
|
|
547
|
-
lambda path=selected: self.
|
|
548
|
+
"Move the selected sidebar item to Trash",
|
|
549
|
+
lambda path=selected: self.trash_sidebar_path(path),
|
|
548
550
|
)
|
|
549
551
|
)
|
|
550
552
|
yield from sorted(commands, key=lambda command: command.title.casefold())
|
|
@@ -563,19 +565,25 @@ class RichedApp(App):
|
|
|
563
565
|
def compose(self) -> ComposeResult:
|
|
564
566
|
yield RichedHeader()
|
|
565
567
|
with Horizontal(id="workspace"):
|
|
566
|
-
yield RichedDirectoryTree(str(self.root), id="
|
|
567
|
-
yield
|
|
568
|
+
yield RichedDirectoryTree(str(self.root), id="sidebar")
|
|
569
|
+
yield SidebarResizeHandle()
|
|
568
570
|
yield Container(id="editor-slot")
|
|
569
571
|
yield RichedFooter(show_command_palette=False)
|
|
570
572
|
|
|
571
573
|
def on_mount(self) -> None:
|
|
572
574
|
self.title = "Riched"
|
|
573
|
-
self.
|
|
574
|
-
|
|
575
|
+
self.set_sidebar_width(SIDEBAR_DEFAULT_WIDTH)
|
|
576
|
+
self._hide_sidebar()
|
|
577
|
+
if self._show_sidebar_on_startup:
|
|
578
|
+
self._show_sidebar()
|
|
579
|
+
if self._initial_path is None:
|
|
580
|
+
self.sub_title = ""
|
|
581
|
+
self._sidebar().focus()
|
|
582
|
+
return
|
|
583
|
+
if self._initial_path is not None:
|
|
584
|
+
self._open_path(self._initial_path)
|
|
585
|
+
else:
|
|
575
586
|
self.sub_title = ""
|
|
576
|
-
self.query_one("#file-tree", DirectoryTree).focus()
|
|
577
|
-
return
|
|
578
|
-
self._open_path(self._initial_path)
|
|
579
587
|
|
|
580
588
|
def _editor_or_none(self) -> TextArea | None:
|
|
581
589
|
editors = list(self.query("#editor"))
|
|
@@ -592,11 +600,11 @@ class RichedApp(App):
|
|
|
592
600
|
previews = list(self.query("#markdown-preview"))
|
|
593
601
|
return previews[0] if previews else None
|
|
594
602
|
|
|
595
|
-
def
|
|
596
|
-
return self.query_one("#
|
|
603
|
+
def _sidebar(self) -> DirectoryTree:
|
|
604
|
+
return self.query_one("#sidebar", DirectoryTree)
|
|
597
605
|
|
|
598
|
-
def
|
|
599
|
-
return self.query_one("#
|
|
606
|
+
def _sidebar_resize_handle(self) -> Static:
|
|
607
|
+
return self.query_one("#sidebar-resize-handle", Static)
|
|
600
608
|
|
|
601
609
|
def _current_file_type_label(self) -> str:
|
|
602
610
|
editor = self._editor_or_none()
|
|
@@ -612,23 +620,23 @@ class RichedApp(App):
|
|
|
612
620
|
def _set_markdown_toc_button_visible(self, visible: bool) -> None:
|
|
613
621
|
self._markdown_toc_button().styles.display = "block" if visible else "none"
|
|
614
622
|
|
|
615
|
-
def
|
|
616
|
-
return self.
|
|
623
|
+
def _is_sidebar_visible(self) -> bool:
|
|
624
|
+
return self._sidebar().styles.display != "none"
|
|
617
625
|
|
|
618
|
-
def
|
|
626
|
+
def _set_sidebar_visible(self, visible: bool) -> None:
|
|
619
627
|
display = "block" if visible else "none"
|
|
620
|
-
self.
|
|
621
|
-
self.
|
|
628
|
+
self._sidebar().styles.display = display
|
|
629
|
+
self._sidebar_resize_handle().styles.display = display
|
|
622
630
|
|
|
623
|
-
def
|
|
624
|
-
self.
|
|
631
|
+
def _show_sidebar(self) -> None:
|
|
632
|
+
self._set_sidebar_visible(True)
|
|
625
633
|
|
|
626
|
-
def
|
|
627
|
-
self.
|
|
634
|
+
def _hide_sidebar(self) -> None:
|
|
635
|
+
self._set_sidebar_visible(False)
|
|
628
636
|
|
|
629
|
-
def
|
|
630
|
-
clamped_width = max(
|
|
631
|
-
tree = self.
|
|
637
|
+
def set_sidebar_width(self, width: int) -> None:
|
|
638
|
+
clamped_width = max(SIDEBAR_MIN_WIDTH, min(SIDEBAR_MAX_WIDTH, width))
|
|
639
|
+
tree = self._sidebar()
|
|
632
640
|
tree.styles.width = clamped_width
|
|
633
641
|
tree.refresh(layout=True)
|
|
634
642
|
|
|
@@ -658,12 +666,12 @@ class RichedApp(App):
|
|
|
658
666
|
self.path = None
|
|
659
667
|
self.sub_title = ""
|
|
660
668
|
self._saved_text = ""
|
|
661
|
-
self.
|
|
662
|
-
self.
|
|
669
|
+
self._show_sidebar()
|
|
670
|
+
self._sidebar().focus()
|
|
663
671
|
self._update_file_type_button()
|
|
664
672
|
|
|
665
|
-
def
|
|
666
|
-
tree = self.
|
|
673
|
+
def _selected_sidebar_path(self) -> Path | None:
|
|
674
|
+
tree = self._sidebar()
|
|
667
675
|
node = tree.cursor_node
|
|
668
676
|
if node is None or node.data is None:
|
|
669
677
|
return None
|
|
@@ -711,7 +719,7 @@ class RichedApp(App):
|
|
|
711
719
|
message = f"trash-put exited with status {result.returncode}"
|
|
712
720
|
raise RuntimeError(message)
|
|
713
721
|
|
|
714
|
-
def
|
|
722
|
+
def trash_sidebar_path(self, path: Path) -> None:
|
|
715
723
|
selected = path.expanduser().resolve(strict=False)
|
|
716
724
|
if self._is_project_root_path(selected):
|
|
717
725
|
self.notify("Cannot move the project root to Trash.", severity="warning")
|
|
@@ -730,17 +738,17 @@ class RichedApp(App):
|
|
|
730
738
|
|
|
731
739
|
def handle(choice: str) -> None:
|
|
732
740
|
if choice == "trash":
|
|
733
|
-
self.
|
|
741
|
+
self._apply_trash_sidebar_path(source)
|
|
734
742
|
return
|
|
735
|
-
self.
|
|
743
|
+
self._sidebar().focus()
|
|
736
744
|
|
|
737
745
|
self.push_screen(
|
|
738
746
|
TrashPathConfirmationScreen(source.name, source.is_dir()),
|
|
739
747
|
handle,
|
|
740
748
|
)
|
|
741
749
|
|
|
742
|
-
def
|
|
743
|
-
tree = self.
|
|
750
|
+
def _apply_trash_sidebar_path(self, path: Path) -> None:
|
|
751
|
+
tree = self._sidebar()
|
|
744
752
|
selected = path.expanduser().resolve(strict=False)
|
|
745
753
|
try:
|
|
746
754
|
self._trash_path(selected)
|
|
@@ -757,28 +765,28 @@ class RichedApp(App):
|
|
|
757
765
|
self._invalidate_quick_open_index()
|
|
758
766
|
self.notify(f"Moved {selected.name} to Trash")
|
|
759
767
|
|
|
760
|
-
def
|
|
768
|
+
def rename_sidebar_path(self, path: Path) -> None:
|
|
761
769
|
selected = path.expanduser().resolve(strict=False)
|
|
762
770
|
root = self.root.expanduser().resolve(strict=False)
|
|
763
771
|
if selected == root:
|
|
764
772
|
self.notify("Cannot rename the project root.", severity="warning")
|
|
765
|
-
self.
|
|
773
|
+
self._sidebar().focus()
|
|
766
774
|
return
|
|
767
775
|
|
|
768
776
|
source = path.expanduser()
|
|
769
777
|
|
|
770
778
|
def validate(new_name: str) -> str | None:
|
|
771
|
-
return self.
|
|
779
|
+
return self._validate_sidebar_rename(source, new_name)
|
|
772
780
|
|
|
773
781
|
def handle(new_name: str | None) -> None:
|
|
774
782
|
if new_name is None:
|
|
775
|
-
self.
|
|
783
|
+
self._sidebar().focus()
|
|
776
784
|
return
|
|
777
|
-
self.
|
|
785
|
+
self._apply_sidebar_rename(source, new_name)
|
|
778
786
|
|
|
779
787
|
self.push_screen(RenamePathScreen(source.name, validate), handle)
|
|
780
788
|
|
|
781
|
-
def
|
|
789
|
+
def _validate_sidebar_rename(self, source: Path, new_name: str) -> str | None:
|
|
782
790
|
if not new_name.strip():
|
|
783
791
|
return "Name cannot be empty."
|
|
784
792
|
if new_name in {".", ".."}:
|
|
@@ -798,7 +806,7 @@ class RichedApp(App):
|
|
|
798
806
|
pass
|
|
799
807
|
return "An item with that name already exists."
|
|
800
808
|
|
|
801
|
-
def
|
|
809
|
+
def _apply_sidebar_rename(self, source: Path, new_name: str) -> None:
|
|
802
810
|
old_path = source.expanduser().resolve(strict=False)
|
|
803
811
|
destination = source.with_name(new_name).expanduser()
|
|
804
812
|
new_path = destination.resolve(strict=False)
|
|
@@ -813,7 +821,7 @@ class RichedApp(App):
|
|
|
813
821
|
source.rename(destination)
|
|
814
822
|
except Exception as exc:
|
|
815
823
|
self.notify(f"Rename failed: {exc}", severity="error")
|
|
816
|
-
self.
|
|
824
|
+
self._sidebar().focus()
|
|
817
825
|
return
|
|
818
826
|
|
|
819
827
|
if old_open_path is not None:
|
|
@@ -823,7 +831,7 @@ class RichedApp(App):
|
|
|
823
831
|
relative_open_path = None
|
|
824
832
|
if relative_open_path is not None:
|
|
825
833
|
assert open_display_path is not None
|
|
826
|
-
self.path = self.
|
|
834
|
+
self.path = self._open_path_after_sidebar_rename(
|
|
827
835
|
open_display_path,
|
|
828
836
|
old_path,
|
|
829
837
|
new_name,
|
|
@@ -841,12 +849,12 @@ class RichedApp(App):
|
|
|
841
849
|
)
|
|
842
850
|
self._update_file_type_button()
|
|
843
851
|
|
|
844
|
-
self.
|
|
852
|
+
self._sidebar().reload()
|
|
845
853
|
self._invalidate_quick_open_index()
|
|
846
|
-
self.
|
|
854
|
+
self._sidebar().focus()
|
|
847
855
|
self.notify(f"Renamed {old_path.name} to {new_path.name}")
|
|
848
856
|
|
|
849
|
-
def
|
|
857
|
+
def _open_path_after_sidebar_rename(
|
|
850
858
|
self,
|
|
851
859
|
open_path: Path,
|
|
852
860
|
old_path: Path,
|
|
@@ -906,11 +914,11 @@ class RichedApp(App):
|
|
|
906
914
|
self.push_screen(UnsavedChangesScreen(), handle)
|
|
907
915
|
|
|
908
916
|
def _refresh_workspace_clean(self) -> None:
|
|
909
|
-
self.
|
|
917
|
+
self._sidebar().reload()
|
|
910
918
|
self._invalidate_quick_open_index()
|
|
911
919
|
if self.path is None:
|
|
912
|
-
self.
|
|
913
|
-
self.
|
|
920
|
+
self._show_sidebar()
|
|
921
|
+
self._sidebar().focus()
|
|
914
922
|
self.notify("Refreshed")
|
|
915
923
|
return
|
|
916
924
|
|
|
@@ -942,7 +950,7 @@ class RichedApp(App):
|
|
|
942
950
|
self._after_saved_or_discarded(lambda: self._open_path(selected))
|
|
943
951
|
|
|
944
952
|
def _create_file_base(self) -> Path:
|
|
945
|
-
tree = self.
|
|
953
|
+
tree = self._sidebar()
|
|
946
954
|
node = tree.cursor_node
|
|
947
955
|
root = self.root.expanduser().resolve(strict=False)
|
|
948
956
|
if (
|
|
@@ -999,7 +1007,7 @@ class RichedApp(App):
|
|
|
999
1007
|
except Exception as exc:
|
|
1000
1008
|
self.notify(f"Create file failed: {exc}", severity="error")
|
|
1001
1009
|
return
|
|
1002
|
-
self.
|
|
1010
|
+
self._sidebar().reload()
|
|
1003
1011
|
self._invalidate_quick_open_index()
|
|
1004
1012
|
self._open_path(target)
|
|
1005
1013
|
if existed:
|
|
@@ -1539,7 +1547,7 @@ class RichedApp(App):
|
|
|
1539
1547
|
def action_quit_check(self) -> None:
|
|
1540
1548
|
self._after_saved_or_discarded(self.exit)
|
|
1541
1549
|
|
|
1542
|
-
def
|
|
1550
|
+
def action_sidebar_quit_check(self) -> None:
|
|
1543
1551
|
if self._is_dirty():
|
|
1544
1552
|
self.action_quit_check()
|
|
1545
1553
|
return
|
|
@@ -1555,19 +1563,19 @@ class RichedApp(App):
|
|
|
1555
1563
|
return
|
|
1556
1564
|
self._after_saved_or_discarded(self._close_buffer)
|
|
1557
1565
|
|
|
1558
|
-
def
|
|
1566
|
+
def action_toggle_sidebar(self) -> None:
|
|
1559
1567
|
editor = self._editor_or_none()
|
|
1560
|
-
if self.
|
|
1561
|
-
self.
|
|
1568
|
+
if self._is_sidebar_visible():
|
|
1569
|
+
self._hide_sidebar()
|
|
1562
1570
|
if editor is not None:
|
|
1563
1571
|
editor.focus()
|
|
1564
1572
|
return
|
|
1565
|
-
self.
|
|
1573
|
+
self._show_sidebar()
|
|
1566
1574
|
|
|
1567
|
-
def
|
|
1568
|
-
tree = self.
|
|
1569
|
-
if not self.
|
|
1570
|
-
self.
|
|
1575
|
+
def action_toggle_sidebar_focus(self) -> None:
|
|
1576
|
+
tree = self._sidebar()
|
|
1577
|
+
if not self._is_sidebar_visible():
|
|
1578
|
+
self._show_sidebar()
|
|
1571
1579
|
tree.focus()
|
|
1572
1580
|
return
|
|
1573
1581
|
editor = self._editor_or_none()
|
|
@@ -1577,7 +1585,7 @@ class RichedApp(App):
|
|
|
1577
1585
|
return
|
|
1578
1586
|
tree.focus()
|
|
1579
1587
|
|
|
1580
|
-
def
|
|
1581
|
-
if not self.
|
|
1582
|
-
self.
|
|
1583
|
-
self.
|
|
1588
|
+
def action_focus_sidebar(self) -> None:
|
|
1589
|
+
if not self._is_sidebar_visible():
|
|
1590
|
+
self._show_sidebar()
|
|
1591
|
+
self._sidebar().focus()
|
|
@@ -8,8 +8,8 @@ app:
|
|
|
8
8
|
key: f1
|
|
9
9
|
display_keys:
|
|
10
10
|
- f1
|
|
11
|
-
- name:
|
|
12
|
-
description: Toggle
|
|
11
|
+
- name: toggle_sidebar
|
|
12
|
+
description: Toggle sidebar
|
|
13
13
|
key: cmd+b,super+b
|
|
14
14
|
- name: quick_open
|
|
15
15
|
description: Quick open
|
|
@@ -129,7 +129,7 @@ editor:
|
|
|
129
129
|
description: Select to line end
|
|
130
130
|
show: false
|
|
131
131
|
|
|
132
|
-
|
|
132
|
+
sidebar:
|
|
133
133
|
- key: cmd+backspace,super+backspace,ctrl+u
|
|
134
134
|
action: trash_selected
|
|
135
135
|
description: Move to Trash
|
|
@@ -17,6 +17,11 @@ def main() -> int:
|
|
|
17
17
|
action="store_true",
|
|
18
18
|
help="Show the version and exit",
|
|
19
19
|
)
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
"--sidebar",
|
|
22
|
+
action="store_true",
|
|
23
|
+
help="Open sidebar at startup",
|
|
24
|
+
)
|
|
20
25
|
parser.add_argument(
|
|
21
26
|
"filename",
|
|
22
27
|
nargs="?",
|
|
@@ -36,5 +41,5 @@ def main() -> int:
|
|
|
36
41
|
class ConfiguredRichEditorApp(RichedApp):
|
|
37
42
|
BINDINGS = build_bindings()
|
|
38
43
|
|
|
39
|
-
ConfiguredRichEditorApp(path, root).run()
|
|
44
|
+
ConfiguredRichEditorApp(path, root, show_sidebar=args.sidebar).run()
|
|
40
45
|
return 0
|
|
@@ -60,8 +60,8 @@ class RichedTextArea(TextArea):
|
|
|
60
60
|
from .app import RichedApp
|
|
61
61
|
|
|
62
62
|
app = self.app
|
|
63
|
-
if isinstance(app, RichedApp) and not app.
|
|
64
|
-
app.
|
|
63
|
+
if isinstance(app, RichedApp) and not app._is_sidebar_visible():
|
|
64
|
+
app.action_sidebar_quit_check()
|
|
65
65
|
event.stop()
|
|
66
66
|
event.prevent_default()
|
|
67
67
|
return
|
|
@@ -131,7 +131,7 @@ class QuitConfirmationScreen(_ConfirmationScreen):
|
|
|
131
131
|
|
|
132
132
|
|
|
133
133
|
class TrashPathConfirmationScreen(_ConfirmationScreen):
|
|
134
|
-
"""Prompt asking whether to move a
|
|
134
|
+
"""Prompt asking whether to move a sidebar item to Trash."""
|
|
135
135
|
|
|
136
136
|
BINDINGS = build_screen_bindings("trash_path_confirmation")
|
|
137
137
|
BUTTONS = (
|
|
@@ -170,7 +170,7 @@ class TrashPathConfirmationScreen(_ConfirmationScreen):
|
|
|
170
170
|
|
|
171
171
|
|
|
172
172
|
class RenamePathScreen(ModalScreen[str | None]):
|
|
173
|
-
"""Prompt for a new
|
|
173
|
+
"""Prompt for a new sidebar item name."""
|
|
174
174
|
|
|
175
175
|
BINDINGS = build_screen_bindings("rename_path")
|
|
176
176
|
|
|
@@ -44,7 +44,8 @@ def _find_tree_node(node: Any, path: Path) -> Any:
|
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
async def _select_tree_path(pilot: Any, app: Any, path: Path) -> DirectoryTree:
|
|
47
|
-
|
|
47
|
+
app._show_sidebar()
|
|
48
|
+
tree = app.query_one("#sidebar", DirectoryTree)
|
|
48
49
|
await tree.reload()
|
|
49
50
|
await pilot.pause()
|
|
50
51
|
tree.focus()
|
|
@@ -112,7 +113,7 @@ async def test_command_palette_includes_toggle_markdown_preview() -> None:
|
|
|
112
113
|
)
|
|
113
114
|
|
|
114
115
|
|
|
115
|
-
async def
|
|
116
|
+
async def test_command_palette_includes_selected_sidebar_item_trash_command() -> None:
|
|
116
117
|
tmp, _, app = _file_app("palette.txt", "palette", root_is_tmp=True)
|
|
117
118
|
selected = tmp / "archive"
|
|
118
119
|
selected.mkdir()
|
|
@@ -71,7 +71,7 @@ async def test_create_file_uses_open_file_folder() -> None:
|
|
|
71
71
|
assert not (tmp / "new.txt").exists()
|
|
72
72
|
|
|
73
73
|
|
|
74
|
-
async def
|
|
74
|
+
async def test_create_file_uses_highlighted_sidebar_folder() -> None:
|
|
75
75
|
tmp, current, app = _file_app("folder/current.txt", "current", root_is_tmp=True)
|
|
76
76
|
selected = tmp / "selected"
|
|
77
77
|
selected.mkdir()
|
|
@@ -79,7 +79,8 @@ async def test_create_file_uses_highlighted_file_tree_folder() -> None:
|
|
|
79
79
|
fallback = current.parent / "new.txt"
|
|
80
80
|
async with app.run_test() as pilot:
|
|
81
81
|
await pilot.pause()
|
|
82
|
-
|
|
82
|
+
app._show_sidebar()
|
|
83
|
+
tree = app.query_one("#sidebar", DirectoryTree)
|
|
83
84
|
await tree.reload()
|
|
84
85
|
await pilot.pause()
|
|
85
86
|
tree.focus()
|
|
@@ -121,7 +122,7 @@ async def test_create_file_without_open_buffer_uses_project_root() -> None:
|
|
|
121
122
|
assert target.exists()
|
|
122
123
|
assert app.path == target.resolve(strict=False)
|
|
123
124
|
assert _editor(app).text == ""
|
|
124
|
-
tree = app.query_one("#
|
|
125
|
+
tree = app.query_one("#sidebar", DirectoryTree)
|
|
125
126
|
assert _tree_has_path(tree.root, target)
|
|
126
127
|
|
|
127
128
|
|
|
@@ -15,7 +15,8 @@ async def _make_dirty(app, pilot, text: str = "changed") -> None:
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
async def _open_quit_modal(app, pilot) -> None:
|
|
18
|
-
app.
|
|
18
|
+
app._show_sidebar()
|
|
19
|
+
app._sidebar().focus()
|
|
19
20
|
await pilot.pause()
|
|
20
21
|
await _press(pilot, "escape")
|
|
21
22
|
|
|
@@ -68,37 +69,35 @@ async def test_quit_dirty_discard_keeps_file() -> None:
|
|
|
68
69
|
assert f.read_text() == "orig"
|
|
69
70
|
|
|
70
71
|
|
|
71
|
-
async def
|
|
72
|
+
async def test_editor_escape_visible_sidebar_does_nothing() -> None:
|
|
72
73
|
_, f, app = _file_app("editor-escape-visible.txt", "foo", root_is_tmp=True)
|
|
73
74
|
async with app.run_test() as pilot:
|
|
75
|
+
await pilot.pause()
|
|
76
|
+
app._show_sidebar()
|
|
74
77
|
await pilot.pause()
|
|
75
78
|
await _press(pilot, "escape")
|
|
76
79
|
assert app.path == f, app.path
|
|
77
80
|
assert list(app.query("#editor"))
|
|
78
81
|
|
|
79
82
|
|
|
80
|
-
async def
|
|
83
|
+
async def test_editor_escape_hidden_sidebar_clean_shows_quit_confirmation() -> None:
|
|
81
84
|
_, f, app = _file_app("editor-escape-clean.txt", "foo", root_is_tmp=True)
|
|
82
85
|
async with app.run_test() as pilot:
|
|
83
|
-
await pilot.pause()
|
|
84
|
-
await _press(pilot, "super+b")
|
|
85
86
|
await pilot.pause()
|
|
86
87
|
await _press(pilot, "escape")
|
|
87
88
|
assert isinstance(app.screen, mod.QuitConfirmationScreen)
|
|
88
89
|
|
|
89
90
|
|
|
90
|
-
async def
|
|
91
|
+
async def test_editor_escape_hidden_sidebar_dirty_shows_unsaved_changes() -> None:
|
|
91
92
|
_, f, app = _file_app("editor-escape-dirty.txt", "orig", root_is_tmp=True)
|
|
92
93
|
async with app.run_test() as pilot:
|
|
93
94
|
await pilot.pause()
|
|
94
95
|
await _make_dirty(app, pilot)
|
|
95
|
-
await _press(pilot, "super+b")
|
|
96
|
-
await pilot.pause()
|
|
97
96
|
await _press(pilot, "escape")
|
|
98
97
|
assert isinstance(app.screen, mod.UnsavedChangesScreen)
|
|
99
98
|
|
|
100
99
|
|
|
101
|
-
async def
|
|
100
|
+
async def test_sidebar_escape_clean_shows_quit_confirmation_then_cancel() -> None:
|
|
102
101
|
_, f, app = _file_app("tree-escape-clean.txt", "foo", root_is_tmp=True)
|
|
103
102
|
async with app.run_test() as pilot:
|
|
104
103
|
await pilot.pause()
|
|
@@ -110,7 +109,7 @@ async def test_file_tree_escape_clean_shows_quit_confirmation_then_cancel() -> N
|
|
|
110
109
|
assert app.path == f, app.path
|
|
111
110
|
|
|
112
111
|
|
|
113
|
-
async def
|
|
112
|
+
async def test_sidebar_escape_clean_quit_exits() -> None:
|
|
114
113
|
_, _, app = _file_app("tree-escape-quit.txt", "foo", root_is_tmp=True)
|
|
115
114
|
async with app.run_test() as pilot:
|
|
116
115
|
await pilot.pause()
|
|
@@ -120,7 +119,7 @@ async def test_file_tree_escape_clean_quit_exits() -> None:
|
|
|
120
119
|
await pilot.pause()
|
|
121
120
|
|
|
122
121
|
|
|
123
|
-
async def
|
|
122
|
+
async def test_sidebar_escape_dirty_shows_unsaved_changes() -> None:
|
|
124
123
|
_, f, app = _file_app("tree-escape-dirty.txt", "orig", root_is_tmp=True)
|
|
125
124
|
async with app.run_test() as pilot:
|
|
126
125
|
await pilot.pause()
|
|
@@ -347,7 +347,7 @@ async def test_alt_z_toggles_word_wrap() -> None:
|
|
|
347
347
|
assert editor.soft_wrap is False
|
|
348
348
|
|
|
349
349
|
|
|
350
|
-
async def
|
|
350
|
+
async def test_sidebar_toggle_aliases_with_and_without_open_buffer() -> None:
|
|
351
351
|
for key, with_buffer in (
|
|
352
352
|
("cmd+b", True),
|
|
353
353
|
("super+b", True),
|
|
@@ -360,12 +360,12 @@ async def test_file_tree_toggle_aliases_with_and_without_open_buffer() -> None:
|
|
|
360
360
|
_, app = _directory_app()
|
|
361
361
|
async with app.run_test() as pilot:
|
|
362
362
|
await pilot.pause()
|
|
363
|
-
tree = app.query_one("#
|
|
364
|
-
assert tree.styles.display == "
|
|
365
|
-
await _press(pilot, key)
|
|
366
|
-
assert tree.styles.display == "none", key
|
|
363
|
+
tree = app.query_one("#sidebar", DirectoryTree)
|
|
364
|
+
assert tree.styles.display == "none"
|
|
367
365
|
await _press(pilot, key)
|
|
368
366
|
assert tree.styles.display == "block", key
|
|
367
|
+
await _press(pilot, key)
|
|
368
|
+
assert tree.styles.display == "none", key
|
|
369
369
|
|
|
370
370
|
|
|
371
371
|
async def test_undo_multiline_insert_that_removes_scrollbar() -> None:
|
|
@@ -31,7 +31,7 @@ async def test_open_directory_starts_with_no_buffer() -> None:
|
|
|
31
31
|
f.write_text("content")
|
|
32
32
|
async with app.run_test() as pilot:
|
|
33
33
|
await pilot.pause()
|
|
34
|
-
tree = app.query_one("#
|
|
34
|
+
tree = app.query_one("#sidebar", DirectoryTree)
|
|
35
35
|
assert Path(tree.path) == tmp, tree.path
|
|
36
36
|
assert app.path is None, app.path
|
|
37
37
|
assert not list(app.query("#editor"))
|
|
@@ -35,7 +35,7 @@ async def test_footer_uses_macos_modifier_symbols_with_preferred_markdown_previe
|
|
|
35
35
|
assert set(labels) == {
|
|
36
36
|
"Save",
|
|
37
37
|
"Command palette",
|
|
38
|
-
"Toggle
|
|
38
|
+
"Toggle sidebar",
|
|
39
39
|
"Quick open",
|
|
40
40
|
"Replace",
|
|
41
41
|
"Create file",
|
|
@@ -45,7 +45,7 @@ async def test_footer_uses_macos_modifier_symbols_with_preferred_markdown_previe
|
|
|
45
45
|
}
|
|
46
46
|
assert labels["Save"] == "⌘S"
|
|
47
47
|
assert labels["Command palette"] == "F1"
|
|
48
|
-
assert labels["Toggle
|
|
48
|
+
assert labels["Toggle sidebar"] == "⌘B"
|
|
49
49
|
assert labels["Quick open"] == "⌘P"
|
|
50
50
|
assert labels["Replace"] == "⌃H"
|
|
51
51
|
assert labels["Create file"] == "⌥N"
|
|
@@ -118,7 +118,8 @@ async def test_format_document_refocuses_editor_so_cmd_z_undoes() -> None:
|
|
|
118
118
|
async with app.run_test() as pilot:
|
|
119
119
|
await pilot.pause()
|
|
120
120
|
editor = _editor(app)
|
|
121
|
-
|
|
121
|
+
app._show_sidebar()
|
|
122
|
+
app._sidebar().focus()
|
|
122
123
|
await pilot.pause()
|
|
123
124
|
|
|
124
125
|
await _press(pilot, "alt+shift+f")
|
|
@@ -147,14 +147,16 @@ async def test_markdown_preview_external_link_opens_without_navigation() -> None
|
|
|
147
147
|
assert preview.document.source == "[Rich](https://github.com/Textualize/rich)"
|
|
148
148
|
|
|
149
149
|
|
|
150
|
-
async def
|
|
150
|
+
async def test_markdown_preview_escape_focuses_sidebar() -> None:
|
|
151
151
|
_, _, app = _file_app("README.md", "# Title", root_is_tmp=True)
|
|
152
152
|
async with app.run_test() as pilot:
|
|
153
153
|
await pilot.pause()
|
|
154
154
|
|
|
155
155
|
await _press(pilot, "cmd+shift+v")
|
|
156
156
|
preview = app.query_one("#markdown-preview", MarkdownViewer)
|
|
157
|
-
|
|
157
|
+
app._show_sidebar()
|
|
158
|
+
tree = app.query_one("#sidebar", DirectoryTree)
|
|
159
|
+
await pilot.pause()
|
|
158
160
|
assert preview.document.has_focus
|
|
159
161
|
|
|
160
162
|
await _press(pilot, "escape")
|
|
@@ -24,12 +24,13 @@ async def test_refresh_button_exists_in_header() -> None:
|
|
|
24
24
|
assert refresh_button.content == "↻"
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
async def
|
|
27
|
+
async def test_refresh_button_reloads_sidebar() -> None:
|
|
28
28
|
tmp, _, app = _file_app("first.txt", "first", root_is_tmp=True)
|
|
29
29
|
created = tmp / "created.txt"
|
|
30
30
|
async with app.run_test() as pilot:
|
|
31
31
|
await pilot.pause()
|
|
32
|
-
|
|
32
|
+
app._show_sidebar()
|
|
33
|
+
tree = app.query_one("#sidebar", DirectoryTree)
|
|
33
34
|
await tree.reload()
|
|
34
35
|
await pilot.pause()
|
|
35
36
|
assert not _tree_has_path(tree.root, created)
|
|
@@ -104,25 +105,28 @@ async def test_refresh_dirty_cancel_leaves_buffer_unchanged() -> None:
|
|
|
104
105
|
assert _editor(app).text == "changed"
|
|
105
106
|
|
|
106
107
|
|
|
107
|
-
async def
|
|
108
|
+
async def test_refresh_deleted_open_file_closes_buffer_and_focuses_sidebar() -> None:
|
|
108
109
|
_, path, app = _file_app("missing-refresh.txt", "orig", root_is_tmp=True)
|
|
109
110
|
async with app.run_test() as pilot:
|
|
110
111
|
await pilot.pause()
|
|
111
112
|
path.unlink()
|
|
112
113
|
await _press(pilot, "cmd+r")
|
|
113
114
|
|
|
114
|
-
tree = app.query_one("#
|
|
115
|
+
tree = app.query_one("#sidebar", DirectoryTree)
|
|
115
116
|
assert app.path is None
|
|
116
117
|
assert not list(app.query("#editor"))
|
|
117
118
|
assert tree.has_focus
|
|
118
119
|
|
|
119
120
|
|
|
120
|
-
async def
|
|
121
|
+
async def test_refresh_no_buffer_reloads_sidebar_and_keeps_sidebar_focus() -> None:
|
|
121
122
|
tmp, app = _directory_app()
|
|
122
123
|
created = tmp / "created.txt"
|
|
123
124
|
async with app.run_test() as pilot:
|
|
124
125
|
await pilot.pause()
|
|
125
|
-
|
|
126
|
+
app._show_sidebar()
|
|
127
|
+
tree = app.query_one("#sidebar", DirectoryTree)
|
|
128
|
+
tree.focus()
|
|
129
|
+
await pilot.pause()
|
|
126
130
|
assert tree.has_focus
|
|
127
131
|
|
|
128
132
|
created.write_text("created")
|
|
@@ -25,7 +25,8 @@ def _find_tree_node(node: Any, path: Path) -> Any:
|
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
async def _select_tree_path(pilot: Any, app: Any, path: Path) -> DirectoryTree:
|
|
28
|
-
|
|
28
|
+
app._show_sidebar()
|
|
29
|
+
tree = app.query_one("#sidebar", DirectoryTree)
|
|
29
30
|
await tree.reload()
|
|
30
31
|
await pilot.pause()
|
|
31
32
|
tree.focus()
|
|
@@ -48,15 +49,16 @@ def _stub_trash(app: Any) -> list[Path]:
|
|
|
48
49
|
return trashed
|
|
49
50
|
|
|
50
51
|
|
|
51
|
-
async def
|
|
52
|
+
async def test_sidebar_is_rooted_at_project_dir() -> None:
|
|
52
53
|
tmp, _, app = _file_app("tree.txt", "tree", root_is_tmp=True)
|
|
53
54
|
async with app.run_test() as pilot:
|
|
54
55
|
await pilot.pause()
|
|
55
|
-
|
|
56
|
+
app._show_sidebar()
|
|
57
|
+
tree = app.query_one("#sidebar", DirectoryTree)
|
|
56
58
|
assert Path(tree.path) == tmp, tree.path
|
|
57
59
|
|
|
58
60
|
|
|
59
|
-
async def
|
|
61
|
+
async def test_sidebar_switch_opens_selected_file() -> None:
|
|
60
62
|
tmp, first, app = _file_app("first.txt", "first", root_is_tmp=True)
|
|
61
63
|
second = tmp / "second.txt"
|
|
62
64
|
second.write_text("second")
|
|
@@ -68,7 +70,7 @@ async def test_file_tree_switch_opens_selected_file() -> None:
|
|
|
68
70
|
assert app.path == second, app.path
|
|
69
71
|
|
|
70
72
|
|
|
71
|
-
async def
|
|
73
|
+
async def test_sidebar_dirty_switch_prompts_then_discard_opens_file() -> None:
|
|
72
74
|
tmp, first, app = _file_app("first.txt", "first", root_is_tmp=True)
|
|
73
75
|
second = tmp / "second.txt"
|
|
74
76
|
second.write_text("second")
|
|
@@ -86,7 +88,7 @@ async def test_file_tree_dirty_switch_prompts_then_discard_opens_file() -> None:
|
|
|
86
88
|
assert first.read_text() == "first"
|
|
87
89
|
|
|
88
90
|
|
|
89
|
-
async def
|
|
91
|
+
async def test_sidebar_enter_renames_selected_file_without_opening_it() -> None:
|
|
90
92
|
tmp, first, app = _file_app("first.txt", "first", root_is_tmp=True)
|
|
91
93
|
selected = tmp / "second.txt"
|
|
92
94
|
renamed = tmp / "renamed.txt"
|
|
@@ -110,7 +112,7 @@ async def test_file_tree_enter_renames_selected_file_without_opening_it() -> Non
|
|
|
110
112
|
assert _editor(app).text == "first"
|
|
111
113
|
|
|
112
114
|
|
|
113
|
-
async def
|
|
115
|
+
async def test_sidebar_rename_cancel_leaves_item_unchanged_and_refocuses_tree() -> None:
|
|
114
116
|
tmp, _, app = _file_app("first.txt", "first", root_is_tmp=True)
|
|
115
117
|
selected = tmp / "second.txt"
|
|
116
118
|
selected.write_text("second")
|
|
@@ -130,7 +132,7 @@ async def test_file_tree_rename_cancel_leaves_item_unchanged_and_refocuses_tree(
|
|
|
130
132
|
assert tree.has_focus
|
|
131
133
|
|
|
132
134
|
|
|
133
|
-
async def
|
|
135
|
+
async def test_sidebar_enter_renames_selected_folder() -> None:
|
|
134
136
|
tmp, _, app = _file_app("first.txt", "first", root_is_tmp=True)
|
|
135
137
|
selected = tmp / "folder"
|
|
136
138
|
renamed = tmp / "renamed"
|
|
@@ -197,7 +199,7 @@ async def test_renaming_open_file_parent_updates_open_path() -> None:
|
|
|
197
199
|
assert (renamed / "open.txt").read_text() == "open"
|
|
198
200
|
|
|
199
201
|
|
|
200
|
-
async def
|
|
202
|
+
async def test_sidebar_space_still_opens_files_and_toggles_folders() -> None:
|
|
201
203
|
tmp, _, app = _file_app("first.txt", "first", root_is_tmp=True)
|
|
202
204
|
selected = tmp / "second.txt"
|
|
203
205
|
selected.write_text("second")
|
|
@@ -223,7 +225,7 @@ async def test_file_tree_space_still_opens_files_and_toggles_folders() -> None:
|
|
|
223
225
|
assert node.is_expanded is not was_expanded
|
|
224
226
|
|
|
225
227
|
|
|
226
|
-
async def
|
|
228
|
+
async def test_sidebar_rename_duplicate_or_invalid_name_stays_open() -> None:
|
|
227
229
|
tmp, _, app = _file_app("first.txt", "first", root_is_tmp=True)
|
|
228
230
|
selected = tmp / "second.txt"
|
|
229
231
|
selected.write_text("second")
|
|
@@ -256,7 +258,7 @@ async def test_file_tree_rename_duplicate_or_invalid_name_stays_open() -> None:
|
|
|
256
258
|
assert selected.read_text() == "second"
|
|
257
259
|
|
|
258
260
|
|
|
259
|
-
async def
|
|
261
|
+
async def test_sidebar_cmd_backspace_moves_selected_file_to_trash() -> None:
|
|
260
262
|
tmp, _, app = _file_app("first.txt", "first", root_is_tmp=True)
|
|
261
263
|
selected = tmp / "second.txt"
|
|
262
264
|
selected.write_text("second")
|
|
@@ -274,7 +276,7 @@ async def test_file_tree_cmd_backspace_moves_selected_file_to_trash() -> None:
|
|
|
274
276
|
assert not selected.exists()
|
|
275
277
|
|
|
276
278
|
|
|
277
|
-
async def
|
|
279
|
+
async def test_sidebar_ctrl_u_moves_selected_file_to_trash() -> None:
|
|
278
280
|
tmp, _, app = _file_app("first.txt", "first", root_is_tmp=True)
|
|
279
281
|
selected = tmp / "second.txt"
|
|
280
282
|
selected.write_text("second")
|
|
@@ -292,7 +294,7 @@ async def test_file_tree_ctrl_u_moves_selected_file_to_trash() -> None:
|
|
|
292
294
|
assert not selected.exists()
|
|
293
295
|
|
|
294
296
|
|
|
295
|
-
async def
|
|
297
|
+
async def test_sidebar_trash_confirmation_cancel_keeps_selected_file() -> None:
|
|
296
298
|
tmp, _, app = _file_app("first.txt", "first", root_is_tmp=True)
|
|
297
299
|
selected = tmp / "second.txt"
|
|
298
300
|
selected.write_text("second")
|
|
@@ -309,7 +311,7 @@ async def test_file_tree_trash_confirmation_cancel_keeps_selected_file() -> None
|
|
|
309
311
|
assert selected.read_text() == "second"
|
|
310
312
|
|
|
311
313
|
|
|
312
|
-
async def
|
|
314
|
+
async def test_sidebar_trash_dirty_open_file_warns_without_confirmation() -> None:
|
|
313
315
|
tmp, child, app = _file_app("folder/child.txt", "child", root_is_tmp=True)
|
|
314
316
|
selected = tmp / "folder"
|
|
315
317
|
trashed = _stub_trash(app)
|
|
@@ -334,7 +336,7 @@ async def test_file_tree_trash_dirty_open_file_warns_without_confirmation() -> N
|
|
|
334
336
|
assert child.exists()
|
|
335
337
|
|
|
336
338
|
|
|
337
|
-
async def
|
|
339
|
+
async def test_editor_cmd_backspace_does_not_trash_sidebar_selection() -> None:
|
|
338
340
|
tmp, _, app = _file_app("first.txt", "hello world", root_is_tmp=True)
|
|
339
341
|
selected = tmp / "second.txt"
|
|
340
342
|
selected.write_text("second")
|
|
@@ -352,7 +354,7 @@ async def test_editor_cmd_backspace_does_not_trash_file_tree_selection() -> None
|
|
|
352
354
|
assert selected.exists()
|
|
353
355
|
|
|
354
356
|
|
|
355
|
-
async def
|
|
357
|
+
async def test_trashing_open_file_closes_buffer_and_focuses_sidebar() -> None:
|
|
356
358
|
tmp, path, app = _file_app("first.txt", "first", root_is_tmp=True)
|
|
357
359
|
trashed = _stub_trash(app)
|
|
358
360
|
async with app.run_test() as pilot:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|