feat(ui): Improved project approval settings

This commit is contained in:
Armin Ronacher
2026-06-09 13:25:54 +02:00
parent 66335d3a49
commit 5cb4f597f7
25 changed files with 767 additions and 374 deletions
+1 -1
View File
@@ -232,7 +232,7 @@ ${chalk.bold("Commands:")}
${APP_NAME} update [source|self|pi] Update pi and installed extensions
${APP_NAME} list [--approve|--no-approve]
List installed extensions from settings
${APP_NAME} config [--no-approve]
${APP_NAME} config [--approve|--no-approve]
Open TUI to enable/disable package resources
${APP_NAME} <command> --help Show help for install/remove/uninstall/update/list
@@ -0,0 +1,62 @@
import chalk from "chalk";
import type { ProjectTrustContext } from "../core/extensions/types.ts";
import type { AppMode } from "../core/project-trust.ts";
import type { SettingsManager } from "../core/settings-manager.ts";
import { showStartupInput, showStartupSelector } from "./startup-ui.ts";
export function createProjectTrustContext(options: {
cwd: string;
mode: AppMode;
settingsManager: SettingsManager;
hasUI: boolean;
}): ProjectTrustContext {
return {
cwd: options.cwd,
mode: options.mode === "interactive" ? "tui" : options.mode,
hasUI: options.hasUI,
ui: {
select: async (title, selectOptions) => {
if (!options.hasUI) {
return undefined;
}
if (options.mode !== "interactive") {
return undefined;
}
return showStartupSelector(
options.settingsManager,
title,
selectOptions.map((option) => ({ label: option, value: option })),
);
},
confirm: async (title, message) => {
if (!options.hasUI) {
return false;
}
if (options.mode !== "interactive") {
return false;
}
return (
(await showStartupSelector(options.settingsManager, `${title}\n${message}`, [
{ label: "Yes", value: true },
{ label: "No", value: false },
])) ?? false
);
},
input: async (title, placeholder) => {
if (!options.hasUI) {
return undefined;
}
if (options.mode !== "interactive") {
return undefined;
}
return showStartupInput(options.settingsManager, title, placeholder);
},
notify: (message, type = "info") => {
if (options.mode !== "interactive") {
const color = type === "error" ? chalk.red : type === "warning" ? chalk.yellow : chalk.cyan;
console.error(color(message));
}
},
},
};
}
@@ -0,0 +1,87 @@
import { ProcessTerminal, setKeybindings, TUI } from "@earendil-works/pi-tui";
import { KeybindingsManager } from "../core/keybindings.ts";
import type { SettingsManager } from "../core/settings-manager.ts";
import { ExtensionInputComponent } from "../modes/interactive/components/extension-input.ts";
import { ExtensionSelectorComponent } from "../modes/interactive/components/extension-selector.ts";
import { initTheme } from "../modes/interactive/theme/theme.ts";
function createStartupTui(settingsManager: SettingsManager): TUI {
initTheme(settingsManager.getTheme());
setKeybindings(KeybindingsManager.create());
const ui = new TUI(new ProcessTerminal(), settingsManager.getShowHardwareCursor());
ui.setClearOnShrink(settingsManager.getClearOnShrink());
return ui;
}
async function clearStartupTui(ui: TUI): Promise<void> {
ui.clear();
ui.requestRender();
await new Promise((resolve) => setTimeout(resolve, 25));
}
export async function showStartupSelector<T>(
settingsManager: SettingsManager,
title: string,
options: Array<{ label: string; value: T }>,
): Promise<T | undefined> {
return new Promise((resolve) => {
const ui = createStartupTui(settingsManager);
let settled = false;
const finish = async (result: T | undefined) => {
if (settled) {
return;
}
settled = true;
await clearStartupTui(ui);
ui.stop();
resolve(result);
};
const selector = new ExtensionSelectorComponent(
title,
options.map((option) => option.label),
(option) => void finish(options.find((entry) => entry.label === option)?.value),
() => void finish(undefined),
{ tui: ui },
);
ui.addChild(selector);
ui.setFocus(selector);
ui.start();
});
}
export async function showStartupInput(
settingsManager: SettingsManager,
title: string,
placeholder?: string,
): Promise<string | undefined> {
return new Promise((resolve) => {
const ui = createStartupTui(settingsManager);
let settled = false;
const finish = async (result: string | undefined) => {
if (settled) {
return;
}
settled = true;
input.dispose();
await clearStartupTui(ui);
ui.stop();
resolve(result);
};
const input = new ExtensionInputComponent(
title,
placeholder,
(value) => void finish(value),
() => void finish(undefined),
{
tui: ui,
},
);
ui.addChild(input);
ui.setFocus(input);
ui.start();
});
}