daps-shell 0.1.7__tar.gz → 0.2.3__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: daps-shell
3
- Version: 0.1.7
3
+ Version: 0.2.3
4
4
  Summary: A customisable Python shell with plugins, aliases, and syntax highlighting
5
5
  License: MIT
6
6
  Requires-Python: >=3.11
@@ -6,16 +6,32 @@ from prompt_toolkit.lexers import Lexer
6
6
  from prompt_toolkit.styles import Style
7
7
  from prompt_toolkit.formatted_text import FormattedText
8
8
  from prompt_toolkit.history import FileHistory
9
+ from prompt_toolkit.keys import Keys
10
+ from prompt_toolkit.key_binding import KeyBindings
9
11
 
10
12
  plugins = {}
11
- _last_cmd_time = None
13
+ _start_time = time.monotonic()
12
14
 
13
15
  THEMES = {
14
- "default": {"username": "#00d787 bold", "at": "#ffffff", "hostname": "#5fafff bold", "paren": "#00d787", "ident": "#00d787 bold", "dash": "#ffffff", "path": "#00d7af bold", "arrow": "#00d787 bold", "errcode": "#ff5f5f bold", "branch": "#d7af5f", "builtin": "#00d787 bold", "alias": "#5fafff bold", "plugin": "#d7af5f bold", "cmd": "#ffffff", "cmd-real": "#ffffff underline", "args": "#aaaaaa", "arg-real": "#aaaaaa underline", "time": "#888888"},
15
- "fire": {"username": "#ff5f00 bold", "at": "#ffffff", "hostname": "#ff8700 bold", "paren": "#ff5f00", "ident": "#ff5f00 bold", "dash": "#ffffff", "path": "#ffaf00 bold", "arrow": "#ff5f00 bold", "errcode": "#ff0000 bold", "branch": "#ffaf5f", "builtin": "#ff5f00 bold", "alias": "#ffaf00 bold", "plugin": "#ff8700 bold", "cmd": "#ffffff", "cmd-real": "#ffffff underline", "args": "#aaaaaa", "arg-real": "#aaaaaa underline", "time": "#888888"},
16
- "ocean": {"username": "#0087ff bold", "at": "#ffffff", "hostname": "#00afff bold", "paren": "#0087ff", "ident": "#0087ff bold", "dash": "#ffffff", "path": "#00d7ff bold", "arrow": "#0087ff bold", "errcode": "#ff5f5f bold", "branch": "#5fafff", "builtin": "#0087ff bold", "alias": "#00afff bold", "plugin": "#5fafff bold", "cmd": "#ffffff", "cmd-real": "#ffffff underline", "args": "#aaaaaa", "arg-real": "#aaaaaa underline", "time": "#888888"},
17
- "candy": {"username": "#ff5faf bold", "at": "#ffffff", "hostname": "#af5fff bold", "paren": "#ff5faf", "ident": "#ff5faf bold", "dash": "#ffffff", "path": "#ff87d7 bold", "arrow": "#ff5faf bold", "errcode": "#ff0000 bold", "branch": "#ffaf5f", "builtin": "#ff5faf bold", "alias": "#af5fff bold", "plugin": "#ff87d7 bold", "cmd": "#ffffff", "cmd-real": "#ffffff underline", "args": "#aaaaaa", "arg-real": "#aaaaaa underline", "time": "#888888"},
18
- "mono": {"username": "#ffffff bold", "at": "#aaaaaa", "hostname": "#ffffff bold", "paren": "#aaaaaa", "ident": "#ffffff bold", "dash": "#aaaaaa", "path": "#ffffff bold", "arrow": "#ffffff bold", "errcode": "#ff5f5f bold", "branch": "#aaaaaa", "builtin": "#ffffff bold", "alias": "#aaaaaa bold", "plugin": "#aaaaaa bold", "cmd": "#ffffff", "cmd-real": "#ffffff underline", "args": "#666666", "arg-real": "#666666 underline", "time": "#555555"},
16
+ "default": {"username":"#00d787 bold","at":"#ffffff","hostname":"#5fafff bold","paren":"#00d787","ident":"#00d787 bold","dash":"#ffffff","path":"#00d7af bold","arrow":"#00d787 bold","errcode":"#ff5f5f bold","branch":"#d7af5f","builtin":"#00d787 bold","alias":"#5fafff bold","plugin":"#d7af5f bold","cmd":"#ffffff","cmd-real":"#ffffff underline","args":"#aaaaaa","arg-real":"#aaaaaa underline","time":"#888888"},
17
+ "fire": {"username":"#ff5f00 bold","at":"#ffffff","hostname":"#ff8700 bold","paren":"#ff5f00","ident":"#ff5f00 bold","dash":"#ffffff","path":"#ffaf00 bold","arrow":"#ff5f00 bold","errcode":"#ff0000 bold","branch":"#ffaf5f","builtin":"#ff5f00 bold","alias":"#ffaf00 bold","plugin":"#ff8700 bold","cmd":"#ffffff","cmd-real":"#ffffff underline","args":"#aaaaaa","arg-real":"#aaaaaa underline","time":"#888888"},
18
+ "ocean": {"username":"#0087ff bold","at":"#ffffff","hostname":"#00afff bold","paren":"#0087ff","ident":"#0087ff bold","dash":"#ffffff","path":"#00d7ff bold","arrow":"#0087ff bold","errcode":"#ff5f5f bold","branch":"#5fafff","builtin":"#0087ff bold","alias":"#00afff bold","plugin":"#5fafff bold","cmd":"#ffffff","cmd-real":"#ffffff underline","args":"#aaaaaa","arg-real":"#aaaaaa underline","time":"#888888"},
19
+ "candy": {"username":"#ff5faf bold","at":"#ffffff","hostname":"#af5fff bold","paren":"#ff5faf","ident":"#ff5faf bold","dash":"#ffffff","path":"#ff87d7 bold","arrow":"#ff5faf bold","errcode":"#ff0000 bold","branch":"#ffaf5f","builtin":"#ff5faf bold","alias":"#af5fff bold","plugin":"#ff87d7 bold","cmd":"#ffffff","cmd-real":"#ffffff underline","args":"#aaaaaa","arg-real":"#aaaaaa underline","time":"#888888"},
20
+ "mono": {"username":"#ffffff bold","at":"#aaaaaa","hostname":"#ffffff bold","paren":"#aaaaaa","ident":"#ffffff bold","dash":"#aaaaaa","path":"#ffffff bold","arrow":"#ffffff bold","errcode":"#ff5f5f bold","branch":"#aaaaaa","builtin":"#ffffff bold","alias":"#aaaaaa bold","plugin":"#aaaaaa bold","cmd":"#ffffff","cmd-real":"#ffffff underline","args":"#666666","arg-real":"#666666 underline","time":"#555555"},
21
+ "cyberpunk": {"username":"#ffff00 bold","at":"#ff00ff","hostname":"#ff00ff bold","paren":"#ffff00","ident":"#ffff00 bold","dash":"#ff00ff","path":"#00ffff bold","arrow":"#ffff00 bold","errcode":"#ff0000 bold","branch":"#ff00ff","builtin":"#ffff00 bold","alias":"#ff00ff bold","plugin":"#00ffff bold","cmd":"#ffffff","cmd-real":"#ffffff underline","args":"#aaaaaa","arg-real":"#aaaaaa underline","time":"#888888"},
22
+ "nord": {"username":"#88c0d0 bold","at":"#e5e9f0","hostname":"#81a1c1 bold","paren":"#88c0d0","ident":"#88c0d0 bold","dash":"#e5e9f0","path":"#8fbcbb bold","arrow":"#88c0d0 bold","errcode":"#bf616a bold","branch":"#ebcb8b","builtin":"#88c0d0 bold","alias":"#81a1c1 bold","plugin":"#b48ead bold","cmd":"#eceff4","cmd-real":"#eceff4 underline","args":"#7b88a1","arg-real":"#7b88a1 underline","time":"#616e88"},
23
+ "dracula": {"username":"#ff79c6 bold","at":"#f8f8f2","hostname":"#bd93f9 bold","paren":"#ff79c6","ident":"#ff79c6 bold","dash":"#f8f8f2","path":"#50fa7b bold","arrow":"#ff79c6 bold","errcode":"#ff5555 bold","branch":"#f1fa8c","builtin":"#ff79c6 bold","alias":"#bd93f9 bold","plugin":"#8be9fd bold","cmd":"#f8f8f2","cmd-real":"#f8f8f2 underline","args":"#6272a4","arg-real":"#6272a4 underline","time":"#44475a"},
24
+ "solarized": {"username":"#859900 bold","at":"#839496","hostname":"#268bd2 bold","paren":"#859900","ident":"#859900 bold","dash":"#839496","path":"#2aa198 bold","arrow":"#859900 bold","errcode":"#dc322f bold","branch":"#b58900","builtin":"#859900 bold","alias":"#268bd2 bold","plugin":"#6c71c4 bold","cmd":"#839496","cmd-real":"#839496 underline","args":"#586e75","arg-real":"#586e75 underline","time":"#657b83"},
25
+ "blood": {"username":"#ff0000 bold","at":"#cc0000","hostname":"#aa0000 bold","paren":"#ff0000","ident":"#ff0000 bold","dash":"#cc0000","path":"#ff3333 bold","arrow":"#ff0000 bold","errcode":"#ffffff bold","branch":"#ff6666","builtin":"#ff0000 bold","alias":"#cc0000 bold","plugin":"#ff3333 bold","cmd":"#ffcccc","cmd-real":"#ffcccc underline","args":"#993333","arg-real":"#993333 underline","time":"#661111"},
26
+ "ice": {"username":"#ffffff bold","at":"#aaffff","hostname":"#00ffff bold","paren":"#ffffff","ident":"#ffffff bold","dash":"#aaffff","path":"#aaffff bold","arrow":"#ffffff bold","errcode":"#ff5f5f bold","branch":"#00ffff","builtin":"#ffffff bold","alias":"#00ffff bold","plugin":"#aaffff bold","cmd":"#ffffff","cmd-real":"#ffffff underline","args":"#88cccc","arg-real":"#88cccc underline","time":"#557777"},
27
+ }
28
+
29
+ PROMPT_FORMATS = {
30
+ "default": "{errcode}{time}{user}@{host} ({ident}) - {cwd}{branch}\n> ",
31
+ "minimal": "{errcode}{user} {cwd}{branch} > ",
32
+ "compact": "{errcode}{ident} {cwd}{branch} > ",
33
+ "powerline":"{errcode} {user} | {cwd} | {branch} \n> ",
34
+ "classic": "[{user}@{host} {cwd}]{branch}{ident} ",
19
35
  }
20
36
 
21
37
  def loadconf():
@@ -59,23 +75,30 @@ def reload_plugins(base_config):
59
75
 
60
76
  def load_local_conf(base):
61
77
  local = os.path.join(os.getcwd(), ".daps")
62
- if not os.path.exists(local):
63
- return base
78
+ if not os.path.exists(local): return base
64
79
  try:
65
- with open(local) as f:
66
- overrides = json.load(f)
80
+ with open(local) as f: overrides = json.load(f)
67
81
  merged = dict(base)
68
82
  merged["aliases"] = {**base.get("aliases", {}), **overrides.get("aliases", {})}
69
83
  for k, v in overrides.items():
70
84
  if k != "aliases": merged[k] = v
71
85
  return merged
72
- except Exception:
73
- return base
86
+ except Exception: return base
74
87
 
75
88
  def save_conf(config):
76
89
  whereis = os.path.join(os.path.expanduser("~/.config/daps"), "config.json")
77
- with open(whereis, "w") as f:
78
- json.dump(config, f, indent=2)
90
+ with open(whereis, "w") as f: json.dump(config, f, indent=2)
91
+
92
+ def get_bookmarks():
93
+ f = os.path.expanduser("~/.config/daps/bookmarks.json")
94
+ if not os.path.exists(f): return {}
95
+ try:
96
+ with open(f) as fp: return json.load(fp)
97
+ except Exception: return {}
98
+
99
+ def save_bookmarks(bm):
100
+ f = os.path.expanduser("~/.config/daps/bookmarks.json")
101
+ with open(f, "w") as fp: json.dump(bm, fp, indent=2)
79
102
 
80
103
  def git_branch():
81
104
  try:
@@ -84,11 +107,12 @@ def git_branch():
84
107
  if not b: return None
85
108
  dirty = subprocess.run(["git", "status", "--porcelain"], capture_output=True, text=True, timeout=1).stdout.strip()
86
109
  return f"{b}{'*' if dirty else ''}"
87
- except Exception:
88
- return None
110
+ except Exception: return None
89
111
 
90
- def expand_env(s):
91
- return os.path.expandvars(os.path.expanduser(s))
112
+ def expand_env(s): return os.path.expandvars(os.path.expanduser(s))
113
+ def is_executable(cmd): return shutil.which(cmd) is not None
114
+ def is_existing_path(token): return os.path.exists(os.path.expanduser(expand_env(token)))
115
+ def is_ssh(): return "SSH_CLIENT" in os.environ or "SSH_TTY" in os.environ
92
116
 
93
117
  def fmt_time(secs):
94
118
  if secs is None: return None
@@ -96,15 +120,14 @@ def fmt_time(secs):
96
120
  if secs < 60: return f"{secs:.1f}s"
97
121
  return f"{int(secs//60)}m{int(secs%60)}s"
98
122
 
99
- def is_ssh(): return "SSH_CLIENT" in os.environ or "SSH_TTY" in os.environ
100
-
101
- BUILTINS = ["cd", "exit", "clear", "plugins", "daps", "which"]
123
+ BUILTINS = ["cd", "exit", "clear", "plugins", "daps", "which", "help", "bookmark", "bm"]
102
124
 
103
125
  class DapsCompleter(Completer):
104
126
  def __init__(self, aliases):
105
127
  self.aliases = aliases
106
128
  def _commands(self):
107
- return list(self.aliases.keys()) + list(plugins.keys()) + BUILTINS
129
+ bm = list(get_bookmarks().keys())
130
+ return list(self.aliases.keys()) + list(plugins.keys()) + BUILTINS + bm
108
131
  def get_completions(self, document, complete_event):
109
132
  text = document.text_before_cursor
110
133
  parts = text.split()
@@ -119,9 +142,6 @@ class DapsCompleter(Completer):
119
142
  if os.path.isdir(m): display += "/"
120
143
  yield Completion(display, start_position=-len(word))
121
144
 
122
- def is_executable(cmd): return shutil.which(cmd) is not None
123
- def is_existing_path(token): return os.path.exists(os.path.expanduser(expand_env(token)))
124
-
125
145
  class DapsLexer(Lexer):
126
146
  def __init__(self, aliases):
127
147
  self.aliases = aliases
@@ -152,28 +172,36 @@ class DapsLexer(Lexer):
152
172
  return inner
153
173
 
154
174
  def make_style(theme_name):
155
- t = THEMES.get(theme_name, THEMES["default"])
156
- return Style.from_dict(t)
175
+ return Style.from_dict(THEMES.get(theme_name, THEMES["default"]))
157
176
 
158
- def build_prompt(user, host, errcode, elapsed, theme_name):
177
+ def build_prompt(user, host, errcode, elapsed, theme_name, fmt_name="default"):
159
178
  home = os.path.expanduser("~")
160
179
  cwd = os.getcwd()
161
180
  display_cwd = cwd.replace(home, "~", 1) if cwd.startswith(home) else cwd
162
- ident = "#" if user == "root" else ("$" if not is_ssh() else "@")
181
+ ident = "#" if user == "root" else ("@" if is_ssh() else "$")
163
182
  branch = git_branch()
164
183
  t = fmt_time(elapsed)
165
184
  tokens = []
166
- if errcode and errcode != "0":
167
- tokens += [("class:errcode", f"[{errcode}]"), ("class:dash", " - ")]
168
- if t:
169
- tokens += [("class:time", f"[{t}]"), ("class:dash", " - ")]
170
- tokens += [
171
- ("class:username", user), ("class:at", "@"), ("class:hostname", host),
172
- ("class:dash", " "), ("class:paren", "("), ("class:ident", ident), ("class:paren", ")"),
173
- ("class:dash", " - "), ("class:path", display_cwd),
174
- ]
175
- if branch: tokens += [("class:dash", " "), ("class:branch", f"[{branch}]")]
176
- tokens += [("", "\n"), ("class:arrow", "> ")]
185
+ if fmt_name == "minimal":
186
+ if errcode and errcode != "0": tokens += [("class:errcode", f"[{errcode}] ")]
187
+ tokens += [("class:username", user), ("class:dash", " "), ("class:path", display_cwd)]
188
+ if branch: tokens += [("class:dash", " "), ("class:branch", f"[{branch}]")]
189
+ tokens += [("", "\n"), ("class:arrow", "> ")]
190
+ elif fmt_name == "compact":
191
+ if errcode and errcode != "0": tokens += [("class:errcode", f"[{errcode}] ")]
192
+ tokens += [("class:ident", ident), ("class:dash", " "), ("class:path", display_cwd)]
193
+ if branch: tokens += [("class:dash", " "), ("class:branch", f"[{branch}]")]
194
+ tokens += [("", "\n"), ("class:arrow", "> ")]
195
+ elif fmt_name == "classic":
196
+ tokens += [("class:paren", "["), ("class:username", user), ("class:at", "@"), ("class:hostname", host), ("class:dash", " "), ("class:path", display_cwd), ("class:paren", "]")]
197
+ if branch: tokens += [("class:branch", f"({branch})")]
198
+ tokens += [("class:ident", ident), ("", " ")]
199
+ else:
200
+ if errcode and errcode != "0": tokens += [("class:errcode", f"[{errcode}]"), ("class:dash", " - ")]
201
+ if t: tokens += [("class:time", f"[{t}]"), ("class:dash", " - ")]
202
+ tokens += [("class:username", user), ("class:at", "@"), ("class:hostname", host), ("class:dash", " "), ("class:paren", "("), ("class:ident", ident), ("class:paren", ")"), ("class:dash", " - "), ("class:path", display_cwd)]
203
+ if branch: tokens += [("class:dash", " "), ("class:branch", f"[{branch}]")]
204
+ tokens += [("", "\n"), ("class:arrow", "> ")]
177
205
  return tokens
178
206
 
179
207
  def fmt_error(msg): print(f"\033[91mdaps error\033[0m {msg}", file=sys.stderr)
@@ -198,6 +226,41 @@ def suggest(cmd, alias):
198
226
  print(f"\033[93mdaps\033[0m command not found: \033[91m{cmd}\033[0m")
199
227
  if matches: print(f" did you mean: {', '.join(f'\033[96m{m}\033[0m' for m in matches)}?")
200
228
 
229
+ def do_help(alias):
230
+ print(f"\033[96m{'─'*50}\033[0m")
231
+ print(f"\033[96mdaps\033[0m built-in commands:")
232
+ helps = {
233
+ "cd [path]": "change directory (- for prev, fuzzy match on miss)",
234
+ "clear": "clear screen (re-runs greeter if cleargreet=yes)",
235
+ "exit": "exit daps",
236
+ "help": "show this help",
237
+ "which <cmd>": "show what a command resolves to",
238
+ "plugins": "list loaded plugins",
239
+ "bookmark <name> [path]": "save/jump to a directory bookmark",
240
+ "bm": "alias for bookmark",
241
+ "daps config set/get": "manage config",
242
+ "daps config ui": "open web config editor",
243
+ "daps alias list": "list aliases",
244
+ "daps theme <n>": "switch theme",
245
+ "daps prompt <fmt>": "switch prompt format",
246
+ "daps reload": "reload config and plugins",
247
+ "daps update": "upgrade daps-shell",
248
+ "daps env set/get/del/list": "manage env vars",
249
+ "!! / sudo !!": "repeat last command",
250
+ "cmd &": "run in background",
251
+ "cmd \\": "multiline input",
252
+ }
253
+ for k, v in helps.items(): print(f" \033[92m{k:<30}\033[0m {v}")
254
+ if alias:
255
+ print(f"\n\033[96mdaps\033[0m aliases ({len(alias)}):")
256
+ for k, v in alias.items(): print(f" \033[96m{k:<20}\033[0m → {v}")
257
+ if plugins:
258
+ print(f"\n\033[96mdaps\033[0m plugins ({len(plugins)}):")
259
+ for name, mod in plugins.items():
260
+ doc = getattr(mod, "__doc__", None) or (mod.run.__doc__ if hasattr(mod, "run") else None) or "no description"
261
+ print(f" \033[93m{name:<20}\033[0m {doc.strip()}")
262
+ print(f"\033[96m{'─'*50}\033[0m")
263
+
201
264
  def do_plugins():
202
265
  if not plugins: fmt_info("no plugins loaded"); return
203
266
  fmt_info(f"{len(plugins)} plugin(s) loaded:")
@@ -218,6 +281,27 @@ def do_which(args, alias):
218
281
  if found: print(f"{cmd}: {found}"); return "0"
219
282
  fmt_error(f"which: {cmd}: not found"); return "1"
220
283
 
284
+ def do_bookmark(args):
285
+ bm = get_bookmarks()
286
+ if not args:
287
+ if not bm: fmt_info("no bookmarks"); return "0"
288
+ fmt_info(f"{len(bm)} bookmark(s):")
289
+ for k, v in bm.items(): print(f" \033[96m{k:<20}\033[0m {v}")
290
+ return "0"
291
+ name = args[0]
292
+ if len(args) >= 2:
293
+ path = os.path.expanduser(" ".join(args[1:]))
294
+ bm[name] = path; save_bookmarks(bm)
295
+ fmt_info(f"bookmark '{name}' → {path}"); return "0"
296
+ if name in bm:
297
+ try: os.chdir(bm[name]); return "0"
298
+ except Exception as e: fmt_error(str(e)); return "1"
299
+ if name == "del" and len(args) >= 2:
300
+ bm.pop(args[1], None); save_bookmarks(bm)
301
+ fmt_info(f"bookmark '{args[1]}' removed"); return "0"
302
+ bm[name] = os.getcwd(); save_bookmarks(bm)
303
+ fmt_info(f"bookmark '{name}' → {os.getcwd()}"); return "0"
304
+
221
305
  def do_cd_fuzzy(target):
222
306
  try: os.chdir(target); return True
223
307
  except FileNotFoundError:
@@ -233,20 +317,38 @@ def do_cd_fuzzy(target):
233
317
  except Exception: pass
234
318
  return False
235
319
 
320
+ def do_env(args, base_config):
321
+ envs = base_config.get("env", {})
322
+ if not args or args[0] == "list":
323
+ if not envs: fmt_info("no env vars set"); return "0"
324
+ fmt_info(f"{len(envs)} env var(s):")
325
+ for k, v in envs.items(): print(f" \033[96m{k}\033[0m={v}")
326
+ return "0"
327
+ if args[0] == "set" and len(args) >= 3:
328
+ key, val = args[1], " ".join(args[2:])
329
+ envs[key] = val; base_config["env"] = envs
330
+ os.environ[key] = val; save_conf(base_config)
331
+ fmt_info(f"set {key}={val}"); return "0"
332
+ if args[0] == "get" and len(args) >= 2:
333
+ v = envs.get(args[1]) or os.environ.get(args[1])
334
+ if v: print(f" {args[1]}={v}"); return "0"
335
+ fmt_error(f"env: {args[1]} not set"); return "1"
336
+ if args[0] == "del" and len(args) >= 2:
337
+ envs.pop(args[1], None); base_config["env"] = envs
338
+ os.environ.pop(args[1], None); save_conf(base_config)
339
+ fmt_info(f"unset {args[1]}"); return "0"
340
+ fmt_error(f"usage: daps env set/get/del/list"); return "1"
341
+
236
342
  def install_plugin(url):
237
343
  name = url.split("/")[-1]
238
344
  if not name.endswith(".py"): name += ".py"
239
345
  dest = os.path.join(os.path.expanduser("~/.config/daps/plugins"), name)
240
- try:
241
- urllib.request.urlretrieve(url, dest)
242
- fmt_info(f"installed plugin: {name}")
243
- return True
244
- except Exception as e:
245
- fmt_error(f"plugin install failed: {e}"); return False
346
+ try: urllib.request.urlretrieve(url, dest); return True, None
347
+ except Exception as e: return False, str(e)
246
348
 
247
349
  def do_daps_cmd(args, base_config):
248
350
  if not args:
249
- fmt_info("usage: daps config set/get/set-alias/del-alias | daps plugins | daps plugin install <url> | daps reload | daps update | daps theme <name> | daps alias list")
351
+ fmt_info("usage: daps config|plugins|plugin|reload|update|theme|prompt|alias|env|config ui")
250
352
  return "0"
251
353
  sub = args[0]
252
354
  if sub == "plugins": do_plugins(); return "0"
@@ -256,23 +358,31 @@ def do_daps_cmd(args, base_config):
256
358
  r = subprocess.run([sys.executable, "-m", "pip", "install", "--upgrade", "daps-shell"])
257
359
  return str(r.returncode)
258
360
  if sub == "theme":
259
- if len(args) < 2:
260
- fmt_info(f"available themes: {', '.join(THEMES.keys())}"); return "0"
361
+ if len(args) < 2: fmt_info(f"available themes: {', '.join(THEMES.keys())}"); return "0"
261
362
  if args[1] not in THEMES: fmt_error(f"unknown theme: {args[1]}"); return "1"
262
- base_config["theme"] = args[1]
263
- save_conf(base_config)
264
- fmt_info(f"theme set to {args[1]} (takes effect next prompt)"); return "0"
363
+ base_config["theme"] = args[1]; save_conf(base_config)
364
+ fmt_info(f"theme set to {args[1]}"); return "0"
365
+ if sub == "prompt":
366
+ if len(args) < 2: fmt_info(f"available formats: {', '.join(PROMPT_FORMATS.keys())}"); return "0"
367
+ if args[1] not in PROMPT_FORMATS: fmt_error(f"unknown format: {args[1]}"); return "1"
368
+ base_config["prompt_format"] = args[1]; save_conf(base_config)
369
+ fmt_info(f"prompt format set to {args[1]}"); return "0"
265
370
  if sub == "plugin" and len(args) >= 3 and args[1] == "install":
266
- install_plugin(args[2]); return "0"
371
+ ok, err = install_plugin(args[2])
372
+ if ok: fmt_info("plugin installed"); return "0"
373
+ fmt_error(f"install failed: {err}"); return "1"
267
374
  if sub == "alias" and len(args) >= 2 and args[1] == "list":
268
375
  aliases = base_config.get("aliases", {})
269
376
  if not aliases: fmt_info("no aliases set"); return "0"
270
377
  fmt_info(f"{len(aliases)} alias(es):")
271
378
  for k, v in aliases.items(): print(f" \033[96m{k}\033[0m → {v}")
272
379
  return "0"
380
+ if sub == "env":
381
+ return do_env(args[1:], base_config)
273
382
  if sub == "config":
274
- if len(args) < 2:
275
- fmt_info("usage: daps config set <key> <value> | daps config get <key>"); return "0"
383
+ if len(args) >= 2 and args[1] == "ui":
384
+ from daps.ui import launch; launch(); return "0"
385
+ if len(args) < 2: fmt_info("usage: daps config set/get/set-alias/del-alias/ui"); return "0"
276
386
  if args[1] == "get" and len(args) >= 3:
277
387
  print(f" {args[2]} = {json.dumps(base_config.get(args[2], None))}"); return "0"
278
388
  if args[1] == "set" and len(args) >= 4:
@@ -294,25 +404,36 @@ def run_bg(cmd_parts):
294
404
  t.start()
295
405
  print(f"\033[96m[bg]\033[0m {' '.join(cmd_parts)}")
296
406
 
407
+ def apply_saved_env(config):
408
+ for k, v in config.get("env", {}).items(): os.environ[k] = v
409
+
297
410
  def main():
298
411
  signal.signal(signal.SIGINT, lambda *_: sys.exit(255))
299
412
  base_config = loadconf()
413
+ apply_saved_env(base_config)
300
414
  history_path = os.path.expanduser("~/.daps.history")
301
415
  user = subprocess.run(["whoami"], capture_output=True, text=True).stdout.strip()
302
416
  try:
303
417
  with open("/etc/hostname") as f: host = f.read().strip()
304
418
  except Exception: host = "daps-station"
305
419
 
420
+ startup_elapsed = time.monotonic() - _start_time
421
+ if startup_elapsed > 0.3:
422
+ print(f"\033[90mstarted in {startup_elapsed*1000:.0f}ms\033[0m")
423
+
306
424
  prev_dir = os.getcwd()
307
425
  errcode = None
308
426
  elapsed = None
309
427
  history = []
428
+ bm = get_bookmarks()
310
429
 
311
430
  def make_session(alias, theme):
431
+ kb = KeyBindings()
312
432
  return PromptSession(
313
433
  history=FileHistory(history_path),
314
434
  completer=DapsCompleter(alias), lexer=DapsLexer(alias),
315
435
  style=make_style(theme), complete_while_typing=False,
436
+ key_bindings=kb, enable_history_search=True,
316
437
  )
317
438
 
318
439
  config = load_local_conf(base_config)
@@ -328,38 +449,36 @@ def main():
328
449
  session = make_session(config.get("aliases", {}), config.get("theme", "default"))
329
450
  prev_dir = cur_dir
330
451
  alias = config.get("aliases", {})
452
+ fmt = config.get("prompt_format", "default")
331
453
  try:
332
- raw = session.prompt(FormattedText(build_prompt(user, host, errcode, elapsed, config.get("theme", "default")))).strip()
333
- except EOFError:
334
- runfc(config, "farewell"); sys.exit(0)
454
+ raw = session.prompt(FormattedText(build_prompt(user, host, errcode, elapsed, config.get("theme", "default"), fmt))).strip()
455
+ except EOFError: runfc(config, "farewell"); sys.exit(0)
335
456
  except KeyboardInterrupt: print(); continue
336
-
337
457
  if not raw: continue
338
458
 
339
459
  if raw == "!!":
340
460
  if not history: fmt_error("no previous command"); continue
341
- raw = history[-1]
342
- print(f"\033[90m{raw}\033[0m")
343
-
461
+ raw = history[-1]; print(f"\033[90m{raw}\033[0m")
344
462
  raw = re.sub(r'\bsudo !!\b', lambda _: f"sudo {history[-1]}" if history else "sudo", raw)
345
-
346
463
  history.append(raw)
347
464
  if len(history) > 1 and history[-1] == history[-2]: history.pop()
348
465
 
349
466
  bg = raw.endswith("&")
350
467
  if bg: raw = raw[:-1].strip()
351
-
352
- continuation = raw
353
- while continuation.endswith("\\"):
354
- continuation = continuation[:-1]
468
+ while raw.endswith("\\"):
469
+ raw = raw[:-1]
355
470
  try: extra = input("... ").strip()
356
471
  except (EOFError, KeyboardInterrupt): break
357
- continuation += " " + extra
358
- raw = continuation
472
+ raw += " " + extra
359
473
 
360
474
  parts = [expand_env(p) for p in raw.split()]
361
475
  cmd_base = parts[0]
362
476
  args = parts[1:]
477
+ bm = get_bookmarks()
478
+ if cmd_base in bm:
479
+ try: os.chdir(bm[cmd_base]); errcode = "0"
480
+ except Exception as e: fmt_error(str(e)); errcode = "1"
481
+ continue
363
482
 
364
483
  if cmd_base in alias:
365
484
  translated = alias[cmd_base]
@@ -370,26 +489,23 @@ def main():
370
489
 
371
490
  ccmd = cmd_parts[0]
372
491
  ccmar = cmd_parts[1:]
373
-
374
492
  t0 = time.monotonic()
375
493
 
376
494
  if ccmd == "cd":
377
- if ccmar and ccmar[0] == "-":
378
- target = prev_dir
379
- else:
380
- target = os.path.expanduser(ccmar[0] if ccmar else "~")
381
- if not do_cd_fuzzy(target):
382
- fmt_error(f"cd: no such directory: {target}"); errcode = "1"
495
+ if ccmar and ccmar[0] == "-": target = prev_dir
496
+ else: target = os.path.expanduser(ccmar[0] if ccmar else "~")
497
+ if not do_cd_fuzzy(target): fmt_error(f"cd: no such directory: {target}"); errcode = "1"
383
498
  else: errcode = "0"
384
499
  elif ccmd == "exit": runfc(config, "farewell"); sys.exit(0)
385
500
  elif ccmd == "clear": do_clear(config); errcode = "0"
386
501
  elif ccmd == "plugins": do_plugins(); errcode = "0"
502
+ elif ccmd == "help": do_help(alias); errcode = "0"
387
503
  elif ccmd == "which": errcode = do_which(ccmar, alias)
504
+ elif ccmd in ("bookmark", "bm"): errcode = do_bookmark(ccmar)
388
505
  elif ccmd == "daps":
389
506
  result = do_daps_cmd(ccmar, base_config)
390
507
  if result is None:
391
- reload_plugins(base_config)
392
- base_config = loadconf()
508
+ reload_plugins(base_config); base_config = loadconf()
393
509
  config = load_local_conf(base_config)
394
510
  session = make_session(config.get("aliases", {}), config.get("theme", "default"))
395
511
  fmt_info("reloaded"); errcode = "0"
@@ -411,5 +527,4 @@ def main():
411
527
 
412
528
  elapsed = time.monotonic() - t0
413
529
  if elapsed < 0.1: elapsed = None
414
-
415
530
  except Exception as e: fmt_error(f"unexpected shell error: {type(e).__name__}: {e}")
@@ -0,0 +1,515 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>daps :: config</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Orbitron:wght@400;700;900&display=swap" rel="stylesheet">
8
+ <style>
9
+ *{box-sizing:border-box;margin:0;padding:0}
10
+ :root{--g:#00ff41;--g2:#00cc33;--g3:#008f11;--g4:#003b00;--g5:#001a00;--bg:#000500;--bg2:#010d01;--bg3:#001500;--bg4:#000a00;--dim:#00ff4133;--dim2:#00ff4111;--red:#ff3333;--font:'Share Tech Mono',monospace;--head:'Orbitron',monospace}
11
+ html,body{height:100%;background:var(--bg);color:var(--g);font-family:var(--font);overflow:hidden}
12
+ canvas#matrix{position:fixed;top:0;left:0;width:100%;height:100%;opacity:0.06;pointer-events:none;z-index:0}
13
+ .scanline{position:fixed;top:0;left:0;width:100%;height:100%;background:repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(0,0,0,0.04) 2px,rgba(0,0,0,0.04) 4px);pointer-events:none;z-index:2;opacity:0.4}
14
+ .app{position:relative;z-index:1;display:grid;grid-template-columns:210px 1fr;grid-template-rows:46px 1fr;height:100vh}
15
+ .topbar{grid-column:1/-1;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--g4);padding:0 1.25rem;background:var(--bg4)}
16
+ .logo{font-family:var(--head);font-size:.95rem;font-weight:900;letter-spacing:4px;text-shadow:0 0 15px var(--g)}
17
+ .logo span{color:var(--g3);font-size:9px;letter-spacing:2px;margin-left:10px;font-family:var(--font)}
18
+ .topbar-right{display:flex;align-items:center;gap:8px}
19
+ .status-dot{width:6px;height:6px;border-radius:50%;background:var(--g);box-shadow:0 0 5px var(--g);animation:pulse 2s infinite}
20
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:0.3}}
21
+ .tbtn{font-family:var(--font);font-size:10px;letter-spacing:1px;padding:4px 10px;background:transparent;border:1px solid var(--g4);color:var(--g3);cursor:pointer;transition:all .2s}
22
+ .tbtn:hover{border-color:var(--g3);color:var(--g)}
23
+ .kbd-hint{font-size:8px;background:var(--bg3);border:1px solid var(--g4);padding:1px 4px;color:var(--g4);margin-left:3px}
24
+ .sidebar{border-right:1px solid var(--g4);background:var(--bg4);display:flex;flex-direction:column;overflow-y:auto}
25
+ .sidebar-label{font-size:8px;letter-spacing:3px;color:var(--g4);padding:10px 14px 3px;font-family:var(--head)}
26
+ .nav-item{display:flex;align-items:center;gap:8px;padding:10px 14px;font-size:11px;letter-spacing:2px;color:var(--g3);cursor:pointer;border-left:2px solid transparent;transition:all .15s;user-select:none}
27
+ .nav-item:hover{color:var(--g);background:var(--dim2)}
28
+ .nav-item.active{color:var(--g);border-left-color:var(--g);background:var(--dim2);text-shadow:0 0 6px var(--g)}
29
+ .nav-icon{font-size:12px;width:16px;text-align:center;opacity:.7}
30
+ .sidebar-sep{height:1px;background:var(--g4);margin:4px 0}
31
+ .undo-bar{margin-top:auto;border-top:1px solid var(--g4);padding:8px 10px;display:flex;gap:5px}
32
+ .undo-bar button{flex:1;font-family:var(--font);font-size:10px;letter-spacing:1px;padding:5px;background:transparent;border:1px solid var(--g4);color:var(--g3);cursor:pointer;transition:all .15s}
33
+ .undo-bar button:hover:not(:disabled){border-color:var(--g3);color:var(--g)}
34
+ .undo-bar button:disabled{opacity:0.25;cursor:default}
35
+ .main{overflow-y:auto;padding:1.25rem;display:flex;flex-direction:column;gap:0}
36
+ .panel{display:none}.panel.active{display:block}
37
+ .sh{font-family:var(--head);font-size:8px;letter-spacing:3px;color:var(--g3);margin-bottom:.85rem;padding-bottom:5px;border-bottom:1px solid var(--g4)}
38
+ input,textarea,select{background:var(--bg3);border:1px solid var(--g3);color:var(--g);font-family:var(--font);font-size:12px;padding:7px 10px;outline:none;transition:border-color .2s,box-shadow .2s}
39
+ input:focus,textarea:focus,select:focus{border-color:var(--g);box-shadow:0 0 7px var(--dim)}
40
+ textarea{resize:vertical;min-height:100px;line-height:1.6;width:100%}
41
+ select option{background:var(--bg)}
42
+ .fl{font-size:9px;color:var(--g3);letter-spacing:1px;margin-bottom:3px}
43
+ button,.btn{font-family:var(--font);font-size:10px;letter-spacing:2px;padding:6px 12px;background:transparent;border:1px solid var(--g3);color:var(--g);cursor:pointer;transition:all .15s}
44
+ button:hover,.btn:hover{background:var(--dim);border-color:var(--g);box-shadow:0 0 7px var(--dim)}
45
+ button:active,.btn:active{transform:scale(0.98)}
46
+ .btn-d{border-color:var(--red);color:var(--red)}.btn-d:hover{background:rgba(255,51,51,0.1);border-color:var(--red);box-shadow:none}
47
+ .btn-p{border-color:var(--g);text-shadow:0 0 5px var(--g)}
48
+ .search-row{position:relative;margin-bottom:.85rem}
49
+ .search-row input{width:100%;padding-left:22px}
50
+ .search-row::before{content:'>';position:absolute;left:8px;top:50%;transform:translateY(-50%);color:var(--g3);font-size:11px;pointer-events:none}
51
+ .alias-list{display:flex;flex-direction:column;gap:5px;margin-bottom:.85rem}
52
+ .alias-item{display:grid;grid-template-columns:130px 1fr auto auto;gap:6px;align-items:center;padding:8px 10px;background:var(--bg3);border:1px solid var(--g4);transition:border-color .15s}
53
+ .alias-item:hover{border-color:var(--g3)}
54
+ .akey{color:var(--g);font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
55
+ .aval{color:var(--g2);font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
56
+ .abtn{padding:3px 7px;font-size:9px;letter-spacing:1px}
57
+ .add-form{background:var(--bg3);border:1px solid var(--g3);padding:.85rem;margin-bottom:.85rem;box-shadow:0 0 12px var(--dim2)}
58
+ .af-grid{display:grid;grid-template-columns:1fr 2fr;gap:6px;margin-bottom:6px}
59
+ .af-btns{display:flex;gap:6px}
60
+ .plugin-item,.mkt-item{display:flex;align-items:center;justify-content:space-between;padding:9px 12px;background:var(--bg3);border:1px solid var(--g4);margin-bottom:5px;transition:border-color .15s}
61
+ .plugin-item:hover,.mkt-item:hover{border-color:var(--g3)}
62
+ .pname{font-size:12px;color:var(--g)}.pdesc{font-size:10px;color:var(--g3);margin-top:1px}
63
+ .mkt-item.inst{opacity:.6;border-color:var(--g4)}
64
+ .inst-row{display:grid;grid-template-columns:1fr auto;gap:6px;margin-bottom:1rem}
65
+ .theme-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:8px;margin-bottom:1rem}
66
+ .theme-card{padding:10px;border:1px solid var(--g4);cursor:pointer;transition:all .15s;position:relative}
67
+ .theme-card:hover{border-color:var(--g3)}.theme-card.sel{border-color:var(--g);box-shadow:0 0 10px var(--dim)}
68
+ .theme-card.sel::after{content:'ON';position:absolute;top:5px;right:6px;font-size:7px;color:var(--g)}
69
+ .tdots{display:flex;gap:3px;margin-bottom:6px}.tdot{width:9px;height:9px;border-radius:50%}
70
+ .tname{font-size:9px;letter-spacing:2px;color:var(--g2)}
71
+ .fmt-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:6px;margin-bottom:1rem}
72
+ .fmt-card{padding:8px 10px;border:1px solid var(--g4);cursor:pointer;transition:all .15s;font-size:10px;color:var(--g3);letter-spacing:1px}
73
+ .fmt-card:hover{border-color:var(--g3);color:var(--g)}.fmt-card.sel{border-color:var(--g);color:var(--g);text-shadow:0 0 5px var(--g)}
74
+ .prev{background:var(--bg4);border:1px solid var(--g3);padding:.85rem 1rem;font-size:12px;line-height:2;margin-bottom:1rem;min-height:60px}
75
+ .toggle-row{display:flex;align-items:center;justify-content:space-between;padding:9px 0;border-bottom:1px solid var(--g4)}
76
+ .toggle-row:last-child{border-bottom:none}
77
+ .tl{font-size:12px;color:var(--g2)}.ts{font-size:9px;color:var(--g3);margin-top:1px}
78
+ .toggle{position:relative;width:36px;height:18px;cursor:pointer;flex-shrink:0}
79
+ .toggle input{opacity:0;width:0;height:0}
80
+ .toggle-sl{position:absolute;inset:0;background:var(--bg3);border:1px solid var(--g3);transition:.25s}
81
+ .toggle input:checked+.toggle-sl{background:var(--g4);border-color:var(--g)}
82
+ .toggle-sl::before{content:'';position:absolute;width:12px;height:12px;left:2px;top:2px;background:var(--g3);transition:.25s}
83
+ .toggle input:checked+.toggle-sl::before{transform:translateX(18px);background:var(--g);box-shadow:0 0 4px var(--g)}
84
+ .card{background:var(--bg2);border:1px solid var(--g4);padding:1rem 1rem 1rem 1.25rem;margin-bottom:.85rem;position:relative}
85
+ .card::before{content:'';position:absolute;left:0;top:0;width:2px;height:100%;background:var(--g3)}
86
+ .json-st{font-size:10px;margin-top:5px;color:var(--g3);min-height:16px}.json-st.ok{color:var(--g)}.json-st.err{color:var(--red)}
87
+ .hist-list{display:flex;flex-direction:column;gap:4px;max-height:400px;overflow-y:auto}
88
+ .hist-item{display:flex;align-items:center;justify-content:space-between;padding:6px 10px;background:var(--bg3);border:1px solid var(--g4);font-size:11px;color:var(--g2);cursor:pointer;transition:border-color .15s}
89
+ .hist-item:hover{border-color:var(--g3);color:var(--g)}
90
+ .hist-num{font-size:9px;color:var(--g4);margin-right:8px;min-width:30px}
91
+ .bm-list{display:flex;flex-direction:column;gap:5px;margin-bottom:.85rem}
92
+ .bm-item{display:grid;grid-template-columns:120px 1fr auto auto;gap:6px;align-items:center;padding:8px 10px;background:var(--bg3);border:1px solid var(--g4)}
93
+ .bm-item:hover{border-color:var(--g3)}
94
+ .bm-name{color:var(--g);font-size:12px}.bm-path{color:var(--g3);font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
95
+ .env-list{display:flex;flex-direction:column;gap:5px;margin-bottom:.85rem}
96
+ .env-item{display:grid;grid-template-columns:150px 1fr auto;gap:6px;align-items:center;padding:7px 10px;background:var(--bg3);border:1px solid var(--g4)}
97
+ .env-item:hover{border-color:var(--g3)}
98
+ .ekey{color:var(--g);font-size:12px}.eval{color:var(--g2);font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
99
+ .log-box{background:var(--bg4);border:1px solid var(--g3);padding:.85rem;font-size:11px;line-height:1.8;min-height:300px;max-height:400px;overflow-y:auto;font-family:var(--font)}
100
+ .log-cmd{color:var(--g)}.log-out{color:var(--g3)}.log-err{color:var(--red)}.log-ts{color:var(--g4);font-size:9px}
101
+ .wizard-step{display:none}.wizard-step.active{display:block}
102
+ .wiz-progress{display:flex;gap:6px;margin-bottom:1.5rem}
103
+ .wiz-dot{width:8px;height:8px;border-radius:50%;background:var(--g4);transition:background .2s}
104
+ .wiz-dot.done{background:var(--g3)}.wiz-dot.current{background:var(--g);box-shadow:0 0 6px var(--g)}
105
+ .toast{position:fixed;bottom:1.25rem;right:1.25rem;background:var(--bg2);border:1px solid var(--g);color:var(--g);font-family:var(--font);font-size:10px;letter-spacing:2px;padding:8px 16px;z-index:100;transform:translateY(50px);opacity:0;transition:all .2s;box-shadow:0 0 12px var(--dim)}
106
+ .toast.show{transform:translateY(0);opacity:1}.toast.err{border-color:var(--red);color:var(--red)}
107
+ .kbdo{position:fixed;inset:0;background:rgba(0,5,0,0.94);z-index:50;display:none;align-items:center;justify-content:center}
108
+ .kbdo.show{display:flex}
109
+ .kbd-box{background:var(--bg2);border:1px solid var(--g3);padding:1.5rem;max-width:440px;width:90%;box-shadow:0 0 30px var(--dim)}
110
+ .kbd-box h2{font-family:var(--head);font-size:10px;letter-spacing:3px;margin-bottom:1.25rem;color:var(--g)}
111
+ .kr{display:flex;align-items:center;justify-content:space-between;padding:6px 0;border-bottom:1px solid var(--g4);font-size:11px}
112
+ .kr:last-child{border-bottom:none}.kr span{color:var(--g3)}
113
+ .kc{display:flex;gap:3px}.kk{background:var(--bg3);border:1px solid var(--g3);padding:1px 6px;font-size:9px;color:var(--g)}
114
+ </style>
115
+ </head>
116
+ <body>
117
+ <canvas id="matrix"></canvas>
118
+ <div class="scanline"></div>
119
+ <div class="app">
120
+ <div class="topbar">
121
+ <div style="display:flex;align-items:center;gap:10px">
122
+ <div class="logo">DAPS <span>// config</span></div>
123
+ <div style="display:flex;align-items:center;gap:5px"><div class="status-dot"></div><span style="font-size:9px;color:var(--g3)" id="save-st">connected</span></div>
124
+ </div>
125
+ <div class="topbar-right">
126
+ <button class="tbtn" onclick="exportCfg()">EXPORT<span class="kbd-hint">E</span></button>
127
+ <button class="tbtn" onclick="document.getElementById('imp-file').click()">IMPORT<span class="kbd-hint">I</span></button>
128
+ <input type="file" id="imp-file" accept=".json" style="display:none" onchange="importCfg(event)">
129
+ <button class="tbtn" onclick="showKbd()">?</button>
130
+ </div>
131
+ </div>
132
+ <div class="sidebar">
133
+ <div class="sidebar-label">main</div>
134
+ <div class="nav-item active" onclick="nav('aliases')"><span class="nav-icon">$</span>ALIASES</div>
135
+ <div class="nav-item" onclick="nav('plugins')"><span class="nav-icon">+</span>PLUGINS</div>
136
+ <div class="nav-item" onclick="nav('marketplace')"><span class="nav-icon">#</span>MARKETPLACE</div>
137
+ <div class="sidebar-sep"></div>
138
+ <div class="sidebar-label">customize</div>
139
+ <div class="nav-item" onclick="nav('themes')"><span class="nav-icon">~</span>THEMES</div>
140
+ <div class="nav-item" onclick="nav('settings')"><span class="nav-icon">@</span>SETTINGS</div>
141
+ <div class="sidebar-sep"></div>
142
+ <div class="sidebar-label">tools</div>
143
+ <div class="nav-item" onclick="nav('history')"><span class="nav-icon">↑</span>HISTORY</div>
144
+ <div class="nav-item" onclick="nav('bookmarks')"><span class="nav-icon">★</span>BOOKMARKS</div>
145
+ <div class="nav-item" onclick="nav('env')"><span class="nav-icon">%</span>ENV VARS</div>
146
+ <div class="nav-item" onclick="nav('log')"><span class="nav-icon">»</span>SHELL LOG</div>
147
+ <div class="sidebar-sep"></div>
148
+ <div class="sidebar-label">advanced</div>
149
+ <div class="nav-item" onclick="nav('wizard')"><span class="nav-icon">✦</span>WIZARD</div>
150
+ <div class="nav-item" onclick="nav('json')"><span class="nav-icon">{}</span>RAW JSON</div>
151
+ <div class="undo-bar">
152
+ <button id="undo-btn" onclick="undo()" disabled>↩ UNDO</button>
153
+ <button id="redo-btn" onclick="redo()" disabled>↪ REDO</button>
154
+ </div>
155
+ </div>
156
+ <div class="main">
157
+
158
+ <div id="panel-aliases" class="panel active">
159
+ <div class="sh">// alias management</div>
160
+ <div class="add-form" id="af" style="display:none">
161
+ <div class="af-grid">
162
+ <div><div class="fl">COMMAND</div><input id="nk" placeholder="ll"/></div>
163
+ <div><div class="fl">EXPANDS TO</div><input id="nv" placeholder="ls -la $*" onkeydown="if(event.key==='Enter')addAlias()"/></div>
164
+ </div>
165
+ <div class="af-btns"><button class="btn btn-p" onclick="addAlias()">SAVE</button><button class="btn" onclick="closeAF()">CANCEL</button></div>
166
+ </div>
167
+ <div style="display:flex;gap:6px;margin-bottom:.85rem">
168
+ <div class="search-row" style="flex:1"><input id="as" placeholder="search aliases..." oninput="renderAliases()"/></div>
169
+ <button class="btn btn-p" onclick="openAF()">+ NEW</button>
170
+ </div>
171
+ <div class="alias-list" id="alias-list"></div>
172
+ </div>
173
+
174
+ <div id="panel-plugins" class="panel">
175
+ <div class="sh">// installed plugins</div>
176
+ <div class="inst-row"><input id="purl" placeholder="https://example.com/plugin.py"/><button class="btn btn-p" onclick="installPlugin()">⬇ INSTALL</button></div>
177
+ <div id="plugin-list"></div>
178
+ </div>
179
+
180
+ <div id="panel-marketplace" class="panel">
181
+ <div class="sh">// dpm marketplace</div>
182
+ <div class="search-row" style="margin-bottom:.85rem"><input id="ms" placeholder="search plugins..." oninput="renderMkt()"/></div>
183
+ <div id="mkt-list"></div>
184
+ </div>
185
+
186
+ <div id="panel-themes" class="panel">
187
+ <div class="sh">// prompt preview</div>
188
+ <div class="prev" id="prev"></div>
189
+ <div class="sh">// theme</div>
190
+ <div class="theme-grid" id="theme-grid"></div>
191
+ <div class="sh">// prompt format</div>
192
+ <div class="fmt-grid" id="fmt-grid"></div>
193
+ <button class="btn btn-p" onclick="saveTheme()">APPLY</button>
194
+ </div>
195
+
196
+ <div id="panel-settings" class="panel">
197
+ <div class="sh">// shell settings</div>
198
+ <div class="card">
199
+ <div class="toggle-row"><div><div class="tl">cleargreet</div><div class="ts">re-run greeter on clear</div></div><label class="toggle"><input type="checkbox" id="tog-cg" onchange="saveSetting('cleargreet',this.checked?'yes':'no')"><div class="toggle-sl"></div></label></div>
200
+ </div>
201
+ <div class="card">
202
+ <div class="sh" style="margin-bottom:.85rem">// commands</div>
203
+ <div class="fl">GREETER</div><input id="gc" placeholder="neofetch" style="width:100%;margin-bottom:.85rem"/>
204
+ <div class="fl">FAREWELL</div><input id="fc" placeholder="echo bye" style="width:100%;margin-bottom:.85rem"/>
205
+ <button class="btn btn-p" onclick="saveCmds()">SAVE</button>
206
+ </div>
207
+ </div>
208
+
209
+ <div id="panel-history" class="panel">
210
+ <div class="sh">// command history</div>
211
+ <div class="search-row" style="margin-bottom:.85rem"><input id="hs" placeholder="search history..." oninput="renderHistory()"/></div>
212
+ <div style="display:flex;gap:6px;margin-bottom:.85rem"><button class="btn" onclick="loadHistory()">REFRESH</button><button class="btn btn-d" onclick="clearHistory()">CLEAR</button></div>
213
+ <div class="hist-list" id="hist-list"></div>
214
+ </div>
215
+
216
+ <div id="panel-bookmarks" class="panel">
217
+ <div class="sh">// directory bookmarks</div>
218
+ <div class="add-form" id="bmf" style="display:none">
219
+ <div class="af-grid">
220
+ <div><div class="fl">NAME</div><input id="bmn" placeholder="projects"/></div>
221
+ <div><div class="fl">PATH</div><input id="bmp" placeholder="~/projects" onkeydown="if(event.key==='Enter')addBm()"/></div>
222
+ </div>
223
+ <div class="af-btns"><button class="btn btn-p" onclick="addBm()">SAVE</button><button class="btn" onclick="document.getElementById('bmf').style.display='none'">CANCEL</button></div>
224
+ </div>
225
+ <div style="display:flex;justify-content:flex-end;margin-bottom:.85rem">
226
+ <button class="btn btn-p" onclick="document.getElementById('bmf').style.display='block';document.getElementById('bmn').focus()">+ NEW</button>
227
+ </div>
228
+ <div class="bm-list" id="bm-list"></div>
229
+ </div>
230
+
231
+ <div id="panel-env" class="panel">
232
+ <div class="sh">// environment variables</div>
233
+ <div class="add-form" id="envf" style="display:none">
234
+ <div class="af-grid">
235
+ <div><div class="fl">KEY</div><input id="envk" placeholder="MY_VAR"/></div>
236
+ <div><div class="fl">VALUE</div><input id="envv" placeholder="value" onkeydown="if(event.key==='Enter')addEnv()"/></div>
237
+ </div>
238
+ <div class="af-btns"><button class="btn btn-p" onclick="addEnv()">SAVE</button><button class="btn" onclick="document.getElementById('envf').style.display='none'">CANCEL</button></div>
239
+ </div>
240
+ <div style="display:flex;justify-content:flex-end;margin-bottom:.85rem">
241
+ <button class="btn btn-p" onclick="document.getElementById('envf').style.display='block';document.getElementById('envk').focus()">+ NEW</button>
242
+ </div>
243
+ <div class="env-list" id="env-list"></div>
244
+ </div>
245
+
246
+ <div id="panel-log" class="panel">
247
+ <div class="sh">// shell log</div>
248
+ <div style="display:flex;gap:6px;margin-bottom:.85rem"><button class="btn" onclick="loadLog()">REFRESH</button><button class="btn btn-d" onclick="clearLog()">CLEAR</button></div>
249
+ <div class="log-box" id="log-box">// no log entries yet — run some commands!</div>
250
+ </div>
251
+
252
+ <div id="panel-wizard" class="panel">
253
+ <div class="sh">// setup wizard</div>
254
+ <div class="wiz-progress" id="wiz-prog"></div>
255
+ <div id="wiz-steps"></div>
256
+ </div>
257
+
258
+ <div id="panel-json" class="panel">
259
+ <div class="sh">// raw config</div>
260
+ <div style="display:flex;gap:6px;margin-bottom:.85rem">
261
+ <button class="btn" onclick="exportCfg()">⬆ EXPORT</button>
262
+ <button class="btn" onclick="document.getElementById('imp-file').click()">⬇ IMPORT</button>
263
+ </div>
264
+ <textarea id="json-raw" spellcheck="false" oninput="validateJson()" style="min-height:350px"></textarea>
265
+ <div class="json-st" id="json-st"></div>
266
+ <div style="display:flex;gap:6px;margin-top:8px">
267
+ <button class="btn btn-p" onclick="saveJson()">SAVE</button>
268
+ <button class="btn" onclick="loadAll()">RELOAD</button>
269
+ </div>
270
+ </div>
271
+
272
+ </div>
273
+ </div>
274
+
275
+ <div class="toast" id="toast"></div>
276
+ <div class="kbdo" id="kbdo" onclick="hideKbd()">
277
+ <div class="kbd-box" onclick="event.stopPropagation()">
278
+ <h2>// KEYBOARD SHORTCUTS</h2>
279
+ <div class="kr"><span>new alias</span><div class="kc"><span class="kk">N</span></div></div>
280
+ <div class="kr"><span>search aliases</span><div class="kc"><span class="kk">/</span></div></div>
281
+ <div class="kr"><span>undo / redo</span><div class="kc"><span class="kk">Ctrl</span><span class="kk">Z</span></div><div class="kc"><span class="kk">Ctrl</span><span class="kk">Y</span></div></div>
282
+ <div class="kr"><span>export</span><div class="kc"><span class="kk">E</span></div></div>
283
+ <div class="kr"><span>import</span><div class="kc"><span class="kk">I</span></div></div>
284
+ <div class="kr"><span>navigate 1-9</span><div class="kc"><span class="kk">1</span><span class="kk">…</span><span class="kk">9</span></div></div>
285
+ <div class="kr"><span>close</span><div class="kc"><span class="kk">Esc</span></div></div>
286
+ <div style="margin-top:1rem;text-align:right"><button class="btn" onclick="hideKbd()">CLOSE</button></div>
287
+ </div>
288
+ </div>
289
+
290
+ <script>
291
+ const THEMES={
292
+ default:{name:'DEFAULT',colors:['#00d787','#5fafff','#00d7af','#d7af5f'],css:{u:'#00d787',h:'#5fafff',p:'#00d7af',b:'#d7af5f',a:'#00d787',e:'#ff5f5f',t:'#888'}},
293
+ fire:{name:'FIRE',colors:['#ff5f00','#ff8700','#ffaf00','#ffaf5f'],css:{u:'#ff5f00',h:'#ff8700',p:'#ffaf00',b:'#ffaf5f',a:'#ff5f00',e:'#ff0000',t:'#888'}},
294
+ ocean:{name:'OCEAN',colors:['#0087ff','#00afff','#00d7ff','#5fafff'],css:{u:'#0087ff',h:'#00afff',p:'#00d7ff',b:'#5fafff',a:'#0087ff',e:'#ff5f5f',t:'#888'}},
295
+ candy:{name:'CANDY',colors:['#ff5faf','#af5fff','#ff87d7','#ffaf5f'],css:{u:'#ff5faf',h:'#af5fff',p:'#ff87d7',b:'#ffaf5f',a:'#ff5faf',e:'#ff0000',t:'#888'}},
296
+ mono:{name:'MONO',colors:['#fff','#aaa','#fff','#aaa'],css:{u:'#fff',h:'#aaa',p:'#fff',b:'#aaa',a:'#fff',e:'#ff5f5f',t:'#555'}},
297
+ cyberpunk:{name:'CYBERPUNK',colors:['#ffff00','#ff00ff','#00ffff','#ff00ff'],css:{u:'#ffff00',h:'#ff00ff',p:'#00ffff',b:'#ff00ff',a:'#ffff00',e:'#ff0000',t:'#888'}},
298
+ nord:{name:'NORD',colors:['#88c0d0','#81a1c1','#8fbcbb','#ebcb8b'],css:{u:'#88c0d0',h:'#81a1c1',p:'#8fbcbb',b:'#ebcb8b',a:'#88c0d0',e:'#bf616a',t:'#616e88'}},
299
+ dracula:{name:'DRACULA',colors:['#ff79c6','#bd93f9','#50fa7b','#f1fa8c'],css:{u:'#ff79c6',h:'#bd93f9',p:'#50fa7b',b:'#f1fa8c',a:'#ff79c6',e:'#ff5555',t:'#44475a'}},
300
+ solarized:{name:'SOLARIZED',colors:['#859900','#268bd2','#2aa198','#b58900'],css:{u:'#859900',h:'#268bd2',p:'#2aa198',b:'#b58900',a:'#859900',e:'#dc322f',t:'#657b83'}},
301
+ blood:{name:'BLOOD',colors:['#ff0000','#aa0000','#ff3333','#ff6666'],css:{u:'#ff0000',h:'#aa0000',p:'#ff3333',b:'#ff6666',a:'#ff0000',e:'#fff',t:'#661111'}},
302
+ ice:{name:'ICE',colors:['#fff','#00ffff','#aaffff','#00ffff'],css:{u:'#fff',h:'#00ffff',p:'#aaffff',b:'#00ffff',a:'#fff',e:'#ff5f5f',t:'#557777'}},
303
+ };
304
+ const FMTS={default:'default',minimal:'minimal',compact:'compact',classic:'classic'};
305
+ const MKT=[
306
+ {name:'dpm',desc:'plugin manager — install from registry',url:'https://raw.githubusercontent.com/daps-shell/plugins/main/dpm.py'},
307
+ {name:'gitstatus',desc:'enhanced git info with diff counts',url:'https://raw.githubusercontent.com/daps-shell/plugins/main/gitstatus.py'},
308
+ {name:'weather',desc:'local weather in your terminal',url:'https://raw.githubusercontent.com/daps-shell/plugins/main/weather.py'},
309
+ {name:'calc',desc:'quick math — calc 2+2',url:'https://raw.githubusercontent.com/daps-shell/plugins/main/calc.py'},
310
+ {name:'note',desc:'quick notes from anywhere',url:'https://raw.githubusercontent.com/daps-shell/plugins/main/note.py'},
311
+ {name:'timer',desc:'countdown timer in the shell',url:'https://raw.githubusercontent.com/daps-shell/plugins/main/timer.py'},
312
+ {name:'todo',desc:'simple task list in the terminal',url:'https://raw.githubusercontent.com/daps-shell/plugins/main/todo.py'},
313
+ {name:'sysinfo',desc:'display system info on demand',url:'https://raw.githubusercontent.com/daps-shell/plugins/main/sysinfo.py'},
314
+ ];
315
+ const WIZARD_STEPS=[
316
+ {title:'welcome',html:'<p style="color:var(--g2);font-size:13px;line-height:1.8">Welcome to daps! This wizard will help you set up your shell.<br><br>Use the buttons below to step through setup.</p>'},
317
+ {title:'username & host',html:'<div class="fl">DISPLAY NAME (optional override)</div><input id="wiz-user" placeholder="akaime" style="width:100%;margin-bottom:.85rem"/><div class="fl">HOSTNAME OVERRIDE</div><input id="wiz-host" placeholder="Jupiter" style="width:100%"/>'},
318
+ {title:'greeter command',html:'<div class="fl">GREETER COMMAND</div><input id="wiz-greeter" placeholder="neofetch" style="width:100%;margin-bottom:.5rem"/><div style="font-size:10px;color:var(--g3)">runs on startup — try neofetch, fastfetch, or echo</div>'},
319
+ {title:'pick a theme',html:'<div id="wiz-theme-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(100px,1fr));gap:6px"></div>'},
320
+ {title:'done!',html:'<p style="color:var(--g2);font-size:13px;line-height:1.8">All set! Your config has been saved.<br><br>Run <span style="color:var(--g)">daps reload</span> in your terminal to apply changes.</p>'},
321
+ ];
322
+
323
+ let cfg={aliases:{},cleargreet:'no',theme:'default'},selTheme='default',selFmt='default';
324
+ let undoStack=[],redoStack=[],editKey=null,wizStep=0,logEntries=[];
325
+
326
+ async function api(p,m='GET',b=null){
327
+ const o={method:m,headers:{'Content-Type':'application/json'}};
328
+ if(b)o.body=JSON.stringify(b);
329
+ return (await fetch(p,o)).json();
330
+ }
331
+ function toast(msg,err=false){
332
+ const t=document.getElementById('toast');t.textContent=msg;t.className='toast'+(err?' err':'');
333
+ t.classList.add('show');clearTimeout(t._t);t._t=setTimeout(()=>t.classList.remove('show'),2200);
334
+ }
335
+ function setSt(s){document.getElementById('save-st').textContent=s}
336
+ function nav(name){
337
+ const names=['aliases','plugins','marketplace','themes','settings','history','bookmarks','env','log','wizard','json'];
338
+ document.querySelectorAll('.nav-item').forEach((el,i)=>el.classList.toggle('active',names[i]===name));
339
+ document.querySelectorAll('.panel').forEach(p=>p.classList.remove('active'));
340
+ document.getElementById('panel-'+name).classList.add('active');
341
+ if(name==='themes')updatePrev();
342
+ if(name==='history')loadHistory();
343
+ if(name==='bookmarks')renderBm();
344
+ if(name==='env')renderEnv();
345
+ if(name==='log')loadLog();
346
+ if(name==='wizard')initWizard();
347
+ }
348
+ function pushU(){undoStack.push(JSON.stringify(cfg));if(undoStack.length>50)undoStack.shift();redoStack=[];updUndoBtns()}
349
+ function undo(){if(!undoStack.length)return;redoStack.push(JSON.stringify(cfg));cfg=JSON.parse(undoStack.pop());updUndoBtns();renderAll();saveConfig(true);toast('undone')}
350
+ function redo(){if(!redoStack.length)return;undoStack.push(JSON.stringify(cfg));cfg=JSON.parse(redoStack.pop());updUndoBtns();renderAll();saveConfig(true);toast('redone')}
351
+ function updUndoBtns(){document.getElementById('undo-btn').disabled=!undoStack.length;document.getElementById('redo-btn').disabled=!redoStack.length}
352
+ function renderAll(){renderAliases();renderPlugins();renderThemes();renderFmts();renderSettings();updateJsonEditor()}
353
+ function renderAliases(){
354
+ const list=document.getElementById('alias-list'),q=document.getElementById('as').value.toLowerCase();
355
+ const entries=Object.entries(cfg.aliases||{}).filter(([k,v])=>!q||k.includes(q)||v.toLowerCase().includes(q));
356
+ list.innerHTML=entries.length?entries.map(([k,v])=>`<div class="alias-item"><div class="akey">${k}</div><div class="aval">${v}</div><button class="btn abtn" onclick="editAlias('${k}')">EDIT</button><button class="btn btn-d abtn" onclick="delAlias('${k}')">DEL</button></div>`).join(''):'<div style="color:var(--g3);font-size:11px;padding:.85rem 0">// no aliases'+(q?' matching':'')+'</div>';
357
+ }
358
+ function openAF(k='',v=''){editKey=k||null;document.getElementById('nk').value=k;document.getElementById('nv').value=v;document.getElementById('af').style.display='block';document.getElementById(k?'nv':'nk').focus()}
359
+ function closeAF(){document.getElementById('af').style.display='none';editKey=null}
360
+ function editAlias(k){openAF(k,cfg.aliases[k])}
361
+ async function addAlias(){
362
+ const k=document.getElementById('nk').value.trim(),v=document.getElementById('nv').value.trim();
363
+ if(!k||!v){toast('key and value required',true);return}
364
+ pushU();if(editKey&&editKey!==k)delete cfg.aliases[editKey];
365
+ cfg.aliases[k]=v;await saveConfig();renderAliases();closeAF();toast('alias saved');
366
+ }
367
+ async function delAlias(k){pushU();delete cfg.aliases[k];await saveConfig();renderAliases();toast(`'${k}' removed`)}
368
+ function renderPlugins(){
369
+ const list=document.getElementById('plugin-list'),plugins=cfg._plugins||[];
370
+ list.innerHTML=plugins.length?plugins.map(p=>`<div class="plugin-item"><div><div class="pname">${p.name}</div><div class="pdesc">${p.desc||'no description'}</div></div><button class="btn btn-d" style="font-size:9px;padding:4px 8px" onclick="removePlugin('${p.name}')">REMOVE</button></div>`).join(''):'<div style="color:var(--g3);font-size:11px;padding:.85rem 0">// no plugins loaded</div>';
371
+ }
372
+ async function installPlugin(){
373
+ const url=document.getElementById('purl').value.trim();if(!url)return;
374
+ setSt('installing...');const r=await api('/api/plugin/install','POST',{url});setSt('connected');
375
+ if(r.ok){toast('installed');document.getElementById('purl').value='';loadAll();}else toast(r.error||'failed',true);
376
+ }
377
+ async function removePlugin(name){const r=await api('/api/plugin/remove','POST',{name});if(r.ok){toast('removed');loadAll();}else toast('failed',true)}
378
+ function renderMkt(){
379
+ const list=document.getElementById('mkt-list'),q=document.getElementById('ms').value.toLowerCase();
380
+ const inst=(cfg._plugins||[]).map(p=>p.name);
381
+ const items=MKT.filter(p=>!q||p.name.includes(q)||p.desc.toLowerCase().includes(q));
382
+ list.innerHTML=items.map(p=>{const isI=inst.includes(p.name);return`<div class="mkt-item${isI?' inst':''}"><div><div class="pname">${p.name}${isI?' <span style="font-size:9px;color:var(--g3)">[installed]</span>':''}</div><div class="pdesc">${p.desc}</div></div><button class="btn${isI?' btn-d':' btn-p'}" style="font-size:9px;white-space:nowrap" onclick="${isI?`removePlugin('${p.name}')`:`instFromMkt('${p.url}')`}">${isI?'REMOVE':'INSTALL'}</button></div>`}).join('');
383
+ }
384
+ async function instFromMkt(url){document.getElementById('purl').value=url;await installPlugin();renderMkt()}
385
+ function renderThemes(){
386
+ document.getElementById('theme-grid').innerHTML=Object.entries(THEMES).map(([k,t])=>`<div class="theme-card${k===selTheme?' sel':''}" onclick="selT('${k}')"><div class="tdots">${t.colors.map(c=>`<div class="tdot" style="background:${c};box-shadow:0 0 3px ${c}66"></div>`).join('')}</div><div class="tname">${t.name}</div></div>`).join('');
387
+ updatePrev();
388
+ }
389
+ function renderFmts(){
390
+ document.getElementById('fmt-grid').innerHTML=Object.keys(FMTS).map(k=>`<div class="fmt-card${k===selFmt?' sel':''}" onclick="selF('${k}')">${k.toUpperCase()}</div>`).join('');
391
+ }
392
+ function selT(n){selTheme=n;renderThemes()}
393
+ function selF(n){selFmt=n;renderFmts();updatePrev()}
394
+ function updatePrev(){
395
+ const c=THEMES[selTheme]?.css||THEMES.default.css;
396
+ const p=document.getElementById('prev');if(!p)return;
397
+ const fmts={
398
+ default:`<span style="color:${c.t}">[1.2s]</span> - <span style="color:${c.u}">user</span>@<span style="color:${c.h}">hostname</span> <span style="color:${c.u}>($)</span> - <span style="color:${c.p}">~/projects</span> <span style="color:${c.b}">[main*]</span><br><span style="color:${c.a}">></span> ls`,
399
+ minimal:`<span style="color:${c.u}">user</span> <span style="color:${c.p}">~/projects</span> <span style="color:${c.b}">[main]</span><br><span style="color:${c.a}">></span> ls`,
400
+ compact:`<span style="color:${c.u}">$</span> <span style="color:${c.p}">~/projects</span> <span style="color:${c.b}">[main]</span><br><span style="color:${c.a}">></span> ls`,
401
+ classic:`<span style="color:var(--g3)">[</span><span style="color:${c.u}">user</span>@<span style="color:${c.h}">hostname</span> <span style="color:${c.p}">~/projects</span><span style="color:var(--g3)">]</span><span style="color:${c.b}">(main)</span><span style="color:${c.u}">$</span> ls`,
402
+ };
403
+ p.innerHTML=fmts[selFmt]||fmts.default;
404
+ }
405
+ async function saveTheme(){pushU();cfg.theme=selTheme;cfg.prompt_format=selFmt;await saveConfig();toast(`theme: ${selTheme} / format: ${selFmt}`)}
406
+ function renderSettings(){document.getElementById('tog-cg').checked=cfg.cleargreet==='yes';document.getElementById('gc').value=cfg.greeter||'';document.getElementById('fc').value=cfg.farewell||''}
407
+ async function saveSetting(k,v){pushU();cfg[k]=v;await saveConfig();toast(`${k} updated`)}
408
+ async function saveCmds(){pushU();const g=document.getElementById('gc').value.trim(),f=document.getElementById('fc').value.trim();if(g)cfg.greeter=g;else delete cfg.greeter;if(f)cfg.farewell=f;else delete cfg.farewell;await saveConfig();toast('saved')}
409
+ async function loadHistory(){
410
+ const r=await api('/api/history');
411
+ const lines=(r.lines||[]).filter(Boolean);
412
+ renderHistory(lines);
413
+ }
414
+ function renderHistory(lines){
415
+ if(!lines){lines=(document.getElementById('hist-list')._cache||[])}
416
+ document.getElementById('hist-list')._cache=lines;
417
+ const q=document.getElementById('hs').value.toLowerCase();
418
+ const filtered=lines.filter(l=>!q||l.toLowerCase().includes(q));
419
+ document.getElementById('hist-list').innerHTML=filtered.length?filtered.slice(-200).reverse().map((l,i)=>`<div class="hist-item" onclick="copyHist('${l.replace(/'/g,"\\'")}')"><span class="hist-num">${filtered.length-i}</span><span>${l}</span></div>`).join(''):'<div style="color:var(--g3);font-size:11px;padding:.85rem 0">// no history</div>';
420
+ }
421
+ function copyHist(cmd){navigator.clipboard.writeText(cmd).then(()=>toast('copied to clipboard'))}
422
+ async function clearHistory(){const r=await api('/api/history/clear','POST');if(r.ok){toast('history cleared');loadHistory()}else toast('failed',true)}
423
+ function renderBm(){
424
+ const bm=cfg.bookmarks||{};
425
+ document.getElementById('bm-list').innerHTML=Object.entries(bm).length?Object.entries(bm).map(([k,v])=>`<div class="bm-item"><div class="bm-name">${k}</div><div class="bm-path">${v}</div><button class="btn abtn" onclick="">EDIT</button><button class="btn btn-d abtn" onclick="delBm('${k}')">DEL</button></div>`).join(''):'<div style="color:var(--g3);font-size:11px;padding:.85rem 0">// no bookmarks</div>';
426
+ }
427
+ async function addBm(){
428
+ const n=document.getElementById('bmn').value.trim(),p=document.getElementById('bmp').value.trim();
429
+ if(!n||!p){toast('name and path required',true);return}
430
+ pushU();cfg.bookmarks=cfg.bookmarks||{};cfg.bookmarks[n]=p;await saveConfig();renderBm();document.getElementById('bmf').style.display='none';toast('bookmark saved');
431
+ }
432
+ async function delBm(k){pushU();delete cfg.bookmarks[k];await saveConfig();renderBm();toast('removed')}
433
+ function renderEnv(){
434
+ const env=cfg.env||{};
435
+ document.getElementById('env-list').innerHTML=Object.entries(env).length?Object.entries(env).map(([k,v])=>`<div class="env-item"><div class="ekey">${k}</div><div class="eval">${v}</div><button class="btn btn-d abtn" onclick="delEnv('${k}')">DEL</button></div>`).join(''):'<div style="color:var(--g3);font-size:11px;padding:.85rem 0">// no env vars set</div>';
436
+ }
437
+ async function addEnv(){
438
+ const k=document.getElementById('envk').value.trim(),v=document.getElementById('envv').value.trim();
439
+ if(!k||!v){toast('key and value required',true);return}
440
+ pushU();cfg.env=cfg.env||{};cfg.env[k]=v;await saveConfig();renderEnv();document.getElementById('envf').style.display='none';toast('env var saved');
441
+ }
442
+ async function delEnv(k){pushU();delete cfg.env[k];await saveConfig();renderEnv();toast('removed')}
443
+ async function loadLog(){
444
+ const r=await api('/api/log');
445
+ const entries=r.entries||[];
446
+ const box=document.getElementById('log-box');
447
+ if(!entries.length){box.innerHTML='// no log entries yet — run some commands!';return}
448
+ box.innerHTML=entries.slice(-100).reverse().map(e=>`<div><span class="log-ts">${e.ts||''}</span> <span class="log-cmd">> ${e.cmd||''}</span>${e.code&&e.code!=='0'?` <span class="log-err">[${e.code}]</span>`:''}</div>`).join('');
449
+ }
450
+ async function clearLog(){const r=await api('/api/log/clear','POST');if(r.ok){toast('log cleared');loadLog()}else toast('failed',true)}
451
+ function initWizard(){
452
+ wizStep=0;
453
+ const prog=document.getElementById('wiz-prog');
454
+ prog.innerHTML=WIZARD_STEPS.map((_,i)=>`<div class="wiz-dot${i===0?' current':''}" id="wd${i}"></div>`).join('');
455
+ renderWizStep();
456
+ }
457
+ function renderWizStep(){
458
+ const s=WIZARD_STEPS[wizStep];
459
+ document.getElementById('wiz-steps').innerHTML=`<div class="sh">// ${s.title}</div><div style="margin-bottom:1.25rem">${s.html}</div><div style="display:flex;gap:6px">${wizStep>0?'<button class="btn" onclick="wizNav(-1)">BACK</button>':''}<button class="btn btn-p" onclick="wizNav(1)">${wizStep===WIZARD_STEPS.length-1?'FINISH':'NEXT'}</button></div>`;
460
+ if(wizStep===3){
461
+ document.getElementById('wiz-theme-grid').innerHTML=Object.entries(THEMES).map(([k,t])=>`<div class="theme-card${k===selTheme?' sel':''}" onclick="selT('${k}');document.querySelectorAll('#wiz-theme-grid .theme-card').forEach(c=>c.classList.remove('sel'));this.classList.add('sel')"><div class="tdots">${t.colors.map(c=>`<div class="tdot" style="background:${c}"></div>`).join('')}</div><div class="tname" style="font-size:8px">${t.name}</div></div>`).join('');
462
+ }
463
+ WIZARD_STEPS.forEach((_,i)=>{const d=document.getElementById('wd'+i);if(d){d.className='wiz-dot'+(i<wizStep?' done':i===wizStep?' current':'')}});
464
+ }
465
+ async function wizNav(dir){
466
+ if(dir===1){
467
+ if(wizStep===1){const u=document.getElementById('wiz-user')?.value.trim(),h=document.getElementById('wiz-host')?.value.trim();if(h){pushU();cfg._hostname_hint=h;}}
468
+ if(wizStep===2){const g=document.getElementById('wiz-greeter')?.value.trim();if(g){pushU();cfg.greeter=g;cfg.cleargreet='yes';}}
469
+ if(wizStep===3){pushU();cfg.theme=selTheme;}
470
+ if(wizStep===WIZARD_STEPS.length-1){await saveConfig();toast('config saved!');nav('aliases');return}
471
+ }
472
+ wizStep=Math.max(0,Math.min(WIZARD_STEPS.length-1,wizStep+dir));
473
+ renderWizStep();
474
+ }
475
+ function updateJsonEditor(){const s={...cfg};delete s._plugins;document.getElementById('json-raw').value=JSON.stringify(s,null,2);validateJson()}
476
+ function validateJson(){const el=document.getElementById('json-st');try{JSON.parse(document.getElementById('json-raw').value);el.textContent='// valid json';el.className='json-st ok'}catch(e){el.textContent='// '+e.message;el.className='json-st err'}}
477
+ async function saveJson(){try{const p=JSON.parse(document.getElementById('json-raw').value);pushU();cfg=p;await saveConfig();toast('saved');loadAll()}catch(e){toast('invalid json',true)}}
478
+ async function saveConfig(silent=false){
479
+ const s={...cfg};delete s._plugins;setSt('saving...');
480
+ try{const r=await api('/api/config','POST',s);setSt(r.ok?'saved':'error');setTimeout(()=>setSt('connected'),1400);if(!r.ok&&!silent)toast('save failed',true)}catch(e){setSt('error')}
481
+ updateJsonEditor();
482
+ }
483
+ function exportCfg(){const s={...cfg};delete s._plugins;const a=document.createElement('a');a.href=URL.createObjectURL(new Blob([JSON.stringify(s,null,2)],{type:'application/json'}));a.download='daps-config.json';a.click();toast('exported')}
484
+ function importCfg(e){const f=e.target.files[0];if(!f)return;const r=new FileReader();r.onload=async ev=>{try{const p=JSON.parse(ev.target.result);pushU();cfg={...cfg,...p};await saveConfig();renderAll();toast('imported')}catch(e){toast('invalid json',true)}};r.readAsText(f);e.target.value=''}
485
+ function showKbd(){document.getElementById('kbdo').classList.add('show')}
486
+ function hideKbd(){document.getElementById('kbdo').classList.remove('show')}
487
+ document.addEventListener('keydown',e=>{
488
+ const tag=document.activeElement.tagName,typing=tag==='INPUT'||tag==='TEXTAREA';
489
+ if(e.key==='Escape'){hideKbd();closeAF();return}
490
+ if(e.ctrlKey&&e.key==='z'){e.preventDefault();undo();return}
491
+ if(e.ctrlKey&&e.key==='y'){e.preventDefault();redo();return}
492
+ if(typing)return;
493
+ if(e.key==='?'){showKbd();return}
494
+ if(e.key==='n'||e.key==='N'){openAF();return}
495
+ if(e.key==='/'){nav('aliases');setTimeout(()=>document.getElementById('as').focus(),50);return}
496
+ if(e.key==='e'||e.key==='E'){exportCfg();return}
497
+ if(e.key==='i'||e.key==='I'){document.getElementById('imp-file').click();return}
498
+ const nm=['aliases','plugins','marketplace','themes','settings','history','bookmarks','env','log','wizard','json'];
499
+ const i=parseInt(e.key)-1;if(i>=0&&i<nm.length)nav(nm[i]);
500
+ });
501
+ async function loadAll(){
502
+ try{const r=await api('/api/config');cfg=r;selTheme=cfg.theme||'default';selFmt=cfg.prompt_format||'default';renderAll();updUndoBtns()}
503
+ catch(e){toast('could not connect',true)}
504
+ }
505
+ (function(){
506
+ const c=document.getElementById('matrix'),ctx=c.getContext('2d');let w,h,cols,drops;
507
+ const chars='アイウエオカキクケコサシスセソ0123456789ABCDEF'.split('');
508
+ function resize(){w=c.width=innerWidth;h=c.height=innerHeight;cols=Math.floor(w/15);drops=Array(cols).fill(1)}
509
+ resize();window.addEventListener('resize',resize);
510
+ setInterval(()=>{ctx.fillStyle='rgba(0,5,0,0.05)';ctx.fillRect(0,0,w,h);ctx.fillStyle='#00ff41';ctx.font='12px Share Tech Mono';drops.forEach((y,i)=>{ctx.fillText(chars[Math.floor(Math.random()*chars.length)],i*15,y*15);if(y*15>h&&Math.random()>.975)drops[i]=0;drops[i]++})},50);
511
+ })();
512
+ loadAll();
513
+ </script>
514
+ </body>
515
+ </html>
@@ -0,0 +1,130 @@
1
+ import os, json, threading, webbrowser, importlib.util, datetime
2
+ from http.server import HTTPServer, BaseHTTPRequestHandler
3
+ from pathlib import Path
4
+
5
+ CONF = Path.home() / ".config" / "daps"
6
+ CONF_FILE = CONF / "config.json"
7
+ PLUGIN_DIR = CONF / "plugins"
8
+ LOG_FILE = CONF / "shell.log"
9
+ HISTORY_FILE = Path.home() / ".daps.history"
10
+ HTML = Path(__file__).parent / "ui.html"
11
+
12
+ def read_config():
13
+ if not CONF_FILE.exists(): return {"aliases":{}, "cleargreet":"no"}
14
+ with open(CONF_FILE) as f: return json.load(f)
15
+
16
+ def write_config(data):
17
+ with open(CONF_FILE,"w") as f: json.dump(data, f, indent=2)
18
+
19
+ def get_plugins():
20
+ plugins = []
21
+ if not PLUGIN_DIR.exists(): return plugins
22
+ for f in PLUGIN_DIR.iterdir():
23
+ if f.suffix != ".py": continue
24
+ desc = "no description"
25
+ try:
26
+ spec = importlib.util.spec_from_file_location(f.stem, f)
27
+ mod = importlib.util.module_from_spec(spec)
28
+ spec.loader.exec_module(mod)
29
+ doc = getattr(mod, "__doc__", None) or (mod.run.__doc__ if hasattr(mod, "run") else None)
30
+ if doc: desc = doc.strip()
31
+ except Exception: pass
32
+ plugins.append({"name": f.stem, "desc": desc, "file": str(f)})
33
+ return plugins
34
+
35
+ def install_plugin(url):
36
+ import urllib.request
37
+ name = url.split("/")[-1]
38
+ if not name.endswith(".py"): name += ".py"
39
+ dest = PLUGIN_DIR / name
40
+ try: urllib.request.urlretrieve(url, dest); return True, None
41
+ except Exception as e: return False, str(e)
42
+
43
+ def remove_plugin(name):
44
+ f = PLUGIN_DIR / f"{name}.py"
45
+ if f.exists(): f.unlink(); return True
46
+ return False
47
+
48
+ def read_history():
49
+ if not HISTORY_FILE.exists(): return []
50
+ try:
51
+ lines = HISTORY_FILE.read_text().splitlines()
52
+ return [l for l in lines if l.strip() and not l.startswith("+")]
53
+ except Exception: return []
54
+
55
+ def clear_history():
56
+ if HISTORY_FILE.exists(): HISTORY_FILE.write_text("")
57
+ return True
58
+
59
+ def read_log():
60
+ if not LOG_FILE.exists(): return []
61
+ try:
62
+ entries = []
63
+ for line in LOG_FILE.read_text().splitlines():
64
+ try: entries.append(json.loads(line))
65
+ except Exception: pass
66
+ return entries
67
+ except Exception: return []
68
+
69
+ def clear_log():
70
+ if LOG_FILE.exists(): LOG_FILE.write_text("")
71
+ return True
72
+
73
+ class Handler(BaseHTTPRequestHandler):
74
+ def log_message(self, *_): pass
75
+
76
+ def send_json(self, data, code=200):
77
+ body = json.dumps(data).encode()
78
+ self.send_response(code)
79
+ self.send_header("Content-Type","application/json")
80
+ self.send_header("Content-Length",len(body))
81
+ self.send_header("Access-Control-Allow-Origin","*")
82
+ self.end_headers()
83
+ self.wfile.write(body)
84
+
85
+ def send_html(self):
86
+ body = HTML.read_bytes()
87
+ self.send_response(200)
88
+ self.send_header("Content-Type","text/html")
89
+ self.send_header("Content-Length",len(body))
90
+ self.end_headers()
91
+ self.wfile.write(body)
92
+
93
+ def do_GET(self):
94
+ if self.path in ("/","/index.html"): self.send_html()
95
+ elif self.path == "/api/config":
96
+ cfg = read_config(); cfg["_plugins"] = get_plugins(); self.send_json(cfg)
97
+ elif self.path == "/api/history": self.send_json({"lines": read_history()})
98
+ elif self.path == "/api/log": self.send_json({"entries": read_log()})
99
+ else: self.send_json({"error":"not found"},404)
100
+
101
+ def do_POST(self):
102
+ length = int(self.headers.get("Content-Length",0))
103
+ body = json.loads(self.rfile.read(length)) if length else {}
104
+ if self.path == "/api/config":
105
+ body.pop("_plugins",None); write_config(body); self.send_json({"ok":True})
106
+ elif self.path == "/api/plugin/install":
107
+ ok, err = install_plugin(body.get("url","")); self.send_json({"ok":ok,"error":err})
108
+ elif self.path == "/api/plugin/remove":
109
+ self.send_json({"ok": remove_plugin(body.get("name",""))})
110
+ elif self.path == "/api/history/clear":
111
+ self.send_json({"ok": clear_history()})
112
+ elif self.path == "/api/log/clear":
113
+ self.send_json({"ok": clear_log()})
114
+ else: self.send_json({"error":"not found"},404)
115
+
116
+ def do_OPTIONS(self):
117
+ self.send_response(200)
118
+ self.send_header("Access-Control-Allow-Origin","*")
119
+ self.send_header("Access-Control-Allow-Methods","GET, POST, OPTIONS")
120
+ self.send_header("Access-Control-Allow-Headers","Content-Type")
121
+ self.end_headers()
122
+
123
+ def launch(port=6473):
124
+ server = HTTPServer(("127.0.0.1", port), Handler)
125
+ url = f"http://127.0.0.1:{port}"
126
+ print(f"\033[96mdaps\033[0m config ui → {url}")
127
+ print(f"\033[96mdaps\033[0m ctrl+c to stop")
128
+ threading.Timer(0.3, lambda: webbrowser.open(url)).start()
129
+ try: server.serve_forever()
130
+ except KeyboardInterrupt: print("\n\033[96mdaps\033[0m stopped")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: daps-shell
3
- Version: 0.1.7
3
+ Version: 0.2.3
4
4
  Summary: A customisable Python shell with plugins, aliases, and syntax highlighting
5
5
  License: MIT
6
6
  Requires-Python: >=3.11
@@ -4,6 +4,8 @@ pyproject.toml
4
4
  daps/__init__.py
5
5
  daps/__main__.py
6
6
  daps/shell.py
7
+ daps/ui.html
8
+ daps/ui.py
7
9
  daps_shell.egg-info/PKG-INFO
8
10
  daps_shell.egg-info/SOURCES.txt
9
11
  daps_shell.egg-info/dependency_links.txt
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "daps-shell"
7
- version = "0.1.7"
7
+ version = "0.2.3"
8
8
  description = "A customisable Python shell with plugins, aliases, and syntax highlighting"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -19,3 +19,6 @@ daps = "daps.__main__:main"
19
19
  [tool.setuptools.packages.find]
20
20
  where = ["."]
21
21
  include = ["daps*"]
22
+
23
+ [tool.setuptools.package-data]
24
+ daps = ["*.html"]
File without changes
File without changes
File without changes
File without changes
File without changes