eosh 0.2.0__tar.gz → 0.3.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.
- {eosh-0.2.0/src/eosh.egg-info → eosh-0.3.0}/PKG-INFO +186 -197
- {eosh-0.2.0 → eosh-0.3.0}/README.md +178 -189
- {eosh-0.2.0 → eosh-0.3.0}/README.pypi.md +181 -192
- eosh-0.3.0/addons/awsut/__init__.py +14 -0
- eosh-0.3.0/addons/awsut/agentcore/__init__.py +31 -0
- {eosh-0.2.0/src/eosh/recipes/_awsut_agentcore → eosh-0.3.0/addons/awsut/agentcore}/harness.py +8 -8
- {eosh-0.2.0/src/eosh/recipes/_awsut_agentcore → eosh-0.3.0/addons/awsut/agentcore}/memory.py +10 -10
- {eosh-0.2.0/src/eosh/recipes/_awsut_agentcore → eosh-0.3.0/addons/awsut/agentcore}/render.py +12 -12
- eosh-0.2.0/src/eosh/recipes/awsut.py → eosh-0.3.0/addons/awsut/cli.py +31 -37
- eosh-0.2.0/src/eosh/recipes/_awsut_common.py → eosh-0.3.0/addons/awsut/common.py +12 -10
- eosh-0.3.0/addons/awsut/sagemaker/__init__.py +29 -0
- {eosh-0.2.0/src/eosh/recipes/_awsut_sagemaker → eosh-0.3.0/addons/awsut/sagemaker}/hub.py +4 -4
- {eosh-0.2.0/src/eosh/recipes/_awsut_sagemaker → eosh-0.3.0/addons/awsut/sagemaker}/hyperpod.py +17 -14
- {eosh-0.2.0/src/eosh/recipes/_awsut_sagemaker → eosh-0.3.0/addons/awsut/sagemaker}/jobs.py +18 -15
- {eosh-0.2.0/src/eosh/recipes/_awsut_sagemaker → eosh-0.3.0/addons/awsut/sagemaker}/render.py +3 -3
- {eosh-0.2.0/src/eosh/recipes/_awsut_sagemaker → eosh-0.3.0/addons/awsut/sagemaker}/studio.py +7 -7
- {eosh-0.2.0 → eosh-0.3.0}/pyproject.toml +15 -7
- eosh-0.3.0/setup.py +23 -0
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh/__init__.py +15 -14
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh/_config.py +132 -75
- eosh-0.3.0/src/eosh/_job_leader.py +115 -0
- eosh-0.3.0/src/eosh/colors.py +129 -0
- eosh-0.3.0/src/eosh/command_context.py +267 -0
- eosh-0.3.0/src/eosh/commands.py +634 -0
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh/completion.py +114 -246
- eosh-0.3.0/src/eosh/context.py +271 -0
- eosh-0.3.0/src/eosh/decorators/__init__.py +38 -0
- eosh-0.3.0/src/eosh/decorators/quiet.py +42 -0
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh/decorators/retry.py +6 -6
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh/decorators/time.py +6 -6
- eosh-0.3.0/src/eosh/decorators/watch.py +147 -0
- eosh-0.3.0/src/eosh/execution.py +99 -0
- eosh-0.3.0/src/eosh/history.py +190 -0
- eosh-0.3.0/src/eosh/hooks.py +151 -0
- eosh-0.3.0/src/eosh/job.py +411 -0
- eosh-0.3.0/src/eosh/keys.py +419 -0
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh/lineedit.py +399 -708
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh/notify.py +29 -14
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh/paths.py +1 -1
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh/pipeline.py +282 -138
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh/process.py +70 -142
- eosh-0.3.0/src/eosh/recipes/__init__.py +222 -0
- eosh-0.3.0/src/eosh/recipes/aws.py +383 -0
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh/recipes/chmod.py +0 -3
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh/recipes/chown.py +2 -4
- eosh-0.3.0/src/eosh/recipes/cobra.py +87 -0
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh/recipes/cp.py +0 -3
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh/recipes/curl.py +0 -3
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh/recipes/git.py +1 -1
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh/recipes/kill.py +2 -2
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh/recipes/ls.py +1 -1
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh/recipes/lsof.py +0 -3
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh/recipes/mv.py +0 -3
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh/recipes/ps.py +1 -4
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh/recipes/rm.py +0 -3
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh/recipes/rsync.py +0 -3
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh/recipes/scp.py +0 -3
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh/recipes/tar.py +0 -3
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh/recipes/terraform.py +1 -5
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh/recipes/top.py +0 -3
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh/recipes/unzip.py +0 -3
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh/recipes/zip.py +0 -3
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh/scrollbar.py +12 -16
- eosh-0.3.0/src/eosh/shell.py +2435 -0
- eosh-0.3.0/src/eosh/shell_integration.py +315 -0
- eosh-0.3.0/src/eosh/slots.py +544 -0
- eosh-0.3.0/src/eosh/switcher.py +362 -0
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh/terminal.py +76 -1
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh/tui.py +55 -314
- eosh-0.3.0/src/eosh/user_errors.py +55 -0
- eosh-0.3.0/src/eosh/variables.py +292 -0
- {eosh-0.2.0 → eosh-0.3.0/src/eosh.egg-info}/PKG-INFO +186 -197
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh.egg-info/SOURCES.txt +40 -20
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh.egg-info/entry_points.txt +3 -0
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh.egg-info/requires.txt +2 -2
- eosh-0.3.0/src/eosh.egg-info/top_level.txt +2 -0
- eosh-0.3.0/tests/test_alias_expansion.py +223 -0
- {eosh-0.2.0 → eosh-0.3.0}/tests/test_aws_recipe.py +22 -26
- eosh-0.3.0/tests/test_cobra_completion.py +277 -0
- eosh-0.3.0/tests/test_colors.py +91 -0
- eosh-0.3.0/tests/test_command_context.py +426 -0
- eosh-0.3.0/tests/test_command_input.py +151 -0
- {eosh-0.2.0 → eosh-0.3.0}/tests/test_commands.py +123 -96
- {eosh-0.2.0 → eosh-0.3.0}/tests/test_completion.py +14 -19
- {eosh-0.2.0 → eosh-0.3.0}/tests/test_completion_picker.py +67 -39
- eosh-0.3.0/tests/test_confirm.py +81 -0
- {eosh-0.2.0 → eosh-0.3.0}/tests/test_context.py +109 -48
- {eosh-0.2.0 → eosh-0.3.0}/tests/test_decorators.py +338 -414
- {eosh-0.2.0 → eosh-0.3.0}/tests/test_env_prefix.py +26 -0
- eosh-0.3.0/tests/test_execution.py +84 -0
- eosh-0.3.0/tests/test_forwarding.py +139 -0
- eosh-0.3.0/tests/test_history.py +259 -0
- eosh-0.3.0/tests/test_hooks.py +208 -0
- eosh-0.3.0/tests/test_job.py +408 -0
- eosh-0.3.0/tests/test_keys.py +385 -0
- {eosh-0.2.0 → eosh-0.3.0}/tests/test_notify.py +51 -83
- {eosh-0.2.0 → eosh-0.3.0}/tests/test_parsing.py +1 -17
- {eosh-0.2.0 → eosh-0.3.0}/tests/test_piped_python_commands.py +114 -27
- {eosh-0.2.0 → eosh-0.3.0}/tests/test_pipeline.py +8 -8
- eosh-0.3.0/tests/test_process.py +57 -0
- eosh-0.3.0/tests/test_recipe_missing.py +70 -0
- eosh-0.3.0/tests/test_recipes.py +145 -0
- eosh-0.3.0/tests/test_run_handler.py +105 -0
- eosh-0.3.0/tests/test_s3_completion.py +161 -0
- {eosh-0.2.0 → eosh-0.3.0}/tests/test_scrollbar.py +2 -9
- eosh-0.3.0/tests/test_sequence_park.py +29 -0
- eosh-0.3.0/tests/test_shell_integration.py +362 -0
- {eosh-0.2.0 → eosh-0.3.0}/tests/test_source_bash.py +16 -16
- {eosh-0.2.0 → eosh-0.3.0}/tests/test_subcommands.py +35 -38
- {eosh-0.2.0 → eosh-0.3.0}/tests/test_tar_recipe.py +5 -6
- eosh-0.3.0/tests/test_terminal_query.py +98 -0
- eosh-0.3.0/tests/test_user_config.py +164 -0
- {eosh-0.2.0 → eosh-0.3.0}/tests/test_variables.py +200 -42
- eosh-0.2.0/src/eosh/colors.py +0 -63
- eosh-0.2.0/src/eosh/commands.py +0 -785
- eosh-0.2.0/src/eosh/context.py +0 -189
- eosh-0.2.0/src/eosh/decorators/__init__.py +0 -263
- eosh-0.2.0/src/eosh/decorators/bg.py +0 -39
- eosh-0.2.0/src/eosh/decorators/quiet.py +0 -39
- eosh-0.2.0/src/eosh/decorators/watch.py +0 -660
- eosh-0.2.0/src/eosh/history.py +0 -36
- eosh-0.2.0/src/eosh/recipes/__init__.py +0 -208
- eosh-0.2.0/src/eosh/recipes/_awsut_agentcore/__init__.py +0 -46
- eosh-0.2.0/src/eosh/recipes/_awsut_sagemaker/__init__.py +0 -36
- eosh-0.2.0/src/eosh/recipes/_missing.py +0 -168
- eosh-0.2.0/src/eosh/recipes/aws.py +0 -239
- eosh-0.2.0/src/eosh/shell.py +0 -4253
- eosh-0.2.0/src/eosh/variables.py +0 -253
- eosh-0.2.0/src/eosh.egg-info/top_level.txt +0 -1
- eosh-0.2.0/tests/test_alias_expansion.py +0 -143
- eosh-0.2.0/tests/test_awsut_credentials.py +0 -171
- eosh-0.2.0/tests/test_awsut_whoami.py +0 -317
- eosh-0.2.0/tests/test_cobra_completion.py +0 -293
- eosh-0.2.0/tests/test_history_completion.py +0 -634
- eosh-0.2.0/tests/test_lineedit_history.py +0 -184
- eosh-0.2.0/tests/test_passthrough_input.py +0 -135
- eosh-0.2.0/tests/test_process.py +0 -116
- eosh-0.2.0/tests/test_recipe_missing.py +0 -144
- eosh-0.2.0/tests/test_recipes.py +0 -246
- eosh-0.2.0/tests/test_sagemaker_recipe.py +0 -2412
- eosh-0.2.0/tests/test_user_config.py +0 -27
- {eosh-0.2.0 → eosh-0.3.0}/LICENSE +0 -0
- {eosh-0.2.0 → eosh-0.3.0}/setup.cfg +0 -0
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh/__main__.py +0 -0
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh/completion_cache.py +0 -0
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh/parsing.py +0 -0
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh/prompt.py +0 -0
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh/recipes/df.py +0 -0
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh/recipes/du.py +0 -0
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh/recipes/find.py +0 -0
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh/recipes/grep.py +0 -0
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh/recipes/make.py +0 -0
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh/recipes/ssh.py +0 -0
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh/recipes/tail.py +0 -0
- {eosh-0.2.0 → eosh-0.3.0}/src/eosh.egg-info/dependency_links.txt +0 -0
- {eosh-0.2.0 → eosh-0.3.0}/tests/test_argcomplete_fallback.py +0 -0
- {eosh-0.2.0 → eosh-0.3.0}/tests/test_completion_cache.py +0 -0
- {eosh-0.2.0 → eosh-0.3.0}/tests/test_make_recipe.py +0 -0
- {eosh-0.2.0 → eosh-0.3.0}/tests/test_redirects.py +0 -0
- {eosh-0.2.0 → eosh-0.3.0}/tests/test_shell_continuation.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: eosh
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A lightweight but powerful terminal shell with rich TAB completion and context switching
|
|
5
5
|
Author: craftware
|
|
6
6
|
License-Expression: MIT
|
|
@@ -24,16 +24,16 @@ Classifier: Topic :: Terminals
|
|
|
24
24
|
Requires-Python: >=3.12
|
|
25
25
|
Description-Content-Type: text/markdown
|
|
26
26
|
License-File: LICENSE
|
|
27
|
-
Provides-Extra:
|
|
28
|
-
Requires-Dist: boto3; extra == "
|
|
29
|
-
Requires-Dist: pexpect; extra == "
|
|
27
|
+
Provides-Extra: awsut
|
|
28
|
+
Requires-Dist: boto3; extra == "awsut"
|
|
29
|
+
Requires-Dist: pexpect; extra == "awsut"
|
|
30
30
|
Provides-Extra: dev
|
|
31
31
|
Requires-Dist: pytest>=8; extra == "dev"
|
|
32
|
-
Requires-Dist: eosh[
|
|
32
|
+
Requires-Dist: eosh[awsut]; extra == "dev"
|
|
33
33
|
Dynamic: license-file
|
|
34
34
|
|
|
35
35
|
<!-- Generated from README.md by scripts/gen_pypi_readme.py; relative
|
|
36
|
-
links rewritten against the v0.
|
|
36
|
+
links rewritten against the v0.3.0 tag for PyPI. Do not edit
|
|
37
37
|
or commit. -->
|
|
38
38
|
|
|
39
39
|
# Eolith Shell
|
|
@@ -41,23 +41,23 @@ Dynamic: license-file
|
|
|
41
41
|
**Eolith Shell** (`eosh`) is a lightweight but powerful terminal shell environment with rich tab completion and context switching.
|
|
42
42
|
|
|
43
43
|
|
|
44
|
-

|
|
45
45
|
|
|
46
46
|
## Features
|
|
47
47
|
|
|
48
48
|
- **Rich tab completion** — per-argument completers with descriptions, inline picker UI
|
|
49
|
-
- **
|
|
50
|
-
- **Context switching** — named environments with variables and working directories,
|
|
49
|
+
- **Flag completion** — TAB on `-` lists every flag with its description; a flag that takes a value goes straight on to completing that value
|
|
50
|
+
- **Context switching** — named environments with variables and working directories, switched by name or live with `Ctrl+]`
|
|
51
51
|
- **PTY process multiplexing** — run processes in contexts and switch between them without killing them
|
|
52
52
|
- **Pipelines and redirections** — `|`, `>`, `>>`, `<`, `2>`, `2>&1`, `;`, `&&`, `||`, globs (`*`, `?`, `**`), and `\`-line-continuation
|
|
53
|
-
- **Pipeline decorators** — wrap any pipeline with `@watch`, `@time`, `@retry
|
|
53
|
+
- **Pipeline decorators** — wrap any pipeline with `@watch`, `@time`, `@retry` or `@quiet`; authoring your own is a few lines of Python
|
|
54
54
|
- **Custom commands** — define Python functions as shell commands with full completion support
|
|
55
55
|
- **Python-backed variables** — `var aws_region=us-east-1` can drive multiple `os.environ` keys via a `Var` subclass; `$NAME` expansion is symmetric
|
|
56
56
|
- **Completion recipes** — opt-in TAB completion for `git`, `make`, `ssh`, `aws`, and more
|
|
57
|
-
- **Protocol fallbacks** —
|
|
57
|
+
- **Protocol fallbacks** — completion for cobra-based tools (`docker`, `kubectl`, `helm`, `gh`, …, opt-in by name) and, automatically, argcomplete-based Python CLIs (`pipx`, `conda`, `pre-commit`, `tox`, …) — no hand-written recipe needed
|
|
58
58
|
- **System command fallback** — anything not a registered command runs through the system shell
|
|
59
59
|
- **Cross-platform** — interactive shell, completion, pipelines, redirects, contexts, and history all work on POSIX and Windows; PTY-backed multiplexing of running native processes is POSIX-only
|
|
60
|
-
- **History** —
|
|
60
|
+
- **History** — shared by every eosh window (SQLite): fish-style suggestions of lines you ran in this directory, `Ctrl+R` search that starts from what you've typed, per-context up/down navigation, and a `history` command
|
|
61
61
|
- **Desktop notifications** — an OS notification when a command that took more than 10 seconds finishes, including ones running in a backgrounded context; native backends only, no extra dependencies
|
|
62
62
|
|
|
63
63
|
## Installation
|
|
@@ -66,14 +66,14 @@ Requires Python 3.12+. The core has no dependencies beyond the standard library.
|
|
|
66
66
|
|
|
67
67
|
```bash
|
|
68
68
|
pip install eosh # or: pipx install eosh / uv tool install eosh
|
|
69
|
-
pip install "eosh[
|
|
70
|
-
uv tool install "eosh[
|
|
69
|
+
pip install "eosh[awsut]" # + the `awsut` add-on (boto3, pexpect)
|
|
70
|
+
uv tool install "eosh[awsut]" # same, as an isolated uv tool
|
|
71
71
|
```
|
|
72
72
|
|
|
73
73
|
On first launch `eosh` writes a starter `~/.eosh/config.py` that enables
|
|
74
|
-
every built-in recipe. Without the `[
|
|
75
|
-
everything else loads normally; running `awsut` then
|
|
76
|
-
|
|
74
|
+
every built-in recipe and bundled add-on. Without the `[awsut]` extra,
|
|
75
|
+
`awsut` is skipped and everything else loads normally; running `awsut` then
|
|
76
|
+
says which module is missing and which extra provides it.
|
|
77
77
|
|
|
78
78
|
`uv tool` and `pipx` install eosh into its own virtualenv, so a
|
|
79
79
|
`pip install boto3` in another shell does not reach it — add packages with
|
|
@@ -108,22 +108,29 @@ eosh
|
|
|
108
108
|
| `alias [NAME[=EXPANSION] ...]` | List aliases, show one, or define a shorthand for a command |
|
|
109
109
|
| `unalias NAME [NAME ...]` | Remove aliases |
|
|
110
110
|
| `source-bash [FILE [ARG ...]]` | Run a bash script (or a pasted block) and import its variables and cwd |
|
|
111
|
+
| `confirm [MESSAGE]` | Ask `MESSAGE [y/N]` (default `Continue?`); exit 0 on `y`, 1 otherwise — `confirm 'Destroy prod?' && terraform destroy` |
|
|
112
|
+
| `history [-n N] [--here] [KEYWORD ...]` | List past command lines (every window, every context), filtered by keywords or this directory |
|
|
111
113
|
| `reload` | Reload `~/.eosh/config.py` without restarting |
|
|
114
|
+
| `config edit` | Open `~/.eosh/config.py` in `$VISUAL` / `$EDITOR`, then reload it |
|
|
112
115
|
| `exit` | Exit the shell |
|
|
113
116
|
|
|
114
117
|
Any command not listed above is passed through to the system shell (e.g., `ls`, `git`, `grep`).
|
|
115
118
|
|
|
116
119
|
### Aliases
|
|
117
120
|
|
|
118
|
-
An alias replaces the
|
|
121
|
+
An alias replaces a command's name with text, as in bash: at the start of a line, after `;` `&&` `||` `|`, and as a decorator's body. The text can be anything a line can hold — pipes, `;` / `&&`, redirects, `$VAR`, decorators. Quote the expansion when it contains spaces:
|
|
119
122
|
|
|
120
123
|
```
|
|
121
124
|
eosh> alias hp='awsut sagemaker hyperpod'
|
|
122
125
|
eosh> hp list # runs: awsut sagemaker hyperpod list
|
|
126
|
+
eosh> alias gl='git log --oneline | head -20'
|
|
127
|
+
eosh> alias deploy='confirm "Deploy to prod?" && make deploy'
|
|
123
128
|
eosh> alias # list all aliases
|
|
124
129
|
eosh> unalias hp
|
|
125
130
|
```
|
|
126
131
|
|
|
132
|
+
`\hp` or `'hp'` runs the command itself, not the alias. An alias may use another (`alias ll='ls -l'` with `alias ls='ls -G'` runs `ls -G -l`), but never itself, so `alias ls='ls -G'` is safe.
|
|
133
|
+
|
|
127
134
|
### Importing Variables from Bash
|
|
128
135
|
|
|
129
136
|
`source-bash` runs a script in a real `bash` — so `export`, `$(…)`, loops, heredocs and conditionals all work — and imports the environment and working directory it leaves behind, the way bash's own `source` would:
|
|
@@ -141,26 +148,27 @@ eosh> source-bash -c 'export A=$(date +%s)'
|
|
|
141
148
|
Contexts let you define named environments with variables that are exported to `os.environ` and a remembered working directory.
|
|
142
149
|
|
|
143
150
|
```
|
|
144
|
-
eosh> context
|
|
145
|
-
|
|
151
|
+
eosh> context new prod
|
|
152
|
+
Created context 'prod'
|
|
146
153
|
[prod] eosh> var ACCOUNT=123456 REGION=us-east-1
|
|
147
|
-
[prod] eosh> context
|
|
148
|
-
|
|
154
|
+
[prod] eosh> context new staging
|
|
155
|
+
Created context 'staging'
|
|
149
156
|
[staging] eosh> var ACCOUNT=789012 REGION=us-west-2
|
|
150
|
-
[staging] eosh> context
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
157
|
+
[staging] eosh> context list
|
|
158
|
+
* staging {'ACCOUNT': '789012', 'REGION': 'us-west-2'}
|
|
159
|
+
prod {'ACCOUNT': '123456', 'REGION': 'us-east-1'}
|
|
160
|
+
default
|
|
161
|
+
[staging] eosh> context close
|
|
162
|
+
Closed 'staging', now in 'prod'
|
|
155
163
|
```
|
|
156
164
|
|
|
157
165
|
Context subcommands:
|
|
158
166
|
|
|
159
167
|
| Subcommand | Description |
|
|
160
168
|
|------------|-------------|
|
|
161
|
-
| `context
|
|
162
|
-
| `context
|
|
163
|
-
| `context switch <name>` | Switch to an existing context
|
|
169
|
+
| `context new <name>` | Create a context (inheriting the current one's variables and history) and switch to it |
|
|
170
|
+
| `context close [name]` | Remove a context (default: the current one); the most recently used remaining one becomes current |
|
|
171
|
+
| `context switch <name>` | Switch to an existing context |
|
|
164
172
|
| `context list` | Show all contexts with their state and variables |
|
|
165
173
|
| `context kill <name>` | Send SIGTERM to the running process in a context |
|
|
166
174
|
|
|
@@ -168,10 +176,10 @@ Context subcommands:
|
|
|
168
176
|
|
|
169
177
|
Press `Ctrl+]` at the shell prompt (or while a process is running) to open a TUI context picker:
|
|
170
178
|
|
|
171
|
-
- Arrow keys or `Ctrl+P
|
|
179
|
+
- Arrow keys or `Ctrl+P` to navigate
|
|
172
180
|
- **Enter** to switch to the selected context
|
|
173
181
|
- **Esc** / `Ctrl+C` to cancel
|
|
174
|
-
-
|
|
182
|
+
- `Ctrl+N` creates a new context, `Ctrl+D` deletes the highlighted one, `Ctrl+R` renames it
|
|
175
183
|
|
|
176
184
|
If the target context has a running process, switching to it resumes that process immediately. The original process keeps running in the background — visible as `[bg:1]` in the prompt.
|
|
177
185
|
|
|
@@ -181,41 +189,28 @@ Press TAB to complete:
|
|
|
181
189
|
- Command names (registered commands + system PATH executables)
|
|
182
190
|
- File/directory paths (default fallback)
|
|
183
191
|
- Custom per-argument completions defined by commands
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
what you've typed
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
Candidates are also scoped to the **directory** you're in: eosh records where
|
|
208
|
-
each command was run (`~/.eosh/history.dirs`) and offers only the lines you ran
|
|
209
|
-
here, so another checkout's `make deploy` stays out of the way. Nothing matching
|
|
210
|
-
run here means no history rows — the picker just shows the ordinary candidates.
|
|
211
|
-
`↑`/`↓` and `Ctrl+R` are not directory-scoped, so lines from elsewhere are still
|
|
212
|
-
one key away.
|
|
213
|
-
|
|
214
|
-
**Flag completion** — when flags are available, TAB opens a multi-select checkbox picker:
|
|
215
|
-
- Navigate with arrows; **Space** toggles a flag; **Enter** confirms
|
|
216
|
-
- Type a letter to jump to the next flag starting with that letter
|
|
217
|
-
- Short boolean flags are combined: selecting `-a` and `-l` inserts `-al`
|
|
218
|
-
- Flags that take a value (e.g. `-d N`) insert `flag ` then open a value picker or show an inline hint
|
|
192
|
+
|
|
193
|
+
### History
|
|
194
|
+
|
|
195
|
+
As you type, the most recent line you ran **in this directory** that starts with what you've typed appears dimmed after the cursor. **→** (or Ctrl+E / End) accepts it, and **Alt+F** accepts one word. Otherwise keep typing:
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
~/proj> git commit -m "fix typo"
|
|
199
|
+
^^^^^^^^^^^^^^^^^^ typed "git co"; the rest is the suggestion
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
- **Ctrl+R** searches every line you've run, anywhere. It starts filtered by what's already typed, and shows where and when each line last ran.
|
|
203
|
+
- **`history`** lists past lines: `history docker run`, `history -n 100 --here`.
|
|
204
|
+
- **↑ / ↓** walk the current context's own list.
|
|
205
|
+
|
|
206
|
+
History is shared by every eosh window you have open (`~/.eosh/history.db`, SQLite). A line run in one window is suggested in another at once, and shells running side by side never overwrite each other's history. See [doc/history.md](https://github.com/crftwr/eosh/blob/v0.3.0/doc/history.md).
|
|
207
|
+
|
|
208
|
+
**Flag completion** — TAB on `-` lists the command's flags as ordinary picker
|
|
209
|
+
rows, each with its description:
|
|
210
|
+
- Type to narrow, Down/Up and **Enter** to pick — one flag per TAB
|
|
211
|
+
- A flag that takes a value is shown as `-d <N>`; picking it inserts `-d ` and
|
|
212
|
+
goes straight on to completing the value (a directory picker for `tar -C`,
|
|
213
|
+
say). When the value has no completer, the status bar shows what to type.
|
|
219
214
|
|
|
220
215
|
### Pipelines, Redirects, and Sequencing
|
|
221
216
|
|
|
@@ -252,7 +247,6 @@ A **decorator** is a token of the form `@name [flags]` at the start of a line th
|
|
|
252
247
|
@time {make && ./run-tests}
|
|
253
248
|
@retry -n 5 --delay 2 curl https://flaky.example.com/
|
|
254
249
|
@quiet pytest -q
|
|
255
|
-
@bg {tail -f /var/log/system.log} # run in a fresh background context
|
|
256
250
|
```
|
|
257
251
|
|
|
258
252
|
**Scope rule.** If the wrapped pipeline contains `|`, `;`, `&&`, `||`, or a redirect, it must be enclosed in `{...}`. Single-command bodies don't need braces. This makes the decorator's scope visible at a glance and side-steps the `watch -n 5 ls | grep abc` ambiguity that POSIX `watch` is famous for.
|
|
@@ -261,17 +255,18 @@ A **decorator** is a token of the form `@name [flags]` at the start of a line th
|
|
|
261
255
|
|
|
262
256
|
| Decorator | Description |
|
|
263
257
|
|-----------|-------------|
|
|
264
|
-
| `@watch [-n SEC] [--no-clear]` |
|
|
258
|
+
| `@watch [-n SEC] [--no-clear]` | Re-run a pipeline on a timer, showing its latest output (`q` quits) |
|
|
265
259
|
| `@time` | Print elapsed wall/user/sys time after the pipeline finishes |
|
|
266
260
|
| `@retry [-n N] [--delay SEC]` | Re-run the pipeline on non-zero exit, up to `N` attempts |
|
|
267
261
|
| `@quiet [--stderr]` | Discard stdout (and stderr with `--stderr`); still propagates the exit code |
|
|
268
|
-
| `@bg [name]` | Run the pipeline in a fresh background context (resumable via `Ctrl+]`) |
|
|
269
262
|
|
|
270
263
|
See the [Custom Decorators](#custom-decorators) section below for authoring your own.
|
|
271
264
|
|
|
272
265
|
## Customization
|
|
273
266
|
|
|
274
|
-
Create `~/.eosh/config.py` to define custom commands and completers. This file is plain Python that imports from eosh. Use `reload` to apply changes without restarting.
|
|
267
|
+
Create `~/.eosh/config.py` to define custom commands and completers. This file is plain Python that imports from eosh. Use `reload` to apply changes without restarting, or `config edit` to open it in your editor and reload when you quit. `reload` starts from a clean slate: everything the previous run registered (commands, decorators, aliases, variables, the prompt, a custom notifier) is dropped before the config runs again.
|
|
268
|
+
|
|
269
|
+
A config can't replace a built-in (`cd`, `help`, `@watch`, the `notify` variable, …) by accident: registering one of those names prints a `config warning:` line and keeps the built-in. Pass `override=True` (`registry.command("cd", override=True)`, `var_registry.register(v, override=True)`) when you mean it; `help` lists your commands apart from the built-ins, and removing the override and running `reload` brings the built-in back.
|
|
275
270
|
|
|
276
271
|
```python
|
|
277
272
|
# ~/.eosh/config.py
|
|
@@ -284,7 +279,7 @@ enable("make", "git", "ssh")
|
|
|
284
279
|
|
|
285
280
|
class InstanceCompleter(Completer):
|
|
286
281
|
def complete(self, ctx):
|
|
287
|
-
account = ctx.args[0] if ctx.args else ctx.shell_context.
|
|
282
|
+
account = ctx.args[0] if ctx.args else ctx.shell_context.get_var("ACCOUNT")
|
|
288
283
|
# fetch instances for account...
|
|
289
284
|
return [Completion(value="i-abc123", description="web-server-1")]
|
|
290
285
|
|
|
@@ -304,37 +299,25 @@ def connect(account, region, instance_id):
|
|
|
304
299
|
|
|
305
300
|
### Python-Backed Variables
|
|
306
301
|
|
|
307
|
-
|
|
302
|
+
Register a variable with `var_registry`, and the built-in `var` command, bare `NAME=VALUE` assignment and `$NAME` / `${NAME}` expansion all go through it — a Python-backed variable is read- and write-symmetric with `os.environ`. An `EnvVar` names one or more environment keys (the shell writes all of them, per context). For a value that should stay out of child processes, subclass `PyVar` (still per-context) or `GlobalVar` (one value for the whole shell).
|
|
308
303
|
|
|
309
304
|
```python
|
|
310
305
|
# ~/.eosh/config.py
|
|
311
|
-
import
|
|
312
|
-
from eosh import Var, EnvVar, var_registry
|
|
306
|
+
from eosh import EnvVar, var_registry
|
|
313
307
|
from eosh.completion import ChoiceCompleter, CallbackCompleter
|
|
314
308
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
os.environ["AWS_DEFAULT_REGION"] = value
|
|
325
|
-
|
|
326
|
-
@property
|
|
327
|
-
def value_completer(self):
|
|
328
|
-
return ChoiceCompleter(["us-east-1", "us-west-2", "eu-west-1"])
|
|
329
|
-
|
|
330
|
-
var_registry.register(AwsRegionVar())
|
|
331
|
-
var_registry.register(
|
|
332
|
-
EnvVar("aws_profile", "AWS_PROFILE",
|
|
333
|
-
completer=CallbackCompleter(lambda: ["default", "prod", "staging"]))
|
|
334
|
-
)
|
|
309
|
+
var_registry.register(EnvVar(
|
|
310
|
+
"aws_region", keys=["AWS_REGION", "AWS_DEFAULT_REGION"],
|
|
311
|
+
completer=ChoiceCompleter(["us-east-1", "us-west-2", "eu-west-1"]),
|
|
312
|
+
description="AWS region — sets AWS_REGION + AWS_DEFAULT_REGION",
|
|
313
|
+
))
|
|
314
|
+
var_registry.register(EnvVar(
|
|
315
|
+
"aws_profile", keys="AWS_PROFILE",
|
|
316
|
+
completer=CallbackCompleter(lambda: ["default", "prod", "staging"]),
|
|
317
|
+
))
|
|
335
318
|
```
|
|
336
319
|
|
|
337
|
-
|
|
320
|
+
With the variables above:
|
|
338
321
|
|
|
339
322
|
```
|
|
340
323
|
eosh> var aws_region=us-west-2
|
|
@@ -345,19 +328,18 @@ eosh> aws ec2 describe-instances --region $aws_region
|
|
|
345
328
|
|
|
346
329
|
### Custom Decorators
|
|
347
330
|
|
|
348
|
-
|
|
331
|
+
A decorator is a command named `@name`: register it with `registry.command("@name", ...)`, exactly as you would a command. The function receives the body as its first positional argument (a `Sequence`: the pipelines inside the braces, joined by `;` / `&&` / `||`) and the parsed flag namespace as kwargs; call `body.run()` to execute it and return the exit code.
|
|
349
332
|
|
|
350
333
|
```python
|
|
351
334
|
# ~/.eosh/config.py
|
|
352
335
|
import sys
|
|
353
336
|
import time
|
|
354
|
-
from eosh.commands import arg
|
|
355
|
-
from eosh.
|
|
356
|
-
from eosh.pipeline import Pipeline
|
|
337
|
+
from eosh.commands import arg, registry
|
|
338
|
+
from eosh.pipeline import Sequence
|
|
357
339
|
|
|
358
|
-
@
|
|
359
|
-
|
|
360
|
-
help="Run the
|
|
340
|
+
@registry.command(
|
|
341
|
+
"@repeat",
|
|
342
|
+
help="Run the body N times, stopping early on the first failure.",
|
|
361
343
|
params=[
|
|
362
344
|
arg("-n", "--count", type=int, default=3, metavar="N",
|
|
363
345
|
help="number of iterations (default 3)"),
|
|
@@ -365,11 +347,11 @@ from eosh.pipeline import Pipeline
|
|
|
365
347
|
help="seconds to sleep between iterations"),
|
|
366
348
|
],
|
|
367
349
|
)
|
|
368
|
-
def repeat(
|
|
350
|
+
def repeat(body: Sequence, *, count: int, delay: float) -> int:
|
|
369
351
|
last = 0
|
|
370
352
|
for i in range(1, count + 1):
|
|
371
353
|
sys.stderr.write(f"@repeat: iteration {i}/{count}\n")
|
|
372
|
-
last =
|
|
354
|
+
last = body.run()
|
|
373
355
|
if last != 0:
|
|
374
356
|
return last
|
|
375
357
|
if delay > 0 and i < count:
|
|
@@ -384,40 +366,33 @@ eosh> @repeat -n 5 --delay 1 ls
|
|
|
384
366
|
eosh> @repeat -n 3 {make && ./run-tests}
|
|
385
367
|
```
|
|
386
368
|
|
|
387
|
-
To share decorators across machines or teammates,
|
|
388
|
-
|
|
389
|
-
```python
|
|
390
|
-
# ~/.eosh/config.py
|
|
391
|
-
from eosh.decorators import add_decorator_path, enable as enable_decorators
|
|
392
|
-
|
|
393
|
-
add_decorator_path("/team/shared/decorators") # optional extra directory
|
|
394
|
-
enable_decorators("repeat") # found in ~/.eosh/decorators/
|
|
395
|
-
# or /team/shared/decorators/
|
|
396
|
-
```
|
|
369
|
+
To share decorators across machines or teammates, put them in a module and import it from `config.py` — see [Your Own Recipes](#your-own-recipes).
|
|
397
370
|
|
|
398
|
-
###
|
|
371
|
+
### Talking to the User: `ctx`
|
|
399
372
|
|
|
400
|
-
|
|
373
|
+
A command that asks questions, offers a choice, or launches an interactive program declares `pass_context=True`. Its handler then receives a `CommandContext` as its first argument:
|
|
401
374
|
|
|
402
375
|
```python
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
376
|
+
@registry.command("deploy", params=[arg("env")], pass_context=True)
|
|
377
|
+
def deploy(ctx, env):
|
|
378
|
+
target = ctx.choose(["web", "worker", "scheduler"], title="Deploy which target?")
|
|
379
|
+
if target is None or not ctx.confirm(f"Deploy {target} to {env}?"):
|
|
380
|
+
return 1
|
|
381
|
+
return ctx.run_interactive(["make", "deploy", f"TARGET={target}"])
|
|
408
382
|
```
|
|
409
383
|
|
|
410
|
-
|
|
384
|
+
| Method | Does |
|
|
385
|
+
|---|---|
|
|
386
|
+
| `ctx.input(prompt)` / `ctx.input_block(prompt)` | read a line / a pasted block (up to a blank line) |
|
|
387
|
+
| `ctx.confirm(prompt, default=False)` | yes/no question; `[y/N]` is appended |
|
|
388
|
+
| `ctx.choose(items, title="")` | inline picker; returns the item, or `None` on Esc |
|
|
389
|
+
| `ctx.run_interactive(argv)` | run `ssh`, an MFA prompt, a TUI… on a terminal of its own |
|
|
390
|
+
| `ctx.get_var(name)` / `ctx.set_var(name, value)` / `ctx.unset_var(name)` | the context's variables, as `$name` / `var name=value` |
|
|
391
|
+
| `ctx.context_name`, `ctx.cwd` | where the command is running |
|
|
411
392
|
|
|
412
|
-
|
|
393
|
+
Use these instead of `input()` and `subprocess.run` for anything that reads the keyboard. A Python command runs on a background thread while the shell reads every key (that is what lets Ctrl+] switch away from it), so `input()` or an interactive `subprocess.run` would split keystrokes with the shell. Non-interactive subprocesses (`capture_output=True`, `pexpect.popen_spawn`, …) are fine as they are.
|
|
413
394
|
|
|
414
|
-
|
|
415
|
-
from eosh import passthrough_input
|
|
416
|
-
|
|
417
|
-
answer = passthrough_input("Continue? [y/N] ")
|
|
418
|
-
```
|
|
419
|
-
|
|
420
|
-
Outside a Python command thread, both helpers fall back to the obvious thing (`subprocess.run` and `input`), so the same code is safe in either context. Non-interactive subprocesses (`capture_output=True`, pipeline stages with explicit pipes, `pexpect.popen_spawn`, …) don't need wrapping.
|
|
395
|
+
`ctx` belongs to the context the command **started in**. A command you send to the background with Ctrl+] still reads and sets that context's variables, so a script started in `prod` can't change `staging`.
|
|
421
396
|
|
|
422
397
|
### Desktop Notifications
|
|
423
398
|
|
|
@@ -429,7 +404,7 @@ When a command runs for at least 10 seconds, eosh posts an OS notification as it
|
|
|
429
404
|
✓ eosh — 1h 05m [bg-1] terraform apply
|
|
430
405
|
```
|
|
431
406
|
|
|
432
|
-
Commands running in a context you backgrounded with `Ctrl+]`
|
|
407
|
+
Commands running in a context you backgrounded with `Ctrl+]` notify too, tagged with the context name. Interactive programs — editors, pagers, `top`, `ssh`, `tmux`, sub-shells — are skipped, since sitting in them for an hour isn't work finishing.
|
|
433
408
|
|
|
434
409
|
Backends are whatever the platform already ships: `osascript` on macOS, `notify-send` on Linux, a PowerShell toast on Windows, and the terminal bell as a fallback. Nothing to install.
|
|
435
410
|
|
|
@@ -453,7 +428,44 @@ notify.SKIP_COMMANDS.add("psql")
|
|
|
453
428
|
notify.set_notifier(lambda title, message: post_to_slack(f"{title}\n{message}"))
|
|
454
429
|
```
|
|
455
430
|
|
|
456
|
-
See [doc/notifications.md](https://github.com/crftwr/eosh/blob/v0.
|
|
431
|
+
See [doc/notifications.md](https://github.com/crftwr/eosh/blob/v0.3.0/doc/notifications.md) for the design and the known limits.
|
|
432
|
+
|
|
433
|
+
### Event Hooks
|
|
434
|
+
|
|
435
|
+
Run your own functions when something happens in the shell. Each event is a decorator from `eosh.hooks`:
|
|
436
|
+
|
|
437
|
+
```python
|
|
438
|
+
# ~/.eosh/config.py
|
|
439
|
+
import os
|
|
440
|
+
from eosh import hooks
|
|
441
|
+
|
|
442
|
+
@hooks.on_context_switched
|
|
443
|
+
def title(old, new):
|
|
444
|
+
print(f"\x1b]2;[{new}] eosh\x07", end="", flush=True) # terminal title
|
|
445
|
+
|
|
446
|
+
@hooks.on_directory_changed
|
|
447
|
+
def per_project(old, new):
|
|
448
|
+
if os.path.exists(os.path.join(new, ".venv")):
|
|
449
|
+
print("(this project has a .venv)")
|
|
450
|
+
|
|
451
|
+
@hooks.on_command_not_found
|
|
452
|
+
def auto_cd(argv): # type a directory name to cd into it
|
|
453
|
+
if len(argv) == 1 and os.path.isdir(argv[0]):
|
|
454
|
+
os.chdir(argv[0])
|
|
455
|
+
return True # claimed: no "command not found"
|
|
456
|
+
return False
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
| Event | Called as |
|
|
460
|
+
|---|---|
|
|
461
|
+
| `on_startup` / `on_exit` | `f()` |
|
|
462
|
+
| `on_directory_changed` | `f(old, new)` — `cd`, a context switch, anything that moves the cwd |
|
|
463
|
+
| `on_context_switched` | `f(old, new)` — Ctrl+], `context switch` / `new` / `close` |
|
|
464
|
+
| `on_command_starting` | `f(line)` — just before a line runs (observe only) |
|
|
465
|
+
| `on_command_finished` | `f(line, status, elapsed)` |
|
|
466
|
+
| `on_command_not_found` | `f(argv) -> bool` — return `True` to claim the command |
|
|
467
|
+
|
|
468
|
+
Hooks run in registration order. A hook that raises is reported with its traceback, and the next one still runs. `reload` forgets them all before re-running the config. See [doc/hooks.md](https://github.com/crftwr/eosh/blob/v0.3.0/doc/hooks.md) for the details and [doc/limitations.md](https://github.com/crftwr/eosh/blob/v0.3.0/doc/limitations.md#event-hooks--what-they-dont-see) for the known gaps.
|
|
457
469
|
|
|
458
470
|
### Prompt Customization
|
|
459
471
|
|
|
@@ -491,9 +503,7 @@ The function is called each time the prompt is displayed, so it reflects dynamic
|
|
|
491
503
|
| `CallbackCompleter(func)` | Complete from a function's return value |
|
|
492
504
|
| `FileCompleter()` | Complete filesystem paths (files and directories) |
|
|
493
505
|
| `DirCompleter()` | Complete directory paths only |
|
|
494
|
-
| `OptionsCompleter(options, args)` | Complete flags
|
|
495
|
-
| `ConditionalCompleter(mapping)` | Pick a sub-completer based on preceding args |
|
|
496
|
-
| `HistoryCompleter(history_fn, limit, ran_here_fn)` | Continue the typed line from past command lines (may span several arguments), scoped to the ones run in the cwd |
|
|
506
|
+
| `OptionsCompleter(options, args)` | Complete flags, one picker row each; `args` declares value-taking flags |
|
|
497
507
|
|
|
498
508
|
### Completion Recipes
|
|
499
509
|
|
|
@@ -506,71 +516,52 @@ enable("git", "make", "ssh", "kill", "ls", "grep", "find", "du", "df", "tail", "
|
|
|
506
516
|
|
|
507
517
|
Each recipe registers flag completion (via `OptionsCompleter`) and positional completions (subcommands, files, branches, etc.) for the named command.
|
|
508
518
|
|
|
509
|
-
Two
|
|
519
|
+
Two protocols cover whole families of tools without a per-tool recipe:
|
|
510
520
|
|
|
511
|
-
- **Cobra-based tools** (`docker`, `kubectl`, `helm`, `gh`, `argocd`, …) — `CobraCompleter` drives their `__complete` subcommand, including live resource enumeration (running containers, k8s resources, GitHub issues, …). See [doc/cobra
|
|
512
|
-
- **argcomplete-based Python CLIs** (`pipx`, `conda`, `pre-commit`, `tox`, `pdm`, `httpie`, …) — `ArgcompleteCompleter` detects the `# PYTHON_ARGCOMPLETE_OK` marker and drives the argcomplete protocol. See [doc/argcomplete-fallback.md](https://github.com/crftwr/eosh/blob/v0.
|
|
521
|
+
- **Cobra-based tools** (`docker`, `kubectl`, `helm`, `gh`, `argocd`, …) — `CobraCompleter` drives their `__complete` subcommand, including live resource enumeration (running containers, k8s resources, GitHub issues, …). Opt-in by name: `enable("cobra")` covers the well-known tools, and `enable_cobra("mytool")` adds your own. See [doc/cobra.md](https://github.com/crftwr/eosh/blob/v0.3.0/doc/cobra.md).
|
|
522
|
+
- **argcomplete-based Python CLIs** (automatic) (`pipx`, `conda`, `pre-commit`, `tox`, `pdm`, `httpie`, …) — `ArgcompleteCompleter` detects the `# PYTHON_ARGCOMPLETE_OK` marker and drives the argcomplete protocol. See [doc/argcomplete-fallback.md](https://github.com/crftwr/eosh/blob/v0.3.0/doc/argcomplete-fallback.md).
|
|
513
523
|
|
|
514
|
-
####
|
|
524
|
+
#### Your Own Recipes
|
|
515
525
|
|
|
516
|
-
|
|
526
|
+
A recipe is plain Python: register a command with no handler, and its `params` drive completion for the external tool. Write it in `config.py`, or in a module next to it — `~/.eosh` is on `sys.path` while `config.py` runs, and `reload` re-runs the modules imported from there:
|
|
517
527
|
|
|
518
528
|
```python
|
|
519
|
-
|
|
520
|
-
enable("git") # built-in
|
|
521
|
-
enable("my_tool") # found in ~/.eosh/recipes/my_tool.py
|
|
522
|
-
```
|
|
523
|
-
|
|
524
|
-
A recipe file must define a `register()` function:
|
|
525
|
-
|
|
526
|
-
```python
|
|
527
|
-
# ~/.eosh/recipes/my_tool.py
|
|
529
|
+
# ~/.eosh/my_tools.py
|
|
528
530
|
from eosh.commands import arg, registry
|
|
529
|
-
from eosh.completion import CallbackCompleter
|
|
530
|
-
|
|
531
|
-
def register():
|
|
532
|
-
registry.command(
|
|
533
|
-
"my-tool",
|
|
534
|
-
help="my-tool — deploy/rollback/status helper",
|
|
535
|
-
params=[
|
|
536
|
-
arg("subcommand", choices=["deploy", "rollback", "status"]),
|
|
537
|
-
arg("target", help="deploy target", completer=CallbackCompleter(_list_targets)),
|
|
538
|
-
arg("-v", "--verbose", action="store_true", help="verbose"),
|
|
539
|
-
arg("--dry-run", action="store_true", help="don't apply changes"),
|
|
540
|
-
],
|
|
541
|
-
)
|
|
531
|
+
from eosh.completion import CallbackCompleter
|
|
542
532
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
533
|
+
registry.command(
|
|
534
|
+
"my-tool",
|
|
535
|
+
help="my-tool — deploy/rollback/status helper",
|
|
536
|
+
params=[
|
|
537
|
+
arg("subcommand", choices=["deploy", "rollback", "status"]),
|
|
538
|
+
arg("target", help="deploy target",
|
|
539
|
+
completer=CallbackCompleter(lambda: ["web", "worker", "scheduler"])),
|
|
540
|
+
arg("-v", "--verbose", action="store_true", help="verbose"),
|
|
541
|
+
arg("--dry-run", action="store_true", help="don't apply changes"),
|
|
542
|
+
],
|
|
543
|
+
)
|
|
546
544
|
```
|
|
547
545
|
|
|
548
|
-
#### Recipe Search Path
|
|
549
|
-
|
|
550
|
-
The default search path contains only `~/.eosh/recipes/`. Call `add_recipe_path()` to add more directories — useful for sharing recipes across a team:
|
|
551
|
-
|
|
552
546
|
```python
|
|
553
|
-
|
|
547
|
+
# ~/.eosh/config.py
|
|
548
|
+
import sys
|
|
549
|
+
sys.path.append("/team/shared/eosh") # optional: recipes shared by a team
|
|
550
|
+
from eosh.recipes import enable
|
|
554
551
|
|
|
555
|
-
|
|
556
|
-
|
|
552
|
+
enable("git") # built-in
|
|
553
|
+
import my_tools # your own, from ~/.eosh
|
|
557
554
|
```
|
|
558
555
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
2. `~/.eosh/recipes/<name>.py` — personal recipes
|
|
563
|
-
3. Additional paths in the order they were added via `add_recipe_path()`
|
|
564
|
-
|
|
565
|
-
You can also read or modify `recipe_search_path` directly (it is a plain `list[Path]`).
|
|
556
|
+
A module you import may use third-party packages, but they must be installed
|
|
557
|
+
in eosh's own environment (see [Installation](#installation) for `uv tool` /
|
|
558
|
+
`pipx`).
|
|
566
559
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
`PATH`); running it says which module is missing and how to install it.
|
|
573
|
-
Naming the recipe explicitly — `enable("my_tool")` — still raises.
|
|
560
|
+
`enable()` never stops your config: an unknown name, or a recipe whose
|
|
561
|
+
`register()` raises, is a `config warning:` line (the latter with its
|
|
562
|
+
traceback) and the remaining names still load. An error in a module of your
|
|
563
|
+
own — a typo in an import, a syntax error — is printed at startup with a
|
|
564
|
+
traceback through your files.
|
|
574
565
|
|
|
575
566
|
### Writing a Custom Completer
|
|
576
567
|
|
|
@@ -580,7 +571,7 @@ Subclass `Completer` and implement `complete()`. The `CompletionContext` gives y
|
|
|
580
571
|
- `args` — previously completed arguments
|
|
581
572
|
- `arg_index` — which argument position is being completed
|
|
582
573
|
- `prefix` — partial text typed so far
|
|
583
|
-
- `shell_context` — the active context (
|
|
574
|
+
- `shell_context` — the active context, read-only: `.get_var(name)`, `.context_name`, `.cwd`
|
|
584
575
|
|
|
585
576
|
To add completion to a system command without wrapping it, register a handler-less command — execution falls through to the real binary:
|
|
586
577
|
|
|
@@ -604,12 +595,12 @@ registry.command(
|
|
|
604
595
|
| Key | Action |
|
|
605
596
|
|-----|--------|
|
|
606
597
|
| `Tab` | Open completion picker |
|
|
607
|
-
| `Ctrl+R` | Search history |
|
|
598
|
+
| `Ctrl+R` | Search history, starting from what's typed |
|
|
608
599
|
| `Ctrl+]` | Open context switcher |
|
|
609
600
|
| `↑` / `↓` or `Ctrl+P/N` | Navigate history |
|
|
610
|
-
| `Ctrl+A` / `Ctrl+E` | Move to start / end of line |
|
|
611
|
-
| `Ctrl+B` / `Ctrl+F
|
|
612
|
-
| `Alt+B` / `Alt+F` | Move one word left / right |
|
|
601
|
+
| `Ctrl+A` / `Ctrl+E` | Move to start / end of line (`Ctrl+E` at the end accepts the suggestion) |
|
|
602
|
+
| `Ctrl+B` / `Ctrl+F`, `←` / `→` | Move one character left / right (at the end, accept the suggestion) |
|
|
603
|
+
| `Alt+B` / `Alt+F` | Move one word left / right (`Alt+F` at the end accepts one word of the suggestion) |
|
|
613
604
|
| `Ctrl+W` | Delete word before cursor |
|
|
614
605
|
| `Ctrl+K` | Delete to end of line |
|
|
615
606
|
| `Ctrl+U` | Delete to beginning of line |
|
|
@@ -621,10 +612,8 @@ registry.command(
|
|
|
621
612
|
| Path | Purpose |
|
|
622
613
|
|------|---------|
|
|
623
614
|
| `~/.eosh/config.py` | User configuration |
|
|
624
|
-
| `~/.eosh/history` | Command history |
|
|
625
|
-
| `~/.eosh
|
|
626
|
-
| `~/.eosh/recipes/<name>.py` | User-defined completion recipes (loaded by `enable("<name>")`) |
|
|
627
|
-
| `~/.eosh/decorators/<name>.py` | User-defined pipeline decorators (loaded by `enable("<name>")`) |
|
|
615
|
+
| `~/.eosh/history.db` | Command history, shared by every eosh process (SQLite) |
|
|
616
|
+
| `~/.eosh/*.py` | Your own modules (recipes, commands, decorators) — on `sys.path` while `config.py` runs |
|
|
628
617
|
|
|
629
618
|
## Platform Support
|
|
630
619
|
|
|
@@ -634,4 +623,4 @@ The one POSIX-only feature is **PTY-backed multiplexing of a live external proce
|
|
|
634
623
|
|
|
635
624
|
## License
|
|
636
625
|
|
|
637
|
-
MIT — see [LICENSE](https://github.com/crftwr/eosh/blob/v0.
|
|
626
|
+
MIT — see [LICENSE](https://github.com/crftwr/eosh/blob/v0.3.0/LICENSE).
|