NeoShell 2.3__py3-none-any.whl → 2.5__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.
- {neoshell-2.3.dist-info → neoshell-2.5.dist-info}/METADATA +1 -1
- neoshell-2.5.dist-info/RECORD +6 -0
- neoshell.py +450 -24
- neoshell-2.3.dist-info/RECORD +0 -6
- {neoshell-2.3.dist-info → neoshell-2.5.dist-info}/WHEEL +0 -0
- {neoshell-2.3.dist-info → neoshell-2.5.dist-info}/entry_points.txt +0 -0
- {neoshell-2.3.dist-info → neoshell-2.5.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
neoshell.py,sha256=suLwuAFBxGidY9VQA3sxOXgJ3S83sIO31uEPjkiL2qo,65934
|
|
2
|
+
neoshell-2.5.dist-info/METADATA,sha256=5jnaPveto-bNDUTPFrCVIoITEaZCyNu3Meb4lMjBAxU,315
|
|
3
|
+
neoshell-2.5.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
4
|
+
neoshell-2.5.dist-info/entry_points.txt,sha256=T5sqOBER4jIRiYq3oW9znixzukrsn_E4okrnfjQgEIk,43
|
|
5
|
+
neoshell-2.5.dist-info/top_level.txt,sha256=CwkcL_wEg3YGVVQJDYOj5NrXFMekQwqGvTtkY6_chI4,9
|
|
6
|
+
neoshell-2.5.dist-info/RECORD,,
|
neoshell.py
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
python neoshell.py -c "dir /b & ver" run one line and exit
|
|
7
7
|
neoshell --install / --uninstall add / remove the Windows Terminal profile
|
|
8
8
|
neoshell --no-install start without auto-registering the profile
|
|
9
|
+
neoshell --setup run the setup wizard (also: type setup in the shell)
|
|
10
|
+
neoshell --no-wizard skip the first-run wizard
|
|
9
11
|
|
|
10
12
|
When built into neoshell.exe (see build_exe.bat) it registers its own Windows
|
|
11
13
|
Terminal profile, with icon and color scheme, the first time it runs.
|
|
@@ -16,6 +18,7 @@ Uses CMD command names and syntax (dir, copy, del, tasklist, findstr, %VAR%,
|
|
|
16
18
|
"""
|
|
17
19
|
import ast
|
|
18
20
|
import contextlib
|
|
21
|
+
import copy
|
|
19
22
|
import datetime
|
|
20
23
|
import fnmatch
|
|
21
24
|
import getpass
|
|
@@ -52,7 +55,7 @@ try:
|
|
|
52
55
|
except ImportError:
|
|
53
56
|
psutil = None
|
|
54
57
|
|
|
55
|
-
VERSION = "2.
|
|
58
|
+
VERSION = "2.3"
|
|
56
59
|
HIST = os.path.expanduser("~/.neoshell_history")
|
|
57
60
|
aliases = {} # doskey macros
|
|
58
61
|
dir_stack = [] # pushd / popd
|
|
@@ -62,6 +65,7 @@ old_dir = None
|
|
|
62
65
|
session = None
|
|
63
66
|
_which_cache = {}
|
|
64
67
|
_prog_cache = None
|
|
68
|
+
CONFIG = {} # user settings, loaded from config.json
|
|
65
69
|
|
|
66
70
|
WIN_INTERNAL = {"assoc", "attrib", "chcp", "cmd", "ftype", "label", "mklink", "vol",
|
|
67
71
|
"verify", "for", "call", "ipconfig", "ping", "netstat", "systeminfo"}
|
|
@@ -223,6 +227,85 @@ def calc(expr):
|
|
|
223
227
|
return ev(ast.parse(expr.strip(), mode="eval"))
|
|
224
228
|
|
|
225
229
|
|
|
230
|
+
# ---------------------------------------------------------------- settings & themes
|
|
231
|
+
DEFAULTS = {
|
|
232
|
+
"name": "", "theme": "neo", "prompt": "full", "git_branch": True, "clock": True,
|
|
233
|
+
"banner": True, "start_dir": "smart", "complete_while_typing": False,
|
|
234
|
+
"manage_profile": True, "startup": [], "macros": {}, "bookmarks": {},
|
|
235
|
+
"wizard_done": False,
|
|
236
|
+
}
|
|
237
|
+
SETTABLE = ("name", "theme", "prompt", "git_branch", "clock", "banner", "start_dir",
|
|
238
|
+
"complete_while_typing", "manage_profile")
|
|
239
|
+
PROMPTS = ("minimal", "full", "twoline")
|
|
240
|
+
THEMES = {
|
|
241
|
+
"neo": dict(c1=(77, 225, 255), c2=(199, 146, 255), path="fg:ansiblue bold", accent="fg:ansicyan"),
|
|
242
|
+
"sunset": dict(c1=(255, 170, 80), c2=(255, 70, 140), path="fg:ansiyellow bold", accent="fg:ansired"),
|
|
243
|
+
"matrix": dict(c1=(120, 255, 160), c2=(0, 170, 70), path="fg:ansigreen bold", accent="fg:ansigreen"),
|
|
244
|
+
"ocean": dict(c1=(90, 170, 255), c2=(70, 235, 210), path="fg:ansicyan bold", accent="fg:ansiblue"),
|
|
245
|
+
"mono": dict(c1=(235, 235, 235), c2=(120, 120, 120), path="bold", accent="fg:ansibrightblack"),
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def config_path():
|
|
250
|
+
if IS_WIN:
|
|
251
|
+
base = os.environ.get("APPDATA") or os.path.expanduser("~")
|
|
252
|
+
else:
|
|
253
|
+
base = os.environ.get("XDG_CONFIG_HOME") or os.path.expanduser("~/.config")
|
|
254
|
+
return os.path.join(base, "neoshell", "config.json")
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def load_config():
|
|
258
|
+
"""Fill CONFIG from disk (falling back to defaults). Returns True if a file existed."""
|
|
259
|
+
CONFIG.clear()
|
|
260
|
+
CONFIG.update(copy.deepcopy(DEFAULTS))
|
|
261
|
+
existed = os.path.isfile(config_path())
|
|
262
|
+
try:
|
|
263
|
+
with open(config_path(), encoding="utf-8") as f:
|
|
264
|
+
data = json.load(f)
|
|
265
|
+
for k, v in data.items():
|
|
266
|
+
if k in DEFAULTS and type(v) is type(DEFAULTS[k]):
|
|
267
|
+
CONFIG[k] = v
|
|
268
|
+
except (OSError, ValueError):
|
|
269
|
+
pass
|
|
270
|
+
if CONFIG["theme"] not in THEMES:
|
|
271
|
+
CONFIG["theme"] = "neo"
|
|
272
|
+
if CONFIG["prompt"] not in PROMPTS:
|
|
273
|
+
CONFIG["prompt"] = "full"
|
|
274
|
+
return existed
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def save_config():
|
|
278
|
+
path = config_path()
|
|
279
|
+
try:
|
|
280
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
281
|
+
tmp = path + ".tmp"
|
|
282
|
+
with open(tmp, "w", encoding="utf-8") as f:
|
|
283
|
+
json.dump(CONFIG, f, indent=2)
|
|
284
|
+
os.replace(tmp, path)
|
|
285
|
+
return True
|
|
286
|
+
except OSError as e:
|
|
287
|
+
print(paint(f"Could not save settings: {e}", "31"))
|
|
288
|
+
return False
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def gradient_text(text, c1, c2):
|
|
292
|
+
if not sys.stdout.isatty():
|
|
293
|
+
return text
|
|
294
|
+
n, out = max(1, len(text) - 1), []
|
|
295
|
+
for i, ch in enumerate(text):
|
|
296
|
+
t = i / n
|
|
297
|
+
r, g, b = (int(c1[k] + (c2[k] - c1[k]) * t) for k in range(3))
|
|
298
|
+
out.append(f"\033[38;2;{r};{g};{b}m{ch}")
|
|
299
|
+
return "".join(out) + "\033[0m"
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _parse_bool(v):
|
|
303
|
+
v = v.lower()
|
|
304
|
+
if v in ("on", "true", "yes", "y", "1"): return True
|
|
305
|
+
if v in ("off", "false", "no", "n", "0"): return False
|
|
306
|
+
raise ValueError(v)
|
|
307
|
+
|
|
308
|
+
|
|
226
309
|
# ---------------------------------------------------------------- builtins
|
|
227
310
|
BUILTINS, HELP = {}, {}
|
|
228
311
|
|
|
@@ -243,6 +326,11 @@ def b_cd(a):
|
|
|
243
326
|
if not rest:
|
|
244
327
|
print(os.getcwd()); return 0
|
|
245
328
|
target = " ".join(rest)
|
|
329
|
+
if target.startswith("@"):
|
|
330
|
+
bm = CONFIG.get("bookmarks", {}).get(target[1:].lower())
|
|
331
|
+
if not bm:
|
|
332
|
+
print(f"No bookmark named {target[1:]}. See: bm"); return 1
|
|
333
|
+
target = bm
|
|
246
334
|
if target == "-" and old_dir:
|
|
247
335
|
target = old_dir
|
|
248
336
|
here = os.getcwd()
|
|
@@ -622,15 +710,18 @@ def b_path(a):
|
|
|
622
710
|
return 0
|
|
623
711
|
|
|
624
712
|
|
|
625
|
-
@builtin("doskey", doc="Define macros. doskey ll=dir /w doskey /macros")
|
|
713
|
+
@builtin("doskey", doc="Define macros (saved). doskey ll=dir /w doskey /macros")
|
|
626
714
|
def b_doskey(a):
|
|
627
715
|
text = " ".join(a)
|
|
628
716
|
if not text or text.lower() == "/macros":
|
|
629
|
-
for k, v in sorted(aliases.items()):
|
|
717
|
+
for k, v in sorted(aliases.items()):
|
|
718
|
+
print(f"{k}={v}")
|
|
630
719
|
elif "=" in text:
|
|
631
720
|
k, v = text.split("=", 1)
|
|
632
721
|
if v.strip(): aliases[k.strip().lower()] = v.strip()
|
|
633
722
|
else: aliases.pop(k.strip().lower(), None)
|
|
723
|
+
CONFIG["macros"] = dict(aliases)
|
|
724
|
+
save_config()
|
|
634
725
|
return 0
|
|
635
726
|
|
|
636
727
|
|
|
@@ -863,6 +954,148 @@ def b_pause(a):
|
|
|
863
954
|
return 0
|
|
864
955
|
|
|
865
956
|
|
|
957
|
+
@builtin("config", doc="Settings: config | config set <key> <value> | config startup add|rm|clear | config reset | config edit")
|
|
958
|
+
def b_config(a):
|
|
959
|
+
sub = a[0].lower() if a else "show"
|
|
960
|
+
if sub == "show":
|
|
961
|
+
print(f"\n neoshell settings {config_path()}\n")
|
|
962
|
+
for k in SETTABLE:
|
|
963
|
+
print(f" {k:<22} {CONFIG[k]}")
|
|
964
|
+
print(f" {'startup commands':<22} {len(CONFIG['startup'])}")
|
|
965
|
+
for n, c in enumerate(CONFIG["startup"], 1):
|
|
966
|
+
print(f" {n}. {c}")
|
|
967
|
+
print(f" {'macros / bookmarks':<22} {len(CONFIG['macros'])} / {len(CONFIG['bookmarks'])}")
|
|
968
|
+
print("\n Change one: config set <key> <value>")
|
|
969
|
+
print(" theme: " + ", ".join(THEMES) + " prompt: " + ", ".join(PROMPTS))
|
|
970
|
+
print(" start_dir: smart, home, launch, or a folder path\n")
|
|
971
|
+
return 0
|
|
972
|
+
if sub == "path":
|
|
973
|
+
print(config_path()); return 0
|
|
974
|
+
if sub == "edit":
|
|
975
|
+
save_config()
|
|
976
|
+
try:
|
|
977
|
+
if IS_WIN:
|
|
978
|
+
subprocess.Popen(["notepad", config_path()]); return 0
|
|
979
|
+
return b_start([config_path()])
|
|
980
|
+
except OSError as e:
|
|
981
|
+
print(f"Cannot open an editor: {e}"); return 1
|
|
982
|
+
if sub == "reset":
|
|
983
|
+
if input("Reset all settings to defaults? Macros and bookmarks are kept. (Y/N)? ").strip().lower() != "y":
|
|
984
|
+
return 0
|
|
985
|
+
for k in SETTABLE:
|
|
986
|
+
CONFIG[k] = copy.deepcopy(DEFAULTS[k])
|
|
987
|
+
CONFIG["startup"] = []
|
|
988
|
+
save_config(); print("Settings reset."); return 0
|
|
989
|
+
if sub == "startup":
|
|
990
|
+
act = a[1].lower() if len(a) > 1 else "list"
|
|
991
|
+
if act == "add" and len(a) > 2:
|
|
992
|
+
CONFIG["startup"].append(" ".join(a[2:])); save_config()
|
|
993
|
+
elif act in ("rm", "remove") and len(a) > 2 and a[2].isdigit() and 1 <= int(a[2]) <= len(CONFIG["startup"]):
|
|
994
|
+
CONFIG["startup"].pop(int(a[2]) - 1); save_config()
|
|
995
|
+
elif act == "clear":
|
|
996
|
+
CONFIG["startup"] = []; save_config()
|
|
997
|
+
elif act != "list":
|
|
998
|
+
print('usage: config startup add "<command>" | rm <number> | clear'); return 1
|
|
999
|
+
for n, c in enumerate(CONFIG["startup"], 1):
|
|
1000
|
+
print(f" {n}. {c}")
|
|
1001
|
+
if not CONFIG["startup"]:
|
|
1002
|
+
print(" (no startup commands)")
|
|
1003
|
+
return 0
|
|
1004
|
+
if sub == "set":
|
|
1005
|
+
if len(a) < 3 or a[1].lower() not in SETTABLE:
|
|
1006
|
+
print("usage: config set <key> <value>\nkeys: " + ", ".join(SETTABLE)); return 1
|
|
1007
|
+
key, val = a[1].lower(), " ".join(a[2:])
|
|
1008
|
+
try:
|
|
1009
|
+
if key == "theme":
|
|
1010
|
+
if val.lower() not in THEMES: raise ValueError
|
|
1011
|
+
new = val.lower()
|
|
1012
|
+
elif key == "prompt":
|
|
1013
|
+
if val.lower() not in PROMPTS: raise ValueError
|
|
1014
|
+
new = val.lower()
|
|
1015
|
+
elif key == "start_dir":
|
|
1016
|
+
if val.lower() in ("smart", "home", "launch"):
|
|
1017
|
+
new = val.lower()
|
|
1018
|
+
else:
|
|
1019
|
+
folder = os.path.expandvars(os.path.expanduser(val.strip('"')))
|
|
1020
|
+
if not os.path.isdir(folder): raise ValueError
|
|
1021
|
+
new = os.path.abspath(folder)
|
|
1022
|
+
elif isinstance(DEFAULTS[key], bool):
|
|
1023
|
+
new = _parse_bool(val)
|
|
1024
|
+
else:
|
|
1025
|
+
new = val
|
|
1026
|
+
except ValueError:
|
|
1027
|
+
print(f"Invalid value for {key}. See: config"); return 1
|
|
1028
|
+
CONFIG[key] = new
|
|
1029
|
+
save_config()
|
|
1030
|
+
print(f"{key} = {new}")
|
|
1031
|
+
if key == "complete_while_typing":
|
|
1032
|
+
print("(takes effect the next time neoshell starts)")
|
|
1033
|
+
return 0
|
|
1034
|
+
print("usage: config [show | set | startup | reset | edit | path]"); return 1
|
|
1035
|
+
|
|
1036
|
+
|
|
1037
|
+
@builtin("theme", doc="Show or switch the color theme: theme | theme <name>")
|
|
1038
|
+
def b_theme(a):
|
|
1039
|
+
if a:
|
|
1040
|
+
name = a[0].lower()
|
|
1041
|
+
if name not in THEMES:
|
|
1042
|
+
print("Unknown theme. Choose from: " + ", ".join(THEMES)); return 1
|
|
1043
|
+
CONFIG["theme"] = name
|
|
1044
|
+
save_config()
|
|
1045
|
+
for n, th in THEMES.items():
|
|
1046
|
+
mark = "*" if n == CONFIG["theme"] else " "
|
|
1047
|
+
print(f" {mark} {gradient_text('neoshell', th['c1'], th['c2'])} {n}")
|
|
1048
|
+
return 0
|
|
1049
|
+
|
|
1050
|
+
|
|
1051
|
+
@builtin("bm", doc="Folder bookmarks: bm | bm add <name> [path] | bm rm <name> | bm <name> (jump) also: cd @name")
|
|
1052
|
+
def b_bm(a):
|
|
1053
|
+
bms = CONFIG["bookmarks"]
|
|
1054
|
+
if not a:
|
|
1055
|
+
if not bms:
|
|
1056
|
+
print("No bookmarks yet. Try: bm add work")
|
|
1057
|
+
for k, v in sorted(bms.items()):
|
|
1058
|
+
print(f" @{k:<14} {v}")
|
|
1059
|
+
return 0
|
|
1060
|
+
sub = a[0].lower()
|
|
1061
|
+
if sub == "add" and len(a) >= 2:
|
|
1062
|
+
path = os.path.abspath(" ".join(a[2:])) if len(a) > 2 else os.getcwd()
|
|
1063
|
+
if not os.path.isdir(path):
|
|
1064
|
+
print("That folder doesn't exist."); return 1
|
|
1065
|
+
bms[a[1].lower()] = path
|
|
1066
|
+
save_config()
|
|
1067
|
+
print(f"@{a[1].lower()} -> {path}"); return 0
|
|
1068
|
+
if sub in ("rm", "del", "remove") and len(a) >= 2:
|
|
1069
|
+
if bms.pop(a[1].lower(), None) is None:
|
|
1070
|
+
print("No such bookmark."); return 1
|
|
1071
|
+
save_config(); return 0
|
|
1072
|
+
return b_cd(["@" + sub])
|
|
1073
|
+
|
|
1074
|
+
|
|
1075
|
+
@builtin("about", doc="Logo and a summary of this machine")
|
|
1076
|
+
def b_about(a):
|
|
1077
|
+
banner(force=True)
|
|
1078
|
+
rows = [("user", f"{getpass.getuser()}@{socket.gethostname()}"),
|
|
1079
|
+
("os", f"{platform.system()} {platform.release()}"),
|
|
1080
|
+
("shell", f"neoshell {VERSION} (Python {platform.python_version()})"),
|
|
1081
|
+
("theme", CONFIG["theme"]), ("folder", os.getcwd())]
|
|
1082
|
+
if psutil:
|
|
1083
|
+
up = int(time.time() - psutil.boot_time())
|
|
1084
|
+
vm = psutil.virtual_memory()
|
|
1085
|
+
rows += [("uptime", f"{up // 3600}h {up % 3600 // 60}m"),
|
|
1086
|
+
("cpu", f"{psutil.cpu_percent(interval=0.2)}% ({psutil.cpu_count()} cores)"),
|
|
1087
|
+
("memory", f"{human(vm.used)} / {human(vm.total)}")]
|
|
1088
|
+
for k, v in rows:
|
|
1089
|
+
print(f" {paint(k.ljust(8), '1;36')} {v}")
|
|
1090
|
+
print()
|
|
1091
|
+
return 0
|
|
1092
|
+
|
|
1093
|
+
|
|
1094
|
+
@builtin("setup", doc="Run the setup wizard again")
|
|
1095
|
+
def b_setup(a):
|
|
1096
|
+
run_wizard(); return 0
|
|
1097
|
+
|
|
1098
|
+
|
|
866
1099
|
@builtin("help", doc="List commands, or help <command>")
|
|
867
1100
|
def b_help(a):
|
|
868
1101
|
if a:
|
|
@@ -1257,6 +1490,7 @@ def uninstall_profile():
|
|
|
1257
1490
|
shutil.rmtree(frag_dir, ignore_errors=True)
|
|
1258
1491
|
shutil.rmtree(inst, ignore_errors=True)
|
|
1259
1492
|
_remove_legacy()
|
|
1493
|
+
user_path_edit(inst, False)
|
|
1260
1494
|
print("Profile removed. Restart Windows Terminal." +
|
|
1261
1495
|
(f"\nIf {inst} is still there, close neoshell and delete it." if os.path.isdir(inst) else ""))
|
|
1262
1496
|
return True
|
|
@@ -1305,12 +1539,19 @@ def start_in_home():
|
|
|
1305
1539
|
|
|
1306
1540
|
# ---------------------------------------------------------------- main loop
|
|
1307
1541
|
def prompt_text():
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
if
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1542
|
+
th = THEMES[CONFIG["theme"]]
|
|
1543
|
+
cwd = os.getcwd()
|
|
1544
|
+
branch = git_branch() if CONFIG["git_branch"] else ""
|
|
1545
|
+
arrow = ("fg:ansigreen bold" if last_status == 0 else "fg:ansired bold", " > ")
|
|
1546
|
+
git = [("fg:ansimagenta", f" ({branch})")] if branch else []
|
|
1547
|
+
if CONFIG["prompt"] == "minimal":
|
|
1548
|
+
folder = os.path.basename(cwd.rstrip("\\/")) or cwd
|
|
1549
|
+
return [(th["path"], folder)] + git + [arrow]
|
|
1550
|
+
if CONFIG["prompt"] == "twoline":
|
|
1551
|
+
who = f"{getpass.getuser()}@{socket.gethostname()}"
|
|
1552
|
+
return ([(th["accent"], "┌ "), ("fg:ansibrightblack", who + " "), (th["path"], cwd)] + git +
|
|
1553
|
+
[("", "\n"), (th["accent"], "└"), arrow])
|
|
1554
|
+
return [(th["path"], cwd)] + git + [arrow]
|
|
1314
1555
|
|
|
1315
1556
|
|
|
1316
1557
|
def rprompt_text():
|
|
@@ -1319,7 +1560,8 @@ def rprompt_text():
|
|
|
1319
1560
|
bits.append(("fg:ansired", f"x{last_status} "))
|
|
1320
1561
|
if last_dur >= 1:
|
|
1321
1562
|
bits.append(("fg:ansibrightblack", f"{last_dur:.1f}s "))
|
|
1322
|
-
|
|
1563
|
+
if CONFIG["clock"]:
|
|
1564
|
+
bits.append(("fg:ansibrightblack", time.strftime("%H:%M")))
|
|
1323
1565
|
return bits
|
|
1324
1566
|
|
|
1325
1567
|
|
|
@@ -1328,30 +1570,201 @@ LOGO = ["┏┓╻┏━╸┏━┓┏━┓╻ ╻┏━╸╻ ╻ ",
|
|
|
1328
1570
|
"╹ ╹┗━╸┗━┛┗━┛╹ ╹┗━╸┗━╸┗━╸"]
|
|
1329
1571
|
|
|
1330
1572
|
|
|
1331
|
-
def banner():
|
|
1332
|
-
"""Gradient
|
|
1573
|
+
def banner(force=False):
|
|
1574
|
+
"""Gradient logo in the current theme's colors."""
|
|
1575
|
+
if not force and not CONFIG.get("banner", True):
|
|
1576
|
+
return
|
|
1333
1577
|
if not sys.stdout.isatty():
|
|
1334
1578
|
print(f"neoshell {VERSION}"); return
|
|
1335
|
-
|
|
1336
|
-
width = max(len(r) for r in LOGO)
|
|
1579
|
+
th = THEMES[CONFIG.get("theme", "neo")]
|
|
1337
1580
|
print()
|
|
1338
1581
|
for row in LOGO:
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1582
|
+
print(" " + gradient_text(row, th["c1"], th["c2"]))
|
|
1583
|
+
who = f"hey {CONFIG['name']} · " if CONFIG.get("name") else ""
|
|
1584
|
+
print(f" \033[90m{who}CMD-style shell · v{VERSION} · type \033[0m\033[1mhelp\033[0m\033[90m for commands\033[0m\n")
|
|
1585
|
+
|
|
1586
|
+
|
|
1587
|
+
def apply_start_dir():
|
|
1588
|
+
mode, home = CONFIG["start_dir"], os.path.expanduser("~")
|
|
1589
|
+
if mode == "home" and os.path.isdir(home):
|
|
1590
|
+
os.chdir(home)
|
|
1591
|
+
elif mode == "launch":
|
|
1592
|
+
pass
|
|
1593
|
+
elif mode == "smart":
|
|
1594
|
+
start_in_home()
|
|
1595
|
+
else:
|
|
1596
|
+
folder = os.path.expandvars(os.path.expanduser(mode))
|
|
1597
|
+
if os.path.isdir(folder): os.chdir(folder)
|
|
1598
|
+
else: start_in_home()
|
|
1599
|
+
|
|
1600
|
+
|
|
1601
|
+
# ---------------------------------------------------------------- Windows PATH (optional)
|
|
1602
|
+
def _broadcast_env_change():
|
|
1603
|
+
try:
|
|
1604
|
+
import ctypes
|
|
1605
|
+
ctypes.windll.user32.SendMessageTimeoutW(0xFFFF, 0x1A, 0, "Environment", 2, 5000,
|
|
1606
|
+
ctypes.byref(ctypes.c_ulong()))
|
|
1607
|
+
except Exception:
|
|
1608
|
+
pass
|
|
1346
1609
|
|
|
1347
1610
|
|
|
1611
|
+
def user_path_edit(directory, add):
|
|
1612
|
+
"""Add/remove a folder in the *user* PATH (no admin needed). Windows only."""
|
|
1613
|
+
if not IS_WIN:
|
|
1614
|
+
return False
|
|
1615
|
+
try:
|
|
1616
|
+
import winreg
|
|
1617
|
+
norm = lambda x: os.path.normcase(os.path.normpath(os.path.expandvars(x)))
|
|
1618
|
+
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Environment", 0,
|
|
1619
|
+
winreg.KEY_READ | winreg.KEY_WRITE) as key:
|
|
1620
|
+
try:
|
|
1621
|
+
value, kind = winreg.QueryValueEx(key, "Path")
|
|
1622
|
+
except FileNotFoundError:
|
|
1623
|
+
value, kind = "", winreg.REG_EXPAND_SZ
|
|
1624
|
+
parts = [x for x in value.split(";") if x]
|
|
1625
|
+
present = any(norm(x) == norm(directory) for x in parts)
|
|
1626
|
+
if add and not present:
|
|
1627
|
+
parts.append(directory)
|
|
1628
|
+
elif not add and present:
|
|
1629
|
+
parts = [x for x in parts if norm(x) != norm(directory)]
|
|
1630
|
+
else:
|
|
1631
|
+
return True
|
|
1632
|
+
winreg.SetValueEx(key, "Path", 0, kind, ";".join(parts))
|
|
1633
|
+
_broadcast_env_change()
|
|
1634
|
+
return True
|
|
1635
|
+
except OSError:
|
|
1636
|
+
return False
|
|
1637
|
+
|
|
1638
|
+
|
|
1639
|
+
# ---------------------------------------------------------------- setup wizard
|
|
1640
|
+
def ask(prompt, default=""):
|
|
1641
|
+
try:
|
|
1642
|
+
ans = input(prompt).strip()
|
|
1643
|
+
except EOFError:
|
|
1644
|
+
raise KeyboardInterrupt
|
|
1645
|
+
return ans or default
|
|
1646
|
+
|
|
1647
|
+
|
|
1648
|
+
def ask_yes_no(question, default=True):
|
|
1649
|
+
hint = "[Y/n]" if default else "[y/N]"
|
|
1650
|
+
while True:
|
|
1651
|
+
a = ask(f" {question} {hint} ").lower()
|
|
1652
|
+
if not a: return default
|
|
1653
|
+
if a in ("y", "yes"): return True
|
|
1654
|
+
if a in ("n", "no"): return False
|
|
1655
|
+
print(" Please answer y or n.")
|
|
1656
|
+
|
|
1657
|
+
|
|
1658
|
+
def ask_choice(options, default=1):
|
|
1659
|
+
for i, label in enumerate(options, 1):
|
|
1660
|
+
print(f" {i}) {label}" + (" (default)" if i == default else ""))
|
|
1661
|
+
while True:
|
|
1662
|
+
a = ask(f" Choose 1-{len(options)} [{default}]: ")
|
|
1663
|
+
if not a: return default
|
|
1664
|
+
if a.isdigit() and 1 <= int(a) <= len(options): return int(a)
|
|
1665
|
+
print(f" Please type a number from 1 to {len(options)}.")
|
|
1666
|
+
|
|
1667
|
+
|
|
1668
|
+
def run_wizard():
|
|
1669
|
+
"""Interactive first-run setup. Nothing is saved until the last step."""
|
|
1670
|
+
new = dict(CONFIG)
|
|
1671
|
+
names = list(THEMES)
|
|
1672
|
+
can_profile = bool(_profile_paths()) # i.e. Windows
|
|
1673
|
+
total = 6 + (1 if can_profile else 0)
|
|
1674
|
+
count = [0]
|
|
1675
|
+
|
|
1676
|
+
def step(title):
|
|
1677
|
+
count[0] += 1
|
|
1678
|
+
print(f"\n{paint(f'[{count[0]}/{total}]', '1;36')} {paint(title, '1')}")
|
|
1679
|
+
|
|
1680
|
+
banner(force=True)
|
|
1681
|
+
print(" Welcome! Let's set up neoshell. Press Enter to accept the [default].")
|
|
1682
|
+
print(" Press Ctrl+C at any time to skip. Nothing is saved until the end.")
|
|
1683
|
+
add_path = False
|
|
1684
|
+
try:
|
|
1685
|
+
step("What should neoshell call you?")
|
|
1686
|
+
default_name = new["name"] or getpass.getuser()
|
|
1687
|
+
new["name"] = ask(f" Name [{default_name}]: ", default_name)
|
|
1688
|
+
|
|
1689
|
+
step("Pick a theme")
|
|
1690
|
+
labels = [f"{gradient_text('neoshell', THEMES[n]['c1'], THEMES[n]['c2'])} {n}" for n in names]
|
|
1691
|
+
new["theme"] = names[ask_choice(labels, names.index(new["theme"]) + 1) - 1]
|
|
1692
|
+
|
|
1693
|
+
step("Prompt style")
|
|
1694
|
+
previews = ["minimal projects >",
|
|
1695
|
+
"full C:\\Users\\you\\projects (main) >",
|
|
1696
|
+
"two lines ┌ you@laptop C:\\Users\\you\\projects (main)\n"
|
|
1697
|
+
" └ >"]
|
|
1698
|
+
new["prompt"] = PROMPTS[ask_choice(previews, PROMPTS.index(new["prompt"]) + 1) - 1]
|
|
1699
|
+
|
|
1700
|
+
step("Prompt extras")
|
|
1701
|
+
new["git_branch"] = ask_yes_no("Show the git branch in the prompt?", new["git_branch"])
|
|
1702
|
+
new["clock"] = ask_yes_no("Show the clock on the right side?", new["clock"])
|
|
1703
|
+
|
|
1704
|
+
step("Startup")
|
|
1705
|
+
new["banner"] = ask_yes_no("Show the neoshell logo when it starts?", new["banner"])
|
|
1706
|
+
|
|
1707
|
+
step("Where should neoshell start?")
|
|
1708
|
+
modes = ["smart", "home", "launch"]
|
|
1709
|
+
opts = ["In my user folder, unless I opened it somewhere on purpose (recommended)",
|
|
1710
|
+
"Always in my user folder",
|
|
1711
|
+
"Wherever it was launched from",
|
|
1712
|
+
"A specific folder..."]
|
|
1713
|
+
choice = ask_choice(opts, modes.index(new["start_dir"]) + 1 if new["start_dir"] in modes else 4)
|
|
1714
|
+
if choice <= 3:
|
|
1715
|
+
new["start_dir"] = modes[choice - 1]
|
|
1716
|
+
else:
|
|
1717
|
+
while True:
|
|
1718
|
+
folder = os.path.expandvars(os.path.expanduser(ask(" Folder path: ").strip('"')))
|
|
1719
|
+
if os.path.isdir(folder):
|
|
1720
|
+
new["start_dir"] = os.path.abspath(folder); break
|
|
1721
|
+
print(" That folder doesn't exist. Try again, or press Ctrl+C to skip.")
|
|
1722
|
+
|
|
1723
|
+
if can_profile:
|
|
1724
|
+
step("Windows Terminal")
|
|
1725
|
+
new["manage_profile"] = ask_yes_no(
|
|
1726
|
+
"Add a neoshell profile (with icon and colors) to Windows Terminal?", new["manage_profile"])
|
|
1727
|
+
if new["manage_profile"] and FROZEN:
|
|
1728
|
+
add_path = ask_yes_no('Add neoshell to your PATH so you can type "neoshell" anywhere?', False)
|
|
1729
|
+
except KeyboardInterrupt:
|
|
1730
|
+
print("\n\n Setup skipped. Nothing was changed. Run setup any time to try again.")
|
|
1731
|
+
CONFIG["wizard_done"] = True
|
|
1732
|
+
save_config()
|
|
1733
|
+
return False
|
|
1734
|
+
|
|
1735
|
+
new["wizard_done"] = True
|
|
1736
|
+
CONFIG.update(new)
|
|
1737
|
+
save_config()
|
|
1738
|
+
|
|
1739
|
+
print("\n" + paint("All set!", "1;32"))
|
|
1740
|
+
for k in ("name", "theme", "prompt", "git_branch", "clock", "banner", "start_dir"):
|
|
1741
|
+
print(f" {k:<12} {CONFIG[k]}")
|
|
1742
|
+
if can_profile and CONFIG["manage_profile"] and not profile_current():
|
|
1743
|
+
install_profile()
|
|
1744
|
+
if add_path:
|
|
1745
|
+
inst = _profile_paths()[0]
|
|
1746
|
+
if user_path_edit(inst, True):
|
|
1747
|
+
print(" Added to PATH. Open a new terminal window and type neoshell")
|
|
1748
|
+
else:
|
|
1749
|
+
print(" Couldn't edit PATH.")
|
|
1750
|
+
print("\n Change anything later with setup or config set <key> <value>.")
|
|
1751
|
+
print(" Handy extras: theme, bm (folder bookmarks), doskey (saved macros), about\n")
|
|
1752
|
+
ask(" Press Enter to start ")
|
|
1753
|
+
return True
|
|
1754
|
+
|
|
1755
|
+
|
|
1756
|
+
# ---------------------------------------------------------------- main loop
|
|
1348
1757
|
def main():
|
|
1349
1758
|
global session, last_dur
|
|
1350
1759
|
argv = sys.argv[1:]
|
|
1760
|
+
load_config()
|
|
1761
|
+
aliases.update(CONFIG["macros"])
|
|
1351
1762
|
if "--uninstall" in argv:
|
|
1352
1763
|
return 0 if uninstall_profile() else 1
|
|
1353
1764
|
if "--install" in argv:
|
|
1354
1765
|
return 0 if install_profile() else 1
|
|
1766
|
+
if "--setup" in argv:
|
|
1767
|
+
run_wizard(); return 0
|
|
1355
1768
|
no_install = "--no-install" in argv
|
|
1356
1769
|
if argv[:1] == ["-c"]:
|
|
1357
1770
|
try:
|
|
@@ -1360,6 +1773,12 @@ def main():
|
|
|
1360
1773
|
return e.code or 0
|
|
1361
1774
|
return last_status
|
|
1362
1775
|
|
|
1776
|
+
if (not CONFIG["wizard_done"] and "--no-wizard" not in argv
|
|
1777
|
+
and sys.stdin.isatty() and sys.stdout.isatty()):
|
|
1778
|
+
run_wizard()
|
|
1779
|
+
print("\033[2J\033[3J\033[H", end="")
|
|
1780
|
+
apply_start_dir()
|
|
1781
|
+
|
|
1363
1782
|
old_hist = os.path.expanduser("~/.pyshell_history")
|
|
1364
1783
|
if os.path.isfile(old_hist) and not os.path.isfile(HIST):
|
|
1365
1784
|
with contextlib.suppress(OSError):
|
|
@@ -1367,18 +1786,25 @@ def main():
|
|
|
1367
1786
|
if HAVE_PT:
|
|
1368
1787
|
session = PromptSession(history=FileHistory(HIST), auto_suggest=AutoSuggestFromHistory(),
|
|
1369
1788
|
lexer=CmdLexer(), completer=CmdCompleter(),
|
|
1370
|
-
complete_while_typing=
|
|
1789
|
+
complete_while_typing=CONFIG["complete_while_typing"])
|
|
1371
1790
|
read = lambda: session.prompt(prompt_text, rprompt=rprompt_text)
|
|
1372
1791
|
else:
|
|
1373
1792
|
print("(tip: pip install prompt_toolkit for syntax highlighting and tab completion)")
|
|
1374
1793
|
read = lambda: input(f"{os.getcwd()}> ")
|
|
1375
1794
|
|
|
1376
|
-
start_in_home()
|
|
1377
1795
|
b_title(["neoshell"])
|
|
1378
1796
|
banner()
|
|
1379
|
-
if FROZEN and not no_install and
|
|
1797
|
+
if (FROZEN and not no_install and CONFIG["manage_profile"] and _profile_paths()
|
|
1798
|
+
and not profile_current()):
|
|
1380
1799
|
install_profile()
|
|
1381
1800
|
print()
|
|
1801
|
+
for cmd in CONFIG["startup"]:
|
|
1802
|
+
try:
|
|
1803
|
+
run_line(cmd)
|
|
1804
|
+
except SystemExit:
|
|
1805
|
+
return 0
|
|
1806
|
+
except Exception as e:
|
|
1807
|
+
print(paint(f"startup command failed: {e}", "31"))
|
|
1382
1808
|
while True:
|
|
1383
1809
|
try:
|
|
1384
1810
|
line = read()
|
neoshell-2.3.dist-info/RECORD
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
neoshell.py,sha256=xCWQJtooRbyJ2r9IJWVBNA1kziF4q3gVQw_kpCXn2f0,48730
|
|
2
|
-
neoshell-2.3.dist-info/METADATA,sha256=Zv-xrWZt8yNRKr4pQ95gZarFL0EJvGlbEoKtqZt0qxY,315
|
|
3
|
-
neoshell-2.3.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
4
|
-
neoshell-2.3.dist-info/entry_points.txt,sha256=T5sqOBER4jIRiYq3oW9znixzukrsn_E4okrnfjQgEIk,43
|
|
5
|
-
neoshell-2.3.dist-info/top_level.txt,sha256=CwkcL_wEg3YGVVQJDYOj5NrXFMekQwqGvTtkY6_chI4,9
|
|
6
|
-
neoshell-2.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|