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) { |
||||
return `${firstName} ${lastName}`; |
||||
} |
||||
import {addEntryCommand} from "./commands/AddEntry"; |
||||
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