eosh 0.2.0__py3-none-any.whl
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/__init__.py +34 -0
- eosh/__main__.py +23 -0
- eosh/_config.py +367 -0
- eosh/colors.py +63 -0
- eosh/commands.py +785 -0
- eosh/completion.py +952 -0
- eosh/completion_cache.py +77 -0
- eosh/context.py +189 -0
- eosh/decorators/__init__.py +263 -0
- eosh/decorators/bg.py +39 -0
- eosh/decorators/quiet.py +39 -0
- eosh/decorators/retry.py +46 -0
- eosh/decorators/time.py +44 -0
- eosh/decorators/watch.py +660 -0
- eosh/history.py +36 -0
- eosh/lineedit.py +1442 -0
- eosh/notify.py +404 -0
- eosh/parsing.py +226 -0
- eosh/paths.py +18 -0
- eosh/pipeline.py +668 -0
- eosh/process.py +465 -0
- eosh/prompt.py +77 -0
- eosh/recipes/__init__.py +208 -0
- eosh/recipes/_awsut_agentcore/__init__.py +46 -0
- eosh/recipes/_awsut_agentcore/harness.py +721 -0
- eosh/recipes/_awsut_agentcore/memory.py +1209 -0
- eosh/recipes/_awsut_agentcore/render.py +337 -0
- eosh/recipes/_awsut_common.py +533 -0
- eosh/recipes/_awsut_sagemaker/__init__.py +36 -0
- eosh/recipes/_awsut_sagemaker/hub.py +866 -0
- eosh/recipes/_awsut_sagemaker/hyperpod.py +1948 -0
- eosh/recipes/_awsut_sagemaker/jobs.py +1041 -0
- eosh/recipes/_awsut_sagemaker/render.py +300 -0
- eosh/recipes/_awsut_sagemaker/studio.py +1584 -0
- eosh/recipes/_missing.py +168 -0
- eosh/recipes/aws.py +239 -0
- eosh/recipes/awsut.py +1649 -0
- eosh/recipes/chmod.py +58 -0
- eosh/recipes/chown.py +117 -0
- eosh/recipes/cp.py +80 -0
- eosh/recipes/curl.py +108 -0
- eosh/recipes/df.py +33 -0
- eosh/recipes/du.py +82 -0
- eosh/recipes/find.py +60 -0
- eosh/recipes/git.py +295 -0
- eosh/recipes/grep.py +54 -0
- eosh/recipes/kill.py +92 -0
- eosh/recipes/ls.py +44 -0
- eosh/recipes/lsof.py +56 -0
- eosh/recipes/make.py +219 -0
- eosh/recipes/mv.py +52 -0
- eosh/recipes/ps.py +73 -0
- eosh/recipes/rm.py +55 -0
- eosh/recipes/rsync.py +143 -0
- eosh/recipes/scp.py +97 -0
- eosh/recipes/ssh.py +92 -0
- eosh/recipes/tail.py +23 -0
- eosh/recipes/tar.py +170 -0
- eosh/recipes/terraform.py +374 -0
- eosh/recipes/top.py +84 -0
- eosh/recipes/unzip.py +96 -0
- eosh/recipes/zip.py +53 -0
- eosh/scrollbar.py +122 -0
- eosh/shell.py +4253 -0
- eosh/terminal.py +446 -0
- eosh/tui.py +1082 -0
- eosh/variables.py +253 -0
- eosh-0.2.0.dist-info/METADATA +637 -0
- eosh-0.2.0.dist-info/RECORD +73 -0
- eosh-0.2.0.dist-info/WHEEL +5 -0
- eosh-0.2.0.dist-info/entry_points.txt +2 -0
- eosh-0.2.0.dist-info/licenses/LICENSE +21 -0
- eosh-0.2.0.dist-info/top_level.txt +1 -0
eosh/__init__.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Eolith Shell — a lightweight but powerful terminal shell environment."""
|
|
2
|
+
|
|
3
|
+
from . import notify
|
|
4
|
+
from .colors import ColorScheme, set_color_scheme
|
|
5
|
+
from .commands import arg, CmdParser, registry as command_registry
|
|
6
|
+
from .prompt import set_prompt
|
|
7
|
+
from .shell import (
|
|
8
|
+
passthrough_input,
|
|
9
|
+
passthrough_input_block,
|
|
10
|
+
passthrough_poll_key,
|
|
11
|
+
passthrough_run,
|
|
12
|
+
)
|
|
13
|
+
from .variables import Var, EnvVar, registry as var_registry
|
|
14
|
+
|
|
15
|
+
#: Single source of truth for the version string -- the ONLY place the literal
|
|
16
|
+
#: appears in this repo. pyproject.toml derives it via its dynamic ``version``
|
|
17
|
+
#: (``attr = "eosh.__version__"``), and ``make tag`` rewrites this line.
|
|
18
|
+
__version__ = "0.2.0"
|
|
19
|
+
__all__ = [
|
|
20
|
+
"arg",
|
|
21
|
+
"CmdParser",
|
|
22
|
+
"ColorScheme",
|
|
23
|
+
"notify",
|
|
24
|
+
"set_color_scheme",
|
|
25
|
+
"set_prompt",
|
|
26
|
+
"Var",
|
|
27
|
+
"EnvVar",
|
|
28
|
+
"var_registry",
|
|
29
|
+
"command_registry",
|
|
30
|
+
"passthrough_run",
|
|
31
|
+
"passthrough_input",
|
|
32
|
+
"passthrough_input_block",
|
|
33
|
+
"passthrough_poll_key",
|
|
34
|
+
]
|
eosh/__main__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Entry point for eosh."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
from . import __version__
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main():
|
|
9
|
+
parser = argparse.ArgumentParser(
|
|
10
|
+
prog="eosh",
|
|
11
|
+
description="A lightweight but powerful terminal shell.",
|
|
12
|
+
)
|
|
13
|
+
parser.add_argument("--version", action="version",
|
|
14
|
+
version=f"%(prog)s {__version__}")
|
|
15
|
+
parser.parse_args()
|
|
16
|
+
|
|
17
|
+
from .shell import Shell
|
|
18
|
+
shell = Shell()
|
|
19
|
+
shell.run()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if __name__ == "__main__":
|
|
23
|
+
main()
|
eosh/_config.py
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
# eosh user configuration
|
|
2
|
+
# Define custom commands and completers here.
|
|
3
|
+
|
|
4
|
+
# ── Simple example: one positional argument ───────────────────────────────────
|
|
5
|
+
|
|
6
|
+
from eosh.commands import registry as command_registry, arg
|
|
7
|
+
from eosh.completion import ChoiceCompleter
|
|
8
|
+
|
|
9
|
+
@command_registry.command(
|
|
10
|
+
name="hello",
|
|
11
|
+
help="Greet someone by name.", # shell-facing description; no docstring needed
|
|
12
|
+
params=[arg("name", nargs="?", default="world", help="name of the person to greet",
|
|
13
|
+
completer=ChoiceCompleter(["world", "there"]))],
|
|
14
|
+
)
|
|
15
|
+
def hello(name):
|
|
16
|
+
print(f"Hello, {name}!")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ── Multi-level sub-command example ───────────────────────────────────────────
|
|
20
|
+
#
|
|
21
|
+
# Build a command tree by:
|
|
22
|
+
# 1. Calling `command_registry.command(name, ...)` bare — returns a `Command` node.
|
|
23
|
+
# 2. Chaining `.command(...)` on the node for each child (group or leaf).
|
|
24
|
+
# 3. Using `@parent.command(name, ...)` as a decorator to attach a handler.
|
|
25
|
+
#
|
|
26
|
+
# Flags declared on a parent node are *inherited* by every leaf automatically;
|
|
27
|
+
# a leaf handler only needs to accept the kwargs it actually uses — the rest
|
|
28
|
+
# are filtered out before the call. So `--verbose` and `--dry-run` declared on
|
|
29
|
+
# the `deploy` root are usable on `deploy app`, `deploy rollback`, etc.
|
|
30
|
+
|
|
31
|
+
deploy = command_registry.command(
|
|
32
|
+
"deploy",
|
|
33
|
+
help="Deploy services to environments.",
|
|
34
|
+
params=[
|
|
35
|
+
# Inherited shared flags — visible on every sub-command.
|
|
36
|
+
arg("-v", "--verbose", action="store_true",
|
|
37
|
+
help="print details for each step"),
|
|
38
|
+
arg("-n", "--dry-run", action="store_true",
|
|
39
|
+
help="show steps, skip execution"),
|
|
40
|
+
],
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@deploy.command(
|
|
45
|
+
"app",
|
|
46
|
+
help="Deploy a service to an environment.",
|
|
47
|
+
params=[
|
|
48
|
+
# choices= drives argparse validation AND TAB completion simultaneously.
|
|
49
|
+
arg("environment", choices=["prod", "staging", "dev"],
|
|
50
|
+
help="target environment"),
|
|
51
|
+
arg("service", nargs="?", default="all",
|
|
52
|
+
choices=["api", "web", "worker"],
|
|
53
|
+
help="service to deploy, or 'all'"),
|
|
54
|
+
# Value-taking flags: completer= drives TAB completion for the value.
|
|
55
|
+
arg("-t", "--timeout", type=int, default=60, metavar="SECONDS",
|
|
56
|
+
help="deployment timeout in seconds",
|
|
57
|
+
completer=ChoiceCompleter(["30", "60", "120", "300"])),
|
|
58
|
+
arg("-b", "--branch", default="main", metavar="BRANCH",
|
|
59
|
+
help="git branch to deploy"),
|
|
60
|
+
],
|
|
61
|
+
)
|
|
62
|
+
def deploy_app(environment, service, dry_run, verbose, timeout, branch):
|
|
63
|
+
import sys, time
|
|
64
|
+
# Inherited flags (dry_run, verbose) arrive as kwargs alongside this leaf's own.
|
|
65
|
+
# While this runs, press Ctrl+] to switch context without killing the deploy.
|
|
66
|
+
prefix = "[DRY RUN] " if dry_run else ""
|
|
67
|
+
print(f"{prefix}Deploying '{service}' to '{environment}' "
|
|
68
|
+
f"branch={branch!r} timeout={timeout}s")
|
|
69
|
+
for step, secs in [("Build image", 2),
|
|
70
|
+
("Push to registry", 3),
|
|
71
|
+
("Update deployment", 2),
|
|
72
|
+
("Wait for rollout", 4),
|
|
73
|
+
("Health checks", 2)]:
|
|
74
|
+
if verbose:
|
|
75
|
+
print(f" -> {step} ...", flush=True)
|
|
76
|
+
if not dry_run:
|
|
77
|
+
# Short sleeps with a flush touch sys.stdout each tick. In a
|
|
78
|
+
# pipeline, Ctrl-C closes the stage's stdout; the next flush()
|
|
79
|
+
# then raises promptly instead of grinding through the rest of
|
|
80
|
+
# the sleeps. Standalone, flush() is just a no-op cost.
|
|
81
|
+
for _ in range(secs * 10):
|
|
82
|
+
time.sleep(0.1)
|
|
83
|
+
sys.stdout.flush()
|
|
84
|
+
print(f" ok {step}")
|
|
85
|
+
print(f"{prefix}Done.")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@deploy.command(
|
|
89
|
+
"rollback",
|
|
90
|
+
help="Roll back a deployment to the previous revision.",
|
|
91
|
+
params=[
|
|
92
|
+
arg("environment", choices=["prod", "staging", "dev"],
|
|
93
|
+
help="target environment"),
|
|
94
|
+
arg("service", nargs="?", default="all",
|
|
95
|
+
choices=["api", "web", "worker"],
|
|
96
|
+
help="service to roll back, or 'all'"),
|
|
97
|
+
],
|
|
98
|
+
)
|
|
99
|
+
def deploy_rollback(environment, service, dry_run, verbose):
|
|
100
|
+
prefix = "[DRY RUN] " if dry_run else ""
|
|
101
|
+
print(f"{prefix}Rolling back '{service}' in '{environment}'")
|
|
102
|
+
if verbose:
|
|
103
|
+
print(" -> fetching previous revision ...")
|
|
104
|
+
print(" -> swapping pointer ...")
|
|
105
|
+
print(f"{prefix}Done.")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@deploy.command(
|
|
109
|
+
"status",
|
|
110
|
+
help="Show deployment status for an environment.",
|
|
111
|
+
params=[
|
|
112
|
+
arg("environment", choices=["prod", "staging", "dev"],
|
|
113
|
+
help="environment to inspect"),
|
|
114
|
+
],
|
|
115
|
+
)
|
|
116
|
+
def deploy_status(environment, verbose):
|
|
117
|
+
# Leaf only declares the inherited flags it cares about (verbose);
|
|
118
|
+
# dry_run is silently dropped because it's not in the signature.
|
|
119
|
+
print(f"Status for '{environment}':")
|
|
120
|
+
for name, ver in [("api", "v1.4.2"), ("web", "v1.4.2"), ("worker", "v1.4.1")]:
|
|
121
|
+
print(f" {name:8} {ver}")
|
|
122
|
+
if verbose:
|
|
123
|
+
print(f" deployed: 2026-05-26 14:32 UTC")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# ── Filter command (reads stdin, writes stdout) ───────────────────────────────
|
|
127
|
+
#
|
|
128
|
+
# Python @registry.command handlers are first-class pipeline stages: they can
|
|
129
|
+
# sit anywhere in a pipe (`producer | py_cmd | consumer`). Each stage runs
|
|
130
|
+
# on its own worker thread with `sys.stdin` / `sys.stdout` rebound to the
|
|
131
|
+
# pipe ends, so `print()` and iteration over `sys.stdin` "just work."
|
|
132
|
+
#
|
|
133
|
+
# Example: `cols` prints the requested whitespace-separated columns from
|
|
134
|
+
# each line — like `awk '{print $2, $5}'` but expressed in Python.
|
|
135
|
+
#
|
|
136
|
+
# ls -la | cols 9 5 # filename and size
|
|
137
|
+
# ps -A | cols 1 4 # PID and command
|
|
138
|
+
# printf 'a b c\nd e f\n' | cols 1 3 | grep d
|
|
139
|
+
#
|
|
140
|
+
# Caveats inherent to the in-process pipeline model — see
|
|
141
|
+
# doc/limitations.md for the full list:
|
|
142
|
+
#
|
|
143
|
+
# * If the consumer closes the pipe early (`cmd | head -1`), this stage
|
|
144
|
+
# gets a BrokenPipeError on its next print(); catching it is the
|
|
145
|
+
# well-behaved pattern for an unbounded producer.
|
|
146
|
+
# * Calling subprocess.run() from inside a piped Python command writes
|
|
147
|
+
# to the terminal, not the pipe, unless you pass stdout=sys.stdout.
|
|
148
|
+
|
|
149
|
+
import sys
|
|
150
|
+
|
|
151
|
+
@command_registry.command(
|
|
152
|
+
name="cols",
|
|
153
|
+
help="Print whitespace-separated columns by 1-based index from each stdin line.",
|
|
154
|
+
params=[
|
|
155
|
+
arg("indices", nargs="+", type=int, metavar="N",
|
|
156
|
+
help="1-based column number(s) to print"),
|
|
157
|
+
arg("-d", "--delim", default=" ", metavar="STR",
|
|
158
|
+
help="output delimiter (default: single space)"),
|
|
159
|
+
],
|
|
160
|
+
)
|
|
161
|
+
def cols(indices, delim):
|
|
162
|
+
try:
|
|
163
|
+
for line in sys.stdin:
|
|
164
|
+
fields = line.split()
|
|
165
|
+
picked = [fields[i - 1] for i in indices if 0 < i <= len(fields)]
|
|
166
|
+
print(delim.join(picked))
|
|
167
|
+
except BrokenPipeError:
|
|
168
|
+
pass # downstream closed early (e.g. `... | head -1`) — normal
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# ── Enable completion recipes for external commands ───────────────────────────
|
|
172
|
+
|
|
173
|
+
from eosh.recipes import enable
|
|
174
|
+
enable("*")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# ── Aliases ───────────────────────────────────────────────────────────────────
|
|
178
|
+
#
|
|
179
|
+
# Aliases expand the first token of a command line (bash-style):
|
|
180
|
+
#
|
|
181
|
+
# hp create ... → awsut sagemaker hyperpod create ...
|
|
182
|
+
# la /tmp → ls -la /tmp
|
|
183
|
+
#
|
|
184
|
+
# They participate in TAB completion: typing the alias name and pressing TAB
|
|
185
|
+
# completes the rest as if the expansion had been typed.
|
|
186
|
+
|
|
187
|
+
command_registry.alias("hp", "awsut sagemaker hyperpod")
|
|
188
|
+
command_registry.alias("la", "ls -la")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# ── Python-backed variables ───────────────────────────────────────────────────
|
|
192
|
+
#
|
|
193
|
+
# Register Vars to give `var NAME=VALUE` custom set logic and TAB completion
|
|
194
|
+
# for the value side. Two flavours:
|
|
195
|
+
#
|
|
196
|
+
# * EnvVar — 1-to-1 passthrough to a single os.environ key.
|
|
197
|
+
# * Var subclass — full control: write multiple env keys, validate, etc.
|
|
198
|
+
#
|
|
199
|
+
# At the prompt:
|
|
200
|
+
#
|
|
201
|
+
# eosh> var editor=<TAB> → vim, emacs, nano, code
|
|
202
|
+
# eosh> var editor=vim
|
|
203
|
+
# eosh> var http_proxy=http://... → sets HTTP_PROXY *and* HTTPS_PROXY
|
|
204
|
+
|
|
205
|
+
import os
|
|
206
|
+
from eosh.variables import registry as var_registry, EnvVar, Var
|
|
207
|
+
|
|
208
|
+
# Simple case: one logical name → one env var, with completion.
|
|
209
|
+
var_registry.register(EnvVar(
|
|
210
|
+
name="editor",
|
|
211
|
+
env_var="EDITOR",
|
|
212
|
+
completer=ChoiceCompleter(["vim", "emacs", "nano", "code"]),
|
|
213
|
+
description="Default text editor",
|
|
214
|
+
))
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# Custom case: one logical name → two env keys. Subclass Var when EnvVar
|
|
218
|
+
# isn't enough — e.g. when set() needs to write multiple env keys, validate
|
|
219
|
+
# the input, or trigger a side effect.
|
|
220
|
+
class _HttpProxyVar(Var):
|
|
221
|
+
"""Sets HTTP_PROXY and HTTPS_PROXY together from one logical name."""
|
|
222
|
+
|
|
223
|
+
@property
|
|
224
|
+
def name(self) -> str:
|
|
225
|
+
return "http_proxy"
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def description(self) -> str:
|
|
229
|
+
return "HTTP/HTTPS proxy — sets HTTP_PROXY + HTTPS_PROXY"
|
|
230
|
+
|
|
231
|
+
@property
|
|
232
|
+
def env_keys(self) -> list[str]:
|
|
233
|
+
# Listed env keys are saved/restored on context switch.
|
|
234
|
+
return ["HTTP_PROXY", "HTTPS_PROXY"]
|
|
235
|
+
|
|
236
|
+
def get(self) -> str | None:
|
|
237
|
+
return os.environ.get("HTTP_PROXY")
|
|
238
|
+
|
|
239
|
+
def set(self, value: str) -> None:
|
|
240
|
+
os.environ["HTTP_PROXY"] = value
|
|
241
|
+
os.environ["HTTPS_PROXY"] = value
|
|
242
|
+
|
|
243
|
+
var_registry.register(_HttpProxyVar())
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
# ── Desktop notifications for long-running commands ───────────────────────────
|
|
247
|
+
#
|
|
248
|
+
# When a command runs longer than the threshold, eosh posts an OS
|
|
249
|
+
# notification as it finishes — you've probably switched to a browser by then.
|
|
250
|
+
# Backends are whatever the platform already ships (osascript / notify-send /
|
|
251
|
+
# a PowerShell toast), so there's nothing to install.
|
|
252
|
+
#
|
|
253
|
+
# At the prompt:
|
|
254
|
+
#
|
|
255
|
+
# eosh> var notify=off → disable for this session
|
|
256
|
+
# eosh> var notify_threshold=30 → only notify for commands ≥ 30s
|
|
257
|
+
#
|
|
258
|
+
# Both variables are process-global: unlike EnvVars above, they are not
|
|
259
|
+
# saved/restored on context switch.
|
|
260
|
+
|
|
261
|
+
from eosh import notify
|
|
262
|
+
|
|
263
|
+
# Default is 10 seconds; raise it if your day is full of 15-second builds.
|
|
264
|
+
notify.configure(threshold=10)
|
|
265
|
+
|
|
266
|
+
# Commands whose long runtime means "I sat in it", not "work finished".
|
|
267
|
+
# The defaults already cover editors, pagers, top, ssh, tmux and sub-shells;
|
|
268
|
+
# add your own interactive tools here.
|
|
269
|
+
notify.SKIP_COMMANDS.update({"psql", "mysql", "sqlite3", "ipython"})
|
|
270
|
+
|
|
271
|
+
# Replace the platform backend entirely — post to Slack, ntfy.sh, tmux, ...
|
|
272
|
+
# Runs on a daemon thread and must not write to the terminal.
|
|
273
|
+
#
|
|
274
|
+
# def my_notifier(title, message):
|
|
275
|
+
# import urllib.request
|
|
276
|
+
# urllib.request.urlopen(
|
|
277
|
+
# urllib.request.Request(
|
|
278
|
+
# "https://ntfy.sh/my-private-topic",
|
|
279
|
+
# data=f"{title}\n{message}".encode(),
|
|
280
|
+
# ),
|
|
281
|
+
# timeout=5,
|
|
282
|
+
# )
|
|
283
|
+
#
|
|
284
|
+
# notify.set_notifier(my_notifier)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
# ── Customize the prompt ──────────────────────────────────────────────────────
|
|
288
|
+
|
|
289
|
+
from datetime import datetime
|
|
290
|
+
from eosh import set_prompt
|
|
291
|
+
|
|
292
|
+
def my_prompt(context_manager):
|
|
293
|
+
"""Replicates the built-in default prompt: [context] parent/cwd HH:MM:SS [bg:N]>."""
|
|
294
|
+
CYAN_BOLD = "\033[1m\033[38;2;0;188;212m"
|
|
295
|
+
BLUE_BOLD = "\033[1m\033[38;2;100;149;237m"
|
|
296
|
+
GREEN = "\033[38;2;80;200;100m"
|
|
297
|
+
YELLOW = "\033[38;2;229;192;123m"
|
|
298
|
+
RESET = "\033[0m"
|
|
299
|
+
|
|
300
|
+
parts = []
|
|
301
|
+
|
|
302
|
+
ctx = context_manager.current()
|
|
303
|
+
if ctx and ctx.name != "default":
|
|
304
|
+
parts.append(f"{CYAN_BOLD}[{ctx.name}]{RESET}")
|
|
305
|
+
|
|
306
|
+
cwd = os.getcwd()
|
|
307
|
+
home = os.path.expanduser("~")
|
|
308
|
+
if cwd == home:
|
|
309
|
+
short_path = "~"
|
|
310
|
+
elif cwd.startswith(home + os.sep):
|
|
311
|
+
rel = cwd[len(home) + 1:]
|
|
312
|
+
rel_parts = rel.split(os.sep)
|
|
313
|
+
if len(rel_parts) <= 2:
|
|
314
|
+
short_path = "~/" + rel
|
|
315
|
+
else:
|
|
316
|
+
short_path = os.sep.join(rel_parts[-2:])
|
|
317
|
+
else:
|
|
318
|
+
abs_parts = cwd.lstrip(os.sep).split(os.sep)
|
|
319
|
+
if len(abs_parts) <= 2:
|
|
320
|
+
short_path = "/" + os.sep.join(abs_parts)
|
|
321
|
+
else:
|
|
322
|
+
short_path = os.sep.join(abs_parts[-2:])
|
|
323
|
+
|
|
324
|
+
timestamp = datetime.now().strftime("%H:%M:%S")
|
|
325
|
+
parts.append(f"{BLUE_BOLD}{short_path}{RESET}")
|
|
326
|
+
parts.append(f"{GREEN}{timestamp}{RESET}")
|
|
327
|
+
|
|
328
|
+
bg_count = 0
|
|
329
|
+
current_name = context_manager.current_name
|
|
330
|
+
for name, c in context_manager.contexts.items():
|
|
331
|
+
if name != current_name and c.process_slot and c.process_slot.is_alive():
|
|
332
|
+
bg_count += 1
|
|
333
|
+
if bg_count:
|
|
334
|
+
parts.append(f"{YELLOW}[bg:{bg_count}]{RESET}")
|
|
335
|
+
|
|
336
|
+
return " ".join(parts) + "> "
|
|
337
|
+
|
|
338
|
+
set_prompt(my_prompt)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
# ── Color scheme ──────────────────────────────────────────────────────────────
|
|
342
|
+
#
|
|
343
|
+
# Choose a built-in scheme or define a fully custom one.
|
|
344
|
+
# Applies to TUI widgets — inline pickers and @watch's framing chrome
|
|
345
|
+
# (header / footer / scrollbar). Prompt colors are deliberately not part
|
|
346
|
+
# of the scheme: customize the prompt by passing a function to set_prompt().
|
|
347
|
+
# Built-in schemes: "dark" (default), "light".
|
|
348
|
+
#
|
|
349
|
+
# Uncomment one of the examples below:
|
|
350
|
+
|
|
351
|
+
from eosh import set_color_scheme, ColorScheme
|
|
352
|
+
|
|
353
|
+
# Built-in schemes (for light- or dark-background terminals):
|
|
354
|
+
# set_color_scheme("dark") # default
|
|
355
|
+
# set_color_scheme("light")
|
|
356
|
+
|
|
357
|
+
# Fully custom scheme — specify any subset of colors as (R, G, B) tuples:
|
|
358
|
+
# set_color_scheme(ColorScheme(
|
|
359
|
+
# picker_row_bg=(50, 50, 60), # non-selected picker row background
|
|
360
|
+
# picker_row_fg=(220, 220, 220), # non-selected picker row foreground
|
|
361
|
+
# picker_sel_bg=(80, 40, 160), # selected row background
|
|
362
|
+
# picker_sel_fg=(255, 255, 255), # selected row foreground
|
|
363
|
+
# scroll_thumb=(120, 120, 120), # scrollbar thumb (picker + @watch)
|
|
364
|
+
# scroll_track=(40, 40, 50), # scrollbar track (picker + @watch)
|
|
365
|
+
# statusbar_bg=(30, 30, 30), # picker bottom bar + @watch header/footer bg
|
|
366
|
+
# statusbar_fg=(200, 200, 200), # picker bottom bar + @watch header/footer fg
|
|
367
|
+
# ))
|
eosh/colors.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Color scheme support."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _fg(r: int, g: int, b: int) -> str:
|
|
9
|
+
return f"\033[38;2;{r};{g};{b}m"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _bg(r: int, g: int, b: int) -> str:
|
|
13
|
+
return f"\033[48;2;{r};{g};{b}m"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class ColorScheme:
|
|
18
|
+
# List rows — picker (and any future scrollable list widget)
|
|
19
|
+
picker_row_bg: tuple[int, int, int] = (68, 68, 68)
|
|
20
|
+
picker_row_fg: tuple[int, int, int] = (220, 220, 220)
|
|
21
|
+
picker_sel_bg: tuple[int, int, int] = (0, 95, 135)
|
|
22
|
+
picker_sel_fg: tuple[int, int, int] = (255, 255, 255)
|
|
23
|
+
# Scroll bar — shared by picker and @watch's body scroll
|
|
24
|
+
scroll_thumb: tuple[int, int, int] = (128, 128, 128)
|
|
25
|
+
scroll_track: tuple[int, int, int] = (48, 48, 48)
|
|
26
|
+
# Status bars — picker bottom hint bar + @watch header/footer
|
|
27
|
+
statusbar_bg: tuple[int, int, int] = (30, 30, 30)
|
|
28
|
+
statusbar_fg: tuple[int, int, int] = (200, 200, 200)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
SCHEMES: dict[str, ColorScheme] = {
|
|
32
|
+
"dark": ColorScheme(),
|
|
33
|
+
"light": ColorScheme(
|
|
34
|
+
picker_row_bg=(220, 220, 220),
|
|
35
|
+
picker_row_fg=(30, 30, 30),
|
|
36
|
+
picker_sel_bg=(0, 100, 180),
|
|
37
|
+
picker_sel_fg=(255, 255, 255),
|
|
38
|
+
scroll_thumb=(160, 160, 160),
|
|
39
|
+
scroll_track=(200, 200, 200),
|
|
40
|
+
statusbar_bg=(195, 210, 225),
|
|
41
|
+
statusbar_fg=(60, 60, 80),
|
|
42
|
+
),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
_active: ColorScheme = SCHEMES["dark"]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def set_color_scheme(scheme: str | ColorScheme) -> None:
|
|
49
|
+
"""Set the active color scheme by name or ColorScheme instance.
|
|
50
|
+
|
|
51
|
+
Available built-in names: "dark", "light".
|
|
52
|
+
"""
|
|
53
|
+
global _active
|
|
54
|
+
if isinstance(scheme, str):
|
|
55
|
+
if scheme not in SCHEMES:
|
|
56
|
+
raise ValueError(f"Unknown color scheme {scheme!r}. Available: {sorted(SCHEMES)}")
|
|
57
|
+
_active = SCHEMES[scheme]
|
|
58
|
+
else:
|
|
59
|
+
_active = scheme
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_color_scheme() -> ColorScheme:
|
|
63
|
+
return _active
|