feat: delete fields tool

fix: field rename
fix: list fields now scrolls
This commit is contained in:
Emmaline Autumn 2025-06-06 10:50:27 -06:00
parent 7a3b3f2161
commit 7c19ada88b
7 changed files with 117 additions and 17 deletions

View File

@ -1,7 +1,7 @@
import { cliPrompt } from "./prompts.ts"; import { cliPrompt } from "./prompts.ts";
import type { TerminalBlock } from "./TerminalLayout.ts"; import type { TerminalBlock } from "./TerminalLayout.ts";
type prompt = [string, (v?: string) => boolean] | string; type prompt = [string, (v?: string) => boolean | undefined] | string;
export async function forceArgs( export async function forceArgs(
args: string[], args: string[],

View File

@ -13,6 +13,7 @@ const toolRegistry: [string, Promise<{ default: ITool }>][] = [
["checkCode", import("../tools/checkCode.ts")], ["checkCode", import("../tools/checkCode.ts")],
["fieldRename", import("../tools/fieldRename.ts")], ["fieldRename", import("../tools/fieldRename.ts")],
["listFormFields", import("../tools/listFormFields.ts")], ["listFormFields", import("../tools/listFormFields.ts")],
["deleteFields", import("../tools/deleteFields.ts")],
]; ];
export class PdfToolsCli { export class PdfToolsCli {

View File

@ -1,4 +1,5 @@
import type { callback } from "../types.ts"; import type { callback } from "../types.ts";
import { log } from "util/logfile.ts";
import { type CLICharEvent, InputManager } from "./InputManager.ts"; import { type CLICharEvent, InputManager } from "./InputManager.ts";
import { cliLog } from "./prompts.ts"; import { cliLog } from "./prompts.ts";
import { colorize } from "./style.ts"; import { colorize } from "./style.ts";
@ -127,13 +128,16 @@ export async function selectMenuInteractive(
export async function multiSelectMenuInteractive( export async function multiSelectMenuInteractive(
q: string, q: string,
options: string[] | [string, callback][], options: (string | [string, callback])[],
config?: ISelectMenuConfig, config?: ISelectMenuConfig & { allOption?: boolean },
): Promise<string[] | null> { ): Promise<string[] | null> {
Deno.stdin.setRaw(true); Deno.stdin.setRaw(true);
let selected = 0; let selected = 0;
let selectedOptions: number[] = config?.initialSelections || []; let selectedOptions: number[] = config?.initialSelections || [];
if (config?.allOption) {
options.unshift("Select All");
}
const rawValues = options.map((i) => typeof i === "string" ? i : i[0]); const rawValues = options.map((i) => typeof i === "string" ? i : i[0]);
if (rawValues.length !== options.length) { if (rawValues.length !== options.length) {
@ -145,6 +149,21 @@ export async function multiSelectMenuInteractive(
terminalBlock.setRenderHeight(Deno.consoleSize().rows); terminalBlock.setRenderHeight(Deno.consoleSize().rows);
} }
const checkSelectAll = () => {
if (selectedOptions.includes(0)) {
log("yeet");
selectedOptions = [];
} else {
log("neat");
selectedOptions = Array.from(options).map((_, i) => i);
}
};
const validateSelectAll = () => {
const allPresent = selectedOptions.length == options.length;
if (!allPresent) selectedOptions = selectedOptions.filter((e) => e != 0);
};
let range: [number, number] = [terminalBlock.lineCount, 1]; let range: [number, number] = [terminalBlock.lineCount, 1];
function renderMenu() { function renderMenu() {
const { rows } = Deno.consoleSize(); const { rows } = Deno.consoleSize();
@ -193,11 +212,14 @@ export async function multiSelectMenuInteractive(
const onSpace = (e: CLICharEvent) => { const onSpace = (e: CLICharEvent) => {
if (e.detail.char !== " ") return; if (e.detail.char !== " ") return;
e.stopImmediatePropagation(); e.stopImmediatePropagation();
if (selectedOptions.includes(selected)) { if (config?.allOption && selected === 0) {
checkSelectAll();
} else if (selectedOptions.includes(selected)) {
selectedOptions = selectedOptions.filter((i) => i !== selected); selectedOptions = selectedOptions.filter((i) => i !== selected);
} else { } else {
selectedOptions.push(selected); selectedOptions.push(selected);
} }
validateSelectAll();
renderMenu(); renderMenu();
}; };
@ -299,7 +321,7 @@ if (import.meta.main) {
"ximenia", "ximenia",
"yuzu", "yuzu",
"zucchini", "zucchini",
], { terminalBlock: block }); ], { terminalBlock: block, allOption: true });
cliLog(val || "No value"); cliLog(val || "No value");
// Deno.stdout.writeSync(new TextEncoder().encode("\x07")); // Deno.stdout.writeSync(new TextEncoder().encode("\x07"));

View File

@ -1,9 +1,9 @@
{ {
"name": "@bearmetal/pdf-tools", "name": "@bearmetal/pdf-tools",
"version": "1.0.8-l", "version": "1.0.8-n",
"license": "GPL 3.0", "license": "GPL 3.0",
"tasks": { "tasks": {
"dev": "deno run -A --env-file=.env main.ts", "dev": "deno run -A main.ts",
"compile": "deno compile -o pdf-tools.exe --target x86_64-pc-windows-msvc --include ./asciiart.txt -A ./main.ts", "compile": "deno compile -o pdf-tools.exe --target x86_64-pc-windows-msvc --include ./asciiart.txt -A ./main.ts",
"install": "deno install -fgq --import-map ./deno.json -n checkfields -R ./main.ts", "install": "deno install -fgq --import-map ./deno.json -n checkfields -R ./main.ts",
"debug": "deno run -A --env-file=.env --inspect-wait --watch main.ts" "debug": "deno run -A --env-file=.env --inspect-wait --watch main.ts"

50
tools/deleteFields.ts Normal file
View File

@ -0,0 +1,50 @@
import { forceArgs } from "../cli/forceArgs.ts";
import { cliPrompt } from "../cli/prompts.ts";
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";
description = "delete multiple form fields from a PDF";
block?: TerminalBlock;
async run(pdfPath: string = "") {
if (!this.block) this.block = new TerminalBlock();
[pdfPath] = await forceArgs([pdfPath], [[
"Please provide path to PDF",
(d) => d?.endsWith(".pdf"),
]], this.block);
const pdf = await loadPdf(pdfPath);
const form = pdf.getForm();
const fields = form.getFields();
let updatesMade = false;
await multiSelectMenuInteractive(
`${pdfPath}\nSelect fields to delete:`,
fields.map<[string, callback]>((
f,
) => [f.getName(), () => {
while (f.acroField.getWidgets().length) {
f.acroField.removeWidget(0);
}
form.removeField(f);
updatesMade = true;
}]),
);
if (!updatesMade) return;
const path = await cliPrompt(
"Save to path (or hit enter to keep current):",
this.block,
) || pdfPath;
await savePdf(pdf, path);
}
help?: (() => Promise<void> | void) | undefined;
done?: (() => Promise<void> | void) | undefined;
setBlock(block: TerminalBlock) {
this.block = block;
}
}
export default new DeleteFormFields();

View File

@ -404,7 +404,7 @@ class RenameFields implements ITool {
if (!this.block) { if (!this.block) {
this.block = new TerminalBlock(); this.block = new TerminalBlock();
} }
this.block.setPreserveHistory(true); this.block.setPreserveHistory(false);
[pdfPath, pattern, change] = await forceArgs( [pdfPath, pattern, change] = await forceArgs(
[pdfPath, pattern, change], [pdfPath, pattern, change],

View File

@ -1,8 +1,8 @@
import { forceArgs } from "../cli/forceArgs.ts"; import { forceArgs } from "../cli/forceArgs.ts";
import { cliAlert } from "../cli/prompts.ts";
import { TerminalBlock } from "../cli/TerminalLayout.ts"; import { TerminalBlock } from "../cli/TerminalLayout.ts";
import { loadPdfForm } from "util/saveLoadPdf.ts"; import { loadPdfForm } from "util/saveLoadPdf.ts";
import type { ITool } from "../types.ts"; import type { ITool } from "../types.ts";
import { InputManager } from "../cli/InputManager.ts";
export class ListFormFields implements ITool { export class ListFormFields implements ITool {
name = "listformfields"; name = "listformfields";
@ -12,21 +12,48 @@ export class ListFormFields implements ITool {
if (!this.block) { if (!this.block) {
this.block = new TerminalBlock(); this.block = new TerminalBlock();
} }
this.block.setPreserveHistory(true); this.block.setPreserveHistory(false);
[pdfPath] = await forceArgs([pdfPath], [[ [pdfPath] = await forceArgs([pdfPath], [[
"Please provide path to PDF:", "Please provide path to PDF:",
(p) => !!p && p.endsWith(".pdf"), (p) => !!p && p.endsWith(".pdf"),
]], this.block); ]], this.block);
const lines = [pdfPath];
let rLines: string[] = [];
const form = await loadPdfForm(pdfPath); const form = await loadPdfForm(pdfPath);
const fields = form.getFields(); const fields = form.getFields();
const fieldNames = fields.map((f) => f.getName()); const fieldNames = fields.map((f) => f.getName());
const lines = [];
for (const fieldName of fieldNames) { let offset = 0;
lines.push(fieldName);
} const buildRLines = () => {
this.block.setLines(lines, [0, 1]); rLines = fieldNames.slice(offset, this.block!.getRenderHeight());
await cliAlert("", this.block); this.block!.setLines(lines.concat(rLines), [0, 1]);
};
buildRLines();
await new Promise<void>((res) => {
const im = InputManager.getInstance();
const up = () => {
if (fieldNames.length < this.block!.getRenderHeight() - 1) return;
offset = Math.max(0, offset - 1);
buildRLines();
};
const down = () => {
if (fieldNames.length < this.block!.getRenderHeight() - 1) return;
offset = Math.min(fieldNames.length, offset + 1);
buildRLines();
};
const enter = () => {
res();
im.removeEventListener("arrow-up", up);
im.removeEventListener("arrow-down", down);
im.removeEventListener("enter", enter);
};
im.addEventListener("arrow-up", up);
im.addEventListener("arrow-down", down);
im.addEventListener("enter", enter);
});
} }
setBlock(terminalBlock: TerminalBlock) { setBlock(terminalBlock: TerminalBlock) {
this.block = terminalBlock; this.block = terminalBlock;