pi-ebb 0.0.1 → 1.1.0

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.
Files changed (3) hide show
  1. package/README.md +15 -7
  2. package/dist/index.js +80 -26
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,10 +1,15 @@
1
1
  # pi-ebb
2
2
 
3
- Spaced repetition in the background of [pi](https://pi.dev). It teaches you words, phrases or any other facts while you work.
3
+ **Learn while your agent thinks.** Spaced repetition in the background of [pi](https://pi.dev): one card above the editor while pi works, graded with one shortcut. A language, or any other facts you want to keep.
4
4
 
5
- ```text
6
- el corazón │ alt+r Forgot alt+g Got it alt+s Skip
7
- ```
5
+ ![ebb in pi: a Spanish card above the editor while pi works](https://raw.githubusercontent.com/r13v/ebb/main/assets/pi.png)
6
+
7
+ - **No new habit.** It uses the seconds you already wait for the agent.
8
+ - **Never in your way.** Grade with `alt+1`, `alt+2` or a click; the editor keeps every key you type. No pop-up, ever.
9
+ - **FSRS-6**, the scheduler of Anki: each card comes back just before you would forget it.
10
+ - **Decks in one command**, checked by back-translation, or a ready deck of about 2000 Spanish words.
11
+
12
+ Site and decks: <https://r13v.github.io/ebb/>
8
13
 
9
14
  ## Install
10
15
 
@@ -20,10 +25,13 @@ pi install npm:pi-ebb
20
25
 
21
26
  The card shows above the editor. The shortcuts never take a key the editor types:
22
27
 
23
- - `alt+r`: Forgot, and Next after the answer.
24
- - `alt+g`: Got it.
25
- - `alt+s`: Skip.
28
+ - `alt+1` (Option+1 on a Mac): Forgot, and Next after the answer.
29
+ - `alt+2`: Got it.
30
+ - `alt+3`: Skip.
26
31
  - `alt+h`: hide or show the card.
32
+ - Or click a button of the band (pi's fullscreen mode, the default).
33
+
34
+ On a Mac, Option must work as Alt: in Ghostty or agterm set `macos-option-as-alt = true`, in iTerm2 set the Option key to Esc+. Otherwise Option+1 types `¡`; click the band instead.
27
35
 
28
36
  The scheduler is FSRS-6. The data is in `$XDG_DATA_HOME/ebb/` (by default `~/.local/share/ebb/`), shared with the [Claude Code mod](https://github.com/r13v/ebb).
29
37
 
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { homedir } from "node:os";
2
- import { matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
2
+ import { MouseRegion, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
3
3
  import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
4
4
  import { dirname } from "node:path";
5
5
  // core/decks.ts
@@ -981,32 +981,72 @@ var fileHost = (home, dir) => ({
981
981
  // pi/index.ts
982
982
  var WIDGET = "ebb";
983
983
  var KEYS = {
984
- forgot: "alt+r",
985
- got: "alt+g",
986
- skip: "alt+s",
984
+ forgot: "alt+1",
985
+ got: "alt+2",
986
+ skip: "alt+3",
987
987
  toggle: "alt+h"
988
988
  };
989
989
  var files = fileHost(homedir(), dataDir(homedir(), process.env.XDG_DATA_HOME));
990
990
  var SETTINGS = `${files.dir}/pi.json`;
991
991
  var textOf = (content) => content.filter((c) => c.type === "text").map((c) => c.text).join("");
992
- /** The lines of the band; none while it is hidden, paused or without a card. */
993
- var bandLines = (v, theme) => {
994
- if (v.kind === "error") return [theme.fg("error", `ebb: cannot read the data: ${v.error}`)];
995
- if (v.kind === "no-deck") return [theme.fg("dim", "ebb: /ebb add <topic> or /ebb install <file or URL> to get cards")];
992
+ /** The pieces of the band; none while it is paused or without a card. */
993
+ var bandParts = (v, theme) => {
994
+ if (v.kind === "error") return [{ text: theme.fg("error", `ebb: cannot read the data: ${v.error}`) }];
995
+ if (v.kind === "no-deck") return [{ text: theme.fg("dim", "ebb: /ebb add <topic> or /ebb install <file or URL> to get cards") }];
996
996
  if (v.kind === "none") return void 0;
997
997
  const f = v.face;
998
- const bar = theme.fg("dim", "│");
999
- const key = (keys, label) => `${theme.fg("accent", keys)} ${label}`;
998
+ const bar = { text: ` ${theme.fg("dim", "│")} ` };
999
+ const button = (key, label, action) => ({
1000
+ text: `${theme.fg("accent", key)} ${label}`,
1001
+ action
1002
+ });
1003
+ const gap = { text: " " };
1000
1004
  if (v.isRevealed) {
1001
1005
  const hint = f.hint ? theme.fg("dim", ` (${f.hint})`) : "";
1002
- return [`${theme.bold(f.ask)} — ${f.answer}${hint} ${bar} ${key(KEYS.forgot, "Next")}`];
1006
+ return [
1007
+ { text: `${theme.bold(f.ask)} — ${f.answer}${hint}` },
1008
+ bar,
1009
+ button(KEYS.forgot, "Next", "next")
1010
+ ];
1003
1011
  }
1004
- const keys = [
1005
- key(KEYS.forgot, "Forgot"),
1006
- key(KEYS.got, "Got it"),
1007
- key(KEYS.skip, "Skip")
1008
- ].join(" ");
1009
- return [`${theme.bold(f.ask)} ${bar} ${keys}`];
1012
+ return [
1013
+ { text: theme.bold(f.ask) },
1014
+ bar,
1015
+ button(KEYS.forgot, "Forgot", "forgot"),
1016
+ gap,
1017
+ button(KEYS.got, "Got it", "got"),
1018
+ gap,
1019
+ button(KEYS.skip, "Skip", "skip")
1020
+ ];
1021
+ };
1022
+ /**
1023
+ * The band as one line, indented as pi indents a widget, with the columns of its buttons. pi passes a
1024
+ * click only in its fullscreen mode, so every button also has a key.
1025
+ */
1026
+ var bandWidget = (parts, press) => (_tui, _theme) => {
1027
+ let buttons = [];
1028
+ return new MouseRegion({
1029
+ render: (width) => {
1030
+ let column = 1;
1031
+ buttons = [];
1032
+ for (const part of parts) {
1033
+ const partWidth = visibleWidth(part.text);
1034
+ if (part.action) buttons.push({
1035
+ from: column,
1036
+ to: column + partWidth,
1037
+ action: part.action
1038
+ });
1039
+ column += partWidth;
1040
+ }
1041
+ return [truncateToWidth(` ${parts.map((p) => p.text).join("")}`, width)];
1042
+ },
1043
+ invalidate: () => void 0
1044
+ }, (event) => {
1045
+ const hit = buttons.find((b) => event.x >= b.from && event.x < b.to);
1046
+ if (event.type !== "click" || event.button !== "left" || !hit) return void 0;
1047
+ press(hit.action);
1048
+ return { handled: true };
1049
+ });
1010
1050
  };
1011
1051
  function ebb(pi) {
1012
1052
  const session = createSession();
@@ -1015,8 +1055,8 @@ function ebb(pi) {
1015
1055
  const draw = async () => {
1016
1056
  if (!ctx?.hasUI) return;
1017
1057
  const ui = ctx.ui;
1018
- const lines = isHidden ? void 0 : bandLines(await session.bandView(host), ui.theme);
1019
- ui.setWidget(WIDGET, lines);
1058
+ const parts = isHidden ? void 0 : bandParts(await session.bandView(host), ui.theme);
1059
+ ui.setWidget(WIDGET, parts && bandWidget(parts, (action) => void press(action)));
1020
1060
  };
1021
1061
  const host = {
1022
1062
  ...files,
@@ -1052,11 +1092,25 @@ function ebb(pi) {
1052
1092
  notify(`ebb: ${String(error)}`, "error");
1053
1093
  }
1054
1094
  };
1055
- /** A band shortcut acts only on a card the band shows. */
1056
- const onCard = (work) => act(async () => {
1095
+ /** What a button of the band does, from its key or a click; only on a card the band shows. */
1096
+ const run = async (action) => {
1057
1097
  const v = await session.bandView(host);
1058
- if (!isHidden && v.kind === "card") await work(v.isRevealed);
1059
- });
1098
+ if (isHidden || v.kind !== "card") return;
1099
+ if (v.isRevealed) {
1100
+ if (action === "forgot" || action === "next") await session.next(host);
1101
+ return;
1102
+ }
1103
+ if (action === "forgot") await session.gradeCard(host, 1);
1104
+ if (action === "got") await session.gradeCard(host, 3);
1105
+ if (action === "skip") await session.skip(host);
1106
+ };
1107
+ const press = async (action) => {
1108
+ try {
1109
+ await run(action);
1110
+ } catch (error) {
1111
+ notify(`ebb: ${String(error)}`, "error");
1112
+ }
1113
+ };
1060
1114
  const showNextCard = async () => {
1061
1115
  const error = await session.showNextCard(host);
1062
1116
  if (error) notify(`ebb: ${error}`, "error");
@@ -1149,15 +1203,15 @@ function ebb(pi) {
1149
1203
  });
1150
1204
  pi.registerShortcut(KEYS.forgot, {
1151
1205
  description: "ebb: Forgot, or Next after the answer",
1152
- handler: onCard((isRevealed) => isRevealed ? session.next(host) : session.gradeCard(host, 1))
1206
+ handler: act(() => run("forgot"))
1153
1207
  });
1154
1208
  pi.registerShortcut(KEYS.got, {
1155
1209
  description: "ebb: Got it",
1156
- handler: onCard(async (isRevealed) => !isRevealed && session.gradeCard(host, 3))
1210
+ handler: act(() => run("got"))
1157
1211
  });
1158
1212
  pi.registerShortcut(KEYS.skip, {
1159
1213
  description: "ebb: skip the card",
1160
- handler: onCard(async (isRevealed) => !isRevealed && session.skip(host))
1214
+ handler: act(() => run("skip"))
1161
1215
  });
1162
1216
  pi.registerShortcut(KEYS.toggle, {
1163
1217
  description: "ebb: hide or show the card above the editor",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-ebb",
3
- "version": "0.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Spaced repetition in the background of pi: a card above the editor, graded with a shortcut",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -12,7 +12,7 @@
12
12
  "bugs": {
13
13
  "url": "https://github.com/r13v/ebb/issues"
14
14
  },
15
- "homepage": "https://github.com/r13v/ebb/tree/main/pi#readme",
15
+ "homepage": "https://r13v.github.io/ebb/",
16
16
  "publishConfig": {
17
17
  "access": "public"
18
18
  },