parent
e80859b35e
commit
68593b1cf0
@ -0,0 +1,7 @@ |
|||||||
|
<component name="ProjectRunConfigurationManager"> |
||||||
|
<configuration default="false" name="add-entry" type="NodeJSConfigurationType" application-parameters="add-entry" path-to-js-file="dist/index.js" working-dir="$PROJECT_DIR$"> |
||||||
|
<method v="2"> |
||||||
|
<option name="RunConfigurationTask" enabled="true" run_configuration_name="build" run_configuration_type="js.build_tools.npm" /> |
||||||
|
</method> |
||||||
|
</configuration> |
||||||
|
</component> |
@ -0,0 +1,12 @@ |
|||||||
|
<component name="ProjectRunConfigurationManager"> |
||||||
|
<configuration default="false" name="build" type="js.build_tools.npm"> |
||||||
|
<package-json value="$PROJECT_DIR$/package.json" /> |
||||||
|
<command value="run" /> |
||||||
|
<scripts> |
||||||
|
<script value="build" /> |
||||||
|
</scripts> |
||||||
|
<node-interpreter value="project" /> |
||||||
|
<envs /> |
||||||
|
<method v="2" /> |
||||||
|
</configuration> |
||||||
|
</component> |
@ -0,0 +1,7 @@ |
|||||||
|
<component name="ProjectRunConfigurationManager"> |
||||||
|
<configuration default="false" name="update-empathy-guide" type="NodeJSConfigurationType" application-parameters="update-empathy-guide" path-to-js-file="dist/index.js" working-dir="$PROJECT_DIR$"> |
||||||
|
<method v="2"> |
||||||
|
<option name="RunConfigurationTask" enabled="true" run_configuration_name="build" run_configuration_type="js.build_tools.npm" /> |
||||||
|
</method> |
||||||
|
</configuration> |
||||||
|
</component> |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,91 @@ |
|||||||
|
import {Separator} from "inquirer"; |
||||||
|
import {inquire} from "../prompts/Inquire"; |
||||||
|
import {summaryPrompt} from "../prompts/implementations/SummaryPrompt"; |
||||||
|
import {journalEntryPrompt} from "../prompts/implementations/JournalEntryPrompt"; |
||||||
|
import {guidedEmpathyListPrompt} from "../prompts/implementations/GuidedEmpathyListPrompt"; |
||||||
|
import {entryMainMenuPrompt} from "../prompts/implementations/EntryMainMenuPrompt"; |
||||||
|
import {CommandModule} from "yargs"; |
||||||
|
import { registerPrompts } from "../prompts/types"; |
||||||
|
import {LocalRepository} from "../repository/LocalRepository"; |
||||||
|
import {conditionPrompt} from "../prompts/implementations/ConditionPrompt"; |
||||||
|
import {entryPrompt} from "../prompts/implementations/EntryPrompt"; |
||||||
|
import {guidedEmpathyPrompt} from "../prompts/implementations/GuidedEmpathyPrompt"; |
||||||
|
import {suicidalityPrompt} from "../prompts/implementations/SuicidalityPrompt"; |
||||||
|
|
||||||
|
export function addEntryCommand(): CommandModule { |
||||||
|
return { |
||||||
|
command: ["add-entry", "*"], |
||||||
|
describe: "Adds a new entry to the journal.", |
||||||
|
handler: async () => { |
||||||
|
registerPrompts() |
||||||
|
const storage = new LocalRepository() |
||||||
|
const empathyGuide = await storage.loadEmpathyGuide() |
||||||
|
|
||||||
|
const condition = conditionPrompt({inquire}) |
||||||
|
const summary = summaryPrompt({inquire}) |
||||||
|
const journal = journalEntryPrompt({inquire}) |
||||||
|
const suicidality = suicidalityPrompt({inquire}) |
||||||
|
const empathy = guidedEmpathyPrompt({ |
||||||
|
inquire, |
||||||
|
guideFactory: () => empathyGuide, |
||||||
|
show: async (value) => console.log(value), |
||||||
|
}) |
||||||
|
const empathyList = guidedEmpathyListPrompt({inquire, promptForEmpathy: empathy}) |
||||||
|
|
||||||
|
const mainMenuItems = [ |
||||||
|
condition.mainMenu, |
||||||
|
summary.mainMenu, |
||||||
|
new Separator(), |
||||||
|
journal.mainMenu, |
||||||
|
empathyList.mainMenu, |
||||||
|
/* |
||||||
|
{ |
||||||
|
name: typeof entry.needs === "object" ? "Check back in on needs" : "Check in on needs", |
||||||
|
value: NEEDS, |
||||||
|
key: "n" |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: typeof entry.personas === "object" ? "Check back in on personas" : "Check in on personas", |
||||||
|
value: PERSONA, |
||||||
|
key: "p" |
||||||
|
}, |
||||||
|
{name: typeof entry.rpg === "object" ? "Change RPG stats" : "Add RPG stats", value: RPG, key: "r"}, |
||||||
|
{ |
||||||
|
name: typeof entry.activities === "object" ? "Change record of recent activities" : "Add record of recent activities", |
||||||
|
value: ACTIVITIES, |
||||||
|
key: "a" |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: typeof entry.music === "object" ? "Change record of recently played music" : "Add record of recently played music", |
||||||
|
value: MUSIC, |
||||||
|
key: "m" |
||||||
|
}, |
||||||
|
*/ |
||||||
|
suicidality.mainMenu, |
||||||
|
/* |
||||||
|
{ |
||||||
|
name: typeof entry.recoveries === "object" ? "Try more recovery methods" : "Try some recovery methods", |
||||||
|
value: RECOVERIES, |
||||||
|
key: "y" |
||||||
|
}, |
||||||
|
*/ |
||||||
|
] |
||||||
|
|
||||||
|
const mainMenu = entryMainMenuPrompt({ |
||||||
|
inquire, |
||||||
|
choices: mainMenuItems, |
||||||
|
showError: async (value) => console.log(value), |
||||||
|
}) |
||||||
|
|
||||||
|
const entry = entryPrompt({ |
||||||
|
promptForCondition: condition, |
||||||
|
promptForSummary: summary, |
||||||
|
promptForEntryMainMenu: mainMenu, |
||||||
|
}) |
||||||
|
|
||||||
|
const writtenEntry = await entry({}) |
||||||
|
await storage.saveEntry(writtenEntry) |
||||||
|
console.log("Entry saved! Good work!") |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,37 @@ |
|||||||
|
import {CommandModule} from "yargs"; |
||||||
|
import {inquire} from "../prompts/Inquire"; |
||||||
|
import {registerPrompts} from "../prompts/types"; |
||||||
|
import {LocalRepository} from "../repository/LocalRepository"; |
||||||
|
import {empathyGroupPrompt} from "../prompts/implementations/EmpathyGroupPrompt"; |
||||||
|
import {empathyGroupListPrompt} from "../prompts/implementations/EmpathyGroupListPrompt"; |
||||||
|
import {empathyGuidePrompt} from "../prompts/implementations/EmpathyGuidePrompt"; |
||||||
|
|
||||||
|
export function updateEmpathyGuideCommand(): CommandModule { |
||||||
|
return { |
||||||
|
command: "update-empathy-guide", |
||||||
|
describe: "Makes changes to the empathy guide.", |
||||||
|
handler: async (): Promise<void> => { |
||||||
|
registerPrompts() |
||||||
|
const storage = new LocalRepository() |
||||||
|
const empathyGuide = await storage.loadEmpathyGuide() |
||||||
|
|
||||||
|
const empathyGroup = empathyGroupPrompt({ |
||||||
|
inquire, |
||||||
|
showError: async (text) => console.error(text), |
||||||
|
}) |
||||||
|
|
||||||
|
const empathyList = empathyGroupListPrompt({ |
||||||
|
inquire, |
||||||
|
promptForEmpathyGroup: empathyGroup |
||||||
|
}) |
||||||
|
|
||||||
|
const newGuide = await empathyGuidePrompt({ |
||||||
|
inquire, |
||||||
|
showError: async (text) => console.error(text), |
||||||
|
promptForEmpathyGroupList: empathyList, |
||||||
|
})({default: empathyGuide}) |
||||||
|
await storage.saveEmpathyGuide(newGuide) |
||||||
|
console.log("Empathy guide saved!") |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,22 @@ |
|||||||
|
import {schema} from "../schemata/SchemaData"; |
||||||
|
|
||||||
|
export enum Condition { |
||||||
|
CRITICAL = "Critical", |
||||||
|
POOR = "Poor", |
||||||
|
SOSO = "So-so", |
||||||
|
GOOD = "Good", |
||||||
|
EXCELLENT = "Excellent", |
||||||
|
UNSURE = "Unsure", |
||||||
|
NUMB = "Numb", |
||||||
|
} |
||||||
|
export const CONDITIONS = [Condition.CRITICAL, Condition.POOR, Condition.SOSO, Condition.GOOD, Condition.EXCELLENT, Condition.UNSURE, Condition.NUMB] |
||||||
|
|
||||||
|
export type ConditionJTD = typeof ConditionJTD |
||||||
|
export const ConditionJTD = schema({ |
||||||
|
schema: { |
||||||
|
enum: CONDITIONS |
||||||
|
}, |
||||||
|
key: "condition", |
||||||
|
references: [], |
||||||
|
}) |
||||||
|
|
@ -0,0 +1,20 @@ |
|||||||
|
import {schema} from "../schemata/SchemaData"; |
||||||
|
|
||||||
|
export interface EmpathyGroup { |
||||||
|
readonly header: string |
||||||
|
readonly items: readonly string[] |
||||||
|
} |
||||||
|
|
||||||
|
export type EmpathyGroupJTD = typeof EmpathyGroupJTD |
||||||
|
export const EmpathyGroupJTD = schema({ |
||||||
|
schema: { |
||||||
|
properties: { |
||||||
|
header: {type: "string"}, |
||||||
|
items: {elements: {type: "string"}} |
||||||
|
} |
||||||
|
}, |
||||||
|
typeHint: null as EmpathyGroup | null, |
||||||
|
key: "empathyGroup", |
||||||
|
references: [], |
||||||
|
}) |
||||||
|
|
@ -0,0 +1,6 @@ |
|||||||
|
import {EmpathyGroup} from "./EmpathyGroup"; |
||||||
|
|
||||||
|
export function totalItems(groups: readonly EmpathyGroup[]): number { |
||||||
|
return groups.map((group) => group.items.length).reduce((a, b) => a + b) |
||||||
|
} |
||||||
|
|
@ -0,0 +1,35 @@ |
|||||||
|
import {schema} from "../schemata/SchemaData"; |
||||||
|
import {EmpathyGroup, EmpathyGroupJTD} from "./EmpathyGroup"; |
||||||
|
|
||||||
|
export interface EmpathyGuide { |
||||||
|
readonly feelings?: { |
||||||
|
readonly pleasant?: readonly EmpathyGroup[] |
||||||
|
readonly unpleasant?: readonly EmpathyGroup[] |
||||||
|
} |
||||||
|
readonly needs?: readonly EmpathyGroup[] |
||||||
|
} |
||||||
|
|
||||||
|
export type EmpathyGuideJTD = typeof EmpathyGuideJTD |
||||||
|
export const EmpathyGuideJTD = schema({ |
||||||
|
schema: { |
||||||
|
optionalProperties: { |
||||||
|
feelings: { |
||||||
|
optionalProperties: { |
||||||
|
pleasant: { |
||||||
|
elements: EmpathyGroupJTD.reference |
||||||
|
}, |
||||||
|
unpleasant: { |
||||||
|
elements: EmpathyGroupJTD.reference |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
needs: { |
||||||
|
elements: EmpathyGroupJTD.reference |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
typeHint: null as EmpathyGuide | null, |
||||||
|
key: "empathyGuide", |
||||||
|
references: [EmpathyGroupJTD], |
||||||
|
}) |
||||||
|
|
@ -0,0 +1,40 @@ |
|||||||
|
import {Condition, ConditionJTD} from "./Condition"; |
||||||
|
import {GuidedEmpathy, GuidedEmpathyJTD} from "./GuidedEmpathy"; |
||||||
|
import {Suicidality, SuicidalityJTD} from "./Suicidality"; |
||||||
|
import {schema} from "../schemata/SchemaData"; |
||||||
|
|
||||||
|
export interface Entry { |
||||||
|
readonly startedAt: Date |
||||||
|
readonly finishedAt: Date |
||||||
|
readonly condition: Condition |
||||||
|
readonly summary?: string |
||||||
|
readonly journalEntry?: string |
||||||
|
readonly guidedEmpathy?: readonly GuidedEmpathy[] |
||||||
|
// readonly needs?: Needs
|
||||||
|
// readonly music?: readonly string[]
|
||||||
|
// readonly rpg?: RPGStats
|
||||||
|
// readonly personas?: Personas
|
||||||
|
// readonly activities?: readonly Activity[]
|
||||||
|
readonly suicidality?: Suicidality |
||||||
|
// readonly recoveries?: readonly Recovery[]
|
||||||
|
} |
||||||
|
export type EntryJTD = typeof EntryJTD |
||||||
|
export const EntryJTD = schema({ |
||||||
|
schema: { |
||||||
|
properties: { |
||||||
|
startedAt: { type: "timestamp" }, |
||||||
|
finishedAt: { type: "timestamp" }, |
||||||
|
condition: ConditionJTD.reference, |
||||||
|
}, |
||||||
|
optionalProperties: { |
||||||
|
summary: { type: "string" }, |
||||||
|
journalEntry: { type: "string" }, |
||||||
|
guidedEmpathy: { elements: GuidedEmpathyJTD.reference }, |
||||||
|
suicidality: SuicidalityJTD.reference, |
||||||
|
} |
||||||
|
}, |
||||||
|
typeHint: null as Entry|null, |
||||||
|
key: "entry", |
||||||
|
references: [ConditionJTD, GuidedEmpathyJTD, SuicidalityJTD] |
||||||
|
}) |
||||||
|
|
@ -0,0 +1,52 @@ |
|||||||
|
import {isPopulatedArray} from "../utils/Arrays"; |
||||||
|
import { |
||||||
|
Persona, |
||||||
|
personaName, |
||||||
|
personaPronoun, |
||||||
|
personaPronounObject, |
||||||
|
personaPronounPossessive, |
||||||
|
personaVerb |
||||||
|
} from "./Persona"; |
||||||
|
import chalk from "chalk"; |
||||||
|
import {schema} from "../schemata/SchemaData"; |
||||||
|
import capitalize from "capitalize"; |
||||||
|
|
||||||
|
export interface GuidedEmpathy { |
||||||
|
readonly feelings?: readonly string[] |
||||||
|
readonly needs?: readonly string[] |
||||||
|
readonly events?: readonly string[] |
||||||
|
readonly requests?: readonly string[] |
||||||
|
} |
||||||
|
export type GuidedEmpathyJTD = typeof GuidedEmpathyJTD |
||||||
|
export const GuidedEmpathyJTD = schema({ |
||||||
|
schema: { |
||||||
|
optionalProperties: { |
||||||
|
feelings: { elements: { type: "string" } }, |
||||||
|
needs: { elements: { type: "string" } }, |
||||||
|
events: { elements: { type: "string" } }, |
||||||
|
requests: { elements: { type: "string" } }, |
||||||
|
} |
||||||
|
}, |
||||||
|
typeHint: null as GuidedEmpathy|null, |
||||||
|
key: "guidedEmpathy", |
||||||
|
references: [], |
||||||
|
}) |
||||||
|
|
||||||
|
export function isPopulatedGuidedEmpathy(empathy: GuidedEmpathy | undefined): boolean { |
||||||
|
return !!empathy && (isPopulatedArray(empathy.feelings) |
||||||
|
|| isPopulatedArray(empathy.needs) |
||||||
|
|| isPopulatedArray(empathy.events) |
||||||
|
|| isPopulatedArray(empathy.requests)) |
||||||
|
} |
||||||
|
|
||||||
|
export function guidedEmpathyToString(empathy: GuidedEmpathy, persona: Persona | undefined): string { |
||||||
|
return (`${(capitalize(personaName(persona), true))} ${personaVerb(persona, "feel", "feels")}: ${empathy.feelings?.join(", ") || chalk.dim("(???)")}...\n` |
||||||
|
+ `Because ${personaPronoun(persona)} ${personaVerb(persona, "need", "needs")}: ${empathy.needs?.join(", ") || chalk.dim("(???)")}...\n` |
||||||
|
+ `And ${personaPronounPossessive(persona)} needs were affected when: ${empathy.events?.join("; ") || chalk.dim("(???)")}...\n` |
||||||
|
+ `So to get good outcomes for ${personaPronounObject(persona)} in the future, can we try: ${empathy.requests?.join("; ") || chalk.dim("(???)")}?`) |
||||||
|
} |
||||||
|
|
||||||
|
export function guidedEmpathyToStringShort(empathy: GuidedEmpathy): string { |
||||||
|
return `Feeling ${empathy.feelings?.join(", ") || chalk.dim("(none)")} // Because ${empathy.needs?.join(", ") || chalk.dim("(none)")} // When ${empathy.events?.join("; ") || chalk.dim("(none)")} // So ${empathy.requests?.join("; ") || chalk.dim("(none)")}` |
||||||
|
} |
||||||
|
|
@ -0,0 +1,64 @@ |
|||||||
|
import {Entry, EntryJTD} from "./Entry"; |
||||||
|
import {schema} from "../schemata/SchemaData"; |
||||||
|
|
||||||
|
/* export interface PersonaState { |
||||||
|
readonly persona: Persona |
||||||
|
readonly condition: Condition |
||||||
|
readonly summary?: string |
||||||
|
readonly journalEntry?: string |
||||||
|
readonly guidedEmpathy?: readonly GuidedEmpathy[] |
||||||
|
} */ |
||||||
|
|
||||||
|
/* export interface RPGStats { |
||||||
|
readonly hpp?: number |
||||||
|
readonly mpp?: number |
||||||
|
readonly tpp?: number |
||||||
|
readonly hope?: number |
||||||
|
readonly determination?: number |
||||||
|
} */ |
||||||
|
|
||||||
|
/* export interface Personas { |
||||||
|
readonly active?: Persona |
||||||
|
readonly states?: readonly PersonaState[] |
||||||
|
} */ |
||||||
|
|
||||||
|
/* export interface Needs { |
||||||
|
readonly food?: string |
||||||
|
readonly water?: string |
||||||
|
readonly sleep?: string |
||||||
|
readonly hygiene?: string |
||||||
|
} */ |
||||||
|
|
||||||
|
/* export interface Recovery { |
||||||
|
readonly name: string |
||||||
|
} */ |
||||||
|
|
||||||
|
/* export interface Activity { |
||||||
|
readonly name: string |
||||||
|
} */ |
||||||
|
|
||||||
|
/* export interface RecoveryMethod { |
||||||
|
readonly name: string |
||||||
|
readonly description: string |
||||||
|
} */ |
||||||
|
|
||||||
|
/* |
||||||
|
// readonly activities?: readonly string[]
|
||||||
|
readonly empathyGuide?: EmpathyGuide |
||||||
|
// readonly recoveryMethods?: readonly RecoveryMethod[]
|
||||||
|
*/ |
||||||
|
|
||||||
|
export interface Journal { |
||||||
|
readonly entries?: readonly Entry[] |
||||||
|
} |
||||||
|
export type JournalJTD = typeof JournalJTD |
||||||
|
export const JournalJTD = schema({ |
||||||
|
schema: { |
||||||
|
optionalProperties: { |
||||||
|
entries: { elements: EntryJTD.reference }, |
||||||
|
}, |
||||||
|
}, |
||||||
|
typeHint: null as Journal|null, |
||||||
|
key: "journal", |
||||||
|
references: [EntryJTD, ...EntryJTD.requiredReferences] |
||||||
|
}) |
@ -0,0 +1,82 @@ |
|||||||
|
import {schema} from "../schemata/SchemaData"; |
||||||
|
|
||||||
|
export enum Persona { |
||||||
|
HEART = "Heart", |
||||||
|
SUCCUBUS = "Succubus", |
||||||
|
CHILD = "Child", |
||||||
|
NURSE = "Nurse", |
||||||
|
SORCERESS = "Sorceress", |
||||||
|
LADY = "Lady", |
||||||
|
SHADOW = "Shadow", |
||||||
|
} |
||||||
|
export const PERSONAS: Persona[] = [Persona.HEART, Persona.SUCCUBUS, Persona.CHILD, Persona.NURSE, Persona.SORCERESS, Persona.LADY, Persona.SHADOW] |
||||||
|
export type PersonaJTD = typeof PersonaJTD |
||||||
|
export const PersonaJTD = schema({ |
||||||
|
schema: { |
||||||
|
enum: PERSONAS |
||||||
|
}, |
||||||
|
key: "persona", |
||||||
|
references: [], |
||||||
|
}) |
||||||
|
|
||||||
|
export interface PersonaPrompt { |
||||||
|
readonly persona?: Persona |
||||||
|
} |
||||||
|
|
||||||
|
export function personaPronoun(persona: Persona | undefined): string { |
||||||
|
switch (persona) { |
||||||
|
case undefined: |
||||||
|
return "you" |
||||||
|
default: |
||||||
|
return "she" |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function personaPronounPossessive(persona: Persona | undefined): string { |
||||||
|
switch (persona) { |
||||||
|
case undefined: |
||||||
|
return "your" |
||||||
|
default: |
||||||
|
return "her" |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function personaPronounObject(persona: Persona | undefined): string { |
||||||
|
switch (persona) { |
||||||
|
case undefined: |
||||||
|
return "you" |
||||||
|
default: |
||||||
|
return "her" |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function personaName(persona: Persona | undefined): string { |
||||||
|
switch (persona) { |
||||||
|
case undefined: |
||||||
|
return "you" |
||||||
|
case Persona.HEART: |
||||||
|
return "Reya Heart" |
||||||
|
default: |
||||||
|
return persona + " Reya" |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function personaPossessive(persona: Persona | undefined): string { |
||||||
|
switch (persona) { |
||||||
|
case undefined: |
||||||
|
return "your" |
||||||
|
case Persona.HEART: |
||||||
|
return "Reya Heart's" |
||||||
|
default: |
||||||
|
return persona + " Reya's" |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function personaVerb(persona: Persona | undefined, secondPerson: string, thirdPerson: string): string { |
||||||
|
switch (persona) { |
||||||
|
case undefined: |
||||||
|
return secondPerson |
||||||
|
default: |
||||||
|
return thirdPerson |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,23 @@ |
|||||||
|
import {schema} from "../schemata/SchemaData"; |
||||||
|
|
||||||
|
export enum Suicidality { |
||||||
|
NONE = "None", |
||||||
|
PASSIVE = "Passive", |
||||||
|
INTRUSIVE = "Intrusive", |
||||||
|
ACTIVE = "Active", |
||||||
|
RESIGNED = "Resigned", |
||||||
|
PLANNING = "Planning", |
||||||
|
PLANNED = "Planned", |
||||||
|
DANGER = "Danger", |
||||||
|
} |
||||||
|
export const SUICIDALITIES: Suicidality[] = [Suicidality.NONE, Suicidality.PASSIVE, Suicidality.INTRUSIVE, Suicidality.ACTIVE, Suicidality.RESIGNED, Suicidality.PLANNING, Suicidality.PLANNED, Suicidality.DANGER] |
||||||
|
|
||||||
|
export type SuicidalityJTD = typeof SuicidalityJTD |
||||||
|
export const SuicidalityJTD = schema({ |
||||||
|
schema: { |
||||||
|
enum: SUICIDALITIES |
||||||
|
}, |
||||||
|
key: "suicidality", |
||||||
|
references: [], |
||||||
|
}) |
||||||
|
|
@ -1,5 +1,13 @@ |
|||||||
function init(firstName: string, lastName: string) { |
import {addEntryCommand} from "./commands/AddEntry"; |
||||||
return `${firstName} ${lastName}`; |
import {updateEmpathyGuideCommand} from "./commands/UpdateEmpathyGuide"; |
||||||
} |
import yargs from "yargs" |
||||||
|
|
||||||
console.log(init('Hello', 'world')); |
yargs |
||||||
|
.scriptName("mari-status-bar") |
||||||
|
.command(addEntryCommand()) |
||||||
|
.command(updateEmpathyGuideCommand()) |
||||||
|
.demandCommand() |
||||||
|
.parseAsync() |
||||||
|
.catch((err) => { |
||||||
|
console.error(err) |
||||||
|
}) |
@ -0,0 +1,9 @@ |
|||||||
|
import {prompt, QuestionCollection} from "inquirer"; |
||||||
|
|
||||||
|
export type InquireFunction = (question: Omit<QuestionCollection, "name">) => Promise<any> |
||||||
|
export type ShowFunction = (text: string) => Promise<void> |
||||||
|
|
||||||
|
export async function inquire(question: Omit<QuestionCollection, "name">): Promise<any> { |
||||||
|
const result = await prompt([{...question, name: "answer"}]) |
||||||
|
return result.answer |
||||||
|
} |
@ -0,0 +1,69 @@ |
|||||||
|
import {ListQuestion, Separator} from "inquirer"; |
||||||
|
import {personaPossessive, PersonaPrompt} from "../../datatypes/Persona"; |
||||||
|
import chalk from "chalk"; |
||||||
|
import {EntryMainMenuChoice, makeEntryMainMenuChoice} from "./EntryMainMenuPrompt"; |
||||||
|
import {asDefault, identity} from "../../utils/Objects"; |
||||||
|
import {InquireFunction} from "../Inquire"; |
||||||
|
import {Condition} from "../../datatypes/Condition"; |
||||||
|
|
||||||
|
export interface ConditionPromptOptions extends Partial<Omit<ListQuestion, "name" | "type" | "choices">>, PersonaPrompt { |
||||||
|
} |
||||||
|
|
||||||
|
export interface ConditionPromptDependencies { |
||||||
|
readonly inquire: InquireFunction |
||||||
|
} |
||||||
|
|
||||||
|
export function conditionPrompt(deps: ConditionPromptDependencies): { |
||||||
|
(options: ConditionPromptOptions): Promise<Condition>, |
||||||
|
mainMenu: EntryMainMenuChoice<"condition">, |
||||||
|
} { |
||||||
|
return makeEntryMainMenuChoice({ |
||||||
|
property: "condition", |
||||||
|
name: (input) => `Change condition ${chalk.dim(`(currently ${chalk.greenBright(input)})`)}`, |
||||||
|
key: "c", |
||||||
|
injected: (options) => promptForCondition(options, deps), |
||||||
|
toOptions: asDefault, |
||||||
|
toProperty: identity, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
export async function promptForCondition(options: ConditionPromptOptions = {}, {inquire}: ConditionPromptDependencies): Promise<Condition> { |
||||||
|
return await inquire({ |
||||||
|
type: "list", |
||||||
|
message: `How's ${personaPossessive(options.persona)} current condition?`, |
||||||
|
choices: [ |
||||||
|
{ |
||||||
|
value: Condition.CRITICAL, |
||||||
|
name: `Critical ${chalk.dim("(very uncomfortable/very unpleasant feelings/completely out of control)")}` |
||||||
|
}, |
||||||
|
{ |
||||||
|
value: Condition.POOR, |
||||||
|
name: `Poor ${chalk.dim("(uncomfortable/unpleasant feelings/losing control)")}` |
||||||
|
}, |
||||||
|
{ |
||||||
|
value: Condition.SOSO, |
||||||
|
name: `So-so ${chalk.dim("(OK/neutral mood/mixed feelings/close to losing control)")}` |
||||||
|
}, |
||||||
|
{ |
||||||
|
value: Condition.GOOD, |
||||||
|
name: `Good ${chalk.dim("(comfortable/pleasant feelings/mostly in control)")}` |
||||||
|
}, |
||||||
|
{ |
||||||
|
value: Condition.EXCELLENT, |
||||||
|
name: `Excellent ${chalk.dim("(very comfortable/very pleasant feelings/secure/in control)")}` |
||||||
|
}, |
||||||
|
new Separator(), |
||||||
|
{ |
||||||
|
value: Condition.NUMB, |
||||||
|
name: `Numb ${chalk.dim("(depressive void state/crystallizing/petrifying/encased in ice)")}` |
||||||
|
}, |
||||||
|
{ |
||||||
|
value: Condition.UNSURE, |
||||||
|
name: `Unsure ${chalk.dim("(strong mixed feelings/low connection to self)")}` |
||||||
|
}, |
||||||
|
], |
||||||
|
default: 2, |
||||||
|
pageSize: 999, |
||||||
|
...options, |
||||||
|
}) |
||||||
|
} |
@ -0,0 +1,66 @@ |
|||||||
|
import {EmpathyGroup} from "../../datatypes/EmpathyGroup"; |
||||||
|
import {isPopulatedArray} from "../../utils/Arrays"; |
||||||
|
import chalk from "chalk"; |
||||||
|
import pluralize from "pluralize"; |
||||||
|
import {InquireFunction} from "../Inquire"; |
||||||
|
import {EmpathyGroupPromptOptions} from "./EmpathyGroupPrompt"; |
||||||
|
|
||||||
|
export interface EmpathyGroupListPromptOptions { |
||||||
|
readonly default?: readonly EmpathyGroup[] |
||||||
|
readonly listName: string |
||||||
|
} |
||||||
|
|
||||||
|
export interface EmpathyGroupListPromptDependencies { |
||||||
|
readonly inquire: InquireFunction |
||||||
|
readonly promptForEmpathyGroup: (opts: EmpathyGroupPromptOptions) => Promise<EmpathyGroup | null> |
||||||
|
} |
||||||
|
|
||||||
|
export function empathyGroupListPrompt(deps: EmpathyGroupListPromptDependencies): (opts: EmpathyGroupListPromptOptions) => Promise<readonly EmpathyGroup[]> { |
||||||
|
return (opts) => promptForEmpathyGroupList(opts, deps) |
||||||
|
} |
||||||
|
|
||||||
|
export async function promptForEmpathyGroupList(opts: EmpathyGroupListPromptOptions, deps: EmpathyGroupListPromptDependencies): Promise<readonly EmpathyGroup[]> { |
||||||
|
const {default: value = [], listName} = opts |
||||||
|
const {inquire, promptForEmpathyGroup} = deps |
||||||
|
const option = isPopulatedArray(value) ? await inquire({ |
||||||
|
type: "list", |
||||||
|
message: `Which group of ${listName} would you like to edit?`, |
||||||
|
default: value.length + 1, |
||||||
|
choices: [ |
||||||
|
...value.map((group, index) => ({ |
||||||
|
value: index, |
||||||
|
name: `${group.header} ${isPopulatedArray(group.items) ? chalk.dim(`(currently ${pluralize("item", group.items.length, true)})`) : ""}` |
||||||
|
})), |
||||||
|
{value: -1, name: chalk.dim("(Add New Group)")}, |
||||||
|
{value: -2, name: "Done"} |
||||||
|
] |
||||||
|
}) : -1 |
||||||
|
switch (option) { |
||||||
|
case -1: |
||||||
|
const newGroup = await promptForEmpathyGroup({listName}) |
||||||
|
if (newGroup === null) { |
||||||
|
if (isPopulatedArray(value)) { |
||||||
|
return promptForEmpathyGroupList(opts, deps) |
||||||
|
} else { |
||||||
|
return [] |
||||||
|
} |
||||||
|
} |
||||||
|
return promptForEmpathyGroupList({...opts, default: [...value, newGroup]}, deps) |
||||||
|
case -2: |
||||||
|
return value |
||||||
|
default: |
||||||
|
const updatedGroup = await promptForEmpathyGroup({default: value[option], listName}) |
||||||
|
if (value.length > 1 || updatedGroup !== null) { |
||||||
|
return promptForEmpathyGroupList({ |
||||||
|
...opts, |
||||||
|
default: [ |
||||||
|
...value.slice(0, option), |
||||||
|
...updatedGroup === null ? [] : [updatedGroup], |
||||||
|
...value.slice(option + 1) |
||||||
|
] |
||||||
|
}, deps) |
||||||
|
} else { |
||||||
|
return [] |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,81 @@ |
|||||||
|
import capitalize from "capitalize"; |
||||||
|
import {editYaml} from "../../schemata/YAMLPrompt"; |
||||||
|
import chalk from "chalk"; |
||||||
|
import {InquireFunction, ShowFunction} from "../Inquire"; |
||||||
|
import {EmpathyGroup, EmpathyGroupJTD} from "../../datatypes/EmpathyGroup"; |
||||||
|
|
||||||
|
export interface EmpathyGroupPromptOptions { |
||||||
|
default?: EmpathyGroup |
||||||
|
listName: string |
||||||
|
} |
||||||
|
|
||||||
|
export interface EmpathyGroupPromptDependencies { |
||||||
|
inquire: InquireFunction |
||||||
|
showError: ShowFunction |
||||||
|
} |
||||||
|
|
||||||
|
export function empathyGroupPrompt(deps: EmpathyGroupPromptDependencies): (opts: EmpathyGroupPromptOptions) => Promise<EmpathyGroup | null> { |
||||||
|
return (opts) => promptForEmpathyGroup(opts, deps) |
||||||
|
} |
||||||
|
|
||||||
|
const PROMPT = "Prompt" |
||||||
|
const AUTO = "Auto" |
||||||
|
const YAML = "YAML" |
||||||
|
|
||||||
|
export async function promptForEmpathyGroup(opts: EmpathyGroupPromptOptions, deps: EmpathyGroupPromptDependencies): Promise<EmpathyGroup | null> { |
||||||
|
const {listName, default: {header = undefined, items = []} = {}} = opts |
||||||
|
const {inquire, showError} = deps |
||||||
|
const mode = await inquire({ |
||||||
|
type: "expand", |
||||||
|
message: `How would you like to ${header ? "edit" : "create"} this ${listName} group?`, |
||||||
|
choices: [ |
||||||
|
{key: "p", name: "Be prompted and use the interactive process", value: PROMPT}, |
||||||
|
{key: "t", name: "Auto-process a plain-text list pasted from another source", value: AUTO}, |
||||||
|
{key: "y", name: `Directly ${header ? "edit" : "write"} YAML`, value: YAML}, |
||||||
|
] |
||||||
|
}) |
||||||
|
switch (mode) { |
||||||
|
case AUTO: |
||||||
|
const text: string = await inquire({ |
||||||
|
type: "editor", |
||||||
|
message: "Enter the header on the first line, and each item on separate lines:", |
||||||
|
default: (header ? [header, ...items] : ["", ...items]).join("\n") |
||||||
|
}) |
||||||
|
const trimmed = text.trim() |
||||||
|
if (trimmed === "") { |
||||||
|
return null |
||||||
|
} |
||||||
|
const [parsedHeader, ...parsedItems] = trimmed.split("\n").map((line) => line.trim()).filter((line) => line !== "") |
||||||
|
return { |
||||||
|
header: capitalize.words(parsedHeader, false), |
||||||
|
items: parsedItems.map((item) => item.toLocaleLowerCase()) |
||||||
|
} |
||||||
|
case YAML: |
||||||
|
return await editYaml({ |
||||||
|
schema: EmpathyGroupJTD, |
||||||
|
currentValue: opts.default, |
||||||
|
name: `this ${listName} group`, |
||||||
|
inquire, |
||||||
|
showError, |
||||||
|
}) || null |
||||||
|
case PROMPT: |
||||||
|
default: |
||||||
|
const newHeader = await inquire({ |
||||||
|
type: "input", |
||||||
|
message: `What should this ${listName} group be called?${typeof header === "string" ? chalk.dim(" (leave blank to delete this group)") : ""}`, |
||||||
|
default: header, |
||||||
|
}) |
||||||
|
if (newHeader === "") { |
||||||
|
return null |
||||||
|
} |
||||||
|
const newItems = await inquire({ |
||||||
|
type: "multitext", |
||||||
|
message: `What ${listName} should the ${newHeader} group contain?`, |
||||||
|
default: items, |
||||||
|
}) |
||||||
|
return { |
||||||
|
header: newHeader, |
||||||
|
items: newItems, |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,120 @@ |
|||||||
|
import {isPopulatedArray} from "../../utils/Arrays"; |
||||||
|
import chalk from "chalk"; |
||||||
|
import pluralize from "pluralize"; |
||||||
|
import {totalItems} from "../../datatypes/EmpathyGroupList"; |
||||||
|
import {Separator} from "inquirer"; |
||||||
|
import {editYaml} from "../../schemata/YAMLPrompt"; |
||||||
|
import {InquireFunction, ShowFunction} from "../Inquire"; |
||||||
|
import {EmpathyGroupListPromptOptions} from "./EmpathyGroupListPrompt"; |
||||||
|
import {EmpathyGroup} from "../../datatypes/EmpathyGroup"; |
||||||
|
import {EmpathyGuide, EmpathyGuideJTD} from "../../datatypes/EmpathyGuide"; |
||||||
|
|
||||||
|
export interface EmpathyGuidePromptOptions { |
||||||
|
readonly default?: EmpathyGuide |
||||||
|
} |
||||||
|
|
||||||
|
export interface EmpathyGuidePromptDependencies { |
||||||
|
readonly inquire: InquireFunction |
||||||
|
readonly showError: ShowFunction |
||||||
|
readonly promptForEmpathyGroupList: (opts: EmpathyGroupListPromptOptions) => Promise<readonly EmpathyGroup[]> |
||||||
|
} |
||||||
|
|
||||||
|
export function empathyGuidePrompt(deps: EmpathyGuidePromptDependencies): (opts: EmpathyGuidePromptOptions) => Promise<EmpathyGuide> { |
||||||
|
return (opts) => promptForEmpathyGuide(opts, deps) |
||||||
|
} |
||||||
|
|
||||||
|
const PLEASANT = "Pleasant" |
||||||
|
const UNPLEASANT = "Unpleasant" |
||||||
|
const NEEDS = "Needs" |
||||||
|
const INSPECT = "Inspect" |
||||||
|
const SAVE = "Save" |
||||||
|
|
||||||
|
export async function promptForEmpathyGuide(opts: EmpathyGuidePromptOptions, deps: EmpathyGuidePromptDependencies): Promise<EmpathyGuide> { |
||||||
|
const {default: value} = opts |
||||||
|
const {inquire, showError, promptForEmpathyGroupList} = deps |
||||||
|
const result = await inquire({ |
||||||
|
type: "expand", |
||||||
|
message: "What would you like to modify?", |
||||||
|
pageSize: 999, |
||||||
|
choices: [ |
||||||
|
{ |
||||||
|
key: "p", |
||||||
|
name: `Pleasant (met needs) feelings${isPopulatedArray(value?.feelings?.pleasant) ? chalk.dim(` (currently ${pluralize("group", value?.feelings?.pleasant?.length || 0, true)} totaling ${pluralize("item", totalItems(value?.feelings?.pleasant || []), true)})`) : ""}`, |
||||||
|
value: PLEASANT, |
||||||
|
}, |
||||||
|
{ |
||||||
|
key: "u", |
||||||
|
name: `Unpleasant (unmet needs) feelings${isPopulatedArray(value?.feelings?.unpleasant) ? chalk.dim(` (currently ${pluralize("group", value?.feelings?.unpleasant?.length || 0, true)} totaling ${pluralize("item", totalItems(value?.feelings?.unpleasant || []), true)})`) : ""}`, |
||||||
|
value: UNPLEASANT, |
||||||
|
}, |
||||||
|
{ |
||||||
|
key: "n", |
||||||
|
name: `Needs${isPopulatedArray(value?.needs) ? chalk.dim(` (currently ${pluralize("group", value?.needs?.length || 0, true)} totaling ${pluralize("item", totalItems(value?.needs || []), true)})`) : ""}`, |
||||||
|
value: NEEDS, |
||||||
|
}, |
||||||
|
new Separator(), |
||||||
|
{ |
||||||
|
key: "i", |
||||||
|
name: "Inspect and modify the current empathy guide in the editor", |
||||||
|
value: INSPECT, |
||||||
|
}, |
||||||
|
{ |
||||||
|
key: "s", |
||||||
|
name: "Save the empathy guide", |
||||||
|
value: SAVE |
||||||
|
}, |
||||||
|
new Separator(), |
||||||
|
] |
||||||
|
}) |
||||||
|
switch (result) { |
||||||
|
case PLEASANT: |
||||||
|
const newPleasant = await promptForEmpathyGroupList({ |
||||||
|
default: value?.feelings?.pleasant, |
||||||
|
listName: "pleasant (met needs) feelings" |
||||||
|
}) |
||||||
|
return promptForEmpathyGuide({ |
||||||
|
default: { |
||||||
|
...value, |
||||||
|
feelings: { |
||||||
|
...value?.feelings || {}, |
||||||
|
pleasant: newPleasant, |
||||||
|
} |
||||||
|
} |
||||||
|
}, deps) |
||||||
|
case UNPLEASANT: |
||||||
|
const newUnpleasant = await promptForEmpathyGroupList({ |
||||||
|
default: value?.feelings?.unpleasant, |
||||||
|
listName: "unpleasant (unmet needs) feelings" |
||||||
|
}) |
||||||
|
return promptForEmpathyGuide({ |
||||||
|
default: { |
||||||
|
...value, |
||||||
|
feelings: { |
||||||
|
...value?.feelings || {}, |
||||||
|
unpleasant: newUnpleasant, |
||||||
|
} |
||||||
|
} |
||||||
|
}, deps) |
||||||
|
case NEEDS: |
||||||
|
const newNeeds = await promptForEmpathyGroupList({default: value?.needs, listName: "needs"}) |
||||||
|
return promptForEmpathyGuide({ |
||||||
|
default: { |
||||||
|
...value, |
||||||
|
needs: newNeeds |
||||||
|
} |
||||||
|
}, deps) |
||||||
|
case SAVE: |
||||||
|
return value || {} |
||||||
|
default: |
||||||
|
case INSPECT: |
||||||
|
return promptForEmpathyGuide({ |
||||||
|
default: await editYaml({ |
||||||
|
schema: EmpathyGuideJTD, |
||||||
|
currentValue: value, |
||||||
|
name: "the current empathy guide", |
||||||
|
inquire, |
||||||
|
showError |
||||||
|
}) |
||||||
|
}, deps) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,112 @@ |
|||||||
|
import {InquireFunction, ShowFunction} from "../Inquire"; |
||||||
|
import {ExpandChoiceOptions, Separator, SeparatorOptions} from "inquirer"; |
||||||
|
import {merge} from "../../utils/Merge"; |
||||||
|
import {Entry, EntryJTD} from "../../datatypes/Entry"; |
||||||
|
import {editYaml} from "../../schemata/YAMLPrompt"; |
||||||
|
|
||||||
|
const VIEW = ".View" |
||||||
|
const DONE = ".Done" |
||||||
|
|
||||||
|
export interface EntryMainMenuChoice<PropertyT extends string & keyof EntryMainMenuOptions> { |
||||||
|
readonly type: "mainMenu" |
||||||
|
readonly property: PropertyT |
||||||
|
choice(input: EntryMainMenuOptions[PropertyT]): ExpandChoiceOptions & { value: PropertyT } |
||||||
|
onSelected(input: EntryMainMenuOptions[PropertyT]): Promise<EntryMainMenuOptions[PropertyT]> |
||||||
|
} |
||||||
|
function getChoice<PropertyT extends string & keyof EntryMainMenuOptions>(choice: EntryMainMenuChoice<PropertyT>, entry: EntryMainMenuOptions): ExpandChoiceOptions & {value: PropertyT} { |
||||||
|
return choice.choice(entry[choice.property]) |
||||||
|
} |
||||||
|
async function onSelected<PropertyT extends string & keyof EntryMainMenuOptions>(choice: EntryMainMenuChoice<PropertyT>, entry: EntryMainMenuOptions): Promise<Partial<EntryMainMenuOptions>> { |
||||||
|
return { |
||||||
|
[choice.property]: await choice.onSelected(entry[choice.property]) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function makeEntryMainMenuChoice<PropertyT extends string & keyof EntryMainMenuOptions, OptionsT, OutputT>({property, key, name, short, injected, toOptions, toProperty}: { |
||||||
|
property: PropertyT, |
||||||
|
key: string, |
||||||
|
name: string|((value: EntryMainMenuOptions[PropertyT]) => string), |
||||||
|
short?: string|((value: EntryMainMenuOptions[PropertyT]) => string), |
||||||
|
injected: (options: OptionsT) => Promise<OutputT>, |
||||||
|
toOptions: (input: EntryMainMenuOptions[PropertyT]) => OptionsT, |
||||||
|
toProperty: (output: OutputT) => EntryMainMenuOptions[PropertyT] |
||||||
|
}): { |
||||||
|
(options: OptionsT): Promise<OutputT>, |
||||||
|
mainMenu: EntryMainMenuChoice<PropertyT> |
||||||
|
} { |
||||||
|
return Object.assign(injected, { |
||||||
|
"mainMenu": { |
||||||
|
type: "mainMenu" as const, |
||||||
|
property, |
||||||
|
choice(input: EntryMainMenuOptions[PropertyT]) { |
||||||
|
return { |
||||||
|
name: typeof name === "string" ? name : name(input), |
||||||
|
short: typeof short === "string" ? short : typeof short === "function" ? short(input) : typeof name === "string" ? name : name(input), |
||||||
|
value: property as PropertyT, |
||||||
|
key, |
||||||
|
} |
||||||
|
}, |
||||||
|
async onSelected(input: EntryMainMenuOptions[PropertyT]): Promise<EntryMainMenuOptions[PropertyT]> { |
||||||
|
const output = await injected(toOptions(input)) |
||||||
|
return toProperty(output) |
||||||
|
}, |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
export interface EntryMainMenuOptions extends Omit<Entry, "finishedAt"> {} |
||||||
|
|
||||||
|
export interface EntryMainMenuDependencies { |
||||||
|
readonly inquire: InquireFunction |
||||||
|
readonly choices: readonly (EntryMainMenuChoice<any>|SeparatorOptions)[] |
||||||
|
readonly showError: ShowFunction |
||||||
|
} |
||||||
|
|
||||||
|
export function entryMainMenuPrompt(deps: EntryMainMenuDependencies): (options: EntryMainMenuOptions) => Promise<Entry> { |
||||||
|
return (options) => promptForEntryMainMenu(options, deps) |
||||||
|
} |
||||||
|
|
||||||
|
export async function promptForEntryMainMenu(entry: EntryMainMenuOptions, deps: EntryMainMenuDependencies): Promise<Entry> { |
||||||
|
const { |
||||||
|
inquire, |
||||||
|
choices, |
||||||
|
showError, |
||||||
|
} = deps |
||||||
|
const result = await inquire({ |
||||||
|
type: "expand", |
||||||
|
message: "Anything else you want to add?", |
||||||
|
pageSize: 999, |
||||||
|
choices: [ |
||||||
|
...choices.map((choice) => choice.type === "separator" ? choice : getChoice(choice, entry)), |
||||||
|
new Separator(), |
||||||
|
{name: "View and edit this entry as yaml", value: VIEW, key: "v"}, |
||||||
|
{name: "Save and upload this entry", value: DONE, key: "q"}, |
||||||
|
new Separator(), |
||||||
|
] |
||||||
|
}) |
||||||
|
|
||||||
|
function continueWith(update: Partial<EntryMainMenuOptions>): Promise<Entry> { |
||||||
|
const data = merge(update, entry) |
||||||
|
return promptForEntryMainMenu(data, deps) |
||||||
|
} |
||||||
|
|
||||||
|
if (result === DONE) { |
||||||
|
return { |
||||||
|
...entry, |
||||||
|
finishedAt: new Date(), |
||||||
|
} |
||||||
|
} else if (result === VIEW) { |
||||||
|
const updated = await editYaml({ |
||||||
|
schema: EntryJTD, |
||||||
|
currentValue: {...entry, finishedAt: new Date()}, |
||||||
|
name: "the current entry", |
||||||
|
inquire, |
||||||
|
showError, |
||||||
|
}) |
||||||
|
const withoutFinishedAt = {...updated, finishedAt: undefined} |
||||||
|
return continueWith(withoutFinishedAt) |
||||||
|
} else { |
||||||
|
const selectedChoice = choices.find((choice) => choice.type === "mainMenu" && choice.property === result) as EntryMainMenuChoice<any> |
||||||
|
return continueWith(await onSelected(selectedChoice, entry)) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,34 @@ |
|||||||
|
import {ConditionPromptOptions} from "./ConditionPrompt"; |
||||||
|
import {Condition} from "../../datatypes/Condition"; |
||||||
|
import {SummaryPromptOptions} from "./SummaryPrompt"; |
||||||
|
import {EntryMainMenuOptions} from "./EntryMainMenuPrompt"; |
||||||
|
import {Entry} from "../../datatypes/Entry"; |
||||||
|
|
||||||
|
interface EntryPromptOptions { |
||||||
|
} |
||||||
|
|
||||||
|
interface EntryPromptDependencies { |
||||||
|
promptForCondition: (options: ConditionPromptOptions) => Promise<Condition> |
||||||
|
promptForSummary: (options: SummaryPromptOptions) => Promise<string | null> |
||||||
|
promptForEntryMainMenu: (options: EntryMainMenuOptions) => Promise<Entry> |
||||||
|
} |
||||||
|
|
||||||
|
export function entryPrompt(deps: EntryPromptDependencies): (options: EntryPromptOptions) => Promise<Entry> { |
||||||
|
return (options) => promptForEntry(options, deps) |
||||||
|
} |
||||||
|
|
||||||
|
export async function promptForEntry(options: EntryPromptOptions, dependencies: EntryPromptDependencies): Promise<Entry> { |
||||||
|
const { |
||||||
|
promptForCondition, |
||||||
|
promptForSummary, |
||||||
|
promptForEntryMainMenu, |
||||||
|
} = dependencies |
||||||
|
const startedAt = new Date() |
||||||
|
const condition = await promptForCondition({}) |
||||||
|
const summary = await promptForSummary({}) |
||||||
|
if (summary === null) { |
||||||
|
return promptForEntryMainMenu({startedAt, condition}) |
||||||
|
} else { |
||||||
|
return promptForEntryMainMenu({startedAt, condition, summary}) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,106 @@ |
|||||||
|
import {PersonaPrompt} from "../../datatypes/Persona"; |
||||||
|
import {guidedEmpathyToStringShort, GuidedEmpathy} from "../../datatypes/GuidedEmpathy"; |
||||||
|
import {InquireFunction} from "../Inquire"; |
||||||
|
import {EntryMainMenuChoice, makeEntryMainMenuChoice} from "./EntryMainMenuPrompt"; |
||||||
|
import chalk from "chalk"; |
||||||
|
import pluralize from "pluralize"; |
||||||
|
import {isPopulatedArray} from "../../utils/Arrays"; |
||||||
|
import {ListChoiceOptions} from "inquirer"; |
||||||
|
import {asDefault} from "../../utils/Objects"; |
||||||
|
import {GuidedEmpathyPromptOptions} from "./GuidedEmpathyPrompt"; |
||||||
|
|
||||||
|
export interface GuidedEmpathyListPromptOptions extends PersonaPrompt { |
||||||
|
readonly default?: readonly GuidedEmpathy[] |
||||||
|
} |
||||||
|
|
||||||
|
export interface GuidedEmpathyListPromptDependencies { |
||||||
|
readonly inquire: InquireFunction |
||||||
|
readonly promptForEmpathy: (input: GuidedEmpathyPromptOptions) => Promise<GuidedEmpathy|null> |
||||||
|
} |
||||||
|
|
||||||
|
export function guidedEmpathyListPrompt(deps: GuidedEmpathyListPromptDependencies): { |
||||||
|
(options: GuidedEmpathyListPromptOptions): Promise<readonly GuidedEmpathy[]>; mainMenu: EntryMainMenuChoice<"guidedEmpathy"> } { |
||||||
|
return makeEntryMainMenuChoice({ |
||||||
|
property: "guidedEmpathy", |
||||||
|
name: (input) => typeof input === "object" ? `Continue guided empathy ${chalk.dim(`(currently ${chalk.greenBright(`${pluralize("entry", input.length, true)}`)})`)}` : "Do some guided empathy", |
||||||
|
key: "e", |
||||||
|
injected: (options) => promptForGuidedEmpathyList(options, deps), |
||||||
|
toOptions: asDefault, |
||||||
|
toProperty: (input) => input.length === 0 ? undefined : input, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
export async function promptForGuidedEmpathyList(options: GuidedEmpathyListPromptOptions, deps: GuidedEmpathyListPromptDependencies): Promise<readonly GuidedEmpathy[]> { |
||||||
|
const {inquire, promptForEmpathy} = deps |
||||||
|
if (!isPopulatedArray(options.default)) { |
||||||
|
const empathy = await promptForEmpathy(typeof options.persona === "string" ? {persona: options.persona} : {}) |
||||||
|
if (empathy === null) { |
||||||
|
return [] |
||||||
|
} else { |
||||||
|
return promptForGuidedEmpathyList({ |
||||||
|
...options, |
||||||
|
default: [empathy], |
||||||
|
}, deps) |
||||||
|
} |
||||||
|
} else { |
||||||
|
const result = await inquire({ |
||||||
|
type: "list", |
||||||
|
message: "Want to change an existing entry or add a new one?", |
||||||
|
default: options.default.length, |
||||||
|
choices: [ |
||||||
|
...options.default.map((item, index): ListChoiceOptions => ({ |
||||||
|
name: guidedEmpathyToStringShort(item), |
||||||
|
value: index, |
||||||
|
short: guidedEmpathyToStringShort(item) |
||||||
|
})), |
||||||
|
{ |
||||||
|
name: "Add New Entry", |
||||||
|
value: -1, |
||||||
|
short: "Add New Entry" |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "Done", |
||||||
|
value: -2, |
||||||
|
short: "Done" |
||||||
|
} |
||||||
|
] |
||||||
|
}) |
||||||
|
if (result === -1) { |
||||||
|
const empathy = await promptForEmpathy(typeof options.persona === "string" ? {persona: options.persona} : {}) |
||||||
|
if (empathy === null) { |
||||||
|
if (!isPopulatedArray(options.default)) { |
||||||
|
return [] |
||||||
|
} else { |
||||||
|
return promptForGuidedEmpathyList({ |
||||||
|
...options, |
||||||
|
default: options.default, |
||||||
|
}, deps) |
||||||
|
} |
||||||
|
} else { |
||||||
|
return promptForGuidedEmpathyList({ |
||||||
|
...options, |
||||||
|
default: [...options.default, empathy], |
||||||
|
}, deps) |
||||||
|
} |
||||||
|
} else if (result === -2) { |
||||||
|
return options.default |
||||||
|
} else { |
||||||
|
const empathy = await promptForEmpathy(typeof options.persona === "string" ? {persona: options.persona, default: options.default[result]} : {default: options.default[result]}) |
||||||
|
if (empathy === null) { |
||||||
|
if (options.default.length === 1) { |
||||||
|
return [] |
||||||
|
} else { |
||||||
|
return promptForGuidedEmpathyList({ |
||||||
|
...options, |
||||||
|
default: [...options.default.slice(0, result), ...options.default.slice(result + 1)] |
||||||
|
}, deps) |
||||||
|
} |
||||||
|
} else { |
||||||
|
return promptForGuidedEmpathyList({ |
||||||
|
...options, |
||||||
|
default: [...options.default.slice(0, result), empathy, ...options.default.slice(result + 1)] |
||||||
|
}, deps) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,95 @@ |
|||||||
|
import { |
||||||
|
personaName, |
||||||
|
PersonaPrompt, |
||||||
|
personaPronounObject, |
||||||
|
personaPronounPossessive, |
||||||
|
personaVerb |
||||||
|
} from "../../datatypes/Persona"; |
||||||
|
import {InquireFunction, ShowFunction} from "../Inquire"; |
||||||
|
import {EmpathyGuide} from "../../datatypes/EmpathyGuide"; |
||||||
|
import {isPopulatedArray} from "../../utils/Arrays"; |
||||||
|
import Separator from "inquirer/lib/objects/separator"; |
||||||
|
import {EmpathyGroup} from "../../datatypes/EmpathyGroup"; |
||||||
|
import {GuidedEmpathy, guidedEmpathyToString, isPopulatedGuidedEmpathy} from "../../datatypes/GuidedEmpathy"; |
||||||
|
import {HierarchicalCheckboxChildChoice, HierarchicalCheckboxParentChoice } from "inquirer"; |
||||||
|
|
||||||
|
export interface GuidedEmpathyPromptOptions extends PersonaPrompt { |
||||||
|
readonly default?: GuidedEmpathy |
||||||
|
} |
||||||
|
|
||||||
|
export interface GuidedEmpathyPromptDependencies { |
||||||
|
readonly inquire: InquireFunction, |
||||||
|
readonly guideFactory: () => EmpathyGuide, |
||||||
|
readonly show: ShowFunction, |
||||||
|
} |
||||||
|
|
||||||
|
export function guidedEmpathyPrompt(deps: GuidedEmpathyPromptDependencies): (options: GuidedEmpathyPromptOptions) => Promise<GuidedEmpathy | null> { |
||||||
|
return (options) => promptForGuidedEmpathy(options, deps) |
||||||
|
} |
||||||
|
|
||||||
|
function toChoices(item: EmpathyGroup): HierarchicalCheckboxParentChoice | HierarchicalCheckboxChildChoice { |
||||||
|
const children = item.items |
||||||
|
if (isPopulatedArray(children)) { |
||||||
|
return { |
||||||
|
name: item.header, |
||||||
|
children: item.items.map((child) => ({ |
||||||
|
name: child, |
||||||
|
value: child, |
||||||
|
})) as [HierarchicalCheckboxChildChoice, ...HierarchicalCheckboxChildChoice[]], |
||||||
|
showChildren: true, |
||||||
|
} |
||||||
|
} else { |
||||||
|
return { |
||||||
|
name: item.header, |
||||||
|
value: null |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export async function promptForGuidedEmpathy(options: GuidedEmpathyPromptOptions, deps: GuidedEmpathyPromptDependencies): Promise<GuidedEmpathy | null> { |
||||||
|
const {inquire, guideFactory, show} = deps |
||||||
|
const guide = guideFactory() |
||||||
|
const {default: value = {}, persona} = options |
||||||
|
if (isPopulatedGuidedEmpathy(value)) { |
||||||
|
await show(guidedEmpathyToString(value, persona)) |
||||||
|
} |
||||||
|
const feelings = await inquire({ |
||||||
|
type: "hierarchical-checkbox", |
||||||
|
message: `What ${personaVerb(persona, "are", "is")} ${personaName(persona)} feeling?`, |
||||||
|
default: value.feelings, |
||||||
|
choices: [ |
||||||
|
...guide.feelings?.pleasant?.map(toChoices) || [], |
||||||
|
...isPopulatedArray(guide.feelings?.pleasant) && isPopulatedArray(guide.feelings?.unpleasant) ? [new Separator()] : [], |
||||||
|
...guide.feelings?.unpleasant?.map(toChoices) || [], |
||||||
|
] |
||||||
|
}) |
||||||
|
const needs = await inquire({ |
||||||
|
type: "hierarchical-checkbox", |
||||||
|
message: `What ${personaVerb(persona, "do", "does")} ${personaName(persona)} need that causes ${personaPronounObject(persona)} to feel that way?`, |
||||||
|
default: value.needs, |
||||||
|
choices: [ |
||||||
|
...guide.needs?.map(toChoices) || [], |
||||||
|
] |
||||||
|
}) |
||||||
|
const events = await inquire({ |
||||||
|
type: "multitext", |
||||||
|
message: `What happened that interacted with ${personaPronounPossessive(persona)} needs?`, |
||||||
|
default: value.events, |
||||||
|
}) |
||||||
|
const requests = await inquire({ |
||||||
|
type: "multitext", |
||||||
|
message: `What would help get the best outcomes for ${personaPronounObject(persona)} in the future?`, |
||||||
|
default: value.requests, |
||||||
|
}) |
||||||
|
const result: GuidedEmpathy = { |
||||||
|
feelings, |
||||||
|
needs, |
||||||
|
events, |
||||||
|
requests, |
||||||
|
} |
||||||
|
if (isPopulatedGuidedEmpathy(result)) { |
||||||
|
return result |
||||||
|
} else { |
||||||
|
return null |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,43 @@ |
|||||||
|
import {EditorQuestion} from "inquirer"; |
||||||
|
import {InquireFunction} from "../Inquire"; |
||||||
|
import {personaPossessive, PersonaPrompt} from "../../datatypes/Persona"; |
||||||
|
import {EntryMainMenuChoice, makeEntryMainMenuChoice} from "./EntryMainMenuPrompt"; |
||||||
|
import chalk from "chalk"; |
||||||
|
import pluralize from "pluralize"; |
||||||
|
import wordcount from "wordcount"; |
||||||
|
import {asDefault} from "../../utils/Objects"; |
||||||
|
|
||||||
|
export interface JournalEntryPromptOptions extends Partial<Omit<EditorQuestion, "name" | "type">>, PersonaPrompt {} |
||||||
|
|
||||||
|
export interface JournalEntryPromptDependencies { |
||||||
|
readonly inquire: InquireFunction |
||||||
|
} |
||||||
|
|
||||||
|
export function journalEntryPrompt(deps: JournalEntryPromptDependencies): { |
||||||
|
(options: JournalEntryPromptOptions): Promise<string|null>, |
||||||
|
mainMenu: EntryMainMenuChoice<"journalEntry">, |
||||||
|
} { |
||||||
|
return makeEntryMainMenuChoice({ |
||||||
|
property: "journalEntry", |
||||||
|
name: (input) => typeof input === "string" ? `Change journal entry ${chalk.dim(`(currently ${chalk.greenBright(`${pluralize("word", wordcount(input), true)} long`)})`)}` : "Add journal entry", |
||||||
|
key: "j", |
||||||
|
injected: (options) => promptForJournalEntry(options, deps), |
||||||
|
toOptions: asDefault, |
||||||
|
toProperty: (input) => input === null ? undefined : input, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
export async function promptForJournalEntry(options: JournalEntryPromptOptions, {inquire}: JournalEntryPromptDependencies): Promise<string | null> { |
||||||
|
const {persona} = options |
||||||
|
const result = await inquire({ |
||||||
|
type: "editor", |
||||||
|
message: typeof options.default === "string" ? `Edit ${personaPossessive(persona)} journal entry:` : `Type up ${personaPossessive(persona)} journal entry:`, |
||||||
|
...options, |
||||||
|
}) |
||||||
|
const trimmed = result.trimEnd() |
||||||
|
if (trimmed === "") { |
||||||
|
return null |
||||||
|
} else { |
||||||
|
return trimmed |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,56 @@ |
|||||||
|
import {ListQuestion} from "inquirer"; |
||||||
|
import {EntryMainMenuChoice, makeEntryMainMenuChoice} from "./EntryMainMenuPrompt"; |
||||||
|
import chalk from "chalk"; |
||||||
|
import {asDefault, identity} from "../../utils/Objects"; |
||||||
|
import {InquireFunction} from "../Inquire"; |
||||||
|
import {Suicidality} from "../../datatypes/Suicidality"; |
||||||
|
|
||||||
|
export interface SuicidalityPromptOptions extends Partial<Omit<ListQuestion, "name" | "type" | "choices">> { |
||||||
|
} |
||||||
|
|
||||||
|
export interface SuicidalityPromptDependencies { |
||||||
|
inquire: InquireFunction |
||||||
|
} |
||||||
|
|
||||||
|
export function suicidalityPrompt(deps: SuicidalityPromptDependencies): { |
||||||
|
(options: SuicidalityPromptOptions): Promise<Suicidality>, |
||||||
|
mainMenu: EntryMainMenuChoice<"suicidality">, |
||||||
|
} { |
||||||
|
return makeEntryMainMenuChoice({ |
||||||
|
property: "suicidality", |
||||||
|
name: (input) => typeof input === "string" ? `Change record of suicidal thoughts ${chalk.dim(`(currently ${chalk.greenBright(input)})`)}` : "Add record of suicidal thoughts", |
||||||
|
key: "x", |
||||||
|
injected: (options) => promptForSuicidality(options, deps), |
||||||
|
toOptions: asDefault, |
||||||
|
toProperty: identity, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
export async function promptForSuicidality(options: SuicidalityPromptOptions, {inquire}: SuicidalityPromptDependencies): Promise<Suicidality> { |
||||||
|
return await inquire({ |
||||||
|
type: "list", |
||||||
|
message: "What stage are your suicidal thoughts at?", |
||||||
|
choices: [ |
||||||
|
{value: Suicidality.NONE, name: `None ${chalk.dim("(no or only rare thoughts present)")}`}, |
||||||
|
{ |
||||||
|
value: Suicidality.PASSIVE, |
||||||
|
name: `Passive ${chalk.dim("(faint call of the void, but without intention)")}` |
||||||
|
}, |
||||||
|
{ |
||||||
|
value: Suicidality.INTRUSIVE, |
||||||
|
name: `Intrusive ${chalk.dim("(intrusive thoughts with intention, not actively engaged with)")}` |
||||||
|
}, |
||||||
|
{value: Suicidality.ACTIVE, name: `Active ${chalk.dim("(actively engaging with thoughts)")}`}, |
||||||
|
{ |
||||||
|
value: Suicidality.RESIGNED, |
||||||
|
name: `Resigned ${chalk.dim("(given in to despair, but nothing further than that)")}` |
||||||
|
}, |
||||||
|
{value: Suicidality.PLANNING, name: `Planning ${chalk.dim("(actively making plans to attempt)")}`}, |
||||||
|
{value: Suicidality.PLANNED, name: `Planned ${chalk.dim("(prepared a plan to attempt)")}`}, |
||||||
|
{value: Suicidality.DANGER, name: `Danger ${chalk.dim("(on the precipice of attempting)")}`}, |
||||||
|
], |
||||||
|
default: 0, |
||||||
|
pageSize: 999, |
||||||
|
...options, |
||||||
|
}) |
||||||
|
} |
@ -0,0 +1,42 @@ |
|||||||
|
import {InputQuestion} from "inquirer"; |
||||||
|
import {InquireFunction} from "../Inquire"; |
||||||
|
import {personaName, personaPossessive, PersonaPrompt, personaVerb} from "../../datatypes/Persona"; |
||||||
|
import {EntryMainMenuChoice, makeEntryMainMenuChoice} from "./EntryMainMenuPrompt"; |
||||||
|
import chalk from "chalk"; |
||||||
|
import pluralize from "pluralize"; |
||||||
|
import wordcount from "wordcount"; |
||||||
|
import {asDefault} from "../../utils/Objects"; |
||||||
|
|
||||||
|
export interface SummaryPromptOptions extends Partial<Omit<InputQuestion, "name" | "type">>, PersonaPrompt {} |
||||||
|
|
||||||
|
export interface SummaryPromptDependencies { |
||||||
|
readonly inquire: InquireFunction |
||||||
|
} |
||||||
|
|
||||||
|
export function summaryPrompt(deps: SummaryPromptDependencies): { |
||||||
|
(options: SummaryPromptOptions): Promise<string|null>, |
||||||
|
mainMenu: EntryMainMenuChoice<"summary">, |
||||||
|
} { |
||||||
|
return makeEntryMainMenuChoice({ |
||||||
|
property: "summary", |
||||||
|
name: (input) => typeof input === "string" ? `Change summary ${chalk.dim(`(currently ${chalk.greenBright(`${pluralize("word", wordcount(input), true)} long`)})`)}` : "Add summary", |
||||||
|
key: "s", |
||||||
|
injected: (options) => promptForSummary(options, deps), |
||||||
|
toOptions: asDefault, |
||||||
|
toProperty: (input) => input === null ? undefined : input, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
export async function promptForSummary(options: SummaryPromptOptions, {inquire}: SummaryPromptDependencies): Promise<string | null> { |
||||||
|
const {persona} = options |
||||||
|
const result = await inquire({ |
||||||
|
type: "input", |
||||||
|
message: typeof options.default === "string" ? `What's ${personaPossessive(persona)} new summary?` : `Can you summarize how ${personaName(persona)} ${personaVerb(persona, "feel", "feels")}?`, |
||||||
|
...options, |
||||||
|
}) |
||||||
|
if (result === "") { |
||||||
|
return null |
||||||
|
} else { |
||||||
|
return result |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,672 @@ |
|||||||
|
import Prompt from "inquirer/lib/prompts/base"; |
||||||
|
import observe from "inquirer/lib/utils/events"; |
||||||
|
import { |
||||||
|
Answers, |
||||||
|
ChoiceOptions, |
||||||
|
HierarchicalCheckboxChoice, |
||||||
|
HierarchicalCheckboxQuestion, |
||||||
|
Question, |
||||||
|
Separator, |
||||||
|
SeparatorOptions, |
||||||
|
} from "inquirer"; |
||||||
|
import {Interface as ReadLineInterface} from "readline"; |
||||||
|
import {filter} from "rxjs/operators" |
||||||
|
import chalk from "chalk"; |
||||||
|
import {Subject} from "rxjs"; |
||||||
|
import figures from "figures"; |
||||||
|
import Paginator from "inquirer/lib/utils/paginator"; |
||||||
|
import {isPopulatedArray} from "../../utils/Arrays"; |
||||||
|
import {filter as fuzzyFilter, FilterOptions} from "fuzzy"; |
||||||
|
|
||||||
|
interface ExtendedReadLine extends ReadLineInterface { |
||||||
|
line: string |
||||||
|
cursor: number |
||||||
|
} |
||||||
|
|
||||||
|
declare module "inquirer" { |
||||||
|
|
||||||
|
export interface HierarchicalCheckboxChoiceBase extends ChoiceOptions { |
||||||
|
readonly value?: string|null |
||||||
|
readonly children?: readonly HierarchicalCheckboxChoice[] |
||||||
|
readonly showChildren?: boolean |
||||||
|
} |
||||||
|
export interface HierarchicalCheckboxParentChoice extends HierarchicalCheckboxChoiceBase { |
||||||
|
readonly showChildren?: boolean |
||||||
|
readonly value?: null |
||||||
|
readonly children: readonly [HierarchicalCheckboxChoice, ...HierarchicalCheckboxChoice[]] |
||||||
|
} |
||||||
|
export interface HierarchicalCheckboxChildChoice extends HierarchicalCheckboxChoiceBase { |
||||||
|
readonly value: string|null |
||||||
|
readonly children?: readonly [] |
||||||
|
readonly showChildren?: false |
||||||
|
} |
||||||
|
|
||||||
|
export type HierarchicalCheckboxChoice = HierarchicalCheckboxParentChoice|HierarchicalCheckboxChildChoice |
||||||
|
|
||||||
|
export interface HierarchicalCheckboxQuestionOptions { |
||||||
|
readonly choices: readonly (HierarchicalCheckboxChoice|SeparatorOptions)[] |
||||||
|
readonly pageSize?: number |
||||||
|
} |
||||||
|
|
||||||
|
export interface HierarchicalCheckboxQuestion<AnswerT extends Answers = Answers> extends Question<AnswerT>, HierarchicalCheckboxQuestionOptions { |
||||||
|
/** @inheritDoc */ |
||||||
|
type: "hierarchical-checkbox" |
||||||
|
default?: string[] |
||||||
|
} |
||||||
|
|
||||||
|
export interface QuestionMap { |
||||||
|
["hierarchical-checkbox"]: HierarchicalCheckboxQuestion |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
interface NormalizedChoiceBase { |
||||||
|
readonly name: string |
||||||
|
readonly value: string|null |
||||||
|
readonly short: string|null |
||||||
|
readonly children?: readonly NormalizedChoice[] |
||||||
|
} |
||||||
|
|
||||||
|
interface NormalizedParentChoice extends NormalizedChoiceBase { |
||||||
|
readonly name: string |
||||||
|
readonly value: null |
||||||
|
readonly short: null |
||||||
|
readonly children: readonly [NormalizedChoice, ...NormalizedChoice[]] |
||||||
|
readonly showChildren: boolean |
||||||
|
} |
||||||
|
|
||||||
|
interface NormalizedLeafChoice extends NormalizedChoiceBase { |
||||||
|
readonly name: string |
||||||
|
readonly value: string |
||||||
|
readonly short: string |
||||||
|
readonly children?: readonly [] |
||||||
|
} |
||||||
|
|
||||||
|
interface NormalizedDisabledChoice extends NormalizedChoiceBase { |
||||||
|
readonly name: string |
||||||
|
readonly value: null |
||||||
|
readonly short: null |
||||||
|
readonly children?: readonly [] |
||||||
|
} |
||||||
|
type NormalizedChoice = NormalizedParentChoice|NormalizedLeafChoice|NormalizedDisabledChoice |
||||||
|
|
||||||
|
function isParentChoice(c: NormalizedChoice): c is NormalizedParentChoice { |
||||||
|
return isPopulatedArray(c.children) |
||||||
|
} |
||||||
|
function isLeafChoice(c: NormalizedChoice): c is NormalizedLeafChoice { |
||||||
|
return c.value !== null |
||||||
|
} |
||||||
|
function isDisabledChoice(c: NormalizedChoice): c is NormalizedDisabledChoice { |
||||||
|
return c.value === null && !isPopulatedArray(c.children) |
||||||
|
} |
||||||
|
|
||||||
|
function normalizeChoice(choice: HierarchicalCheckboxChoice|SeparatorOptions, valueMap: ValueMap): NormalizedChoice { |
||||||
|
if (choice.type === "separator") { |
||||||
|
return { |
||||||
|
name: choice.line || new Separator().line, |
||||||
|
value: null, |
||||||
|
short: null, |
||||||
|
children: [], |
||||||
|
} |
||||||
|
} |
||||||
|
const { name = chalk.dim("undefined") } = choice |
||||||
|
const children = isPopulatedArray(choice.children) ? choice.children.map((choice) => normalizeChoice(choice, valueMap)) : []; |
||||||
|
if (isPopulatedArray(children)) { |
||||||
|
const { showChildren = true } = choice |
||||||
|
return { |
||||||
|
name, |
||||||
|
value: null, |
||||||
|
short: null, |
||||||
|
children, |
||||||
|
showChildren, |
||||||
|
} |
||||||
|
} else { |
||||||
|
const { name: originalName = null } = choice |
||||||
|
const { value = originalName } = choice |
||||||
|
if (value === null) { |
||||||
|
// Disabled choice, no need for a short value
|
||||||
|
return { |
||||||
|
name, |
||||||
|
value: null, |
||||||
|
short: null, |
||||||
|
children: [], |
||||||
|
} |
||||||
|
} else { |
||||||
|
// Selectable leaf choice
|
||||||
|
const { short = name } = choice |
||||||
|
const result: NormalizedLeafChoice = { |
||||||
|
name, |
||||||
|
value, |
||||||
|
short, |
||||||
|
children: [] |
||||||
|
} |
||||||
|
addValueToMap(result, valueMap) |
||||||
|
return result |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
interface BaseBreadcrumb { |
||||||
|
readonly parent: Breadcrumb|null |
||||||
|
readonly index: number|null |
||||||
|
readonly children: readonly NormalizedChoice[] |
||||||
|
readonly searchable: readonly (NormalizedLeafChoice|NormalizedParentChoice)[] |
||||||
|
} |
||||||
|
|
||||||
|
interface RootBreadcrumb extends BaseBreadcrumb { |
||||||
|
readonly parent: null |
||||||
|
readonly index: null |
||||||
|
readonly selectable: readonly number[] |
||||||
|
} |
||||||
|
|
||||||
|
interface ChildBreadcrumb extends BaseBreadcrumb { |
||||||
|
readonly parent: Breadcrumb |
||||||
|
readonly index: number |
||||||
|
readonly selectable: readonly number[] |
||||||
|
} |
||||||
|
|
||||||
|
interface FilterBreadcrumb extends BaseBreadcrumb { |
||||||
|
readonly parent: Breadcrumb |
||||||
|
readonly index: null |
||||||
|
readonly filter: string |
||||||
|
readonly adjustingFilter: boolean |
||||||
|
} |
||||||
|
|
||||||
|
type Breadcrumb = RootBreadcrumb|ChildBreadcrumb|FilterBreadcrumb |
||||||
|
|
||||||
|
function isRootBreadcrumb(b: Breadcrumb): b is RootBreadcrumb { |
||||||
|
return b.parent === null && b.index === null |
||||||
|
} |
||||||
|
function isFilterBreadcrumb(b: Breadcrumb): b is FilterBreadcrumb { |
||||||
|
return b.parent !== null && b.index === null |
||||||
|
} |
||||||
|
|
||||||
|
interface ValueMap { |
||||||
|
[value: string]: NormalizedLeafChoice|[NormalizedLeafChoice, NormalizedLeafChoice, ...NormalizedLeafChoice[]] |
||||||
|
} |
||||||
|
interface ValidValueMap extends ValueMap { |
||||||
|
[value: string]: NormalizedLeafChoice |
||||||
|
} |
||||||
|
interface InvalidValueMap extends ValueMap { |
||||||
|
[value: string]: [NormalizedLeafChoice, NormalizedLeafChoice, ...NormalizedLeafChoice[]] |
||||||
|
} |
||||||
|
|
||||||
|
function addValueToMap(choice: NormalizedLeafChoice, map: ValueMap) { |
||||||
|
if (map.hasOwnProperty(choice.value)) { |
||||||
|
const existing = map[choice.value] |
||||||
|
if (Array.isArray(existing) && !existing.some((item) => item.name === choice.name && item.short === choice.short)) { |
||||||
|
map[choice.value] = [choice, ...existing] |
||||||
|
} else if (!Array.isArray(existing) && !(existing.name === choice.name && existing.short === choice.short)) { |
||||||
|
map[choice.value] = [choice, existing] |
||||||
|
} else { |
||||||
|
// It's a duplicate, but one that's the same as something already in the books.
|
||||||
|
} |
||||||
|
} else { |
||||||
|
map[choice.value] = choice |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function splitValues(map: ValueMap): {valid: ValidValueMap, invalid: InvalidValueMap} { |
||||||
|
const valid: ValidValueMap = {} |
||||||
|
const invalid: InvalidValueMap = {} |
||||||
|
Object.keys(map).forEach((value) => { |
||||||
|
const item = map[value] |
||||||
|
if (Array.isArray(item)) { |
||||||
|
invalid[value] = item |
||||||
|
} else { |
||||||
|
valid[value] = item |
||||||
|
} |
||||||
|
}) |
||||||
|
return { valid, invalid } |
||||||
|
} |
||||||
|
|
||||||
|
function getSearchableList(base: readonly NormalizedChoice[]): (NormalizedLeafChoice|NormalizedParentChoice)[] { |
||||||
|
const result = base.flatMap((choice) => { |
||||||
|
if (isParentChoice(choice)) { |
||||||
|
return [choice, ...getSearchableList(choice.children)] |
||||||
|
} else if (isLeafChoice(choice)) { |
||||||
|
return [choice] |
||||||
|
} else { |
||||||
|
return [] |
||||||
|
} |
||||||
|
}) |
||||||
|
result.sort((a: NormalizedLeafChoice|NormalizedParentChoice, b: NormalizedLeafChoice|NormalizedParentChoice): number => a.name.localeCompare(b.name)) |
||||||
|
return result.filter((value, index) => result.findIndex((match) => match.name === value.name && match.value === value.value) === index) |
||||||
|
} |
||||||
|
|
||||||
|
function getSelectableList(base: readonly NormalizedChoice[]): number[] { |
||||||
|
return base.map((_, index) => index).filter((choice) => !isDisabledChoice(base[choice])) |
||||||
|
} |
||||||
|
|
||||||
|
const [searchHighlightPrefix, searchHighlightSuffix] = chalk.yellowBright.bold("Placeholder").split("Placeholder") as [string, string] |
||||||
|
|
||||||
|
export class HierarchicalCheckboxInput<AnswerT extends Answers = Answers, QuestionT extends HierarchicalCheckboxQuestion<AnswerT> = HierarchicalCheckboxQuestion> extends Prompt<QuestionT> { |
||||||
|
|
||||||
|
constructor(question: QuestionT, readline: ExtendedReadLine, answers: AnswerT) { |
||||||
|
super(question, readline, answers); |
||||||
|
const valueMap: ValueMap = {} |
||||||
|
const rootChoices = question.choices.map((choice) => normalizeChoice(choice, valueMap)) |
||||||
|
this.location = { |
||||||
|
parent: null, |
||||||
|
index: null, |
||||||
|
children: rootChoices, |
||||||
|
selectable: getSelectableList(rootChoices), |
||||||
|
searchable: getSearchableList(rootChoices) |
||||||
|
} |
||||||
|
const { valid, invalid } = splitValues(valueMap) |
||||||
|
this.valueMap = valid |
||||||
|
this.invalidValues = invalid |
||||||
|
this.value = question.default?.slice() || [] |
||||||
|
this.activeIndex = 0 |
||||||
|
this.updateUnlistedValues() |
||||||
|
} |
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
_run(callback: HierarchicalCheckboxInput["done"], error: (err: Error) => void) { |
||||||
|
const invalidChoices = Object.keys(this.invalidValues); |
||||||
|
const invalidValues = this.value.filter((value) => !this.valueMap.hasOwnProperty(value)); |
||||||
|
if (isPopulatedArray(invalidChoices)) { |
||||||
|
error(Error(`Duplicate values: ${invalidChoices.join()}`)) |
||||||
|
} else if (isPopulatedArray(invalidValues)) { |
||||||
|
error(Error(`Unknown values: ${invalidValues.join()}`)) |
||||||
|
} |
||||||
|
this.rl.on("history", (history) => { |
||||||
|
history.splice(0, history.length) |
||||||
|
}) |
||||||
|
const events = observe(this.rl); |
||||||
|
this.status = "touched" |
||||||
|
this.done = callback |
||||||
|
events.keypress.pipe(filter((event) => { |
||||||
|
return event.key.name !== "up" && event.key.name !== "down" && event.key.name !== "tab" && event.key.name !== "backspace" && event.key.name !== "space" && !(event.key.name === "x" && event.key.ctrl === true) |
||||||
|
})).subscribe(() => this.onOtherKey()) |
||||||
|
events.normalizedUpKey.subscribe(() => this.onUpKey()) |
||||||
|
events.normalizedDownKey.subscribe(() => this.onDownKey()) |
||||||
|
events.keypress.pipe(filter((event) => event.key.name === "tab")).subscribe(() => this.onTab()) |
||||||
|
events.keypress.pipe(filter((event) => event.key.name === "backspace")).subscribe(() => this.onBack()) |
||||||
|
events.keypress.pipe(filter((event) => event.key.name === "space")).subscribe(() => this.onSpacebar()) |
||||||
|
events.line.subscribe(() => this.onLine()) |
||||||
|
events.keypress.pipe(filter((event) => event.key.name === "x" && event.key.ctrl === true)).subscribe(() => this.onClear()) |
||||||
|
const onSubmit = this.handleSubmitEvents(this.submitter) |
||||||
|
onSubmit.success.subscribe((result) => this.onValidated(result.value)) |
||||||
|
onSubmit.error.subscribe((result) => this.onValidationError(result.isValid)) |
||||||
|
this.scheduleRender() |
||||||
|
} |
||||||
|
|
||||||
|
private readonly valueMap: ValidValueMap |
||||||
|
private readonly invalidValues: InvalidValueMap |
||||||
|
private readonly value: string[] |
||||||
|
private done: (state: any) => void|null |
||||||
|
private location: Breadcrumb |
||||||
|
private activeIndex: number |
||||||
|
private unlistedValues: string[] |
||||||
|
private submitter: Subject<string[]> = new Subject<string[]>() |
||||||
|
// @ts-ignore
|
||||||
|
private paginator: Paginator = new Paginator(this.screen, {isInfinite: true}); |
||||||
|
private scheduledRender: NodeJS.Immediate|null = null |
||||||
|
|
||||||
|
private get readline(): ExtendedReadLine { |
||||||
|
return this.rl |
||||||
|
} |
||||||
|
|
||||||
|
private get activeChoice(): NormalizedChoice|null { |
||||||
|
if (this.activeIndex >= this.totalLength) { |
||||||
|
return null |
||||||
|
} |
||||||
|
if (this.isUnlistedActive()) { |
||||||
|
return this.valueMap[this.unlistedValues[this.activeUnlistedIndex()]] || null |
||||||
|
} else { |
||||||
|
return this.location.children[this.activeChildIndex()] || null |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private render(error?: string) { |
||||||
|
this.scheduledRender = null |
||||||
|
const outputs: string[] = [this.getQuestion()] |
||||||
|
const bottomOutputs: string[] = [] |
||||||
|
const isRoot = isRootBreadcrumb(this.location) |
||||||
|
const isFilter = isFilterBreadcrumb(this.location) |
||||||
|
const hasFilter = isFilterBreadcrumb(this.location) && this.location.filter !== "" |
||||||
|
const isAdjustingFilter = isFilterBreadcrumb(this.location) && this.location.adjustingFilter |
||||||
|
const isParent = this.activeChoice && isParentChoice(this.activeChoice) |
||||||
|
const isLeaf = this.activeChoice && isLeafChoice(this.activeChoice) |
||||||
|
if (this.status === "answered") { |
||||||
|
outputs.push(this.value.map((s) => `${chalk.cyan(this.valueMap[s].short)}`).join(", ")) |
||||||
|
} else { |
||||||
|
if (typeof error === "string") { |
||||||
|
bottomOutputs.push(`${figures.warning} ${error}`) |
||||||
|
} else { |
||||||
|
if (isFilterBreadcrumb(this.location)) { |
||||||
|
outputs.push(isAdjustingFilter ? chalk.cyan("Searching: ") : chalk.dim("Searching: ")) |
||||||
|
outputs.push(this.location.filter) |
||||||
|
} |
||||||
|
bottomOutputs.push(this.value.map((s) => this.valueMap[s].short).join(", ") || chalk.dim("(nothing selected)")) |
||||||
|
if (isAdjustingFilter) { |
||||||
|
bottomOutputs.push(chalk.dim(`Type to search, ${hasFilter ? "" : `${chalk.yellow("<backspace>")} to stop searching, `}` |
||||||
|
+ `${chalk.yellow("<tab>")} to switch to the search results, ` |
||||||
|
+ `${chalk.yellow("<Ctrl>+x")} to stop searching, ${chalk.yellow("<enter>")} to select the current option and finish the search.`)) |
||||||
|
|
||||||
|
} else { |
||||||
|
bottomOutputs.push(chalk.dim(`Use arrow keys to move, ${isRoot ? "" : isFilter ? hasFilter ? "" : `${chalk.yellow("<backspace>")} to stop searching, ` : `${chalk.yellow("<backspace>")} to ascend, `}` |
||||||
|
+ (isFilter ? `${chalk.yellow("<tab>")} to switch to the search box, ` : `${chalk.yellow("<tab>")} to search this view, `) |
||||||
|
+ `${isParent ? `${chalk.yellow("<space>")} to descend, ` : isLeaf ? `${chalk.yellow("<space>")} to toggle, ` : ""}` |
||||||
|
+ `${isFilter ? `${chalk.yellow("<Ctrl>+x")} to stop searching, ` : isPopulatedArray(this.value) ? `${chalk.yellow("<Ctrl>+x")} to clear selection, ` : ""}${isFilter ? `${chalk.yellow("<enter>")} to select the current option and finish the search.` : `${chalk.yellow("<enter>")} to submit.`}`)) |
||||||
|
} |
||||||
|
} |
||||||
|
bottomOutputs.push(this.renderList()) |
||||||
|
} |
||||||
|
this.screen.render(outputs.join(""), bottomOutputs.join("\n")) |
||||||
|
} |
||||||
|
|
||||||
|
private renderList(): string { |
||||||
|
const outputs: string[] = [] |
||||||
|
const isAdjustingFilter = isFilterBreadcrumb(this.location) && this.location.adjustingFilter |
||||||
|
|
||||||
|
outputs.push(...this.location.children.map((entry, index):string => { |
||||||
|
const isSelected = index === this.activeChildIndex() |
||||||
|
const isActive = isLeafChoice(entry) && this.value.includes(entry.value) |
||||||
|
const isDisabled = isDisabledChoice(entry) |
||||||
|
const isParent = isParentChoice(entry) |
||||||
|
const showChildren = isParentChoice(entry) && entry.showChildren |
||||||
|
return `${isSelected ? chalk.cyan(figures.pointer) : " "} ${isDisabled ? " " : isParent ? figures.arrowRight : isActive ? chalk.greenBright(figures.checkboxCircleOn) : figures.checkboxCircleOff} ${isSelected && !isAdjustingFilter ? (isActive ? chalk.blueBright(entry.name) : chalk.cyan(entry.name)) : (isActive ? chalk.green(entry.name) : entry.name)}${isParentChoice(entry) && showChildren ? `${chalk.dim(` (${entry.children.map((child) => child.name).join(", ")})`)}` : ""}` |
||||||
|
})) |
||||||
|
|
||||||
|
if (!isFilterBreadcrumb(this.location) && isPopulatedArray(this.unlistedValues)) { |
||||||
|
outputs.push(" " + new Separator().line) |
||||||
|
outputs.push(...this.unlistedValues.map((value, index): string => { |
||||||
|
const isSelected = index === this.activeUnlistedIndex() |
||||||
|
const isActive = this.value.includes(value) |
||||||
|
const entry = this.valueMap[value] |
||||||
|
return `${isSelected ? chalk.cyan(figures.pointer) : " "} ${isActive ? chalk.greenBright(figures.checkboxCircleOn) : figures.checkboxCircleOff} ${isSelected ? (isActive ? chalk.blueBright(entry.name) : chalk.cyan(entry.name)) : (isActive ? chalk.green(entry.name) : entry.name)}` |
||||||
|
})) |
||||||
|
} |
||||||
|
|
||||||
|
outputs.push(" " + new Separator().line) |
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
return this.paginator.paginate(outputs.join("\n"), this.activeIndex, this.opt.pageSize) |
||||||
|
} |
||||||
|
|
||||||
|
private isUnlistedActive() { |
||||||
|
return !isFilterBreadcrumb(this.location) && this.activeUnlistedIndex() >= 0 |
||||||
|
} |
||||||
|
|
||||||
|
private activeUnlistedIndex() { |
||||||
|
if (isFilterBreadcrumb(this.location) || (this.activeIndex < this.location.selectable.length)) { |
||||||
|
return -1; |
||||||
|
} else { |
||||||
|
return this.activeIndex - this.location.selectable.length; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private activeChildIndex() { |
||||||
|
if (isFilterBreadcrumb(this.location)) { |
||||||
|
return this.activeIndex |
||||||
|
} else { |
||||||
|
if (this.isUnlistedActive()) { |
||||||
|
return -1 |
||||||
|
} else { |
||||||
|
return this.location.selectable[this.activeIndex]; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private onUpKey() { |
||||||
|
if (isFilterBreadcrumb(this.location)) { |
||||||
|
if (this.location.adjustingFilter) { |
||||||
|
this.location = { |
||||||
|
...this.location, |
||||||
|
adjustingFilter: false |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
if (this.activeIndex > 0) { |
||||||
|
this.changeIndex(-1) |
||||||
|
} else { |
||||||
|
this.activeIndex = this.totalLength |
||||||
|
this.changeIndex(-1) |
||||||
|
} |
||||||
|
this.scheduleRender() |
||||||
|
} |
||||||
|
|
||||||
|
private onDownKey() { |
||||||
|
if (isFilterBreadcrumb(this.location)) { |
||||||
|
if (this.location.adjustingFilter) { |
||||||
|
this.location = { |
||||||
|
...this.location, |
||||||
|
adjustingFilter: false |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
if (this.activeIndex < this.totalLength - 1) { |
||||||
|
this.changeIndex(+1) |
||||||
|
} else { |
||||||
|
this.activeIndex = -1 |
||||||
|
this.changeIndex(+1) |
||||||
|
} |
||||||
|
this.scheduleRender() |
||||||
|
} |
||||||
|
|
||||||
|
private onTab() { |
||||||
|
if (!isFilterBreadcrumb(this.location)) { |
||||||
|
this.readline.line = "" |
||||||
|
this.readline.cursor = 0 |
||||||
|
this.updateFilter() |
||||||
|
} else if (this.location.adjustingFilter) { |
||||||
|
this.location = { |
||||||
|
...this.location, |
||||||
|
adjustingFilter: false |
||||||
|
} |
||||||
|
this.readline.line = this.location.filter |
||||||
|
this.readline.cursor = this.location.filter.length |
||||||
|
} else { |
||||||
|
this.location = { |
||||||
|
...this.location, |
||||||
|
adjustingFilter: true |
||||||
|
} |
||||||
|
this.readline.line = this.location.filter |
||||||
|
this.readline.cursor = this.location.filter.length |
||||||
|
} |
||||||
|
|
||||||
|
this.scheduleRender() |
||||||
|
} |
||||||
|
|
||||||
|
private onBack() { |
||||||
|
if (isFilterBreadcrumb(this.location)) { |
||||||
|
if (!this.location.adjustingFilter && (this.location.filter !== this.readline.line || this.readline.cursor !== this.location.filter.length)) { |
||||||
|
this.location = { |
||||||
|
...this.location, |
||||||
|
adjustingFilter: true |
||||||
|
} |
||||||
|
} |
||||||
|
if (this.location.filter !== "") { |
||||||
|
this.updateFilter() |
||||||
|
this.scheduleRender() |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
const oldLocation = this.location |
||||||
|
if (isRootBreadcrumb(oldLocation)) { |
||||||
|
return |
||||||
|
} |
||||||
|
if (isFilterBreadcrumb(oldLocation)) { |
||||||
|
this.location = oldLocation.parent |
||||||
|
this.activeIndex = 0 |
||||||
|
} else if (isFilterBreadcrumb(oldLocation.parent)) { |
||||||
|
this.location = oldLocation.parent.parent |
||||||
|
this.activeIndex = 0 |
||||||
|
} else { |
||||||
|
this.location = oldLocation.parent |
||||||
|
this.activeIndex = oldLocation.index |
||||||
|
} |
||||||
|
this.updateUnlistedValues() |
||||||
|
this.updatePrompt() |
||||||
|
this.scheduleRender() |
||||||
|
} |
||||||
|
|
||||||
|
private onSpacebar() { |
||||||
|
if (isFilterBreadcrumb(this.location) && this.location.adjustingFilter) { |
||||||
|
this.updateFilter() |
||||||
|
this.scheduleRender() |
||||||
|
return |
||||||
|
} |
||||||
|
const currentChoice = this.activeChoice |
||||||
|
if (currentChoice === null) { |
||||||
|
return |
||||||
|
} else if (isLeafChoice(currentChoice)) { |
||||||
|
const index = this.value.indexOf(currentChoice.value) |
||||||
|
if (index === -1) { |
||||||
|
this.value.push(currentChoice.value) |
||||||
|
} else { |
||||||
|
this.value.splice(index, 1) |
||||||
|
} |
||||||
|
this.scheduleRender() |
||||||
|
} else if (isParentChoice(currentChoice)) { |
||||||
|
this.location = { |
||||||
|
parent: this.location, |
||||||
|
index: this.activeIndex, |
||||||
|
children: currentChoice.children, |
||||||
|
selectable: getSelectableList(currentChoice.children), |
||||||
|
searchable: getSearchableList(currentChoice.children), |
||||||
|
} |
||||||
|
this.activeIndex = 0 |
||||||
|
this.updateUnlistedValues() |
||||||
|
this.scheduleRender() |
||||||
|
} else { |
||||||
|
// disabled choice, do nothing
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private updateUnlistedValues() { |
||||||
|
if (isFilterBreadcrumb(this.location)) { |
||||||
|
this.unlistedValues = [] |
||||||
|
} else { |
||||||
|
this.unlistedValues = this.value.filter((value) => !this.location.children.some((choice) => choice.value === value)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private updateFilter() { |
||||||
|
if (!isFilterBreadcrumb(this.location)) { |
||||||
|
this.location = { |
||||||
|
parent: this.location, |
||||||
|
index: null, |
||||||
|
filter: "", |
||||||
|
adjustingFilter: true, |
||||||
|
children: this.location.searchable, |
||||||
|
searchable: this.location.searchable, |
||||||
|
} |
||||||
|
this.activeIndex = 0 |
||||||
|
this.updatePrompt() |
||||||
|
} |
||||||
|
if (this.location.adjustingFilter && this.readline.line !== this.location.filter) { |
||||||
|
this.location = { |
||||||
|
...this.location, |
||||||
|
filter: this.readline.line, |
||||||
|
children: this.readline.line === "" ? this.location.searchable : fuzzyFilter(this.readline.line, this.location.searchable.slice(), { |
||||||
|
pre: searchHighlightPrefix, |
||||||
|
post: searchHighlightSuffix, |
||||||
|
extract(input: any): string { |
||||||
|
return input.name |
||||||
|
} |
||||||
|
} as FilterOptions<NormalizedParentChoice | NormalizedLeafChoice>).map((result) => ({ |
||||||
|
...result.original, |
||||||
|
name: result.string, |
||||||
|
})) |
||||||
|
} |
||||||
|
this.activeIndex = 0 |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private onLine() { |
||||||
|
if (isFilterBreadcrumb(this.location)) { |
||||||
|
if (!isPopulatedArray(this.location.children)) { |
||||||
|
return |
||||||
|
} |
||||||
|
if (this.location.adjustingFilter) { |
||||||
|
this.location = { |
||||||
|
...this.location, |
||||||
|
adjustingFilter: false, |
||||||
|
} |
||||||
|
} |
||||||
|
// Activate the selected item.
|
||||||
|
this.onSpacebar() |
||||||
|
const newLocation = this.location |
||||||
|
if (isFilterBreadcrumb(newLocation)) { |
||||||
|
this.location = this.location.parent |
||||||
|
} |
||||||
|
this.updateUnlistedValues() |
||||||
|
this.updatePrompt() |
||||||
|
this.scheduleRender() |
||||||
|
} else { |
||||||
|
this.submitter.next(this.value.slice()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private onClear() { |
||||||
|
if (isFilterBreadcrumb(this.location)) { |
||||||
|
this.location = this.location.parent |
||||||
|
this.activeIndex = 0 |
||||||
|
this.updateUnlistedValues() |
||||||
|
this.updatePrompt() |
||||||
|
this.scheduleRender() |
||||||
|
} |
||||||
|
this.value.splice(0, this.value.length) |
||||||
|
this.scheduleRender() |
||||||
|
} |
||||||
|
|
||||||
|
private scheduleRender() { |
||||||
|
if (this.scheduledRender === null) { |
||||||
|
this.scheduledRender = setImmediate(() => this.render()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private changeIndex(by: number) { |
||||||
|
this.activeIndex += by |
||||||
|
if (this.activeIndex < 0) { |
||||||
|
this.activeIndex = 0 |
||||||
|
} else if (this.activeIndex >= this.totalLength) { |
||||||
|
this.activeIndex = this.totalLength - 1 |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private get totalLength() { |
||||||
|
if (isFilterBreadcrumb(this.location)) { |
||||||
|
return this.location.children.length; |
||||||
|
} else { |
||||||
|
return this.location.selectable.length + this.unlistedValues.length; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private onValidationError(isValid: false|string) { |
||||||
|
this.render(typeof isValid === "string" ? chalk.red(isValid) : chalk.dim.red("Invalid input")) |
||||||
|
} |
||||||
|
|
||||||
|
private onValidated(value: string[]) { |
||||||
|
this.value.splice(0, this.value.length, ...value) |
||||||
|
this.status = "answered" |
||||||
|
this.render() |
||||||
|
this.screen.done() |
||||||
|
this.done(value) |
||||||
|
} |
||||||
|
|
||||||
|
private onOtherKey() { |
||||||
|
if (isFilterBreadcrumb(this.location)) { |
||||||
|
if (!this.location.adjustingFilter && (this.location.filter !== this.readline.line || this.readline.cursor !== this.location.filter.length)) { |
||||||
|
this.location = { |
||||||
|
...this.location, |
||||||
|
adjustingFilter: true |
||||||
|
} |
||||||
|
} |
||||||
|
this.updateFilter() |
||||||
|
} else if (this.readline.line !== "") { |
||||||
|
this.updateFilter() |
||||||
|
} |
||||||
|
this.scheduleRender() |
||||||
|
} |
||||||
|
|
||||||
|
private updatePrompt() { |
||||||
|
if (isFilterBreadcrumb(this.location)) { |
||||||
|
this.readline.setPrompt("Searching: ") |
||||||
|
} else { |
||||||
|
this.readline.setPrompt("") |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,218 @@ |
|||||||
|
import Prompt from "inquirer/lib/prompts/base"; |
||||||
|
import observe from "inquirer/lib/utils/events"; |
||||||
|
import {Answers, MultiTextInputQuestion, Question} from "inquirer"; |
||||||
|
import {Interface as ReadLineInterface} from "readline"; |
||||||
|
import {filter} from "rxjs/operators" |
||||||
|
import chalk from "chalk"; |
||||||
|
import {Subject} from "rxjs"; |
||||||
|
import figures from "figures"; |
||||||
|
import Paginator from "inquirer/lib/utils/paginator"; |
||||||
|
|
||||||
|
interface ExtendedReadLine extends ReadLineInterface { |
||||||
|
line: string |
||||||
|
cursor: number |
||||||
|
} |
||||||
|
|
||||||
|
declare module "inquirer" { |
||||||
|
export interface MultiTextInputQuestionOptions { |
||||||
|
pageSize?: number |
||||||
|
} |
||||||
|
|
||||||
|
export interface MultiTextInputQuestion<AnswerT extends Answers = Answers> extends Question<AnswerT>, MultiTextInputQuestionOptions { |
||||||
|
/** @inheritDoc */ |
||||||
|
type: "multitext" |
||||||
|
default?: string[] |
||||||
|
} |
||||||
|
|
||||||
|
export interface QuestionMap { |
||||||
|
["multitext"]: HierarchicalCheckboxQuestion |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export class MultiTextInput<AnswerT extends Answers = Answers, QuestionT extends MultiTextInputQuestion<AnswerT> = MultiTextInputQuestion> extends Prompt<QuestionT> { |
||||||
|
|
||||||
|
constructor(question: QuestionT, readline: ExtendedReadLine, answers: AnswerT) { |
||||||
|
super(question, readline, answers); |
||||||
|
this.value = question.default?.slice() || [] |
||||||
|
this.activeIndex = this.value.length |
||||||
|
} |
||||||
|
|
||||||
|
_run(callback: MultiTextInput["done"]) { |
||||||
|
this.status = "touched" |
||||||
|
this.done = callback |
||||||
|
const events = observe(this.rl); |
||||||
|
this.rl.on("history", (history) => { |
||||||
|
history.splice(0, history.length) |
||||||
|
}) |
||||||
|
events.normalizedUpKey.subscribe(() => this.onUpKey()) |
||||||
|
events.normalizedDownKey.subscribe(() => this.onDownKey()) |
||||||
|
events.line.subscribe((line) => this.onLine(line)) |
||||||
|
events.keypress.pipe(filter((event) => { |
||||||
|
return event.key.name === "r" && event.key.ctrl === true |
||||||
|
})).subscribe(() => this.onReset()) |
||||||
|
events.keypress.pipe(filter((event) => { |
||||||
|
return event.key.name === "x" && event.key.ctrl === true |
||||||
|
})).subscribe(() => this.onClear()) |
||||||
|
events.keypress.subscribe(() => this.scheduleRender()) |
||||||
|
const onSubmit = this.handleSubmitEvents(this.submitter) |
||||||
|
onSubmit.success.subscribe((result) => this.onValidated(result.value)) |
||||||
|
onSubmit.error.subscribe((result) => this.onValidationError(result.isValid)) |
||||||
|
this.scheduleRender() |
||||||
|
} |
||||||
|
|
||||||
|
private done: (state: any) => void|null |
||||||
|
private activeIndex: number |
||||||
|
private readonly value: string[] |
||||||
|
private submitter: Subject<string[]> = new Subject<string[]>() |
||||||
|
// @ts-ignore
|
||||||
|
private paginator: Paginator = new Paginator(this.screen, {isInfinite: false}); |
||||||
|
private scheduledRender: NodeJS.Immediate|null = null |
||||||
|
|
||||||
|
private get readline(): ExtendedReadLine { |
||||||
|
return this.rl |
||||||
|
} |
||||||
|
|
||||||
|
private get isNew(): boolean { |
||||||
|
return this.activeIndex === this.value.length |
||||||
|
} |
||||||
|
|
||||||
|
private get activeValue(): string { |
||||||
|
return this.isNew ? "" : this.value[this.activeIndex] |
||||||
|
} |
||||||
|
|
||||||
|
private set activeValue(newValue: string) { |
||||||
|
if (this.isNew) { |
||||||
|
this.value.push(newValue) |
||||||
|
} else { |
||||||
|
this.value[this.activeIndex] = newValue |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private render(error?: string) { |
||||||
|
this.scheduledRender = null |
||||||
|
const outputs: string[] = [this.getQuestion()] |
||||||
|
const bottomOutputs: string[] = [] |
||||||
|
if (this.status === "answered") { |
||||||
|
outputs.push(...this.value.map((s) => chalk.cyan(`${s}`)).join(", ")) |
||||||
|
} else { |
||||||
|
if (typeof error === "string") { |
||||||
|
bottomOutputs.push(`${figures.warning} ${error}`) |
||||||
|
} else if (this.readline.line === "") { |
||||||
|
if (this.isNew) { |
||||||
|
bottomOutputs.push(chalk.dim(`Begin typing to add a new entry. Press ${chalk.yellow("<enter>")} to submit these entries, ${chalk.yellow("<ctrl>+x")} to delete all entries, or ${chalk.yellow("<up>")}/${chalk.yellow("<down>")} to change existing entries.`)) |
||||||
|
} else { |
||||||
|
bottomOutputs.push(chalk.dim(`Press ${chalk.yellow("<enter>")} or ${chalk.yellow("<ctrl>+x")} to delete this entry or ${chalk.yellow("<ctrl>+r")} to revert.`)) |
||||||
|
} |
||||||
|
} else { |
||||||
|
outputs.push(this.readline.line) |
||||||
|
if (this.isNew) { |
||||||
|
bottomOutputs.push(chalk.dim(`Press ${chalk.yellow("<enter>")} to add this entry or ${chalk.yellow("<ctrl>+r")} or ${chalk.yellow("<ctrl>+x")} to clear it.`)) |
||||||
|
} else if (this.activeValue !== this.readline.line) { |
||||||
|
bottomOutputs.push(chalk.dim(`Press ${chalk.yellow("<enter>")} to save this entry, ${chalk.yellow("<ctrl>+r")} to revert it, or ${chalk.yellow("<ctrl>+x")} to delete it.`)) |
||||||
|
} else { |
||||||
|
bottomOutputs.push(chalk.dim(`Begin typing to change this entry or press ${chalk.yellow("<ctrl>+x")} to delete it. Press ${chalk.yellow("<up>")}/${chalk.yellow("<down>")} to change other entries.`)) |
||||||
|
} |
||||||
|
} |
||||||
|
bottomOutputs.push(this.renderList()) |
||||||
|
} |
||||||
|
this.screen.render(outputs.join(""), bottomOutputs.join("\n")) |
||||||
|
} |
||||||
|
|
||||||
|
private renderList(): string { |
||||||
|
const outputs: string[] = [] |
||||||
|
|
||||||
|
outputs.push(...this.value.map((entry, index):string => { |
||||||
|
const isSelected = index === this.activeIndex |
||||||
|
return `${isSelected ? chalk.cyan(figures.pointer) : " "} ${isSelected ? chalk.cyan(entry) : entry}` |
||||||
|
})) |
||||||
|
|
||||||
|
outputs.push(`${this.isNew ? chalk.cyan(figures.pointer) : " "} ${this.isNew ? chalk.cyan.dim("(New Entry)") : chalk.dim("(New Entry)")}`) |
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
return this.paginator.paginate(outputs.join("\n"), this.activeIndex, this.opt.pageSize) |
||||||
|
} |
||||||
|
|
||||||
|
private onUpKey() { |
||||||
|
if (this.readline.line !== this.activeValue) { |
||||||
|
return |
||||||
|
} |
||||||
|
if (this.activeIndex > 0) { |
||||||
|
this.changeIndex(-1) |
||||||
|
this.scheduleRender() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private onDownKey() { |
||||||
|
if (this.readline.line !== this.activeValue) { |
||||||
|
return |
||||||
|
} |
||||||
|
if (this.activeIndex < this.value.length) { |
||||||
|
this.changeIndex(+1) |
||||||
|
this.scheduleRender() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private onLine(line: string) { |
||||||
|
if (line === "") { |
||||||
|
if (this.isNew) { |
||||||
|
this.submitter.next(this.value.slice()) |
||||||
|
} else { |
||||||
|
this.value.splice(this.activeIndex, 1) |
||||||
|
this.changeIndex(0) |
||||||
|
} |
||||||
|
} else { |
||||||
|
this.activeValue = line |
||||||
|
this.changeIndex(+1) |
||||||
|
} |
||||||
|
this.scheduleRender() |
||||||
|
} |
||||||
|
|
||||||
|
private onReset() { |
||||||
|
this.changeIndex(0) |
||||||
|
this.scheduleRender() |
||||||
|
} |
||||||
|
|
||||||
|
private onClear() { |
||||||
|
if (!this.isNew) { |
||||||
|
this.value.splice(this.activeIndex, 1) |
||||||
|
this.changeIndex(0) |
||||||
|
this.scheduleRender() |
||||||
|
} else if (this.readline.line !== "") { |
||||||
|
this.readline.line = "" |
||||||
|
} else { |
||||||
|
this.value.splice(0, this.value.length) |
||||||
|
this.activeIndex = 0 |
||||||
|
this.changeIndex(0) |
||||||
|
} |
||||||
|
this.scheduleRender() |
||||||
|
} |
||||||
|
|
||||||
|
private scheduleRender() { |
||||||
|
if (this.scheduledRender === null) { |
||||||
|
this.scheduledRender = setImmediate(() => this.render()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private changeIndex(by: number) { |
||||||
|
this.activeIndex += by |
||||||
|
if (this.activeIndex < 0) { |
||||||
|
this.activeIndex = 0 |
||||||
|
} else if (this.activeIndex > this.value.length) { |
||||||
|
this.activeIndex = this.value.length |
||||||
|
} |
||||||
|
this.readline.line = this.activeValue |
||||||
|
this.readline.cursor = this.activeValue.length |
||||||
|
} |
||||||
|
|
||||||
|
private onValidationError(isValid: false|string) { |
||||||
|
this.render(typeof isValid === "string" ? chalk.red(isValid) : chalk.dim.red("Invalid input")) |
||||||
|
} |
||||||
|
|
||||||
|
private onValidated(value: string[]) { |
||||||
|
this.value.splice(0, this.value.length, ...value) |
||||||
|
this.status = "answered" |
||||||
|
this.render() |
||||||
|
this.screen.done() |
||||||
|
this.done(value) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,8 @@ |
|||||||
|
import {registerPrompt} from "inquirer"; |
||||||
|
import {MultiTextInput} from "./MultiTextInput"; |
||||||
|
import {HierarchicalCheckboxInput} from "./HierarchicalCheckboxInput"; |
||||||
|
|
||||||
|
export function registerPrompts() { |
||||||
|
registerPrompt("multitext", MultiTextInput) |
||||||
|
registerPrompt("hierarchical-checkbox", HierarchicalCheckboxInput) |
||||||
|
} |
@ -0,0 +1,205 @@ |
|||||||
|
import {EmpathyGuide, EmpathyGuideJTD} from "../datatypes/EmpathyGuide"; |
||||||
|
import {Entry} from "../datatypes/Entry"; |
||||||
|
import { |
||||||
|
ReferencedSchema, |
||||||
|
ReferencedTypes, |
||||||
|
schema, |
||||||
|
SchemaData, |
||||||
|
UntypedReferenceList |
||||||
|
} from "../schemata/SchemaData"; |
||||||
|
import {rm, open, FileHandle} from "fs/promises"; |
||||||
|
import {join, dirname} from "path"; |
||||||
|
import {dump, load} from "js-yaml"; |
||||||
|
import envPaths from "env-paths"; |
||||||
|
import makeDir from "make-dir"; |
||||||
|
import {Journal, JournalJTD} from "../datatypes/Journal"; |
||||||
|
|
||||||
|
export type AutosaveFile = Record<string, unknown> |
||||||
|
export type AutosaveFileJTD = typeof AutosaveFileJTD |
||||||
|
export const AutosaveFileJTD = schema({ |
||||||
|
schema: { |
||||||
|
values: {} |
||||||
|
}, |
||||||
|
typeHint: null as AutosaveFile|null, |
||||||
|
key: "autosaveFile", |
||||||
|
references: [] |
||||||
|
}) |
||||||
|
|
||||||
|
export class LocalRepository { |
||||||
|
private readonly paths: envPaths.Paths |
||||||
|
|
||||||
|
constructor() { |
||||||
|
this.paths = envPaths("guided-journal") |
||||||
|
} |
||||||
|
|
||||||
|
private getJournalFilePath(): string { |
||||||
|
return join(this.paths.data, "journal.yaml") |
||||||
|
} |
||||||
|
|
||||||
|
private getEmpathyGuideFilePath(): string { |
||||||
|
return join(this.paths.config, "empathyGuide.yaml") |
||||||
|
} |
||||||
|
|
||||||
|
private getAutosaveFilePath(autosaveNamespace: string): string { |
||||||
|
return join(this.paths.data, "autosave", `${autosaveNamespace}.yaml`) |
||||||
|
} |
||||||
|
|
||||||
|
async loadEmpathyGuide(): Promise<EmpathyGuide> { |
||||||
|
const path = this.getEmpathyGuideFilePath() |
||||||
|
let handle: FileHandle |
||||||
|
try { |
||||||
|
handle = await open(path, "a+") |
||||||
|
} catch (e) { |
||||||
|
if (typeof e === "object" && e.hasOwnProperty("code") && typeof e.code === "string" && e.code === "ENOENT") { |
||||||
|
return {} // no empathy guide exists, so return a blank one
|
||||||
|
} |
||||||
|
throw e |
||||||
|
} |
||||||
|
try { |
||||||
|
const fileYaml = await handle.readFile({encoding: "utf-8"}) |
||||||
|
const saved = load(fileYaml) |
||||||
|
if (!EmpathyGuideJTD.validate(saved)) { |
||||||
|
throw Error(`Empathy guide file was corrupted`) |
||||||
|
} |
||||||
|
return saved |
||||||
|
} finally { |
||||||
|
await handle.close() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async saveEmpathyGuide(empathyGuide: EmpathyGuide): Promise<void> { |
||||||
|
const path = this.getEmpathyGuideFilePath() |
||||||
|
await makeDir(dirname(path)) |
||||||
|
const handle = await open(path, "w") |
||||||
|
try { |
||||||
|
await handle.writeFile(dump(empathyGuide), {encoding: "utf-8"}) |
||||||
|
} finally { |
||||||
|
await handle.close() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async getEntries(): Promise<Journal> { |
||||||
|
const path = this.getJournalFilePath() |
||||||
|
let handle: FileHandle |
||||||
|
try { |
||||||
|
handle = await open(path, "r") |
||||||
|
} catch (e) { |
||||||
|
if (typeof e === "object" && e.hasOwnProperty("code") && typeof e.code === "string" && e.code === "ENOENT") { |
||||||
|
// file does not exist :(
|
||||||
|
return {entries: []} // no entries saved
|
||||||
|
} |
||||||
|
throw e |
||||||
|
} |
||||||
|
try { |
||||||
|
const fileYaml = await handle.readFile({encoding: "utf-8"}) |
||||||
|
const saved = load(fileYaml) |
||||||
|
if (!JournalJTD.validate(saved)) { |
||||||
|
throw Error(`Journal file was corrupted`) |
||||||
|
} |
||||||
|
return saved |
||||||
|
} finally { |
||||||
|
await handle.close() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async saveEntry(entry: Entry): Promise<void> { |
||||||
|
const path = this.getJournalFilePath() |
||||||
|
await makeDir(dirname(path)) |
||||||
|
const handle = await open(path, "a+") |
||||||
|
try { |
||||||
|
const fileYaml = await handle.readFile({encoding: "utf-8"}) |
||||||
|
const original = fileYaml !== "" ? load(fileYaml) : {} |
||||||
|
if (!JournalJTD.validate(original)) { |
||||||
|
throw Error(`Journal file was corrupted`) |
||||||
|
} |
||||||
|
const appended = {entries: [...original.entries || [], entry]} |
||||||
|
await handle.truncate(0) |
||||||
|
await handle.writeFile(dump(appended), {encoding: "utf-8"}) |
||||||
|
} finally { |
||||||
|
await handle.close() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async loadAutosaveObjects<TypesT extends UntypedReferenceList>( |
||||||
|
{autosaveNamespace, schemas}: { autosaveNamespace: string, schemas: TypesT }): Promise<{ |
||||||
|
validated: Partial<ReferencedTypes<TypesT>>, |
||||||
|
unvalidated: Partial<Record<keyof ReferencedTypes<TypesT>, unknown>> |
||||||
|
}> { |
||||||
|
const path = this.getAutosaveFilePath(autosaveNamespace) |
||||||
|
let handle: FileHandle |
||||||
|
try { |
||||||
|
handle = await open(path, "r") |
||||||
|
} catch (e) { |
||||||
|
if (typeof e === "object" && e.hasOwnProperty("code") && typeof e.code === "string" && e.code === "ENOENT") { |
||||||
|
// file does not exist :(
|
||||||
|
return {validated: {}, unvalidated: {}} // nothing was autosaved
|
||||||
|
} |
||||||
|
throw e |
||||||
|
} |
||||||
|
try { |
||||||
|
const fileYaml = await handle.readFile({encoding: "utf-8"}) |
||||||
|
const autosaved = load(fileYaml) |
||||||
|
if (!AutosaveFileJTD.validate(autosaved)) { |
||||||
|
throw Error(`Autosave file ${autosaveNamespace} was too corrupted to make sense of`) |
||||||
|
} |
||||||
|
const validated: Partial<ReferencedTypes<TypesT>> = {} |
||||||
|
const unvalidated: Partial<Record<keyof ReferencedTypes<TypesT>, unknown>> = {} |
||||||
|
schemas.forEach((schema: ReferencedSchema<TypesT>) => { |
||||||
|
if (autosaved.hasOwnProperty(schema.key)) { |
||||||
|
const value = autosaved[schema.key] |
||||||
|
if (!schema.validate(value)) { |
||||||
|
unvalidated[schema.key as keyof ReferencedTypes<TypesT>] = value |
||||||
|
} else { |
||||||
|
validated[schema.key as keyof ReferencedTypes<TypesT>] = value |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
return {validated, unvalidated} |
||||||
|
} finally { |
||||||
|
await handle.close() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async saveAutosaveObject<T>({schema, value, autosaveNamespace}: {autosaveNamespace: string, schema: SchemaData<T, any, any, any>, value: T}): Promise<void> { |
||||||
|
const path = this.getAutosaveFilePath(autosaveNamespace) |
||||||
|
await makeDir(dirname(path)) |
||||||
|
const handle = await open(path, "a+") |
||||||
|
try { |
||||||
|
const fileYaml = await handle.readFile({encoding: "utf-8"}) |
||||||
|
const original = load(fileYaml) |
||||||
|
if (!AutosaveFileJTD.validate(original)) { |
||||||
|
throw Error(`Autosave file ${autosaveNamespace} was corrupted`) |
||||||
|
} |
||||||
|
original[schema.key] = value |
||||||
|
await handle.truncate(0) |
||||||
|
await handle.writeFile(dump(original), {encoding: "utf-8"}) |
||||||
|
} finally { |
||||||
|
await handle.close() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async clearAutosaveObject({schema, autosaveNamespace}: {autosaveNamespace: string, schema: SchemaData<any, any, any, any>}): Promise<void> { |
||||||
|
const path = this.getAutosaveFilePath(autosaveNamespace) |
||||||
|
await makeDir(dirname(path)) |
||||||
|
const handle = await open(path, "a+") |
||||||
|
try { |
||||||
|
const fileYaml = await handle.readFile({encoding: "utf-8"}) |
||||||
|
const original = load(fileYaml) |
||||||
|
if (!AutosaveFileJTD.validate(original)) { |
||||||
|
throw Error(`Autosave file ${autosaveNamespace} was corrupted`) |
||||||
|
} |
||||||
|
delete original[schema.key] |
||||||
|
await handle.truncate(0) |
||||||
|
await handle.writeFile(dump(original), {encoding: "utf-8"}) |
||||||
|
} finally { |
||||||
|
await handle.close() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async clearAutosaveObjects(autosaveNamespace: string): Promise<void> { |
||||||
|
const path = this.getAutosaveFilePath(autosaveNamespace) |
||||||
|
await rm(path, { |
||||||
|
force: true |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,54 @@ |
|||||||
|
|
||||||
|
import AJV, {ValidateFunction, JTDSchemaType} from "ajv/dist/jtd"; |
||||||
|
|
||||||
|
const ajv = new AJV(); |
||||||
|
|
||||||
|
export type SchemaData<RepresentedT, ReferenceT extends string, ReferencesT extends UntypedReferenceList, DefinitionsT extends ReferencedTypes<ReferencesT>> = { |
||||||
|
value?: RepresentedT, |
||||||
|
schema: JTDSchemaType<RepresentedT, DefinitionsT>, |
||||||
|
key: ReferenceT, |
||||||
|
definition: { [key in ReferenceT]: JTDSchemaType<RepresentedT, DefinitionsT> } |
||||||
|
referenced?: { [key in ReferenceT]: RepresentedT } |
||||||
|
reference: { "ref": ReferenceT }, |
||||||
|
validate: ValidateFunction<RepresentedT> |
||||||
|
requiredReferences: ReferencesT |
||||||
|
} |
||||||
|
|
||||||
|
export type UntypedReferenceList = SchemaData<any, any, any, any>[] |
||||||
|
|
||||||
|
export type Schema<DataT extends SchemaData<any, any, any, any>> = Exclude<DataT["schema"], undefined> |
||||||
|
export type Value<DataT extends SchemaData<any, any, any, any>> = Exclude<DataT["value"], undefined> |
||||||
|
export type Definition<DataT extends SchemaData<any, any, any, any>> = Exclude<DataT["definition"], undefined> |
||||||
|
export type Reference<DataT extends SchemaData<any, any, any, any>> = Exclude<DataT["reference"], undefined> |
||||||
|
export type ReferenceKey<DataT extends SchemaData<any, any, any, any>> = Exclude<DataT["key"], undefined> |
||||||
|
export type Referenced<DataT extends SchemaData<any, any, any, any>> = Exclude<DataT["referenced"], undefined> |
||||||
|
|
||||||
|
export type ReferencedSchemaMap<ReferencesT extends UntypedReferenceList> = { |
||||||
|
[Property in keyof ReferencesT as ReferencesT[Property] extends SchemaData<any, any, any, any> ? ReferenceKey<ReferencesT[Property]> : never]: ReferencesT[Property] extends SchemaData<any, any, any, any> ? ReferencesT[Property] : never |
||||||
|
} |
||||||
|
export type ReferencedSchema<ReferencesT extends UntypedReferenceList> = ReferencedSchemaMap<ReferencesT>[keyof ReferencedSchemaMap<ReferencesT>] |
||||||
|
export type ReferencedDefinitions<ReferencesT extends UntypedReferenceList> = { |
||||||
|
[Property in keyof ReferencedSchemaMap<ReferencesT>]?: Definition<ReferencedSchemaMap<ReferencesT>[Property]> |
||||||
|
} |
||||||
|
export type ReferencedTypes<ReferencesT extends UntypedReferenceList> = { |
||||||
|
[Property in keyof ReferencedSchemaMap<ReferencesT>]?: Value<ReferencedSchemaMap<ReferencesT>[Property]> |
||||||
|
} |
||||||
|
|
||||||
|
export function schema< |
||||||
|
RepresentedT, |
||||||
|
KeyT extends string, |
||||||
|
ReferencesT extends SchemaData<any, any, any, ReferencedTypes<ReferencesT>>[], |
||||||
|
>({schema, key, references}: { schema: JTDSchemaType<RepresentedT, ReferencedTypes<ReferencesT>>, key: KeyT, references: ReferencesT, typeHint?: RepresentedT|null }): SchemaData<RepresentedT, KeyT, ReferencesT, ReferencedTypes<ReferencesT>> { |
||||||
|
const definition = {[key]: schema} as Definition<SchemaData<RepresentedT, KeyT, ReferencesT, ReferencedTypes<ReferencesT>>> |
||||||
|
const reference = {ref: key} |
||||||
|
const definitions = references.reduce((definitions, reference) => ({...reference.definition, ...definitions}), {} as ReferencedDefinitions<ReferencesT>) |
||||||
|
const validate = ajv.compile<RepresentedT>({ ...schema, definitions }) |
||||||
|
return { |
||||||
|
schema, |
||||||
|
key, |
||||||
|
definition, |
||||||
|
reference, |
||||||
|
validate, |
||||||
|
requiredReferences: references |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,51 @@ |
|||||||
|
import {SchemaData} from "./SchemaData"; |
||||||
|
import {dump, load} from "js-yaml"; |
||||||
|
import {EditorQuestionOptions} from "inquirer"; |
||||||
|
import {InquireFunction, ShowFunction} from "../prompts/Inquire"; |
||||||
|
|
||||||
|
const RETRY = "Retry" |
||||||
|
const ABORT = "Abort" |
||||||
|
|
||||||
|
export async function editYaml<ObjectT>({schema, currentValue, name, inquire, showError}: {schema: SchemaData<ObjectT, any, any, any>, currentValue: ObjectT, name: string, inquire: InquireFunction, showError: ShowFunction}): Promise<ObjectT> |
||||||
|
export async function editYaml<ObjectT>({schema, currentValue, name, inquire, showError}: {schema: SchemaData<ObjectT, any, any, any>, currentValue: ObjectT|undefined, name: string, inquire: InquireFunction, showError: ShowFunction}): Promise<ObjectT|undefined> |
||||||
|
export async function editYaml<ObjectT>({schema, currentValue, name, inquire, showError}: {schema: SchemaData<ObjectT, any, any, any>, currentValue: ObjectT|undefined, name: string, inquire: InquireFunction, showError: ShowFunction}): Promise<ObjectT|undefined> { |
||||||
|
const original = dump(currentValue) |
||||||
|
let text = original |
||||||
|
while (true) { |
||||||
|
const modified = await inquire({ |
||||||
|
type: "editor", |
||||||
|
message: `View and edit ${name}:`, |
||||||
|
default: text, |
||||||
|
} as EditorQuestionOptions) |
||||||
|
if (original !== modified) { |
||||||
|
try { |
||||||
|
const result = load(modified) |
||||||
|
if (schema.validate(result)) { |
||||||
|
return result |
||||||
|
} else { |
||||||
|
await showError(schema.validate.errors?.map((e) => e.toString()).join("; ") || "Failed validation") |
||||||
|
} |
||||||
|
} catch (e) { |
||||||
|
await showError(e.toString()) |
||||||
|
} |
||||||
|
const option = await inquire({ |
||||||
|
type: "expand", |
||||||
|
message: "Choose how to proceed:", |
||||||
|
choices: [ |
||||||
|
{key: "r", name: "Retry edits with current text", value: RETRY}, |
||||||
|
{key: "a", name: "Abort editing and keep original state", value: ABORT} |
||||||
|
], |
||||||
|
}) |
||||||
|
switch (option) { |
||||||
|
default: |
||||||
|
case RETRY: |
||||||
|
text = modified |
||||||
|
break |
||||||
|
case ABORT: |
||||||
|
return currentValue |
||||||
|
} |
||||||
|
} else { |
||||||
|
return currentValue |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,3 @@ |
|||||||
|
export function isPopulatedArray<T>(array: readonly T[]|null|undefined): array is [T, ...T[]] { |
||||||
|
return array !== null && array !== undefined && array.length > 0 |
||||||
|
} |
@ -0,0 +1,13 @@ |
|||||||
|
export function merge<Update extends {readonly [key: string]: any}, Data extends Update>(update: Update, base: Data): Data { |
||||||
|
const data = { |
||||||
|
...update, |
||||||
|
...base, |
||||||
|
} |
||||||
|
// Purge keys that were intentionally removed...
|
||||||
|
Object.keys(update).forEach((key: keyof typeof update) => { |
||||||
|
if (typeof update[key] === "undefined") { |
||||||
|
delete data[key] |
||||||
|
} |
||||||
|
}) |
||||||
|
return data |
||||||
|
} |
@ -0,0 +1,7 @@ |
|||||||
|
export function asDefault<InputT>(input: InputT): { default: InputT } { |
||||||
|
return {default: input} |
||||||
|
} |
||||||
|
|
||||||
|
export function identity<InputT>(input: InputT): InputT { |
||||||
|
return input |
||||||
|
} |
@ -0,0 +1,4 @@ |
|||||||
|
declare module "wordcount" { |
||||||
|
function wordcount(input: string): number |
||||||
|
export = wordcount |
||||||
|
} |
Loading…
Reference in new issue