strom-research 1.16.0-beta.1 → 1.16.0-beta.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/CHANGELOG.cs.md +8 -0
- package/CHANGELOG.de.md +10 -0
- package/CHANGELOG.md +8 -0
- package/assets/lang/cs.json +18 -0
- package/assets/lang/de.json +18 -0
- package/dist/cli/menu-settings.js +25 -4
- package/dist/cli/terminals.js +50 -0
- package/dist/cli/ui.js +18 -0
- package/dist/cli/wizard.js +15 -0
- package/dist/commands/links.js +16 -6
- package/dist/commands/setup.js +17 -0
- package/dist/commands/start.js +7 -2
- package/dist/commands/sync.js +20 -16
- package/dist/core/config.js +21 -2
- package/dist/core/import.js +39 -10
- package/dist/core/live.js +1 -1
- package/dist/core/shortcut.js +24 -1
- package/dist/core/sync.js +90 -10
- package/dist/core/terminal.js +70 -0
- package/dist/core/tree.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.cs.md
CHANGED
|
@@ -7,11 +7,19 @@ Změny každé verze ve zkratce. Úplné poznámky a instalační řádky jsou n
|
|
|
7
7
|
|
|
8
8
|
## 1.16.0
|
|
9
9
|
|
|
10
|
+
- **Terminál pro okna výzkumu (macOS).** Co aplikace Strom otevírá v terminálu a rozhovor předaný do nového okna
|
|
11
|
+
se může otevírat v iTerm2 místo v terminálu systému: první položka nastavení, průvodce, když jsou tu oba, nebo
|
|
12
|
+
`strom config set terminal iterm2`. Když zvolený terminál chybí, otevře se terminál systému a okno to řekne;
|
|
13
|
+
`strom doctor` ukáže, který je zvolený. Na Windows terminál určuje systém.
|
|
10
14
|
- **Každá osoba na kraji stromu s vlastními variantami.** Když se otevřená hypotéza napojuje na strom u více osob,
|
|
11
15
|
každá nese varianty, které ji jmenují, a rodinu mimo strom, ke které patří; aplikace Strom tak může ukázat všechny,
|
|
12
16
|
ne jen první.
|
|
13
17
|
- **Kraj stromu osobu po osobě.** `strom edge --json` vypíše každou osobu, na kterou hypotéza navazuje, s variantami
|
|
14
18
|
a rodinou každé z nich.
|
|
19
|
+
- **Příjmení po sňatku oběma směry s aplikací Strom.** Jméno po sňatku, které aplikace Strom pošle zpět, se zapíše
|
|
20
|
+
jako jméno po sňatku, nikdy jako přejmenování ani jako další tvar rodného jména; stejné příjmení je stejné jméno,
|
|
21
|
+
ať s ním jde jakékoli křestní. Když se v aplikaci změní nebo smaže, staré zmizí tam, kde ho daly jen úpravy
|
|
22
|
+
v aplikaci; jméno doložené záznamem zůstává. `strom intake` čte `_MARNM` jiných programů jako jméno po sňatku.
|
|
15
23
|
- **Linie výzkumu.** České texty všude říkají „linie výzkumu“, nikde už „směry“.
|
|
16
24
|
|
|
17
25
|
## 1.15.0
|
package/CHANGELOG.de.md
CHANGED
|
@@ -7,11 +7,21 @@ Die Änderungen jeder Version in Kürze. Die vollständigen Hinweise und die Ins
|
|
|
7
7
|
|
|
8
8
|
## 1.16.0
|
|
9
9
|
|
|
10
|
+
- **Das Terminal für die Fenster der Forschung (macOS).** Was die Strom-App in einem Terminal öffnet, und ein an ein
|
|
11
|
+
neues Fenster übergebenes Gespräch, kann sich in iTerm2 statt im Terminal des Systems öffnen: der erste Punkt der
|
|
12
|
+
Einstellungen, die Einrichtung, wenn beide da sind, oder `strom config set terminal iterm2`. Ein nicht installiertes
|
|
13
|
+
Terminal fällt auf das des Systems zurück und sagt es im Fenster; `strom doctor` nennt das gewählte. Unter Windows
|
|
14
|
+
bestimmt es das System.
|
|
10
15
|
- **Jede Person am Rand des Stammbaums mit eigenen Varianten.** Wenn eine offene Hypothese den Stammbaum bei mehreren
|
|
11
16
|
Personen anschließt, trägt jede die Varianten, die sie nennen, und die Familie außerhalb des Stammbaums, zu der sie
|
|
12
17
|
gehört; so kann die Strom-App alle zeigen, nicht nur die erste.
|
|
13
18
|
- **Der Rand des Stammbaums, Person für Person.** `strom edge --json` nennt jede Person, an die eine Hypothese
|
|
14
19
|
anschließt, mit den Varianten und der Familie jeder einzelnen.
|
|
20
|
+
- **Der Familienname nach der Heirat, in beide Richtungen mit der Strom-App.** Ein Name nach der Heirat, den die
|
|
21
|
+
Strom-App zurückschickt, kommt als Name nach der Heirat an, nie als Umbenennung oder als weitere Form des
|
|
22
|
+
Geburtsnamens; derselbe Familienname ist derselbe Name, mit welchem Vornamen auch immer. In der App geändert oder
|
|
23
|
+
entfernt, geht der alte dort, wo ihn nur Änderungen in der App gaben; einer, den ein Eintrag belegt, bleibt.
|
|
24
|
+
`strom intake` liest `_MARNM` anderer Programme als Namen nach der Heirat.
|
|
15
25
|
- **Forschungslinien auf Tschechisch.** Die tschechischen Texte sagen jetzt überall „linie výzkumu“ (Forschungslinien).
|
|
16
26
|
|
|
17
27
|
## 1.15.0
|
package/CHANGELOG.md
CHANGED
|
@@ -7,11 +7,19 @@ The changes of each version in short. The full notes and the install lines are o
|
|
|
7
7
|
|
|
8
8
|
## 1.16.0
|
|
9
9
|
|
|
10
|
+
- **The terminal for the research's windows (macOS).** What the Strom app opens in a terminal, and a conversation
|
|
11
|
+
handed over to a new window, can open in iTerm2 instead of the system's terminal: the settings' first item, the
|
|
12
|
+
setup when both are here, or `strom config set terminal iterm2`. A terminal not installed falls back to the
|
|
13
|
+
system's and says so in the window; `strom doctor` names the one chosen. On Windows the system decides it.
|
|
10
14
|
- **Each person where the tree ends, with its own variants.** When an open hypothesis joins the tree at several
|
|
11
15
|
people, each of them carries the variants that name it and the family off the tree it belongs to, so the Strom app
|
|
12
16
|
can show all of them, not only the first.
|
|
13
17
|
- **Where the tree ends, person by person.** `strom edge --json` lists each person a hypothesis joins, with the
|
|
14
18
|
variants and the family of each one.
|
|
19
|
+
- **The surname after a marriage, both ways with the Strom app.** A name after a marriage the Strom app sends back
|
|
20
|
+
comes in as a name of the married kind, never as a rename or another form of the birth name; the same surname is
|
|
21
|
+
the same name whatever first name goes with it. Changed or taken off in the app, the old one goes where only edits
|
|
22
|
+
in the app gave it; one a record gives stays. `strom intake` reads `_MARNM` of other programs as a married name.
|
|
15
23
|
- **Lines of research in Czech.** The Czech texts say „linie výzkumu“ (lines of research) everywhere now.
|
|
16
24
|
|
|
17
25
|
## 1.15.0
|
package/assets/lang/cs.json
CHANGED
|
@@ -1123,6 +1123,10 @@
|
|
|
1123
1123
|
"ui.sync.cause": "příčina {x}",
|
|
1124
1124
|
"ui.sync.name.new": "{n}. {who}: další jméno — {text}",
|
|
1125
1125
|
"ui.sync.name.changed": "{n}. {who}: změněné jméno — {old} → {new}",
|
|
1126
|
+
"ui.sync.name.married": "{n}. {who}: jméno po sňatku — {text}",
|
|
1127
|
+
"ui.sync.name.gone": "{n}. {who}: jméno po sňatku už v něm není — {text}",
|
|
1128
|
+
"ui.sync.do.name.gone": "odebere se (měly ho jen úpravy v aplikaci nebo jiné rodokmeny)",
|
|
1129
|
+
"ui.sync.do.name.kept": "zůstává: dokládá ho záznam",
|
|
1126
1130
|
"ui.sync.do.name.user": "platí úprava z aplikace: osoba se ukazuje pod novým jménem, jméno ze záznamu zůstane vedle něj",
|
|
1127
1131
|
"ui.sync.name.title": "{n}. {who}: {title} — {old} → {new}",
|
|
1128
1132
|
"ui.sync.do.title": "doplní se ke jménu s odkazem na strom z aplikace",
|
|
@@ -1581,6 +1585,20 @@
|
|
|
1581
1585
|
"ui.settings.archives": "Archivy: přihlášení, souhlas se stahováním",
|
|
1582
1586
|
"ui.settings.wizard.what": "Co nastavit?",
|
|
1583
1587
|
"ui.settings.wizard.all": "Všechno, krok za krokem (průvodce nastavením)",
|
|
1588
|
+
"ui.settings.terminal": "Terminál pro okna výzkumu (z aplikace Strom, předaný rozhovor): {state}",
|
|
1589
|
+
"ui.settings.terminal.archive": "Terminál pro okna výzkumu (z aplikace Strom): {state}",
|
|
1590
|
+
"ui.terminal.system": "výchozí systému ({name})",
|
|
1591
|
+
"ui.terminal.system.plain": "výchozí systému",
|
|
1592
|
+
"ui.terminal.name.terminal": "Terminál",
|
|
1593
|
+
"ui.terminal.name.iterm2": "iTerm2",
|
|
1594
|
+
"ui.terminal.notHere": "{name} (tu není nainstalovaný: otevře se výchozí systému)",
|
|
1595
|
+
"ui.terminal.bySystem": "určuje systém",
|
|
1596
|
+
"ui.terminal.windows": "Na Windows terminál určuje systém: Nastavení → Systém → Pro vývojáře → Terminál.",
|
|
1597
|
+
"ui.terminal.pick": "Ve kterém terminálu otevírat okna výzkumu?",
|
|
1598
|
+
"ui.terminal.missing": "{name} tu není nainstalovaný, okno se otevřelo ve výchozím terminálu.",
|
|
1599
|
+
"ui.doc.terminal": "terminál pro okna",
|
|
1600
|
+
"ui.doc.terminal.missing": "zvolený {name} tu není nainstalovaný: okna se otevírají ve výchozím systému",
|
|
1601
|
+
"ui.doc.terminal.unknown": "terminál „{value}“ tu strom nezná: okna se otevírají ve výchozím systému",
|
|
1584
1602
|
"ui.settings.maintenance": "Údržba: kontrola instalace, místo na disku",
|
|
1585
1603
|
"ui.settings.maintenance.tidy": "Údržba: kontrola instalace, místo na disku (jde uvolnit {size})",
|
|
1586
1604
|
"ui.settings.maintenance.update": "Údržba: kontrola instalace, místo na disku, aktualizace na {version}",
|
package/assets/lang/de.json
CHANGED
|
@@ -1123,6 +1123,10 @@
|
|
|
1123
1123
|
"ui.sync.cause": "Ursache {x}",
|
|
1124
1124
|
"ui.sync.name.new": "{n}. {who}: ein weiterer Name — {text}",
|
|
1125
1125
|
"ui.sync.name.changed": "{n}. {who}: Name geändert — {old} → {new}",
|
|
1126
|
+
"ui.sync.name.married": "{n}. {who}: Name nach der Heirat — {text}",
|
|
1127
|
+
"ui.sync.name.gone": "{n}. {who}: Name nach der Heirat nicht mehr darin — {text}",
|
|
1128
|
+
"ui.sync.do.name.gone": "wird entfernt (nur Änderungen in der App oder andere Stammbäume gaben ihn)",
|
|
1129
|
+
"ui.sync.do.name.kept": "bleibt: ein Eintrag belegt ihn",
|
|
1126
1130
|
"ui.sync.do.name.user": "Die eigene Änderung gilt: die Person erscheint unter dem neuen Namen, der Name aus dem Eintrag bleibt daneben",
|
|
1127
1131
|
"ui.sync.name.title": "{n}. {who}: {title} — {old} → {new}",
|
|
1128
1132
|
"ui.sync.do.title": "wird dem Namen hinzugefügt, mit Verweis auf den eigenen Stammbaum",
|
|
@@ -1581,6 +1585,20 @@
|
|
|
1581
1585
|
"ui.settings.archives": "Archive: Anmeldungen, Zustimmung zum Herunterladen",
|
|
1582
1586
|
"ui.settings.wizard.what": "Was einstellen?",
|
|
1583
1587
|
"ui.settings.wizard.all": "Alles, Schritt für Schritt (die Einrichtung)",
|
|
1588
|
+
"ui.settings.terminal": "Terminal für die Fenster der Forschung (aus der Strom-App, ein übergebenes Gespräch): {state}",
|
|
1589
|
+
"ui.settings.terminal.archive": "Terminal für die Fenster der Forschung (aus der Strom-App): {state}",
|
|
1590
|
+
"ui.terminal.system": "Standard des Systems ({name})",
|
|
1591
|
+
"ui.terminal.system.plain": "Standard des Systems",
|
|
1592
|
+
"ui.terminal.name.terminal": "Terminal",
|
|
1593
|
+
"ui.terminal.name.iterm2": "iTerm2",
|
|
1594
|
+
"ui.terminal.notHere": "{name} (hier nicht installiert: der Standard des Systems öffnet sich)",
|
|
1595
|
+
"ui.terminal.bySystem": "bestimmt das System",
|
|
1596
|
+
"ui.terminal.windows": "Unter Windows bestimmt das System das Terminal: Einstellungen → System → Für Entwickler → Terminal.",
|
|
1597
|
+
"ui.terminal.pick": "In welchem Terminal sollen sich die Fenster der Forschung öffnen?",
|
|
1598
|
+
"ui.terminal.missing": "{name} ist hier nicht installiert; das Fenster wurde im Standardterminal geöffnet.",
|
|
1599
|
+
"ui.doc.terminal": "Terminal für die Fenster",
|
|
1600
|
+
"ui.doc.terminal.missing": "{name} gewählt, hier nicht installiert: die Fenster öffnen sich im Standard des Systems",
|
|
1601
|
+
"ui.doc.terminal.unknown": "das Terminal „{value}“ kennt strom hier nicht: die Fenster öffnen sich im Standard des Systems",
|
|
1584
1602
|
"ui.settings.maintenance": "Wartung: Prüfung der Installation, Speicherplatz",
|
|
1585
1603
|
"ui.settings.maintenance.tidy": "Wartung: Prüfung der Installation, Speicherplatz ({size} lassen sich freigeben)",
|
|
1586
1604
|
"ui.settings.maintenance.update": "Wartung: Prüfung der Installation, Speicherplatz, Aktualisierung auf {version}",
|
|
@@ -23,6 +23,7 @@ import { agentsHere } from "../core/apps.js";
|
|
|
23
23
|
import { mb, tidyPlan, TIDY_SAID } from "../core/tidy.js";
|
|
24
24
|
import { calibrationOffer, viewSizesFor } from "../core/viewsizes.js";
|
|
25
25
|
import { archivePermissions, archiveState, browserOnlyHere } from "./menu-archives.js";
|
|
26
|
+
import { chooseTerminal, terminalState } from "./terminals.js";
|
|
26
27
|
export async function settingsMenu(ctx, run, lang, root, newer) {
|
|
27
28
|
const t = translator(lang);
|
|
28
29
|
return subMenu(ctx, lang, () => {
|
|
@@ -46,14 +47,22 @@ export async function settingsMenu(ctx, run, lang, root, newer) {
|
|
|
46
47
|
key: "1",
|
|
47
48
|
label: t(archive ? "ui.settings.wizard.archive" : "ui.settings.wizard"),
|
|
48
49
|
act: async () => {
|
|
49
|
-
// the setup's model step alone, or the whole setup (Enter); 0 back
|
|
50
|
+
// the setup's model step alone, the terminal for the research's windows, or the whole setup (Enter); 0 back
|
|
51
|
+
// with nothing changed (the menu has nine items: these are steps of the setup)
|
|
52
|
+
const parts = [];
|
|
50
53
|
if (modelShown) {
|
|
51
54
|
const kept = ctx.settings.resolve("model.lead", tree, agent);
|
|
52
|
-
|
|
55
|
+
parts.push({ label: t("ui.settings.model", { agent: PROFILES[agent].name, model: kept ? String(kept.value) : t("ui.settings.model.own") }), act: () => modelStep(ctx, run, lang, agent, tree) });
|
|
56
|
+
}
|
|
57
|
+
// macOS: the terminal chosen here; Windows: the system decides it, said where
|
|
58
|
+
if (process.platform === "darwin" || process.platform === "win32")
|
|
59
|
+
parts.push({ label: t(archive ? "ui.settings.terminal.archive" : "ui.settings.terminal", { state: terminalState(ctx, lang) }), act: () => terminalStep(ctx, run, lang) });
|
|
60
|
+
if (parts.length) {
|
|
61
|
+
const i = await ctx.choose(t("ui.settings.wizard.what"), [{ label: t("ui.settings.wizard.all") }, ...parts.map((p) => ({ label: p.label }))], 0, { back: t("ui.browse.back") });
|
|
53
62
|
if (i === undefined)
|
|
54
63
|
return;
|
|
55
|
-
if (i
|
|
56
|
-
return void (await
|
|
64
|
+
if (i > 0)
|
|
65
|
+
return void (await parts[i - 1].act());
|
|
57
66
|
}
|
|
58
67
|
// After the wizard the menu starts afresh: the research may be in another folder now.
|
|
59
68
|
await run(["setup"]);
|
|
@@ -130,6 +139,18 @@ export async function settingsMenu(ctx, run, lang, root, newer) {
|
|
|
130
139
|
return { title: t("ui.settings.title"), items };
|
|
131
140
|
}, { fixed: true });
|
|
132
141
|
}
|
|
142
|
+
/** The terminal for the research's windows: picked among those here (0 back, nothing changed); Windows: where the system decides it. */
|
|
143
|
+
async function terminalStep(ctx, run, lang) {
|
|
144
|
+
if (process.platform === "win32") {
|
|
145
|
+
ctx.io.stdout(translator(lang)("ui.terminal.windows") + "\n");
|
|
146
|
+
return pause(ctx, lang);
|
|
147
|
+
}
|
|
148
|
+
const now = ctx.settings.terminal() ?? "system";
|
|
149
|
+
const picked = await chooseTerminal(ctx, lang, now, { back: translator(lang)("ui.browse.back") });
|
|
150
|
+
if (picked === undefined || picked === now)
|
|
151
|
+
return;
|
|
152
|
+
await run(picked === "system" ? ["config", "unset", "terminal"] : ["config", "set", "terminal", picked], true);
|
|
153
|
+
}
|
|
133
154
|
/** Maintenance: the check of the installation, what strom keeps beside the research (disk space), the newer strom when one is out. */
|
|
134
155
|
async function maintenance(ctx, run, lang, root, newer) {
|
|
135
156
|
const t = translator(lang);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// The terminal for the research's windows as a person sees and picks it (the setting terminal, core/terminal.ts): the
|
|
2
|
+
// system's default with its name first, then the terminals found on this computer — macOS; Windows decides it itself.
|
|
3
|
+
import { ui } from "./ui.js";
|
|
4
|
+
import { systemTerminal, terminalHere, terminalOfMark, terminalsHere } from "../core/terminal.js";
|
|
5
|
+
/** A terminal's name, as a person reads it. */
|
|
6
|
+
export function terminalName(lang, choice) {
|
|
7
|
+
return ui(lang, `ui.terminal.name.${choice}`);
|
|
8
|
+
}
|
|
9
|
+
/** The system's default, with its name when the system says. */
|
|
10
|
+
export function systemLabel(env, lang, platform = process.platform) {
|
|
11
|
+
const sys = systemTerminal(env, platform);
|
|
12
|
+
const name = sys?.choice ? terminalName(lang, sys.choice) : sys?.name;
|
|
13
|
+
return name ? ui(lang, "ui.terminal.system", { name }) : ui(lang, "ui.terminal.system.plain");
|
|
14
|
+
}
|
|
15
|
+
/** What holds now, in a few words: the one chosen (said when it is not here), else the system's default. */
|
|
16
|
+
export function terminalState(ctx, lang, platform = process.platform) {
|
|
17
|
+
if (platform === "win32")
|
|
18
|
+
return ui(lang, "ui.terminal.bySystem");
|
|
19
|
+
const chosen = ctx.settings.terminal();
|
|
20
|
+
if (!chosen)
|
|
21
|
+
return systemLabel(ctx.env, lang, platform);
|
|
22
|
+
return terminalHere(chosen, ctx.env, platform) ? terminalName(lang, chosen) : ui(lang, "ui.terminal.notHere", { name: terminalName(lang, chosen) });
|
|
23
|
+
}
|
|
24
|
+
/** The choices offered: the system's default, then the terminals found here (the one chosen too, though it is gone). */
|
|
25
|
+
function terminalOptions(ctx, lang, platform) {
|
|
26
|
+
const found = terminalsHere(ctx.env, platform);
|
|
27
|
+
const chosen = ctx.settings.terminal();
|
|
28
|
+
return [
|
|
29
|
+
{ value: "system", label: systemLabel(ctx.env, lang, platform) },
|
|
30
|
+
...found.map((v) => ({ value: v, label: terminalName(lang, v) })),
|
|
31
|
+
...(chosen && !found.includes(chosen) ? [{ value: chosen, label: ui(lang, "ui.terminal.notHere", { name: terminalName(lang, chosen) }) }] : []),
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The terminal the setup suggests: the one chosen; none, the one the setup runs in (TERM_PROGRAM) — the system's
|
|
36
|
+
* default when that is the same one.
|
|
37
|
+
*/
|
|
38
|
+
export function suggestedTerminal(ctx, platform = process.platform) {
|
|
39
|
+
const chosen = ctx.settings.terminal();
|
|
40
|
+
if (chosen)
|
|
41
|
+
return chosen;
|
|
42
|
+
const here = terminalOfMark(ctx.env.TERM_PROGRAM);
|
|
43
|
+
return !here || systemTerminal(ctx.env, platform)?.choice === here ? "system" : here;
|
|
44
|
+
}
|
|
45
|
+
/** The person picks the terminal (Enter: `suggested`): its value (system: the system's default); undefined: back. */
|
|
46
|
+
export async function chooseTerminal(ctx, lang, suggested, opts = {}, platform = process.platform) {
|
|
47
|
+
const options = terminalOptions(ctx, lang, platform);
|
|
48
|
+
const i = await ctx.choose(ui(lang, "ui.terminal.pick"), options.map((o) => ({ label: o.label })), Math.max(0, options.findIndex((o) => o.value === suggested)), opts);
|
|
49
|
+
return i === undefined ? undefined : options[i].value;
|
|
50
|
+
}
|
package/dist/cli/ui.js
CHANGED
|
@@ -1027,6 +1027,10 @@ export const UI = {
|
|
|
1027
1027
|
"ui.sync.cause": "cause {x}",
|
|
1028
1028
|
"ui.sync.name.new": "{n}. {who}: another name — {text}",
|
|
1029
1029
|
"ui.sync.name.changed": "{n}. {who}: name changed — {old} → {new}",
|
|
1030
|
+
"ui.sync.name.married": "{n}. {who}: a name after the marriage — {text}",
|
|
1031
|
+
"ui.sync.name.gone": "{n}. {who}: a name after the marriage no longer in it — {text}",
|
|
1032
|
+
"ui.sync.do.name.gone": "taken off (only edits in the app or other family trees gave it)",
|
|
1033
|
+
"ui.sync.do.name.kept": "kept: a record gives it",
|
|
1030
1034
|
"ui.sync.do.name.user": "the edit wins: shown by the new name, the record's kept beside it",
|
|
1031
1035
|
"ui.sync.name.title": "{n}. {who}: {title} — {old} → {new}",
|
|
1032
1036
|
"ui.sync.do.title": "added to the name, citing the tree sent",
|
|
@@ -1494,6 +1498,20 @@ export const UI = {
|
|
|
1494
1498
|
"ui.settings.archives": "Archives: logins, downloaders asking first",
|
|
1495
1499
|
"ui.settings.wizard.what": "What to set up?",
|
|
1496
1500
|
"ui.settings.wizard.all": "Everything, step by step (the setup)",
|
|
1501
|
+
"ui.settings.terminal": "Terminal for the research's windows (from the Strom app, a conversation handed over): {state}",
|
|
1502
|
+
"ui.settings.terminal.archive": "Terminal for the research's windows (from the Strom app): {state}",
|
|
1503
|
+
"ui.terminal.system": "the system's default ({name})",
|
|
1504
|
+
"ui.terminal.system.plain": "the system's default",
|
|
1505
|
+
"ui.terminal.name.terminal": "Terminal",
|
|
1506
|
+
"ui.terminal.name.iterm2": "iTerm2",
|
|
1507
|
+
"ui.terminal.notHere": "{name} (not installed here: the system's default opens)",
|
|
1508
|
+
"ui.terminal.bySystem": "decided by the system",
|
|
1509
|
+
"ui.terminal.windows": "On Windows the system decides the terminal: Settings → System → For developers → Terminal.",
|
|
1510
|
+
"ui.terminal.pick": "Which terminal should the research's windows open in?",
|
|
1511
|
+
"ui.terminal.missing": "{name} is not installed here; the window opened in the default terminal.",
|
|
1512
|
+
"ui.doc.terminal": "terminal for the windows",
|
|
1513
|
+
"ui.doc.terminal.missing": "{name} chosen, not installed here: the windows open in the system's default",
|
|
1514
|
+
"ui.doc.terminal.unknown": "the terminal \"{value}\" is not one strom knows here: the windows open in the system's default",
|
|
1497
1515
|
"ui.settings.maintenance": "Maintenance: the check of the installation, disk space",
|
|
1498
1516
|
"ui.settings.maintenance.tidy": "Maintenance: the check of the installation, disk space ({size} can be freed)",
|
|
1499
1517
|
"ui.settings.maintenance.update": "Maintenance: the check of the installation, disk space, the update to {version}",
|
package/dist/cli/wizard.js
CHANGED
|
@@ -11,6 +11,8 @@ import { gitVersion } from "../core/git.js";
|
|
|
11
11
|
import { fixGit, offerAgent } from "./fixes.js";
|
|
12
12
|
import { agentsHere, suggestedWay, waysHere, whereToTalk } from "../core/apps.js";
|
|
13
13
|
import { chooseWay, wayName } from "./ways.js";
|
|
14
|
+
import { chooseTerminal, suggestedTerminal } from "./terminals.js";
|
|
15
|
+
import { terminalsHere } from "../core/terminal.js";
|
|
14
16
|
import { chooseEffort, chooseModel } from "./model-choice.js";
|
|
15
17
|
import { ADDON_SWITCHES, PROFILES } from "../agents/profiles.js";
|
|
16
18
|
import { writeStored, PERMISSION_LEVELS } from "../core/config.js";
|
|
@@ -238,6 +240,19 @@ export async function setupWizard(ctx, opts = {}) {
|
|
|
238
240
|
await settleFromApp(ctx, lang);
|
|
239
241
|
else if (stromAppState(s) !== "no" && appOpensLinks(s))
|
|
240
242
|
await offerLinks(ctx, lang);
|
|
243
|
+
// 10. The terminal for the research's windows (a link from the Strom app, a conversation handed over): asked only on a
|
|
244
|
+
// Mac with more than one — the one the setup runs in suggested (run again: the one chosen); this computer's alone
|
|
245
|
+
if (process.platform === "darwin" && terminalsHere(ctx.env).length > 1) {
|
|
246
|
+
const now = s.terminal() ?? "system";
|
|
247
|
+
const picked = await chooseTerminal(ctx, lang, suggestedTerminal(ctx), keep);
|
|
248
|
+
if (picked !== undefined && picked !== now) {
|
|
249
|
+
if (picked === "system")
|
|
250
|
+
delete cfg.terminal;
|
|
251
|
+
else
|
|
252
|
+
cfg.terminal = picked;
|
|
253
|
+
s.save();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
241
256
|
out();
|
|
242
257
|
out(`${ui(lang, "ui.setup.done")} (${langName(lang, lang)} · ${ctx.display(home)})`);
|
|
243
258
|
return { home, lang, ...(agent ? { agent } : {}), permissions: level };
|
package/dist/commands/links.js
CHANGED
|
@@ -47,6 +47,15 @@ function keepAnswer(ctx, answer) {
|
|
|
47
47
|
ctx.settings.config.links = answer;
|
|
48
48
|
ctx.settings.save();
|
|
49
49
|
}
|
|
50
|
+
/** The window a link opens: in the terminal the person chose (this computer's setting, never the link's), said in their language. */
|
|
51
|
+
function windowOf(ctx) {
|
|
52
|
+
const terminal = ctx.settings.terminal();
|
|
53
|
+
return { ...(terminal ? { terminal } : {}), lang: ctx.uiLang() };
|
|
54
|
+
}
|
|
55
|
+
/** The chosen terminal not here: the window opened in the system's (said in it). */
|
|
56
|
+
function fellBack(opened) {
|
|
57
|
+
return opened === "fallback" ? { terminal: "fallback" } : {};
|
|
58
|
+
}
|
|
50
59
|
/** macOS: the terminal program this runs in, by its mark (TERM_PROGRAM). */
|
|
51
60
|
const TERMINALS = { Apple_Terminal: "Terminal", "iTerm.app": "iTerm", WezTerm: "WezTerm", ghostty: "Ghostty" };
|
|
52
61
|
/**
|
|
@@ -404,8 +413,8 @@ register({
|
|
|
404
413
|
if (link.action === "menu") {
|
|
405
414
|
const where = ctx.settings.home()?.value ?? configDir(ctx.env);
|
|
406
415
|
fs.mkdirSync(where, { recursive: true });
|
|
407
|
-
openInNewTerminal([], where, ctx.env);
|
|
408
|
-
return { text: "", data: { done: true, action: "menu" } };
|
|
416
|
+
const opened = openInNewTerminal([], where, ctx.env, process.platform, windowOf(ctx));
|
|
417
|
+
return { text: "", data: { done: true, action: "menu", ...fellBack(opened) } };
|
|
409
418
|
}
|
|
410
419
|
// a tree of the app becomes a research: always in a terminal (it asks the name, waits for the app)
|
|
411
420
|
if (link.action === "new") {
|
|
@@ -419,9 +428,10 @@ register({
|
|
|
419
428
|
}
|
|
420
429
|
const where = ctx.settings.home()?.value ?? configDir(ctx.env);
|
|
421
430
|
fs.mkdirSync(where, { recursive: true });
|
|
422
|
-
|
|
431
|
+
const opened = openInNewTerminal(["link", "open", linkText(link, linkScheme(ctx.env))], where, ctx.env, process.platform, windowOf(ctx));
|
|
432
|
+
if (opened === false)
|
|
423
433
|
return say(ctx, "ui.link.noterminal.new");
|
|
424
|
-
return { text: "", data: { done: true, action: "new" } };
|
|
434
|
+
return { text: "", data: { done: true, action: "new", ...fellBack(opened) } };
|
|
425
435
|
}
|
|
426
436
|
if (link.action === "media") {
|
|
427
437
|
// an original the app sent: the file itself, in the viewer of the system; nothing written
|
|
@@ -452,12 +462,12 @@ register({
|
|
|
452
462
|
const quiet = await withoutTerminal(ctx, link, copies[0].root);
|
|
453
463
|
if (quiet)
|
|
454
464
|
return quiet;
|
|
455
|
-
const opened = openInNewTerminal(["link", "open", linkText(link, linkScheme(ctx.env))], copies[0].root, ctx.env);
|
|
465
|
+
const opened = openInNewTerminal(["link", "open", linkText(link, linkScheme(ctx.env))], copies[0].root, ctx.env, process.platform, windowOf(ctx));
|
|
456
466
|
if (opened === false)
|
|
457
467
|
return link.action === "send"
|
|
458
468
|
? say(ctx, "ui.link.noterminal", { item: `${ui(ctx.uiLang(), "ui.more.title")} → ${ui(ctx.uiLang(), "ui.more.sync")}` })
|
|
459
469
|
: say(ctx, "ui.link.noterminal.any", { name: copies[0].name });
|
|
460
|
-
return { text: "", data: { done: true, action: link.action, tree: copies[0].root } };
|
|
470
|
+
return { text: "", data: { done: true, action: link.action, tree: copies[0].root, ...fellBack(opened) } };
|
|
461
471
|
}
|
|
462
472
|
const got = excerptOf(ctx, link);
|
|
463
473
|
if ("file" in got) {
|
package/dist/commands/setup.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
// setup · doctor · config — the environment around the trees.
|
|
2
|
+
import { terminalHere } from "../core/terminal.js";
|
|
3
|
+
import { terminalName, terminalState } from "../cli/terminals.js";
|
|
2
4
|
import { refusedHosts } from "../core/terms.js";
|
|
3
5
|
import { gitSize, lastCompacted } from "../core/history.js";
|
|
4
6
|
import fs from "node:fs";
|
|
@@ -410,6 +412,21 @@ function diagnose(ctx) {
|
|
|
410
412
|
add("appurl", "fail", t(appUrl.source === "env" ? "ui.doc.appurl.env" : "ui.doc.appurl.config", { value: appUrl.value }), appUrl.source === "env"
|
|
411
413
|
? t("ui.doc.appurl.env.fix", { web: appWebPages(ctx.env) })
|
|
412
414
|
: placeholders(lang, "strom config set strom.app.url <address> · strom config unset strom.app.url"));
|
|
415
|
+
// The terminal strom opens its windows in (a link from the Strom app, a conversation handed over): the one chosen
|
|
416
|
+
// (macOS), gone: said with the way back to the system's; Windows: the system decides it. A value this system does not
|
|
417
|
+
// take (by hand, a newer strom's, another system's) counts as the system's: said with the way back too
|
|
418
|
+
const unknownTerm = ctx.settings.terminalUnknown();
|
|
419
|
+
if (unknownTerm !== undefined)
|
|
420
|
+
add("terminal", "warn", t("ui.doc.terminal.unknown", { value: unknownTerm.replace(/\p{Cc}/gu, "").slice(0, 40) }), "strom config unset terminal");
|
|
421
|
+
else if (process.platform === "darwin") {
|
|
422
|
+
const term = ctx.settings.terminal();
|
|
423
|
+
if (term && !terminalHere(term, ctx.env))
|
|
424
|
+
add("terminal", "warn", t("ui.doc.terminal.missing", { name: terminalName(lang, term) }), "strom config unset terminal");
|
|
425
|
+
else
|
|
426
|
+
add("terminal", "ok", terminalState(ctx, lang));
|
|
427
|
+
}
|
|
428
|
+
else if (process.platform === "win32")
|
|
429
|
+
add("terminal", "ok", t("ui.terminal.windows"));
|
|
413
430
|
// …and whether it may start the research here (strom-research:// links): only while the app is wanted
|
|
414
431
|
if (app !== "no" && appOpensLinks(ctx.settings)) {
|
|
415
432
|
const links = linkHandlerState(ctx.env);
|
package/dist/commands/start.js
CHANGED
|
@@ -189,10 +189,15 @@ register({
|
|
|
189
189
|
// In a terminal: a new window, not a conversation inside this one.
|
|
190
190
|
if (agentHere && !opts.print) {
|
|
191
191
|
const say = about ? ["--task", about.id] : said ? ["--say", said] : [];
|
|
192
|
-
|
|
192
|
+
// in the terminal the person chose (this computer's setting), said in the research's language
|
|
193
|
+
const terminal = ctx.settings.terminal();
|
|
194
|
+
const opened = openInNewTerminal(["chat", "--agent", agent, "--where", "terminal", ...say], tree.root, ctx.env, process.platform, { ...(terminal ? { terminal } : {}), lang });
|
|
193
195
|
if (opened === "recent")
|
|
194
196
|
return { text: ui(lang, "ui.chat.handover.recent", { agent: profile.name }), data: { agent, handover: "recent", cwd: tree.root } };
|
|
195
|
-
return {
|
|
197
|
+
return {
|
|
198
|
+
text: ui(lang, opened ? "ui.chat.handover" : "ui.chat.handover.failed", { agent: profile.name }),
|
|
199
|
+
data: { agent, handover: opened ? "window" : "none", cwd: tree.root, ...(opened === "fallback" ? { terminal: "fallback" } : {}) },
|
|
200
|
+
};
|
|
196
201
|
}
|
|
197
202
|
const base = {
|
|
198
203
|
root: tree.root,
|
package/dist/commands/sync.js
CHANGED
|
@@ -124,7 +124,9 @@ function changeLine(tree, c, incoming, lang) {
|
|
|
124
124
|
? "ui.sync.place.located"
|
|
125
125
|
: c.kind === "fact.part"
|
|
126
126
|
? `ui.sync.fact.part${!c.part ? ".gone" : c.wasPart && c.wasPart.role !== c.part.role ? ".role" : c.wasPart ? ".link" : ""}`
|
|
127
|
-
:
|
|
127
|
+
: c.kind === "name.new" && c.married
|
|
128
|
+
? "ui.sync.name.married"
|
|
129
|
+
: `ui.sync.${c.kind}`;
|
|
128
130
|
// the app's sources and what a fact takes from them: said what becomes of them (a reading of a record, or a lead)
|
|
129
131
|
const does = c.kept
|
|
130
132
|
? "ui.sync.do.kept"
|
|
@@ -136,21 +138,23 @@ function changeLine(tree, c, incoming, lang) {
|
|
|
136
138
|
? "ui.sync.do.parents"
|
|
137
139
|
: c.kind === "name.changed" && c.action === "user"
|
|
138
140
|
? "ui.sync.do.name.user"
|
|
139
|
-
: c.kind === "name.
|
|
140
|
-
? `ui.sync.do.
|
|
141
|
-
: c.kind === "
|
|
142
|
-
?
|
|
143
|
-
: c.kind === "
|
|
144
|
-
? "ui.sync.do.
|
|
145
|
-
: c.kind === "
|
|
146
|
-
? "ui.sync.do.
|
|
147
|
-
: c.kind === "fact.
|
|
148
|
-
?
|
|
149
|
-
: c.kind
|
|
150
|
-
? `ui.sync.do
|
|
151
|
-
: c.kind === "fact.
|
|
152
|
-
?
|
|
153
|
-
:
|
|
141
|
+
: c.kind === "name.gone"
|
|
142
|
+
? `ui.sync.do.name.${c.action === "remove" ? "gone" : "kept"}`
|
|
143
|
+
: c.kind === "name.title" && (c.action === "add" || c.action === "user")
|
|
144
|
+
? `ui.sync.do.title${c.action === "user" ? ".user" : ""}`
|
|
145
|
+
: c.kind === "note.changed" && c.action !== "pick"
|
|
146
|
+
? "ui.sync.do.note.changed"
|
|
147
|
+
: c.kind === "family.union" && c.action !== "report"
|
|
148
|
+
? "ui.sync.do.family.union"
|
|
149
|
+
: c.kind === "fact.detail" && c.action === "add"
|
|
150
|
+
? "ui.sync.do.detail"
|
|
151
|
+
: c.kind === "fact.part" && (c.action === "add" || c.action === "user")
|
|
152
|
+
? `ui.sync.do.part${c.action === "user" ? ".user" : ""}`
|
|
153
|
+
: c.kind.startsWith("source.") || c.kind === "fact.cite" || c.kind === "person.cite" || c.kind === "family.cite"
|
|
154
|
+
? `ui.sync.do.${c.kind}${c.reads || (c.kind === "source.new" && newReads(c, incoming)) ? ".read" : ""}`
|
|
155
|
+
: c.kind === "fact.new" && c.reads
|
|
156
|
+
? "ui.sync.do.reading"
|
|
157
|
+
: `ui.sync.do.${c.action}`;
|
|
154
158
|
// an archive: nobody reads a record there, nor is an agent told — said without it
|
|
155
159
|
const said = actsAsArchive(tree) && `${does}.archive` in UI ? `${does}.archive` : does;
|
|
156
160
|
return `${ui(lang, key, v)} → ${ui(lang, said)}`;
|
package/dist/core/config.js
CHANGED
|
@@ -17,6 +17,7 @@ import { acquireLock } from "./lock.js";
|
|
|
17
17
|
import { defaultAppUrl, isAppVersion, isStromAppOrigin, STROM_APP_BETA_URL, STROM_APP_URL } from "./stromapp.js";
|
|
18
18
|
import { updateChannel } from "./update.js";
|
|
19
19
|
import { WEB_PER_HOST, WEB_SOFT } from "./metrics.js";
|
|
20
|
+
import { TERMINAL_CHOICES, terminalChoices } from "./terminal.js";
|
|
20
21
|
export const PERMISSION_LEVELS = ["ask", "auto", "full"];
|
|
21
22
|
/** Earlier names of the levels, still understood. */
|
|
22
23
|
const PERMISSION_ALIASES = { list: "auto", bypass: "full" };
|
|
@@ -88,6 +89,9 @@ export const SETTINGS = [
|
|
|
88
89
|
{ key: "updates", env: "STROM_UPDATES", tree: false, kind: "choice", choices: ["check", "off"], description: "look for new versions of strom: check (default — at most once a day, one small file from the project's releases; strom says so, strom update installs it) or off" },
|
|
89
90
|
{ key: "strom.app", env: "", tree: false, kind: "choice", choices: ["yes", "no"], description: "you use the Strom app: yes (strom says which file to import into it), no (strom never mentions it) — unset: strom notices it itself" },
|
|
90
91
|
{ key: "app.browser", env: "STROM_APP_BROWSER", tree: false, kind: "choice", choices: ["chrome", "edge", "brave", "opera", "firefox", "chromium"], description: "the browser strom opens the Strom app in — the one its tree came from (strom keeps it), or yours: chrome, edge, brave, opera, firefox, chromium (another Chromium: Vivaldi, Arc). The Strom app installed from a browser always comes first (from this one when it is installed from several); without one, a tab of this browser — unset: of the default browser when the app reaches strom from it, else of the first such browser here" },
|
|
92
|
+
// This computer's alone (never a research's strom.json: a research is packed and shared, its folder would choose the
|
|
93
|
+
// program that runs) and never from a link; fixed values only, each a fixed bundle ID (core/terminal.ts).
|
|
94
|
+
{ key: "terminal", env: "STROM_TERMINAL", tree: false, kind: "choice", choices: TERMINAL_CHOICES, description: "the terminal strom opens its windows in (a link from the Strom app, a conversation handed over): system (default: what opens .command files), terminal, iterm2 — macOS; one not installed: the system's (said in the window). Windows: the system's default terminal application decides" },
|
|
91
95
|
// Measured, never typed: strom media calibrate (a person starts it) keeps it per agent and model; unset: the defaults.
|
|
92
96
|
{ key: "views.size", env: "", tree: false, kind: "views", description: "the size of the scan views for the research's agent and model: find (a whole image, to find an entry; default 1400 px) and read (a half, a crop, a reader's view; default what the model takes) — set by strom media calibrate, which a person starts; strom config unset views.size returns to the defaults" },
|
|
93
97
|
{ key: "tune.transcripts", env: "STROM_TUNE_TRANSCRIPTS", tree: false, kind: "choice", choices: ["on", "off"], description: "the transcripts Claude Code keeps on this computer of the sessions strom started, read to tune how scans are read (the context cleared, files opened again, what each reader cost): on (default — only the tokens of each request, the names of the tools and the files read, never the conversation; nothing leaves this computer) or off (nothing read)" },
|
|
@@ -211,8 +215,10 @@ export function checkValue(def, raw, resolvePath, env = {}) {
|
|
|
211
215
|
return def.key === "model.effort" ? v.toLowerCase() : v;
|
|
212
216
|
case "choice": {
|
|
213
217
|
const c = def.key === "agent.permissions" ? (PERMISSION_ALIASES[v.toLowerCase()] ?? v.toLowerCase()) : v.toLowerCase();
|
|
214
|
-
|
|
215
|
-
|
|
218
|
+
// the terminal: what this system takes (macOS its terminals, elsewhere the system's alone)
|
|
219
|
+
const choices = def.key === "terminal" ? terminalChoices() : def.choices;
|
|
220
|
+
if (!choices?.includes(c))
|
|
221
|
+
throw new UsageError(`invalid ${def.key} "${raw}"`, { hint: choices?.join(", ") });
|
|
216
222
|
return c;
|
|
217
223
|
}
|
|
218
224
|
case "pace": {
|
|
@@ -474,6 +480,19 @@ export class Settings {
|
|
|
474
480
|
const r = this.resolve("app.browser");
|
|
475
481
|
return r ? String(r.value) : undefined;
|
|
476
482
|
}
|
|
483
|
+
/**
|
|
484
|
+
* The terminal strom opens its windows in, if the person chose one (env, else the config; never a tree's). A value of
|
|
485
|
+
* the config this system does not take (written by hand, by a newer strom, on another system) counts as the system's.
|
|
486
|
+
*/
|
|
487
|
+
terminal(platform = process.platform) {
|
|
488
|
+
const r = this.resolve("terminal");
|
|
489
|
+
return r && r.value !== "system" && terminalChoices(platform).includes(String(r.value)) ? String(r.value) : undefined;
|
|
490
|
+
}
|
|
491
|
+
/** The terminal in the config this system does not take (doctor says it, with the way back): none when it takes it. */
|
|
492
|
+
terminalUnknown(platform = process.platform) {
|
|
493
|
+
const v = readStored(this.config, "terminal", "");
|
|
494
|
+
return v !== undefined && !this.env.STROM_TERMINAL && !this.flagOf("terminal") && !terminalChoices(platform).includes(String(v)) ? String(v) : undefined;
|
|
495
|
+
}
|
|
477
496
|
/** Where the user wants to talk with the agent, if they said (env, else the config); see whereToTalk. */
|
|
478
497
|
agentWhere() {
|
|
479
498
|
const r = this.resolve("agent.where");
|
package/dist/core/import.js
CHANGED
|
@@ -173,6 +173,30 @@ export function readGedName(n, file) {
|
|
|
173
173
|
const nameless = file.app && !surn && (file.unknownIsNoSurname ? /^\?\s*\/\s*(?:Unknown\s*)?\/$/u : /^\?\s*\/\s*\/$/u).test(n.value.trim());
|
|
174
174
|
return { name, nameless };
|
|
175
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* The married names a file gives as _MARNM (Legacy, PAF and other programs): under a NAME (2 _MARNM) of that name's
|
|
178
|
+
* given name, under the person (1 _MARNM) of their first name's. A value with slashes is a whole name, else a surname.
|
|
179
|
+
* Read the same way by strom intake and strom sync.
|
|
180
|
+
*/
|
|
181
|
+
export function marnmNames(r, givenOf) {
|
|
182
|
+
const out = [];
|
|
183
|
+
const add = (value, given) => {
|
|
184
|
+
const v = value.replace(/\s+/g, " ").trim();
|
|
185
|
+
if (!v)
|
|
186
|
+
return;
|
|
187
|
+
const n = v.includes("/") ? parseName(v) : { given: "", surname: v };
|
|
188
|
+
const name = { given: n.given || given, surname: n.surname, kind: "married" };
|
|
189
|
+
if ((name.given || name.surname) && !out.some((x) => sameName(x, name)))
|
|
190
|
+
out.push(name);
|
|
191
|
+
};
|
|
192
|
+
const names = children(r, "NAME");
|
|
193
|
+
for (const n of names)
|
|
194
|
+
for (const m of children(n, "_MARNM"))
|
|
195
|
+
add(m.value, givenOf(n));
|
|
196
|
+
for (const m of children(r, "_MARNM"))
|
|
197
|
+
add(m.value, names[0] ? givenOf(names[0]) : "");
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
176
200
|
export function importGedcom(tree, text, opts) {
|
|
177
201
|
const { records, problems } = parseGedcomText(text);
|
|
178
202
|
/** A file of the Strom app (its HEAD: 1 SOUR STROM): its own ways of writing are read as it means them. */
|
|
@@ -334,16 +358,21 @@ export function importGedcom(tree, text, opts) {
|
|
|
334
358
|
tree.put(rec, op);
|
|
335
359
|
return true;
|
|
336
360
|
};
|
|
337
|
-
const namesOf = (r) =>
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
found.given
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
361
|
+
const namesOf = (r) => {
|
|
362
|
+
const names = children(r, "NAME")
|
|
363
|
+
.map((n) => {
|
|
364
|
+
const found = readGedName(n, fromApp).name;
|
|
365
|
+
// a word that describes the person in place of a name (stillborn, son): the name is "?", the word goes to a note
|
|
366
|
+
if (notAName(found.given))
|
|
367
|
+
found.given = "?";
|
|
368
|
+
// a slash inside a name ("/⟨K/Č⟩emenská/") is kept as "|": the name stays one GEDCOM can write
|
|
369
|
+
return { ...found, given: found.given.replace(/\//g, "|"), surname: found.surname.replace(/\//g, "|") };
|
|
370
|
+
})
|
|
371
|
+
.map((n) => (n.given || n.surname ? n : { ...n, given: "?", surname: "" }));
|
|
372
|
+
// a married name other programs give apart (_MARNM): a name of the married kind, unless the person has it already
|
|
373
|
+
const given = (n) => (notAName(readGedName(n, fromApp).name.given) ? "" : readGedName(n, fromApp).name.given.replace(/\//g, "|"));
|
|
374
|
+
return [...names, ...marnmNames(r, given).filter((m) => !names.some((n) => sameName(n, m)))];
|
|
375
|
+
};
|
|
347
376
|
/** The Strom app keeps what a birth or death entry adds in the person's note ("Úmrtí: Věk: 61 let"): back to the fact; the other lines are returned. */
|
|
348
377
|
const factNotes = (r, personEvents) => {
|
|
349
378
|
const personNotes = [];
|
package/dist/core/live.js
CHANGED
|
@@ -679,7 +679,7 @@ function idsOf(applied, known) {
|
|
|
679
679
|
return Object.keys(ids.persons).length || Object.keys(ids.sources).length ? { ids } : {};
|
|
680
680
|
}
|
|
681
681
|
/** What the bridge does that an app may ask about (each added once, never taken away). */
|
|
682
|
-
export const BRIDGE_FEATURES = ["sync.again", "sync.undoneSince", "sync.takenBack", "sync.conflictEdit", "sync.since", "sync.ids", "family.noCouple", "family.alone", "adopt.transfer", "adopt.empty", "material.list", "person.titles", "media.codes", "hypothesis.links", "status.recent", "spend.readers", "hypothesis.intree", "archive.permission", "task.check", "waiting.groups", "waiting.sent", "waiting.note", "batch.kept", "live.end", "whats.new"];
|
|
682
|
+
export const BRIDGE_FEATURES = ["sync.again", "sync.undoneSince", "sync.takenBack", "sync.conflictEdit", "sync.since", "sync.ids", "family.noCouple", "family.alone", "adopt.transfer", "adopt.empty", "material.list", "person.titles", "media.codes", "hypothesis.links", "status.recent", "spend.readers", "hypothesis.intree", "archive.permission", "task.check", "waiting.groups", "waiting.sent", "waiting.note", "batch.kept", "live.end", "whats.new", "terminal.choice", "person.marriedSurname"];
|
|
683
683
|
/** /status recent, when it is ready. */
|
|
684
684
|
function recentOf(root, env, head) {
|
|
685
685
|
const recent = head ? recentNow(root, env, head) : undefined;
|
package/dist/core/shortcut.js
CHANGED
|
@@ -9,6 +9,8 @@ import { configDir, desktopDir, userHome } from "./paths.js";
|
|
|
9
9
|
import { stromLauncher } from "./self.js";
|
|
10
10
|
import { isAgentMark } from "./which.js";
|
|
11
11
|
import { assetPath } from "./assets.js";
|
|
12
|
+
import { macTerminal } from "./terminal.js";
|
|
13
|
+
import { ui } from "../cli/ui.js";
|
|
12
14
|
/** Strom Research's icon, as each system takes it. */
|
|
13
15
|
export const ICON = { png: assetPath("icon", "icon-512.png"), ico: assetPath("icon", "icon.ico") };
|
|
14
16
|
/** Where the Windows shortcut's command file lives (the desktop has the .lnk with the icon). */
|
|
@@ -97,7 +99,11 @@ export function createShortcut(name, env, platform = process.platform) {
|
|
|
97
99
|
*/
|
|
98
100
|
/** One handover window at a time: another asked within this long is not opened. */
|
|
99
101
|
export const HANDOVER_GAP_MS = 15_000;
|
|
100
|
-
|
|
102
|
+
/**
|
|
103
|
+
* `terminal`: the one the person chose (core/terminal.ts; macOS) — opened by its bundle ID; not installed: the system's,
|
|
104
|
+
* the window saying so first in `lang` ("fallback").
|
|
105
|
+
*/
|
|
106
|
+
export function openInNewTerminal(argv, cwd, env, platform = process.platform, opts = {}) {
|
|
101
107
|
if (env.STROM_NO_OPEN === "1")
|
|
102
108
|
return false;
|
|
103
109
|
const { command, args } = stromLauncher();
|
|
@@ -129,6 +135,23 @@ export function openInNewTerminal(argv, cwd, env, platform = process.platform) {
|
|
|
129
135
|
const body = ["#!/bin/sh", 'rm -f -- "$0"', ...(marks.length ? [`unset ${marks.join(" ")}`] : []), ...own.map(([k, v]) => `export ${k}=${q(v)}`), "export STROM_HANDOVER=1", `cd ${q(cwd)}`, `exec ${[command, ...args, ...argv].map(q).join(" ")}`, ""];
|
|
130
136
|
fs.writeFileSync(file, body.join("\n"), { mode: 0o755 });
|
|
131
137
|
fs.chmodSync(file, 0o755);
|
|
138
|
+
// macOS, a terminal chosen: opened by its bundle ID (LaunchServices, as a double-click: no permission asked); not
|
|
139
|
+
// installed (open says so), the system's — the window says it first, then goes on
|
|
140
|
+
const chosen = macTerminal(opts.terminal, platform);
|
|
141
|
+
if (chosen) {
|
|
142
|
+
const r = spawnSync("open", ["-b", chosen.id, file], { stdio: "ignore", timeout: 15_000, env: env });
|
|
143
|
+
if (r.status === 0 || (r.status === null && r.error?.code !== "ENOENT"))
|
|
144
|
+
return true;
|
|
145
|
+
if (r.status === null)
|
|
146
|
+
return false; // no open here
|
|
147
|
+
const lang = opts.lang ?? "en";
|
|
148
|
+
const said = ui(lang, "ui.terminal.missing", { name: ui(lang, `ui.terminal.name.${opts.terminal}`) });
|
|
149
|
+
fs.writeFileSync(file, [body[0], body[1], `printf '%s\\n\\n' ${q(said)}`, ...body.slice(2)].join("\n"), { mode: 0o755 });
|
|
150
|
+
const o = spawn("open", [file], { detached: true, stdio: "ignore", env: env });
|
|
151
|
+
o.on("error", () => undefined);
|
|
152
|
+
o.unref();
|
|
153
|
+
return "fallback";
|
|
154
|
+
}
|
|
132
155
|
// macOS: a .command file opens in Terminal. Linux: the desktop's terminal, when there is one.
|
|
133
156
|
const how = platform === "darwin"
|
|
134
157
|
? [["open", [file]]]
|
package/dist/core/sync.js
CHANGED
|
@@ -27,7 +27,7 @@ import { phrase } from "./phrases.js";
|
|
|
27
27
|
import { addChild, addEvent, addFamily, addName, addNote, editEvent, editPerson, retractEvent, retractPerson } from "./actions.js";
|
|
28
28
|
import { exportGedcom, REFN_TYPE } from "../gedcom/export.js";
|
|
29
29
|
import { children, parseGedcomText, val } from "../gedcom/parse.js";
|
|
30
|
-
import { fromFlexDate, houseOf, importDate, readGedName, readGedSex, stromAppFile } from "./import.js";
|
|
30
|
+
import { fromFlexDate, houseOf, importDate, marnmNames, readGedName, readGedSex, stromAppFile } from "./import.js";
|
|
31
31
|
import { humanAge, normalizeAge } from "./age.js";
|
|
32
32
|
import { dateYears } from "./gdate.js";
|
|
33
33
|
import { create, update } from "./records.js";
|
|
@@ -42,7 +42,7 @@ import { now, Tree, typeOfId } from "./tree.js";
|
|
|
42
42
|
import { actsAsArchive } from "./state.js";
|
|
43
43
|
import { appSides } from "./conflicts.js";
|
|
44
44
|
import { APP_DATA_SEX_UNKNOWN, isAppVersion } from "./stromapp.js";
|
|
45
|
-
import { cleanTitle, familiesAsPartner, formatName, foundTitles, gedcomTitledName, primaryName, titledName } from "./people.js";
|
|
45
|
+
import { cleanTitle, familiesAsPartner, formatName, foundTitles, gedcomTitledName, parseName, primaryName, sameName as sameWords, titledName } from "./people.js";
|
|
46
46
|
/** The words of a _STAT (the Strom app's own, also Legacy's and FTM's) as a union, or nothing it names. */
|
|
47
47
|
function unionOfStat(value) {
|
|
48
48
|
const v = (value ?? "").trim().toLowerCase();
|
|
@@ -532,9 +532,17 @@ export function readGedcom(text) {
|
|
|
532
532
|
const titles = named[0] ? titlesFrom(named[0].name.prefix, named[0].name.suffix) : undefined;
|
|
533
533
|
// (the research's own person: no name at all, as the research writes it — never another name, never a rename);
|
|
534
534
|
// what stands for no name ("N/A /Chrpa/", "N.N.") is the research's "?" (T08)
|
|
535
|
-
const
|
|
536
|
-
|
|
537
|
-
|
|
535
|
+
const kept = named.filter(({ nameless }) => !(ours && nameless));
|
|
536
|
+
const names = kept.map(({ name, nameless }, i) => (nameless ? "? //" : gedcomTitledName(i === 0 && titles ? { given: name.given, surname: name.surname } : name)));
|
|
537
|
+
// the names after a marriage (TYPE married; another program's _MARNM, a name of its own), never the one shown
|
|
538
|
+
const married = names.filter((_, i) => i > 0 && kept[i].name.kind === "married");
|
|
539
|
+
for (const m of names.length ? marnmNames(r, (n) => readGedName(n, fromApp).name.given) : []) {
|
|
540
|
+
const line = gedcomTitledName(m);
|
|
541
|
+
if (names.some((n) => nameKey(n) === nameKey(line)))
|
|
542
|
+
continue;
|
|
543
|
+
names.push(line);
|
|
544
|
+
married.push(line);
|
|
545
|
+
}
|
|
538
546
|
// a placeholder of the Strom app (an unknown parent drawn in the tree): nobody
|
|
539
547
|
if (!ours && !names.length)
|
|
540
548
|
continue;
|
|
@@ -557,6 +565,7 @@ export function readGedcom(text) {
|
|
|
557
565
|
said: allNotes(r, noteText),
|
|
558
566
|
...(cites.length ? { cites } : {}),
|
|
559
567
|
...(titles ? { titles } : {}),
|
|
568
|
+
...(married.length ? { married } : {}),
|
|
560
569
|
});
|
|
561
570
|
}
|
|
562
571
|
// how a child belongs to a family (the child's FAMC + PEDI: adopted, foster, step): none said is birth
|
|
@@ -778,10 +787,16 @@ export function readStromJson(data) {
|
|
|
778
787
|
}
|
|
779
788
|
const cites = citesOf(p.sourceIds);
|
|
780
789
|
const titles = titlesFrom(p.titleBefore, p.titleAfter);
|
|
790
|
+
// the surname after the marriage (spec 010) with the person's first name: a name of the married kind, after the one
|
|
791
|
+
// shown and before the other forms (as the app's GEDCOM gives it)
|
|
792
|
+
const marriedSurname = typeof p.marriedSurname === "string" ? p.marriedSurname.replace(/[/\s]+/g, " ").trim() : "";
|
|
793
|
+
const married = marriedSurname ? `${p.firstName ?? ""} /${marriedSurname}/`.trim() : undefined;
|
|
794
|
+
const names = [name, ...(married ? [married] : []), ...(p.nameVariants ?? [])].filter((n) => n.replace(/[/?\s]/g, ""));
|
|
781
795
|
persons.set(key, {
|
|
782
796
|
key,
|
|
783
797
|
...(titles ? { titles } : {}),
|
|
784
|
-
names:
|
|
798
|
+
names: names.filter((n, i) => n !== married || names.findIndex((m) => nameKey(m) === nameKey(n)) === i),
|
|
799
|
+
...(married && names.findIndex((n) => nameKey(n) === nameKey(married)) > 0 ? { married: [married] } : {}),
|
|
785
800
|
// the sex left unknown a value like the others (U01): the app's gender unknown from its data version 12, the
|
|
786
801
|
// research's unknown standing beside its stand-in (research.sexU) before it; else male and female only — a gender
|
|
787
802
|
// missing says nothing of it (B5-d)
|
|
@@ -2091,6 +2106,48 @@ export function planSync(tree, incoming, edits, opts = {}) {
|
|
|
2091
2106
|
push({ kind: "note.new", action: "add", ...owner, text: out.join("\n") });
|
|
2092
2107
|
}
|
|
2093
2108
|
};
|
|
2109
|
+
/**
|
|
2110
|
+
* The names after a marriage (spec 010; the Strom app keeps one, the last, with its own first name): the same by its
|
|
2111
|
+
* surname — "Marie /Nováková/" of the app is the research's "Maria /Nováková/". One the research lacks is added of
|
|
2112
|
+
* the married kind (a name of the research the file now says is one after a marriage: that kind); one the app was
|
|
2113
|
+
* given and no longer has (taken off, or changed: the new one added) goes where only the user's edits or other
|
|
2114
|
+
* family trees gave it, a record's stays (said) — never the name they are shown by.
|
|
2115
|
+
*/
|
|
2116
|
+
const marriedDiff = (id, name, p, o, b) => {
|
|
2117
|
+
const surname = (n) => exact(/\/([^/]*)\//.exec(n)?.[1] ?? "");
|
|
2118
|
+
const same = (n, list) => list.some((m) => nameKey(m) === nameKey(n) || (!!surname(n) && surname(m) === surname(n)));
|
|
2119
|
+
const person = tree.get(id);
|
|
2120
|
+
if (!person)
|
|
2121
|
+
return;
|
|
2122
|
+
const ofResearch = (n) => person.names.find((m) => nameKey(gedcomName(m)) === nameKey(n));
|
|
2123
|
+
const shown = o.names[0] ?? "";
|
|
2124
|
+
for (const n of p.married ?? []) {
|
|
2125
|
+
if (same(n, o.married ?? []))
|
|
2126
|
+
continue;
|
|
2127
|
+
const has = ofResearch(n);
|
|
2128
|
+
if (has) {
|
|
2129
|
+
// the research's own words of another kind: the file says they are a name after a marriage
|
|
2130
|
+
if (!has.kind && nameKey(n) !== nameKey(shown))
|
|
2131
|
+
push({ kind: "name.new", action: "add", person: id, name, text: n, married: true });
|
|
2132
|
+
continue;
|
|
2133
|
+
}
|
|
2134
|
+
// the research took it off since the app was given it: that stays
|
|
2135
|
+
if (b && (b.married ?? []).some((m) => nameKey(m) === nameKey(n)))
|
|
2136
|
+
continue;
|
|
2137
|
+
push({ kind: "name.new", action: "add", person: id, name, text: n, married: true });
|
|
2138
|
+
}
|
|
2139
|
+
if (!b || partial)
|
|
2140
|
+
return;
|
|
2141
|
+
for (const g of b.married ?? []) {
|
|
2142
|
+
// still in the file (as a name after a marriage by its surname, or as another form), or no longer the research's
|
|
2143
|
+
if (p.names.some((n) => nameKey(n) === nameKey(g)) || same(g, p.married ?? []))
|
|
2144
|
+
continue;
|
|
2145
|
+
const mine = ofResearch(g);
|
|
2146
|
+
if (!mine || mine.kind !== "married" || nameKey(g) === nameKey(shown))
|
|
2147
|
+
continue;
|
|
2148
|
+
push({ kind: "name.gone", action: nameBacked(tree, mine) ? "report" : "remove", person: id, name, text: gedcomName(mine) });
|
|
2149
|
+
}
|
|
2150
|
+
};
|
|
2094
2151
|
// people
|
|
2095
2152
|
for (const [key, p] of incoming.persons) {
|
|
2096
2153
|
const name = p.names[0] ?? "?";
|
|
@@ -2099,7 +2156,7 @@ export function planSync(tree, incoming, edits, opts = {}) {
|
|
|
2099
2156
|
factsDiff({ person: key, name }, p.facts, [], undefined, false);
|
|
2100
2157
|
citesDiff({ person: key, name }, p.cites, undefined, undefined);
|
|
2101
2158
|
for (const n of p.names.slice(1))
|
|
2102
|
-
push({ kind: "name.new", action: "add", person: key, name, text: n });
|
|
2159
|
+
push({ kind: "name.new", action: "add", person: key, name, text: n, ...(p.married?.includes(n) ? { married: true } : {}) });
|
|
2103
2160
|
for (const t of p.notes)
|
|
2104
2161
|
push({ kind: "note.new", action: "add", person: key, name, text: t });
|
|
2105
2162
|
continue;
|
|
@@ -2127,9 +2184,11 @@ export function planSync(tree, incoming, edits, opts = {}) {
|
|
|
2127
2184
|
const shown = primaryName(tree.get(id));
|
|
2128
2185
|
push({ kind: "name.changed", action: renamed && !nameBacked(tree, shown) ? "correct" : edits === "user" ? "user" : "conflict", person: id, name, text: now, wasName: mine });
|
|
2129
2186
|
}
|
|
2187
|
+
const theirMarried = new Set(p.married ?? []);
|
|
2130
2188
|
for (const n of p.names)
|
|
2131
|
-
if (!((renamed || both) && n === now) && !ourNames.has(nameKey(n)) && !baseNames.has(nameKey(n)))
|
|
2189
|
+
if (!theirMarried.has(n) && !((renamed || both) && n === now) && !ourNames.has(nameKey(n)) && !baseNames.has(nameKey(n)))
|
|
2132
2190
|
push({ kind: "name.new", action: "add", person: id, name, text: n });
|
|
2191
|
+
marriedDiff(id, name, p, o, b);
|
|
2133
2192
|
// the titles of the name they are shown by (T07), each apart and never as a change of the name itself: the user's
|
|
2134
2193
|
// edit where the file's differs from what the app was given — one the name lacks added, a lead's corrected, a
|
|
2135
2194
|
// record's name the user's to decide (or their word: sync.edits user, an archive); the research's changed since
|
|
@@ -3446,9 +3505,30 @@ export function applySync(tree, plan, incoming, source, only) {
|
|
|
3446
3505
|
case "name.new": {
|
|
3447
3506
|
if (!owner || !c.text)
|
|
3448
3507
|
break;
|
|
3508
|
+
// a name of the research the file says is one after a marriage: of that kind now, citing the sync (undo: as it was)
|
|
3509
|
+
const had = c.married ? tree.get(owner)?.names.find((n) => sameWords(n, parseName(c.text))) : undefined;
|
|
3449
3510
|
// beside the name the person is shown by, always last: what the app shows does not move
|
|
3450
|
-
const { name } = addName(tree, owner, { name: c.text, citation: cite("name"), other: true });
|
|
3451
|
-
|
|
3511
|
+
const { name } = addName(tree, owner, { name: c.text, citation: cite("name"), other: true, ...(c.married ? { kind: "married" } : {}) });
|
|
3512
|
+
if (had)
|
|
3513
|
+
applied.push({ do: "name.edit", id: owner, before: JSON.stringify({ name: had, now: exact(`${name.given}|${name.surname}`) }) });
|
|
3514
|
+
else
|
|
3515
|
+
applied.push({ do: "name.add", id: owner, before: exact(`${name.given}|${name.surname}`) });
|
|
3516
|
+
break;
|
|
3517
|
+
}
|
|
3518
|
+
case "name.gone": {
|
|
3519
|
+
// a name after a marriage taken off in the app that only the user's edits or other family trees gave: off,
|
|
3520
|
+
// where it stood kept for an undo (a record's stays: only said)
|
|
3521
|
+
const p = owner && c.action === "remove" && c.text ? tree.get(owner) : undefined;
|
|
3522
|
+
const at = p ? p.names.findIndex((n) => n.kind === "married" && n !== primaryName(p) && nameKey(gedcomName(n)) === nameKey(c.text) && !nameBacked(tree, n)) : -1;
|
|
3523
|
+
if (!p || at < 0)
|
|
3524
|
+
break;
|
|
3525
|
+
const gone = p.names[at];
|
|
3526
|
+
update(tree, p.id, "person", (x) => ({ ...x, names: x.names.filter((_, i) => i !== at) }), {
|
|
3527
|
+
op: "name.remove",
|
|
3528
|
+
summary: `${p.id} name ${gedcomName(gone)} removed: ${phrase(tree.lang, "archive.removed")}`,
|
|
3529
|
+
reason: `${phrase(tree.lang, "archive.removed")}: ${reason}`,
|
|
3530
|
+
});
|
|
3531
|
+
applied.push({ do: "name.drop", id: p.id, before: JSON.stringify({ name: gone, at }) });
|
|
3452
3532
|
break;
|
|
3453
3533
|
}
|
|
3454
3534
|
case "name.changed": {
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// The terminal strom opens its windows in (a link from the Strom app, a
|
|
2
|
+
// conversation handed over): the system's — what opens a .command file — unless
|
|
3
|
+
// the person chose another (terminal, a setting of this computer alone: never a
|
|
4
|
+
// research's strom.json, never a link). macOS only for now: each choice maps to
|
|
5
|
+
// a fixed bundle ID, opened with `open -b`; one not installed: the system's.
|
|
6
|
+
// Windows decides it itself (its default terminal application), Linux keeps its
|
|
7
|
+
// order (core/shortcut.ts).
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { spawnSync } from "node:child_process";
|
|
11
|
+
import { userHome } from "./paths.js";
|
|
12
|
+
export const MAC_TERMINALS = {
|
|
13
|
+
terminal: { id: "com.apple.Terminal", app: "Terminal.app", name: "Terminal", mark: "Apple_Terminal" },
|
|
14
|
+
iterm2: { id: "com.googlecode.iterm2", app: "iTerm.app", name: "iTerm2", mark: "iTerm.app" },
|
|
15
|
+
};
|
|
16
|
+
/** Every value of the setting, on any system (the help names them). */
|
|
17
|
+
export const TERMINAL_CHOICES = ["system", ...Object.keys(MAC_TERMINALS)];
|
|
18
|
+
/** The values this system takes: macOS its terminals, elsewhere the system's alone. */
|
|
19
|
+
export function terminalChoices(platform = process.platform) {
|
|
20
|
+
return platform === "darwin" ? [...TERMINAL_CHOICES] : ["system"];
|
|
21
|
+
}
|
|
22
|
+
/** The chosen terminal of macOS (none: the system's) — one of its own keys only, never one of an object's prototype. */
|
|
23
|
+
export function macTerminal(choice, platform = process.platform) {
|
|
24
|
+
return platform === "darwin" && choice && Object.hasOwn(MAC_TERMINALS, choice) ? MAC_TERMINALS[choice] : undefined;
|
|
25
|
+
}
|
|
26
|
+
/** The folders apps lie in (STROM_APP_DIRS: folders to look in instead — tests). */
|
|
27
|
+
function appDirs(env) {
|
|
28
|
+
if (env.STROM_APP_DIRS !== undefined)
|
|
29
|
+
return env.STROM_APP_DIRS.split(path.delimiter).filter(Boolean);
|
|
30
|
+
return ["/Applications", "/Applications/Utilities", "/System/Applications/Utilities", path.join(userHome(env), "Applications")];
|
|
31
|
+
}
|
|
32
|
+
/** Ask the system (JXA) — nothing when tests give folders of their own. */
|
|
33
|
+
function jxa(env, js, args = []) {
|
|
34
|
+
if (env.STROM_APP_DIRS !== undefined)
|
|
35
|
+
return undefined;
|
|
36
|
+
const r = spawnSync("osascript", ["-l", "JavaScript", "-e", js, ...args], { encoding: "utf8", timeout: 10_000 });
|
|
37
|
+
return r.status === 0 ? r.stdout.trim() : undefined;
|
|
38
|
+
}
|
|
39
|
+
/** The terminals of macOS on this computer, in strom's order: by their bundle ID (NSWorkspace), else in the app folders. */
|
|
40
|
+
export function terminalsHere(env, platform = process.platform) {
|
|
41
|
+
if (platform !== "darwin")
|
|
42
|
+
return [];
|
|
43
|
+
const keys = Object.keys(MAC_TERMINALS);
|
|
44
|
+
const js = 'function run(a){ObjC.import("AppKit");return a.map(function(id){var u=$.NSWorkspace.sharedWorkspace.URLForApplicationWithBundleIdentifier(id);return u.isNil()?"":"y"}).join(",")}';
|
|
45
|
+
const said = jxa(env, js, keys.map((k) => MAC_TERMINALS[k].id))?.split(",");
|
|
46
|
+
return keys.filter((k, i) => said?.[i] === "y" || appDirs(env).some((d) => fs.existsSync(path.join(d, MAC_TERMINALS[k].app))));
|
|
47
|
+
}
|
|
48
|
+
/** Is the chosen terminal on this computer? (the system's always is) */
|
|
49
|
+
export function terminalHere(choice, env, platform = process.platform) {
|
|
50
|
+
return !macTerminal(choice, platform) || terminalsHere(env, platform).includes(choice);
|
|
51
|
+
}
|
|
52
|
+
/** What opens a .command file here (the system's terminal), when the system says: one of the choices, else its name. */
|
|
53
|
+
export function systemTerminal(env, platform = process.platform) {
|
|
54
|
+
if (platform !== "darwin")
|
|
55
|
+
return undefined;
|
|
56
|
+
const js = 'ObjC.import("CoreServices");ObjC.import("AppKit");var r=$.LSCopyDefaultRoleHandlerForContentType($("com.apple.terminal.shell-script"),$.kLSRolesAll);var id=r?ObjC.castRefToObject(r):$();var s=id.isNil()?"":id.js;var u=s?$.NSWorkspace.sharedWorkspace.URLForApplicationWithBundleIdentifier(s):$();s+"\\n"+(s&&!u.isNil()?$.NSFileManager.defaultManager.displayNameAtPath(u.path).js:"")';
|
|
57
|
+
const said = jxa(env, js);
|
|
58
|
+
if (!said)
|
|
59
|
+
return undefined;
|
|
60
|
+
const [id, shown] = said.split("\n");
|
|
61
|
+
const choice = Object.keys(MAC_TERMINALS).find((k) => MAC_TERMINALS[k].id.toLowerCase() === id?.toLowerCase());
|
|
62
|
+
if (choice)
|
|
63
|
+
return { choice, name: MAC_TERMINALS[choice].name };
|
|
64
|
+
const name = shown?.replace(/\.app$/u, "");
|
|
65
|
+
return name ? { name } : undefined;
|
|
66
|
+
}
|
|
67
|
+
/** The terminal a window runs in, by its mark (TERM_PROGRAM): one of the choices, else none. */
|
|
68
|
+
export function terminalOfMark(mark) {
|
|
69
|
+
return Object.keys(MAC_TERMINALS).find((k) => MAC_TERMINALS[k].mark === mark);
|
|
70
|
+
}
|
package/dist/core/tree.js
CHANGED
|
@@ -27,7 +27,7 @@ import { lastBackup } from "./backup.js";
|
|
|
27
27
|
export const TREE_FILE = "strom.json";
|
|
28
28
|
// between releases the candidate being made (a bridge run from the sources says what it is: the Strom app goes by it);
|
|
29
29
|
// at a release the release's own (package.json)
|
|
30
|
-
export const VERSION = "1.16.0-beta.
|
|
30
|
+
export const VERSION = "1.16.0-beta.2";
|
|
31
31
|
const GITIGNORE = `# Strom Research — local state and large files are not versioned
|
|
32
32
|
.strom/
|
|
33
33
|
/media/
|