@fiete/drift 1.0.1 → 1.0.2

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.
package/README.md CHANGED
@@ -13,25 +13,9 @@ npm install -g @brechtknecht/drift
13
13
  ```bash
14
14
  drift # open the TUI browser
15
15
  drift add # add a new command interactively
16
- drift add "git log --oneline -20" # pre-fill the command
17
- drift add "cd /project && npm run dev" --desc "Start dev" --tags "node,dev" # save without TUI
18
- drift remove <id>
16
+ drift add "git log --oneline -20" # pre-fill the command
19
17
  ```
20
18
 
21
- ## TUI controls
22
-
23
- | Key | Action |
24
- |-----|--------|
25
- | `↑` / `↓` | Navigate |
26
- | `Shift+↑` / `Shift+↓` | Reorder |
27
- | `/` | Enter search mode |
28
- | `ESC` | Exit search / quit |
29
- | `Enter` | Execute command |
30
- | `Ctrl+E` | Copy to clipboard |
31
- | `a` | Add new command |
32
- | `e` | Edit selected |
33
- | `Ctrl+D` | Delete selected |
34
-
35
19
  ## Commands
36
20
 
37
21
  Each saved command has:
@@ -40,10 +24,6 @@ Each saved command has:
40
24
  - **Directory** *(optional)* — when set, executing or copying prepends `cd <dir> &&`
41
25
  - **Tags** — for organisation and search
42
26
 
43
- ## Search
44
-
45
- Press `/` to enter search mode. Fuzzy search across command text, description, and tags. Press `Enter` to lock the filter, `ESC` to clear.
46
-
47
27
  ## Data
48
28
 
49
29
  Commands are stored at `~/.config/drift/commands.json`.
@@ -6,8 +6,8 @@ import {
6
6
  import "./chunk-V6RC35DD.js";
7
7
 
8
8
  // src/ui/Browser.tsx
9
- import { useState as useState2, useCallback as useCallback2 } from "react";
10
- import { Box as Box6, Text as Text6, useInput as useInput3, useApp } from "ink";
9
+ import { useState as useState3, useCallback as useCallback2 } from "react";
10
+ import { Box as Box7, Text as Text7, useInput as useInput4, useApp } from "ink";
11
11
  import clipboard from "clipboardy";
12
12
 
13
13
  // src/hooks/useFuzzySearch.ts
@@ -226,19 +226,158 @@ function EditModal({ command, onSave, onCancel }) {
226
226
  );
227
227
  }
228
228
 
229
- // src/ui/Browser.tsx
229
+ // src/ui/Onboarding.tsx
230
+ import { useEffect, useRef, useState as useState2 } from "react";
231
+ import { Box as Box6, Text as Text6, useInput as useInput3 } from "ink";
230
232
  import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
233
+ var W = 64;
234
+ var H = 20;
235
+ var PATHS = [
236
+ // 1 — bottom-center → sweep hard left → exit top-right (original)
237
+ [{ x: 0.45, y: 1.12 }, { x: 0.45, y: 0.55 }, { x: 0.1, y: 0.12 }, { x: 1.12, y: 0.1 }],
238
+ // 2 — bottom-right → wide left arc → exit left-middle
239
+ [{ x: 0.8, y: 1.12 }, { x: 0.8, y: 0.45 }, { x: 0.15, y: 0.18 }, { x: -0.12, y: 0.45 }],
240
+ // 3 — bottom-left → arc right → exit top-right (mirror of 1)
241
+ [{ x: 0.22, y: 1.12 }, { x: 0.22, y: 0.55 }, { x: 0.88, y: 0.12 }, { x: 1.12, y: 0.1 }],
242
+ // 4 — left-middle → sweep right across → exit top-right
243
+ [{ x: -0.12, y: 0.7 }, { x: 0.22, y: 0.7 }, { x: 0.78, y: 0.12 }, { x: 1.12, y: 0.12 }],
244
+ // 5 — right-middle → sweep left across → exit top-left
245
+ [{ x: 1.12, y: 0.7 }, { x: 0.78, y: 0.7 }, { x: 0.22, y: 0.12 }, { x: -0.12, y: 0.12 }]
246
+ ];
247
+ function bezier(bp, t) {
248
+ const m = 1 - t;
249
+ return {
250
+ x: m * m * m * bp[0].x + 3 * m * m * t * bp[1].x + 3 * m * t * t * bp[2].x + t * t * t * bp[3].x,
251
+ y: m * m * m * bp[0].y + 3 * m * m * t * bp[1].y + 3 * m * t * t * bp[2].y + t * t * t * bp[3].y
252
+ };
253
+ }
254
+ function bezierAngleDeg(bp, t) {
255
+ const d = 1e-3;
256
+ const a = bezier(bp, Math.max(0, t - d));
257
+ const b = bezier(bp, Math.min(1, t + d));
258
+ return Math.atan2(b.y - a.y, b.x - a.x) * 180 / Math.PI;
259
+ }
260
+ var SKID_CH = ["\u2500", "\u2572", "\u2502", "\u2571", "\u2500", "\u2572", "\u2502", "\u2571"];
261
+ var SMOKE_CH = ["\u2217", "\xB0", "\xB7", "`", "'", "~", "\u2218", "*"];
262
+ var DURATION = 4200;
263
+ var PAUSE = 900;
264
+ var TRAIL_DT = 0.016;
265
+ var DRIFT_DEG = 24;
266
+ function spawnTrail(marks, smokes, carX, carY, bodyDeg) {
267
+ const bRad = bodyDeg * Math.PI / 180;
268
+ const rearX = carX - Math.cos(bRad) * 3.5;
269
+ const rearY = carY - Math.sin(bRad) * 3.5;
270
+ const pRad = bRad + Math.PI / 2;
271
+ const ws = 1.8;
272
+ const di = Math.round((bodyDeg % 360 + 360) % 360 / 45) % 8;
273
+ const sk = SKID_CH[di];
274
+ marks.push({ x: rearX + Math.cos(pRad) * ws, y: rearY + Math.sin(pRad) * ws, ch: sk, age: 0, maxAge: 90 });
275
+ marks.push({ x: rearX - Math.cos(pRad) * ws, y: rearY - Math.sin(pRad) * ws, ch: sk, age: 0, maxAge: 90 });
276
+ const count = Math.random() < 0.5 ? 1 : 2;
277
+ for (let i = 0; i < count; i++) {
278
+ smokes.push({
279
+ x: rearX + (Math.random() - 0.5) * 5,
280
+ y: rearY + (Math.random() - 0.5) * 2.5,
281
+ ch: SMOKE_CH[Math.floor(Math.random() * SMOKE_CH.length)],
282
+ age: 0,
283
+ maxAge: 28
284
+ });
285
+ }
286
+ }
287
+ function renderFrame(marks, smokes) {
288
+ const buf = Array.from({ length: H }, () => new Array(W).fill(" "));
289
+ function blit(x, y, ch) {
290
+ const xi = Math.round(x), yi = Math.round(y);
291
+ if (xi >= 0 && xi < W && yi >= 0 && yi < H) buf[yi][xi] = ch;
292
+ }
293
+ for (const m of marks) {
294
+ const fade = m.age / m.maxAge;
295
+ if (fade < 0.5) blit(m.x, m.y, m.ch);
296
+ else if (fade < 0.75) blit(m.x, m.y, "\xB7");
297
+ else blit(m.x, m.y, ".");
298
+ }
299
+ for (const s of smokes) {
300
+ if (s.age / s.maxAge < 0.65) blit(s.x, s.y, s.ch);
301
+ }
302
+ return buf.map((row) => row.join("")).join("\n");
303
+ }
304
+ function Onboarding({ onDismiss }) {
305
+ const [canvas, setCanvas] = useState2(() => Array(H).fill(" ".repeat(W)).join("\n"));
306
+ const anim = useRef({
307
+ marks: [],
308
+ smokes: [],
309
+ lastTrail: -1,
310
+ startTime: Date.now(),
311
+ pathIndex: 0,
312
+ cycleCount: 0
313
+ });
314
+ useInput3(() => {
315
+ onDismiss();
316
+ });
317
+ useEffect(() => {
318
+ const id = setInterval(() => {
319
+ const s = anim.current;
320
+ const elapsed = Date.now() - s.startTime;
321
+ const cycle = elapsed % (DURATION + PAUSE);
322
+ if (cycle < 40) {
323
+ s.cycleCount++;
324
+ s.pathIndex = s.cycleCount % PATHS.length;
325
+ s.lastTrail = -1;
326
+ }
327
+ const bp = PATHS[s.pathIndex];
328
+ const T = cycle < DURATION ? cycle / DURATION : 1;
329
+ const pos = bezier(bp, T);
330
+ const velDeg = bezierAngleDeg(bp, T);
331
+ const bodyDeg = velDeg + DRIFT_DEG;
332
+ if (T < 0.99 && T - s.lastTrail >= TRAIL_DT) {
333
+ s.lastTrail = T;
334
+ spawnTrail(s.marks, s.smokes, pos.x * W, pos.y * H, bodyDeg);
335
+ }
336
+ for (const m of s.marks) m.age++;
337
+ for (const sm of s.smokes) sm.age++;
338
+ s.marks = s.marks.filter((m) => m.age < m.maxAge);
339
+ s.smokes = s.smokes.filter((sm) => sm.age < sm.maxAge);
340
+ setCanvas(renderFrame(s.marks, s.smokes));
341
+ }, 40);
342
+ return () => clearInterval(id);
343
+ }, []);
344
+ const termH = process.stdout.rows ?? 24;
345
+ return /* @__PURE__ */ jsx6(Box6, { height: termH, alignItems: "center", justifyContent: "center", children: /* @__PURE__ */ jsxs6(
346
+ Box6,
347
+ {
348
+ borderStyle: "round",
349
+ borderColor: "white",
350
+ flexDirection: "column",
351
+ paddingX: 2,
352
+ paddingY: 1,
353
+ width: W + 8,
354
+ children: [
355
+ /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", alignItems: "center", marginBottom: 1, children: [
356
+ /* @__PURE__ */ jsx6(Text6, { bold: true, children: " drift " }),
357
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "your commands. always within reach." })
358
+ ] }),
359
+ /* @__PURE__ */ jsx6(Box6, { width: W, children: /* @__PURE__ */ jsx6(Text6, { children: canvas }) }),
360
+ /* @__PURE__ */ jsx6(Box6, { justifyContent: "center", marginTop: 1, children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "press any key to continue" }) })
361
+ ]
362
+ }
363
+ ) });
364
+ }
365
+
366
+ // src/ui/Browser.tsx
367
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
231
368
  var HEADER_ROWS = 4;
232
369
  var FOOTER_ROWS = 3;
233
- function Browser({ onExecute }) {
370
+ function Browser({ onExecute, forceOnboarding = false }) {
234
371
  const { exit } = useApp();
235
372
  const { commands, updateCommand, moveCommand, removeCommand } = useStore();
236
- const [query, setQuery] = useState2("");
237
- const [isSearching, setIsSearching] = useState2(false);
238
- const [selectedIndex, setSelectedIndex] = useState2(0);
239
- const [viewportStart, setViewportStart] = useState2(0);
240
- const [screen, setScreen] = useState2("browser");
241
- const [statusMessage, setStatusMessage] = useState2("");
373
+ const [query, setQuery] = useState3("");
374
+ const [isSearching, setIsSearching] = useState3(false);
375
+ const [selectedIndex, setSelectedIndex] = useState3(0);
376
+ const [viewportStart, setViewportStart] = useState3(0);
377
+ const [screen, setScreen] = useState3(
378
+ () => forceOnboarding || commands.length === 0 ? "onboarding" : "browser"
379
+ );
380
+ const [statusMessage, setStatusMessage] = useState3("");
242
381
  const filtered = useFuzzySearch(commands, query);
243
382
  const clampedIndex = Math.min(selectedIndex, Math.max(0, filtered.length - 1));
244
383
  const selected = filtered[clampedIndex] ?? null;
@@ -262,7 +401,7 @@ function Browser({ onExecute }) {
262
401
  },
263
402
  [filtered.length, visibleRows]
264
403
  );
265
- useInput3(
404
+ useInput4(
266
405
  (input, key) => {
267
406
  if (screen === "confirm-delete") return;
268
407
  if (isSearching) {
@@ -357,8 +496,11 @@ function Browser({ onExecute }) {
357
496
  },
358
497
  { isActive: screen === "browser" }
359
498
  );
499
+ if (screen === "onboarding") {
500
+ return /* @__PURE__ */ jsx7(Onboarding, { onDismiss: () => setScreen("browser") });
501
+ }
360
502
  if (screen === "add") {
361
- return /* @__PURE__ */ jsx6(
503
+ return /* @__PURE__ */ jsx7(
362
504
  AddForm,
363
505
  {
364
506
  onSave: () => {
@@ -370,9 +512,9 @@ function Browser({ onExecute }) {
370
512
  );
371
513
  }
372
514
  const isOverlay = screen === "confirm-delete" || screen === "edit";
373
- return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", height: process.stdout.rows ?? 24, children: [
374
- /* @__PURE__ */ jsx6(SearchBar, { query, isSearching, total: commands.length, filtered: filtered.length }),
375
- /* @__PURE__ */ jsx6(Box6, { flexDirection: "column", flexGrow: 1, children: filtered.length === 0 ? /* @__PURE__ */ jsx6(Box6, { paddingX: 2, paddingY: 1, children: /* @__PURE__ */ jsx6(Text6, { color: "gray", dimColor: true, children: commands.length === 0 ? "No commands saved yet. Press `a` to add your first command." : "No matches. Keep typing or clear the search." }) }) : visibleCommands.map((cmd, i) => /* @__PURE__ */ jsx6(
515
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", height: process.stdout.rows ?? 24, children: [
516
+ /* @__PURE__ */ jsx7(SearchBar, { query, isSearching, total: commands.length, filtered: filtered.length }),
517
+ /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", flexGrow: 1, children: filtered.length === 0 ? /* @__PURE__ */ jsx7(Box7, { paddingX: 2, paddingY: 1, children: /* @__PURE__ */ jsx7(Text7, { color: "gray", dimColor: true, children: commands.length === 0 ? "No commands saved yet. Press `a` to add your first command." : "No matches. Keep typing or clear the search." }) }) : visibleCommands.map((cmd, i) => /* @__PURE__ */ jsx7(
376
518
  CommandItem,
377
519
  {
378
520
  command: cmd,
@@ -381,7 +523,7 @@ function Browser({ onExecute }) {
381
523
  },
382
524
  cmd.id
383
525
  )) }),
384
- screen === "edit" && selected && /* @__PURE__ */ jsx6(
526
+ screen === "edit" && selected && /* @__PURE__ */ jsx7(
385
527
  EditModal,
386
528
  {
387
529
  command: selected,
@@ -393,7 +535,7 @@ function Browser({ onExecute }) {
393
535
  onCancel: () => setScreen("browser")
394
536
  }
395
537
  ),
396
- screen === "confirm-delete" && selected && /* @__PURE__ */ jsx6(
538
+ screen === "confirm-delete" && selected && /* @__PURE__ */ jsx7(
397
539
  ConfirmDelete,
398
540
  {
399
541
  command: selected,
@@ -406,7 +548,7 @@ function Browser({ onExecute }) {
406
548
  onCancel: () => setScreen("browser")
407
549
  }
408
550
  ),
409
- /* @__PURE__ */ jsx6(StatusBar, { message: statusMessage })
551
+ /* @__PURE__ */ jsx7(StatusBar, { message: statusMessage })
410
552
  ] });
411
553
  }
412
554
  export {
package/dist/index.js CHANGED
@@ -6,13 +6,16 @@ import { render } from "ink";
6
6
  import React from "react";
7
7
  import { execa } from "execa";
8
8
  program.name("drift").description("cmdvault \u2014 A TUI command vault for your shell").version("1.0.0");
9
- program.command("list", { isDefault: true }).description("Browse saved commands (default)").action(async () => {
10
- const { Browser } = await import("./Browser-6EKJ6MS7.js");
9
+ program.command("list", { isDefault: true }).description("Browse saved commands (default)").option("--onboarding", "Force the onboarding screen (debug)").action(async (opts) => {
10
+ const { Browser } = await import("./Browser-UPOZVAV4.js");
11
11
  let commandToRun;
12
12
  const { waitUntilExit } = render(
13
- React.createElement(Browser, { onExecute: (cmd) => {
14
- commandToRun = cmd;
15
- } })
13
+ React.createElement(Browser, {
14
+ onExecute: (cmd) => {
15
+ commandToRun = cmd;
16
+ },
17
+ forceOnboarding: opts.onboarding ?? false
18
+ })
16
19
  );
17
20
  await waitUntilExit();
18
21
  process.stdout.write("\x1Bc");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fiete/drift",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "A TUI command vault for your shell",
5
5
  "type": "module",
6
6
  "bin": {