anyshell 0.1.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.
anyshell-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 David Brochart
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,337 @@
1
+ Metadata-Version: 2.4
2
+ Name: anyshell
3
+ Version: 0.1.0
4
+ Summary: An async Python command shell with an optional Anytext frontend
5
+ Author: David Brochart
6
+ Author-email: David Brochart <david.brochart@gmail.com>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Requires-Dist: anyio>=4.11,<5
10
+ Requires-Dist: cyclopts>=5.4,<6
11
+ Requires-Dist: anytext>=9.1,<10 ; extra == 'anytext'
12
+ Requires-Dist: trio>=0.31 ; extra == 'trio'
13
+ Requires-Python: >=3.11
14
+ Provides-Extra: anytext
15
+ Provides-Extra: trio
16
+ Description-Content-Type: text/markdown
17
+
18
+ # Anyshell
19
+
20
+ An async shell whose commands are Python functions. The engine is independent of
21
+ its optional Anytext widget: both can run locally on the desktop or in Pyodide.
22
+ No shell command starts an OS process or needs a worker thread.
23
+
24
+ ## Run it
25
+
26
+ ```sh
27
+ uv sync --extra anytext --extra trio
28
+ uv run --extra anytext anyshell
29
+ uv run --extra anytext --extra trio anyshell --backend trio
30
+ uv run anyshell -c 'ls | where type file | select name | sort'
31
+ ```
32
+
33
+ `/` maps to the selected workspace directory, which defaults to your current
34
+ working directory. Use `--root /path/to/workspace` to choose another directory.
35
+ The session never changes Python's process working directory. Paths use `/` on
36
+ all platforms. Local symlinks must resolve inside the workspace; this is a path
37
+ boundary, not a sandbox for arbitrary Python commands or untrusted code.
38
+
39
+ The prompt is inline by default: it follows the latest output in a shared scrollback
40
+ area. Typing or completing a command brings it back into view. For a fixed bottom
41
+ editor, run `uv run --extra anytext anyshell --prompt-position bottom` or construct
42
+ `Shell(session, prompt_position="bottom")`. Both `Shell` and `ShellApp` accept
43
+ `prompt_position="inline"` or `prompt_position="bottom"`.
44
+
45
+ The widget provides a scrollable transcript, Up/Down history, Tab completion for
46
+ command names, options, and unquoted paths, Ctrl+C cancellation, and Ctrl+Q to quit the demo.
47
+ Tab first extends the shared prefix when several completions match. If no more
48
+ characters can be filled in, Tab opens a menu beside the cursor. Use Up/Down or
49
+ Tab to select, Enter to insert, and Escape to dismiss; typing also dismisses it.
50
+ Long choices remain on one line and can be read using the horizontal scrollbar.
51
+
52
+ ## Multiline commands
53
+
54
+ Shift+Enter inserts a newline without running the command. Enter submits a
55
+ complete command, or inserts a continuation line when a quote is still open or
56
+ the command ends with a pipe or backslash. Ctrl+O also inserts a newline on
57
+ terminals that cannot distinguish Shift+Enter. Ctrl+Enter forces submission,
58
+ including syntax errors. The editor grows up to eight rows, then scrolls.
59
+
60
+ ```sh
61
+ echo 'hello
62
+ world'
63
+ cat notes.txt |
64
+ grep Python |
65
+ count
66
+ ```
67
+
68
+ Unquoted newlines separate commands, which execute in order. A backslash followed
69
+ by a newline joins lines without inserting a character. Newlines inside quotes
70
+ remain part of the argument. All commands are parsed and validated before any
71
+ runs; a runtime failure does not prevent later commands from running, and the
72
+ final status is that of the last command. Ctrl+C cancels the active sequence or
73
+ clears unfinished input. History keeps a multiline submission as one entry.
74
+ Up/Down moves within a multiline editor; Alt+Up/Down navigates command history.
75
+ The same syntax works through `Session.execute()` and headless `-c` execution.
76
+
77
+ ## Commands and pipelines
78
+
79
+ ```sh
80
+ help
81
+ ls
82
+ ls -la
83
+ help ls
84
+ ls | where type file | select name | sort
85
+ cat README.md | grep Python | count
86
+ echo 'a quoted | character'
87
+ mkdir notes
88
+ touch empty.txt
89
+ rmdir notes
90
+ mkdir notes
91
+ cd notes
92
+ write hello.txt 'Hello from Python'
93
+ cat hello.txt
94
+ edit hello.txt
95
+ cp hello.txt backup.txt
96
+ mv backup.txt renamed.txt
97
+ rm hello.txt
98
+ cd /
99
+ sleep 30
100
+ clear
101
+ ```
102
+
103
+ Pipelines exchange Python values through bounded AnyIO streams. `ls` emits
104
+ records, displayed as names in columns in the widget (directories have a trailing
105
+ `/` and blue styling). Headless output lists one name per line. Pipeline values
106
+ remain records; `select` extracts fields; `count` emits an integer. `cat` and `grep`
107
+ stream text lines. `sort` buffers its input. A failure in any stage fails the
108
+ pipeline. Filesystem mutations, `cd`, and `clear` run outside pipelines.
109
+
110
+ `ls -l` (`--long`) displays type, byte size, modification time in UTC, and name.
111
+ Its pipeline records include `size` and `modified` (Unix seconds); directory size
112
+ is `None`. `ls -a` (`--all`) includes hidden entries, and flags combine as `-la`.
113
+ `grep -i` ignores case and `sort -r` reverses the order.
114
+ `touch PATH...` creates missing files or updates modification times without
115
+ changing existing contents. Existing directories can also be touched.
116
+ `rm PATH...` removes files, continuing with remaining paths if one fails.
117
+ `rmdir DIRECTORY...` removes empty directories and rejects files and nonempty
118
+ directories; its completion suggests only directories.
119
+
120
+ This is an initial shell language: quoting, multiline commands, and pipes are supported. Bash scripts,
121
+ redirection, environment expansion, glob expansion, process execution, job
122
+ control, and persistent history are not implemented. Commands are trusted Python
123
+ code, not a sandbox. Browser filesystem changes are in-memory and reset on reload.
124
+
125
+ ## Find files
126
+
127
+ ```sh
128
+ find
129
+ find docs -name '*.txt'
130
+ find . -iname '*.py' -type f
131
+ find . -type d -maxdepth 2
132
+ find docs other-directory -type f | select path
133
+ find . -type f | count
134
+ ```
135
+
136
+ `find [PATH...]` walks starting paths recursively, including the starting entries
137
+ and hidden names. It defaults to `.`. `-name`/`--name` matches a basename using
138
+ shell-style wildcard patterns; `-iname`/`--iname` ignores letter case.
139
+ `-type`/`--type` accepts `f` (file), `d` (directory), or `l` (symbolic link).
140
+ `-maxdepth`/`--max-depth` limits traversal, with starting paths at depth zero.
141
+ Filters combine with AND. Quote wildcard patterns to pass them unchanged.
142
+
143
+ Output records contain `name` (basename), `path` (relative to the current working
144
+ directory, or absolute when the starting path is absolute), and `type` (`file`,
145
+ `directory`, or `symlink`). The UI displays paths with the usual directory suffix;
146
+ `select path` extracts plain paths. Symbolic links, including starting paths,
147
+ are listed without following them. Errors are reported while traversal continues
148
+ with other entries and starting paths; any error makes the command fail.
149
+ Custom filesystem implementations provide `entry_type()` without following the
150
+ final symbolic link, alongside the existing directory-listing methods.
151
+
152
+ ## Move and copy files
153
+
154
+ ```sh
155
+ mv old.txt new.txt
156
+ mv notes archive
157
+ cp file.txt backup.txt
158
+ cp file.txt existing-directory/
159
+ cp -r notes notes-backup
160
+ ```
161
+
162
+ `mv SOURCE DESTINATION` moves or renames a file or a whole directory.
163
+ `cp SOURCE DESTINATION` copies a file; `cp -r` (or `--recursive`) also copies
164
+ whole directory trees, including hidden entries. If the destination is an
165
+ existing directory, the source keeps its name inside it. Existing files are
166
+ replaced; existing directory trees are not merged or replaced. Contents and
167
+ modification times are preserved. Both commands accept one source and one
168
+ destination and run outside pipelines. Moving the current directory or one of
169
+ its ancestors updates the session's working path.
170
+
171
+ ## Page through text
172
+
173
+ ```sh
174
+ more README.md
175
+ more first.txt second.txt
176
+ cat notes.txt | grep Python | more
177
+ ls -l | more
178
+ ```
179
+
180
+ `more` fills the Shell widget with a read-only, scrollable view, then restores the
181
+ transcript and prompt when closed. Space/Page Down moves forward a page;
182
+ `b`/Page Up moves back. Enter/Down advances one line, Up moves back one line,
183
+ and Home/End jumps to the beginning/end. `q`, Ctrl+Q, or Escape closes the pager.
184
+ Mouse scrolling also works. Shortcuts appear in the footer.
185
+
186
+ The pager accepts files or pipeline input, formatting Python values for display.
187
+ It streams input into scrollback; closing it stops consuming the pipeline.
188
+ Headless execution reports that an interactive frontend is required. Other
189
+ frontends can handle the `page` event's `PageRequest`, consume its `lines`, mark
190
+ it `handled`, and return when their pager closes.
191
+
192
+ ## Search with less
193
+
194
+ ```sh
195
+ less README.md
196
+ less first.txt second.txt
197
+ cat notes.txt | grep Python | less
198
+ ```
199
+
200
+ `less` uses the same in-shell pager and navigation shortcuts as `more`, adding
201
+ Python regex search. Press `/` to search forward or `?` to search backward,
202
+ then enter a pattern and press Enter. `n` finds the next match in that direction;
203
+ Shift+N finds the previous one. Searches wrap around the text. An empty search
204
+ reuses the last pattern, and Escape cancels the search field. Invalid patterns
205
+ and missing matches appear as notifications. The title shows the matching line.
206
+ The search field disappears after submission, leaving no permanent bottom line.
207
+
208
+ ## Edit files
209
+
210
+ `edit PATH` opens a plain text editor that fills the Shell widget, restoring the
211
+ transcript and prompt when closed. It loads an
212
+ existing UTF-8 file or starts empty for a new file. Ctrl+S saves without closing;
213
+ Ctrl+Q closes the editor and returns to the shell. Modified files show `*` beside the path. Closing with unsaved
214
+ changes updates the footer shortcuts: a second Ctrl+Q discards and closes (Ctrl+D
215
+ also works), Ctrl+S saves,
216
+ and Escape keeps editing. Save errors appear as notifications and leave the
217
+ editor and its text open. The title marks unsaved edits with `*`; shortcuts appear
218
+ in the app footer. The editor has no bottom status line or button rows. When embedding in
219
+ another Anytext app, give its app-wide Ctrl+Q quit binding `priority=False` so
220
+ the focused editor can handle that key first, as `ShellApp` does.
221
+
222
+ The editor uses the session filesystem on desktop and in the thread-free browser.
223
+ New files are created when saved. Browser files remain in memory until reload.
224
+ Headless `-c` execution reports that an interactive frontend is required; `edit`
225
+ cannot run in a pipeline. Other frontends can handle the `edit` event's
226
+ `EditRequest`, mark it `handled`, and return after their editor closes.
227
+
228
+ ## Use the engine
229
+
230
+ ```python
231
+ import anyio
232
+ from anyshell import Event, MemoryFileSystem, Session
233
+
234
+ session = Session(MemoryFileSystem({"hello.txt": "hello\nworld\n"}))
235
+
236
+
237
+ async def receive(event: Event):
238
+ if event.kind == "output":
239
+ print(event.value)
240
+
241
+
242
+ async def main():
243
+ await session.execute("cat hello.txt | count", receive)
244
+
245
+
246
+ anyio.run(main)
247
+ ```
248
+
249
+ Commands use Cyclopts to parse typed Python parameters. `--help` (or `-h`)
250
+ and `help COMMAND` write help to the transcript. Invalid arguments produce shell
251
+ errors without exiting the app. All pipeline arguments are validated before any
252
+ stage starts. Use `--` before a filename beginning with a dash.
253
+
254
+ Register a `Command` with a synchronous or asynchronous function. Parameters
255
+ named `ctx` (or `context`), `stdin`, and `stdout` are injected by the engine and
256
+ must be keyword-only. Other parameters become command arguments and options.
257
+ Input and output are AnyIO object streams; a returned value is also emitted as
258
+ one pipeline value. Command functions should stay cooperative for cancellation.
259
+ `Session.cancel()` must be called from the session's event loop.
260
+
261
+ ```python
262
+ from typing import Annotated
263
+
264
+ from cyclopts import Parameter
265
+ from anyshell import Command
266
+
267
+
268
+ async def upper(
269
+ *,
270
+ prefix: Annotated[str, Parameter(name=["--prefix", "-p"])] = "",
271
+ stdin,
272
+ stdout,
273
+ ):
274
+ async for value in stdin:
275
+ await stdout.send(prefix + str(value).upper())
276
+
277
+
278
+ session.register(Command("upper", "Uppercase input values", upper))
279
+ # Now: echo hello | upper -p '> '
280
+ ```
281
+
282
+ `ctx.fs` exposes the filesystem and `ctx.emit` emits frontend events. Parsing
283
+ and help never expose the injected parameters as command options.
284
+
285
+ For the frontend, import `Shell` from `anyshell.frontend` and yield
286
+ `Shell(session)` in any Anytext app. History and working directory live in the
287
+ session, so the engine and UI can evolve independently.
288
+
289
+ ## Browser demo
290
+
291
+ ```sh
292
+ uv run python tools/build_browser.py
293
+ uv run python -m http.server --directory .cache/browser-site 8000
294
+ ```
295
+
296
+ Open http://localhost:8000. The Prompt selector switches between inline and bottom
297
+ layouts by reloading the demo (`?prompt=inline` or `?prompt=bottom`). The demo downloads Pyodide and Anytext from public
298
+ package servers and installs this checkout's Anyshell wheel. It uses an in-memory
299
+ workspace and disables threads. Internet access is needed on first load.
300
+
301
+ ## Development
302
+
303
+ ```sh
304
+ uv run pytest
305
+ uv run ruff check .
306
+ uv run ruff format --check .
307
+ uv build
308
+ ```
309
+
310
+ For the real browser regression test:
311
+
312
+ ```sh
313
+ uv run python tools/build_browser.py
314
+ npm ci --prefix tests/browser
315
+ cd tests/browser
316
+ npx playwright install chromium
317
+ npm test
318
+ ```
319
+
320
+ Async engine and widget tests run on both asyncio and Trio. The engine depends
321
+ on AnyIO and Cyclopts; install `anyshell[anytext]` for the UI and `anyshell[trio]` for Trio.
322
+
323
+ The browser terminal scaffolding is adapted from Anytext's MIT-licensed browser demo.
324
+
325
+ ## Releasing
326
+
327
+ Set `project.version` in `pyproject.toml`, update the lockfile with `uv lock`, and
328
+ commit the release changes. Push a matching version tag, without a `v` prefix:
329
+
330
+ ```sh
331
+ git tag 0.1.0
332
+ git push origin 0.1.0
333
+ ```
334
+
335
+ The `release.yml` workflow checks that the tag matches the package version,
336
+ builds and validates the wheel and source distribution, then publishes them to
337
+ PyPI using the configured trusted publisher. No PyPI API token is needed.
@@ -0,0 +1,320 @@
1
+ # Anyshell
2
+
3
+ An async shell whose commands are Python functions. The engine is independent of
4
+ its optional Anytext widget: both can run locally on the desktop or in Pyodide.
5
+ No shell command starts an OS process or needs a worker thread.
6
+
7
+ ## Run it
8
+
9
+ ```sh
10
+ uv sync --extra anytext --extra trio
11
+ uv run --extra anytext anyshell
12
+ uv run --extra anytext --extra trio anyshell --backend trio
13
+ uv run anyshell -c 'ls | where type file | select name | sort'
14
+ ```
15
+
16
+ `/` maps to the selected workspace directory, which defaults to your current
17
+ working directory. Use `--root /path/to/workspace` to choose another directory.
18
+ The session never changes Python's process working directory. Paths use `/` on
19
+ all platforms. Local symlinks must resolve inside the workspace; this is a path
20
+ boundary, not a sandbox for arbitrary Python commands or untrusted code.
21
+
22
+ The prompt is inline by default: it follows the latest output in a shared scrollback
23
+ area. Typing or completing a command brings it back into view. For a fixed bottom
24
+ editor, run `uv run --extra anytext anyshell --prompt-position bottom` or construct
25
+ `Shell(session, prompt_position="bottom")`. Both `Shell` and `ShellApp` accept
26
+ `prompt_position="inline"` or `prompt_position="bottom"`.
27
+
28
+ The widget provides a scrollable transcript, Up/Down history, Tab completion for
29
+ command names, options, and unquoted paths, Ctrl+C cancellation, and Ctrl+Q to quit the demo.
30
+ Tab first extends the shared prefix when several completions match. If no more
31
+ characters can be filled in, Tab opens a menu beside the cursor. Use Up/Down or
32
+ Tab to select, Enter to insert, and Escape to dismiss; typing also dismisses it.
33
+ Long choices remain on one line and can be read using the horizontal scrollbar.
34
+
35
+ ## Multiline commands
36
+
37
+ Shift+Enter inserts a newline without running the command. Enter submits a
38
+ complete command, or inserts a continuation line when a quote is still open or
39
+ the command ends with a pipe or backslash. Ctrl+O also inserts a newline on
40
+ terminals that cannot distinguish Shift+Enter. Ctrl+Enter forces submission,
41
+ including syntax errors. The editor grows up to eight rows, then scrolls.
42
+
43
+ ```sh
44
+ echo 'hello
45
+ world'
46
+ cat notes.txt |
47
+ grep Python |
48
+ count
49
+ ```
50
+
51
+ Unquoted newlines separate commands, which execute in order. A backslash followed
52
+ by a newline joins lines without inserting a character. Newlines inside quotes
53
+ remain part of the argument. All commands are parsed and validated before any
54
+ runs; a runtime failure does not prevent later commands from running, and the
55
+ final status is that of the last command. Ctrl+C cancels the active sequence or
56
+ clears unfinished input. History keeps a multiline submission as one entry.
57
+ Up/Down moves within a multiline editor; Alt+Up/Down navigates command history.
58
+ The same syntax works through `Session.execute()` and headless `-c` execution.
59
+
60
+ ## Commands and pipelines
61
+
62
+ ```sh
63
+ help
64
+ ls
65
+ ls -la
66
+ help ls
67
+ ls | where type file | select name | sort
68
+ cat README.md | grep Python | count
69
+ echo 'a quoted | character'
70
+ mkdir notes
71
+ touch empty.txt
72
+ rmdir notes
73
+ mkdir notes
74
+ cd notes
75
+ write hello.txt 'Hello from Python'
76
+ cat hello.txt
77
+ edit hello.txt
78
+ cp hello.txt backup.txt
79
+ mv backup.txt renamed.txt
80
+ rm hello.txt
81
+ cd /
82
+ sleep 30
83
+ clear
84
+ ```
85
+
86
+ Pipelines exchange Python values through bounded AnyIO streams. `ls` emits
87
+ records, displayed as names in columns in the widget (directories have a trailing
88
+ `/` and blue styling). Headless output lists one name per line. Pipeline values
89
+ remain records; `select` extracts fields; `count` emits an integer. `cat` and `grep`
90
+ stream text lines. `sort` buffers its input. A failure in any stage fails the
91
+ pipeline. Filesystem mutations, `cd`, and `clear` run outside pipelines.
92
+
93
+ `ls -l` (`--long`) displays type, byte size, modification time in UTC, and name.
94
+ Its pipeline records include `size` and `modified` (Unix seconds); directory size
95
+ is `None`. `ls -a` (`--all`) includes hidden entries, and flags combine as `-la`.
96
+ `grep -i` ignores case and `sort -r` reverses the order.
97
+ `touch PATH...` creates missing files or updates modification times without
98
+ changing existing contents. Existing directories can also be touched.
99
+ `rm PATH...` removes files, continuing with remaining paths if one fails.
100
+ `rmdir DIRECTORY...` removes empty directories and rejects files and nonempty
101
+ directories; its completion suggests only directories.
102
+
103
+ This is an initial shell language: quoting, multiline commands, and pipes are supported. Bash scripts,
104
+ redirection, environment expansion, glob expansion, process execution, job
105
+ control, and persistent history are not implemented. Commands are trusted Python
106
+ code, not a sandbox. Browser filesystem changes are in-memory and reset on reload.
107
+
108
+ ## Find files
109
+
110
+ ```sh
111
+ find
112
+ find docs -name '*.txt'
113
+ find . -iname '*.py' -type f
114
+ find . -type d -maxdepth 2
115
+ find docs other-directory -type f | select path
116
+ find . -type f | count
117
+ ```
118
+
119
+ `find [PATH...]` walks starting paths recursively, including the starting entries
120
+ and hidden names. It defaults to `.`. `-name`/`--name` matches a basename using
121
+ shell-style wildcard patterns; `-iname`/`--iname` ignores letter case.
122
+ `-type`/`--type` accepts `f` (file), `d` (directory), or `l` (symbolic link).
123
+ `-maxdepth`/`--max-depth` limits traversal, with starting paths at depth zero.
124
+ Filters combine with AND. Quote wildcard patterns to pass them unchanged.
125
+
126
+ Output records contain `name` (basename), `path` (relative to the current working
127
+ directory, or absolute when the starting path is absolute), and `type` (`file`,
128
+ `directory`, or `symlink`). The UI displays paths with the usual directory suffix;
129
+ `select path` extracts plain paths. Symbolic links, including starting paths,
130
+ are listed without following them. Errors are reported while traversal continues
131
+ with other entries and starting paths; any error makes the command fail.
132
+ Custom filesystem implementations provide `entry_type()` without following the
133
+ final symbolic link, alongside the existing directory-listing methods.
134
+
135
+ ## Move and copy files
136
+
137
+ ```sh
138
+ mv old.txt new.txt
139
+ mv notes archive
140
+ cp file.txt backup.txt
141
+ cp file.txt existing-directory/
142
+ cp -r notes notes-backup
143
+ ```
144
+
145
+ `mv SOURCE DESTINATION` moves or renames a file or a whole directory.
146
+ `cp SOURCE DESTINATION` copies a file; `cp -r` (or `--recursive`) also copies
147
+ whole directory trees, including hidden entries. If the destination is an
148
+ existing directory, the source keeps its name inside it. Existing files are
149
+ replaced; existing directory trees are not merged or replaced. Contents and
150
+ modification times are preserved. Both commands accept one source and one
151
+ destination and run outside pipelines. Moving the current directory or one of
152
+ its ancestors updates the session's working path.
153
+
154
+ ## Page through text
155
+
156
+ ```sh
157
+ more README.md
158
+ more first.txt second.txt
159
+ cat notes.txt | grep Python | more
160
+ ls -l | more
161
+ ```
162
+
163
+ `more` fills the Shell widget with a read-only, scrollable view, then restores the
164
+ transcript and prompt when closed. Space/Page Down moves forward a page;
165
+ `b`/Page Up moves back. Enter/Down advances one line, Up moves back one line,
166
+ and Home/End jumps to the beginning/end. `q`, Ctrl+Q, or Escape closes the pager.
167
+ Mouse scrolling also works. Shortcuts appear in the footer.
168
+
169
+ The pager accepts files or pipeline input, formatting Python values for display.
170
+ It streams input into scrollback; closing it stops consuming the pipeline.
171
+ Headless execution reports that an interactive frontend is required. Other
172
+ frontends can handle the `page` event's `PageRequest`, consume its `lines`, mark
173
+ it `handled`, and return when their pager closes.
174
+
175
+ ## Search with less
176
+
177
+ ```sh
178
+ less README.md
179
+ less first.txt second.txt
180
+ cat notes.txt | grep Python | less
181
+ ```
182
+
183
+ `less` uses the same in-shell pager and navigation shortcuts as `more`, adding
184
+ Python regex search. Press `/` to search forward or `?` to search backward,
185
+ then enter a pattern and press Enter. `n` finds the next match in that direction;
186
+ Shift+N finds the previous one. Searches wrap around the text. An empty search
187
+ reuses the last pattern, and Escape cancels the search field. Invalid patterns
188
+ and missing matches appear as notifications. The title shows the matching line.
189
+ The search field disappears after submission, leaving no permanent bottom line.
190
+
191
+ ## Edit files
192
+
193
+ `edit PATH` opens a plain text editor that fills the Shell widget, restoring the
194
+ transcript and prompt when closed. It loads an
195
+ existing UTF-8 file or starts empty for a new file. Ctrl+S saves without closing;
196
+ Ctrl+Q closes the editor and returns to the shell. Modified files show `*` beside the path. Closing with unsaved
197
+ changes updates the footer shortcuts: a second Ctrl+Q discards and closes (Ctrl+D
198
+ also works), Ctrl+S saves,
199
+ and Escape keeps editing. Save errors appear as notifications and leave the
200
+ editor and its text open. The title marks unsaved edits with `*`; shortcuts appear
201
+ in the app footer. The editor has no bottom status line or button rows. When embedding in
202
+ another Anytext app, give its app-wide Ctrl+Q quit binding `priority=False` so
203
+ the focused editor can handle that key first, as `ShellApp` does.
204
+
205
+ The editor uses the session filesystem on desktop and in the thread-free browser.
206
+ New files are created when saved. Browser files remain in memory until reload.
207
+ Headless `-c` execution reports that an interactive frontend is required; `edit`
208
+ cannot run in a pipeline. Other frontends can handle the `edit` event's
209
+ `EditRequest`, mark it `handled`, and return after their editor closes.
210
+
211
+ ## Use the engine
212
+
213
+ ```python
214
+ import anyio
215
+ from anyshell import Event, MemoryFileSystem, Session
216
+
217
+ session = Session(MemoryFileSystem({"hello.txt": "hello\nworld\n"}))
218
+
219
+
220
+ async def receive(event: Event):
221
+ if event.kind == "output":
222
+ print(event.value)
223
+
224
+
225
+ async def main():
226
+ await session.execute("cat hello.txt | count", receive)
227
+
228
+
229
+ anyio.run(main)
230
+ ```
231
+
232
+ Commands use Cyclopts to parse typed Python parameters. `--help` (or `-h`)
233
+ and `help COMMAND` write help to the transcript. Invalid arguments produce shell
234
+ errors without exiting the app. All pipeline arguments are validated before any
235
+ stage starts. Use `--` before a filename beginning with a dash.
236
+
237
+ Register a `Command` with a synchronous or asynchronous function. Parameters
238
+ named `ctx` (or `context`), `stdin`, and `stdout` are injected by the engine and
239
+ must be keyword-only. Other parameters become command arguments and options.
240
+ Input and output are AnyIO object streams; a returned value is also emitted as
241
+ one pipeline value. Command functions should stay cooperative for cancellation.
242
+ `Session.cancel()` must be called from the session's event loop.
243
+
244
+ ```python
245
+ from typing import Annotated
246
+
247
+ from cyclopts import Parameter
248
+ from anyshell import Command
249
+
250
+
251
+ async def upper(
252
+ *,
253
+ prefix: Annotated[str, Parameter(name=["--prefix", "-p"])] = "",
254
+ stdin,
255
+ stdout,
256
+ ):
257
+ async for value in stdin:
258
+ await stdout.send(prefix + str(value).upper())
259
+
260
+
261
+ session.register(Command("upper", "Uppercase input values", upper))
262
+ # Now: echo hello | upper -p '> '
263
+ ```
264
+
265
+ `ctx.fs` exposes the filesystem and `ctx.emit` emits frontend events. Parsing
266
+ and help never expose the injected parameters as command options.
267
+
268
+ For the frontend, import `Shell` from `anyshell.frontend` and yield
269
+ `Shell(session)` in any Anytext app. History and working directory live in the
270
+ session, so the engine and UI can evolve independently.
271
+
272
+ ## Browser demo
273
+
274
+ ```sh
275
+ uv run python tools/build_browser.py
276
+ uv run python -m http.server --directory .cache/browser-site 8000
277
+ ```
278
+
279
+ Open http://localhost:8000. The Prompt selector switches between inline and bottom
280
+ layouts by reloading the demo (`?prompt=inline` or `?prompt=bottom`). The demo downloads Pyodide and Anytext from public
281
+ package servers and installs this checkout's Anyshell wheel. It uses an in-memory
282
+ workspace and disables threads. Internet access is needed on first load.
283
+
284
+ ## Development
285
+
286
+ ```sh
287
+ uv run pytest
288
+ uv run ruff check .
289
+ uv run ruff format --check .
290
+ uv build
291
+ ```
292
+
293
+ For the real browser regression test:
294
+
295
+ ```sh
296
+ uv run python tools/build_browser.py
297
+ npm ci --prefix tests/browser
298
+ cd tests/browser
299
+ npx playwright install chromium
300
+ npm test
301
+ ```
302
+
303
+ Async engine and widget tests run on both asyncio and Trio. The engine depends
304
+ on AnyIO and Cyclopts; install `anyshell[anytext]` for the UI and `anyshell[trio]` for Trio.
305
+
306
+ The browser terminal scaffolding is adapted from Anytext's MIT-licensed browser demo.
307
+
308
+ ## Releasing
309
+
310
+ Set `project.version` in `pyproject.toml`, update the lockfile with `uv lock`, and
311
+ commit the release changes. Push a matching version tag, without a `v` prefix:
312
+
313
+ ```sh
314
+ git tag 0.1.0
315
+ git push origin 0.1.0
316
+ ```
317
+
318
+ The `release.yml` workflow checks that the tag matches the package version,
319
+ builds and validates the wheel and source distribution, then publishes them to
320
+ PyPI using the configured trusted publisher. No PyPI API token is needed.