feat: esc to cancel select menus
feat: fieldVisibility tool
This commit is contained in:
parent
7c19ada88b
commit
04d5044c43
@ -6,12 +6,13 @@ interface EventMap {
|
||||
exit: Event;
|
||||
enter: Event;
|
||||
backspace: Event;
|
||||
escape: Event;
|
||||
delete: Event;
|
||||
"arrow-left": Event;
|
||||
"arrow-right": Event;
|
||||
"arrow-up": Event;
|
||||
"arrow-down": Event;
|
||||
[key: string]: Event;
|
||||
// [key: string]: Event;
|
||||
}
|
||||
|
||||
interface EventDetailMap {
|
||||
@ -146,6 +147,12 @@ export class InputManager extends ManagerEventTarget {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (byte === 27 && i + 1 >= n) {
|
||||
this.dispatchEvent(new Event("escape"));
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Escape sequences
|
||||
if (byte === 27 && i + 1 < n && buf[i + 1] === 91) {
|
||||
const code = buf[i + 2];
|
||||
@ -193,4 +200,23 @@ export class InputManager extends ManagerEventTarget {
|
||||
|
||||
if (raw) await Deno.stdin.setRaw(false);
|
||||
}
|
||||
|
||||
dispatchKey(key: string) {
|
||||
switch (key) {
|
||||
case "enter":
|
||||
case "backspace":
|
||||
case "arrow-up":
|
||||
case "arrow-down":
|
||||
case "arrow-right":
|
||||
case "arrow-left":
|
||||
case "delete":
|
||||
case "escape":
|
||||
this.dispatchEvent(new Event(key));
|
||||
break;
|
||||
default:
|
||||
this.dispatchEvent(
|
||||
new CLICharEvent({ key: key.charCodeAt(0), char: key }),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ const toolRegistry: [string, Promise<{ default: ITool }>][] = [
|
||||
["fieldRename", import("../tools/fieldRename.ts")],
|
||||
["listFormFields", import("../tools/listFormFields.ts")],
|
||||
["deleteFields", import("../tools/deleteFields.ts")],
|
||||
["fieldVisibility", import("../tools/fieldVisibility.ts")],
|
||||
];
|
||||
|
||||
export class PdfToolsCli {
|
||||
|
@ -1,5 +1,4 @@
|
||||
import type { callback } from "../types.ts";
|
||||
import { log } from "util/logfile.ts";
|
||||
import { type CLICharEvent, InputManager } from "./InputManager.ts";
|
||||
import { cliLog } from "./prompts.ts";
|
||||
import { colorize } from "./style.ts";
|
||||
@ -92,7 +91,17 @@ export async function selectMenuInteractive(
|
||||
inputBuffer = inputBuffer.slice(0, -1);
|
||||
};
|
||||
|
||||
let resolve: null | ((value: string) => void) = null;
|
||||
let resolve: null | ((value: string | null) => void) = null;
|
||||
|
||||
const onEscape = () => {
|
||||
im.removeEventListener("arrow-up", onUp);
|
||||
im.removeEventListener("arrow-down", onDown);
|
||||
im.removeEventListener("char", onKey);
|
||||
im.removeEventListener("backspace", onBackspace);
|
||||
im.removeEventListener("enter", onEnter);
|
||||
im.removeEventListener("escape", onEscape);
|
||||
resolve?.(null);
|
||||
};
|
||||
|
||||
const onEnter = (e: Event) => {
|
||||
e.stopImmediatePropagation();
|
||||
@ -108,22 +117,24 @@ export async function selectMenuInteractive(
|
||||
im.removeEventListener("char", onKey);
|
||||
im.removeEventListener("backspace", onBackspace);
|
||||
im.removeEventListener("enter", onEnter);
|
||||
im.removeEventListener("escape", onEscape);
|
||||
resolve?.(options[selected]);
|
||||
};
|
||||
|
||||
renderMenu();
|
||||
await new Promise<string>((res) => {
|
||||
const final = await new Promise<string | null>((res) => {
|
||||
resolve = res;
|
||||
im.addEventListener("char", onKey);
|
||||
im.addEventListener("backspace", onBackspace);
|
||||
im.addEventListener("enter", onEnter);
|
||||
im.addEventListener("arrow-up", onUp);
|
||||
im.addEventListener("arrow-down", onDown);
|
||||
im.addEventListener("escape", onEscape);
|
||||
});
|
||||
|
||||
terminalBlock.setLines(["Selected: " + options[selected]], range);
|
||||
terminalBlock.setLines(["Selected: " + final], range);
|
||||
|
||||
return options[selected];
|
||||
return final;
|
||||
}
|
||||
|
||||
export async function multiSelectMenuInteractive(
|
||||
@ -151,10 +162,8 @@ export async function multiSelectMenuInteractive(
|
||||
|
||||
const checkSelectAll = () => {
|
||||
if (selectedOptions.includes(0)) {
|
||||
log("yeet");
|
||||
selectedOptions = [];
|
||||
} else {
|
||||
log("neat");
|
||||
selectedOptions = Array.from(options).map((_, i) => i);
|
||||
}
|
||||
};
|
||||
@ -195,7 +204,7 @@ export async function multiSelectMenuInteractive(
|
||||
const im = InputManager.getInstance();
|
||||
im.activate();
|
||||
|
||||
let resolve = null as null | ((value: number[]) => void);
|
||||
let resolve = null as null | ((value: number[] | null) => void);
|
||||
|
||||
const onUp = (e: Event) => {
|
||||
e.stopImmediatePropagation();
|
||||
@ -223,24 +232,36 @@ export async function multiSelectMenuInteractive(
|
||||
renderMenu();
|
||||
};
|
||||
|
||||
const onEnter = (e: Event) => {
|
||||
e.stopImmediatePropagation();
|
||||
resolve?.(selectedOptions);
|
||||
const onEscape = () => {
|
||||
im.removeEventListener("arrow-up", onUp);
|
||||
im.removeEventListener("arrow-down", onDown);
|
||||
im.removeEventListener("char", onSpace);
|
||||
im.removeEventListener("enter", onEnter);
|
||||
im.removeEventListener("escape", onEscape);
|
||||
resolve?.(null);
|
||||
};
|
||||
|
||||
const onEnter = (e: Event) => {
|
||||
e.stopImmediatePropagation();
|
||||
im.removeEventListener("arrow-up", onUp);
|
||||
im.removeEventListener("arrow-down", onDown);
|
||||
im.removeEventListener("char", onSpace);
|
||||
im.removeEventListener("enter", onEnter);
|
||||
im.removeEventListener("escape", onEscape);
|
||||
resolve?.(selectedOptions);
|
||||
};
|
||||
|
||||
renderMenu();
|
||||
|
||||
const selections = await new Promise<number[]>((res) => {
|
||||
const selections = await new Promise<number[] | null>((res) => {
|
||||
resolve = res;
|
||||
im.addEventListener("arrow-up", onUp);
|
||||
im.addEventListener("arrow-down", onDown);
|
||||
im.addEventListener("char", onSpace);
|
||||
im.addEventListener("enter", onEnter);
|
||||
im.addEventListener("escape", onEscape);
|
||||
});
|
||||
if (!selections) return null;
|
||||
for (const optionI of selections) {
|
||||
const option = options[optionI];
|
||||
if (Array.isArray(option)) {
|
||||
@ -322,7 +343,7 @@ if (import.meta.main) {
|
||||
"yuzu",
|
||||
"zucchini",
|
||||
], { terminalBlock: block, allOption: true });
|
||||
cliLog(val || "No value");
|
||||
cliLog(val || "No value", block);
|
||||
|
||||
// Deno.stdout.writeSync(new TextEncoder().encode("\x07"));
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import { multiSelectMenuInteractive } from "../cli/selectMenu.ts";
|
||||
import { TerminalBlock } from "../cli/TerminalLayout.ts";
|
||||
import type { callback, ITool } from "../types.ts";
|
||||
import { loadPdf, savePdf } from "util/saveLoadPdf.ts";
|
||||
import { log } from "util/logfile.ts";
|
||||
|
||||
export class DeleteFormFields implements ITool {
|
||||
name = "deleteFormFields";
|
||||
|
133
tools/fieldVisibility.ts
Normal file
133
tools/fieldVisibility.ts
Normal file
@ -0,0 +1,133 @@
|
||||
import { forceArgs } from "../cli/forceArgs.ts";
|
||||
import { selectMenuInteractive } from "../cli/selectMenu.ts";
|
||||
import { TerminalBlock } from "../cli/TerminalLayout.ts";
|
||||
import type { ITool } from "../types.ts";
|
||||
import { loadPdf, savePdf } from "util/saveLoadPdf.ts";
|
||||
|
||||
import { PDFName, type PDFNumber, type PDFWidgetAnnotation } from "pdf-lib";
|
||||
import { cliPrompt } from "../cli/prompts.ts";
|
||||
|
||||
type AcrobatVisibility =
|
||||
| "Visible"
|
||||
| "Hidden"
|
||||
| "VisibleButDoesNotPrint"
|
||||
| "HiddenButPrintable";
|
||||
|
||||
export function getAcrobatVisibility(
|
||||
widget: PDFWidgetAnnotation,
|
||||
): AcrobatVisibility {
|
||||
const raw = widget.dict.lookup(PDFName.of("F")) as PDFNumber;
|
||||
const flags = raw?.asNumber?.() ?? 0;
|
||||
|
||||
const isInvisible = (flags & (1 << 1)) !== 0;
|
||||
const isHidden = (flags & (1 << 2)) !== 0;
|
||||
const isNoPrint = (flags & (1 << 3)) !== 0;
|
||||
const isPrint = (flags & (1 << 2)) === 0 && !isNoPrint;
|
||||
|
||||
if (isInvisible && isHidden) {
|
||||
return isPrint ? "HiddenButPrintable" : "Hidden";
|
||||
} else if (isNoPrint) {
|
||||
return "VisibleButDoesNotPrint";
|
||||
} else {
|
||||
return "Visible";
|
||||
}
|
||||
}
|
||||
|
||||
export function setAcrobatVisibility(
|
||||
widget: PDFWidgetAnnotation,
|
||||
visibility: AcrobatVisibility,
|
||||
) {
|
||||
let flags = 0;
|
||||
|
||||
switch (visibility) {
|
||||
case "Visible":
|
||||
// No visibility bits set
|
||||
break;
|
||||
case "Hidden":
|
||||
flags |= 1 << 1; // Invisible
|
||||
flags |= 1 << 2; // Hidden
|
||||
break;
|
||||
case "VisibleButDoesNotPrint":
|
||||
flags |= 1 << 3; // NoPrint
|
||||
break;
|
||||
case "HiddenButPrintable":
|
||||
flags |= 1 << 1; // Invisible
|
||||
flags |= 1 << 2; // Hidden
|
||||
flags |= 1 << 3; // NoPrint — UNset this to allow print
|
||||
break;
|
||||
}
|
||||
|
||||
widget.dict.set(PDFName.of("F"), widget.dict.context.obj(flags));
|
||||
}
|
||||
|
||||
export class FieldVisibility implements ITool {
|
||||
name = "Field Visibility";
|
||||
description = "Change visibility of fields";
|
||||
block?: TerminalBlock;
|
||||
async run(pdfPath: string) {
|
||||
if (!this.block) this.block = new TerminalBlock();
|
||||
this.block.setPreserveHistory(false);
|
||||
[pdfPath] = await forceArgs([pdfPath], [[
|
||||
"Please provide path to PDF:",
|
||||
(e) => e?.endsWith(".pdf"),
|
||||
]], this.block);
|
||||
|
||||
const pdf = await loadPdf(pdfPath);
|
||||
const form = pdf.getForm();
|
||||
const fields = form.getFields();
|
||||
|
||||
let changesMade = false;
|
||||
|
||||
while (true) {
|
||||
const fieldAndVisibility = await selectMenuInteractive(
|
||||
`Select a field to change visibility (ESC to ${
|
||||
changesMade ? "continue" : "cancel"
|
||||
})`,
|
||||
fields.flatMap((f) => {
|
||||
const name = f.getName();
|
||||
const visibility = f.acroField.getWidgets().map((w, i, a) =>
|
||||
`${name}${a.length > 1 ? "#" + i : ""} :: ${
|
||||
getAcrobatVisibility(w)
|
||||
}`
|
||||
);
|
||||
return visibility;
|
||||
}),
|
||||
);
|
||||
if (!fieldAndVisibility) break;
|
||||
const visibility = await selectMenuInteractive(
|
||||
fieldAndVisibility,
|
||||
[
|
||||
"Visible",
|
||||
"Hidden",
|
||||
"HiddenButPrintable",
|
||||
"VisibleButDoesNotPrint",
|
||||
] as AcrobatVisibility[],
|
||||
) as AcrobatVisibility | null;
|
||||
if (!visibility) continue;
|
||||
|
||||
const [fName, widgetIndex] = fieldAndVisibility.split("::")[0].trim()
|
||||
.split("#");
|
||||
const field = fields.find((f) => f.getName() === fName);
|
||||
if (!field) break;
|
||||
|
||||
const widget = field.acroField.getWidgets()[Number(widgetIndex) || 0];
|
||||
setAcrobatVisibility(widget, visibility);
|
||||
changesMade = true;
|
||||
}
|
||||
|
||||
if (changesMade) {
|
||||
const path = await cliPrompt(
|
||||
"Save to path (or hit enter to keep current):",
|
||||
this.block,
|
||||
) || pdfPath;
|
||||
savePdf(pdf, path);
|
||||
}
|
||||
}
|
||||
help?: (() => Promise<void> | void) | undefined;
|
||||
done?: (() => Promise<void> | void) | undefined;
|
||||
setBlock(block: TerminalBlock) {
|
||||
this.block = block;
|
||||
}
|
||||
}
|
||||
|
||||
export default new FieldVisibility();
|
Loading…
x
Reference in New Issue
Block a user