First commit
commit
d7934fe348
@ -0,0 +1,5 @@
|
||||
# CHANGELOG
|
||||
|
||||
## 1.2.0
|
||||
|
||||
- Add support for Foundry v10
|
@ -0,0 +1,30 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Asacolips Projects / Foundry Mods
|
||||
|
||||
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.
|
||||
|
||||
This license does not apply to the compendium content listed in this software's
|
||||
"packs" directory. See the README for licensing information for the compendium
|
||||
packs.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
IMAGE LICENSES
|
||||
|
||||
anvil-impact.png by Lorc under CC BY 3.0 from game-icons.net
|
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
@ -0,0 +1,366 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap");
|
||||
/* Global styles */
|
||||
.window-app {
|
||||
font-family: "Roboto", sans-serif;
|
||||
}
|
||||
|
||||
.rollable:hover, .rollable:focus {
|
||||
color: #000;
|
||||
text-shadow: 0 0 10px red;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.grid,
|
||||
.grid-2col {
|
||||
display: grid;
|
||||
grid-column: span 2/span 2;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
margin: 10px 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.grid-3col {
|
||||
grid-column: span 3/span 3;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.grid-4col {
|
||||
grid-column: span 4/span 4;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.grid-5col {
|
||||
grid-column: span 5/span 5;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.grid-6col {
|
||||
grid-column: span 6/span 6;
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.grid-7col {
|
||||
grid-column: span 7/span 7;
|
||||
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.grid-8col {
|
||||
grid-column: span 8/span 8;
|
||||
grid-template-columns: repeat(8, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.grid-9col {
|
||||
grid-column: span 9/span 9;
|
||||
grid-template-columns: repeat(9, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.grid-10col {
|
||||
grid-column: span 10/span 10;
|
||||
grid-template-columns: repeat(10, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.grid-11col {
|
||||
grid-column: span 11/span 11;
|
||||
grid-template-columns: repeat(11, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.grid-12col {
|
||||
grid-column: span 12/span 12;
|
||||
grid-template-columns: repeat(12, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.grid-start-2 {
|
||||
grid-column-start: 2;
|
||||
}
|
||||
|
||||
.grid-start-3 {
|
||||
grid-column-start: 3;
|
||||
}
|
||||
|
||||
.grid-start-4 {
|
||||
grid-column-start: 4;
|
||||
}
|
||||
|
||||
.grid-start-5 {
|
||||
grid-column-start: 5;
|
||||
}
|
||||
|
||||
.grid-start-6 {
|
||||
grid-column-start: 6;
|
||||
}
|
||||
|
||||
.grid-start-7 {
|
||||
grid-column-start: 7;
|
||||
}
|
||||
|
||||
.grid-start-8 {
|
||||
grid-column-start: 8;
|
||||
}
|
||||
|
||||
.grid-start-9 {
|
||||
grid-column-start: 9;
|
||||
}
|
||||
|
||||
.grid-start-10 {
|
||||
grid-column-start: 10;
|
||||
}
|
||||
|
||||
.grid-start-11 {
|
||||
grid-column-start: 11;
|
||||
}
|
||||
|
||||
.grid-start-12 {
|
||||
grid-column-start: 12;
|
||||
}
|
||||
|
||||
.grid-span-2 {
|
||||
grid-column-end: span 2;
|
||||
}
|
||||
|
||||
.grid-span-3 {
|
||||
grid-column-end: span 3;
|
||||
}
|
||||
|
||||
.grid-span-4 {
|
||||
grid-column-end: span 4;
|
||||
}
|
||||
|
||||
.grid-span-5 {
|
||||
grid-column-end: span 5;
|
||||
}
|
||||
|
||||
.grid-span-6 {
|
||||
grid-column-end: span 6;
|
||||
}
|
||||
|
||||
.grid-span-7 {
|
||||
grid-column-end: span 7;
|
||||
}
|
||||
|
||||
.grid-span-8 {
|
||||
grid-column-end: span 8;
|
||||
}
|
||||
|
||||
.grid-span-9 {
|
||||
grid-column-end: span 9;
|
||||
}
|
||||
|
||||
.grid-span-10 {
|
||||
grid-column-end: span 10;
|
||||
}
|
||||
|
||||
.grid-span-11 {
|
||||
grid-column-end: span 11;
|
||||
}
|
||||
|
||||
.grid-span-12 {
|
||||
grid-column-end: span 12;
|
||||
}
|
||||
|
||||
.flex-group-center,
|
||||
.flex-group-left,
|
||||
.flex-group-right {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.flex-group-left {
|
||||
justify-content: flex-start;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.flex-group-right {
|
||||
justify-content: flex-end;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.flexshrink {
|
||||
flex: 0;
|
||||
}
|
||||
|
||||
.flex-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.flexlarge {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
justify-content: flex-start;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.align-right {
|
||||
justify-content: flex-end;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.align-center {
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Styles limited to wanderhome sheets */
|
||||
.wanderhome .item-form {
|
||||
font-family: "Roboto", sans-serif;
|
||||
}
|
||||
.wanderhome .sheet-header {
|
||||
flex: 0 auto;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.wanderhome .sheet-header .profile-img {
|
||||
flex: 0 0 100px;
|
||||
height: 100px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.wanderhome .sheet-header .header-fields {
|
||||
flex: 1;
|
||||
}
|
||||
.wanderhome .sheet-header h1.charname {
|
||||
height: 50px;
|
||||
padding: 0px;
|
||||
margin: 5px 0;
|
||||
border-bottom: 0;
|
||||
}
|
||||
.wanderhome .sheet-header h1.charname input {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
.wanderhome .sheet-tabs {
|
||||
flex: 0;
|
||||
}
|
||||
.wanderhome .sheet-body,
|
||||
.wanderhome .sheet-body .tab,
|
||||
.wanderhome .sheet-body .tab .editor {
|
||||
height: 100%;
|
||||
}
|
||||
.wanderhome .tox .tox-editor-container {
|
||||
background: #fff;
|
||||
}
|
||||
.wanderhome .tox .tox-edit-area {
|
||||
padding: 0 8px;
|
||||
}
|
||||
.wanderhome .resource-label {
|
||||
font-weight: bold;
|
||||
}
|
||||
.wanderhome .items-header {
|
||||
height: 28px;
|
||||
margin: 2px 0;
|
||||
padding: 0;
|
||||
align-items: center;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border: 2px groove #eeede0;
|
||||
font-weight: bold;
|
||||
}
|
||||
.wanderhome .items-header > * {
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
.wanderhome .items-header .item-name {
|
||||
font-weight: bold;
|
||||
padding-left: 5px;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
}
|
||||
.wanderhome .items-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
color: #444;
|
||||
}
|
||||
.wanderhome .items-list .item-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.wanderhome .items-list .item-name {
|
||||
flex: 2;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
font-size: 13px;
|
||||
text-align: left;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
.wanderhome .items-list .item-name h3, .wanderhome .items-list .item-name h4 {
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.wanderhome .items-list .item-controls {
|
||||
display: flex;
|
||||
flex: 0 0 100px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.wanderhome .items-list .item-controls a {
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
margin: 0 6px;
|
||||
}
|
||||
.wanderhome .items-list .item {
|
||||
align-items: center;
|
||||
padding: 0 2px;
|
||||
border-bottom: 1px solid #c9c7b8;
|
||||
}
|
||||
.wanderhome .items-list .item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.wanderhome .items-list .item .item-name {
|
||||
color: #191813;
|
||||
}
|
||||
.wanderhome .items-list .item .item-name .item-image {
|
||||
flex: 0 0 30px;
|
||||
height: 30px;
|
||||
background-size: 30px;
|
||||
border: none;
|
||||
margin-right: 5px;
|
||||
}
|
||||
.wanderhome .items-list .item-prop {
|
||||
text-align: center;
|
||||
border-left: 1px solid #c9c7b8;
|
||||
border-right: 1px solid #c9c7b8;
|
||||
font-size: 12px;
|
||||
}
|
||||
.wanderhome .items-list .items-header {
|
||||
height: 28px;
|
||||
margin: 2px 0;
|
||||
padding: 0;
|
||||
align-items: center;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border: 2px groove #eeede0;
|
||||
font-weight: bold;
|
||||
}
|
||||
.wanderhome .items-list .items-header > * {
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
.wanderhome .items-list .items-header .item-name {
|
||||
padding-left: 5px;
|
||||
text-align: left;
|
||||
}
|
||||
.wanderhome .item-formula {
|
||||
flex: 0 0 200px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
.wanderhome .effects .item .effect-source,
|
||||
.wanderhome .effects .item .effect-duration,
|
||||
.wanderhome .effects .item .effect-controls {
|
||||
text-align: center;
|
||||
border-left: 1px solid #c9c7b8;
|
||||
border-right: 1px solid #c9c7b8;
|
||||
font-size: 12px;
|
||||
}
|
||||
.wanderhome .effects .item .effect-controls {
|
||||
border: none;
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
{
|
||||
"WANDERHOME": {
|
||||
"Ability": {
|
||||
"Str": { "long": "Strength", "abbr": "str" },
|
||||
"Con": { "long": "Constitution", "abbr": "con" },
|
||||
"Dex": { "long": "Dexterity", "abbr": "dex" },
|
||||
"Int": { "long": "Intelligence", "abbr": "int" },
|
||||
"Wis": { "long": "Wisdom", "abbr": "wis" },
|
||||
"Cha": { "long": "Charisma", "abbr": "cha" }
|
||||
},
|
||||
"SheetLabels": {
|
||||
"Actor": "Wanderhome Actor Sheet",
|
||||
"Item": "Wanderhome Item Sheet"
|
||||
},
|
||||
"Item": {
|
||||
"Spell": {
|
||||
"SpellLVL": "Level {level} Spells",
|
||||
"AddLVL": "Add LVL {level}"
|
||||
}
|
||||
},
|
||||
"Effect": {
|
||||
"Source": "Source",
|
||||
"Toggle": "Toggle Effect",
|
||||
"Temporary": "Temporary Effects",
|
||||
"Passive": "Passive Effects",
|
||||
"Inactive": "Inactive Effects"
|
||||
}
|
||||
},
|
||||
|
||||
"TYPES": {
|
||||
"Actor": {
|
||||
"character": "Character",
|
||||
"npc": "NPC"
|
||||
},
|
||||
"Item": {
|
||||
"item": "Item",
|
||||
"feature": "Feature",
|
||||
"spell": "Spell"
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
// Export Actors
|
||||
export {default as WanderhomeActorBase} from "./base-actor.mjs";
|
||||
export {default as WanderhomeCharacter} from "./actor-character.mjs";
|
||||
export {default as WanderhomeNPC} from "./actor-npc.mjs";
|
||||
|
||||
// Export Items
|
||||
export {default as WanderhomeItemBase} from "./base-item.mjs";
|
||||
export {default as WanderhomeItem} from "./item-item.mjs";
|
||||
export {default as WanderhomeFeature} from "./item-feature.mjs";
|
||||
export {default as WanderhomeSpell} from "./item-spell.mjs";
|
@ -0,0 +1,52 @@
|
||||
import WanderhomeActorBase from "./base-actor.mjs";
|
||||
|
||||
export default class WanderhomeCharacter extends WanderhomeActorBase {
|
||||
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
const requiredInteger = { required: true, nullable: false, integer: true };
|
||||
const schema = super.defineSchema();
|
||||
|
||||
schema.attributes = new fields.SchemaField({
|
||||
level: new fields.SchemaField({
|
||||
value: new fields.NumberField({ ...requiredInteger, initial: 1 })
|
||||
}),
|
||||
});
|
||||
|
||||
// Iterate over ability names and create a new SchemaField for each.
|
||||
schema.abilities = new fields.SchemaField(Object.keys(CONFIG.WANDERHOME.abilities).reduce((obj, ability) => {
|
||||
obj[ability] = new fields.SchemaField({
|
||||
value: new fields.NumberField({ ...requiredInteger, initial: 10, min: 0 }),
|
||||
});
|
||||
return obj;
|
||||
}, {}));
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
prepareDerivedData() {
|
||||
// Loop through ability scores, and add their modifiers to our sheet output.
|
||||
for (const key in this.abilities) {
|
||||
// Calculate the modifier using d20 rules.
|
||||
this.abilities[key].mod = Math.floor((this.abilities[key].value - 10) / 2);
|
||||
// Handle ability label localization.
|
||||
this.abilities[key].label = game.i18n.localize(CONFIG.WANDERHOME.abilities[key]) ?? key;
|
||||
}
|
||||
}
|
||||
|
||||
getRollData() {
|
||||
const data = {};
|
||||
|
||||
// Copy the ability scores to the top level, so that rolls can use
|
||||
// formulas like `@str.mod + 4`.
|
||||
if (this.abilities) {
|
||||
for (let [k,v] of Object.entries(this.abilities)) {
|
||||
data[k] = foundry.utils.deepClone(v);
|
||||
}
|
||||
}
|
||||
|
||||
data.lvl = this.attributes.level.value;
|
||||
|
||||
return data
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
import WanderhomeActorBase from "./base-actor.mjs";
|
||||
|
||||
export default class WanderhomeNPC extends WanderhomeActorBase {
|
||||
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
const requiredInteger = { required: true, nullable: false, integer: true };
|
||||
const schema = super.defineSchema();
|
||||
|
||||
schema.cr = new fields.NumberField({ ...requiredInteger, initial: 1, min: 0 });
|
||||
schema.xp = new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 });
|
||||
|
||||
return schema
|
||||
}
|
||||
|
||||
prepareDerivedData() {
|
||||
this.xp = this.cr * this.cr * 100;
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
import WanderhomeDataModel from "./base-model.mjs";
|
||||
|
||||
export default class WanderhomeActorBase extends WanderhomeDataModel {
|
||||
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
const requiredInteger = { required: true, nullable: false, integer: true };
|
||||
const schema = {};
|
||||
|
||||
schema.health = new fields.SchemaField({
|
||||
value: new fields.NumberField({ ...requiredInteger, initial: 10, min: 0 }),
|
||||
max: new fields.NumberField({ ...requiredInteger, initial: 10 })
|
||||
});
|
||||
schema.power = new fields.SchemaField({
|
||||
value: new fields.NumberField({ ...requiredInteger, initial: 5, min: 0 }),
|
||||
max: new fields.NumberField({ ...requiredInteger, initial: 5 })
|
||||
});
|
||||
schema.biography = new fields.StringField({ required: true, blank: true }); // equivalent to passing ({initial: ""}) for StringFields
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
import WanderhomeDataModel from "./base-model.mjs";
|
||||
|
||||
export default class WanderhomeItemBase extends WanderhomeDataModel {
|
||||
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
const schema = {};
|
||||
|
||||
schema.description = new fields.StringField({ required: true, blank: true });
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
export default class WanderhomeDataModel extends foundry.abstract.TypeDataModel {
|
||||
/**
|
||||
* Convert the schema to a plain object.
|
||||
*
|
||||
* The built in `toObject()` method will ignore derived data when using Data Models.
|
||||
* This additional method will instead use the spread operator to return a simplified
|
||||
* version of the data.
|
||||
*
|
||||
* @returns {object} Plain object either via deepClone or the spread operator.
|
||||
*/
|
||||
toPlainObject() {
|
||||
return {...this};
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
import WanderhomeItemBase from "./base-item.mjs";
|
||||
|
||||
export default class WanderhomeFeature extends WanderhomeItemBase {}
|
@ -0,0 +1,31 @@
|
||||
import WanderhomeItemBase from "./base-item.mjs";
|
||||
|
||||
export default class WanderhomeItem extends WanderhomeItemBase {
|
||||
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
const requiredInteger = { required: true, nullable: false, integer: true };
|
||||
const schema = super.defineSchema();
|
||||
|
||||
schema.quantity = new fields.NumberField({ ...requiredInteger, initial: 1, min: 1 });
|
||||
schema.weight = new fields.NumberField({ required: true, nullable: false, initial: 0, min: 0 });
|
||||
|
||||
// Break down roll formula into three independent fields
|
||||
schema.roll = new fields.SchemaField({
|
||||
diceNum: new fields.NumberField({ ...requiredInteger, initial: 1, min: 1 }),
|
||||
diceSize: new fields.StringField({ initial: "d20" }),
|
||||
diceBonus: new fields.StringField({ initial: "+@str.mod+ceil(@lvl / 2)" })
|
||||
})
|
||||
|
||||
schema.formula = new fields.StringField({ blank: true });
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
prepareDerivedData() {
|
||||
// Build the formula dynamically using string interpolation
|
||||
const roll = this.roll;
|
||||
|
||||
this.formula = `${roll.diceNum}${roll.diceSize}${roll.diceBonus}`
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
import WanderhomeItemBase from "./base-item.mjs";
|
||||
|
||||
export default class WanderhomeSpell extends WanderhomeItemBase {
|
||||
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
const schema = super.defineSchema();
|
||||
|
||||
schema.spellLevel = new fields.NumberField({ required: true, nullable: false, integer: true, initial: 1, min: 1, max: 9 });
|
||||
|
||||
return schema;
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Extend the base Actor document by defining a custom roll data structure which is ideal for the Simple system.
|
||||
* @extends {Actor}
|
||||
*/
|
||||
export class WanderhomeActor extends Actor {
|
||||
/** @override */
|
||||
prepareData() {
|
||||
// Prepare data for the actor. Calling the super version of this executes
|
||||
// the following, in order: data reset (to clear active effects),
|
||||
// prepareBaseData(), prepareEmbeddedDocuments() (including active effects),
|
||||
// prepareDerivedData().
|
||||
super.prepareData();
|
||||
}
|
||||
|
||||
/** @override */
|
||||
prepareBaseData() {
|
||||
// Data modifications in this step occur before processing embedded
|
||||
// documents or derived data.
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
* Augment the actor source data with additional dynamic data that isn't
|
||||
* handled by the actor's DataModel. Data calculated in this step should be
|
||||
* available both inside and outside of character sheets (such as if an actor
|
||||
* is queried and has a roll executed directly from it).
|
||||
*/
|
||||
prepareDerivedData() {
|
||||
const actorData = this;
|
||||
const flags = actorData.flags.wanderhome || {};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @override
|
||||
* Augment the actor's default getRollData() method by appending the data object
|
||||
* generated by the its DataModel's getRollData(), or null. This polymorphic
|
||||
* approach is useful when you have actors & items that share a parent Document,
|
||||
* but have slightly different data preparation needs.
|
||||
*/
|
||||
getRollData() {
|
||||
return { ...super.getRollData(), ...this.system.getRollData?.() ?? null };
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the actor document to a plain object.
|
||||
*
|
||||
* The built in `toObject()` method will ignore derived data when using Data Models.
|
||||
* This additional method will instead use the spread operator to return a simplified
|
||||
* version of the data.
|
||||
*
|
||||
* @returns {object} Plain object either via deepClone or the spread operator.
|
||||
*/
|
||||
toPlainObject() {
|
||||
const result = {...this};
|
||||
|
||||
// Simplify system data.
|
||||
result.system = this.system.toPlainObject();
|
||||
|
||||
// Add items.
|
||||
result.items = this.items?.size > 0 ? this.items.contents : [];
|
||||
|
||||
// Add effects.
|
||||
result.effects = this.effects?.size > 0 ? this.effects.contents : [];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Extend the basic Item with some very simple modifications.
|
||||
* @extends {Item}
|
||||
*/
|
||||
export class WanderhomeItem extends Item {
|
||||
/**
|
||||
* Augment the basic Item data model with additional dynamic data.
|
||||
*/
|
||||
prepareData() {
|
||||
// As with the actor class, items are documents that can have their data
|
||||
// preparation methods overridden (such as prepareBaseData()).
|
||||
super.prepareData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a data object which defines the data schema used by dice roll commands against this Item
|
||||
* @override
|
||||
*/
|
||||
getRollData() {
|
||||
// Starts off by populating the roll data with a shallow copy of `this.system`
|
||||
const rollData = { ...this.system };
|
||||
|
||||
// Quit early if there's no parent actor
|
||||
if (!this.actor) return rollData;
|
||||
|
||||
// If present, add the actor's roll data
|
||||
rollData.actor = this.actor.getRollData();
|
||||
|
||||
return rollData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the actor document to a plain object.
|
||||
*
|
||||
* The built in `toObject()` method will ignore derived data when using Data Models.
|
||||
* This additional method will instead use the spread operator to return a simplified
|
||||
* version of the data.
|
||||
*
|
||||
* @returns {object} Plain object either via deepClone or the spread operator.
|
||||
*/
|
||||
toPlainObject() {
|
||||
const result = { ...this };
|
||||
|
||||
// Simplify system data.
|
||||
result.system = this.system.toPlainObject();
|
||||
|
||||
// Add effects.
|
||||
result.effects = this.effects?.size > 0 ? this.effects.contents : [];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle clickable rolls.
|
||||
* @param {Event} event The originating click event
|
||||
* @private
|
||||
*/
|
||||
async roll() {
|
||||
const item = this;
|
||||
|
||||
// Initialize chat data.
|
||||
const speaker = ChatMessage.getSpeaker({ actor: this.actor });
|
||||
const rollMode = game.settings.get('core', 'rollMode');
|
||||
const label = `[${item.type}] ${item.name}`;
|
||||
|
||||
// If there's no roll data, send a chat message.
|
||||
if (!this.system.formula) {
|
||||
ChatMessage.create({
|
||||
speaker: speaker,
|
||||
rollMode: rollMode,
|
||||
flavor: label,
|
||||
content: item.system.description ?? '',
|
||||
});
|
||||
}
|
||||
// Otherwise, create a roll and send a chat message from it.
|
||||
else {
|
||||
// Retrieve roll data.
|
||||
const rollData = this.getRollData();
|
||||
|
||||
// Invoke the roll and submit it to chat.
|
||||
const roll = new Roll(rollData.formula, rollData.actor);
|
||||
// If you need to store the value first, uncomment the next line.
|
||||
// const result = await roll.evaluate();
|
||||
roll.toMessage({
|
||||
speaker: speaker,
|
||||
rollMode: rollMode,
|
||||
flavor: label,
|
||||
});
|
||||
return roll;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
export const WANDERHOME = {};
|
||||
|
||||
/**
|
||||
* The set of Ability Scores used within the system.
|
||||
* @type {Object}
|
||||
*/
|
||||
WANDERHOME.abilities = {
|
||||
str: 'WANDERHOME.Ability.Str.long',
|
||||
dex: 'WANDERHOME.Ability.Dex.long',
|
||||
con: 'WANDERHOME.Ability.Con.long',
|
||||
int: 'WANDERHOME.Ability.Int.long',
|
||||
wis: 'WANDERHOME.Ability.Wis.long',
|
||||
cha: 'WANDERHOME.Ability.Cha.long',
|
||||
};
|
||||
|
||||
WANDERHOME.abilityAbbreviations = {
|
||||
str: 'WANDERHOME.Ability.Str.abbr',
|
||||
dex: 'WANDERHOME.Ability.Dex.abbr',
|
||||
con: 'WANDERHOME.Ability.Con.abbr',
|
||||
int: 'WANDERHOME.Ability.Int.abbr',
|
||||
wis: 'WANDERHOME.Ability.Wis.abbr',
|
||||
cha: 'WANDERHOME.Ability.Cha.abbr',
|
||||
};
|
@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Manage Active Effect instances through an Actor or Item Sheet via effect control buttons.
|
||||
* @param {MouseEvent} event The left-click event on the effect control
|
||||
* @param {Actor|Item} owner The owning document which manages this effect
|
||||
*/
|
||||
export function onManageActiveEffect(event, owner) {
|
||||
event.preventDefault();
|
||||
const a = event.currentTarget;
|
||||
const li = a.closest('li');
|
||||
const effect = li.dataset.effectId
|
||||
? owner.effects.get(li.dataset.effectId)
|
||||
: null;
|
||||
switch (a.dataset.action) {
|
||||
case 'create':
|
||||
return owner.createEmbeddedDocuments('ActiveEffect', [
|
||||
{
|
||||
name: game.i18n.format('DOCUMENT.New', {
|
||||
type: game.i18n.localize('DOCUMENT.ActiveEffect'),
|
||||
}),
|
||||
icon: 'icons/svg/aura.svg',
|
||||
origin: owner.uuid,
|
||||
'duration.rounds':
|
||||
li.dataset.effectType === 'temporary' ? 1 : undefined,
|
||||
disabled: li.dataset.effectType === 'inactive',
|
||||
},
|
||||
]);
|
||||
case 'edit':
|
||||
return effect.sheet.render(true);
|
||||
case 'delete':
|
||||
return effect.delete();
|
||||
case 'toggle':
|
||||
return effect.update({ disabled: !effect.disabled });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the data structure for Active Effects which are currently embedded in an Actor or Item.
|
||||
* @param {ActiveEffect[]} effects A collection or generator of Active Effect documents to prepare sheet data for
|
||||
* @return {object} Data for rendering
|
||||
*/
|
||||
export function prepareActiveEffectCategories(effects) {
|
||||
// Define effect header categories
|
||||
const categories = {
|
||||
temporary: {
|
||||
type: 'temporary',
|
||||
label: game.i18n.localize('WANDERHOME.Effect.Temporary'),
|
||||
effects: [],
|
||||
},
|
||||
passive: {
|
||||
type: 'passive',
|
||||
label: game.i18n.localize('WANDERHOME.Effect.Passive'),
|
||||
effects: [],
|
||||
},
|
||||
inactive: {
|
||||
type: 'inactive',
|
||||
label: game.i18n.localize('WANDERHOME.Effect.Inactive'),
|
||||
effects: [],
|
||||
},
|
||||
};
|
||||
|
||||
// Iterate over active effects, classifying them into categories
|
||||
for (let e of effects) {
|
||||
if (e.disabled) categories.inactive.effects.push(e);
|
||||
else if (e.isTemporary) categories.temporary.effects.push(e);
|
||||
else categories.passive.effects.push(e);
|
||||
}
|
||||
return categories;
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Define a set of template paths to pre-load
|
||||
* Pre-loaded templates are compiled and cached for fast access when rendering
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const preloadHandlebarsTemplates = async function () {
|
||||
return loadTemplates([
|
||||
// Actor partials.
|
||||
'systems/wanderhome/templates/actor/parts/actor-features.hbs',
|
||||
'systems/wanderhome/templates/actor/parts/actor-items.hbs',
|
||||
'systems/wanderhome/templates/actor/parts/actor-spells.hbs',
|
||||
'systems/wanderhome/templates/actor/parts/actor-effects.hbs',
|
||||
// Item partials
|
||||
'systems/wanderhome/templates/item/parts/item-effects.hbs',
|
||||
]);
|
||||
};
|
@ -0,0 +1,256 @@
|
||||
import {
|
||||
onManageActiveEffect,
|
||||
prepareActiveEffectCategories,
|
||||
} from '../helpers/effects.mjs';
|
||||
|
||||
/**
|
||||
* Extend the basic ActorSheet with some very simple modifications
|
||||
* @extends {ActorSheet}
|
||||
*/
|
||||
export class WanderhomeActorSheet extends ActorSheet {
|
||||
/** @override */
|
||||
static get defaultOptions() {
|
||||
return foundry.utils.mergeObject(super.defaultOptions, {
|
||||
classes: ['wanderhome', 'sheet', 'actor'],
|
||||
width: 600,
|
||||
height: 600,
|
||||
tabs: [
|
||||
{
|
||||
navSelector: '.sheet-tabs',
|
||||
contentSelector: '.sheet-body',
|
||||
initial: 'features',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
/** @override */
|
||||
get template() {
|
||||
return `systems/wanderhome/templates/actor/actor-${this.actor.type}-sheet.hbs`;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
async getData() {
|
||||
// Retrieve the data structure from the base sheet. You can inspect or log
|
||||
// the context variable to see the structure, but some key properties for
|
||||
// sheets are the actor object, the data object, whether or not it's
|
||||
// editable, the items array, and the effects array.
|
||||
const context = super.getData();
|
||||
|
||||
// Use a safe clone of the actor data for further operations.
|
||||
const actorData = this.document.toPlainObject();
|
||||
|
||||
// Add the actor's data to context.data for easier access, as well as flags.
|
||||
context.system = actorData.system;
|
||||
context.flags = actorData.flags;
|
||||
|
||||
// Adding a pointer to CONFIG.WANDERHOME
|
||||
context.config = CONFIG.WANDERHOME;
|
||||
|
||||
// Prepare character data and items.
|
||||
if (actorData.type == 'character') {
|
||||
this._prepareItems(context);
|
||||
this._prepareCharacterData(context);
|
||||
}
|
||||
|
||||
// Prepare NPC data and items.
|
||||
if (actorData.type == 'npc') {
|
||||
this._prepareItems(context);
|
||||
}
|
||||
|
||||
// Enrich biography info for display
|
||||
// Enrichment turns text like `[[/r 1d20]]` into buttons
|
||||
context.enrichedBiography = await TextEditor.enrichHTML(
|
||||
this.actor.system.biography,
|
||||
{
|
||||
// Whether to show secret blocks in the finished html
|
||||
secrets: this.document.isOwner,
|
||||
// Necessary in v11, can be removed in v12
|
||||
async: true,
|
||||
// Data to fill in for inline rolls
|
||||
rollData: this.actor.getRollData(),
|
||||
// Relative UUID resolution
|
||||
relativeTo: this.actor,
|
||||
}
|
||||
);
|
||||
|
||||
// Prepare active effects
|
||||
context.effects = prepareActiveEffectCategories(
|
||||
// A generator that returns all effects stored on the actor
|
||||
// as well as any items
|
||||
this.actor.allApplicableEffects()
|
||||
);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Character-specific context modifications
|
||||
*
|
||||
* @param {object} context The context object to mutate
|
||||
*/
|
||||
_prepareCharacterData(context) {
|
||||
// This is where you can enrich character-specific editor fields
|
||||
// or setup anything else that's specific to this type
|
||||
}
|
||||
|
||||
/**
|
||||
* Organize and classify Items for Actor sheets.
|
||||
*
|
||||
* @param {object} context The context object to mutate
|
||||
*/
|
||||
_prepareItems(context) {
|
||||
// Initialize containers.
|
||||
const gear = [];
|
||||
const features = [];
|
||||
const spells = {
|
||||
0: [],
|
||||
1: [],
|
||||
2: [],
|
||||
3: [],
|
||||
4: [],
|
||||
5: [],
|
||||
6: [],
|
||||
7: [],
|
||||
8: [],
|
||||
9: [],
|
||||
};
|
||||
|
||||
// Iterate through items, allocating to containers
|
||||
for (let i of context.items) {
|
||||
i.img = i.img || Item.DEFAULT_ICON;
|
||||
// Append to gear.
|
||||
if (i.type === 'item') {
|
||||
gear.push(i);
|
||||
}
|
||||
// Append to features.
|
||||
else if (i.type === 'feature') {
|
||||
features.push(i);
|
||||
}
|
||||
// Append to spells.
|
||||
else if (i.type === 'spell') {
|
||||
if (i.system.spellLevel != undefined) {
|
||||
spells[i.system.spellLevel].push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Assign and return
|
||||
context.gear = gear;
|
||||
context.features = features;
|
||||
context.spells = spells;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
activateListeners(html) {
|
||||
super.activateListeners(html);
|
||||
|
||||
// Render the item sheet for viewing/editing prior to the editable check.
|
||||
html.on('click', '.item-edit', (ev) => {
|
||||
const li = $(ev.currentTarget).parents('.item');
|
||||
const item = this.actor.items.get(li.data('itemId'));
|
||||
item.sheet.render(true);
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Everything below here is only needed if the sheet is editable
|
||||
if (!this.isEditable) return;
|
||||
|
||||
// Add Inventory Item
|
||||
html.on('click', '.item-create', this._onItemCreate.bind(this));
|
||||
|
||||
// Delete Inventory Item
|
||||
html.on('click', '.item-delete', (ev) => {
|
||||
const li = $(ev.currentTarget).parents('.item');
|
||||
const item = this.actor.items.get(li.data('itemId'));
|
||||
item.delete();
|
||||
li.slideUp(200, () => this.render(false));
|
||||
});
|
||||
|
||||
// Active Effect management
|
||||
html.on('click', '.effect-control', (ev) => {
|
||||
const row = ev.currentTarget.closest('li');
|
||||
const document =
|
||||
row.dataset.parentId === this.actor.id
|
||||
? this.actor
|
||||
: this.actor.items.get(row.dataset.parentId);
|
||||
onManageActiveEffect(ev, document);
|
||||
});
|
||||
|
||||
// Rollable abilities.
|
||||
html.on('click', '.rollable', this._onRoll.bind(this));
|
||||
|
||||
// Drag events for macros.
|
||||
if (this.actor.isOwner) {
|
||||
let handler = (ev) => this._onDragStart(ev);
|
||||
html.find('li.item').each((i, li) => {
|
||||
if (li.classList.contains('inventory-header')) return;
|
||||
li.setAttribute('draggable', true);
|
||||
li.addEventListener('dragstart', handler, false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle creating a new Owned Item for the actor using initial data defined in the HTML dataset
|
||||
* @param {Event} event The originating click event
|
||||
* @private
|
||||
*/
|
||||
async _onItemCreate(event) {
|
||||
event.preventDefault();
|
||||
const header = event.currentTarget;
|
||||
// Get the type of item to create.
|
||||
const type = header.dataset.type;
|
||||
// Grab any data associated with this control.
|
||||
const data = duplicate(header.dataset);
|
||||
// Initialize a default name.
|
||||
const name = `New ${type.capitalize()}`;
|
||||
// Prepare the item object.
|
||||
const itemData = {
|
||||
name: name,
|
||||
type: type,
|
||||
system: data,
|
||||
};
|
||||
// Remove the type from the dataset since it's in the itemData.type prop.
|
||||
delete itemData.system['type'];
|
||||
|
||||
// Finally, create the item!
|
||||
return await Item.create(itemData, { parent: this.actor });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle clickable rolls.
|
||||
* @param {Event} event The originating click event
|
||||
* @private
|
||||
*/
|
||||
_onRoll(event) {
|
||||
event.preventDefault();
|
||||
const element = event.currentTarget;
|
||||
const dataset = element.dataset;
|
||||
|
||||
// Handle item rolls.
|
||||
if (dataset.rollType) {
|
||||
if (dataset.rollType == 'item') {
|
||||
const itemId = element.closest('.item').dataset.itemId;
|
||||
const item = this.actor.items.get(itemId);
|
||||
if (item) return item.roll();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle rolls that supply the formula directly.
|
||||
if (dataset.roll) {
|
||||
let label = dataset.label ? `[ability] ${dataset.label}` : '';
|
||||
let roll = new Roll(dataset.roll, this.actor.getRollData());
|
||||
roll.toMessage({
|
||||
speaker: ChatMessage.getSpeaker({ actor: this.actor }),
|
||||
flavor: label,
|
||||
rollMode: game.settings.get('core', 'rollMode'),
|
||||
});
|
||||
return roll;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
import {
|
||||
onManageActiveEffect,
|
||||
prepareActiveEffectCategories,
|
||||
} from '../helpers/effects.mjs';
|
||||
|
||||
/**
|
||||
* Extend the basic ItemSheet with some very simple modifications
|
||||
* @extends {ItemSheet}
|
||||
*/
|
||||
export class WanderhomeItemSheet extends ItemSheet {
|
||||
/** @override */
|
||||
static get defaultOptions() {
|
||||
return foundry.utils.mergeObject(super.defaultOptions, {
|
||||
classes: ['wanderhome', 'sheet', 'item'],
|
||||
width: 520,
|
||||
height: 480,
|
||||
tabs: [
|
||||
{
|
||||
navSelector: '.sheet-tabs',
|
||||
contentSelector: '.sheet-body',
|
||||
initial: 'description',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
/** @override */
|
||||
get template() {
|
||||
const path = 'systems/wanderhome/templates/item';
|
||||
// Return a single sheet for all item types.
|
||||
// return `${path}/item-sheet.hbs`;
|
||||
|
||||
// Alternatively, you could use the following return statement to do a
|
||||
// unique item sheet by type, like `weapon-sheet.hbs`.
|
||||
return `${path}/item-${this.item.type}-sheet.hbs`;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
async getData() {
|
||||
// Retrieve base data structure.
|
||||
const context = super.getData();
|
||||
|
||||
// Use a safe clone of the item data for further operations.
|
||||
const itemData = this.document.toPlainObject();
|
||||
|
||||
// Enrich description info for display
|
||||
// Enrichment turns text like `[[/r 1d20]]` into buttons
|
||||
context.enrichedDescription = await TextEditor.enrichHTML(
|
||||
this.item.system.description,
|
||||
{
|
||||
// Whether to show secret blocks in the finished html
|
||||
secrets: this.document.isOwner,
|
||||
// Necessary in v11, can be removed in v12
|
||||
async: true,
|
||||
// Data to fill in for inline rolls
|
||||
rollData: this.item.getRollData(),
|
||||
// Relative UUID resolution
|
||||
relativeTo: this.item,
|
||||
}
|
||||
);
|
||||
|
||||
// Add the item's data to context.data for easier access, as well as flags.
|
||||
context.system = itemData.system;
|
||||
context.flags = itemData.flags;
|
||||
|
||||
// Adding a pointer to CONFIG.WANDERHOME
|
||||
context.config = CONFIG.WANDERHOME;
|
||||
|
||||
// Prepare active effects for easier access
|
||||
context.effects = prepareActiveEffectCategories(this.item.effects);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
activateListeners(html) {
|
||||
super.activateListeners(html);
|
||||
|
||||
// Everything below here is only needed if the sheet is editable
|
||||
if (!this.isEditable) return;
|
||||
|
||||
// Roll handlers, click handlers, etc. would go here.
|
||||
|
||||
// Active Effect management
|
||||
html.on('click', '.effect-control', (ev) =>
|
||||
onManageActiveEffect(ev, this.item)
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,158 @@
|
||||
// Import document classes.
|
||||
import { WanderhomeActor } from './documents/actor.mjs';
|
||||
import { WanderhomeItem } from './documents/item.mjs';
|
||||
// Import sheet classes.
|
||||
import { WanderhomeActorSheet } from './sheets/actor-sheet.mjs';
|
||||
import { WanderhomeItemSheet } from './sheets/item-sheet.mjs';
|
||||
// Import helper/utility classes and constants.
|
||||
import { preloadHandlebarsTemplates } from './helpers/templates.mjs';
|
||||
import { WANDERHOME } from './helpers/config.mjs';
|
||||
// Import DataModel classes
|
||||
import * as models from './data/_module.mjs';
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Init Hook */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
Hooks.once('init', function () {
|
||||
// Add utility classes to the global game object so that they're more easily
|
||||
// accessible in global contexts.
|
||||
game.wanderhome = {
|
||||
WanderhomeActor,
|
||||
WanderhomeItem,
|
||||
rollItemMacro,
|
||||
};
|
||||
|
||||
// Add custom constants for configuration.
|
||||
CONFIG.WANDERHOME = WANDERHOME;
|
||||
|
||||
/**
|
||||
* Set an initiative formula for the system
|
||||
* @type {String}
|
||||
*/
|
||||
CONFIG.Combat.initiative = {
|
||||
formula: '1d20 + @abilities.dex.mod',
|
||||
decimals: 2,
|
||||
};
|
||||
|
||||
// Define custom Document and DataModel classes
|
||||
CONFIG.Actor.documentClass = WanderhomeActor;
|
||||
|
||||
// Note that you don't need to declare a DataModel
|
||||
// for the base actor/item classes - they are included
|
||||
// with the Character/NPC as part of super.defineSchema()
|
||||
CONFIG.Actor.dataModels = {
|
||||
character: models.WanderhomeCharacter,
|
||||
npc: models.WanderhomeNPC
|
||||
}
|
||||
CONFIG.Item.documentClass = WanderhomeItem;
|
||||
CONFIG.Item.dataModels = {
|
||||
item: models.WanderhomeItem,
|
||||
feature: models.WanderhomeFeature,
|
||||
spell: models.WanderhomeSpell
|
||||
}
|
||||
|
||||
// Active Effects are never copied to the Actor,
|
||||
// but will still apply to the Actor from within the Item
|
||||
// if the transfer property on the Active Effect is true.
|
||||
CONFIG.ActiveEffect.legacyTransferral = false;
|
||||
|
||||
// Register sheet application classes
|
||||
Actors.unregisterSheet('core', ActorSheet);
|
||||
Actors.registerSheet('wanderhome', WanderhomeActorSheet, {
|
||||
makeDefault: true,
|
||||
label: 'WANDERHOME.SheetLabels.Actor',
|
||||
});
|
||||
Items.unregisterSheet('core', ItemSheet);
|
||||
Items.registerSheet('wanderhome', WanderhomeItemSheet, {
|
||||
makeDefault: true,
|
||||
label: 'WANDERHOME.SheetLabels.Item',
|
||||
});
|
||||
|
||||
// Preload Handlebars templates.
|
||||
return preloadHandlebarsTemplates();
|
||||
});
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Handlebars Helpers */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
// If you need to add Handlebars helpers, here is a useful example:
|
||||
Handlebars.registerHelper('toLowerCase', function (str) {
|
||||
return str.toLowerCase();
|
||||
});
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Ready Hook */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
Hooks.once('ready', function () {
|
||||
// Wait to register hotbar drop hook on ready so that modules could register earlier if they want to
|
||||
Hooks.on('hotbarDrop', (bar, data, slot) => createItemMacro(data, slot));
|
||||
});
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Hotbar Macros */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Create a Macro from an Item drop.
|
||||
* Get an existing item macro if one exists, otherwise create a new one.
|
||||
* @param {Object} data The dropped data
|
||||
* @param {number} slot The hotbar slot to use
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function createItemMacro(data, slot) {
|
||||
// First, determine if this is a valid owned item.
|
||||
if (data.type !== 'Item') return;
|
||||
if (!data.uuid.includes('Actor.') && !data.uuid.includes('Token.')) {
|
||||
return ui.notifications.warn(
|
||||
'You can only create macro buttons for owned Items'
|
||||
);
|
||||
}
|
||||
// If it is, retrieve it based on the uuid.
|
||||
const item = await Item.fromDropData(data);
|
||||
|
||||
// Create the macro command using the uuid.
|
||||
const command = `game.wanderhome.rollItemMacro("${data.uuid}");`;
|
||||
let macro = game.macros.find(
|
||||
(m) => m.name === item.name && m.command === command
|
||||
);
|
||||
if (!macro) {
|
||||
macro = await Macro.create({
|
||||
name: item.name,
|
||||
type: 'script',
|
||||
img: item.img,
|
||||
command: command,
|
||||
flags: { 'wanderhome.itemMacro': true },
|
||||
});
|
||||
}
|
||||
game.user.assignHotbarMacro(macro, slot);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Macro from an Item drop.
|
||||
* Get an existing item macro if one exists, otherwise create a new one.
|
||||
* @param {string} itemUuid
|
||||
*/
|
||||
function rollItemMacro(itemUuid) {
|
||||
// Reconstruct the drop data so that we can load the item.
|
||||
const dropData = {
|
||||
type: 'Item',
|
||||
uuid: itemUuid,
|
||||
};
|
||||
// Load the item from the uuid.
|
||||
Item.fromDropData(dropData).then((item) => {
|
||||
// Determine if the item loaded and if it's an owned item.
|
||||
if (!item || !item.parent) {
|
||||
const itemName = item?.name ?? itemUuid;
|
||||
return ui.notifications.warn(
|
||||
`Could not find item ${itemName}. You may need to delete and recreate this macro.`
|
||||
);
|
||||
}
|
||||
|
||||
// Trigger the item roll
|
||||
item.roll();
|
||||
});
|
||||
}
|
@ -0,0 +1 @@
|
||||
../detect-libc/bin/detect-libc.js
|
@ -0,0 +1 @@
|
||||
../sass/sass.js
|
@ -0,0 +1,804 @@
|
||||
{
|
||||
"name": "wanderhome",
|
||||
"version": "2.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"node_modules/@inquirer/checkbox": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.2.0.tgz",
|
||||
"integrity": "sha512-fdSw07FLJEU5vbpOPzXo5c6xmMGDzbZE2+niuDHX5N6mc6V0Ebso/q3xiHra4D73+PMsC8MJmcaZKuAAoaQsSA==",
|
||||
"dependencies": {
|
||||
"@inquirer/core": "^10.1.15",
|
||||
"@inquirer/figures": "^1.0.13",
|
||||
"@inquirer/type": "^3.0.8",
|
||||
"ansi-escapes": "^4.3.2",
|
||||
"yoctocolors-cjs": "^2.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/confirm": {
|
||||
"version": "5.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.14.tgz",
|
||||
"integrity": "sha512-5yR4IBfe0kXe59r1YCTG8WXkUbl7Z35HK87Sw+WUyGD8wNUx7JvY7laahzeytyE1oLn74bQnL7hstctQxisQ8Q==",
|
||||
"dependencies": {
|
||||
"@inquirer/core": "^10.1.15",
|
||||
"@inquirer/type": "^3.0.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/core": {
|
||||
"version": "10.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.15.tgz",
|
||||
"integrity": "sha512-8xrp836RZvKkpNbVvgWUlxjT4CraKk2q+I3Ksy+seI2zkcE+y6wNs1BVhgcv8VyImFecUhdQrYLdW32pAjwBdA==",
|
||||
"dependencies": {
|
||||
"@inquirer/figures": "^1.0.13",
|
||||
"@inquirer/type": "^3.0.8",
|
||||
"ansi-escapes": "^4.3.2",
|
||||
"cli-width": "^4.1.0",
|
||||
"mute-stream": "^2.0.0",
|
||||
"signal-exit": "^4.1.0",
|
||||
"wrap-ansi": "^6.2.0",
|
||||
"yoctocolors-cjs": "^2.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/editor": {
|
||||
"version": "4.2.16",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.16.tgz",
|
||||
"integrity": "sha512-iSzLjT4C6YKp2DU0fr8T7a97FnRRxMO6CushJnW5ktxLNM2iNeuyUuUA5255eOLPORoGYCrVnuDOEBdGkHGkpw==",
|
||||
"dependencies": {
|
||||
"@inquirer/core": "^10.1.15",
|
||||
"@inquirer/external-editor": "^1.0.0",
|
||||
"@inquirer/type": "^3.0.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/expand": {
|
||||
"version": "4.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.17.tgz",
|
||||
"integrity": "sha512-PSqy9VmJx/VbE3CT453yOfNa+PykpKg/0SYP7odez1/NWBGuDXgPhp4AeGYYKjhLn5lUUavVS/JbeYMPdH50Mw==",
|
||||
"dependencies": {
|
||||
"@inquirer/core": "^10.1.15",
|
||||
"@inquirer/type": "^3.0.8",
|
||||
"yoctocolors-cjs": "^2.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/external-editor": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.0.tgz",
|
||||
"integrity": "sha512-5v3YXc5ZMfL6OJqXPrX9csb4l7NlQA2doO1yynUjpUChT9hg4JcuBVP0RbsEJ/3SL/sxWEyFjT2W69ZhtoBWqg==",
|
||||
"dependencies": {
|
||||
"chardet": "^2.1.0",
|
||||
"iconv-lite": "^0.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/figures": {
|
||||
"version": "1.0.13",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.13.tgz",
|
||||
"integrity": "sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/input": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.2.1.tgz",
|
||||
"integrity": "sha512-tVC+O1rBl0lJpoUZv4xY+WGWY8V5b0zxU1XDsMsIHYregdh7bN5X5QnIONNBAl0K765FYlAfNHS2Bhn7SSOVow==",
|
||||
"dependencies": {
|
||||
"@inquirer/core": "^10.1.15",
|
||||
"@inquirer/type": "^3.0.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/number": {
|
||||
"version": "3.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.17.tgz",
|
||||
"integrity": "sha512-GcvGHkyIgfZgVnnimURdOueMk0CztycfC8NZTiIY9arIAkeOgt6zG57G+7vC59Jns3UX27LMkPKnKWAOF5xEYg==",
|
||||
"dependencies": {
|
||||
"@inquirer/core": "^10.1.15",
|
||||
"@inquirer/type": "^3.0.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/password": {
|
||||
"version": "4.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.17.tgz",
|
||||
"integrity": "sha512-DJolTnNeZ00E1+1TW+8614F7rOJJCM4y4BAGQ3Gq6kQIG+OJ4zr3GLjIjVVJCbKsk2jmkmv6v2kQuN/vriHdZA==",
|
||||
"dependencies": {
|
||||
"@inquirer/core": "^10.1.15",
|
||||
"@inquirer/type": "^3.0.8",
|
||||
"ansi-escapes": "^4.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/prompts": {
|
||||
"version": "7.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.8.1.tgz",
|
||||
"integrity": "sha512-LpBPeIpyCF1H3C7SK/QxJQG4iV1/SRmJdymfcul8PuwtVhD0JI1CSwqmd83VgRgt1QEsDojQYFSXJSgo81PVMw==",
|
||||
"dependencies": {
|
||||
"@inquirer/checkbox": "^4.2.0",
|
||||
"@inquirer/confirm": "^5.1.14",
|
||||
"@inquirer/editor": "^4.2.16",
|
||||
"@inquirer/expand": "^4.0.17",
|
||||
"@inquirer/input": "^4.2.1",
|
||||
"@inquirer/number": "^3.0.17",
|
||||
"@inquirer/password": "^4.0.17",
|
||||
"@inquirer/rawlist": "^4.1.5",
|
||||
"@inquirer/search": "^3.1.0",
|
||||
"@inquirer/select": "^4.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/rawlist": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.5.tgz",
|
||||
"integrity": "sha512-R5qMyGJqtDdi4Ht521iAkNqyB6p2UPuZUbMifakg1sWtu24gc2Z8CJuw8rP081OckNDMgtDCuLe42Q2Kr3BolA==",
|
||||
"dependencies": {
|
||||
"@inquirer/core": "^10.1.15",
|
||||
"@inquirer/type": "^3.0.8",
|
||||
"yoctocolors-cjs": "^2.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/search": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.1.0.tgz",
|
||||
"integrity": "sha512-PMk1+O/WBcYJDq2H7foV0aAZSmDdkzZB9Mw2v/DmONRJopwA/128cS9M/TXWLKKdEQKZnKwBzqu2G4x/2Nqx8Q==",
|
||||
"dependencies": {
|
||||
"@inquirer/core": "^10.1.15",
|
||||
"@inquirer/figures": "^1.0.13",
|
||||
"@inquirer/type": "^3.0.8",
|
||||
"yoctocolors-cjs": "^2.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/select": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.3.1.tgz",
|
||||
"integrity": "sha512-Gfl/5sqOF5vS/LIrSndFgOh7jgoe0UXEizDqahFRkq5aJBLegZ6WjuMh/hVEJwlFQjyLq1z9fRtvUMkb7jM1LA==",
|
||||
"dependencies": {
|
||||
"@inquirer/core": "^10.1.15",
|
||||
"@inquirer/figures": "^1.0.13",
|
||||
"@inquirer/type": "^3.0.8",
|
||||
"ansi-escapes": "^4.3.2",
|
||||
"yoctocolors-cjs": "^2.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/type": {
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.8.tgz",
|
||||
"integrity": "sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw==",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
|
||||
"integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^1.0.3",
|
||||
"is-glob": "^4.0.3",
|
||||
"micromatch": "^4.0.5",
|
||||
"node-addon-api": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@parcel/watcher-android-arm64": "2.5.1",
|
||||
"@parcel/watcher-darwin-arm64": "2.5.1",
|
||||
"@parcel/watcher-darwin-x64": "2.5.1",
|
||||
"@parcel/watcher-freebsd-x64": "2.5.1",
|
||||
"@parcel/watcher-linux-arm-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-arm-musl": "2.5.1",
|
||||
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-arm64-musl": "2.5.1",
|
||||
"@parcel/watcher-linux-x64-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-x64-musl": "2.5.1",
|
||||
"@parcel/watcher-win32-arm64": "2.5.1",
|
||||
"@parcel/watcher-win32-ia32": "2.5.1",
|
||||
"@parcel/watcher-win32-x64": "2.5.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-x64-glibc": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
|
||||
"integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-x64-musl": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
|
||||
"integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz",
|
||||
"integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-escapes": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
|
||||
"integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
|
||||
"dependencies": {
|
||||
"type-fest": "^0.21.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/chardet": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.0.tgz",
|
||||
"integrity": "sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA=="
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"readdirp": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.16.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-width": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz",
|
||||
"integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
||||
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"bin": {
|
||||
"detect-libc": "bin/detect-libc.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/immutable": {
|
||||
"version": "5.1.3",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.3.tgz",
|
||||
"integrity": "sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/inquirer": {
|
||||
"version": "12.9.1",
|
||||
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-12.9.1.tgz",
|
||||
"integrity": "sha512-G7uXAb9OiLcd+9jmA/7KKrItvFF00kKk/jb6CtG+Tm2zSPWfzzhyJwDhVCy+mBmE32o2zJnB5JnknIIv2Ft+AA==",
|
||||
"dependencies": {
|
||||
"@inquirer/core": "^10.1.15",
|
||||
"@inquirer/prompts": "^7.8.1",
|
||||
"@inquirer/type": "^3.0.8",
|
||||
"ansi-escapes": "^4.3.2",
|
||||
"mute-stream": "^2.0.0",
|
||||
"run-async": "^4.0.5",
|
||||
"rxjs": "^7.8.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-glob": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"braces": "^3.0.3",
|
||||
"picomatch": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mute-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz",
|
||||
"integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==",
|
||||
"engines": {
|
||||
"node": "^18.17.0 || >=20.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
||||
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 14.18.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/run-async": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/run-async/-/run-async-4.0.6.tgz",
|
||||
"integrity": "sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ==",
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rxjs": {
|
||||
"version": "7.8.2",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
||||
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.90.0",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.90.0.tgz",
|
||||
"integrity": "sha512-9GUyuksjw70uNpb1MTYWsH9MQHOHY6kwfnkafC24+7aOMZn9+rVMBxRbLvw756mrBFbIsFg6Xw9IkR2Fnn3k+Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.0",
|
||||
"immutable": "^5.0.2",
|
||||
"source-map-js": ">=0.6.2 <2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"sass": "sass.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@parcel/watcher": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
||||
},
|
||||
"node_modules/type-fest": {
|
||||
"version": "0.21.3",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
|
||||
"integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.10.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
|
||||
"integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yoctocolors-cjs": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz",
|
||||
"integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
Copyright (c) 2025 Simon Boudrias
|
||||
|
||||
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.
|
@ -0,0 +1,182 @@
|
||||
# `@inquirer/checkbox`
|
||||
|
||||
Simple interactive command line prompt to display a list of checkboxes (multi select).
|
||||
|
||||

|
||||
|
||||
# Special Thanks
|
||||
|
||||
<div align="center" markdown="1">
|
||||
|
||||
[](https://graphite.dev/?utm_source=npmjs&utm_medium=repo&utm_campaign=inquirerjs)<br>
|
||||
|
||||
### [Graphite is the AI developer productivity platform helping teams on GitHub ship higher quality software, faster](https://graphite.dev/?utm_source=npmjs&utm_medium=repo&utm_campaign=inquirerjs)
|
||||
|
||||
</div>
|
||||
|
||||
# Installation
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>npm</th>
|
||||
<th>yarn</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
```sh
|
||||
npm install @inquirer/prompts
|
||||
```
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
```sh
|
||||
yarn add @inquirer/prompts
|
||||
```
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colSpan="2" align="center">Or</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
```sh
|
||||
npm install @inquirer/checkbox
|
||||
```
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
```sh
|
||||
yarn add @inquirer/checkbox
|
||||
```
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
# Usage
|
||||
|
||||
```js
|
||||
import { checkbox, Separator } from '@inquirer/prompts';
|
||||
// Or
|
||||
// import checkbox, { Separator } from '@inquirer/checkbox';
|
||||
|
||||
const answer = await checkbox({
|
||||
message: 'Select a package manager',
|
||||
choices: [
|
||||
{ name: 'npm', value: 'npm' },
|
||||
{ name: 'yarn', value: 'yarn' },
|
||||
new Separator(),
|
||||
{ name: 'pnpm', value: 'pnpm', disabled: true },
|
||||
{
|
||||
name: 'pnpm',
|
||||
value: 'pnpm',
|
||||
disabled: '(pnpm is not available)',
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
| Property | Type | Required | Description |
|
||||
| --------- | --------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| message | `string` | yes | The question to ask |
|
||||
| choices | `Choice[]` | yes | List of the available choices. |
|
||||
| pageSize | `number` | no | By default, lists of choice longer than 7 will be paginated. Use this option to control how many choices will appear on the screen at once. |
|
||||
| loop | `boolean` | no | Defaults to `true`. When set to `false`, the cursor will be constrained to the top and bottom of the choice list without looping. |
|
||||
| required | `boolean` | no | When set to `true`, ensures at least one choice must be selected. |
|
||||
| validate | `async (Choice[]) => boolean \| string` | no | On submit, validate the choices. When returning a string, it'll be used as the error message displayed to the user. Note: returning a rejected promise, we'll assume a code error happened and crash. |
|
||||
| shortcuts | [See Shortcuts](#Shortcuts) | no | Customize shortcut keys for `all` and `invert`. |
|
||||
| theme | [See Theming](#Theming) | no | Customize look of the prompt. |
|
||||
|
||||
`Separator` objects can be used in the `choices` array to render non-selectable lines in the choice list. By default it'll render a line, but you can provide the text as argument (`new Separator('-- Dependencies --')`). This option is often used to add labels to groups within long list of options.
|
||||
|
||||
### `Choice` object
|
||||
|
||||
The `Choice` object is typed as
|
||||
|
||||
```ts
|
||||
type Choice<Value> = {
|
||||
value: Value;
|
||||
name?: string;
|
||||
description?: string;
|
||||
short?: string;
|
||||
checked?: boolean;
|
||||
disabled?: boolean | string;
|
||||
};
|
||||
```
|
||||
|
||||
Here's each property:
|
||||
|
||||
- `value`: The value is what will be returned by `await checkbox()`.
|
||||
- `name`: This is the string displayed in the choice list.
|
||||
- `description`: Option for a longer description string that'll appear under the list when the cursor highlight a given choice.
|
||||
- `short`: Once the prompt is done (press enter), we'll use `short` if defined to render next to the question. By default we'll use `name`.
|
||||
- `checked`: If `true`, the option will be checked by default.
|
||||
- `disabled`: Disallow the option from being selected. If `disabled` is a string, it'll be used as a help tip explaining why the choice isn't available.
|
||||
|
||||
Also note the `choices` array can contain `Separator`s to help organize long lists.
|
||||
|
||||
`choices` can also be an array of string, in which case the string will be used both as the `value` and the `name`.
|
||||
|
||||
## Shortcuts
|
||||
|
||||
You can customize the shortcut keys for `all` and `invert` or disable them by setting them to `null`.
|
||||
|
||||
```ts
|
||||
type Shortcuts = {
|
||||
all?: string | null; // default: 'a'
|
||||
invert?: string | null; // default: 'i'
|
||||
};
|
||||
```
|
||||
|
||||
## Theming
|
||||
|
||||
You can theme a prompt by passing a `theme` object option. The theme object only need to includes the keys you wish to modify, we'll fallback on the defaults for the rest.
|
||||
|
||||
```ts
|
||||
type Theme = {
|
||||
prefix: string | { idle: string; done: string };
|
||||
spinner: {
|
||||
interval: number;
|
||||
frames: string[];
|
||||
};
|
||||
style: {
|
||||
answer: (text: string) => string;
|
||||
message: (text: string, status: 'idle' | 'done' | 'loading') => string;
|
||||
error: (text: string) => string;
|
||||
defaultAnswer: (text: string) => string;
|
||||
help: (text: string) => string;
|
||||
highlight: (text: string) => string;
|
||||
key: (text: string) => string;
|
||||
disabledChoice: (text: string) => string;
|
||||
description: (text: string) => string;
|
||||
renderSelectedChoices: <T>(
|
||||
selectedChoices: ReadonlyArray<Choice<T>>,
|
||||
allChoices: ReadonlyArray<Choice<T> | Separator>,
|
||||
) => string;
|
||||
};
|
||||
icon: {
|
||||
checked: string;
|
||||
unchecked: string;
|
||||
cursor: string;
|
||||
};
|
||||
helpMode: 'always' | 'never' | 'auto';
|
||||
};
|
||||
```
|
||||
|
||||
### `theme.helpMode`
|
||||
|
||||
- `auto` (default): Hide the help tips after an interaction occurs. The scroll tip will hide after any interactions, the selection tip will hide as soon as a first selection is done.
|
||||
- `always`: The help tips will always show and never hide.
|
||||
- `never`: The help tips will never show.
|
||||
|
||||
# License
|
||||
|
||||
Copyright (c) 2023 Simon Boudrias (twitter: [@vaxilart](https://twitter.com/Vaxilart))<br/>
|
||||
Licensed under the MIT license.
|
@ -0,0 +1,52 @@
|
||||
import { Separator, type Theme } from '@inquirer/core';
|
||||
import type { PartialDeep } from '@inquirer/type';
|
||||
type CheckboxTheme = {
|
||||
icon: {
|
||||
checked: string;
|
||||
unchecked: string;
|
||||
cursor: string;
|
||||
};
|
||||
style: {
|
||||
disabledChoice: (text: string) => string;
|
||||
renderSelectedChoices: <T>(selectedChoices: ReadonlyArray<NormalizedChoice<T>>, allChoices: ReadonlyArray<NormalizedChoice<T> | Separator>) => string;
|
||||
description: (text: string) => string;
|
||||
};
|
||||
helpMode: 'always' | 'never' | 'auto';
|
||||
};
|
||||
type CheckboxShortcuts = {
|
||||
all?: string | null;
|
||||
invert?: string | null;
|
||||
};
|
||||
type Choice<Value> = {
|
||||
value: Value;
|
||||
name?: string;
|
||||
description?: string;
|
||||
short?: string;
|
||||
disabled?: boolean | string;
|
||||
checked?: boolean;
|
||||
type?: never;
|
||||
};
|
||||
type NormalizedChoice<Value> = {
|
||||
value: Value;
|
||||
name: string;
|
||||
description?: string;
|
||||
short: string;
|
||||
disabled: boolean | string;
|
||||
checked: boolean;
|
||||
};
|
||||
declare const _default: <Value>(config: {
|
||||
message: string;
|
||||
prefix?: string | undefined;
|
||||
pageSize?: number | undefined;
|
||||
instructions?: string | boolean | undefined;
|
||||
choices: readonly (string | Separator)[] | readonly (Separator | Choice<Value>)[];
|
||||
loop?: boolean | undefined;
|
||||
required?: boolean | undefined;
|
||||
validate?: ((choices: readonly Choice<Value>[]) => boolean | string | Promise<string | boolean>) | undefined;
|
||||
theme?: PartialDeep<Theme<CheckboxTheme>> | undefined;
|
||||
shortcuts?: CheckboxShortcuts | undefined;
|
||||
}, context?: import("@inquirer/type").Context) => Promise<Value[]> & {
|
||||
cancel: () => void;
|
||||
};
|
||||
export default _default;
|
||||
export { Separator } from '@inquirer/core';
|
@ -0,0 +1,207 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.Separator = void 0;
|
||||
const core_1 = require("@inquirer/core");
|
||||
const yoctocolors_cjs_1 = __importDefault(require("yoctocolors-cjs"));
|
||||
const figures_1 = __importDefault(require("@inquirer/figures"));
|
||||
const ansi_escapes_1 = __importDefault(require("ansi-escapes"));
|
||||
const checkboxTheme = {
|
||||
icon: {
|
||||
checked: yoctocolors_cjs_1.default.green(figures_1.default.circleFilled),
|
||||
unchecked: figures_1.default.circle,
|
||||
cursor: figures_1.default.pointer,
|
||||
},
|
||||
style: {
|
||||
disabledChoice: (text) => yoctocolors_cjs_1.default.dim(`- ${text}`),
|
||||
renderSelectedChoices: (selectedChoices) => selectedChoices.map((choice) => choice.short).join(', '),
|
||||
description: (text) => yoctocolors_cjs_1.default.cyan(text),
|
||||
},
|
||||
helpMode: 'auto',
|
||||
};
|
||||
function isSelectable(item) {
|
||||
return !core_1.Separator.isSeparator(item) && !item.disabled;
|
||||
}
|
||||
function isChecked(item) {
|
||||
return isSelectable(item) && Boolean(item.checked);
|
||||
}
|
||||
function toggle(item) {
|
||||
return isSelectable(item) ? { ...item, checked: !item.checked } : item;
|
||||
}
|
||||
function check(checked) {
|
||||
return function (item) {
|
||||
return isSelectable(item) ? { ...item, checked } : item;
|
||||
};
|
||||
}
|
||||
function normalizeChoices(choices) {
|
||||
return choices.map((choice) => {
|
||||
if (core_1.Separator.isSeparator(choice))
|
||||
return choice;
|
||||
if (typeof choice === 'string') {
|
||||
return {
|
||||
value: choice,
|
||||
name: choice,
|
||||
short: choice,
|
||||
disabled: false,
|
||||
checked: false,
|
||||
};
|
||||
}
|
||||
const name = choice.name ?? String(choice.value);
|
||||
const normalizedChoice = {
|
||||
value: choice.value,
|
||||
name,
|
||||
short: choice.short ?? name,
|
||||
disabled: choice.disabled ?? false,
|
||||
checked: choice.checked ?? false,
|
||||
};
|
||||
if (choice.description) {
|
||||
normalizedChoice.description = choice.description;
|
||||
}
|
||||
return normalizedChoice;
|
||||
});
|
||||
}
|
||||
exports.default = (0, core_1.createPrompt)((config, done) => {
|
||||
const { instructions, pageSize = 7, loop = true, required, validate = () => true, } = config;
|
||||
const shortcuts = { all: 'a', invert: 'i', ...config.shortcuts };
|
||||
const theme = (0, core_1.makeTheme)(checkboxTheme, config.theme);
|
||||
const firstRender = (0, core_1.useRef)(true);
|
||||
const [status, setStatus] = (0, core_1.useState)('idle');
|
||||
const prefix = (0, core_1.usePrefix)({ status, theme });
|
||||
const [items, setItems] = (0, core_1.useState)(normalizeChoices(config.choices));
|
||||
const bounds = (0, core_1.useMemo)(() => {
|
||||
const first = items.findIndex(isSelectable);
|
||||
const last = items.findLastIndex(isSelectable);
|
||||
if (first === -1) {
|
||||
throw new core_1.ValidationError('[checkbox prompt] No selectable choices. All choices are disabled.');
|
||||
}
|
||||
return { first, last };
|
||||
}, [items]);
|
||||
const [active, setActive] = (0, core_1.useState)(bounds.first);
|
||||
const [showHelpTip, setShowHelpTip] = (0, core_1.useState)(true);
|
||||
const [errorMsg, setError] = (0, core_1.useState)();
|
||||
(0, core_1.useKeypress)(async (key) => {
|
||||
if ((0, core_1.isEnterKey)(key)) {
|
||||
const selection = items.filter(isChecked);
|
||||
const isValid = await validate([...selection]);
|
||||
if (required && !items.some(isChecked)) {
|
||||
setError('At least one choice must be selected');
|
||||
}
|
||||
else if (isValid === true) {
|
||||
setStatus('done');
|
||||
done(selection.map((choice) => choice.value));
|
||||
}
|
||||
else {
|
||||
setError(isValid || 'You must select a valid value');
|
||||
}
|
||||
}
|
||||
else if ((0, core_1.isUpKey)(key) || (0, core_1.isDownKey)(key)) {
|
||||
if (loop ||
|
||||
((0, core_1.isUpKey)(key) && active !== bounds.first) ||
|
||||
((0, core_1.isDownKey)(key) && active !== bounds.last)) {
|
||||
const offset = (0, core_1.isUpKey)(key) ? -1 : 1;
|
||||
let next = active;
|
||||
do {
|
||||
next = (next + offset + items.length) % items.length;
|
||||
} while (!isSelectable(items[next]));
|
||||
setActive(next);
|
||||
}
|
||||
}
|
||||
else if ((0, core_1.isSpaceKey)(key)) {
|
||||
setError(undefined);
|
||||
setShowHelpTip(false);
|
||||
setItems(items.map((choice, i) => (i === active ? toggle(choice) : choice)));
|
||||
}
|
||||
else if (key.name === shortcuts.all) {
|
||||
const selectAll = items.some((choice) => isSelectable(choice) && !choice.checked);
|
||||
setItems(items.map(check(selectAll)));
|
||||
}
|
||||
else if (key.name === shortcuts.invert) {
|
||||
setItems(items.map(toggle));
|
||||
}
|
||||
else if ((0, core_1.isNumberKey)(key)) {
|
||||
const selectedIndex = Number(key.name) - 1;
|
||||
// Find the nth item (ignoring separators)
|
||||
let selectableIndex = -1;
|
||||
const position = items.findIndex((item) => {
|
||||
if (core_1.Separator.isSeparator(item))
|
||||
return false;
|
||||
selectableIndex++;
|
||||
return selectableIndex === selectedIndex;
|
||||
});
|
||||
const selectedItem = items[position];
|
||||
if (selectedItem && isSelectable(selectedItem)) {
|
||||
setActive(position);
|
||||
setItems(items.map((choice, i) => (i === position ? toggle(choice) : choice)));
|
||||
}
|
||||
}
|
||||
});
|
||||
const message = theme.style.message(config.message, status);
|
||||
let description;
|
||||
const page = (0, core_1.usePagination)({
|
||||
items,
|
||||
active,
|
||||
renderItem({ item, isActive }) {
|
||||
if (core_1.Separator.isSeparator(item)) {
|
||||
return ` ${item.separator}`;
|
||||
}
|
||||
if (item.disabled) {
|
||||
const disabledLabel = typeof item.disabled === 'string' ? item.disabled : '(disabled)';
|
||||
return theme.style.disabledChoice(`${item.name} ${disabledLabel}`);
|
||||
}
|
||||
if (isActive) {
|
||||
description = item.description;
|
||||
}
|
||||
const checkbox = item.checked ? theme.icon.checked : theme.icon.unchecked;
|
||||
const color = isActive ? theme.style.highlight : (x) => x;
|
||||
const cursor = isActive ? theme.icon.cursor : ' ';
|
||||
return color(`${cursor}${checkbox} ${item.name}`);
|
||||
},
|
||||
pageSize,
|
||||
loop,
|
||||
});
|
||||
if (status === 'done') {
|
||||
const selection = items.filter(isChecked);
|
||||
const answer = theme.style.answer(theme.style.renderSelectedChoices(selection, items));
|
||||
return `${prefix} ${message} ${answer}`;
|
||||
}
|
||||
let helpTipTop = '';
|
||||
let helpTipBottom = '';
|
||||
if (theme.helpMode === 'always' ||
|
||||
(theme.helpMode === 'auto' &&
|
||||
showHelpTip &&
|
||||
(instructions === undefined || instructions))) {
|
||||
if (typeof instructions === 'string') {
|
||||
helpTipTop = instructions;
|
||||
}
|
||||
else {
|
||||
const keys = [
|
||||
`${theme.style.key('space')} to select`,
|
||||
shortcuts.all ? `${theme.style.key(shortcuts.all)} to toggle all` : '',
|
||||
shortcuts.invert
|
||||
? `${theme.style.key(shortcuts.invert)} to invert selection`
|
||||
: '',
|
||||
`and ${theme.style.key('enter')} to proceed`,
|
||||
];
|
||||
helpTipTop = ` (Press ${keys.filter((key) => key !== '').join(', ')})`;
|
||||
}
|
||||
if (items.length > pageSize &&
|
||||
(theme.helpMode === 'always' ||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
(theme.helpMode === 'auto' && firstRender.current))) {
|
||||
helpTipBottom = `\n${theme.style.help('(Use arrow keys to reveal more choices)')}`;
|
||||
firstRender.current = false;
|
||||
}
|
||||
}
|
||||
const choiceDescription = description
|
||||
? `\n${theme.style.description(description)}`
|
||||
: ``;
|
||||
let error = '';
|
||||
if (errorMsg) {
|
||||
error = `\n${theme.style.error(errorMsg)}`;
|
||||
}
|
||||
return `${prefix} ${message}${helpTipTop}\n${page}${helpTipBottom}${choiceDescription}${error}${ansi_escapes_1.default.cursorHide}`;
|
||||
});
|
||||
var core_2 = require("@inquirer/core");
|
||||
Object.defineProperty(exports, "Separator", { enumerable: true, get: function () { return core_2.Separator; } });
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "commonjs"
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
import { Separator, type Theme } from '@inquirer/core';
|
||||
import type { PartialDeep } from '@inquirer/type';
|
||||
type CheckboxTheme = {
|
||||
icon: {
|
||||
checked: string;
|
||||
unchecked: string;
|
||||
cursor: string;
|
||||
};
|
||||
style: {
|
||||
disabledChoice: (text: string) => string;
|
||||
renderSelectedChoices: <T>(selectedChoices: ReadonlyArray<NormalizedChoice<T>>, allChoices: ReadonlyArray<NormalizedChoice<T> | Separator>) => string;
|
||||
description: (text: string) => string;
|
||||
};
|
||||
helpMode: 'always' | 'never' | 'auto';
|
||||
};
|
||||
type CheckboxShortcuts = {
|
||||
all?: string | null;
|
||||
invert?: string | null;
|
||||
};
|
||||
type Choice<Value> = {
|
||||
value: Value;
|
||||
name?: string;
|
||||
description?: string;
|
||||
short?: string;
|
||||
disabled?: boolean | string;
|
||||
checked?: boolean;
|
||||
type?: never;
|
||||
};
|
||||
type NormalizedChoice<Value> = {
|
||||
value: Value;
|
||||
name: string;
|
||||
description?: string;
|
||||
short: string;
|
||||
disabled: boolean | string;
|
||||
checked: boolean;
|
||||
};
|
||||
declare const _default: <Value>(config: {
|
||||
message: string;
|
||||
prefix?: string | undefined;
|
||||
pageSize?: number | undefined;
|
||||
instructions?: string | boolean | undefined;
|
||||
choices: readonly (string | Separator)[] | readonly (Separator | Choice<Value>)[];
|
||||
loop?: boolean | undefined;
|
||||
required?: boolean | undefined;
|
||||
validate?: ((choices: readonly Choice<Value>[]) => boolean | string | Promise<string | boolean>) | undefined;
|
||||
theme?: PartialDeep<Theme<CheckboxTheme>> | undefined;
|
||||
shortcuts?: CheckboxShortcuts | undefined;
|
||||
}, context?: import("@inquirer/type").Context) => Promise<Value[]> & {
|
||||
cancel: () => void;
|
||||
};
|
||||
export default _default;
|
||||
export { Separator } from '@inquirer/core';
|
@ -0,0 +1,200 @@
|
||||
import { createPrompt, useState, useKeypress, usePrefix, usePagination, useRef, useMemo, makeTheme, isUpKey, isDownKey, isSpaceKey, isNumberKey, isEnterKey, ValidationError, Separator, } from '@inquirer/core';
|
||||
import colors from 'yoctocolors-cjs';
|
||||
import figures from '@inquirer/figures';
|
||||
import ansiEscapes from 'ansi-escapes';
|
||||
const checkboxTheme = {
|
||||
icon: {
|
||||
checked: colors.green(figures.circleFilled),
|
||||
unchecked: figures.circle,
|
||||
cursor: figures.pointer,
|
||||
},
|
||||
style: {
|
||||
disabledChoice: (text) => colors.dim(`- ${text}`),
|
||||
renderSelectedChoices: (selectedChoices) => selectedChoices.map((choice) => choice.short).join(', '),
|
||||
description: (text) => colors.cyan(text),
|
||||
},
|
||||
helpMode: 'auto',
|
||||
};
|
||||
function isSelectable(item) {
|
||||
return !Separator.isSeparator(item) && !item.disabled;
|
||||
}
|
||||
function isChecked(item) {
|
||||
return isSelectable(item) && Boolean(item.checked);
|
||||
}
|
||||
function toggle(item) {
|
||||
return isSelectable(item) ? { ...item, checked: !item.checked } : item;
|
||||
}
|
||||
function check(checked) {
|
||||
return function (item) {
|
||||
return isSelectable(item) ? { ...item, checked } : item;
|
||||
};
|
||||
}
|
||||
function normalizeChoices(choices) {
|
||||
return choices.map((choice) => {
|
||||
if (Separator.isSeparator(choice))
|
||||
return choice;
|
||||
if (typeof choice === 'string') {
|
||||
return {
|
||||
value: choice,
|
||||
name: choice,
|
||||
short: choice,
|
||||
disabled: false,
|
||||
checked: false,
|
||||
};
|
||||
}
|
||||
const name = choice.name ?? String(choice.value);
|
||||
const normalizedChoice = {
|
||||
value: choice.value,
|
||||
name,
|
||||
short: choice.short ?? name,
|
||||
disabled: choice.disabled ?? false,
|
||||
checked: choice.checked ?? false,
|
||||
};
|
||||
if (choice.description) {
|
||||
normalizedChoice.description = choice.description;
|
||||
}
|
||||
return normalizedChoice;
|
||||
});
|
||||
}
|
||||
export default createPrompt((config, done) => {
|
||||
const { instructions, pageSize = 7, loop = true, required, validate = () => true, } = config;
|
||||
const shortcuts = { all: 'a', invert: 'i', ...config.shortcuts };
|
||||
const theme = makeTheme(checkboxTheme, config.theme);
|
||||
const firstRender = useRef(true);
|
||||
const [status, setStatus] = useState('idle');
|
||||
const prefix = usePrefix({ status, theme });
|
||||
const [items, setItems] = useState(normalizeChoices(config.choices));
|
||||
const bounds = useMemo(() => {
|
||||
const first = items.findIndex(isSelectable);
|
||||
const last = items.findLastIndex(isSelectable);
|
||||
if (first === -1) {
|
||||
throw new ValidationError('[checkbox prompt] No selectable choices. All choices are disabled.');
|
||||
}
|
||||
return { first, last };
|
||||
}, [items]);
|
||||
const [active, setActive] = useState(bounds.first);
|
||||
const [showHelpTip, setShowHelpTip] = useState(true);
|
||||
const [errorMsg, setError] = useState();
|
||||
useKeypress(async (key) => {
|
||||
if (isEnterKey(key)) {
|
||||
const selection = items.filter(isChecked);
|
||||
const isValid = await validate([...selection]);
|
||||
if (required && !items.some(isChecked)) {
|
||||
setError('At least one choice must be selected');
|
||||
}
|
||||
else if (isValid === true) {
|
||||
setStatus('done');
|
||||
done(selection.map((choice) => choice.value));
|
||||
}
|
||||
else {
|
||||
setError(isValid || 'You must select a valid value');
|
||||
}
|
||||
}
|
||||
else if (isUpKey(key) || isDownKey(key)) {
|
||||
if (loop ||
|
||||
(isUpKey(key) && active !== bounds.first) ||
|
||||
(isDownKey(key) && active !== bounds.last)) {
|
||||
const offset = isUpKey(key) ? -1 : 1;
|
||||
let next = active;
|
||||
do {
|
||||
next = (next + offset + items.length) % items.length;
|
||||
} while (!isSelectable(items[next]));
|
||||
setActive(next);
|
||||
}
|
||||
}
|
||||
else if (isSpaceKey(key)) {
|
||||
setError(undefined);
|
||||
setShowHelpTip(false);
|
||||
setItems(items.map((choice, i) => (i === active ? toggle(choice) : choice)));
|
||||
}
|
||||
else if (key.name === shortcuts.all) {
|
||||
const selectAll = items.some((choice) => isSelectable(choice) && !choice.checked);
|
||||
setItems(items.map(check(selectAll)));
|
||||
}
|
||||
else if (key.name === shortcuts.invert) {
|
||||
setItems(items.map(toggle));
|
||||
}
|
||||
else if (isNumberKey(key)) {
|
||||
const selectedIndex = Number(key.name) - 1;
|
||||
// Find the nth item (ignoring separators)
|
||||
let selectableIndex = -1;
|
||||
const position = items.findIndex((item) => {
|
||||
if (Separator.isSeparator(item))
|
||||
return false;
|
||||
selectableIndex++;
|
||||
return selectableIndex === selectedIndex;
|
||||
});
|
||||
const selectedItem = items[position];
|
||||
if (selectedItem && isSelectable(selectedItem)) {
|
||||
setActive(position);
|
||||
setItems(items.map((choice, i) => (i === position ? toggle(choice) : choice)));
|
||||
}
|
||||
}
|
||||
});
|
||||
const message = theme.style.message(config.message, status);
|
||||
let description;
|
||||
const page = usePagination({
|
||||
items,
|
||||
active,
|
||||
renderItem({ item, isActive }) {
|
||||
if (Separator.isSeparator(item)) {
|
||||
return ` ${item.separator}`;
|
||||
}
|
||||
if (item.disabled) {
|
||||
const disabledLabel = typeof item.disabled === 'string' ? item.disabled : '(disabled)';
|
||||
return theme.style.disabledChoice(`${item.name} ${disabledLabel}`);
|
||||
}
|
||||
if (isActive) {
|
||||
description = item.description;
|
||||
}
|
||||
const checkbox = item.checked ? theme.icon.checked : theme.icon.unchecked;
|
||||
const color = isActive ? theme.style.highlight : (x) => x;
|
||||
const cursor = isActive ? theme.icon.cursor : ' ';
|
||||
return color(`${cursor}${checkbox} ${item.name}`);
|
||||
},
|
||||
pageSize,
|
||||
loop,
|
||||
});
|
||||
if (status === 'done') {
|
||||
const selection = items.filter(isChecked);
|
||||
const answer = theme.style.answer(theme.style.renderSelectedChoices(selection, items));
|
||||
return `${prefix} ${message} ${answer}`;
|
||||
}
|
||||
let helpTipTop = '';
|
||||
let helpTipBottom = '';
|
||||
if (theme.helpMode === 'always' ||
|
||||
(theme.helpMode === 'auto' &&
|
||||
showHelpTip &&
|
||||
(instructions === undefined || instructions))) {
|
||||
if (typeof instructions === 'string') {
|
||||
helpTipTop = instructions;
|
||||
}
|
||||
else {
|
||||
const keys = [
|
||||
`${theme.style.key('space')} to select`,
|
||||
shortcuts.all ? `${theme.style.key(shortcuts.all)} to toggle all` : '',
|
||||
shortcuts.invert
|
||||
? `${theme.style.key(shortcuts.invert)} to invert selection`
|
||||
: '',
|
||||
`and ${theme.style.key('enter')} to proceed`,
|
||||
];
|
||||
helpTipTop = ` (Press ${keys.filter((key) => key !== '').join(', ')})`;
|
||||
}
|
||||
if (items.length > pageSize &&
|
||||
(theme.helpMode === 'always' ||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
(theme.helpMode === 'auto' && firstRender.current))) {
|
||||
helpTipBottom = `\n${theme.style.help('(Use arrow keys to reveal more choices)')}`;
|
||||
firstRender.current = false;
|
||||
}
|
||||
}
|
||||
const choiceDescription = description
|
||||
? `\n${theme.style.description(description)}`
|
||||
: ``;
|
||||
let error = '';
|
||||
if (errorMsg) {
|
||||
error = `\n${theme.style.error(errorMsg)}`;
|
||||
}
|
||||
return `${prefix} ${message}${helpTipTop}\n${page}${helpTipBottom}${choiceDescription}${error}${ansiEscapes.cursorHide}`;
|
||||
});
|
||||
export { Separator } from '@inquirer/core';
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
@ -0,0 +1,112 @@
|
||||
{
|
||||
"name": "@inquirer/checkbox",
|
||||
"version": "4.2.0",
|
||||
"description": "Inquirer checkbox prompt",
|
||||
"keywords": [
|
||||
"answer",
|
||||
"answers",
|
||||
"ask",
|
||||
"base",
|
||||
"cli",
|
||||
"command",
|
||||
"command-line",
|
||||
"confirm",
|
||||
"enquirer",
|
||||
"generate",
|
||||
"generator",
|
||||
"hyper",
|
||||
"input",
|
||||
"inquire",
|
||||
"inquirer",
|
||||
"interface",
|
||||
"iterm",
|
||||
"javascript",
|
||||
"menu",
|
||||
"node",
|
||||
"nodejs",
|
||||
"prompt",
|
||||
"promptly",
|
||||
"prompts",
|
||||
"question",
|
||||
"readline",
|
||||
"scaffold",
|
||||
"scaffolder",
|
||||
"scaffolding",
|
||||
"stdin",
|
||||
"stdout",
|
||||
"terminal",
|
||||
"tty",
|
||||
"ui",
|
||||
"yeoman",
|
||||
"yo",
|
||||
"zsh"
|
||||
],
|
||||
"homepage": "https://github.com/SBoudrias/Inquirer.js/blob/main/packages/checkbox/README.md",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/SBoudrias/Inquirer.js.git"
|
||||
},
|
||||
"license": "MIT",
|
||||
"author": "Simon Boudrias <admin@simonboudrias.com>",
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./package.json": "./package.json",
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./dist/esm/index.d.ts",
|
||||
"default": "./dist/esm/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/commonjs/index.d.ts",
|
||||
"default": "./dist/commonjs/index.js"
|
||||
}
|
||||
}
|
||||
},
|
||||
"main": "./dist/commonjs/index.js",
|
||||
"module": "./dist/esm/index.js",
|
||||
"types": "./dist/commonjs/index.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"attw": "attw --pack",
|
||||
"tsc": "tshy"
|
||||
},
|
||||
"dependencies": {
|
||||
"@inquirer/core": "^10.1.15",
|
||||
"@inquirer/figures": "^1.0.13",
|
||||
"@inquirer/type": "^3.0.8",
|
||||
"ansi-escapes": "^4.3.2",
|
||||
"yoctocolors-cjs": "^2.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@arethetypeswrong/cli": "^0.18.2",
|
||||
"@inquirer/testing": "^2.1.49",
|
||||
"tshy": "^3.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"tshy": {
|
||||
"exclude": [
|
||||
"src/**/*.test.ts"
|
||||
],
|
||||
"exports": {
|
||||
"./package.json": "./package.json",
|
||||
".": "./src/index.ts"
|
||||
}
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"gitHead": "c1a755fe8b50377b685ea5951e0794985ce8d356"
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
Copyright (c) 2025 Simon Boudrias
|
||||
|
||||
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.
|
@ -0,0 +1,102 @@
|
||||
# `@inquirer/confirm`
|
||||
|
||||
Simple interactive command line prompt to gather boolean input from users.
|
||||
|
||||

|
||||
|
||||
# Special Thanks
|
||||
|
||||
<div align="center" markdown="1">
|
||||
|
||||
[](https://graphite.dev/?utm_source=npmjs&utm_medium=repo&utm_campaign=inquirerjs)<br>
|
||||
|
||||
### [Graphite is the AI developer productivity platform helping teams on GitHub ship higher quality software, faster](https://graphite.dev/?utm_source=npmjs&utm_medium=repo&utm_campaign=inquirerjs)
|
||||
|
||||
</div>
|
||||
|
||||
# Installation
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>npm</th>
|
||||
<th>yarn</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
```sh
|
||||
npm install @inquirer/prompts
|
||||
```
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
```sh
|
||||
yarn add @inquirer/prompts
|
||||
```
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colSpan="2" align="center">Or</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
```sh
|
||||
npm install @inquirer/confirm
|
||||
```
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
```sh
|
||||
yarn add @inquirer/confirm
|
||||
```
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
# Usage
|
||||
|
||||
```js
|
||||
import { confirm } from '@inquirer/prompts';
|
||||
// Or
|
||||
// import confirm from '@inquirer/confirm';
|
||||
|
||||
const answer = await confirm({ message: 'Continue?' });
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
| Property | Type | Required | Description |
|
||||
| ----------- | ----------------------- | -------- | ------------------------------------------------------- |
|
||||
| message | `string` | yes | The question to ask |
|
||||
| default | `boolean` | no | Default answer (true or false) |
|
||||
| transformer | `(boolean) => string` | no | Transform the prompt printed message to a custom string |
|
||||
| theme | [See Theming](#Theming) | no | Customize look of the prompt. |
|
||||
|
||||
## Theming
|
||||
|
||||
You can theme a prompt by passing a `theme` object option. The theme object only need to includes the keys you wish to modify, we'll fallback on the defaults for the rest.
|
||||
|
||||
```ts
|
||||
type Theme = {
|
||||
prefix: string | { idle: string; done: string };
|
||||
spinner: {
|
||||
interval: number;
|
||||
frames: string[];
|
||||
};
|
||||
style: {
|
||||
answer: (text: string) => string;
|
||||
message: (text: string, status: 'idle' | 'done' | 'loading') => string;
|
||||
defaultAnswer: (text: string) => string;
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
# License
|
||||
|
||||
Copyright (c) 2023 Simon Boudrias (twitter: [@vaxilart](https://twitter.com/Vaxilart))<br/>
|
||||
Licensed under the MIT license.
|
@ -0,0 +1,10 @@
|
||||
import { type Theme } from '@inquirer/core';
|
||||
import type { PartialDeep } from '@inquirer/type';
|
||||
type ConfirmConfig = {
|
||||
message: string;
|
||||
default?: boolean;
|
||||
transformer?: (value: boolean) => string;
|
||||
theme?: PartialDeep<Theme>;
|
||||
};
|
||||
declare const _default: import("@inquirer/type").Prompt<boolean, ConfirmConfig>;
|
||||
export default _default;
|
@ -0,0 +1,48 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const core_1 = require("@inquirer/core");
|
||||
function getBooleanValue(value, defaultValue) {
|
||||
let answer = defaultValue !== false;
|
||||
if (/^(y|yes)/i.test(value))
|
||||
answer = true;
|
||||
else if (/^(n|no)/i.test(value))
|
||||
answer = false;
|
||||
return answer;
|
||||
}
|
||||
function boolToString(value) {
|
||||
return value ? 'Yes' : 'No';
|
||||
}
|
||||
exports.default = (0, core_1.createPrompt)((config, done) => {
|
||||
const { transformer = boolToString } = config;
|
||||
const [status, setStatus] = (0, core_1.useState)('idle');
|
||||
const [value, setValue] = (0, core_1.useState)('');
|
||||
const theme = (0, core_1.makeTheme)(config.theme);
|
||||
const prefix = (0, core_1.usePrefix)({ status, theme });
|
||||
(0, core_1.useKeypress)((key, rl) => {
|
||||
if ((0, core_1.isEnterKey)(key)) {
|
||||
const answer = getBooleanValue(value, config.default);
|
||||
setValue(transformer(answer));
|
||||
setStatus('done');
|
||||
done(answer);
|
||||
}
|
||||
else if (key.name === 'tab') {
|
||||
const answer = boolToString(!getBooleanValue(value, config.default));
|
||||
rl.clearLine(0); // Remove the tab character.
|
||||
rl.write(answer);
|
||||
setValue(answer);
|
||||
}
|
||||
else {
|
||||
setValue(rl.line);
|
||||
}
|
||||
});
|
||||
let formattedValue = value;
|
||||
let defaultValue = '';
|
||||
if (status === 'done') {
|
||||
formattedValue = theme.style.answer(value);
|
||||
}
|
||||
else {
|
||||
defaultValue = ` ${theme.style.defaultAnswer(config.default === false ? 'y/N' : 'Y/n')}`;
|
||||
}
|
||||
const message = theme.style.message(config.message, status);
|
||||
return `${prefix} ${message}${defaultValue} ${formattedValue}`;
|
||||
});
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "commonjs"
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
import { type Theme } from '@inquirer/core';
|
||||
import type { PartialDeep } from '@inquirer/type';
|
||||
type ConfirmConfig = {
|
||||
message: string;
|
||||
default?: boolean;
|
||||
transformer?: (value: boolean) => string;
|
||||
theme?: PartialDeep<Theme>;
|
||||
};
|
||||
declare const _default: import("@inquirer/type").Prompt<boolean, ConfirmConfig>;
|
||||
export default _default;
|
@ -0,0 +1,46 @@
|
||||
import { createPrompt, useState, useKeypress, isEnterKey, usePrefix, makeTheme, } from '@inquirer/core';
|
||||
function getBooleanValue(value, defaultValue) {
|
||||
let answer = defaultValue !== false;
|
||||
if (/^(y|yes)/i.test(value))
|
||||
answer = true;
|
||||
else if (/^(n|no)/i.test(value))
|
||||
answer = false;
|
||||
return answer;
|
||||
}
|
||||
function boolToString(value) {
|
||||
return value ? 'Yes' : 'No';
|
||||
}
|
||||
export default createPrompt((config, done) => {
|
||||
const { transformer = boolToString } = config;
|
||||
const [status, setStatus] = useState('idle');
|
||||
const [value, setValue] = useState('');
|
||||
const theme = makeTheme(config.theme);
|
||||
const prefix = usePrefix({ status, theme });
|
||||
useKeypress((key, rl) => {
|
||||
if (isEnterKey(key)) {
|
||||
const answer = getBooleanValue(value, config.default);
|
||||
setValue(transformer(answer));
|
||||
setStatus('done');
|
||||
done(answer);
|
||||
}
|
||||
else if (key.name === 'tab') {
|
||||
const answer = boolToString(!getBooleanValue(value, config.default));
|
||||
rl.clearLine(0); // Remove the tab character.
|
||||
rl.write(answer);
|
||||
setValue(answer);
|
||||
}
|
||||
else {
|
||||
setValue(rl.line);
|
||||
}
|
||||
});
|
||||
let formattedValue = value;
|
||||
let defaultValue = '';
|
||||
if (status === 'done') {
|
||||
formattedValue = theme.style.answer(value);
|
||||
}
|
||||
else {
|
||||
defaultValue = ` ${theme.style.defaultAnswer(config.default === false ? 'y/N' : 'Y/n')}`;
|
||||
}
|
||||
const message = theme.style.message(config.message, status);
|
||||
return `${prefix} ${message}${defaultValue} ${formattedValue}`;
|
||||
});
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
@ -0,0 +1,109 @@
|
||||
{
|
||||
"name": "@inquirer/confirm",
|
||||
"version": "5.1.14",
|
||||
"description": "Inquirer confirm prompt",
|
||||
"keywords": [
|
||||
"answer",
|
||||
"answers",
|
||||
"ask",
|
||||
"base",
|
||||
"cli",
|
||||
"command",
|
||||
"command-line",
|
||||
"confirm",
|
||||
"enquirer",
|
||||
"generate",
|
||||
"generator",
|
||||
"hyper",
|
||||
"input",
|
||||
"inquire",
|
||||
"inquirer",
|
||||
"interface",
|
||||
"iterm",
|
||||
"javascript",
|
||||
"menu",
|
||||
"node",
|
||||
"nodejs",
|
||||
"prompt",
|
||||
"promptly",
|
||||
"prompts",
|
||||
"question",
|
||||
"readline",
|
||||
"scaffold",
|
||||
"scaffolder",
|
||||
"scaffolding",
|
||||
"stdin",
|
||||
"stdout",
|
||||
"terminal",
|
||||
"tty",
|
||||
"ui",
|
||||
"yeoman",
|
||||
"yo",
|
||||
"zsh"
|
||||
],
|
||||
"homepage": "https://github.com/SBoudrias/Inquirer.js/blob/main/packages/confirm/README.md",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/SBoudrias/Inquirer.js.git"
|
||||
},
|
||||
"license": "MIT",
|
||||
"author": "Simon Boudrias <admin@simonboudrias.com>",
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./package.json": "./package.json",
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./dist/esm/index.d.ts",
|
||||
"default": "./dist/esm/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/commonjs/index.d.ts",
|
||||
"default": "./dist/commonjs/index.js"
|
||||
}
|
||||
}
|
||||
},
|
||||
"main": "./dist/commonjs/index.js",
|
||||
"module": "./dist/esm/index.js",
|
||||
"types": "./dist/commonjs/index.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"attw": "attw --pack",
|
||||
"tsc": "tshy"
|
||||
},
|
||||
"dependencies": {
|
||||
"@inquirer/core": "^10.1.15",
|
||||
"@inquirer/type": "^3.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@arethetypeswrong/cli": "^0.18.2",
|
||||
"@inquirer/testing": "^2.1.49",
|
||||
"tshy": "^3.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"tshy": {
|
||||
"exclude": [
|
||||
"src/**/*.test.ts"
|
||||
],
|
||||
"exports": {
|
||||
"./package.json": "./package.json",
|
||||
".": "./src/index.ts"
|
||||
}
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"gitHead": "c1a755fe8b50377b685ea5951e0794985ce8d356"
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
Copyright (c) 2025 Simon Boudrias
|
||||
|
||||
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.
|
@ -0,0 +1,13 @@
|
||||
export * from './lib/key.ts';
|
||||
export * from './lib/errors.ts';
|
||||
export { usePrefix } from './lib/use-prefix.ts';
|
||||
export { useState } from './lib/use-state.ts';
|
||||
export { useEffect } from './lib/use-effect.ts';
|
||||
export { useMemo } from './lib/use-memo.ts';
|
||||
export { useRef } from './lib/use-ref.ts';
|
||||
export { useKeypress } from './lib/use-keypress.ts';
|
||||
export { makeTheme } from './lib/make-theme.ts';
|
||||
export type { Theme, Status } from './lib/theme.ts';
|
||||
export { usePagination } from './lib/pagination/use-pagination.ts';
|
||||
export { createPrompt } from './lib/create-prompt.ts';
|
||||
export { Separator } from './lib/Separator.ts';
|
@ -0,0 +1,39 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
||||
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.Separator = exports.createPrompt = exports.usePagination = exports.makeTheme = exports.useKeypress = exports.useRef = exports.useMemo = exports.useEffect = exports.useState = exports.usePrefix = void 0;
|
||||
__exportStar(require("./lib/key.js"), exports);
|
||||
__exportStar(require("./lib/errors.js"), exports);
|
||||
var use_prefix_ts_1 = require("./lib/use-prefix.js");
|
||||
Object.defineProperty(exports, "usePrefix", { enumerable: true, get: function () { return use_prefix_ts_1.usePrefix; } });
|
||||
var use_state_ts_1 = require("./lib/use-state.js");
|
||||
Object.defineProperty(exports, "useState", { enumerable: true, get: function () { return use_state_ts_1.useState; } });
|
||||
var use_effect_ts_1 = require("./lib/use-effect.js");
|
||||
Object.defineProperty(exports, "useEffect", { enumerable: true, get: function () { return use_effect_ts_1.useEffect; } });
|
||||
var use_memo_ts_1 = require("./lib/use-memo.js");
|
||||
Object.defineProperty(exports, "useMemo", { enumerable: true, get: function () { return use_memo_ts_1.useMemo; } });
|
||||
var use_ref_ts_1 = require("./lib/use-ref.js");
|
||||
Object.defineProperty(exports, "useRef", { enumerable: true, get: function () { return use_ref_ts_1.useRef; } });
|
||||
var use_keypress_ts_1 = require("./lib/use-keypress.js");
|
||||
Object.defineProperty(exports, "useKeypress", { enumerable: true, get: function () { return use_keypress_ts_1.useKeypress; } });
|
||||
var make_theme_ts_1 = require("./lib/make-theme.js");
|
||||
Object.defineProperty(exports, "makeTheme", { enumerable: true, get: function () { return make_theme_ts_1.makeTheme; } });
|
||||
var use_pagination_ts_1 = require("./lib/pagination/use-pagination.js");
|
||||
Object.defineProperty(exports, "usePagination", { enumerable: true, get: function () { return use_pagination_ts_1.usePagination; } });
|
||||
var create_prompt_ts_1 = require("./lib/create-prompt.js");
|
||||
Object.defineProperty(exports, "createPrompt", { enumerable: true, get: function () { return create_prompt_ts_1.createPrompt; } });
|
||||
var Separator_ts_1 = require("./lib/Separator.js");
|
||||
Object.defineProperty(exports, "Separator", { enumerable: true, get: function () { return Separator_ts_1.Separator; } });
|
@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Separator object
|
||||
* Used to space/separate choices group
|
||||
*/
|
||||
export declare class Separator {
|
||||
readonly separator: string;
|
||||
readonly type: string;
|
||||
constructor(separator?: string);
|
||||
static isSeparator(choice: unknown): choice is Separator;
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.Separator = void 0;
|
||||
const yoctocolors_cjs_1 = __importDefault(require("yoctocolors-cjs"));
|
||||
const figures_1 = __importDefault(require("@inquirer/figures"));
|
||||
/**
|
||||
* Separator object
|
||||
* Used to space/separate choices group
|
||||
*/
|
||||
class Separator {
|
||||
separator = yoctocolors_cjs_1.default.dim(Array.from({ length: 15 }).join(figures_1.default.line));
|
||||
type = 'separator';
|
||||
constructor(separator) {
|
||||
if (separator) {
|
||||
this.separator = separator;
|
||||
}
|
||||
}
|
||||
static isSeparator(choice) {
|
||||
return Boolean(choice &&
|
||||
typeof choice === 'object' &&
|
||||
'type' in choice &&
|
||||
choice.type === 'separator');
|
||||
}
|
||||
}
|
||||
exports.Separator = Separator;
|
@ -0,0 +1,4 @@
|
||||
import { type Prompt, type Prettify } from '@inquirer/type';
|
||||
type ViewFunction<Value, Config> = (config: Prettify<Config>, done: (value: Value) => void) => string | [string, string | undefined];
|
||||
export declare function createPrompt<Value, Config>(view: ViewFunction<Value, Config>): Prompt<Value, Config>;
|
||||
export {};
|
@ -0,0 +1,156 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createPrompt = createPrompt;
|
||||
const readline = __importStar(require("node:readline"));
|
||||
const node_async_hooks_1 = require("node:async_hooks");
|
||||
const mute_stream_1 = __importDefault(require("mute-stream"));
|
||||
const signal_exit_1 = require("signal-exit");
|
||||
const screen_manager_ts_1 = __importDefault(require("./screen-manager.js"));
|
||||
const promise_polyfill_ts_1 = require("./promise-polyfill.js");
|
||||
const hook_engine_ts_1 = require("./hook-engine.js");
|
||||
const errors_ts_1 = require("./errors.js");
|
||||
function getCallSites() {
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
const _prepareStackTrace = Error.prepareStackTrace;
|
||||
let result = [];
|
||||
try {
|
||||
Error.prepareStackTrace = (_, callSites) => {
|
||||
const callSitesWithoutCurrent = callSites.slice(1);
|
||||
result = callSitesWithoutCurrent;
|
||||
return callSitesWithoutCurrent;
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
new Error().stack;
|
||||
}
|
||||
catch {
|
||||
// An error will occur if the Node flag --frozen-intrinsics is used.
|
||||
// https://nodejs.org/api/cli.html#--frozen-intrinsics
|
||||
return result;
|
||||
}
|
||||
Error.prepareStackTrace = _prepareStackTrace;
|
||||
return result;
|
||||
}
|
||||
function createPrompt(view) {
|
||||
const callSites = getCallSites();
|
||||
const prompt = (config, context = {}) => {
|
||||
// Default `input` to stdin
|
||||
const { input = process.stdin, signal } = context;
|
||||
const cleanups = new Set();
|
||||
// Add mute capabilities to the output
|
||||
const output = new mute_stream_1.default();
|
||||
output.pipe(context.output ?? process.stdout);
|
||||
const rl = readline.createInterface({
|
||||
terminal: true,
|
||||
input,
|
||||
output,
|
||||
});
|
||||
const screen = new screen_manager_ts_1.default(rl);
|
||||
const { promise, resolve, reject } = promise_polyfill_ts_1.PromisePolyfill.withResolver();
|
||||
const cancel = () => reject(new errors_ts_1.CancelPromptError());
|
||||
if (signal) {
|
||||
const abort = () => reject(new errors_ts_1.AbortPromptError({ cause: signal.reason }));
|
||||
if (signal.aborted) {
|
||||
abort();
|
||||
return Object.assign(promise, { cancel });
|
||||
}
|
||||
signal.addEventListener('abort', abort);
|
||||
cleanups.add(() => signal.removeEventListener('abort', abort));
|
||||
}
|
||||
cleanups.add((0, signal_exit_1.onExit)((code, signal) => {
|
||||
reject(new errors_ts_1.ExitPromptError(`User force closed the prompt with ${code} ${signal}`));
|
||||
}));
|
||||
// SIGINT must be explicitly handled by the prompt so the ExitPromptError can be handled.
|
||||
// Otherwise, the prompt will stop and in some scenarios never resolve.
|
||||
// Ref issue #1741
|
||||
const sigint = () => reject(new errors_ts_1.ExitPromptError(`User force closed the prompt with SIGINT`));
|
||||
rl.on('SIGINT', sigint);
|
||||
cleanups.add(() => rl.removeListener('SIGINT', sigint));
|
||||
// Re-renders only happen when the state change; but the readline cursor could change position
|
||||
// and that also requires a re-render (and a manual one because we mute the streams).
|
||||
// We set the listener after the initial workLoop to avoid a double render if render triggered
|
||||
// by a state change sets the cursor to the right position.
|
||||
const checkCursorPos = () => screen.checkCursorPos();
|
||||
rl.input.on('keypress', checkCursorPos);
|
||||
cleanups.add(() => rl.input.removeListener('keypress', checkCursorPos));
|
||||
return (0, hook_engine_ts_1.withHooks)(rl, (cycle) => {
|
||||
// The close event triggers immediately when the user press ctrl+c. SignalExit on the other hand
|
||||
// triggers after the process is done (which happens after timeouts are done triggering.)
|
||||
// We triggers the hooks cleanup phase on rl `close` so active timeouts can be cleared.
|
||||
const hooksCleanup = node_async_hooks_1.AsyncResource.bind(() => hook_engine_ts_1.effectScheduler.clearAll());
|
||||
rl.on('close', hooksCleanup);
|
||||
cleanups.add(() => rl.removeListener('close', hooksCleanup));
|
||||
cycle(() => {
|
||||
try {
|
||||
const nextView = view(config, (value) => {
|
||||
setImmediate(() => resolve(value));
|
||||
});
|
||||
// Typescript won't allow this, but not all users rely on typescript.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (nextView === undefined) {
|
||||
const callerFilename = callSites[1]?.getFileName();
|
||||
throw new Error(`Prompt functions must return a string.\n at ${callerFilename}`);
|
||||
}
|
||||
const [content, bottomContent] = typeof nextView === 'string' ? [nextView] : nextView;
|
||||
screen.render(content, bottomContent);
|
||||
hook_engine_ts_1.effectScheduler.run();
|
||||
}
|
||||
catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
return Object.assign(promise
|
||||
.then((answer) => {
|
||||
hook_engine_ts_1.effectScheduler.clearAll();
|
||||
return answer;
|
||||
}, (error) => {
|
||||
hook_engine_ts_1.effectScheduler.clearAll();
|
||||
throw error;
|
||||
})
|
||||
// Wait for the promise to settle, then cleanup.
|
||||
.finally(() => {
|
||||
cleanups.forEach((cleanup) => cleanup());
|
||||
screen.done({ clearContent: Boolean(context.clearPromptOnDone) });
|
||||
output.end();
|
||||
})
|
||||
// Once cleanup is done, let the expose promise resolve/reject to the internal one.
|
||||
.then(() => promise), { cancel });
|
||||
});
|
||||
};
|
||||
return prompt;
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
export declare class AbortPromptError extends Error {
|
||||
name: string;
|
||||
message: string;
|
||||
constructor(options?: {
|
||||
cause?: unknown;
|
||||
});
|
||||
}
|
||||
export declare class CancelPromptError extends Error {
|
||||
name: string;
|
||||
message: string;
|
||||
}
|
||||
export declare class ExitPromptError extends Error {
|
||||
name: string;
|
||||
}
|
||||
export declare class HookError extends Error {
|
||||
name: string;
|
||||
}
|
||||
export declare class ValidationError extends Error {
|
||||
name: string;
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ValidationError = exports.HookError = exports.ExitPromptError = exports.CancelPromptError = exports.AbortPromptError = void 0;
|
||||
class AbortPromptError extends Error {
|
||||
name = 'AbortPromptError';
|
||||
message = 'Prompt was aborted';
|
||||
constructor(options) {
|
||||
super();
|
||||
this.cause = options?.cause;
|
||||
}
|
||||
}
|
||||
exports.AbortPromptError = AbortPromptError;
|
||||
class CancelPromptError extends Error {
|
||||
name = 'CancelPromptError';
|
||||
message = 'Prompt was canceled';
|
||||
}
|
||||
exports.CancelPromptError = CancelPromptError;
|
||||
class ExitPromptError extends Error {
|
||||
name = 'ExitPromptError';
|
||||
}
|
||||
exports.ExitPromptError = ExitPromptError;
|
||||
class HookError extends Error {
|
||||
name = 'HookError';
|
||||
}
|
||||
exports.HookError = HookError;
|
||||
class ValidationError extends Error {
|
||||
name = 'ValidationError';
|
||||
}
|
||||
exports.ValidationError = ValidationError;
|
@ -0,0 +1,23 @@
|
||||
import type { InquirerReadline } from '@inquirer/type';
|
||||
export declare function withHooks<T>(rl: InquirerReadline, cb: (cycle: (render: () => void) => void) => T): T;
|
||||
export declare function readline(): InquirerReadline;
|
||||
export declare function withUpdates<Args extends unknown[], R>(fn: (...args: Args) => R): (...args: Args) => R;
|
||||
type SetPointer<Value> = {
|
||||
get(): Value;
|
||||
set(value: Value): void;
|
||||
initialized: true;
|
||||
};
|
||||
type UnsetPointer<Value> = {
|
||||
get(): void;
|
||||
set(value: Value): void;
|
||||
initialized: false;
|
||||
};
|
||||
type Pointer<Value> = SetPointer<Value> | UnsetPointer<Value>;
|
||||
export declare function withPointer<Value, ReturnValue>(cb: (pointer: Pointer<Value>) => ReturnValue): ReturnValue;
|
||||
export declare function handleChange(): void;
|
||||
export declare const effectScheduler: {
|
||||
queue(cb: (readline: InquirerReadline) => void | (() => void)): void;
|
||||
run(): void;
|
||||
clearAll(): void;
|
||||
};
|
||||
export {};
|
@ -0,0 +1,118 @@
|
||||
"use strict";
|
||||
/* eslint @typescript-eslint/no-explicit-any: ["off"] */
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.effectScheduler = void 0;
|
||||
exports.withHooks = withHooks;
|
||||
exports.readline = readline;
|
||||
exports.withUpdates = withUpdates;
|
||||
exports.withPointer = withPointer;
|
||||
exports.handleChange = handleChange;
|
||||
const node_async_hooks_1 = require("node:async_hooks");
|
||||
const errors_ts_1 = require("./errors.js");
|
||||
const hookStorage = new node_async_hooks_1.AsyncLocalStorage();
|
||||
function createStore(rl) {
|
||||
const store = {
|
||||
rl,
|
||||
hooks: [],
|
||||
hooksCleanup: [],
|
||||
hooksEffect: [],
|
||||
index: 0,
|
||||
handleChange() { },
|
||||
};
|
||||
return store;
|
||||
}
|
||||
// Run callback in with the hook engine setup.
|
||||
function withHooks(rl, cb) {
|
||||
const store = createStore(rl);
|
||||
return hookStorage.run(store, () => {
|
||||
function cycle(render) {
|
||||
store.handleChange = () => {
|
||||
store.index = 0;
|
||||
render();
|
||||
};
|
||||
store.handleChange();
|
||||
}
|
||||
return cb(cycle);
|
||||
});
|
||||
}
|
||||
// Safe getStore utility that'll return the store or throw if undefined.
|
||||
function getStore() {
|
||||
const store = hookStorage.getStore();
|
||||
if (!store) {
|
||||
throw new errors_ts_1.HookError('[Inquirer] Hook functions can only be called from within a prompt');
|
||||
}
|
||||
return store;
|
||||
}
|
||||
function readline() {
|
||||
return getStore().rl;
|
||||
}
|
||||
// Merge state updates happening within the callback function to avoid multiple renders.
|
||||
function withUpdates(fn) {
|
||||
const wrapped = (...args) => {
|
||||
const store = getStore();
|
||||
let shouldUpdate = false;
|
||||
const oldHandleChange = store.handleChange;
|
||||
store.handleChange = () => {
|
||||
shouldUpdate = true;
|
||||
};
|
||||
const returnValue = fn(...args);
|
||||
if (shouldUpdate) {
|
||||
oldHandleChange();
|
||||
}
|
||||
store.handleChange = oldHandleChange;
|
||||
return returnValue;
|
||||
};
|
||||
return node_async_hooks_1.AsyncResource.bind(wrapped);
|
||||
}
|
||||
function withPointer(cb) {
|
||||
const store = getStore();
|
||||
const { index } = store;
|
||||
const pointer = {
|
||||
get() {
|
||||
return store.hooks[index];
|
||||
},
|
||||
set(value) {
|
||||
store.hooks[index] = value;
|
||||
},
|
||||
initialized: index in store.hooks,
|
||||
};
|
||||
const returnValue = cb(pointer);
|
||||
store.index++;
|
||||
return returnValue;
|
||||
}
|
||||
function handleChange() {
|
||||
getStore().handleChange();
|
||||
}
|
||||
exports.effectScheduler = {
|
||||
queue(cb) {
|
||||
const store = getStore();
|
||||
const { index } = store;
|
||||
store.hooksEffect.push(() => {
|
||||
store.hooksCleanup[index]?.();
|
||||
const cleanFn = cb(readline());
|
||||
if (cleanFn != null && typeof cleanFn !== 'function') {
|
||||
throw new errors_ts_1.ValidationError('useEffect return value must be a cleanup function or nothing.');
|
||||
}
|
||||
store.hooksCleanup[index] = cleanFn;
|
||||
});
|
||||
},
|
||||
run() {
|
||||
const store = getStore();
|
||||
withUpdates(() => {
|
||||
store.hooksEffect.forEach((effect) => {
|
||||
effect();
|
||||
});
|
||||
// Warning: Clean the hooks before exiting the `withUpdates` block.
|
||||
// Failure to do so means an updates would hit the same effects again.
|
||||
store.hooksEffect.length = 0;
|
||||
})();
|
||||
},
|
||||
clearAll() {
|
||||
const store = getStore();
|
||||
store.hooksCleanup.forEach((cleanFn) => {
|
||||
cleanFn?.();
|
||||
});
|
||||
store.hooksEffect.length = 0;
|
||||
store.hooksCleanup.length = 0;
|
||||
},
|
||||
};
|
@ -0,0 +1,10 @@
|
||||
export type KeypressEvent = {
|
||||
name: string;
|
||||
ctrl: boolean;
|
||||
};
|
||||
export declare const isUpKey: (key: KeypressEvent) => boolean;
|
||||
export declare const isDownKey: (key: KeypressEvent) => boolean;
|
||||
export declare const isSpaceKey: (key: KeypressEvent) => boolean;
|
||||
export declare const isBackspaceKey: (key: KeypressEvent) => boolean;
|
||||
export declare const isNumberKey: (key: KeypressEvent) => boolean;
|
||||
export declare const isEnterKey: (key: KeypressEvent) => boolean;
|
@ -0,0 +1,27 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.isEnterKey = exports.isNumberKey = exports.isBackspaceKey = exports.isSpaceKey = exports.isDownKey = exports.isUpKey = void 0;
|
||||
const isUpKey = (key) =>
|
||||
// The up key
|
||||
key.name === 'up' ||
|
||||
// Vim keybinding
|
||||
key.name === 'k' ||
|
||||
// Emacs keybinding
|
||||
(key.ctrl && key.name === 'p');
|
||||
exports.isUpKey = isUpKey;
|
||||
const isDownKey = (key) =>
|
||||
// The down key
|
||||
key.name === 'down' ||
|
||||
// Vim keybinding
|
||||
key.name === 'j' ||
|
||||
// Emacs keybinding
|
||||
(key.ctrl && key.name === 'n');
|
||||
exports.isDownKey = isDownKey;
|
||||
const isSpaceKey = (key) => key.name === 'space';
|
||||
exports.isSpaceKey = isSpaceKey;
|
||||
const isBackspaceKey = (key) => key.name === 'backspace';
|
||||
exports.isBackspaceKey = isBackspaceKey;
|
||||
const isNumberKey = (key) => '1234567890'.includes(key.name);
|
||||
exports.isNumberKey = isNumberKey;
|
||||
const isEnterKey = (key) => key.name === 'enter' || key.name === 'return';
|
||||
exports.isEnterKey = isEnterKey;
|
@ -0,0 +1,3 @@
|
||||
import type { Prettify, PartialDeep } from '@inquirer/type';
|
||||
import { type Theme } from './theme.ts';
|
||||
export declare function makeTheme<SpecificTheme extends object>(...themes: ReadonlyArray<undefined | PartialDeep<Theme<SpecificTheme>>>): Prettify<Theme<SpecificTheme>>;
|
@ -0,0 +1,33 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.makeTheme = makeTheme;
|
||||
const theme_ts_1 = require("./theme.js");
|
||||
function isPlainObject(value) {
|
||||
if (typeof value !== 'object' || value === null)
|
||||
return false;
|
||||
let proto = value;
|
||||
while (Object.getPrototypeOf(proto) !== null) {
|
||||
proto = Object.getPrototypeOf(proto);
|
||||
}
|
||||
return Object.getPrototypeOf(value) === proto;
|
||||
}
|
||||
function deepMerge(...objects) {
|
||||
const output = {};
|
||||
for (const obj of objects) {
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
const prevValue = output[key];
|
||||
output[key] =
|
||||
isPlainObject(prevValue) && isPlainObject(value)
|
||||
? deepMerge(prevValue, value)
|
||||
: value;
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
function makeTheme(...themes) {
|
||||
const themesToMerge = [
|
||||
theme_ts_1.defaultTheme,
|
||||
...themes.filter((theme) => theme != null),
|
||||
];
|
||||
return deepMerge(...themesToMerge);
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
import type { Prettify } from '@inquirer/type';
|
||||
export declare function usePagination<T>({ items, active, renderItem, pageSize, loop, }: {
|
||||
items: ReadonlyArray<T>;
|
||||
/** The index of the active item. */
|
||||
active: number;
|
||||
/** Renders an item as part of a page. */
|
||||
renderItem: (layout: Prettify<{
|
||||
item: T;
|
||||
index: number;
|
||||
isActive: boolean;
|
||||
}>) => string;
|
||||
/** The size of the page. */
|
||||
pageSize: number;
|
||||
/** Allows creating an infinitely looping list. `true` if unspecified. */
|
||||
loop?: boolean;
|
||||
}): string;
|
@ -0,0 +1,124 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.usePagination = usePagination;
|
||||
const use_ref_ts_1 = require("../use-ref.js");
|
||||
const utils_ts_1 = require("../utils.js");
|
||||
function usePointerPosition({ active, renderedItems, pageSize, loop, }) {
|
||||
const state = (0, use_ref_ts_1.useRef)({
|
||||
lastPointer: active,
|
||||
lastActive: undefined,
|
||||
});
|
||||
const { lastPointer, lastActive } = state.current;
|
||||
const middle = Math.floor(pageSize / 2);
|
||||
const renderedLength = renderedItems.reduce((acc, item) => acc + item.length, 0);
|
||||
const defaultPointerPosition = renderedItems
|
||||
.slice(0, active)
|
||||
.reduce((acc, item) => acc + item.length, 0);
|
||||
let pointer = defaultPointerPosition;
|
||||
if (renderedLength > pageSize) {
|
||||
if (loop) {
|
||||
/**
|
||||
* Creates the next position for the pointer considering an infinitely
|
||||
* looping list of items to be rendered on the page.
|
||||
*
|
||||
* The goal is to progressively move the cursor to the middle position as the user move down, and then keep
|
||||
* the cursor there. When the user move up, maintain the cursor position.
|
||||
*/
|
||||
// By default, keep the cursor position as-is.
|
||||
pointer = lastPointer;
|
||||
if (
|
||||
// First render, skip this logic.
|
||||
lastActive != null &&
|
||||
// Only move the pointer down when the user moves down.
|
||||
lastActive < active &&
|
||||
// Check user didn't move up across page boundary.
|
||||
active - lastActive < pageSize) {
|
||||
pointer = Math.min(
|
||||
// Furthest allowed position for the pointer is the middle of the list
|
||||
middle, Math.abs(active - lastActive) === 1
|
||||
? Math.min(
|
||||
// Move the pointer at most the height of the last active item.
|
||||
lastPointer + (renderedItems[lastActive]?.length ?? 0),
|
||||
// If the user moved by one item, move the pointer to the natural position of the active item as
|
||||
// long as it doesn't move the cursor up.
|
||||
Math.max(defaultPointerPosition, lastPointer))
|
||||
: // Otherwise, move the pointer down by the difference between the active and last active item.
|
||||
lastPointer + active - lastActive);
|
||||
}
|
||||
}
|
||||
else {
|
||||
/**
|
||||
* Creates the next position for the pointer considering a finite list of
|
||||
* items to be rendered on a page.
|
||||
*
|
||||
* The goal is to keep the pointer in the middle of the page whenever possible, until
|
||||
* we reach the bounds of the list (top or bottom). In which case, the cursor moves progressively
|
||||
* to the bottom or top of the list.
|
||||
*/
|
||||
const spaceUnderActive = renderedItems
|
||||
.slice(active)
|
||||
.reduce((acc, item) => acc + item.length, 0);
|
||||
pointer =
|
||||
spaceUnderActive < pageSize - middle
|
||||
? // If the active item is near the end of the list, progressively move the cursor towards the end.
|
||||
pageSize - spaceUnderActive
|
||||
: // Otherwise, progressively move the pointer to the middle of the list.
|
||||
Math.min(defaultPointerPosition, middle);
|
||||
}
|
||||
}
|
||||
// Save state for the next render
|
||||
state.current.lastPointer = pointer;
|
||||
state.current.lastActive = active;
|
||||
return pointer;
|
||||
}
|
||||
function usePagination({ items, active, renderItem, pageSize, loop = true, }) {
|
||||
const width = (0, utils_ts_1.readlineWidth)();
|
||||
const bound = (num) => ((num % items.length) + items.length) % items.length;
|
||||
const renderedItems = items.map((item, index) => {
|
||||
if (item == null)
|
||||
return [];
|
||||
return (0, utils_ts_1.breakLines)(renderItem({ item, index, isActive: index === active }), width).split('\n');
|
||||
});
|
||||
const renderedLength = renderedItems.reduce((acc, item) => acc + item.length, 0);
|
||||
const renderItemAtIndex = (index) => renderedItems[index] ?? [];
|
||||
const pointer = usePointerPosition({ active, renderedItems, pageSize, loop });
|
||||
// Render the active item to decide the position.
|
||||
// If the active item fits under the pointer, we render it there.
|
||||
// Otherwise, we need to render it to fit at the bottom of the page; moving the pointer up.
|
||||
const activeItem = renderItemAtIndex(active).slice(0, pageSize);
|
||||
const activeItemPosition = pointer + activeItem.length <= pageSize ? pointer : pageSize - activeItem.length;
|
||||
// Create an array of lines for the page, and add the lines of the active item into the page
|
||||
const pageBuffer = Array.from({ length: pageSize });
|
||||
pageBuffer.splice(activeItemPosition, activeItem.length, ...activeItem);
|
||||
// Store to prevent rendering the same item twice
|
||||
const itemVisited = new Set([active]);
|
||||
// Fill the page under the active item
|
||||
let bufferPointer = activeItemPosition + activeItem.length;
|
||||
let itemPointer = bound(active + 1);
|
||||
while (bufferPointer < pageSize &&
|
||||
!itemVisited.has(itemPointer) &&
|
||||
(loop && renderedLength > pageSize ? itemPointer !== active : itemPointer > active)) {
|
||||
const lines = renderItemAtIndex(itemPointer);
|
||||
const linesToAdd = lines.slice(0, pageSize - bufferPointer);
|
||||
pageBuffer.splice(bufferPointer, linesToAdd.length, ...linesToAdd);
|
||||
// Move pointers for next iteration
|
||||
itemVisited.add(itemPointer);
|
||||
bufferPointer += linesToAdd.length;
|
||||
itemPointer = bound(itemPointer + 1);
|
||||
}
|
||||
// Fill the page over the active item
|
||||
bufferPointer = activeItemPosition - 1;
|
||||
itemPointer = bound(active - 1);
|
||||
while (bufferPointer >= 0 &&
|
||||
!itemVisited.has(itemPointer) &&
|
||||
(loop && renderedLength > pageSize ? itemPointer !== active : itemPointer < active)) {
|
||||
const lines = renderItemAtIndex(itemPointer);
|
||||
const linesToAdd = lines.slice(Math.max(0, lines.length - bufferPointer - 1));
|
||||
pageBuffer.splice(bufferPointer - linesToAdd.length + 1, linesToAdd.length, ...linesToAdd);
|
||||
// Move pointers for next iteration
|
||||
itemVisited.add(itemPointer);
|
||||
bufferPointer -= linesToAdd.length;
|
||||
itemPointer = bound(itemPointer - 1);
|
||||
}
|
||||
return pageBuffer.filter((line) => typeof line === 'string').join('\n');
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
export declare class PromisePolyfill<T> extends Promise<T> {
|
||||
static withResolver<T>(): {
|
||||
promise: Promise<T>;
|
||||
resolve: (value: T) => void;
|
||||
reject: (error: unknown) => void;
|
||||
};
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.PromisePolyfill = void 0;
|
||||
// TODO: Remove this class once Node 22 becomes the minimum supported version.
|
||||
class PromisePolyfill extends Promise {
|
||||
// Available starting from Node 22
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers
|
||||
static withResolver() {
|
||||
let resolve;
|
||||
let reject;
|
||||
const promise = new Promise((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
return { promise, resolve: resolve, reject: reject };
|
||||
}
|
||||
}
|
||||
exports.PromisePolyfill = PromisePolyfill;
|
@ -0,0 +1,14 @@
|
||||
import type { InquirerReadline } from '@inquirer/type';
|
||||
export default class ScreenManager {
|
||||
private height;
|
||||
private extraLinesUnderPrompt;
|
||||
private cursorPos;
|
||||
private readonly rl;
|
||||
constructor(rl: InquirerReadline);
|
||||
write(content: string): void;
|
||||
render(content: string, bottomContent?: string): void;
|
||||
checkCursorPos(): void;
|
||||
done({ clearContent }: {
|
||||
clearContent: boolean;
|
||||
}): void;
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const node_util_1 = require("node:util");
|
||||
const ansi_escapes_1 = __importDefault(require("ansi-escapes"));
|
||||
const utils_ts_1 = require("./utils.js");
|
||||
const height = (content) => content.split('\n').length;
|
||||
const lastLine = (content) => content.split('\n').pop() ?? '';
|
||||
function cursorDown(n) {
|
||||
return n > 0 ? ansi_escapes_1.default.cursorDown(n) : '';
|
||||
}
|
||||
class ScreenManager {
|
||||
// These variables are keeping information to allow correct prompt re-rendering
|
||||
height = 0;
|
||||
extraLinesUnderPrompt = 0;
|
||||
cursorPos;
|
||||
rl;
|
||||
constructor(rl) {
|
||||
this.rl = rl;
|
||||
this.cursorPos = rl.getCursorPos();
|
||||
}
|
||||
write(content) {
|
||||
this.rl.output.unmute();
|
||||
this.rl.output.write(content);
|
||||
this.rl.output.mute();
|
||||
}
|
||||
render(content, bottomContent = '') {
|
||||
// Write message to screen and setPrompt to control backspace
|
||||
const promptLine = lastLine(content);
|
||||
const rawPromptLine = (0, node_util_1.stripVTControlCharacters)(promptLine);
|
||||
// Remove the rl.line from our prompt. We can't rely on the content of
|
||||
// rl.line (mainly because of the password prompt), so just rely on it's
|
||||
// length.
|
||||
let prompt = rawPromptLine;
|
||||
if (this.rl.line.length > 0) {
|
||||
prompt = prompt.slice(0, -this.rl.line.length);
|
||||
}
|
||||
this.rl.setPrompt(prompt);
|
||||
// SetPrompt will change cursor position, now we can get correct value
|
||||
this.cursorPos = this.rl.getCursorPos();
|
||||
const width = (0, utils_ts_1.readlineWidth)();
|
||||
content = (0, utils_ts_1.breakLines)(content, width);
|
||||
bottomContent = (0, utils_ts_1.breakLines)(bottomContent, width);
|
||||
// Manually insert an extra line if we're at the end of the line.
|
||||
// This prevent the cursor from appearing at the beginning of the
|
||||
// current line.
|
||||
if (rawPromptLine.length % width === 0) {
|
||||
content += '\n';
|
||||
}
|
||||
let output = content + (bottomContent ? '\n' + bottomContent : '');
|
||||
/**
|
||||
* Re-adjust the cursor at the correct position.
|
||||
*/
|
||||
// We need to consider parts of the prompt under the cursor as part of the bottom
|
||||
// content in order to correctly cleanup and re-render.
|
||||
const promptLineUpDiff = Math.floor(rawPromptLine.length / width) - this.cursorPos.rows;
|
||||
const bottomContentHeight = promptLineUpDiff + (bottomContent ? height(bottomContent) : 0);
|
||||
// Return cursor to the input position (on top of the bottomContent)
|
||||
if (bottomContentHeight > 0)
|
||||
output += ansi_escapes_1.default.cursorUp(bottomContentHeight);
|
||||
// Return cursor to the initial left offset.
|
||||
output += ansi_escapes_1.default.cursorTo(this.cursorPos.cols);
|
||||
/**
|
||||
* Render and store state for future re-rendering
|
||||
*/
|
||||
this.write(cursorDown(this.extraLinesUnderPrompt) +
|
||||
ansi_escapes_1.default.eraseLines(this.height) +
|
||||
output);
|
||||
this.extraLinesUnderPrompt = bottomContentHeight;
|
||||
this.height = height(output);
|
||||
}
|
||||
checkCursorPos() {
|
||||
const cursorPos = this.rl.getCursorPos();
|
||||
if (cursorPos.cols !== this.cursorPos.cols) {
|
||||
this.write(ansi_escapes_1.default.cursorTo(cursorPos.cols));
|
||||
this.cursorPos = cursorPos;
|
||||
}
|
||||
}
|
||||
done({ clearContent }) {
|
||||
this.rl.setPrompt('');
|
||||
let output = cursorDown(this.extraLinesUnderPrompt);
|
||||
output += clearContent ? ansi_escapes_1.default.eraseLines(this.height) : '\n';
|
||||
output += ansi_escapes_1.default.cursorShow;
|
||||
this.write(output);
|
||||
this.rl.close();
|
||||
}
|
||||
}
|
||||
exports.default = ScreenManager;
|
@ -0,0 +1,155 @@
|
||||
import type { Prettify } from '@inquirer/type';
|
||||
/**
|
||||
* Union type representing the possible statuses of a prompt.
|
||||
*
|
||||
* - `'loading'`: The prompt is currently loading.
|
||||
* - `'idle'`: The prompt is loaded and currently waiting for the user to
|
||||
* submit an answer.
|
||||
* - `'done'`: The user has submitted an answer and the prompt is finished.
|
||||
* - `string`: Any other string: The prompt is in a custom state.
|
||||
*/
|
||||
export type Status = 'loading' | 'idle' | 'done' | (string & {});
|
||||
type DefaultTheme = {
|
||||
/**
|
||||
* Prefix to prepend to the message. If a function is provided, it will be
|
||||
* called with the current status of the prompt, and the return value will be
|
||||
* used as the prefix.
|
||||
*
|
||||
* @remarks
|
||||
* If `status === 'loading'`, this property is ignored and the spinner (styled
|
||||
* by the `spinner` property) will be displayed instead.
|
||||
*
|
||||
* @defaultValue
|
||||
* ```ts
|
||||
* // import colors from 'yoctocolors-cjs';
|
||||
* (status) => status === 'done' ? colors.green('✔') : colors.blue('?')
|
||||
* ```
|
||||
*/
|
||||
prefix: string | Prettify<Omit<Record<Status, string>, 'loading'>>;
|
||||
/**
|
||||
* Configuration for the spinner that is displayed when the prompt is in the
|
||||
* `'loading'` state.
|
||||
*
|
||||
* We recommend the use of {@link https://github.com/sindresorhus/cli-spinners|cli-spinners} for a list of available spinners.
|
||||
*/
|
||||
spinner: {
|
||||
/**
|
||||
* The time interval between frames, in milliseconds.
|
||||
*
|
||||
* @defaultValue
|
||||
* ```ts
|
||||
* 80
|
||||
* ```
|
||||
*/
|
||||
interval: number;
|
||||
/**
|
||||
* A list of frames to show for the spinner.
|
||||
*
|
||||
* @defaultValue
|
||||
* ```ts
|
||||
* ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
||||
* ```
|
||||
*/
|
||||
frames: string[];
|
||||
};
|
||||
/**
|
||||
* Object containing functions to style different parts of the prompt.
|
||||
*/
|
||||
style: {
|
||||
/**
|
||||
* Style to apply to the user's answer once it has been submitted.
|
||||
*
|
||||
* @param text - The user's answer.
|
||||
* @returns The styled answer.
|
||||
*
|
||||
* @defaultValue
|
||||
* ```ts
|
||||
* // import colors from 'yoctocolors-cjs';
|
||||
* (text) => colors.cyan(text)
|
||||
* ```
|
||||
*/
|
||||
answer: (text: string) => string;
|
||||
/**
|
||||
* Style to apply to the message displayed to the user.
|
||||
*
|
||||
* @param text - The message to style.
|
||||
* @param status - The current status of the prompt.
|
||||
* @returns The styled message.
|
||||
*
|
||||
* @defaultValue
|
||||
* ```ts
|
||||
* // import colors from 'yoctocolors-cjs';
|
||||
* (text, status) => colors.bold(text)
|
||||
* ```
|
||||
*/
|
||||
message: (text: string, status: Status) => string;
|
||||
/**
|
||||
* Style to apply to error messages.
|
||||
*
|
||||
* @param text - The error message.
|
||||
* @returns The styled error message.
|
||||
*
|
||||
* @defaultValue
|
||||
* ```ts
|
||||
* // import colors from 'yoctocolors-cjs';
|
||||
* (text) => colors.red(`> ${text}`)
|
||||
* ```
|
||||
*/
|
||||
error: (text: string) => string;
|
||||
/**
|
||||
* Style to apply to the default answer when one is provided.
|
||||
*
|
||||
* @param text - The default answer.
|
||||
* @returns The styled default answer.
|
||||
*
|
||||
* @defaultValue
|
||||
* ```ts
|
||||
* // import colors from 'yoctocolors-cjs';
|
||||
* (text) => colors.dim(`(${text})`)
|
||||
* ```
|
||||
*/
|
||||
defaultAnswer: (text: string) => string;
|
||||
/**
|
||||
* Style to apply to help text.
|
||||
*
|
||||
* @param text - The help text.
|
||||
* @returns The styled help text.
|
||||
*
|
||||
* @defaultValue
|
||||
* ```ts
|
||||
* // import colors from 'yoctocolors-cjs';
|
||||
* (text) => colors.dim(text)
|
||||
* ```
|
||||
*/
|
||||
help: (text: string) => string;
|
||||
/**
|
||||
* Style to apply to highlighted text.
|
||||
*
|
||||
* @param text - The text to highlight.
|
||||
* @returns The highlighted text.
|
||||
*
|
||||
* @defaultValue
|
||||
* ```ts
|
||||
* // import colors from 'yoctocolors-cjs';
|
||||
* (text) => colors.cyan(text)
|
||||
* ```
|
||||
*/
|
||||
highlight: (text: string) => string;
|
||||
/**
|
||||
* Style to apply to keyboard keys referred to in help texts.
|
||||
*
|
||||
* @param text - The key to style.
|
||||
* @returns The styled key.
|
||||
*
|
||||
* @defaultValue
|
||||
* ```ts
|
||||
* // import colors from 'yoctocolors-cjs';
|
||||
* (text) => colors.cyan(colors.bold(`<${text}>`))
|
||||
* ```
|
||||
*/
|
||||
key: (text: string) => string;
|
||||
};
|
||||
};
|
||||
export type Theme<Extension extends object = object> = Prettify<Extension & DefaultTheme>;
|
||||
export declare const defaultTheme: DefaultTheme;
|
||||
export {};
|
@ -0,0 +1,28 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.defaultTheme = void 0;
|
||||
const yoctocolors_cjs_1 = __importDefault(require("yoctocolors-cjs"));
|
||||
const figures_1 = __importDefault(require("@inquirer/figures"));
|
||||
exports.defaultTheme = {
|
||||
prefix: {
|
||||
idle: yoctocolors_cjs_1.default.blue('?'),
|
||||
// TODO: use figure
|
||||
done: yoctocolors_cjs_1.default.green(figures_1.default.tick),
|
||||
},
|
||||
spinner: {
|
||||
interval: 80,
|
||||
frames: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'].map((frame) => yoctocolors_cjs_1.default.yellow(frame)),
|
||||
},
|
||||
style: {
|
||||
answer: yoctocolors_cjs_1.default.cyan,
|
||||
message: yoctocolors_cjs_1.default.bold,
|
||||
error: (text) => yoctocolors_cjs_1.default.red(`> ${text}`),
|
||||
defaultAnswer: (text) => yoctocolors_cjs_1.default.dim(`(${text})`),
|
||||
help: yoctocolors_cjs_1.default.dim,
|
||||
highlight: yoctocolors_cjs_1.default.cyan,
|
||||
key: (text) => yoctocolors_cjs_1.default.cyan(yoctocolors_cjs_1.default.bold(`<${text}>`)),
|
||||
},
|
||||
};
|
@ -0,0 +1,2 @@
|
||||
import type { InquirerReadline } from '@inquirer/type';
|
||||
export declare function useEffect(cb: (rl: InquirerReadline) => void | (() => void), depArray: ReadonlyArray<unknown>): void;
|
@ -0,0 +1,14 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.useEffect = useEffect;
|
||||
const hook_engine_ts_1 = require("./hook-engine.js");
|
||||
function useEffect(cb, depArray) {
|
||||
(0, hook_engine_ts_1.withPointer)((pointer) => {
|
||||
const oldDeps = pointer.get();
|
||||
const hasChanged = !Array.isArray(oldDeps) || depArray.some((dep, i) => !Object.is(dep, oldDeps[i]));
|
||||
if (hasChanged) {
|
||||
hook_engine_ts_1.effectScheduler.queue(cb);
|
||||
}
|
||||
pointer.set(depArray);
|
||||
});
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
import { type InquirerReadline } from '@inquirer/type';
|
||||
import { type KeypressEvent } from './key.ts';
|
||||
export declare function useKeypress(userHandler: (event: KeypressEvent, rl: InquirerReadline) => void | Promise<void>): void;
|
@ -0,0 +1,23 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.useKeypress = useKeypress;
|
||||
const use_ref_ts_1 = require("./use-ref.js");
|
||||
const use_effect_ts_1 = require("./use-effect.js");
|
||||
const hook_engine_ts_1 = require("./hook-engine.js");
|
||||
function useKeypress(userHandler) {
|
||||
const signal = (0, use_ref_ts_1.useRef)(userHandler);
|
||||
signal.current = userHandler;
|
||||
(0, use_effect_ts_1.useEffect)((rl) => {
|
||||
let ignore = false;
|
||||
const handler = (0, hook_engine_ts_1.withUpdates)((_input, event) => {
|
||||
if (ignore)
|
||||
return;
|
||||
void signal.current(event, rl);
|
||||
});
|
||||
rl.input.on('keypress', handler);
|
||||
return () => {
|
||||
ignore = true;
|
||||
rl.input.removeListener('keypress', handler);
|
||||
};
|
||||
}, []);
|
||||
}
|
@ -0,0 +1 @@
|
||||
export declare function useMemo<Value>(fn: () => Value, dependencies: ReadonlyArray<unknown>): Value;
|
@ -0,0 +1,17 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.useMemo = useMemo;
|
||||
const hook_engine_ts_1 = require("./hook-engine.js");
|
||||
function useMemo(fn, dependencies) {
|
||||
return (0, hook_engine_ts_1.withPointer)((pointer) => {
|
||||
const prev = pointer.get();
|
||||
if (!prev ||
|
||||
prev.dependencies.length !== dependencies.length ||
|
||||
prev.dependencies.some((dep, i) => dep !== dependencies[i])) {
|
||||
const value = fn();
|
||||
pointer.set({ value, dependencies });
|
||||
return value;
|
||||
}
|
||||
return prev.value;
|
||||
});
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
import type { Theme, Status } from './theme.ts';
|
||||
export declare function usePrefix({ status, theme, }: {
|
||||
status?: Status;
|
||||
theme?: Theme;
|
||||
}): string;
|
@ -0,0 +1,38 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.usePrefix = usePrefix;
|
||||
const use_state_ts_1 = require("./use-state.js");
|
||||
const use_effect_ts_1 = require("./use-effect.js");
|
||||
const make_theme_ts_1 = require("./make-theme.js");
|
||||
function usePrefix({ status = 'idle', theme, }) {
|
||||
const [showLoader, setShowLoader] = (0, use_state_ts_1.useState)(false);
|
||||
const [tick, setTick] = (0, use_state_ts_1.useState)(0);
|
||||
const { prefix, spinner } = (0, make_theme_ts_1.makeTheme)(theme);
|
||||
(0, use_effect_ts_1.useEffect)(() => {
|
||||
if (status === 'loading') {
|
||||
let tickInterval;
|
||||
let inc = -1;
|
||||
// Delay displaying spinner by 300ms, to avoid flickering
|
||||
const delayTimeout = setTimeout(() => {
|
||||
setShowLoader(true);
|
||||
tickInterval = setInterval(() => {
|
||||
inc = inc + 1;
|
||||
setTick(inc % spinner.frames.length);
|
||||
}, spinner.interval);
|
||||
}, 300);
|
||||
return () => {
|
||||
clearTimeout(delayTimeout);
|
||||
clearInterval(tickInterval);
|
||||
};
|
||||
}
|
||||
else {
|
||||
setShowLoader(false);
|
||||
}
|
||||
}, [status]);
|
||||
if (showLoader) {
|
||||
return spinner.frames[tick];
|
||||
}
|
||||
// There's a delay before we show the loader. So we want to ignore `loading` here, and pass idle instead.
|
||||
const iconName = status === 'loading' ? 'idle' : status;
|
||||
return typeof prefix === 'string' ? prefix : (prefix[iconName] ?? prefix['idle']);
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
export declare function useRef<Value>(val: Value): {
|
||||
current: Value;
|
||||
};
|
||||
export declare function useRef<Value>(val?: Value): {
|
||||
current: Value | undefined;
|
||||
};
|
@ -0,0 +1,7 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.useRef = useRef;
|
||||
const use_state_ts_1 = require("./use-state.js");
|
||||
function useRef(val) {
|
||||
return (0, use_state_ts_1.useState)({ current: val })[0];
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
type NotFunction<T> = T extends (...args: never) => unknown ? never : T;
|
||||
export declare function useState<Value>(defaultValue: NotFunction<Value> | (() => Value)): [Value, (newValue: Value) => void];
|
||||
export declare function useState<Value>(defaultValue?: NotFunction<Value> | (() => Value)): [Value | undefined, (newValue?: Value) => void];
|
||||
export {};
|
@ -0,0 +1,23 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.useState = useState;
|
||||
const node_async_hooks_1 = require("node:async_hooks");
|
||||
const hook_engine_ts_1 = require("./hook-engine.js");
|
||||
function useState(defaultValue) {
|
||||
return (0, hook_engine_ts_1.withPointer)((pointer) => {
|
||||
const setState = node_async_hooks_1.AsyncResource.bind(function setState(newValue) {
|
||||
// Noop if the value is still the same.
|
||||
if (pointer.get() !== newValue) {
|
||||
pointer.set(newValue);
|
||||
// Trigger re-render
|
||||
(0, hook_engine_ts_1.handleChange)();
|
||||
}
|
||||
});
|
||||
if (pointer.initialized) {
|
||||
return [pointer.get(), setState];
|
||||
}
|
||||
const value = typeof defaultValue === 'function' ? defaultValue() : defaultValue;
|
||||
pointer.set(value);
|
||||
return [value, setState];
|
||||
});
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Force line returns at specific width. This function is ANSI code friendly and it'll
|
||||
* ignore invisible codes during width calculation.
|
||||
* @param {string} content
|
||||
* @param {number} width
|
||||
* @return {string}
|
||||
*/
|
||||
export declare function breakLines(content: string, width: number): string;
|
||||
/**
|
||||
* Returns the width of the active readline, or 80 as default value.
|
||||
* @returns {number}
|
||||
*/
|
||||
export declare function readlineWidth(): number;
|
@ -0,0 +1,32 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.breakLines = breakLines;
|
||||
exports.readlineWidth = readlineWidth;
|
||||
const cli_width_1 = __importDefault(require("cli-width"));
|
||||
const wrap_ansi_1 = __importDefault(require("wrap-ansi"));
|
||||
const hook_engine_ts_1 = require("./hook-engine.js");
|
||||
/**
|
||||
* Force line returns at specific width. This function is ANSI code friendly and it'll
|
||||
* ignore invisible codes during width calculation.
|
||||
* @param {string} content
|
||||
* @param {number} width
|
||||
* @return {string}
|
||||
*/
|
||||
function breakLines(content, width) {
|
||||
return content
|
||||
.split('\n')
|
||||
.flatMap((line) => (0, wrap_ansi_1.default)(line, width, { trim: false, hard: true })
|
||||
.split('\n')
|
||||
.map((str) => str.trimEnd()))
|
||||
.join('\n');
|
||||
}
|
||||
/**
|
||||
* Returns the width of the active readline, or 80 as default value.
|
||||
* @returns {number}
|
||||
*/
|
||||
function readlineWidth() {
|
||||
return (0, cli_width_1.default)({ defaultWidth: 80, output: (0, hook_engine_ts_1.readline)().output });
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "commonjs"
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
export * from './lib/key.ts';
|
||||
export * from './lib/errors.ts';
|
||||
export { usePrefix } from './lib/use-prefix.ts';
|
||||
export { useState } from './lib/use-state.ts';
|
||||
export { useEffect } from './lib/use-effect.ts';
|
||||
export { useMemo } from './lib/use-memo.ts';
|
||||
export { useRef } from './lib/use-ref.ts';
|
||||
export { useKeypress } from './lib/use-keypress.ts';
|
||||
export { makeTheme } from './lib/make-theme.ts';
|
||||
export type { Theme, Status } from './lib/theme.ts';
|
||||
export { usePagination } from './lib/pagination/use-pagination.ts';
|
||||
export { createPrompt } from './lib/create-prompt.ts';
|
||||
export { Separator } from './lib/Separator.ts';
|
@ -0,0 +1,12 @@
|
||||
export * from "./lib/key.js";
|
||||
export * from "./lib/errors.js";
|
||||
export { usePrefix } from "./lib/use-prefix.js";
|
||||
export { useState } from "./lib/use-state.js";
|
||||
export { useEffect } from "./lib/use-effect.js";
|
||||
export { useMemo } from "./lib/use-memo.js";
|
||||
export { useRef } from "./lib/use-ref.js";
|
||||
export { useKeypress } from "./lib/use-keypress.js";
|
||||
export { makeTheme } from "./lib/make-theme.js";
|
||||
export { usePagination } from "./lib/pagination/use-pagination.js";
|
||||
export { createPrompt } from "./lib/create-prompt.js";
|
||||
export { Separator } from "./lib/Separator.js";
|
@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Separator object
|
||||
* Used to space/separate choices group
|
||||
*/
|
||||
export declare class Separator {
|
||||
readonly separator: string;
|
||||
readonly type: string;
|
||||
constructor(separator?: string);
|
||||
static isSeparator(choice: unknown): choice is Separator;
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
import colors from 'yoctocolors-cjs';
|
||||
import figures from '@inquirer/figures';
|
||||
/**
|
||||
* Separator object
|
||||
* Used to space/separate choices group
|
||||
*/
|
||||
export class Separator {
|
||||
separator = colors.dim(Array.from({ length: 15 }).join(figures.line));
|
||||
type = 'separator';
|
||||
constructor(separator) {
|
||||
if (separator) {
|
||||
this.separator = separator;
|
||||
}
|
||||
}
|
||||
static isSeparator(choice) {
|
||||
return Boolean(choice &&
|
||||
typeof choice === 'object' &&
|
||||
'type' in choice &&
|
||||
choice.type === 'separator');
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
import { type Prompt, type Prettify } from '@inquirer/type';
|
||||
type ViewFunction<Value, Config> = (config: Prettify<Config>, done: (value: Value) => void) => string | [string, string | undefined];
|
||||
export declare function createPrompt<Value, Config>(view: ViewFunction<Value, Config>): Prompt<Value, Config>;
|
||||
export {};
|
@ -0,0 +1,117 @@
|
||||
import * as readline from 'node:readline';
|
||||
import { AsyncResource } from 'node:async_hooks';
|
||||
import MuteStream from 'mute-stream';
|
||||
import { onExit as onSignalExit } from 'signal-exit';
|
||||
import ScreenManager from "./screen-manager.js";
|
||||
import { PromisePolyfill } from "./promise-polyfill.js";
|
||||
import { withHooks, effectScheduler } from "./hook-engine.js";
|
||||
import { AbortPromptError, CancelPromptError, ExitPromptError } from "./errors.js";
|
||||
function getCallSites() {
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
const _prepareStackTrace = Error.prepareStackTrace;
|
||||
let result = [];
|
||||
try {
|
||||
Error.prepareStackTrace = (_, callSites) => {
|
||||
const callSitesWithoutCurrent = callSites.slice(1);
|
||||
result = callSitesWithoutCurrent;
|
||||
return callSitesWithoutCurrent;
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
new Error().stack;
|
||||
}
|
||||
catch {
|
||||
// An error will occur if the Node flag --frozen-intrinsics is used.
|
||||
// https://nodejs.org/api/cli.html#--frozen-intrinsics
|
||||
return result;
|
||||
}
|
||||
Error.prepareStackTrace = _prepareStackTrace;
|
||||
return result;
|
||||
}
|
||||
export function createPrompt(view) {
|
||||
const callSites = getCallSites();
|
||||
const prompt = (config, context = {}) => {
|
||||
// Default `input` to stdin
|
||||
const { input = process.stdin, signal } = context;
|
||||
const cleanups = new Set();
|
||||
// Add mute capabilities to the output
|
||||
const output = new MuteStream();
|
||||
output.pipe(context.output ?? process.stdout);
|
||||
const rl = readline.createInterface({
|
||||
terminal: true,
|
||||
input,
|
||||
output,
|
||||
});
|
||||
const screen = new ScreenManager(rl);
|
||||
const { promise, resolve, reject } = PromisePolyfill.withResolver();
|
||||
const cancel = () => reject(new CancelPromptError());
|
||||
if (signal) {
|
||||
const abort = () => reject(new AbortPromptError({ cause: signal.reason }));
|
||||
if (signal.aborted) {
|
||||
abort();
|
||||
return Object.assign(promise, { cancel });
|
||||
}
|
||||
signal.addEventListener('abort', abort);
|
||||
cleanups.add(() => signal.removeEventListener('abort', abort));
|
||||
}
|
||||
cleanups.add(onSignalExit((code, signal) => {
|
||||
reject(new ExitPromptError(`User force closed the prompt with ${code} ${signal}`));
|
||||
}));
|
||||
// SIGINT must be explicitly handled by the prompt so the ExitPromptError can be handled.
|
||||
// Otherwise, the prompt will stop and in some scenarios never resolve.
|
||||
// Ref issue #1741
|
||||
const sigint = () => reject(new ExitPromptError(`User force closed the prompt with SIGINT`));
|
||||
rl.on('SIGINT', sigint);
|
||||
cleanups.add(() => rl.removeListener('SIGINT', sigint));
|
||||
// Re-renders only happen when the state change; but the readline cursor could change position
|
||||
// and that also requires a re-render (and a manual one because we mute the streams).
|
||||
// We set the listener after the initial workLoop to avoid a double render if render triggered
|
||||
// by a state change sets the cursor to the right position.
|
||||
const checkCursorPos = () => screen.checkCursorPos();
|
||||
rl.input.on('keypress', checkCursorPos);
|
||||
cleanups.add(() => rl.input.removeListener('keypress', checkCursorPos));
|
||||
return withHooks(rl, (cycle) => {
|
||||
// The close event triggers immediately when the user press ctrl+c. SignalExit on the other hand
|
||||
// triggers after the process is done (which happens after timeouts are done triggering.)
|
||||
// We triggers the hooks cleanup phase on rl `close` so active timeouts can be cleared.
|
||||
const hooksCleanup = AsyncResource.bind(() => effectScheduler.clearAll());
|
||||
rl.on('close', hooksCleanup);
|
||||
cleanups.add(() => rl.removeListener('close', hooksCleanup));
|
||||
cycle(() => {
|
||||
try {
|
||||
const nextView = view(config, (value) => {
|
||||
setImmediate(() => resolve(value));
|
||||
});
|
||||
// Typescript won't allow this, but not all users rely on typescript.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (nextView === undefined) {
|
||||
const callerFilename = callSites[1]?.getFileName();
|
||||
throw new Error(`Prompt functions must return a string.\n at ${callerFilename}`);
|
||||
}
|
||||
const [content, bottomContent] = typeof nextView === 'string' ? [nextView] : nextView;
|
||||
screen.render(content, bottomContent);
|
||||
effectScheduler.run();
|
||||
}
|
||||
catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
return Object.assign(promise
|
||||
.then((answer) => {
|
||||
effectScheduler.clearAll();
|
||||
return answer;
|
||||
}, (error) => {
|
||||
effectScheduler.clearAll();
|
||||
throw error;
|
||||
})
|
||||
// Wait for the promise to settle, then cleanup.
|
||||
.finally(() => {
|
||||
cleanups.forEach((cleanup) => cleanup());
|
||||
screen.done({ clearContent: Boolean(context.clearPromptOnDone) });
|
||||
output.end();
|
||||
})
|
||||
// Once cleanup is done, let the expose promise resolve/reject to the internal one.
|
||||
.then(() => promise), { cancel });
|
||||
});
|
||||
};
|
||||
return prompt;
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
export declare class AbortPromptError extends Error {
|
||||
name: string;
|
||||
message: string;
|
||||
constructor(options?: {
|
||||
cause?: unknown;
|
||||
});
|
||||
}
|
||||
export declare class CancelPromptError extends Error {
|
||||
name: string;
|
||||
message: string;
|
||||
}
|
||||
export declare class ExitPromptError extends Error {
|
||||
name: string;
|
||||
}
|
||||
export declare class HookError extends Error {
|
||||
name: string;
|
||||
}
|
||||
export declare class ValidationError extends Error {
|
||||
name: string;
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
export class AbortPromptError extends Error {
|
||||
name = 'AbortPromptError';
|
||||
message = 'Prompt was aborted';
|
||||
constructor(options) {
|
||||
super();
|
||||
this.cause = options?.cause;
|
||||
}
|
||||
}
|
||||
export class CancelPromptError extends Error {
|
||||
name = 'CancelPromptError';
|
||||
message = 'Prompt was canceled';
|
||||
}
|
||||
export class ExitPromptError extends Error {
|
||||
name = 'ExitPromptError';
|
||||
}
|
||||
export class HookError extends Error {
|
||||
name = 'HookError';
|
||||
}
|
||||
export class ValidationError extends Error {
|
||||
name = 'ValidationError';
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
import type { InquirerReadline } from '@inquirer/type';
|
||||
export declare function withHooks<T>(rl: InquirerReadline, cb: (cycle: (render: () => void) => void) => T): T;
|
||||
export declare function readline(): InquirerReadline;
|
||||
export declare function withUpdates<Args extends unknown[], R>(fn: (...args: Args) => R): (...args: Args) => R;
|
||||
type SetPointer<Value> = {
|
||||
get(): Value;
|
||||
set(value: Value): void;
|
||||
initialized: true;
|
||||
};
|
||||
type UnsetPointer<Value> = {
|
||||
get(): void;
|
||||
set(value: Value): void;
|
||||
initialized: false;
|
||||
};
|
||||
type Pointer<Value> = SetPointer<Value> | UnsetPointer<Value>;
|
||||
export declare function withPointer<Value, ReturnValue>(cb: (pointer: Pointer<Value>) => ReturnValue): ReturnValue;
|
||||
export declare function handleChange(): void;
|
||||
export declare const effectScheduler: {
|
||||
queue(cb: (readline: InquirerReadline) => void | (() => void)): void;
|
||||
run(): void;
|
||||
clearAll(): void;
|
||||
};
|
||||
export {};
|
@ -0,0 +1,110 @@
|
||||
/* eslint @typescript-eslint/no-explicit-any: ["off"] */
|
||||
import { AsyncLocalStorage, AsyncResource } from 'node:async_hooks';
|
||||
import { HookError, ValidationError } from "./errors.js";
|
||||
const hookStorage = new AsyncLocalStorage();
|
||||
function createStore(rl) {
|
||||
const store = {
|
||||
rl,
|
||||
hooks: [],
|
||||
hooksCleanup: [],
|
||||
hooksEffect: [],
|
||||
index: 0,
|
||||
handleChange() { },
|
||||
};
|
||||
return store;
|
||||
}
|
||||
// Run callback in with the hook engine setup.
|
||||
export function withHooks(rl, cb) {
|
||||
const store = createStore(rl);
|
||||
return hookStorage.run(store, () => {
|
||||
function cycle(render) {
|
||||
store.handleChange = () => {
|
||||
store.index = 0;
|
||||
render();
|
||||
};
|
||||
store.handleChange();
|
||||
}
|
||||
return cb(cycle);
|
||||
});
|
||||
}
|
||||
// Safe getStore utility that'll return the store or throw if undefined.
|
||||
function getStore() {
|
||||
const store = hookStorage.getStore();
|
||||
if (!store) {
|
||||
throw new HookError('[Inquirer] Hook functions can only be called from within a prompt');
|
||||
}
|
||||
return store;
|
||||
}
|
||||
export function readline() {
|
||||
return getStore().rl;
|
||||
}
|
||||
// Merge state updates happening within the callback function to avoid multiple renders.
|
||||
export function withUpdates(fn) {
|
||||
const wrapped = (...args) => {
|
||||
const store = getStore();
|
||||
let shouldUpdate = false;
|
||||
const oldHandleChange = store.handleChange;
|
||||
store.handleChange = () => {
|
||||
shouldUpdate = true;
|
||||
};
|
||||
const returnValue = fn(...args);
|
||||
if (shouldUpdate) {
|
||||
oldHandleChange();
|
||||
}
|
||||
store.handleChange = oldHandleChange;
|
||||
return returnValue;
|
||||
};
|
||||
return AsyncResource.bind(wrapped);
|
||||
}
|
||||
export function withPointer(cb) {
|
||||
const store = getStore();
|
||||
const { index } = store;
|
||||
const pointer = {
|
||||
get() {
|
||||
return store.hooks[index];
|
||||
},
|
||||
set(value) {
|
||||
store.hooks[index] = value;
|
||||
},
|
||||
initialized: index in store.hooks,
|
||||
};
|
||||
const returnValue = cb(pointer);
|
||||
store.index++;
|
||||
return returnValue;
|
||||
}
|
||||
export function handleChange() {
|
||||
getStore().handleChange();
|
||||
}
|
||||
export const effectScheduler = {
|
||||
queue(cb) {
|
||||
const store = getStore();
|
||||
const { index } = store;
|
||||
store.hooksEffect.push(() => {
|
||||
store.hooksCleanup[index]?.();
|
||||
const cleanFn = cb(readline());
|
||||
if (cleanFn != null && typeof cleanFn !== 'function') {
|
||||
throw new ValidationError('useEffect return value must be a cleanup function or nothing.');
|
||||
}
|
||||
store.hooksCleanup[index] = cleanFn;
|
||||
});
|
||||
},
|
||||
run() {
|
||||
const store = getStore();
|
||||
withUpdates(() => {
|
||||
store.hooksEffect.forEach((effect) => {
|
||||
effect();
|
||||
});
|
||||
// Warning: Clean the hooks before exiting the `withUpdates` block.
|
||||
// Failure to do so means an updates would hit the same effects again.
|
||||
store.hooksEffect.length = 0;
|
||||
})();
|
||||
},
|
||||
clearAll() {
|
||||
const store = getStore();
|
||||
store.hooksCleanup.forEach((cleanFn) => {
|
||||
cleanFn?.();
|
||||
});
|
||||
store.hooksEffect.length = 0;
|
||||
store.hooksCleanup.length = 0;
|
||||
},
|
||||
};
|
@ -0,0 +1,10 @@
|
||||
export type KeypressEvent = {
|
||||
name: string;
|
||||
ctrl: boolean;
|
||||
};
|
||||
export declare const isUpKey: (key: KeypressEvent) => boolean;
|
||||
export declare const isDownKey: (key: KeypressEvent) => boolean;
|
||||
export declare const isSpaceKey: (key: KeypressEvent) => boolean;
|
||||
export declare const isBackspaceKey: (key: KeypressEvent) => boolean;
|
||||
export declare const isNumberKey: (key: KeypressEvent) => boolean;
|
||||
export declare const isEnterKey: (key: KeypressEvent) => boolean;
|
@ -0,0 +1,18 @@
|
||||
export const isUpKey = (key) =>
|
||||
// The up key
|
||||
key.name === 'up' ||
|
||||
// Vim keybinding
|
||||
key.name === 'k' ||
|
||||
// Emacs keybinding
|
||||
(key.ctrl && key.name === 'p');
|
||||
export const isDownKey = (key) =>
|
||||
// The down key
|
||||
key.name === 'down' ||
|
||||
// Vim keybinding
|
||||
key.name === 'j' ||
|
||||
// Emacs keybinding
|
||||
(key.ctrl && key.name === 'n');
|
||||
export const isSpaceKey = (key) => key.name === 'space';
|
||||
export const isBackspaceKey = (key) => key.name === 'backspace';
|
||||
export const isNumberKey = (key) => '1234567890'.includes(key.name);
|
||||
export const isEnterKey = (key) => key.name === 'enter' || key.name === 'return';
|
@ -0,0 +1,3 @@
|
||||
import type { Prettify, PartialDeep } from '@inquirer/type';
|
||||
import { type Theme } from './theme.ts';
|
||||
export declare function makeTheme<SpecificTheme extends object>(...themes: ReadonlyArray<undefined | PartialDeep<Theme<SpecificTheme>>>): Prettify<Theme<SpecificTheme>>;
|
@ -0,0 +1,30 @@
|
||||
import { defaultTheme } from "./theme.js";
|
||||
function isPlainObject(value) {
|
||||
if (typeof value !== 'object' || value === null)
|
||||
return false;
|
||||
let proto = value;
|
||||
while (Object.getPrototypeOf(proto) !== null) {
|
||||
proto = Object.getPrototypeOf(proto);
|
||||
}
|
||||
return Object.getPrototypeOf(value) === proto;
|
||||
}
|
||||
function deepMerge(...objects) {
|
||||
const output = {};
|
||||
for (const obj of objects) {
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
const prevValue = output[key];
|
||||
output[key] =
|
||||
isPlainObject(prevValue) && isPlainObject(value)
|
||||
? deepMerge(prevValue, value)
|
||||
: value;
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
export function makeTheme(...themes) {
|
||||
const themesToMerge = [
|
||||
defaultTheme,
|
||||
...themes.filter((theme) => theme != null),
|
||||
];
|
||||
return deepMerge(...themesToMerge);
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
import type { Prettify } from '@inquirer/type';
|
||||
export declare function usePagination<T>({ items, active, renderItem, pageSize, loop, }: {
|
||||
items: ReadonlyArray<T>;
|
||||
/** The index of the active item. */
|
||||
active: number;
|
||||
/** Renders an item as part of a page. */
|
||||
renderItem: (layout: Prettify<{
|
||||
item: T;
|
||||
index: number;
|
||||
isActive: boolean;
|
||||
}>) => string;
|
||||
/** The size of the page. */
|
||||
pageSize: number;
|
||||
/** Allows creating an infinitely looping list. `true` if unspecified. */
|
||||
loop?: boolean;
|
||||
}): string;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue