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.
- {riched-1.6.4 → riched-1.7.0}/PKG-INFO +4 -1
- {riched-1.6.4 → riched-1.7.0}/README.md +3 -0
- {riched-1.6.4 → riched-1.7.0}/pyproject.toml +1 -1
- {riched-1.6.4 → riched-1.7.0}/src/rich_editor/app.py +21 -0
- {riched-1.6.4 → riched-1.7.0}/src/rich_editor/editor.py +7 -0
- riched-1.7.0/tests/test_select_to_copy.py +128 -0
- {riched-1.6.4 → riched-1.7.0}/uv.lock +1 -1
- {riched-1.6.4 → riched-1.7.0}/.gitignore +0 -0
- {riched-1.6.4 → riched-1.7.0}/.python-version +0 -0
- {riched-1.6.4 → riched-1.7.0}/AGENTS.md +0 -0
- {riched-1.6.4 → riched-1.7.0}/docs/command-palette.svg +0 -0
- {riched-1.6.4 → riched-1.7.0}/docs/markdown-preview.svg +0 -0
- {riched-1.6.4 → riched-1.7.0}/docs/python-buffer.svg +0 -0
- {riched-1.6.4 → riched-1.7.0}/docs/search-replace.svg +0 -0
- {riched-1.6.4 → riched-1.7.0}/plans/esc-unified-sidebar-escape.md +0 -0
- {riched-1.6.4 → riched-1.7.0}/src/rich_editor/__init__.py +0 -0
- {riched-1.6.4 → riched-1.7.0}/src/rich_editor/bindings.yaml +0 -0
- {riched-1.6.4 → riched-1.7.0}/src/rich_editor/cli.py +0 -0
- {riched-1.6.4 → riched-1.7.0}/src/rich_editor/formatting.py +0 -0
- {riched-1.6.4 → riched-1.7.0}/src/rich_editor/keybindings.py +0 -0
- {riched-1.6.4 → riched-1.7.0}/src/rich_editor/quick_open.py +0 -0
- {riched-1.6.4 → riched-1.7.0}/src/rich_editor/screens.py +0 -0
- {riched-1.6.4 → riched-1.7.0}/src/rich_editor/settings.py +0 -0
- {riched-1.6.4 → riched-1.7.0}/src/rich_editor/syntax.py +0 -0
- {riched-1.6.4 → riched-1.7.0}/tests/__init__.py +0 -0
- {riched-1.6.4 → riched-1.7.0}/tests/helpers.py +0 -0
- {riched-1.6.4 → riched-1.7.0}/tests/runner.py +0 -0
- {riched-1.6.4 → riched-1.7.0}/tests/test_cli.py +0 -0
- {riched-1.6.4 → riched-1.7.0}/tests/test_command_palette.py +0 -0
- {riched-1.6.4 → riched-1.7.0}/tests/test_create_file.py +0 -0
- {riched-1.6.4 → riched-1.7.0}/tests/test_dirty_buffers.py +0 -0
- {riched-1.6.4 → riched-1.7.0}/tests/test_editor_shortcuts.py +0 -0
- {riched-1.6.4 → riched-1.7.0}/tests/test_file_io.py +0 -0
- {riched-1.6.4 → riched-1.7.0}/tests/test_footer.py +0 -0
- {riched-1.6.4 → riched-1.7.0}/tests/test_formatting.py +0 -0
- {riched-1.6.4 → riched-1.7.0}/tests/test_markdown_preview.py +0 -0
- {riched-1.6.4 → riched-1.7.0}/tests/test_quick_open.py +0 -0
- {riched-1.6.4 → riched-1.7.0}/tests/test_refresh.py +0 -0
- {riched-1.6.4 → riched-1.7.0}/tests/test_replace.py +0 -0
- {riched-1.6.4 → riched-1.7.0}/tests/test_sidebar.py +0 -0
- {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.
|
|
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
|
|
@@ -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)
|
|
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
|
|
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
|