NeoShell 2.4__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: NeoShell
3
- Version: 2.4
3
+ Version: 2.5
4
4
  Summary: light and fast shell for any os linux, mac, windows.
5
5
  Author: burgerkayson
6
6
  Requires-Python: >=3.8
@@ -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.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()): print(f"{k}={v}")
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
- parts = [("fg:ansiblue bold", os.getcwd())]
1309
- br = git_branch()
1310
- if br:
1311
- parts.append(("fg:ansimagenta", f" ({br})"))
1312
- parts.append(("fg:ansigreen bold" if last_status == 0 else "fg:ansired bold", " > "))
1313
- return parts
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
- bits.append(("fg:ansibrightblack", time.strftime("%H:%M")))
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 ASCII logo (cyan -> violet, same colors as the app icon)."""
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
- c1, c2 = (77, 225, 255), (199, 146, 255)
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
- out = ""
1340
- for x, ch in enumerate(row):
1341
- t = x / max(1, width - 1)
1342
- r, g, b = (int(c1[i] + (c2[i] - c1[i]) * t) for i in range(3))
1343
- out += f"\033[38;2;{r};{g};{b}m{ch}"
1344
- print(" " + out + "\033[0m")
1345
- print(f" \033[90mCMD-style shell · v{VERSION} · type \033[0m\033[1mhelp\033[0m\033[90m for commands\033[0m\n")
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=False)
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 _profile_paths() and not profile_current():
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()
@@ -1,6 +0,0 @@
1
- neoshell.py,sha256=xCWQJtooRbyJ2r9IJWVBNA1kziF4q3gVQw_kpCXn2f0,48730
2
- neoshell-2.4.dist-info/METADATA,sha256=JLo6HtbbCVJhK-uWBHzyHpT1cL_ybVYBtl7qTCPGnJw,315
3
- neoshell-2.4.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
4
- neoshell-2.4.dist-info/entry_points.txt,sha256=T5sqOBER4jIRiYq3oW9znixzukrsn_E4okrnfjQgEIk,43
5
- neoshell-2.4.dist-info/top_level.txt,sha256=CwkcL_wEg3YGVVQJDYOj5NrXFMekQwqGvTtkY6_chI4,9
6
- neoshell-2.4.dist-info/RECORD,,
File without changes