mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-07 02:06:05 -10:00
feat(plugin): multi-line text input via ui.input-multiline (#1968)
* feat(plugin): multi-line text input via ui.input-multiline Adds host::input_multiline: a multi-line text dialog pre-filled with a value, dismissed to none, confirmed with Cmd/Ctrl+Enter. Same rules as input (command or event of a window, no capability, wait not charged). Prompt::Input gains a multiline flag the frontend renders as a textarea; the jobs fixture, SDK (wasm + mock), WIT and the plugin API table are updated, and all plugin wasms rebuilt. Fixes #1946 * chore: regenerate bindings after merging detail-provider rename
This commit is contained in:
1 parent
e68b35f534
commit
2945d20208
54 files changed
+160
-43
No files matched your search
@@ -2339,9 +2339,14 @@ fn commands_ask_the_user_and_reveal_their_views() {
|
||||
|
||||
let (result, prompt) = run_and_answer(&f, "input", Some("Main"), Some(&["Other"]), none);
|
||||
assert_eq!(result.unwrap().as_deref(), Some("Other"));
|
||||
assert_eq!(prompt, Prompt::Input { prompt: "Name".into(), value: Some("Main".into()) });
|
||||
assert_eq!(prompt, Prompt::Input { prompt: "Name".into(), value: Some("Main".into()), multiline: false });
|
||||
assert_eq!(run_and_answer(&f, "input", None, None, none).0.unwrap().as_deref(), Some("none"));
|
||||
|
||||
let (result, prompt) = run_and_answer(&f, "input-multiline", Some("one\ntwo"), Some(&["a\nb"]), none);
|
||||
assert_eq!(result.unwrap().as_deref(), Some("a\nb"));
|
||||
assert_eq!(prompt, Prompt::Input { prompt: "Name".into(), value: Some("one\ntwo".into()), multiline: true });
|
||||
assert_eq!(run_and_answer(&f, "input-multiline", None, None, none).0.unwrap().as_deref(), Some("none"));
|
||||
|
||||
let (result, prompt) = run_and_answer(&f, "confirm", None, Some(&[]), none);
|
||||
assert_eq!(result.unwrap().as_deref(), Some("true"));
|
||||
assert_eq!(prompt, Prompt::Confirm { title: "Delete".into(), message: "Really?".into() });
|
||||
@@ -2360,7 +2365,7 @@ fn commands_ask_the_user_and_reveal_their_views() {
|
||||
// Without a window there is nothing to attach the dialog to.
|
||||
f.take_events();
|
||||
let no_window = Invocation { client: None, ..f.inv() };
|
||||
for cmd in ["pick", "input", "confirm"] {
|
||||
for cmd in ["pick", "input", "input-multiline", "confirm"] {
|
||||
let err = f.host.run_command("jobs", cmd, None, no_window.clone()).unwrap_err().message;
|
||||
assert!(err.contains("from a command the user invoked"), "{cmd}: {err}");
|
||||
}
|
||||
|
||||
@@ -77,14 +77,15 @@ pub struct PickItem {
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
/// What a plugin asks the user (`ui.pick`, `ui.pick-many`, `ui.input`, `ui.confirm`).
|
||||
/// What a plugin asks the user (`ui.pick`, `ui.pick-many`, `ui.input`,
|
||||
/// `ui.input-multiline`, `ui.confirm`).
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Type)]
|
||||
#[serde(tag = "kind", rename_all = "camelCase")]
|
||||
pub enum Prompt {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
QuickPick { title: String, items: Vec<PickItem>, multiple: bool },
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Input { prompt: String, value: Option<String> },
|
||||
Input { prompt: String, value: Option<String>, multiline: bool },
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Confirm { title: String, message: String },
|
||||
}
|
||||
|
||||
@@ -355,7 +355,14 @@ impl ui::Host for StoreData {
|
||||
}
|
||||
|
||||
fn input(&mut self, prompt: String, value: Option<String>) -> Fallible<Option<String>> {
|
||||
let prompt = Prompt::Input { prompt: clip(prompt, MAX_TITLE), value: value.map(|v| clip(v, 4096)) };
|
||||
let prompt =
|
||||
Prompt::Input { prompt: clip(prompt, MAX_TITLE), value: value.map(|v| clip(v, 4096)), multiline: false };
|
||||
Ok(self.ask(prompt).map(|answer| answer.and_then(|text| text.into_iter().next())))
|
||||
}
|
||||
|
||||
fn input_multiline(&mut self, prompt: String, value: Option<String>) -> Fallible<Option<String>> {
|
||||
let prompt =
|
||||
Prompt::Input { prompt: clip(prompt, MAX_TITLE), value: value.map(|v| clip(v, 4096)), multiline: true };
|
||||
Ok(self.ask(prompt).map(|answer| answer.and_then(|text| text.into_iter().next())))
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
8a4006a730cf82fa412e28c1d181cdb96b7d16e5fe9a635b735d96d06862a80b
|
||||
19b672d86ff84edbbdb32ba06aefba08344e28b00310c09260d935a6fd2d6d92
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
f576b03db2e7bfaad3d5fc8c562c678c245d096bfa0c52bcadba4663991012bc
|
||||
5916e03cba741eea838c99757a50b86ecae020a75dc856d0301a5afcd4beb437
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
52ae124ebab17dbbb182997efe13bdcd48943f3fcb7730f1cdf466cdbd1fc40b
|
||||
cb9b6f980b976d04ff25289a7978fe6f0b1f77e99ac9bdbaa8fd0720e8306f29
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
5bfac74cff6560738bbd639691211219cfe731e260cf02bb86aec3bd8f4edc06
|
||||
e399272912a4664c9ce307ff128ce2059a2276e31b7806e1553870c4041c27b5
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
4a9eadbf94cc03648fd51e2ed892d23bdae68084762098981dce2a15b97c3fbd
|
||||
5ad3afb9518e1e2d06e05b5ba5d7339b8aafa7d03502cad00bdacbfe73419775
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
2c3be2b558b10824cd2420df5de0b7dee30c68a63cffc3f03cfae16ee2d96574
|
||||
93840a797e8f6d86e3d75432395370f19e6a632444d8441b346b9db6c32cd6f9
|
||||
@@ -75,6 +75,7 @@ impl Plugin for Jobs {
|
||||
Ok(Some(picked.map_or("none".to_owned(), |keys| keys.join(","))))
|
||||
}
|
||||
"input" => Ok(Some(host::input("Name", arg)?.unwrap_or_else(|| "none".to_owned()))),
|
||||
"input-multiline" => Ok(Some(host::input_multiline("Name", arg)?.unwrap_or_else(|| "none".to_owned()))),
|
||||
"confirm" => Ok(Some(host::confirm("Delete", "Really?")?.to_string())),
|
||||
"reveal" => host::reveal_view(arg.unwrap_or_default()).map(|()| None),
|
||||
"selection" => {
|
||||
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
e1cbbe2c814e554ef9622204ddcfce87c4d1919993d5e78f9739dfaa46ba128c
|
||||
44a29b9c311e10a055625fc43940fb9ecd361639f8a11c992ac6817f39e95e36
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
332840c4966244f309be9853c44caae56a5706bc102b9df0b4de4475a2a5d2fd
|
||||
36a33de65d506d7753722d38aea7576722fba583ef615de502a5e341b781ef18
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
ac8c4a1b4b4df9c3ffd4725e321df3f7836955a1c052387a276f0ad075c85527
|
||||
a4f9b7a13a7c2aa1243359df865e659eb4b8f86fb6623d743dcc17988a10e183
|
||||
@@ -381,6 +381,7 @@ then run `openidea-plugin check`.
|
||||
| `host::open_url(url)` | `ui.open-url` | Opens an `http://` or `https://` URL in the user's default browser. |
|
||||
| `host::pick(title, items)`, `pick_many` | `ui.pick`, `ui.pick-many` | Asks the user to pick from `PickItem`s and waits: the chosen key(s), or `None` when dismissed. Only from a command or event of a window (else `invalid`); the wait isn't charged to the time limit. |
|
||||
| `host::input(prompt, value)` | `ui.input` | A one-line text box pre-filled with `value`; `None` when dismissed. Same rules as `pick`. |
|
||||
| `host::input_multiline(prompt, value)` | `ui.input-multiline` | A multi-line text box pre-filled with `value`; `None` when dismissed; Cmd/Ctrl+Enter confirms. Same rules as `input`. |
|
||||
| `host::confirm(title, body)` | `ui.confirm` | OK / Cancel; `true` on OK. Same rules as `pick`. |
|
||||
| `host::locale()` | `ui.locale` | The IDE's UI language as a BCP 47 tag (`en`, `fr`, `zh-CN`); no capability. See _Translations_. |
|
||||
| `host::log(level, msg)` | `log` | IDE log, tagged with the plugin id. `eprintln!` output is logged too and shown when the plugin crashes. |
|
||||
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
e6d7996c21d41d1cfe585c1037fbe4b04ba523c5fdba676f0852ee8a8438f6c9
|
||||
103165fc45ab6a995cdddd10848ae81f3f137438c1981375e6741185813ccdb3
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
2cc22faa9f1a4b4c27d22f740144e5968083444898c36574d1b3e7780e003dc0
|
||||
e0845829cfaa2d8f471ccfd8a41726757443df6591e1e06be9e499d6781c02fe
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
2ed5f73c78e165a494d5f91f6ed2b55682e28a9d4e516553c9332b9d8ab84ab4
|
||||
6490dcca17d4d87336c70bbf8fbfac90f711ed0079f43d16534703b179211069
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
3274e8023091ec67b018d10e67e6180c3bc875747d51779a3e67ebfe76178907
|
||||
aa807e1f712896e0ffe1f16115bd8ab991b59df5fcae73e97b6a40d6c94aaf8f
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
6060665c10f800787c9c7d03fbbd7dcdefb9fe475827b6b6902fdd72324f7d98
|
||||
225ca906d21299fa45059c11d12468e6c3fc577aebb59441c51774a760e54a26
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
929cbc24e4c07131e8a46aa333632b703909b6d978b36b474207f6ca233cc9c1
|
||||
189e378fc52adfb058243feef4e100835600a1d4751adc23a262d13e703ef5e9
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
2f26af0ac018d56a0f93a7060ac0a4180b9760c20a98465ef9d4a6f06878455b
|
||||
2b2e162de966b670918b1bbe937c34cf9bef014834fb38be820ec25f20b09159
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
7aafd74237661ec2907a4c8950b685a17bed760a10a2fc1062139171636e7f2d
|
||||
6d13cd671dfed3c9c73be0e40b81cc5f757e4f8ebf6a9eecba0001fa8c314d47
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
af6ba8a45e97858fbc9c530bdb8e9e56c6229c9715a91bd543399fa0bc7cc04f
|
||||
a941b31f326427d41cf3e62373bc421d086789a0ac1cb8c3647eafafe29c5834
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
e9f8ce2cf2d1cc4bcd05f4fed106d11542a2fa4b12e256fe675cbb0aa41dd3af
|
||||
86428f64ea11d4863794595f12a9c100423725981d66d6f2a862191a0951fecf
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
737d1a1eb063a6757e93f0359fc32ab2b624ef8c251a4cba31aa3c36f7587002
|
||||
fc3b11c7b09f9a57c319af579efdd6f00d9ed51dccc6e86404c5164c6460d007
|
||||
@@ -117,6 +117,10 @@ mod imp {
|
||||
wit::ui::input(prompt, value).map_err(error)
|
||||
}
|
||||
|
||||
pub fn input_multiline(prompt: &str, value: Option<&str>) -> Result<Option<String>, Error> {
|
||||
wit::ui::input_multiline(prompt, value).map_err(error)
|
||||
}
|
||||
|
||||
pub fn confirm(title: &str, body: &str) -> Result<bool, Error> {
|
||||
wit::ui::confirm(title, body).map_err(error)
|
||||
}
|
||||
@@ -730,6 +734,12 @@ pub fn input(prompt: &str, value: Option<&str>) -> Result<Option<String>, Error>
|
||||
imp::input(prompt, value)
|
||||
}
|
||||
|
||||
/// Asks for text over several lines, pre-filled with `value`; `None` when
|
||||
/// dismissed. Same rules as [`input`]; the dialog confirms on Cmd/Ctrl+Enter.
|
||||
pub fn input_multiline(prompt: &str, value: Option<&str>) -> Result<Option<String>, Error> {
|
||||
imp::input_multiline(prompt, value)
|
||||
}
|
||||
|
||||
/// Asks for confirmation: `true` on OK, `false` on Cancel or Escape.
|
||||
/// Same rules as [`pick`].
|
||||
pub fn confirm(title: &str, body: &str) -> Result<bool, Error> {
|
||||
|
||||
@@ -491,6 +491,10 @@ pub mod current {
|
||||
Ok(with(|s| s.answers.pop_front().flatten()).and_then(|a| a.into_iter().next()))
|
||||
}
|
||||
|
||||
pub fn input_multiline(_prompt: &str, _value: Option<&str>) -> Result<Option<String>, Error> {
|
||||
Ok(with(|s| s.answers.pop_front().flatten()).and_then(|a| a.into_iter().next()))
|
||||
}
|
||||
|
||||
pub fn confirm(_title: &str, _body: &str) -> Result<bool, Error> {
|
||||
Ok(with(|s| s.answers.pop_front().flatten()).is_some())
|
||||
}
|
||||
@@ -937,6 +941,10 @@ mod tests {
|
||||
assert!(matches!(host::pick_many("Pick", &items), Err(Error::Invalid(_))));
|
||||
mock.answer_next_prompt(Some(&["x.txt"]));
|
||||
assert_eq!(host::input("Name", Some("a.txt")).unwrap().as_deref(), Some("x.txt"));
|
||||
mock.answer_next_prompt(Some(&["a\nb"]));
|
||||
assert_eq!(host::input_multiline("Notes", None).unwrap().as_deref(), Some("a\nb"));
|
||||
mock.answer_next_prompt(None);
|
||||
assert_eq!(host::input_multiline("Notes", None).unwrap(), None);
|
||||
mock.answer_next_prompt(Some(&[]));
|
||||
assert!(host::confirm("Sure?", "Really").unwrap());
|
||||
mock.answer_next_prompt(None);
|
||||
|
||||
@@ -62,6 +62,9 @@ interface ui {
|
||||
/// Asks for a line of text, pre-filled with `value`; `none` when dismissed.
|
||||
/// Same rules as `pick`.
|
||||
input: func(prompt: string, value: option<string>) -> result<option<string>, error>;
|
||||
/// Asks for text over several lines, pre-filled with `value`; `none` when
|
||||
/// dismissed. Same rules as `input`; the dialog confirms on Cmd/Ctrl+Enter.
|
||||
input-multiline: func(prompt: string, value: option<string>) -> result<option<string>, error>;
|
||||
/// Asks for confirmation with OK and Cancel: `true` on OK, `false` on
|
||||
/// Cancel or Escape. Same rules as `pick`.
|
||||
confirm: func(title: string, body: string) -> result<bool, error>;
|
||||
|
||||
+5
-2
@@ -6284,8 +6284,11 @@ export type ProjectStructureSettings = {
|
||||
excludeFiles?: string,
|
||||
};
|
||||
|
||||
/** What a plugin asks the user (`ui.pick`, `ui.pick-many`, `ui.input`, `ui.confirm`). */
|
||||
export type Prompt = { kind: "quickPick"; title: string; items: PickItem[]; multiple: boolean } | { kind: "input"; prompt: string; value: string | null } | { kind: "confirm"; title: string; message: string };
|
||||
/**
|
||||
* What a plugin asks the user (`ui.pick`, `ui.pick-many`, `ui.input`,
|
||||
* `ui.input-multiline`, `ui.confirm`).
|
||||
*/
|
||||
export type Prompt = { kind: "quickPick"; title: string; items: PickItem[]; multiple: boolean } | { kind: "input"; prompt: string; value: string | null; multiline: boolean } | { kind: "confirm"; title: string; message: string };
|
||||
|
||||
export type ProxyKind = "http" | "socks";
|
||||
|
||||
|
||||
@@ -536,13 +536,33 @@ describe("plugin prompts", () => {
|
||||
type: "promptRequested",
|
||||
pluginId: "p",
|
||||
promptId: "2",
|
||||
prompt: { kind: "input", prompt: "Name", value: "Main" },
|
||||
prompt: { kind: "input", prompt: "Name", value: "Main", multiline: false },
|
||||
});
|
||||
});
|
||||
await user.click(await screen.findByRole("button", { name: "Cancel" }));
|
||||
await waitFor(() => expect(answerPrompt).toHaveBeenCalledWith("2", null));
|
||||
});
|
||||
|
||||
it("asks multi-line text and confirms on Ctrl+Enter", async () => {
|
||||
const user = userEvent.setup();
|
||||
const answerPrompt = vi.fn().mockResolvedValue(undefined);
|
||||
setPluginsBackend({ ...createMemoryPluginsBackend(), answerPrompt });
|
||||
render(<InputDialogsHost />);
|
||||
act(() => {
|
||||
handlePluginEvent({
|
||||
type: "promptRequested",
|
||||
pluginId: "p",
|
||||
promptId: "5",
|
||||
prompt: { kind: "input", prompt: "Notes", value: "one", multiline: true },
|
||||
});
|
||||
});
|
||||
const area = await screen.findByRole("textbox");
|
||||
expect(area.tagName).toBe("TEXTAREA");
|
||||
await user.clear(area);
|
||||
await user.type(area, "a{Enter}b{Control>}{Enter}{/Control}");
|
||||
await waitFor(() => expect(answerPrompt).toHaveBeenCalledWith("5", ["a\nb"]));
|
||||
});
|
||||
|
||||
it("returns the typed text untrimmed", async () => {
|
||||
const user = userEvent.setup();
|
||||
const answerPrompt = vi.fn().mockResolvedValue(undefined);
|
||||
@@ -553,7 +573,7 @@ describe("plugin prompts", () => {
|
||||
type: "promptRequested",
|
||||
pluginId: "p",
|
||||
promptId: "4",
|
||||
prompt: { kind: "input", prompt: "Name", value: " Main " },
|
||||
prompt: { kind: "input", prompt: "Name", value: " Main ", multiline: false },
|
||||
});
|
||||
});
|
||||
await user.click(await screen.findByRole("button", { name: "OK" }));
|
||||
@@ -569,7 +589,7 @@ describe("plugin prompts", () => {
|
||||
type: "promptRequested",
|
||||
pluginId: "p",
|
||||
promptId: "3",
|
||||
prompt: { kind: "input", prompt: "Name", value: null },
|
||||
prompt: { kind: "input", prompt: "Name", value: null, multiline: false },
|
||||
});
|
||||
});
|
||||
await screen.findByRole("dialog");
|
||||
|
||||
@@ -327,6 +327,7 @@ async function askUser(prompt: Prompt, signal: AbortSignal): Promise<string[] |
|
||||
initial: prompt.value ?? "",
|
||||
validate: () => null,
|
||||
keepWhitespace: true,
|
||||
multiline: prompt.multiline,
|
||||
signal,
|
||||
});
|
||||
return text === null ? null : [text];
|
||||
|
||||
@@ -32,3 +32,31 @@
|
||||
.activeRow {
|
||||
color: var(--fg-strong);
|
||||
}
|
||||
.area {
|
||||
resize: vertical;
|
||||
min-height: 60px;
|
||||
max-height: 240px;
|
||||
padding: 4px 8px;
|
||||
border: 0;
|
||||
border-radius: var(--radius-control, 4px);
|
||||
background: var(--bg-field, var(--bg-button));
|
||||
box-shadow: inset 0 0 0 1px var(--border-button);
|
||||
color: var(--fg-strong);
|
||||
font-family: inherit;
|
||||
font-size: var(--fs-ui);
|
||||
line-height: 20px;
|
||||
outline: none;
|
||||
}
|
||||
.area:focus {
|
||||
box-shadow:
|
||||
inset 0 0 0 1px var(--focus-ring),
|
||||
0 0 0 1px var(--focus-ring);
|
||||
}
|
||||
.area[aria-invalid="true"] {
|
||||
box-shadow:
|
||||
inset 0 0 0 1px var(--danger),
|
||||
0 0 0 1px var(--danger);
|
||||
}
|
||||
.area::placeholder {
|
||||
color: var(--fg-faint);
|
||||
}
|
||||
@@ -31,6 +31,8 @@ export interface PromptInputOptions {
|
||||
signal?: AbortSignal;
|
||||
/** Resolve with the text as typed instead of trimmed. */
|
||||
keepWhitespace?: boolean;
|
||||
/** A multi-line field: Enter adds a line, Cmd/Ctrl+Enter confirms. */
|
||||
multiline?: boolean;
|
||||
}
|
||||
|
||||
export interface ChooseItemOptions {
|
||||
@@ -171,19 +173,43 @@ function PromptDialog({ options, done }: { options: PromptInputOptions; done: (v
|
||||
<T>{options.label}</T>
|
||||
</label>
|
||||
)}
|
||||
<Input
|
||||
data-autofocus
|
||||
aria-label={options.label ?? options.title}
|
||||
value={value}
|
||||
placeholder={options.placeholder}
|
||||
disabled={pending}
|
||||
invalid={(touched || !!serverError) && !!error}
|
||||
onChange={(e) => {
|
||||
setTouched(true);
|
||||
setValue(e.target.value);
|
||||
setServerError(null);
|
||||
}}
|
||||
/>
|
||||
{options.multiline ? (
|
||||
<textarea
|
||||
data-autofocus
|
||||
aria-label={options.label ?? options.title}
|
||||
className={styles.area}
|
||||
rows={5}
|
||||
value={value}
|
||||
placeholder={options.placeholder}
|
||||
disabled={pending}
|
||||
aria-invalid={(touched || !!serverError) && !!error}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && (e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey) {
|
||||
e.preventDefault();
|
||||
void submit();
|
||||
}
|
||||
}}
|
||||
onChange={(e) => {
|
||||
setTouched(true);
|
||||
setValue(e.target.value);
|
||||
setServerError(null);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
data-autofocus
|
||||
aria-label={options.label ?? options.title}
|
||||
value={value}
|
||||
placeholder={options.placeholder}
|
||||
disabled={pending}
|
||||
invalid={(touched || !!serverError) && !!error}
|
||||
onChange={(e) => {
|
||||
setTouched(true);
|
||||
setValue(e.target.value);
|
||||
setServerError(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{(touched || !!serverError) && error && (
|
||||
<div className={styles.error}>
|
||||
<T>{error}</T>
|
||||
|
||||
@@ -62,6 +62,9 @@ interface ui {
|
||||
/// Asks for a line of text, pre-filled with `value`; `none` when dismissed.
|
||||
/// Same rules as `pick`.
|
||||
input: func(prompt: string, value: option<string>) -> result<option<string>, error>;
|
||||
/// Asks for text over several lines, pre-filled with `value`; `none` when
|
||||
/// dismissed. Same rules as `input`; the dialog confirms on Cmd/Ctrl+Enter.
|
||||
input-multiline: func(prompt: string, value: option<string>) -> result<option<string>, error>;
|
||||
/// Asks for confirmation with OK and Cancel: `true` on OK, `false` on
|
||||
/// Cancel or Escape. Same rules as `pick`.
|
||||
confirm: func(title: string, body: string) -> result<bool, error>;
|
||||
|
||||
Reference in new issue
Block a user