pi-ebb 1.0.0 → 1.1.1
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 +15 -7
- package/dist/index.js +84 -30
- 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)
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+

|
|
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+
|
|
24
|
-
- `alt+
|
|
25
|
-
- `alt+
|
|
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
|
|
@@ -707,8 +707,8 @@ var createSession = () => {
|
|
|
707
707
|
const endTurn = async (host) => {
|
|
708
708
|
if (!band.isRevealed) await showNextCard(host, void 0, true);
|
|
709
709
|
};
|
|
710
|
-
/** What the band draws now
|
|
711
|
-
const bandView = async (host
|
|
710
|
+
/** What the band draws now. A reader on every render: it never throws. */
|
|
711
|
+
const bandView = async (host) => {
|
|
712
712
|
let data;
|
|
713
713
|
try {
|
|
714
714
|
data = await load(host);
|
|
@@ -719,13 +719,13 @@ var createSession = () => {
|
|
|
719
719
|
};
|
|
720
720
|
}
|
|
721
721
|
if (!Object.keys(data.decks).length) return { kind: "no-deck" };
|
|
722
|
-
const card =
|
|
722
|
+
const card = band.cardId ? data.cards[band.cardId] : void 0;
|
|
723
723
|
const f = card && face(data, card);
|
|
724
724
|
if ((data.meta.pausedUntil ?? 0) > Date.now() || !f) return { kind: "none" };
|
|
725
725
|
return {
|
|
726
726
|
kind: "card",
|
|
727
727
|
face: f,
|
|
728
|
-
isRevealed:
|
|
728
|
+
isRevealed: band.isRevealed
|
|
729
729
|
};
|
|
730
730
|
};
|
|
731
731
|
/**
|
|
@@ -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+
|
|
985
|
-
got: "alt+
|
|
986
|
-
skip: "alt+
|
|
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
|
|
993
|
-
var
|
|
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
|
|
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 [
|
|
1006
|
+
return [
|
|
1007
|
+
{ text: `${theme.bold(f.ask)} — ${f.answer}${hint}` },
|
|
1008
|
+
bar,
|
|
1009
|
+
button(KEYS.forgot, "Next", "next")
|
|
1010
|
+
];
|
|
1003
1011
|
}
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
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
|
|
1019
|
-
ui.setWidget(WIDGET,
|
|
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
|
-
/**
|
|
1056
|
-
const
|
|
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 (
|
|
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:
|
|
1206
|
+
handler: act(() => run("forgot"))
|
|
1153
1207
|
});
|
|
1154
1208
|
pi.registerShortcut(KEYS.got, {
|
|
1155
1209
|
description: "ebb: Got it",
|
|
1156
|
-
handler:
|
|
1210
|
+
handler: act(() => run("got"))
|
|
1157
1211
|
});
|
|
1158
1212
|
pi.registerShortcut(KEYS.skip, {
|
|
1159
1213
|
description: "ebb: skip the card",
|
|
1160
|
-
handler:
|
|
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": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
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.
|
|
15
|
+
"homepage": "https://r13v.github.io/ebb/",
|
|
16
16
|
"publishConfig": {
|
|
17
17
|
"access": "public"
|
|
18
18
|
},
|