From 7c19ada88b3c6770710df427af089a2d1be010ab Mon Sep 17 00:00:00 2001 From: Emmaline Date: Fri, 6 Jun 2025 10:50:27 -0600 Subject: [PATCH] feat: delete fields tool fix: field rename fix: list fields now scrolls --- cli/forceArgs.ts | 2 +- cli/index.ts | 1 + cli/selectMenu.ts | 30 +++++++++++++++++++++---- deno.json | 6 ++--- tools/deleteFields.ts | 50 +++++++++++++++++++++++++++++++++++++++++ tools/fieldRename.ts | 2 +- tools/listFormFields.ts | 43 ++++++++++++++++++++++++++++------- 7 files changed, 117 insertions(+), 17 deletions(-) create mode 100644 tools/deleteFields.ts diff --git a/cli/forceArgs.ts b/cli/forceArgs.ts index f901f46..784c227 100644 --- a/cli/forceArgs.ts +++ b/cli/forceArgs.ts @@ -1,7 +1,7 @@ import { cliPrompt } from "./prompts.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( args: string[], diff --git a/cli/index.ts b/cli/index.ts index 83f7f91..042ede6 100644 --- a/cli/index.ts +++ b/cli/index.ts @@ -13,6 +13,7 @@ const toolRegistry: [string, Promise<{ default: ITool }>][] = [ ["checkCode", import("../tools/checkCode.ts")], ["fieldRename", import("../tools/fieldRename.ts")], ["listFormFields", import("../tools/listFormFields.ts")], + ["deleteFields", import("../tools/deleteFields.ts")], ]; export class PdfToolsCli { diff --git a/cli/selectMenu.ts b/cli/selectMenu.ts index 5f23e32..b4bc824 100644 --- a/cli/selectMenu.ts +++ b/cli/selectMenu.ts @@ -1,4 +1,5 @@ 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"; @@ -127,13 +128,16 @@ export async function selectMenuInteractive( export async function multiSelectMenuInteractive( q: string, - options: string[] | [string, callback][], - config?: ISelectMenuConfig, + options: (string | [string, callback])[], + config?: ISelectMenuConfig & { allOption?: boolean }, ): Promise { Deno.stdin.setRaw(true); let selected = 0; let selectedOptions: number[] = config?.initialSelections || []; + if (config?.allOption) { + options.unshift("Select All"); + } const rawValues = options.map((i) => typeof i === "string" ? i : i[0]); if (rawValues.length !== options.length) { @@ -145,6 +149,21 @@ export async function multiSelectMenuInteractive( 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]; function renderMenu() { const { rows } = Deno.consoleSize(); @@ -193,11 +212,14 @@ export async function multiSelectMenuInteractive( const onSpace = (e: CLICharEvent) => { if (e.detail.char !== " ") return; e.stopImmediatePropagation(); - if (selectedOptions.includes(selected)) { + if (config?.allOption && selected === 0) { + checkSelectAll(); + } else if (selectedOptions.includes(selected)) { selectedOptions = selectedOptions.filter((i) => i !== selected); } else { selectedOptions.push(selected); } + validateSelectAll(); renderMenu(); }; @@ -299,7 +321,7 @@ if (import.meta.main) { "ximenia", "yuzu", "zucchini", - ], { terminalBlock: block }); + ], { terminalBlock: block, allOption: true }); cliLog(val || "No value"); // Deno.stdout.writeSync(new TextEncoder().encode("\x07")); diff --git a/deno.json b/deno.json index 66b6252..269c573 100644 --- a/deno.json +++ b/deno.json @@ -1,9 +1,9 @@ { "name": "@bearmetal/pdf-tools", - "version": "1.0.8-l", + "version": "1.0.8-n", "license": "GPL 3.0", "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", "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" @@ -32,4 +32,4 @@ "./must_await_cli_prompts.ts" ] } -} \ No newline at end of file +} diff --git a/tools/deleteFields.ts b/tools/deleteFields.ts new file mode 100644 index 0000000..04130ed --- /dev/null +++ b/tools/deleteFields.ts @@ -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) | undefined; + done?: (() => Promise | void) | undefined; + setBlock(block: TerminalBlock) { + this.block = block; + } +} +export default new DeleteFormFields(); diff --git a/tools/fieldRename.ts b/tools/fieldRename.ts index 1b75e6a..8c404e9 100644 --- a/tools/fieldRename.ts +++ b/tools/fieldRename.ts @@ -404,7 +404,7 @@ class RenameFields implements ITool { if (!this.block) { this.block = new TerminalBlock(); } - this.block.setPreserveHistory(true); + this.block.setPreserveHistory(false); [pdfPath, pattern, change] = await forceArgs( [pdfPath, pattern, change], diff --git a/tools/listFormFields.ts b/tools/listFormFields.ts index a75c64f..090cb5c 100644 --- a/tools/listFormFields.ts +++ b/tools/listFormFields.ts @@ -1,8 +1,8 @@ import { forceArgs } from "../cli/forceArgs.ts"; -import { cliAlert } from "../cli/prompts.ts"; import { TerminalBlock } from "../cli/TerminalLayout.ts"; import { loadPdfForm } from "util/saveLoadPdf.ts"; import type { ITool } from "../types.ts"; +import { InputManager } from "../cli/InputManager.ts"; export class ListFormFields implements ITool { name = "listformfields"; @@ -12,21 +12,48 @@ export class ListFormFields implements ITool { if (!this.block) { this.block = new TerminalBlock(); } - this.block.setPreserveHistory(true); + this.block.setPreserveHistory(false); [pdfPath] = await forceArgs([pdfPath], [[ "Please provide path to PDF:", (p) => !!p && p.endsWith(".pdf"), ]], this.block); + const lines = [pdfPath]; + let rLines: string[] = []; const form = await loadPdfForm(pdfPath); const fields = form.getFields(); const fieldNames = fields.map((f) => f.getName()); - const lines = []; - for (const fieldName of fieldNames) { - lines.push(fieldName); - } - this.block.setLines(lines, [0, 1]); - await cliAlert("", this.block); + + let offset = 0; + + const buildRLines = () => { + rLines = fieldNames.slice(offset, this.block!.getRenderHeight()); + this.block!.setLines(lines.concat(rLines), [0, 1]); + }; + buildRLines(); + + await new Promise((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) { this.block = terminalBlock;