xmyshell 0.2.10__tar.gz → 0.2.12__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. {xmyshell-0.2.10/xmyshell.egg-info → xmyshell-0.2.12}/PKG-INFO +25 -1
  2. {xmyshell-0.2.10 → xmyshell-0.2.12}/README.md +24 -0
  3. {xmyshell-0.2.10 → xmyshell-0.2.12}/pyproject.toml +1 -1
  4. {xmyshell-0.2.10 → xmyshell-0.2.12}/xmyshell/completer.py +47 -8
  5. {xmyshell-0.2.10 → xmyshell-0.2.12}/xmyshell/environment.py +3 -0
  6. {xmyshell-0.2.10 → xmyshell-0.2.12}/xmyshell/kernel.py +55 -38
  7. {xmyshell-0.2.10 → xmyshell-0.2.12}/xmyshell/keybindings.py +24 -11
  8. {xmyshell-0.2.10 → xmyshell-0.2.12}/xmyshell/lexer.py +41 -1
  9. {xmyshell-0.2.10 → xmyshell-0.2.12}/xmyshell/main.py +3 -2
  10. {xmyshell-0.2.10 → xmyshell-0.2.12}/xmyshell/meta.py +2 -0
  11. xmyshell-0.2.12/xmyshell/stubs/__init__.py +1 -0
  12. xmyshell-0.2.12/xmyshell/stubs/__init__.pyi +17 -0
  13. xmyshell-0.2.12/xmyshell/stubs/py.typed +0 -0
  14. {xmyshell-0.2.10 → xmyshell-0.2.12/xmyshell.egg-info}/PKG-INFO +25 -1
  15. {xmyshell-0.2.10 → xmyshell-0.2.12}/xmyshell.egg-info/SOURCES.txt +4 -1
  16. {xmyshell-0.2.10 → xmyshell-0.2.12}/LICENSE +0 -0
  17. {xmyshell-0.2.10 → xmyshell-0.2.12}/setup.cfg +0 -0
  18. {xmyshell-0.2.10 → xmyshell-0.2.12}/xmyshell/__init__.py +0 -0
  19. {xmyshell-0.2.10 → xmyshell-0.2.12}/xmyshell/__main__.py +0 -0
  20. {xmyshell-0.2.10 → xmyshell-0.2.12}/xmyshell/data/commands.json +0 -0
  21. {xmyshell-0.2.10 → xmyshell-0.2.12}/xmyshell/init.py +0 -0
  22. {xmyshell-0.2.10 → xmyshell-0.2.12}/xmyshell/themes.py +0 -0
  23. {xmyshell-0.2.10 → xmyshell-0.2.12}/xmyshell/update.py +0 -0
  24. {xmyshell-0.2.10 → xmyshell-0.2.12}/xmyshell/utils.py +0 -0
  25. {xmyshell-0.2.10 → xmyshell-0.2.12}/xmyshell.egg-info/dependency_links.txt +0 -0
  26. {xmyshell-0.2.10 → xmyshell-0.2.12}/xmyshell.egg-info/entry_points.txt +0 -0
  27. {xmyshell-0.2.10 → xmyshell-0.2.12}/xmyshell.egg-info/requires.txt +0 -0
  28. {xmyshell-0.2.10 → xmyshell-0.2.12}/xmyshell.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xmyshell
3
- Version: 0.2.10
3
+ Version: 0.2.12
4
4
  Summary: XmyShell is a shell-like tool built on top of prompt_toolkit which provides a persistent Python runtime.
5
5
  Requires-Python: >=3.12
6
6
  Description-Content-Type: text/markdown
@@ -118,6 +118,15 @@ or
118
118
  ❯ unalias ll
119
119
  ```
120
120
 
121
+ - Lists, tuples, generators, etc. will be expanded into shell arguments.
122
+ E.g. `{["arg1", "arg2 xxx"]}` will be expanded into `arg1 'arg2 xxx'`
123
+
124
+ Here is an example (though there are other ways to do this, it is just an example )
125
+ ```
126
+ ❯ pip install {cat("requirements.txt").splitlines()}
127
+ ```
128
+
129
+
121
130
  - Syntax highlighting via `prompt_toolkit`
122
131
  - Powerful auto-completion, history search, and automatic bracket/quote pairing
123
132
  - Startup configuration: `~/.xmyshell/config.py` loaded at launch
@@ -154,6 +163,12 @@ The exit code of the last command.
154
163
  - `exec_duration`
155
164
  The duration which last command took.
156
165
 
166
+ - `SHOW_WELCOME`
167
+ Toggle whether to show welcome message when staring.
168
+
169
+ - `SHELL_CMD`
170
+ Shell command line prefix, defaults to `["cmd", "/c"]` on Windows and `["bin/sh", "-c"]` on other OS.
171
+
157
172
  - `HOME_DIR`
158
173
  The home directory, similar to `~`.
159
174
 
@@ -170,6 +185,15 @@ The path of config file.
170
185
  - Auto bracket/parenthesis/quote pairing
171
186
 
172
187
 
188
+ ### Stubs
189
+
190
+ You can write your xmyshell script with these imports:
191
+ ```python
192
+ from xmyshell.themes import ... # themes
193
+ from xmyshell.stubs import ... # stubs for built-in names
194
+ ```
195
+
196
+
173
197
  ## Limitations
174
198
 
175
199
  **XmyShell** is Python‑first. This brings power, but also intentional trade‑offs.
@@ -108,6 +108,15 @@ or
108
108
  ❯ unalias ll
109
109
  ```
110
110
 
111
+ - Lists, tuples, generators, etc. will be expanded into shell arguments.
112
+ E.g. `{["arg1", "arg2 xxx"]}` will be expanded into `arg1 'arg2 xxx'`
113
+
114
+ Here is an example (though there are other ways to do this, it is just an example )
115
+ ```
116
+ ❯ pip install {cat("requirements.txt").splitlines()}
117
+ ```
118
+
119
+
111
120
  - Syntax highlighting via `prompt_toolkit`
112
121
  - Powerful auto-completion, history search, and automatic bracket/quote pairing
113
122
  - Startup configuration: `~/.xmyshell/config.py` loaded at launch
@@ -144,6 +153,12 @@ The exit code of the last command.
144
153
  - `exec_duration`
145
154
  The duration which last command took.
146
155
 
156
+ - `SHOW_WELCOME`
157
+ Toggle whether to show welcome message when staring.
158
+
159
+ - `SHELL_CMD`
160
+ Shell command line prefix, defaults to `["cmd", "/c"]` on Windows and `["bin/sh", "-c"]` on other OS.
161
+
147
162
  - `HOME_DIR`
148
163
  The home directory, similar to `~`.
149
164
 
@@ -160,6 +175,15 @@ The path of config file.
160
175
  - Auto bracket/parenthesis/quote pairing
161
176
 
162
177
 
178
+ ### Stubs
179
+
180
+ You can write your xmyshell script with these imports:
181
+ ```python
182
+ from xmyshell.themes import ... # themes
183
+ from xmyshell.stubs import ... # stubs for built-in names
184
+ ```
185
+
186
+
163
187
  ## Limitations
164
188
 
165
189
  **XmyShell** is Python‑first. This brings power, but also intentional trade‑offs.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "xmyshell"
3
- version = "0.2.10"
3
+ version = "0.2.12"
4
4
  description = "XmyShell is a shell-like tool built on top of prompt_toolkit which provides a persistent Python runtime."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -1,5 +1,6 @@
1
1
  from collections.abc import Generator, Iterable
2
2
  import os
3
+ import shlex
3
4
  import sys
4
5
  import re
5
6
  import time
@@ -118,16 +119,51 @@ class ShellCompleter(Completer):
118
119
  others = sorted(c for c in commands if c not in COMMON and c not in BUILTINS)
119
120
  return builtins + common + others
120
121
 
121
- def _path_completions(self, text: str, max_results: int = 100):
122
+ def _path_completions(self, text: str, max_results: int = 100, in_string: bool=False):
122
123
  if text and text[-1] in whitespace: return
123
- last = text.split()[-1] if text.split() else ""
124
+
125
+ def last_token_raw(text: str) -> str:
126
+ quote = None
127
+ start = 0
128
+ i = 0
129
+ while i < len(text):
130
+ ch = text[i]
131
+ if quote:
132
+ if ch == quote:
133
+ quote = None
134
+ elif ch == "\\" and quote == '"':
135
+ i += 1
136
+ else:
137
+ if ch in ("'", '"'):
138
+ quote = ch
139
+ elif ch in " \t":
140
+ start = i + 1
141
+ elif ch == "\\":
142
+ i += 1
143
+ i += 1
144
+ return text[start:]
145
+
146
+ def start_pos(text: str, prefix: str) -> int:
147
+ original_len = len(text)
148
+ if text and text[-1] in ("'", '"') and not text.endswith(prefix):
149
+ text = text[:-1]
150
+ if not prefix or not text.endswith(prefix):
151
+ return -len(prefix)
152
+ start = len(text) - len(prefix)
153
+ if start > 0 and text[start - 1] in ("'", '"'):
154
+ start -= 1
155
+ return start - original_len
156
+
157
+ last = last_token_raw(text)
158
+ in_string = in_string or last.startswith('"') or last.startswith("'")
159
+ last = last.lstrip('"\'')
124
160
  if last == '~': return # don't remove this
125
161
  path = os.path.expanduser(last)
126
162
  dirname = os.path.dirname(path) or "."
127
- prefix = os.path.basename(path)
163
+ prefix = os.path.basename(path).strip('"\'')
128
164
 
129
165
  try:
130
- matches = []
166
+ matches: list[str] = []
131
167
  for item in os.listdir(dirname):
132
168
  if item.startswith(prefix):
133
169
  matches.append(item)
@@ -137,11 +173,14 @@ class ShellCompleter(Completer):
137
173
  for item in sorted(matches):
138
174
  full = os.path.join(dirname, item)
139
175
  is_dir = os.path.isdir(full)
140
- name = item + ("/" if is_dir else "")
176
+ if ' ' in item and not in_string:
177
+ name = f'"{item}"'
178
+ else:
179
+ name = item
141
180
  yield Completion(
142
181
  name,
143
- display=truncate(name, MAX_DISPLAY_LEN),
144
- start_position=-len(prefix),
182
+ display=truncate(item, MAX_DISPLAY_LEN),
183
+ start_position=start_pos(text, prefix),
145
184
  display_meta="directory" if is_dir else "file",
146
185
  )
147
186
  except OSError:
@@ -207,7 +246,7 @@ class ShellCompleter(Completer):
207
246
 
208
247
  string = _in_string(text)
209
248
  if string:
210
- yield from self._path_completions(string)
249
+ yield from self._path_completions(string, in_string=True)
211
250
  return
212
251
 
213
252
  if text and text[-1].isspace():
@@ -10,6 +10,7 @@ from .utils import XMYSHELL_MAGIC, getcwd, getlogin, HOME_DIR, xmyshell_cat
10
10
  namespace: dict[str, Any] = {}
11
11
  aliases: dict[str, str] = {}
12
12
  init_environ: dict[str, str] = dict(os.environ)
13
+ SHELL_CMD = ["cmd", "/c"] if os.name == "nt" else ["/bin/sh", "-c"]
13
14
 
14
15
 
15
16
  def xmyshell_alias(altname: str, target: str) -> None:
@@ -34,6 +35,8 @@ _init_namespace: dict[str, Any] = {
34
35
  "cat": xmyshell_cat,
35
36
  "alias": xmyshell_alias,
36
37
  "unalias": xmyshell_unalias,
38
+ "SHOW_WELCOME": True,
39
+ "SHELL_CMD": SHELL_CMD,
37
40
  "HOME_DIR": HOME_DIR,
38
41
  "PROFILE": f"{HOME_DIR}/.xmyshell/config.py",
39
42
  "exit_code": 0,
@@ -1,11 +1,12 @@
1
1
  from collections.abc import Generator, Sequence
2
+ from contextlib import contextmanager
2
3
  import os
3
4
  import sys
4
5
  import runpy
5
6
  import subprocess
6
7
  import signal
7
8
  import shlex
8
- from .environment import namespace, aliases, xmyshell_alias, xmyshell_unalias
9
+ from .environment import namespace, aliases, xmyshell_alias, xmyshell_unalias, SHELL_CMD
9
10
  from .utils import pywarning, pyerror, getcwd
10
11
  from .meta import HELP_MESSAGE
11
12
 
@@ -14,8 +15,20 @@ def _reload() -> None:
14
15
  from .init import xmyshell_reload
15
16
  xmyshell_reload()
16
17
 
18
+ def _reset_sigint():
19
+ signal.signal(signal.SIGINT, signal.SIG_DFL)
17
20
 
18
- def _run(*args, **kwargs) -> subprocess.CompletedProcess:
21
+ _kwargs_preexec = {}
22
+ if os.name == "posix":
23
+ _kwargs_preexec["preexec_fn"] = _reset_sigint
24
+
25
+ @contextmanager
26
+ def _no_interrupt():
27
+ old = signal.signal(signal.SIGINT, signal.SIG_IGN)
28
+ yield
29
+ signal.signal(signal.SIGINT, old)
30
+
31
+ def _run(cmd_line: list[str] | str, **kwargs) -> subprocess.CompletedProcess:
19
32
  # Block SIGINT in the parent while the child runs, so that a Ctrl-C
20
33
  # delivered to the console/process group is handled solely by the child.
21
34
  #
@@ -26,17 +39,15 @@ def _run(*args, **kwargs) -> subprocess.CompletedProcess:
26
39
  # KeyboardInterrupt handler only fires for Ctrl-C at the prompt.
27
40
  #
28
41
  # Must run on the main thread: signal.signal() is not allowed elsewhere.
29
- kwargs_preexec = {}
30
- if os.name == "posix":
31
- def _reset_sigint():
32
- signal.signal(signal.SIGINT, signal.SIG_DFL)
33
- kwargs_preexec["preexec_fn"] = _reset_sigint
34
- old = signal.signal(signal.SIGINT, signal.SIG_IGN)
35
- try:
36
- return subprocess.run(*args, **kwargs_preexec, **kwargs)
37
- finally:
38
- signal.signal(signal.SIGINT, old)
39
-
42
+ with _no_interrupt():
43
+ if isinstance(cmd_line, list):
44
+ return subprocess.run(cmd_line, **_kwargs_preexec, **kwargs)
45
+ else:
46
+ return subprocess.run(
47
+ namespace.get("SHELL_CMD", SHELL_CMD) + [cmd_line],
48
+ **_kwargs_preexec,
49
+ **kwargs,
50
+ )
40
51
 
41
52
  def xmyshell_source(path: str) -> None:
42
53
  _updated = runpy.run_path(
@@ -176,8 +187,13 @@ def xmyshell_raw_command(cmd_line: str) -> int | None:
176
187
  try:
177
188
  result = eval(expr, dict(os.environ), namespace)
178
189
  print(result)
190
+ namespace["last_error"] = None
179
191
  return 0
180
192
  except Exception as e:
193
+ import traceback
194
+
195
+ namespace["last_error"] = e
196
+ namespace["last_traceback"] = traceback.format_exc()
181
197
  pyerror(f"{type(e).__name__}: {e}")
182
198
  return -1
183
199
 
@@ -245,29 +261,30 @@ def xmyshell(cmd_line: str) -> int:
245
261
  cmd_line, sh_cmd = cmd_line.split("|>", 1)
246
262
 
247
263
  if sh_cmd:
248
- proc = subprocess.Popen(
249
- sh_cmd,
250
- shell=True,
251
- env=os.environ,
252
- stdin=subprocess.PIPE,
253
- stdout=subprocess.PIPE,
254
- text=True,
255
- )
256
- old_stdout = sys.stdout
257
- sys.stdout = proc.stdin
258
- return_code = xmyshell_raw_command(cmd_line)
259
- if return_code is None:
260
- pyerror(f"'{cmd_line}' cannot be used in a pipeline with '|>'")
261
- return -1
262
- assert proc.stdin
263
- proc.stdin.close()
264
- sys.stdout = old_stdout
265
- if target_var:
266
- result, _ = proc.communicate()
267
- namespace[target_var] = result
268
- return proc.returncode
269
- else:
270
- return proc.wait()
264
+ with _no_interrupt():
265
+ proc = subprocess.Popen(
266
+ namespace.get("SHELL_CMD", SHELL_CMD) + [sh_cmd],
267
+ env=os.environ,
268
+ stdin=subprocess.PIPE,
269
+ stdout=subprocess.PIPE,
270
+ text=True,
271
+ **_kwargs_preexec,
272
+ )
273
+ old_stdout = sys.stdout
274
+ sys.stdout = proc.stdin
275
+ return_code = xmyshell_raw_command(cmd_line)
276
+ if return_code is None:
277
+ pyerror(f"'{cmd_line}' cannot be used in a pipeline with '|>'")
278
+ return -1
279
+ assert proc.stdin
280
+ proc.stdin.close()
281
+ sys.stdout = old_stdout
282
+ if target_var:
283
+ result, _ = proc.communicate()
284
+ namespace[target_var] = result
285
+ return proc.returncode
286
+ else:
287
+ return proc.wait()
271
288
 
272
289
  if target_var:
273
290
  from io import StringIO
@@ -337,10 +354,10 @@ def xmyshell(cmd_line: str) -> int:
337
354
  cmd_line = _fix_first_word(cmd_line)
338
355
  if target_var:
339
356
  result = _run(
340
- cmd_line, shell=True, env=os.environ, capture_output=True, text=True
357
+ cmd_line, env=os.environ, capture_output=True, text=True
341
358
  )
342
359
  namespace[target_var] = result.stdout
343
360
  else:
344
- result = _run(cmd_line, shell=True, env=os.environ)
361
+ result = _run(cmd_line, env=os.environ)
345
362
 
346
363
  return result.returncode
@@ -3,7 +3,6 @@ from prompt_toolkit.buffer import Buffer
3
3
  from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent
4
4
  from prompt_toolkit.selection import SelectionState
5
5
  from prompt_toolkit.keys import Keys
6
- from prompt_toolkit.key_binding.bindings.auto_suggest import load_auto_suggest_bindings
7
6
  from dataclasses import dataclass
8
7
 
9
8
  @dataclass
@@ -16,7 +15,7 @@ class BufferInfo:
16
15
  return BufferInfo(buffer.text, buffer.cursor_position)
17
16
 
18
17
 
19
- MAX_HISTORY_ENTRY = 100
18
+ MAX_UNDO_LIST_LEN = 100
20
19
  UNDO_MERGE_TIMEOUT = 0.5
21
20
  bindings = KeyBindings()
22
21
  _auto_editing: bool = False
@@ -35,6 +34,8 @@ def on_text_changed(buffer: Buffer) -> None:
35
34
  _redo_list.clear()
36
35
  if not _should_merge(buffer):
37
36
  _undo_list.append(_last_info)
37
+ while len(_undo_list) > MAX_UNDO_LIST_LEN:
38
+ del _undo_list[0]
38
39
  _last_info = BufferInfo.from_buffer(buffer)
39
40
 
40
41
 
@@ -198,15 +199,12 @@ def _(event: KeyPressEvent) -> None:
198
199
  stack.pop()
199
200
  _auto_editing = False
200
201
  return
201
- if should_completion(buffer) and (
202
- len(buffer.text) > 0
203
- and buffer.cursor_position > 0
204
- and (
205
- buffer.text[buffer.cursor_position - 1] in r"!@#$%^&*:;,./?|\+-=~()[]{}<>_"
206
- or buffer.text[buffer.cursor_position - 1].isspace()
207
- )
208
- or buffer.cursor_position == 0
209
- ):
202
+ pos = buffer.cursor_position
203
+ text = buffer.text
204
+ before_ok = pos == 0 or (pos > 0 and (text[pos - 1].isspace()
205
+ or text[pos - 1] in set(r"!@#$%^&*:;,./?|+-=~()[]{}<>_")))
206
+ not_on_quote = pos >= len(text) or text[pos] != '"'
207
+ if should_completion(buffer) and before_ok and not_on_quote:
210
208
  _auto_editing = True
211
209
  buffer.insert_text('""')
212
210
  buffer.cursor_left()
@@ -357,3 +355,18 @@ def _(event: KeyPressEvent) -> None:
357
355
  _auto_editing = False
358
356
  _last_info = BufferInfo.from_buffer(buffer)
359
357
  _reset_undo_merge()
358
+
359
+ @bindings.add(Keys.Tab)
360
+ def _(event: KeyPressEvent) -> None:
361
+ buffer = event.current_buffer
362
+ if buffer.complete_state:
363
+ if buffer.complete_state.current_completion is None:
364
+ buffer.complete_next()
365
+ if buffer.complete_state.current_completion:
366
+ # Trailing space bypasses prompt_toolkit's `completion_does_nothing`
367
+ # filter (which hides completions identical to the replaced text) and
368
+ # doubles as the shell argument separator. Invisible characters would
369
+ # also bypass the filter, but would corrupt command parsing.
370
+ text = buffer.complete_state.current_completion.text
371
+ buffer.complete_state.current_completion.text = text.rstrip()
372
+ buffer.apply_completion(buffer.complete_state.current_completion)
@@ -14,6 +14,46 @@ def _is_number_literal(s: str) -> bool:
14
14
  except (ValueError, SyntaxError):
15
15
  return False
16
16
 
17
+ def _shell_split_keep_sep(text: str) -> list[str]:
18
+ parts: list[str] = []
19
+ quote: str | None = None
20
+ i = 0
21
+ n = len(text)
22
+ seg_start = 0
23
+
24
+ def flush(end: int) -> None:
25
+ nonlocal seg_start
26
+ if end > seg_start:
27
+ parts.append(text[seg_start:end])
28
+ seg_start = end
29
+
30
+ while i < n:
31
+ ch = text[i]
32
+ if quote is None:
33
+ if ch in " \t":
34
+ flush(i)
35
+ j = i
36
+ while j < n and text[j] in " \t":
37
+ j += 1
38
+ parts.append(text[i:j])
39
+ seg_start = j
40
+ i = j
41
+ continue
42
+ elif ch == "\\" and i + 1 < n:
43
+ i += 2
44
+ continue
45
+ elif ch in ("'", '"'):
46
+ quote = ch
47
+ else:
48
+ if ch == quote:
49
+ quote = None
50
+ elif ch == "\\" and quote == '"' and i + 1 < n:
51
+ i += 2
52
+ continue
53
+ i += 1
54
+ flush(n)
55
+ return parts
56
+
17
57
  def _split_with_braces_and_pipes(line: str) -> list[str]:
18
58
  parts = []
19
59
  outer_buf = []
@@ -86,7 +126,7 @@ def _split_with_braces_and_pipes(line: str) -> list[str]:
86
126
  return parts
87
127
 
88
128
  def _lex_cmd(cmd: str, first_word: bool = True) -> list[tuple[str, str]]:
89
- parts: list[str] = re.split(r"(\s+)", cmd)
129
+ parts: list[str] = _shell_split_keep_sep(cmd)
90
130
  result: list[tuple[str, str]] = []
91
131
  for part in parts:
92
132
  if part == "":
@@ -17,8 +17,9 @@ from .lexer import XmyShellLexer
17
17
  def xmyshell_main() -> None:
18
18
  xmyshell_init()
19
19
 
20
- print(WELCOME_MESSAGE)
21
- print(f"Initialized with {(time_ns() - _start_time) // 1_000_000}ms.")
20
+ if namespace.get("SHOW_WELCOME"):
21
+ print(WELCOME_MESSAGE)
22
+ print(f"Initialized with {(time_ns() - _start_time) // 1_000_000}ms.")
22
23
 
23
24
  session = PromptSession(
24
25
  history=FileHistory(os.path.expanduser("~/.xmyshell/history.txt")),
@@ -59,6 +59,8 @@ Built-in variables:
59
59
  exec_duration - The duration which last command took.
60
60
  HOME_DIR - The home directory, similar to "~".
61
61
  PROFILE - The path of config file.
62
+ SHOW_WELCOME - Toggle whether to show welcome message.
63
+ SHELL_CMD - Shell command line prefix, e.g. ["cmd", "/c"]
62
64
 
63
65
  Keyboard Shortcuts:
64
66
  ↑↓ - Navigate through command history.
@@ -0,0 +1 @@
1
+ # Nothing here
@@ -0,0 +1,17 @@
1
+ # XmyShell built-in names
2
+
3
+ def getlogin() -> str: ...
4
+ def getcwd() -> str: ...
5
+ def dirname(p: str) -> str: ...
6
+ def basename(p: str) -> str: ...
7
+ def cat(file_path: str) -> str: ...
8
+ def alias(altname: str, target: str) -> None: ...
9
+ def unalias(altname: str) -> None: ...
10
+ SHOW_WELCOME: bool
11
+ SHELL_CMD: list[str]
12
+ HOME_DIR: str
13
+ PROFILE: str
14
+ exit_code: int
15
+ exec_duration: int
16
+ last_error: Exception | None
17
+ last_traceback: str | None
File without changes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xmyshell
3
- Version: 0.2.10
3
+ Version: 0.2.12
4
4
  Summary: XmyShell is a shell-like tool built on top of prompt_toolkit which provides a persistent Python runtime.
5
5
  Requires-Python: >=3.12
6
6
  Description-Content-Type: text/markdown
@@ -118,6 +118,15 @@ or
118
118
  ❯ unalias ll
119
119
  ```
120
120
 
121
+ - Lists, tuples, generators, etc. will be expanded into shell arguments.
122
+ E.g. `{["arg1", "arg2 xxx"]}` will be expanded into `arg1 'arg2 xxx'`
123
+
124
+ Here is an example (though there are other ways to do this, it is just an example )
125
+ ```
126
+ ❯ pip install {cat("requirements.txt").splitlines()}
127
+ ```
128
+
129
+
121
130
  - Syntax highlighting via `prompt_toolkit`
122
131
  - Powerful auto-completion, history search, and automatic bracket/quote pairing
123
132
  - Startup configuration: `~/.xmyshell/config.py` loaded at launch
@@ -154,6 +163,12 @@ The exit code of the last command.
154
163
  - `exec_duration`
155
164
  The duration which last command took.
156
165
 
166
+ - `SHOW_WELCOME`
167
+ Toggle whether to show welcome message when staring.
168
+
169
+ - `SHELL_CMD`
170
+ Shell command line prefix, defaults to `["cmd", "/c"]` on Windows and `["bin/sh", "-c"]` on other OS.
171
+
157
172
  - `HOME_DIR`
158
173
  The home directory, similar to `~`.
159
174
 
@@ -170,6 +185,15 @@ The path of config file.
170
185
  - Auto bracket/parenthesis/quote pairing
171
186
 
172
187
 
188
+ ### Stubs
189
+
190
+ You can write your xmyshell script with these imports:
191
+ ```python
192
+ from xmyshell.themes import ... # themes
193
+ from xmyshell.stubs import ... # stubs for built-in names
194
+ ```
195
+
196
+
173
197
  ## Limitations
174
198
 
175
199
  **XmyShell** is Python‑first. This brings power, but also intentional trade‑offs.
@@ -20,4 +20,7 @@ xmyshell.egg-info/dependency_links.txt
20
20
  xmyshell.egg-info/entry_points.txt
21
21
  xmyshell.egg-info/requires.txt
22
22
  xmyshell.egg-info/top_level.txt
23
- xmyshell/data/commands.json
23
+ xmyshell/data/commands.json
24
+ xmyshell/stubs/__init__.py
25
+ xmyshell/stubs/__init__.pyi
26
+ xmyshell/stubs/py.typed
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes