Skip to content

Commit

Permalink
add disable option to select and multiselect
Browse files Browse the repository at this point in the history
  • Loading branch information
blefnk committed Nov 27, 2024
1 parent f0834ad commit d5afebc
Show file tree
Hide file tree
Showing 10 changed files with 178 additions and 45 deletions.
1 change: 1 addition & 0 deletions bump.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ import { defineConfig } from "bumpp";
export default defineConfig({
push: false,
commit: false,
tag: false,
});
Binary file modified bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"dflt",
"Doesnt",
"Dont",
"Dothraki",
"Downey",
"elegido",
"emojify",
Expand Down
41 changes: 36 additions & 5 deletions examples/src/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { detect } from "detect-package-manager";
import { emojify } from "node-emoji";
import { bold } from "picocolors";

import pkg from "~/../package.json" assert { type: "json" };
import pkg from "~/../package.json" with { type: "json" };
import { anykeyPrompt } from "~/main.js";
import { multiselectPrompt } from "~/main.js";
import { progressbar } from "~/main.js";
Expand Down Expand Up @@ -206,12 +206,18 @@ export async function showSelectPrompt(): Promise<UserInput["lang"]> {
options: [
{ label: "English", value: "en", hint: "Default" },
{ label: "Ukrainian", value: "uk", hint: "Українська" },
{
label: "Dothraki",
value: "dothraki",
hint: "Dothraki",
disabled: true,
},
{ label: "Polish", value: "pl", hint: "Polski" },
{ label: "French", value: "fr", hint: "Français" },
{ label: "German", value: "de", hint: "Deutsch" },
{ label: "Spanish", value: "es", hint: "Español" },
{ label: "Italian", value: "it", hint: "Italiano" },
{ label: "Other", value: "else", hint: "Other" },
{ label: "Other", value: "other", hint: "Other" },
],
defaultValue: "en",
...experimentalConfig,
Expand All @@ -224,6 +230,12 @@ export async function showSelectPrompt(): Promise<UserInput["lang"]> {
case "uk":
msg({ type: "M_INFO", title: "Ви обрали українську" });
break;
case "dothraki":
msg({
type: "M_INFO",
title: "You have unlocked the Dothraki language! Great job! 🎉",
});
break;
case "pl":
msg({ type: "M_INFO", title: "Wybrałeś język polski" });
break;
Expand All @@ -242,7 +254,7 @@ export async function showSelectPrompt(): Promise<UserInput["lang"]> {
case "it":
msg({ type: "M_INFO", title: "Hai scelto l'italiano" });
break;
case "else":
case "other":
msg({ type: "M_INFO", title: "You selected Other" });
break;
}
Expand Down Expand Up @@ -282,6 +294,12 @@ export async function showMultiselectPrompt(): Promise<UserInput["langs"]> {
value: "JavaScript",
hint: "💛 Versatile and widely-used",
},
{
label: "Pawn",
value: "Pawn",
hint: "🎮 Simple and easy to learn",
disabled: true,
},
{
label: "CoffeeScript",
value: "CoffeeScript",
Expand All @@ -292,15 +310,28 @@ export async function showMultiselectPrompt(): Promise<UserInput["langs"]> {
value: "Python",
hint: "🐍 Powerful and easy to learn",
},
{ label: "Java", value: "Java", hint: "🍵 Robust and portable" },
{
label: "Java",
value: "Java",
hint: "🍵 Robust and portable",
},
{
label: "CSharp",
value: "CSharp",
hint: "🔢 Modern and object-oriented",
},
{ label: "Go", value: "Go", hint: "🐋 Simple and efficient" },
{ label: "Rust", value: "Rust", hint: "🦀 Fast and memory-safe" },
{ label: "Swift", value: "Swift", hint: "🐦 Safe and performant" },
{
label: "Swift",
value: "Swift",
hint: "🐦 Safe and performant",
},
{
label: "Other",
value: "Other",
hint: "Other",
},
],
required: true,
initial: ["TypeScript", "JavaScript"],
Expand Down
6 changes: 4 additions & 2 deletions jsr.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@
".gitignore",
".eslintcache",
"biome.jsonc",
"build.optim.ts",
"build.config.ts",
"build.optim.ts",
"build.publish.ts",
"bump.config.ts",
".putout.json",
"bun.lockb",
"knip.jsonc",
Expand All @@ -32,4 +34,4 @@
"cspell.json"
]
}
}
}
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"license": "MIT",
"dependencies": {
"@figliolia/chalk-animation": "^1.0.4",
"@reliverse/relinka": "^1.2.1",
"@reliverse/relinka": "^1.2.2",
"@sinclair/typebox": "^0.34.9",
"ansi-diff-stream": "^1.2.1",
"ansi-escapes": "^7.0.0",
Expand Down Expand Up @@ -90,7 +90,7 @@
"devDependencies": {
"@arethetypeswrong/cli": "^0.17.0",
"@biomejs/biome": "1.9.4",
"@cspell/dict-npm": "^5.1.13",
"@cspell/dict-npm": "^5.1.14",
"@eslint/js": "^9.15.0",
"@types/ansi-diff-stream": "^1.2.3",
"@types/bun": "^1.1.14",
Expand Down
75 changes: 56 additions & 19 deletions src/components/multiselect/multiselect-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { deleteLastLine } from "~/utils/terminal.js";

export async function multiselectPrompt<T extends string>(params: {
title: string;
options: { label: string; value: T; hint?: string }[];
options: { label: string; value: T; hint?: string; disabled?: boolean }[];
required?: boolean;
initial?: T[];
borderColor?: ColorName;
Expand All @@ -41,11 +41,23 @@ export async function multiselectPrompt<T extends string>(params: {
maxItems,
} = params;

let pointer = 0;
let pointer =
initial.length > 0
? options.findIndex((opt) => initial.includes(opt.value) && !opt.disabled)
: 0;

// If no valid initial pointer, default to the first non-disabled option
if (pointer === -1) {
pointer = options.findIndex((opt) => !opt.disabled);
if (pointer === -1) {
pointer = 0;
} // Fallback if all options are disabled
}

const selectedOptions = new Set<number>(
initial
.map((opt) => options.findIndex((o) => o.value === opt))
.filter((i) => i >= 0),
.filter((i) => i >= 0 && !options[i].disabled), // Ensure initial selections are not disabled
);

const rl = readline.createInterface({ input, output });
Expand Down Expand Up @@ -121,12 +133,21 @@ export async function multiselectPrompt<T extends string>(params: {
const option = options[index];
const isSelected = selectedOptions.has(index);
const isHighlighted = index === pointer;
const isDisabled = option.disabled;
const checkbox = isSelected ? "[x]" : "[ ]";
const prefix = isHighlighted ? "> " : " ";
const optionLabel = isHighlighted
? cyanBright(option.label)
: option.label;
const hint = option.hint ? ` (${option.hint})` : "";

// Dim the label and hint if the option is disabled
const optionLabel = isDisabled
? dim(option.label)
: isHighlighted
? cyanBright(option.label)
: option.label;

const hint = option.hint
? ` (${isDisabled ? dim(option.hint) : option.hint})`
: "";

outputStr += `${formattedBar} ${prefix}${checkbox} ${optionLabel}${dim(hint)}\n`;
}

Expand All @@ -150,30 +171,46 @@ export async function multiselectPrompt<T extends string>(params: {
return new Promise<T[]>((resolve) => {
function onKeyPress(str: string, key: readline.Key) {
if (key.name === "up" || key.name === "k") {
// Move up
pointer = (pointer - 1 + options.length) % options.length;
// Move up and skip disabled options
const originalPointer = pointer;
do {
pointer = (pointer - 1 + options.length) % options.length;
if (!options[pointer].disabled) {
break;
}
} while (pointer !== originalPointer);
errorMessage = ""; // Clear error message on navigation
renderOptions();
} else if (key.name === "down" || key.name === "j") {
// Move down
pointer = (pointer + 1) % options.length;
// Move down and skip disabled options
const originalPointer = pointer;
do {
pointer = (pointer + 1) % options.length;
if (!options[pointer].disabled) {
break;
}
} while (pointer !== originalPointer);
errorMessage = ""; // Clear error message on navigation
renderOptions();
} else if (key.name === "space") {
// Toggle selection
if (selectedOptions.has(pointer)) {
selectedOptions.delete(pointer);
const currentOption = options[pointer];
if (currentOption.disabled) {
errorMessage = "This option is disabled";
} else {
selectedOptions.add(pointer);
if (selectedOptions.has(pointer)) {
selectedOptions.delete(pointer);
} else {
selectedOptions.add(pointer);
}
errorMessage = ""; // Clear error message on successful toggle
}
errorMessage = ""; // Clear error message on selection
renderOptions();
} else if (key.name === "return") {
// Return selected options
if (!required || selectedOptions.size > 0) {
const selectedValues = Array.from(selectedOptions).map(
(index) => options[index].value,
);
const selectedValues = Array.from(selectedOptions)
.filter((index) => !options[index].disabled) // Ensure no disabled options are selected
.map((index) => options[index].value);
cleanup();
resolve(selectedValues);
deleteLastLine();
Expand Down
Loading

0 comments on commit d5afebc

Please sign in to comment.