riched 1.6.4__tar.gz → 1.7.0__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 (41) hide show
  1. {riched-1.6.4 → riched-1.7.0}/PKG-INFO +4 -1
  2. {riched-1.6.4 → riched-1.7.0}/README.md +3 -0
  3. {riched-1.6.4 → riched-1.7.0}/pyproject.toml +1 -1
  4. {riched-1.6.4 → riched-1.7.0}/src/rich_editor/app.py +21 -0
  5. {riched-1.6.4 → riched-1.7.0}/src/rich_editor/editor.py +7 -0
  6. riched-1.7.0/tests/test_select_to_copy.py +128 -0
  7. {riched-1.6.4 → riched-1.7.0}/uv.lock +1 -1
  8. {riched-1.6.4 → riched-1.7.0}/.gitignore +0 -0
  9. {riched-1.6.4 → riched-1.7.0}/.python-version +0 -0
  10. {riched-1.6.4 → riched-1.7.0}/AGENTS.md +0 -0
  11. {riched-1.6.4 → riched-1.7.0}/docs/command-palette.svg +0 -0
  12. {riched-1.6.4 → riched-1.7.0}/docs/markdown-preview.svg +0 -0
  13. {riched-1.6.4 → riched-1.7.0}/docs/python-buffer.svg +0 -0
  14. {riched-1.6.4 → riched-1.7.0}/docs/search-replace.svg +0 -0
  15. {riched-1.6.4 → riched-1.7.0}/plans/esc-unified-sidebar-escape.md +0 -0
  16. {riched-1.6.4 → riched-1.7.0}/src/rich_editor/__init__.py +0 -0
  17. {riched-1.6.4 → riched-1.7.0}/src/rich_editor/bindings.yaml +0 -0
  18. {riched-1.6.4 → riched-1.7.0}/src/rich_editor/cli.py +0 -0
  19. {riched-1.6.4 → riched-1.7.0}/src/rich_editor/formatting.py +0 -0
  20. {riched-1.6.4 → riched-1.7.0}/src/rich_editor/keybindings.py +0 -0
  21. {riched-1.6.4 → riched-1.7.0}/src/rich_editor/quick_open.py +0 -0
  22. {riched-1.6.4 → riched-1.7.0}/src/rich_editor/screens.py +0 -0
  23. {riched-1.6.4 → riched-1.7.0}/src/rich_editor/settings.py +0 -0
  24. {riched-1.6.4 → riched-1.7.0}/src/rich_editor/syntax.py +0 -0
  25. {riched-1.6.4 → riched-1.7.0}/tests/__init__.py +0 -0
  26. {riched-1.6.4 → riched-1.7.0}/tests/helpers.py +0 -0
  27. {riched-1.6.4 → riched-1.7.0}/tests/runner.py +0 -0
  28. {riched-1.6.4 → riched-1.7.0}/tests/test_cli.py +0 -0
  29. {riched-1.6.4 → riched-1.7.0}/tests/test_command_palette.py +0 -0
  30. {riched-1.6.4 → riched-1.7.0}/tests/test_create_file.py +0 -0
  31. {riched-1.6.4 → riched-1.7.0}/tests/test_dirty_buffers.py +0 -0
  32. {riched-1.6.4 → riched-1.7.0}/tests/test_editor_shortcuts.py +0 -0
  33. {riched-1.6.4 → riched-1.7.0}/tests/test_file_io.py +0 -0
  34. {riched-1.6.4 → riched-1.7.0}/tests/test_footer.py +0 -0
  35. {riched-1.6.4 → riched-1.7.0}/tests/test_formatting.py +0 -0
  36. {riched-1.6.4 → riched-1.7.0}/tests/test_markdown_preview.py +0 -0
  37. {riched-1.6.4 → riched-1.7.0}/tests/test_quick_open.py +0 -0
  38. {riched-1.6.4 → riched-1.7.0}/tests/test_refresh.py +0 -0
  39. {riched-1.6.4 → riched-1.7.0}/tests/test_replace.py +0 -0
  40. {riched-1.6.4 → riched-1.7.0}/tests/test_sidebar.py +0 -0
  41. {riched-1.6.4 → riched-1.7.0}/tests/test_syntax.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: riched
3
- Version: 1.6.4
3
+ Version: 1.7.0
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
@@ -56,6 +56,7 @@ Terminal Editor with VS Code Keybindings Powered by [Rich](https://github.com/Te
56
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
+ - mouse select-to-copy in the source editor and Markdown preview
59
60
  - document formatting through external formatter CLIs
60
61
  - multiple built-in theme(atom, drcula, nordic ...etc)
61
62
  - syntax highlighting for major file formats with manual file type override
@@ -139,6 +140,8 @@ Riched persists the selected Textual theme in `settings.yaml` under the user con
139
140
 
140
141
  Use `⌘⇧V` (`⌃⇧V` if Ghostty still owns `⌘⇧V`), the `Preview` button in the footer, or `F1` → `Toggle Markdown preview` to toggle an in-terminal rendered preview for Markdown files. The preview uses the current editor buffer, so unsaved edits are shown without writing them to disk.
141
142
 
143
+ Drag to select text in the preview to copy the rendered text to the clipboard. The same select-to-copy behavior works in the source editor.
144
+
142
145
  When Markdown preview is open, the header shows a `☰` button on the right. Click it to show or hide the Markdown table of contents. The button is hidden while editing the text buffer.
143
146
 
144
147
  ## Syntax And File Types
@@ -41,6 +41,7 @@ Terminal Editor with VS Code Keybindings Powered by [Rich](https://github.com/Te
41
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
+ - mouse select-to-copy in the source editor and Markdown preview
44
45
  - document formatting through external formatter CLIs
45
46
  - multiple built-in theme(atom, drcula, nordic ...etc)
46
47
  - syntax highlighting for major file formats with manual file type override
@@ -124,6 +125,8 @@ Riched persists the selected Textual theme in `settings.yaml` under the user con
124
125
 
125
126
  Use `⌘⇧V` (`⌃⇧V` if Ghostty still owns `⌘⇧V`), the `Preview` button in the footer, or `F1` → `Toggle Markdown preview` to toggle an in-terminal rendered preview for Markdown files. The preview uses the current editor buffer, so unsaved edits are shown without writing them to disk.
126
127
 
128
+ Drag to select text in the preview to copy the rendered text to the clipboard. The same select-to-copy behavior works in the source editor.
129
+
127
130
  When Markdown preview is open, the header shows a `☰` button on the right. Click it to show or hide the Markdown table of contents. The button is hidden while editing the text buffer.
128
131
 
129
132
  ## Syntax And File Types
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "riched"
3
- version = "1.6.4"
3
+ version = "1.7.0"
4
4
  description = "Terminal Editor with VS Code Keybindings"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -669,6 +669,27 @@ class RichedApp(App):
669
669
  previews = list(self.query("#markdown-preview"))
670
670
  return previews[0] if previews else None
671
671
 
672
+ def _copy_preview_selection(self) -> None:
673
+ preview = self._markdown_preview_or_none()
674
+ if preview is None:
675
+ return
676
+ selected = self.screen.get_selected_text()
677
+ if not selected:
678
+ return
679
+ in_preview = any(
680
+ preview in widget.ancestors_with_self for widget in self.screen.selections
681
+ )
682
+ if not in_preview:
683
+ return
684
+ self.copy_to_clipboard(selected)
685
+
686
+ def on_text_selected(self, event: events.TextSelected) -> None:
687
+ self._copy_preview_selection()
688
+
689
+ def on_click(self, event: events.Click) -> None:
690
+ if event.chain >= 2:
691
+ self._copy_preview_selection()
692
+
672
693
  def _sidebar(self) -> DirectoryTree:
673
694
  return self.query_one("#sidebar", DirectoryTree)
674
695
 
@@ -53,8 +53,15 @@ class _FormattingApp(Protocol):
53
53
  class RichedTextArea(TextArea):
54
54
  """TextArea with VS Code-style line-edit shortcuts."""
55
55
 
56
+ ALLOW_SELECT = False
56
57
  BINDINGS = build_static_bindings("editor")
57
58
 
59
+ def _end_mouse_selection(self) -> None:
60
+ was_selecting = self._selecting
61
+ super()._end_mouse_selection()
62
+ if was_selecting and self.selected_text:
63
+ self.app.copy_to_clipboard(self.selected_text)
64
+
58
65
  async def _on_key(self, event: events.Key) -> None:
59
66
  if event.key == "escape":
60
67
  from .app import RichedApp
@@ -0,0 +1,128 @@
1
+ from __future__ import annotations
2
+
3
+ from textual.widgets import MarkdownViewer, TextArea
4
+ from textual.widgets.markdown import MarkdownBlock
5
+
6
+ from .helpers import _editor, _file_app, _press
7
+
8
+ SENTINEL = "__clipboard_sentinel__"
9
+
10
+
11
+ def _editor_content_offset(
12
+ editor: TextArea, column: int, row: int = 0
13
+ ) -> tuple[int, int]:
14
+ return (
15
+ editor.gutter.left + editor.gutter_width + column,
16
+ editor.gutter.top + row,
17
+ )
18
+
19
+
20
+ async def _drag(pilot, widget, start: tuple[int, int], end: tuple[int, int]) -> None:
21
+ await pilot.mouse_down(widget, offset=start)
22
+ await pilot.pause()
23
+ await pilot.hover(widget, offset=end)
24
+ await pilot.pause()
25
+ await pilot.mouse_up(widget, offset=end)
26
+ await pilot.pause()
27
+
28
+
29
+ async def test_editor_mouse_select_copies_source_text() -> None:
30
+ _, _, app = _file_app("hello.txt", "hello world")
31
+ async with app.run_test() as pilot:
32
+ await pilot.pause()
33
+ editor = _editor(app)
34
+ app.copy_to_clipboard(SENTINEL)
35
+
36
+ await _drag(
37
+ pilot,
38
+ editor,
39
+ _editor_content_offset(editor, 0),
40
+ _editor_content_offset(editor, 5),
41
+ )
42
+
43
+ assert editor.selected_text == "hello", repr(editor.selected_text)
44
+ assert app.clipboard == "hello", repr(app.clipboard)
45
+ assert "1" not in app.clipboard
46
+
47
+
48
+ async def test_editor_click_without_drag_does_not_copy() -> None:
49
+ _, _, app = _file_app("hello.txt", "hello world")
50
+ async with app.run_test() as pilot:
51
+ await pilot.pause()
52
+ editor = _editor(app)
53
+ app.copy_to_clipboard(SENTINEL)
54
+ offset = _editor_content_offset(editor, 0)
55
+
56
+ await pilot.mouse_down(editor, offset=offset)
57
+ await pilot.pause()
58
+ await pilot.mouse_up(editor, offset=offset)
59
+ await pilot.pause()
60
+
61
+ assert editor.selection.is_empty
62
+ assert app.clipboard == SENTINEL, repr(app.clipboard)
63
+
64
+
65
+ async def test_editor_keyboard_selection_does_not_copy() -> None:
66
+ _, _, app = _file_app("hello.txt", "hello world")
67
+ async with app.run_test() as pilot:
68
+ await pilot.pause()
69
+ editor = _editor(app)
70
+ app.copy_to_clipboard(SENTINEL)
71
+ editor.move_cursor((0, 0))
72
+ await pilot.pause()
73
+
74
+ await _press(pilot, "shift+right")
75
+
76
+ assert editor.selected_text == "h", repr(editor.selected_text)
77
+ assert app.clipboard == SENTINEL, repr(app.clipboard)
78
+
79
+
80
+ async def test_markdown_preview_mouse_select_copies_rendered_text() -> None:
81
+ _, _, app = _file_app(
82
+ "README.md",
83
+ "# Heading\n\ncopy-me-please\n",
84
+ root_is_tmp=True,
85
+ edit_mode=True,
86
+ )
87
+ async with app.run_test() as pilot:
88
+ await pilot.pause()
89
+ await _press(pilot, "cmd+shift+v")
90
+ preview = app.query_one("#markdown-preview", MarkdownViewer)
91
+ paragraph = next(
92
+ block
93
+ for block in preview.document.query(MarkdownBlock)
94
+ if (block.source or "").find("copy-me-please") >= 0
95
+ )
96
+ app.copy_to_clipboard(SENTINEL)
97
+
98
+ await _drag(pilot, paragraph, (0, 0), (max(1, paragraph.size.width - 1), 0))
99
+
100
+ assert "copy-me-please" in app.clipboard, repr(app.clipboard)
101
+ assert "# Heading" not in app.clipboard
102
+ assert SENTINEL not in app.clipboard
103
+
104
+
105
+ async def test_markdown_preview_click_without_drag_does_not_copy() -> None:
106
+ _, _, app = _file_app(
107
+ "README.md",
108
+ "# Heading\n\ncopy-me-please\n",
109
+ root_is_tmp=True,
110
+ edit_mode=True,
111
+ )
112
+ async with app.run_test() as pilot:
113
+ await pilot.pause()
114
+ await _press(pilot, "cmd+shift+v")
115
+ preview = app.query_one("#markdown-preview", MarkdownViewer)
116
+ paragraph = next(
117
+ block
118
+ for block in preview.document.query(MarkdownBlock)
119
+ if (block.source or "").find("copy-me-please") >= 0
120
+ )
121
+ app.copy_to_clipboard(SENTINEL)
122
+
123
+ await pilot.mouse_down(paragraph, offset=(0, 0))
124
+ await pilot.pause()
125
+ await pilot.mouse_up(paragraph, offset=(0, 0))
126
+ await pilot.pause()
127
+
128
+ assert app.clipboard == SENTINEL, repr(app.clipboard)
@@ -187,7 +187,7 @@ wheels = [
187
187
 
188
188
  [[package]]
189
189
  name = "riched"
190
- version = "1.6.4"
190
+ version = "1.7.0"
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
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