parent
548f6766e4
commit
b1f03c0cf4
@ -0,0 +1,5 @@ |
|||||||
|
{ |
||||||
|
"flags": { |
||||||
|
"gulpfile": "gulpfile.cjs" |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,96 @@ |
|||||||
|
import {schema} from "../schemata/SchemaData.js"; |
||||||
|
|
||||||
|
export enum SleepQuality { |
||||||
|
ACIDIC = "Acidic", |
||||||
|
RESTLESS = "Restless", |
||||||
|
NIGHTMARISH = "Nightmarish", |
||||||
|
TIMESKIP = "Timeskip", |
||||||
|
DREAMLESS = "Dreamless", |
||||||
|
DREAMY = "Dreamy", |
||||||
|
ECSTASY = "Ecstasy", |
||||||
|
} |
||||||
|
export const SLEEP_QUALITIES: SleepQuality[] = [SleepQuality.ACIDIC, SleepQuality.RESTLESS, SleepQuality.NIGHTMARISH, SleepQuality.TIMESKIP, SleepQuality.DREAMLESS, SleepQuality.DREAMY, SleepQuality.ECSTASY] |
||||||
|
|
||||||
|
export type SleepQualityJTD = typeof SleepQualityJTD |
||||||
|
export const SleepQualityJTD = schema({ |
||||||
|
schema: { |
||||||
|
enum: SLEEP_QUALITIES |
||||||
|
}, |
||||||
|
key: "sleepQuality", |
||||||
|
references: [], |
||||||
|
}) |
||||||
|
|
||||||
|
export enum WakeQuality { |
||||||
|
AGONIZED = "Agonized", |
||||||
|
EXHAUSTED = "Exhausted", |
||||||
|
DROWSY = "Drowsy", |
||||||
|
RESTED = "Rested", |
||||||
|
ENERGIZED = "Energized", |
||||||
|
} |
||||||
|
export const WAKE_QUALITIES: WakeQuality[] = [WakeQuality.AGONIZED, WakeQuality.EXHAUSTED, WakeQuality.DROWSY, WakeQuality.RESTED, WakeQuality.ENERGIZED] |
||||||
|
|
||||||
|
export type WakeQualityJTD = typeof WakeQualityJTD |
||||||
|
export const WakeQualityJTD = schema({ |
||||||
|
schema: { |
||||||
|
enum: WAKE_QUALITIES |
||||||
|
}, |
||||||
|
key: "wakeQuality", |
||||||
|
references: [], |
||||||
|
}) |
||||||
|
|
||||||
|
export interface SleepRecord { |
||||||
|
readonly sleepAt?: Date |
||||||
|
readonly sleepQuality?: SleepQuality |
||||||
|
readonly interruptions?: number |
||||||
|
readonly wakeAt?: Date |
||||||
|
readonly wakeQuality?: WakeQuality |
||||||
|
readonly dreams?: string |
||||||
|
} |
||||||
|
export type SleepRecordJTD = typeof SleepRecordJTD |
||||||
|
export const SleepRecordJTD = schema({ |
||||||
|
schema: { |
||||||
|
optionalProperties: { |
||||||
|
sleepAt: { type: "timestamp" }, |
||||||
|
sleepQuality: SleepQualityJTD.reference, |
||||||
|
interruptions: { type: "uint32" }, |
||||||
|
wakeAt: { type: "timestamp" }, |
||||||
|
wakeQuality: WakeQualityJTD.reference, |
||||||
|
dreams: { type: "string" }, |
||||||
|
} |
||||||
|
}, |
||||||
|
typeHint: null as SleepRecord|null, |
||||||
|
key: "sleepRecord", |
||||||
|
references: [SleepQualityJTD, WakeQualityJTD], |
||||||
|
}) |
||||||
|
|
||||||
|
export type SleepRecordListJTD = typeof SleepRecordListJTD |
||||||
|
export const SleepRecordListJTD = schema({ |
||||||
|
schema: { |
||||||
|
elements: SleepRecordJTD.reference |
||||||
|
}, |
||||||
|
typeHint: null as (readonly SleepRecord[])|null, |
||||||
|
key: "sleepRecords", |
||||||
|
references: [SleepRecordJTD, ...SleepRecordJTD.requiredReferences], |
||||||
|
}) |
||||||
|
|
||||||
|
const TimeFormat = Intl.DateTimeFormat([], { |
||||||
|
dateStyle: undefined, |
||||||
|
timeStyle: undefined, |
||||||
|
year: undefined, |
||||||
|
month: undefined, |
||||||
|
day: undefined, |
||||||
|
weekday: undefined, |
||||||
|
hour: "numeric", |
||||||
|
minute: "2-digit", |
||||||
|
second: undefined, |
||||||
|
fractionalSecondDigits: undefined, |
||||||
|
timeZone: undefined, |
||||||
|
hour12: true, |
||||||
|
hourCycle: "h12", |
||||||
|
}) |
||||||
|
|
||||||
|
export function sleepRecordToString(record: SleepRecord) { |
||||||
|
const sleepAt = record.sleepAt === undefined ? null : TimeFormat.format(record.sleepAt) |
||||||
|
const wakeAt = record.wakeAt === undefined ? null : TimeFormat.format(record.wakeAt) |
||||||
|
return `${sleepAt ?? "?:??"} (${record.sleepQuality ?? "???"}) -${record.interruptions ?? "?"}-> ${wakeAt ?? "?:??"} (${record.wakeQuality ?? "???"})${typeof record.dreams === "string" ? " with dream journal" : ""}` |
||||||
|
} |
@ -1,13 +1,15 @@ |
|||||||
import {addEntryCommand} from "./commands/AddEntry"; |
import {addEntryCommand} from "./commands/AddEntry.js"; |
||||||
import {updateEmpathyGuideCommand} from "./commands/UpdateEmpathyGuide"; |
import {updateEmpathyGuideCommand} from "./commands/UpdateEmpathyGuide.js"; |
||||||
import yargs from "yargs" |
import {Argv, default as yargs} from "yargs" |
||||||
|
|
||||||
yargs |
const Yargs = yargs as unknown as {(): Argv} |
||||||
|
|
||||||
|
Yargs() |
||||||
.scriptName("mari-status-bar") |
.scriptName("mari-status-bar") |
||||||
.command(addEntryCommand()) |
.command(addEntryCommand()) |
||||||
.command(updateEmpathyGuideCommand()) |
.command(updateEmpathyGuideCommand()) |
||||||
.demandCommand() |
.demandCommand() |
||||||
.parseAsync() |
.parseAsync() |
||||||
.catch((err) => { |
.catch((err: unknown) => { |
||||||
console.error(err) |
console.error(err) |
||||||
}) |
}) |
@ -0,0 +1,196 @@ |
|||||||
|
import { |
||||||
|
DateQuestion, |
||||||
|
EditorQuestion, |
||||||
|
ListQuestion, |
||||||
|
NumberQuestion, |
||||||
|
} from "inquirer"; |
||||||
|
import {Separator} from "../Inquire.js"; |
||||||
|
import {InquireFunction} from "../Inquire.js"; |
||||||
|
import {SleepQuality, SleepRecord, sleepRecordToString, WakeQuality} from "../../datatypes/SleepRecord.js"; |
||||||
|
import {DateTime} from "luxon"; |
||||||
|
import chalk from "chalk"; |
||||||
|
import {EntryMainMenuChoice, makeEntryMainMenuChoice} from "./EntryMainMenuPrompt.js"; |
||||||
|
|
||||||
|
export interface SleepRecordPromptOptions extends Partial<Omit<EditorQuestion, "name"|"type"|"default"> & Omit<DateQuestion, "name"|"type"|"default"> & Omit<ListQuestion, "name"|"type"|"default"> & Omit<NumberQuestion, "name"|"type"|"default">>{ |
||||||
|
default?: SleepRecord, |
||||||
|
} |
||||||
|
|
||||||
|
export interface SleepRecordPromptDependencies { |
||||||
|
readonly inquire: InquireFunction<EditorQuestion, string> & InquireFunction<DateQuestion, Date|null> & InquireFunction<ListQuestion & {default: SleepQuality|undefined}, SleepQuality|undefined> & InquireFunction<ListQuestion & {default: WakeQuality|undefined}, WakeQuality|undefined> & InquireFunction<NumberQuestion, number> |
||||||
|
} |
||||||
|
|
||||||
|
export function sleepRecordPrompt(deps: SleepRecordPromptDependencies): { |
||||||
|
(options: SleepRecordPromptOptions): Promise<SleepRecord|null>, |
||||||
|
mainMenu: EntryMainMenuChoice<"sleepRecords">, |
||||||
|
} { |
||||||
|
return makeEntryMainMenuChoice({ |
||||||
|
property: "sleepRecords", |
||||||
|
name: (input) => typeof input !== "object" || input.length === 0 ? `Add sleep record` : `Change sleep record ${chalk.dim(`(currently ${chalk.greenBright(sleepRecordToString(input[0]))})`)}`, |
||||||
|
key: "z", |
||||||
|
injected: (options) => promptForSleepRecord(options, deps), |
||||||
|
toOptions: (value) => typeof value !== "object" || value.length === 0 ? {} : {default: value[0]}, |
||||||
|
toProperty: (value) => value === null ? undefined : [value], |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
export async function promptForSleepRecord(options: SleepRecordPromptOptions, {inquire}: SleepRecordPromptDependencies): Promise<SleepRecord | null> { |
||||||
|
const oldBedTime = options.default?.sleepAt |
||||||
|
const newBedTime = await inquire({ |
||||||
|
...options, |
||||||
|
type: "date", |
||||||
|
message: "About when did you get to sleep?", |
||||||
|
clearable: true, |
||||||
|
startCleared: false, |
||||||
|
default: oldBedTime ?? DateTime.local().set({hour: 23, minute: 0, second: 0, millisecond: 0}).minus({days: 1}).toJSDate(), |
||||||
|
format: { |
||||||
|
year: undefined, |
||||||
|
weekday: "short", |
||||||
|
month: "short", |
||||||
|
day: "numeric", |
||||||
|
hour: "numeric", |
||||||
|
minute: "numeric", |
||||||
|
second: undefined, |
||||||
|
fractionalSecondDigits: undefined, |
||||||
|
timeZoneName: "short", |
||||||
|
}, |
||||||
|
startingDatePart: "hour", |
||||||
|
deltas: { |
||||||
|
weekday: 1, |
||||||
|
hour: [1, 5, 10], |
||||||
|
minute: [15, 5, 1], |
||||||
|
default: 0 |
||||||
|
} |
||||||
|
}) |
||||||
|
const oldSleepQuality = options.default?.sleepQuality |
||||||
|
const newSleepQuality = await inquire({ |
||||||
|
...options, |
||||||
|
type: "list", |
||||||
|
message: `How'd you sleep?`, |
||||||
|
choices: [ |
||||||
|
{ |
||||||
|
value: SleepQuality.ACIDIC, |
||||||
|
name: `Acidic ${chalk.dim("(extremely poor/extremely restless/heavy acid reflux)")}` |
||||||
|
}, |
||||||
|
{ |
||||||
|
value: SleepQuality.RESTLESS, |
||||||
|
name: `Restless ${chalk.dim("(very poor/very restless/fever dreamy)")}` |
||||||
|
}, |
||||||
|
{ |
||||||
|
value: SleepQuality.NIGHTMARISH, |
||||||
|
name: `Nightmarish ${chalk.dim("(poor/restless/plagued with nightmares)")}` |
||||||
|
}, |
||||||
|
{ |
||||||
|
value: SleepQuality.TIMESKIP, |
||||||
|
name: `Timeskip ${chalk.dim("(neutral/passage of time not recognized)")}` |
||||||
|
}, |
||||||
|
{ |
||||||
|
value: SleepQuality.DREAMLESS, |
||||||
|
name: `Dreamless ${chalk.dim("(good/peaceful/no dreams had or remembered)")}` |
||||||
|
}, |
||||||
|
{ |
||||||
|
value: SleepQuality.DREAMY, |
||||||
|
name: `Dreamy ${chalk.dim("(very good/peaceful/neutral to somewhat pleasant dreams)")}` |
||||||
|
}, |
||||||
|
{ |
||||||
|
value: SleepQuality.ECSTASY, |
||||||
|
name: `Ecstasy ${chalk.dim("(extremely good/very peaceful/very pleasant dreams)")}` |
||||||
|
}, |
||||||
|
new Separator(), |
||||||
|
{ |
||||||
|
value: undefined, |
||||||
|
name: `Uncertain` |
||||||
|
}, |
||||||
|
], |
||||||
|
default: oldSleepQuality ?? SleepQuality.TIMESKIP, |
||||||
|
pageSize: 999, |
||||||
|
}) |
||||||
|
const oldInterruptions = options.default?.interruptions |
||||||
|
const newInterruptionsRaw = await inquire({ |
||||||
|
...options, |
||||||
|
type: "number", |
||||||
|
message: "How many times did you end up waking up during the sleep?", |
||||||
|
default: oldInterruptions ?? -1, |
||||||
|
}) |
||||||
|
const newInterruptions = newInterruptionsRaw < 0 ? undefined : Math.round(newInterruptionsRaw) |
||||||
|
const oldWakeTime = options.default?.wakeAt |
||||||
|
const newWakeTime = await inquire({ |
||||||
|
...options, |
||||||
|
type: "date", |
||||||
|
message: "Around when did you get up?", |
||||||
|
clearable: true, |
||||||
|
startCleared: false, |
||||||
|
default: oldWakeTime ?? DateTime.local().set({hour: 7, minute: 0, second: 0, millisecond: 0}).toJSDate(), |
||||||
|
format: { |
||||||
|
year: undefined, |
||||||
|
weekday: "short", |
||||||
|
month: "short", |
||||||
|
day: "numeric", |
||||||
|
hour: "numeric", |
||||||
|
minute: "numeric", |
||||||
|
second: undefined, |
||||||
|
fractionalSecondDigits: undefined, |
||||||
|
timeZoneName: "short", |
||||||
|
}, |
||||||
|
startingDatePart: "hour", |
||||||
|
deltas: { |
||||||
|
hour: [1, 5, 10], |
||||||
|
minute: [15, 5, 1], |
||||||
|
default: 0 |
||||||
|
} |
||||||
|
}) |
||||||
|
const oldWakeQuality = options.default?.wakeQuality |
||||||
|
const newWakeQuality = await inquire({ |
||||||
|
...options, |
||||||
|
type: "list", |
||||||
|
message: `How'd you feel when you got up?`, |
||||||
|
choices: [ |
||||||
|
{ |
||||||
|
value: WakeQuality.AGONIZED, |
||||||
|
name: `Agonized ${chalk.dim("(in physical pain/barely able or unable to get out of bed)")}` |
||||||
|
}, |
||||||
|
{ |
||||||
|
value: WakeQuality.EXHAUSTED, |
||||||
|
name: `Exhausted ${chalk.dim("(very tired/not wanting to get out of bed)")}` |
||||||
|
}, |
||||||
|
{ |
||||||
|
value: WakeQuality.DROWSY, |
||||||
|
name: `Drowsy ${chalk.dim("(a bit sleepy but willing to get out of bed)")}` |
||||||
|
}, |
||||||
|
{ |
||||||
|
value: WakeQuality.RESTED, |
||||||
|
name: `Rested ${chalk.dim("(well rested and ready to get going)")}` |
||||||
|
}, |
||||||
|
{ |
||||||
|
value: WakeQuality.ENERGIZED, |
||||||
|
name: `Energized ${chalk.dim("(thoroughly recharged and eager to get up and running)")}` |
||||||
|
}, |
||||||
|
new Separator(), |
||||||
|
{ |
||||||
|
value: undefined, |
||||||
|
name: `Uncertain` |
||||||
|
}, |
||||||
|
], |
||||||
|
default: oldWakeQuality ?? WakeQuality.DROWSY, |
||||||
|
pageSize: 999, |
||||||
|
}) |
||||||
|
const oldDreamJournal = options.default?.dreams |
||||||
|
const newDreamJournalRaw = (await inquire({ |
||||||
|
...options, |
||||||
|
type: "editor", |
||||||
|
message: typeof oldDreamJournal === "string" ? `Edit dream journal for this sleep session:` : `Type up dream journal for this sleep session:`, |
||||||
|
default: oldDreamJournal, |
||||||
|
})).trimEnd() |
||||||
|
const newDreamJournal = newDreamJournalRaw === "" ? undefined : newDreamJournalRaw |
||||||
|
const result: SleepRecord = { |
||||||
|
dreams: newDreamJournal, |
||||||
|
interruptions: newInterruptions, |
||||||
|
sleepAt: newBedTime ?? undefined, |
||||||
|
sleepQuality: newSleepQuality, |
||||||
|
wakeAt: newWakeTime ?? undefined, |
||||||
|
wakeQuality: newWakeQuality, |
||||||
|
} |
||||||
|
return ( |
||||||
|
Object.keys(result).map((key: keyof SleepRecord) => result[key]).some(value => (value !== undefined)) |
||||||
|
? result |
||||||
|
: null) |
||||||
|
} |
@ -0,0 +1,386 @@ |
|||||||
|
/* |
||||||
|
* Adapted from https://github.com/haversnail/inquirer-date-prompt:
|
||||||
|
* MIT License |
||||||
|
* |
||||||
|
* Copyright (c) 2021 Alex Havermale |
||||||
|
* |
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy |
||||||
|
* of this software and associated documentation files (the "Software"), to deal |
||||||
|
* in the Software without restriction, including without limitation the rights |
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||||
|
* copies of the Software, and to permit persons to whom the Software is |
||||||
|
* furnished to do so, subject to the following conditions: |
||||||
|
* |
||||||
|
* The above copyright notice and this permission notice shall be included in all |
||||||
|
* copies or substantial portions of the Software. |
||||||
|
* |
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||||
|
* SOFTWARE. |
||||||
|
*/ |
||||||
|
|
||||||
|
import chalk from "chalk"; |
||||||
|
import {DateTime, DurationObjectUnits} from "luxon"; |
||||||
|
import inquirer, {Answers, DateQuestion, Question} from "inquirer"; |
||||||
|
import {Interface as ReadLineInterface, Key} from "readline"; |
||||||
|
import Prompt from "inquirer/lib/prompts/base.js"; |
||||||
|
import observe from "inquirer/lib/utils/events.js"; |
||||||
|
import {map, takeUntil} from "rxjs/operators/index.js"; |
||||||
|
import cliCursor from "cli-cursor" |
||||||
|
import SuccessfulPromptStateData = inquirer.prompts.SuccessfulPromptStateData; |
||||||
|
import FailedPromptStateData = inquirer.prompts.FailedPromptStateData; |
||||||
|
|
||||||
|
declare module "inquirer" { |
||||||
|
|
||||||
|
export interface DateQuestionOptions<AnswerT extends Answers> { |
||||||
|
/** |
||||||
|
* Transforms the value to display to the user. |
||||||
|
* |
||||||
|
* @param date |
||||||
|
* The currently selected date in string format. |
||||||
|
* |
||||||
|
* @param answers |
||||||
|
* The answers provided by the users. |
||||||
|
* |
||||||
|
* @param flags |
||||||
|
* Additional information about the value. |
||||||
|
* |
||||||
|
* @returns |
||||||
|
* The value to display to the user. |
||||||
|
*/ |
||||||
|
transformer?( |
||||||
|
date: string, |
||||||
|
answers: AnswerT, |
||||||
|
flags: { isDirty?: boolean; isCleared?: boolean; isFinal?: boolean }, |
||||||
|
): string | Promise<string>; |
||||||
|
|
||||||
|
/** |
||||||
|
* A Boolean value indicating whether the prompt is clearable. |
||||||
|
* If `true`, pressing `backspace` or `delete` will replace the current value with `null`. |
||||||
|
*/ |
||||||
|
clearable?: boolean; |
||||||
|
|
||||||
|
/** |
||||||
|
* A Boolean value indicating whether the prompt should start cleared even though a default is provided. |
||||||
|
* If `true`, the value will start off null, but clearing it will change to the default date. |
||||||
|
*/ |
||||||
|
startCleared?: boolean; |
||||||
|
|
||||||
|
/** |
||||||
|
* A specific locale to use when formatting the date. |
||||||
|
* If no locale is provided, it will default to the user's current locale. |
||||||
|
* @see the {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat|Intl.DateTimeFormat} docs for more info.
|
||||||
|
*/ |
||||||
|
locale?: string; |
||||||
|
|
||||||
|
/** |
||||||
|
* A set of options for customizing the date format. |
||||||
|
* @see the {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat|Intl.DateTimeFormat} docs for more info.
|
||||||
|
*/ |
||||||
|
format?: Intl.DateTimeFormatOptions; |
||||||
|
/** The date part that should be highlighted when the prompt is shown. */ |
||||||
|
startingDatePart?: Intl.DateTimeFormatPartTypes |
||||||
|
/** The amount by which each value should change. If null, the value will not be selectable. */ |
||||||
|
deltas?: { |
||||||
|
[key in Intl.DateTimeFormatPartTypes|"default"]?: number|[number]|[number,number]|[number,number,number]|[number,number,number,number]|0 |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export interface DateQuestion<AnswerT extends Answers = Answers> extends Question<AnswerT>, DateQuestionOptions<AnswerT> { |
||||||
|
/** @inheritDoc */ |
||||||
|
type: "date" |
||||||
|
/** If the default is not provided or null, the value will be the present date at the time that the prompt is shown. */ |
||||||
|
default?: Date|null |
||||||
|
} |
||||||
|
|
||||||
|
export interface QuestionMap { |
||||||
|
["date"]: DateQuestion |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* A lookup object that maps each date part type to the corresponding field of a duration. |
||||||
|
*/ |
||||||
|
const offsetLookup: Partial<Record<Intl.DateTimeFormatPartTypes, keyof DurationObjectUnits>> = { |
||||||
|
year: "years", |
||||||
|
month: "months", |
||||||
|
day: "days", |
||||||
|
hour: "hours", |
||||||
|
minute: "minutes", |
||||||
|
second: "seconds", |
||||||
|
weekday: "days", |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns the index of the _last_ element in the array where predicate is true, and -1 otherwise. |
||||||
|
*/ |
||||||
|
function findLastIndex<T>(array: T[], predicate: (value: T, index: number, obj: T[]) => boolean) { |
||||||
|
let l = array.length; |
||||||
|
while (l--) { |
||||||
|
if (predicate(array[l], l, array)) return l; |
||||||
|
} |
||||||
|
return -1; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Represents a date prompt. |
||||||
|
*/ |
||||||
|
export class DateInput<AnswerT extends Answers = Answers, QuestionT extends DateQuestion<AnswerT> = DateQuestion> extends Prompt<QuestionT> { |
||||||
|
date: DateTime |
||||||
|
readonly transformer: DateQuestion["transformer"] |
||||||
|
readonly clearable: boolean |
||||||
|
readonly format: Intl.DateTimeFormatOptions |
||||||
|
readonly deltas: NonNullable<DateQuestion["deltas"]> |
||||||
|
done: ((state: unknown) => void)|null = null |
||||||
|
isDirty: boolean |
||||||
|
isCleared: boolean |
||||||
|
cursorIndex: number |
||||||
|
firstEditableIndex: number |
||||||
|
lastEditableIndex: number |
||||||
|
|
||||||
|
constructor(questions: QuestionT, rl: ReadLineInterface, answers: AnswerT) { |
||||||
|
super(questions, rl, answers); |
||||||
|
// Set the format object based on the user's specified options:
|
||||||
|
const { transformer, clearable, startCleared, locale, format = {}, default: date, deltas, startingDatePart } = this.opt; |
||||||
|
this.transformer = transformer |
||||||
|
this.deltas = deltas ?? {} |
||||||
|
this.clearable = clearable ?? false |
||||||
|
this.format = { |
||||||
|
year: "numeric", |
||||||
|
month: "numeric", |
||||||
|
day: "numeric", |
||||||
|
hour: "numeric", |
||||||
|
minute: "numeric", |
||||||
|
...format, |
||||||
|
}; |
||||||
|
// Set the date object with either the default value or the current date:
|
||||||
|
this.date = DateTime.fromJSDate(date ?? new Date()); |
||||||
|
if (typeof locale === "string") { |
||||||
|
this.date = this.date.setLocale(locale) |
||||||
|
} |
||||||
|
// Clear the default value option (so it won't be printed by the Prompt class):
|
||||||
|
this.opt.default = null; |
||||||
|
this.isDirty = false; |
||||||
|
this.isCleared = startCleared ?? false; |
||||||
|
// Set the first and last indices of the editable date parts:
|
||||||
|
this.firstEditableIndex = this.dateParts.findIndex((part) => this.isDatePartEditable(part.type)); |
||||||
|
this.lastEditableIndex = findLastIndex(this.dateParts, (part) => this.isDatePartEditable(part.type)); |
||||||
|
// Set the cursor index to the first editable part:
|
||||||
|
this.cursorIndex = !!startingDatePart && this.isDatePartEditable(startingDatePart) ? this.dateParts.findIndex((part) => part.type === startingDatePart) : this.firstEditableIndex; |
||||||
|
} |
||||||
|
|
||||||
|
// Called by parent class:
|
||||||
|
_run(cb: DateInput["done"]) { |
||||||
|
this.done = cb; |
||||||
|
|
||||||
|
// Observe events:
|
||||||
|
const events = observe(this.rl); |
||||||
|
const submit = events.line.pipe(map(() => (this.isCleared ? null : this.date.toJSDate()))); |
||||||
|
const validation = this.handleSubmitEvents(submit); |
||||||
|
validation.success.forEach(this.onEnd.bind(this)); |
||||||
|
validation.error.forEach(this.onError.bind(this)); |
||||||
|
events.keypress.pipe(takeUntil(validation.success)).forEach(this.onKeypress.bind(this)); |
||||||
|
|
||||||
|
// Init the prompt:
|
||||||
|
cliCursor.hide(); |
||||||
|
this.render(); |
||||||
|
|
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Renders the prompt. |
||||||
|
* @param {string} [error] |
||||||
|
*/ |
||||||
|
render(error?: string) { |
||||||
|
let message = this.getQuestion(); // The question portion of the output, including any prefix and suffix
|
||||||
|
|
||||||
|
const { isDirty, isCleared } = this; |
||||||
|
const isFinal = this.status === "answered"; |
||||||
|
|
||||||
|
if (!isCleared) { |
||||||
|
const dateString = this.dateParts |
||||||
|
.map(({ value }, index) => |
||||||
|
isFinal |
||||||
|
? chalk.cyan(value) |
||||||
|
: index === this.cursorIndex |
||||||
|
? chalk.inverse(value) |
||||||
|
: !isDirty |
||||||
|
? chalk.dim(value) |
||||||
|
: value, |
||||||
|
) |
||||||
|
.join(""); |
||||||
|
|
||||||
|
// Apply the transformer function if one was provided:
|
||||||
|
message += this.opt.transformer |
||||||
|
? this.opt.transformer(dateString, this.answers as AnswerT, { isDirty, isCleared, isFinal }) |
||||||
|
: dateString; |
||||||
|
|
||||||
|
// Display info on how to clear if the prompt is clearable:
|
||||||
|
if (this.opt.clearable && !isFinal) { |
||||||
|
message += chalk.dim(" (<delete> to clear) "); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const bottomContent = error ? chalk.red(">> ") + error : ""; |
||||||
|
|
||||||
|
// Render the final message:
|
||||||
|
this.screen.render(message, bottomContent); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* The end event handler. |
||||||
|
*/ |
||||||
|
onEnd({ value }: SuccessfulPromptStateData<Date|null>) { |
||||||
|
this.status = "answered"; |
||||||
|
|
||||||
|
// Re-render prompt
|
||||||
|
this.render(); |
||||||
|
|
||||||
|
this.screen.done(); |
||||||
|
cliCursor.show(); |
||||||
|
if (this.done !== null) { |
||||||
|
this.done(value); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* The error event handler. |
||||||
|
*/ |
||||||
|
onError({ isValid }: FailedPromptStateData) { |
||||||
|
this.render(isValid || undefined); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* The array of date part objects according to the user's specified format. |
||||||
|
*/ |
||||||
|
get dateParts() { |
||||||
|
return this.date.toLocaleParts(this.format); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* The currently selected date part. |
||||||
|
*/ |
||||||
|
get currentDatePart() { |
||||||
|
return this.dateParts[this.cursorIndex]; |
||||||
|
} |
||||||
|
|
||||||
|
isDatePartEditable(part: Intl.DateTimeFormatPartTypes): boolean { |
||||||
|
return offsetLookup.hasOwnProperty(part) && ((this.deltas[part] ?? this.deltas["default"] ?? 1) !== 0) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* A Boolean value indicating whether the currently selected date part is editable. |
||||||
|
*/ |
||||||
|
get isCurrentDatePartEditable() { |
||||||
|
return this.isDatePartEditable(this.currentDatePart.type); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Moves the cursor index to the right. |
||||||
|
*/ |
||||||
|
incrementCursorIndex() { |
||||||
|
if (this.cursorIndex < this.lastEditableIndex) { |
||||||
|
this.cursorIndex++; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Moves the cursor index to the left. |
||||||
|
*/ |
||||||
|
decrementCursorIndex() { |
||||||
|
if (this.cursorIndex > this.firstEditableIndex) { |
||||||
|
this.cursorIndex--; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Shifts the currently selected date part to the specified offset value. |
||||||
|
* The default value is `0`. |
||||||
|
* @param {number} offset |
||||||
|
*/ |
||||||
|
shiftDatePartValue(offset = 0) { |
||||||
|
const { type } = this.currentDatePart; |
||||||
|
const duration: DurationObjectUnits = {} |
||||||
|
const offsetProperty = offsetLookup[type] |
||||||
|
if (offset !== 0 && typeof offsetProperty === "string") { |
||||||
|
duration[offsetProperty] = offset |
||||||
|
// Set the input as "dirty" now that the initial date is being changed:
|
||||||
|
this.isDirty = true; |
||||||
|
this.date = this.date.plus(duration) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Increments the currently selected date part by one. |
||||||
|
*/ |
||||||
|
incrementDatePartValueBy(value = 1) { |
||||||
|
this.shiftDatePartValue(value); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Decrements the currently selected date part by one. |
||||||
|
*/ |
||||||
|
decrementDatePartValueBy(value = 1) { |
||||||
|
this.shiftDatePartValue(-1 * value); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* The keypress event handler. |
||||||
|
*/ |
||||||
|
onKeypress({ key }: {key: Key}) { |
||||||
|
// Reset cleared state if any other key is pressed:
|
||||||
|
if (this.isCleared) { |
||||||
|
this.isCleared = false; |
||||||
|
this.isDirty = true; |
||||||
|
return |
||||||
|
} |
||||||
|
// Calculate the amount to increment/decrement by based on modifiers:
|
||||||
|
const deltas = this.deltas[this.currentDatePart.type] ?? this.deltas["default"] ?? [1, 10, 100] |
||||||
|
const amount = ((): number => { |
||||||
|
if (typeof deltas === "number") { |
||||||
|
return deltas |
||||||
|
} else { |
||||||
|
switch (deltas.length) { |
||||||
|
case 1: |
||||||
|
return deltas[0] |
||||||
|
case 2: |
||||||
|
return (key.shift || key.meta) ? deltas[1] : deltas[0] |
||||||
|
case 3: |
||||||
|
return (key.shift || key.meta) ? (key.shift && key.meta) ? deltas[2] : deltas[1] : deltas[0] |
||||||
|
case 4: |
||||||
|
return key.shift ? key.meta ? deltas[3] : deltas[1] : key.meta ? deltas[2] : deltas[0] |
||||||
|
} |
||||||
|
} |
||||||
|
})() |
||||||
|
|
||||||
|
switch (key.name) { |
||||||
|
case "right": |
||||||
|
do { |
||||||
|
this.incrementCursorIndex(); |
||||||
|
} while (!this.isCurrentDatePartEditable); // increments the cursor index until it hits an editable value
|
||||||
|
break; |
||||||
|
case "left": |
||||||
|
do { |
||||||
|
this.decrementCursorIndex(); |
||||||
|
} while (!this.isCurrentDatePartEditable); // decrements the cursor index until it hits an editable value
|
||||||
|
break; |
||||||
|
case "up": |
||||||
|
this.incrementDatePartValueBy(amount); |
||||||
|
break; |
||||||
|
case "down": |
||||||
|
this.decrementDatePartValueBy(amount); |
||||||
|
break; |
||||||
|
case "delete": |
||||||
|
case "backspace": |
||||||
|
if (this.clearable) this.isCleared = true; |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
this.render(); |
||||||
|
} |
||||||
|
} |
@ -1,8 +1,10 @@ |
|||||||
import {registerPrompt} from "inquirer"; |
import {registerPrompt} from "../Inquire.js"; |
||||||
import {MultiTextInput} from "./MultiTextInput"; |
import {MultiTextInput} from "./MultiTextInput.js"; |
||||||
import {HierarchicalCheckboxInput} from "./HierarchicalCheckboxInput"; |
import {HierarchicalCheckboxInput} from "./HierarchicalCheckboxInput.js"; |
||||||
|
import {DateInput} from "./DateInput.js"; |
||||||
|
|
||||||
export function registerPrompts() { |
export function registerPrompts() { |
||||||
registerPrompt("multitext", MultiTextInput) |
registerPrompt("multitext", MultiTextInput) |
||||||
registerPrompt("hierarchical-checkbox", HierarchicalCheckboxInput) |
registerPrompt("hierarchical-checkbox", HierarchicalCheckboxInput) |
||||||
|
registerPrompt("date", DateInput) |
||||||
} |
} |
Loading…
Reference in new issue