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.
Files changed (39) hide show
  1. {riched-1.3.3 → riched-1.3.4}/AGENTS.md +3 -3
  2. {riched-1.3.3 → riched-1.3.4}/PKG-INFO +9 -9
  3. {riched-1.3.3 → riched-1.3.4}/README.md +8 -8
  4. {riched-1.3.3 → riched-1.3.4}/pyproject.toml +1 -1
  5. {riched-1.3.3 → riched-1.3.4}/src/rich_editor/app.py +93 -85
  6. {riched-1.3.3 → riched-1.3.4}/src/rich_editor/bindings.yaml +3 -3
  7. {riched-1.3.3 → riched-1.3.4}/src/rich_editor/cli.py +6 -1
  8. {riched-1.3.3 → riched-1.3.4}/src/rich_editor/editor.py +2 -2
  9. {riched-1.3.3 → riched-1.3.4}/src/rich_editor/screens.py +2 -2
  10. {riched-1.3.3 → riched-1.3.4}/tests/test_cli.py +1 -1
  11. {riched-1.3.3 → riched-1.3.4}/tests/test_command_palette.py +3 -2
  12. {riched-1.3.3 → riched-1.3.4}/tests/test_create_file.py +4 -3
  13. {riched-1.3.3 → riched-1.3.4}/tests/test_dirty_buffers.py +10 -11
  14. {riched-1.3.3 → riched-1.3.4}/tests/test_editor_shortcuts.py +5 -5
  15. {riched-1.3.3 → riched-1.3.4}/tests/test_file_io.py +1 -1
  16. {riched-1.3.3 → riched-1.3.4}/tests/test_footer.py +2 -2
  17. {riched-1.3.3 → riched-1.3.4}/tests/test_formatting.py +2 -1
  18. {riched-1.3.3 → riched-1.3.4}/tests/test_markdown_preview.py +4 -2
  19. {riched-1.3.3 → riched-1.3.4}/tests/test_refresh.py +10 -6
  20. riched-1.3.3/tests/test_file_tree.py → riched-1.3.4/tests/test_sidebar.py +18 -16
  21. {riched-1.3.3 → riched-1.3.4}/uv.lock +1 -1
  22. {riched-1.3.3 → riched-1.3.4}/.gitignore +0 -0
  23. {riched-1.3.3 → riched-1.3.4}/.python-version +0 -0
  24. {riched-1.3.3 → riched-1.3.4}/docs/command-palette.svg +0 -0
  25. {riched-1.3.3 → riched-1.3.4}/docs/markdown-preview.svg +0 -0
  26. {riched-1.3.3 → riched-1.3.4}/docs/python-buffer.svg +0 -0
  27. {riched-1.3.3 → riched-1.3.4}/docs/search-replace.svg +0 -0
  28. {riched-1.3.3 → riched-1.3.4}/src/rich_editor/__init__.py +0 -0
  29. {riched-1.3.3 → riched-1.3.4}/src/rich_editor/formatting.py +0 -0
  30. {riched-1.3.3 → riched-1.3.4}/src/rich_editor/keybindings.py +0 -0
  31. {riched-1.3.3 → riched-1.3.4}/src/rich_editor/quick_open.py +0 -0
  32. {riched-1.3.3 → riched-1.3.4}/src/rich_editor/settings.py +0 -0
  33. {riched-1.3.3 → riched-1.3.4}/src/rich_editor/syntax.py +0 -0
  34. {riched-1.3.3 → riched-1.3.4}/tests/__init__.py +0 -0
  35. {riched-1.3.3 → riched-1.3.4}/tests/helpers.py +0 -0
  36. {riched-1.3.3 → riched-1.3.4}/tests/runner.py +0 -0
  37. {riched-1.3.3 → riched-1.3.4}/tests/test_quick_open.py +0 -0
  38. {riched-1.3.3 → riched-1.3.4}/tests/test_replace.py +0 -0
  39. {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 file tree, `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.
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, file tree switching, quick-open search/indexing, or syntax highlighting. Use temporary files as existing tests do.
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 file tree` 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.
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
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 file tree with create, rename, quick refresh, and move-to-trash support
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 file tree |
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
- File tree shortcuts:
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 file tree and reloads the current buffer from disk. If the buffer has unsaved edits, Riched prompts to save, discard, or cancel before reloading.
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 file tree, 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.
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 file tree 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.
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 file tree 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 file-tree item. Both paths ask for confirmation before moving the item.
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 file tree.
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 file tree with create, rename, quick refresh, and move-to-trash support
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 file tree |
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
- File tree shortcuts:
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 file tree and reloads the current buffer from disk. If the buffer has unsaved edits, Riched prompts to save, discard, or cancel before reloading.
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 file tree, 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.
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 file tree 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.
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 file tree 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 file-tree item. Both paths ask for confirmation before moving the item.
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 file tree.
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
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "riched"
3
- version = "1.3.3"
3
+ version = "1.3.4"
4
4
  description = "Terminal Editor with VS Code Keybindings"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -71,9 +71,9 @@ from .syntax import (
71
71
  supported_file_types,
72
72
  )
73
73
 
74
- FILE_TREE_DEFAULT_WIDTH = 30
75
- FILE_TREE_MIN_WIDTH = 18
76
- FILE_TREE_MAX_WIDTH = 44
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("file_tree"),
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.action_file_tree_quit_check()
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.rename_file_tree_path(Path(node.data.path))
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.trash_file_tree_path(Path(node.data.path))
320
+ app.trash_sidebar_path(Path(node.data.path))
321
321
 
322
322
 
323
- class FileTreeResizeHandle(Static):
324
- """Mouse handle for resizing the file tree."""
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="file-tree-resize-handle")
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("#file-tree", DirectoryTree)
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.set_file_tree_width(self._drag_start_width + delta)
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", "focus_file_tree", "Focus file tree", show=False),
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 action_focus_file_tree(self) -> None:
383
+ def action_focus_sidebar(self) -> None:
384
384
  app = self.app
385
385
  if isinstance(app, RichedApp):
386
- app.action_focus_file_tree()
386
+ app.action_focus_sidebar()
387
387
 
388
388
 
389
389
  class RichedApp(App):
390
- """TUI text editor with a project file tree."""
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
- #file-tree {
401
+ #sidebar {
402
402
  width: 30;
403
403
  min-width: 18;
404
404
  max-width: 44;
405
405
  }
406
- #file-tree-resize-handle {
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
- #file-tree-resize-handle:hover,
414
- #file-tree-resize-handle.dragging {
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._selected_file_tree_path()
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 file tree item to Trash",
547
- lambda path=selected: self.trash_file_tree_path(path),
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="file-tree")
567
- yield FileTreeResizeHandle()
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.set_file_tree_width(FILE_TREE_DEFAULT_WIDTH)
574
- if self._initial_path is None:
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 _file_tree(self) -> DirectoryTree:
596
- return self.query_one("#file-tree", DirectoryTree)
603
+ def _sidebar(self) -> DirectoryTree:
604
+ return self.query_one("#sidebar", DirectoryTree)
597
605
 
598
- def _file_tree_resize_handle(self) -> Static:
599
- return self.query_one("#file-tree-resize-handle", Static)
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 _is_file_tree_visible(self) -> bool:
616
- return self._file_tree().styles.display != "none"
623
+ def _is_sidebar_visible(self) -> bool:
624
+ return self._sidebar().styles.display != "none"
617
625
 
618
- def _set_file_tree_visible(self, visible: bool) -> None:
626
+ def _set_sidebar_visible(self, visible: bool) -> None:
619
627
  display = "block" if visible else "none"
620
- self._file_tree().styles.display = display
621
- self._file_tree_resize_handle().styles.display = display
628
+ self._sidebar().styles.display = display
629
+ self._sidebar_resize_handle().styles.display = display
622
630
 
623
- def _show_file_tree(self) -> None:
624
- self._set_file_tree_visible(True)
631
+ def _show_sidebar(self) -> None:
632
+ self._set_sidebar_visible(True)
625
633
 
626
- def _hide_file_tree(self) -> None:
627
- self._set_file_tree_visible(False)
634
+ def _hide_sidebar(self) -> None:
635
+ self._set_sidebar_visible(False)
628
636
 
629
- def set_file_tree_width(self, width: int) -> None:
630
- clamped_width = max(FILE_TREE_MIN_WIDTH, min(FILE_TREE_MAX_WIDTH, width))
631
- tree = self._file_tree()
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._show_file_tree()
662
- self._file_tree().focus()
669
+ self._show_sidebar()
670
+ self._sidebar().focus()
663
671
  self._update_file_type_button()
664
672
 
665
- def _selected_file_tree_path(self) -> Path | None:
666
- tree = self._file_tree()
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 trash_file_tree_path(self, path: Path) -> None:
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._apply_trash_file_tree_path(source)
741
+ self._apply_trash_sidebar_path(source)
734
742
  return
735
- self._file_tree().focus()
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 _apply_trash_file_tree_path(self, path: Path) -> None:
743
- tree = self._file_tree()
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 rename_file_tree_path(self, path: Path) -> None:
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._file_tree().focus()
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._validate_file_tree_rename(source, new_name)
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._file_tree().focus()
783
+ self._sidebar().focus()
776
784
  return
777
- self._apply_file_tree_rename(source, new_name)
785
+ self._apply_sidebar_rename(source, new_name)
778
786
 
779
787
  self.push_screen(RenamePathScreen(source.name, validate), handle)
780
788
 
781
- def _validate_file_tree_rename(self, source: Path, new_name: str) -> str | None:
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 _apply_file_tree_rename(self, source: Path, new_name: str) -> None:
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._file_tree().focus()
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._open_path_after_file_tree_rename(
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._file_tree().reload()
852
+ self._sidebar().reload()
845
853
  self._invalidate_quick_open_index()
846
- self._file_tree().focus()
854
+ self._sidebar().focus()
847
855
  self.notify(f"Renamed {old_path.name} to {new_path.name}")
848
856
 
849
- def _open_path_after_file_tree_rename(
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._file_tree().reload()
917
+ self._sidebar().reload()
910
918
  self._invalidate_quick_open_index()
911
919
  if self.path is None:
912
- self._show_file_tree()
913
- self._file_tree().focus()
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._file_tree()
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._file_tree().reload()
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 action_file_tree_quit_check(self) -> None:
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 action_toggle_file_tree(self) -> None:
1566
+ def action_toggle_sidebar(self) -> None:
1559
1567
  editor = self._editor_or_none()
1560
- if self._is_file_tree_visible():
1561
- self._hide_file_tree()
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._show_file_tree()
1573
+ self._show_sidebar()
1566
1574
 
1567
- def action_toggle_file_tree_focus(self) -> None:
1568
- tree = self._file_tree()
1569
- if not self._is_file_tree_visible():
1570
- self._show_file_tree()
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 action_focus_file_tree(self) -> None:
1581
- if not self._is_file_tree_visible():
1582
- self._show_file_tree()
1583
- self._file_tree().focus()
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: toggle_file_tree
12
- description: Toggle file tree
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
- file_tree:
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._is_file_tree_visible():
64
- app.action_file_tree_quit_check()
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 file tree item to Trash."""
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 file tree item name."""
173
+ """Prompt for a new sidebar item name."""
174
174
 
175
175
  BINDINGS = build_screen_bindings("rename_path")
176
176
 
@@ -13,7 +13,7 @@ from rich_editor import cli
13
13
  captured = {}
14
14
 
15
15
  class StubApp:
16
- def __init__(self, path, root):
16
+ def __init__(self, path, root, show_sidebar=False):
17
17
  captured["path"] = path
18
18
  captured["root"] = root
19
19
 
@@ -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
- tree = app.query_one("#file-tree", DirectoryTree)
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 test_command_palette_includes_selected_file_tree_item_trash_command() -> None:
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 test_create_file_uses_highlighted_file_tree_folder() -> None:
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
- tree = app.query_one("#file-tree", DirectoryTree)
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("#file-tree", DirectoryTree)
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.query_one("#file-tree", DirectoryTree).focus()
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 test_editor_escape_visible_tree_does_nothing() -> None:
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 test_editor_escape_hidden_tree_clean_shows_quit_confirmation() -> None:
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 test_editor_escape_hidden_tree_dirty_shows_unsaved_changes() -> None:
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 test_file_tree_escape_clean_shows_quit_confirmation_then_cancel() -> None:
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 test_file_tree_escape_clean_quit_exits() -> None:
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 test_file_tree_escape_dirty_shows_unsaved_changes() -> None:
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 test_file_tree_toggle_aliases_with_and_without_open_buffer() -> None:
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("#file-tree", DirectoryTree)
364
- assert tree.styles.display == "block"
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("#file-tree", DirectoryTree)
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 file tree",
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 file tree"] == "⌘B"
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
- _ = app.query_one("#file-tree", DirectoryTree).focus()
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 test_markdown_preview_escape_focuses_file_tree() -> None:
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
- tree = app.query_one("#file-tree", DirectoryTree)
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 test_refresh_button_reloads_file_tree() -> None:
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
- tree = app.query_one("#file-tree", DirectoryTree)
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 test_refresh_deleted_open_file_closes_buffer_and_focuses_file_tree() -> None:
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("#file-tree", DirectoryTree)
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 test_refresh_no_buffer_reloads_file_tree_and_keeps_tree_focus() -> None:
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
- tree = app.query_one("#file-tree", DirectoryTree)
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
- tree = app.query_one("#file-tree", DirectoryTree)
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 test_file_tree_is_rooted_at_project_dir() -> None:
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
- tree = app.query_one("#file-tree", DirectoryTree)
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 test_file_tree_switch_opens_selected_file() -> None:
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 test_file_tree_dirty_switch_prompts_then_discard_opens_file() -> None:
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 test_file_tree_enter_renames_selected_file_without_opening_it() -> None:
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 test_file_tree_rename_cancel_leaves_item_unchanged_and_refocuses_tree() -> None:
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 test_file_tree_enter_renames_selected_folder() -> None:
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 test_file_tree_space_still_opens_files_and_toggles_folders() -> None:
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 test_file_tree_rename_duplicate_or_invalid_name_stays_open() -> None:
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 test_file_tree_cmd_backspace_moves_selected_file_to_trash() -> None:
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 test_file_tree_ctrl_u_moves_selected_file_to_trash() -> None:
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 test_file_tree_trash_confirmation_cancel_keeps_selected_file() -> None:
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 test_file_tree_trash_dirty_open_file_warns_without_confirmation() -> None:
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 test_editor_cmd_backspace_does_not_trash_file_tree_selection() -> None:
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 test_trashing_open_file_closes_buffer_and_focuses_file_tree() -> None:
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:
@@ -187,7 +187,7 @@ wheels = [
187
187
 
188
188
  [[package]]
189
189
  name = "riched"
190
- version = "1.3.2"
190
+ version = "1.3.4"
191
191
  source = { editable = "." }
192
192
  dependencies = [
193
193
  { name = "pyyaml" },
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