xmyshell 0.1.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.
xmyshell/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ from .main import xmyshell_main
2
+ from .meta import NAME, VERSION, AUTHOR, EMAIL
3
+
4
+ __title__ = NAME
5
+ __version__ = VERSION
6
+ __author__ = AUTHOR
7
+ __author_email__ = EMAIL
xmyshell/__main__.py ADDED
@@ -0,0 +1,12 @@
1
+ import sys
2
+
3
+ def main() -> None:
4
+ if "--version" in sys.argv:
5
+ from .meta import WELCOME_MESSAGE
6
+ print(WELCOME_MESSAGE)
7
+ elif "--help" in sys.argv:
8
+ from .meta import HELP_MESSAGE
9
+ print(HELP_MESSAGE)
10
+ else:
11
+ from .main import xmyshell_main
12
+ xmyshell_main()
xmyshell/completer.py ADDED
@@ -0,0 +1,405 @@
1
+ import os
2
+ import re
3
+ import time
4
+ import json
5
+ import keyword
6
+ import pkgutil
7
+ from string import whitespace
8
+ from prompt_toolkit.completion import Completer, Completion
9
+ from prompt_toolkit.document import Document
10
+ from .environment import namespace
11
+ from .utils import truncate
12
+ from pathlib import Path
13
+
14
+ MAX_DISPLAY_LEN = 24
15
+ MAX_META_LEN = 12
16
+
17
+ BUILTINS = ("cd", "cp", "cat", "echo", "exit", "import", "ls", "print", "pyexec", "pwd", "source",
18
+ "clear", "grep", "find", "from", "mkdir", "rm", "reload", "mv", "help")
19
+
20
+ COMMON = (
21
+ "python", "python3", "pip", "pip3", "git", "conda", "uv",
22
+ "ssh", "scp", "curl", "wget", "winget", "docker",
23
+ "vim", "nvim", "nano", "code",
24
+ "sudo", "apt", "yum", "brew", "chmod", "chown",
25
+ "ps", "top", "htop", "kill", "pkill", "killall",
26
+ "systemctl", "journalctl", "service", "watch", "screen", "tmux", "nohup",
27
+ "ping", "dig", "nslookup", "traceroute", "nc", "route", "arp", "iptables", "ufw",
28
+ "ip", "ifconfig", "netstat", "ss",
29
+ "tar", "gzip", "gunzip", "bzip2", "xz", "zip", "unzip",
30
+ "node", "npm", "npx", "yarn", "pnpm",
31
+ "java", "javac", "mvn", "gradle", "ant",
32
+ "gcc", "g++", "make", "cmake",
33
+ "go", "rustc", "cargo", "gdb",
34
+ "sqlite3", "mysql", "psql", "mongosh", "redis-cli",
35
+ "free", "ffmpeg", "df", "du", "iostat", "vmstat",
36
+ "awk", "sed", "xargs"
37
+ )
38
+
39
+ KEYWORDS = list(keyword.kwlist)
40
+
41
+ COMMAND_TREE = {}
42
+ json_path = Path(__file__).parent / "data" / "commands.json"
43
+ try:
44
+ with open(json_path, "r", encoding="utf-8") as f:
45
+ COMMAND_TREE = json.load(f)
46
+ except (FileNotFoundError, json.JSONDecodeError):
47
+ pass
48
+
49
+ class ShellCompleter(Completer):
50
+
51
+ def __init__(self, cache_duration: int = 3) -> None:
52
+ self._cache: list[str] = []
53
+ self._cache_time: float = 0
54
+ self._cache_duration = cache_duration
55
+ self._get_commands()
56
+
57
+ def _strip_ext(self, name: str) -> str:
58
+ if os.name == "nt":
59
+ ext = os.path.splitext(name)[1].lower()
60
+ if ext in {".exe", ".bat", ".cmd", ".ps1", ".py", ".com"}:
61
+ return os.path.splitext(name)[0]
62
+ return name
63
+
64
+ def _get_commands(self) -> list[str]:
65
+ now = time.time()
66
+ if self._cache and now - self._cache_time < self._cache_duration:
67
+ return self._cache
68
+
69
+ commands: set[str] = set(BUILTINS)
70
+ for path in os.environ.get("PATH", "").split(os.pathsep):
71
+ if not path or not os.path.isdir(path) or path.startswith("/mnt/"):
72
+ continue
73
+ try:
74
+ for name in os.listdir(path):
75
+ if os.name == "nt":
76
+ name = name.lower()
77
+ full = os.path.join(path, name)
78
+ if os.path.isfile(full) and self._is_executable(name, full):
79
+ commands.add(self._strip_ext(name))
80
+ except (OSError, PermissionError):
81
+ continue
82
+
83
+ self._cache = self._sort_commands(commands)
84
+ self._cache_time = now
85
+ return self._cache
86
+
87
+ def _is_executable(self, name: str, full_path: str) -> bool:
88
+ if name.endswith((".dll", ".pyd", ".pyc", ".sys", ".msi", ".drv")):
89
+ return False
90
+
91
+ if os.name == "nt":
92
+ ext = os.path.splitext(name)[1].lower()
93
+ return ext in {".exe", ".bat", ".cmd", ".ps1", ".py", ".com"}
94
+
95
+ return os.access(full_path, os.X_OK) and not name.endswith((".so", ".a", ".o"))
96
+
97
+ def _inside_braces(self, text: str) -> bool:
98
+ count = 0
99
+ i = 0
100
+ while i < len(text):
101
+ ch = text[i]
102
+ if ch == '{' and (i == 0 or text[i-1] != '{'):
103
+ count += 1
104
+ i += 1
105
+ elif ch == '}' and (i == 0 or text[i-1] != '}'):
106
+ count -= 1
107
+ i += 1
108
+ else:
109
+ i += 1
110
+ return count > 0
111
+
112
+ def _sort_commands(self, commands: set[str]) -> list[str]:
113
+ builtins = [c for c in BUILTINS if c in commands]
114
+ common = [c for c in COMMON if c in commands]
115
+ others = sorted(c for c in commands if c not in COMMON and c not in BUILTINS)
116
+ return builtins + common + others
117
+
118
+ def _path_completions(self, text: str, max_results: int = 100):
119
+ if text and text[-1] in whitespace: return
120
+ last = text.split()[-1] if text.split() else ""
121
+ if last == '~': return # don't remove this
122
+ path = os.path.expanduser(last)
123
+ dirname = os.path.dirname(path) or "."
124
+ prefix = os.path.basename(path)
125
+
126
+ try:
127
+ matches = []
128
+ for item in os.listdir(dirname):
129
+ if item.startswith(prefix):
130
+ matches.append(item)
131
+ if len(matches) >= max_results:
132
+ break
133
+
134
+ for item in matches:
135
+ full = os.path.join(dirname, item)
136
+ is_dir = os.path.isdir(full)
137
+ name = item + ("/" if is_dir else "")
138
+ yield Completion(
139
+ name,
140
+ display=truncate(name, MAX_DISPLAY_LEN),
141
+ start_position=-len(prefix),
142
+ display_meta="directory" if is_dir else "file",
143
+ )
144
+ except OSError:
145
+ pass
146
+
147
+ @staticmethod
148
+ def _last_identifier(text: str) -> str:
149
+ i = len(text) - 1
150
+ while i >= 0 and (text[i].isalnum() or text[i] == '_'):
151
+ i -= 1
152
+ start = i + 1
153
+ identifier = text[start:]
154
+ return identifier
155
+
156
+ def _environment_completion(self, text: str):
157
+ last_word = self._last_identifier(text)
158
+ if not last_word:
159
+ return
160
+
161
+ candidates = set(os.environ)
162
+ for name in sorted(candidates):
163
+ if name.startswith(last_word):
164
+ yield Completion(
165
+ name,
166
+ start_position=-len(last_word),
167
+ display=truncate(name, MAX_DISPLAY_LEN),
168
+ display_meta="environment"
169
+ )
170
+
171
+ def _python_completion(self, text: str):
172
+ if text and text[-1].isspace():
173
+ return
174
+ last_token = text.split()[-1] if text.split() else ''
175
+ if not last_token:
176
+ return
177
+
178
+ if '.' in last_token:
179
+ last_dot = last_token.rfind('.')
180
+ after_dot = last_token[last_dot+1:]
181
+ if after_dot == "" or after_dot.isidentifier():
182
+ parts = last_token.split('.')
183
+
184
+ # find the start point of identifiers chain
185
+ start = 0
186
+ for idx in range(len(parts)-2, -1, -1):
187
+ if not parts[idx].isidentifier():
188
+ start = idx
189
+ break
190
+
191
+ obj = None
192
+ root = self._last_identifier(parts[start])
193
+ if root in namespace:
194
+ obj = namespace[root]
195
+ elif hasattr(__builtins__, root):
196
+ obj = getattr(__builtins__, root)
197
+ elif isinstance(__builtins__, dict) and root in __builtins__:
198
+ obj = __builtins__[root]
199
+
200
+ if obj is None:
201
+ return
202
+
203
+ for part in parts[start+1:-1]:
204
+ try:
205
+ obj = getattr(obj, part)
206
+ except AttributeError:
207
+ return
208
+
209
+ prefix = parts[-1]
210
+ attrs = dir(obj)
211
+ attrs.sort(key=lambda x: (x.startswith('_'), x))
212
+ for attr in attrs:
213
+ if attr.startswith(prefix):
214
+ try:
215
+ value = getattr(obj, attr)
216
+ meta = (
217
+ "method"
218
+ if callable(value)
219
+ else truncate(type(value).__name__, MAX_META_LEN)
220
+ )
221
+ except Exception:
222
+ meta = "attr"
223
+ yield Completion(
224
+ attr,
225
+ start_position=-len(prefix),
226
+ display=attr,
227
+ display_meta=meta,
228
+ )
229
+ return
230
+
231
+ last_word = self._last_identifier(last_token)
232
+ if not last_word:
233
+ return
234
+
235
+ builtins_candidates = []
236
+ if isinstance(__builtins__, dict):
237
+ builtins_candidates = list(__builtins__.keys())
238
+ elif hasattr(__builtins__, '__dict__'):
239
+ builtins_candidates = dir(__builtins__)
240
+ else:
241
+ builtins_candidates = []
242
+
243
+ candidates = set(namespace.keys()) | set(builtins_candidates)
244
+ for name in sorted(candidates) + KEYWORDS:
245
+ if name in namespace:
246
+ meta = truncate(type(namespace[name]).__name__, MAX_META_LEN)
247
+ elif name in __builtins__:
248
+ meta = truncate(type(__builtins__[name]).__name__, MAX_META_LEN)
249
+ else:
250
+ meta = "python"
251
+ if name.startswith(last_word):
252
+ yield Completion(
253
+ name,
254
+ start_position=-len(last_word),
255
+ display=name,
256
+ display_meta=meta
257
+ )
258
+ yield from self._environment_completion(text)
259
+
260
+ def _subcommand_completion(self, text: str):
261
+ parts = text.split()
262
+ if not parts: return
263
+ sub_cmd = COMMAND_TREE.get(parts[0])
264
+ if not sub_cmd: return
265
+ global_flags: set[str] = set()
266
+ if sub_cmd.get("_global"):
267
+ global_flags.update(sub_cmd["_global"])
268
+ if not text.endswith(parts[-1]):
269
+ parts.append("")
270
+ last_word = parts[-1]
271
+
272
+ for part in parts[1:-1]:
273
+ if part.startswith("-"):
274
+ continue
275
+ if sub_cmd.get("_global"):
276
+ global_flags.update(sub_cmd["_global"])
277
+ if sub_cmd.get(part):
278
+ sub_cmd = sub_cmd[part]
279
+ else:
280
+ sub_cmd = {}
281
+ break
282
+
283
+ for name in sorted(sub_cmd.keys()):
284
+ if name == "_global" or name == "_flags": continue
285
+ if name in parts: continue
286
+ if name.startswith(last_word):
287
+ yield Completion(
288
+ name,
289
+ start_position=-len(last_word),
290
+ display=truncate(name, MAX_DISPLAY_LEN),
291
+ display_meta="subcommand"
292
+ )
293
+
294
+ for name in sorted(sub_cmd.get("_flags") or set()):
295
+ if name in parts: continue
296
+ if name.startswith(last_word):
297
+ yield Completion(
298
+ name,
299
+ start_position=-len(last_word),
300
+ display=truncate(name, MAX_DISPLAY_LEN),
301
+ display_meta="flag",
302
+ )
303
+
304
+ for name in sorted(global_flags):
305
+ if name in parts: continue
306
+ if name.startswith(last_word):
307
+ yield Completion(
308
+ name,
309
+ start_position=-len(last_word),
310
+ display=truncate(name, MAX_DISPLAY_LEN),
311
+ display_meta="flag",
312
+ )
313
+
314
+ def get_completions(self, document: Document, complete_event):
315
+ text = document.text_before_cursor
316
+ if "=>" in text: return
317
+ segments = re.split(r'\|>|\|', text)
318
+ current_segment = segments[-1] if segments else text
319
+ lstripped = current_segment.lstrip()
320
+ if lstripped.startswith("sudo"):
321
+ lstripped = lstripped[4:].lstrip()
322
+
323
+ if self._inside_braces(lstripped):
324
+ yield from self._python_completion(lstripped)
325
+ return
326
+
327
+ last_token = lstripped.split()[-1] if lstripped.split() else ''
328
+ if '.' in last_token and not last_token.startswith('.'):
329
+ yield from self._python_completion(lstripped)
330
+ return
331
+
332
+ if " " not in lstripped:
333
+ if len(lstripped) < 1:
334
+ return
335
+ for cmd in self._get_commands():
336
+ if cmd.startswith(lstripped):
337
+ meta = "command" if cmd in BUILTINS else "program"
338
+ yield Completion(
339
+ cmd,
340
+ start_position=-len(lstripped),
341
+ display=truncate(cmd, MAX_DISPLAY_LEN),
342
+ display_meta=meta,
343
+ )
344
+ return
345
+
346
+ # special built-ins
347
+ parts = lstripped.split()
348
+ if len(parts) >= 1:
349
+ cmd = parts[0]
350
+ if cmd in ("print", "pyexec"):
351
+ param_text = lstripped[len(cmd):].lstrip()
352
+ yield from self._python_completion(param_text)
353
+ return
354
+ if cmd == "from":
355
+ if not lstripped.endswith(parts[-1]):
356
+ parts.append("")
357
+ elif len(parts) == 2:
358
+ for module in pkgutil.iter_modules():
359
+ name: str = module.name
360
+ if not name.startswith(parts[-1]):
361
+ continue
362
+ if name.startswith("_"):
363
+ continue
364
+ yield Completion(
365
+ name,
366
+ start_position=-len(parts[-1]),
367
+ display=truncate(name, MAX_DISPLAY_LEN),
368
+ display_meta="module",
369
+ )
370
+ elif len(parts) == 3:
371
+ yield Completion(
372
+ "import",
373
+ start_position=-len(parts[-1]),
374
+ display_meta="keyword",
375
+ )
376
+ return
377
+ if cmd == "import":
378
+ if not lstripped.endswith(parts[-1]):
379
+ parts.append("")
380
+ elif len(parts) == 2:
381
+ for module in pkgutil.iter_modules():
382
+ name: str = module.name
383
+ if not name.startswith(parts[-1]):
384
+ continue
385
+ if name.startswith("_"):
386
+ continue
387
+ yield Completion(
388
+ name,
389
+ start_position=-len(parts[-1]),
390
+ display=truncate(name, MAX_DISPLAY_LEN),
391
+ display_meta="module",
392
+ )
393
+ elif len(parts) == 3:
394
+ yield Completion(
395
+ "as",
396
+ start_position=-len(parts[-1]),
397
+ display_meta="keyword",
398
+ )
399
+ return
400
+
401
+ yield from self._subcommand_completion(lstripped)
402
+ yield from self._path_completions(lstripped)
403
+
404
+ def completer_init() -> None:
405
+ namespace["shell_completer"] = ShellCompleter()