From 6a3a8d39227fe98c937863d0e045e14283f3b36c Mon Sep 17 00:00:00 2001 From: Rizumu Ayaka Date: Sat, 12 Oct 2024 02:05:31 +0800 Subject: [PATCH] feat(cli): add qifi command and parse args --- cspell.config.yaml | 1 + package.json | 1 + packages/cli/README.md | 33 ++------ packages/cli/bin/qifi.mjs | 5 ++ packages/cli/build.config.ts | 1 + packages/cli/package.json | 9 ++- packages/cli/src/cli.ts | 85 +++++++++++++++++++ packages/cli/src/index.ts | 69 +--------------- packages/cli/src/parse-args.ts | 144 +++++++++++++++++++++++++++++++++ packages/cli/src/shared.ts | 12 +++ pnpm-lock.yaml | 23 ++---- 11 files changed, 269 insertions(+), 114 deletions(-) create mode 100755 packages/cli/bin/qifi.mjs create mode 100644 packages/cli/src/cli.ts create mode 100644 packages/cli/src/parse-args.ts create mode 100644 packages/cli/src/shared.ts diff --git a/cspell.config.yaml b/cspell.config.yaml index 35efc8c..e550f39 100644 --- a/cspell.config.yaml +++ b/cspell.config.yaml @@ -9,6 +9,7 @@ words: - luby - Nuxt - pako + - Positionals - qifi - QRCODE - Soliton diff --git a/package.json b/package.json index cadca69..3dcdf45 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "nuxt": "^3.13.2", "ohash": "^1.1.4", "pinia": "^2.2.4", + "tsx": "^4.19.1", "typescript": "^5.6.2", "unbuild": "^2.0.0", "vitest": "^2.1.1", diff --git a/packages/cli/README.md b/packages/cli/README.md index 6ae0b60..8239299 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -1,4 +1,4 @@ -# @qifi/generate +# QiFi CLI

@@ -9,11 +9,11 @@ docs - docs + sponsors

-Stream Generated QR Codes for data transmission +Stream Generated QR Codes for file transmission in your terminal ## Sponsors @@ -31,29 +31,6 @@ Stream Generated QR Codes for data transmission ## Usage -```javascript -import { - createGeneraterANSI, - createGeneraterUnicode, - createGeneraterUnicodeCompact, - createGeneraterSVG, - createGeneraterQRCodeArray, -} from '@qifi/generate' - -const generaterSvg = createGeneraterSVG(new Uint8Array(file.buffer)) - -const generaterANSI = createGeneraterANSI(new Uint8Array(file.buffer), { - // Size of each data slice - sliceSize: 250, - // Error correction level - ecc: 'L', - // Border width - border: 2, -}) - -// display QR Code in terminal -for (const blockQRCode of generaterANSI()) { - console.log(blockQRCode) -} - +```bash +npx qifi [options] ``` diff --git a/packages/cli/bin/qifi.mjs b/packages/cli/bin/qifi.mjs new file mode 100755 index 0000000..ad4bca8 --- /dev/null +++ b/packages/cli/bin/qifi.mjs @@ -0,0 +1,5 @@ +#!/usr/bin/env node +'use strict' +import { main } from '../dist/cli.mjs' + +main() diff --git a/packages/cli/build.config.ts b/packages/cli/build.config.ts index b5264d2..110eb47 100644 --- a/packages/cli/build.config.ts +++ b/packages/cli/build.config.ts @@ -3,6 +3,7 @@ import { defineBuildConfig } from 'unbuild' export default defineBuildConfig({ entries: [ 'src/index.ts', + 'src/cli.ts', ], declaration: true, rollup: { diff --git a/packages/cli/package.json b/packages/cli/package.json index 14d0cf2..d31e0fd 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -22,17 +22,20 @@ "main": "./dist/index.mjs", "module": "./dist/index.mjs", "types": "./dist/index.d.mts", + "bin": { + "qifi": "./bin/qifi.mjs" + }, "files": [ "dist" ], "scripts": { - "dev": "tsx ./src", + "dev": "tsx ./src/cli", "build": "unbuild", "stub": "unbuild --stub" }, "dependencies": { "@qifi/generate": "workspace:*", - "mime": "^4.0.4", - "tsx": "^4.19.1" + "cac": "^6.7.14", + "mime": "^4.0.4" } } diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts new file mode 100644 index 0000000..90dc45a --- /dev/null +++ b/packages/cli/src/cli.ts @@ -0,0 +1,85 @@ +import fs from 'node:fs' +import path from 'node:path' +import process from 'node:process' +import readline from 'node:readline' +import { appendFileHeaderMetaToBuffer, createGeneraterANSI, createGeneraterUnicode, createGeneraterUnicodeCompact } from '@qifi/generate' +import mime from 'mime' +import { + black, + boostEcc, + border, + compact, + contentType, + ecc, + fps, + invert, + maskPattern, + maxVersion, + minVersion, + positional, + prefix, + sliceSize, + unicode, + white, +} from './parse-args' +import { handleArgsError } from './shared' + +function chooseGenerater() { + if (compact) { + return createGeneraterUnicodeCompact + } + if (unicode) { + return createGeneraterUnicode + } + return createGeneraterANSI +} + +// Function to read file and generate QR codes +export async function main() { + const fullPath = path.resolve(positional) + + console.log('fullPath:', fullPath) + await new Promise(resolve => setTimeout(resolve, 1000)) + if (!fs.existsSync(fullPath)) { + handleArgsError(new TypeError(`File not found: ${fullPath}`)) + } + + const fileBuffer = fs.readFileSync(fullPath) + const data = new Uint8Array(fileBuffer) + const meta = { + filename: path.basename(fullPath), + contentType: contentType || mime.getType(fullPath) || 'application/octet-stream', + } + + const merged = appendFileHeaderMetaToBuffer(data, meta) + + const generator = chooseGenerater()(merged, { + sliceSize, + urlPrefix: prefix, + ...{ + whiteChar: white, + blackChar: black, + } as any, + invert, + ecc, + maskPattern, + boostEcc, + minVersion, + maxVersion, + border, + }) + + // Clear console function + const clearConsole = () => { + readline.cursorTo(process.stdout, 0, 0) + readline.clearScreenDown(process.stdout) + } + + // Display QR codes + for (const blockQRCode of generator.fountain()) { + clearConsole() + process.stdout.write(`${blockQRCode}\n`) + process.stdout.write(`${meta.filename} (${meta.contentType}) | size: ${data.length} bytes`) + await new Promise(resolve => setTimeout(resolve, 1000 / fps)) + } +} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 4083895..5c96d4e 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,67 +1,2 @@ -#!/usr/bin/env tsx - -import fs from 'node:fs' -import path from 'node:path' -import process from 'node:process' -import readline from 'node:readline' -import { appendFileHeaderMetaToBuffer, createGeneraterANSI } from '@qifi/generate' -import mime from 'mime' - -// Function to read file and generate QR codes -async function generateQRCodes(filePath: string, sliceSize: number = 80, fps: number = 20) { - const fullPath = path.resolve(filePath) - - console.log('fullPath:', fullPath) - await new Promise(resolve => setTimeout(resolve, 1000)) - if (!fs.existsSync(fullPath)) { - console.error(`File not found: ${fullPath}`) - process.exit(1) - } - - const fileBuffer = fs.readFileSync(fullPath) - const data = new Uint8Array(fileBuffer) - const meta = { - filename: path.basename(fullPath), - contentType: mime.getType(fullPath) || 'application/octet-stream', - } - - const merged = appendFileHeaderMetaToBuffer(data, meta) - - const generator = createGeneraterANSI(merged, { - urlPrefix: 'https://qrss.netlify.app/#', - sliceSize, - border: 2, - }) - - // Clear console function - const clearConsole = () => { - readline.cursorTo(process.stdout, 0, 0) - readline.clearScreenDown(process.stdout) - } - - // Display QR codes - for (const blockQRCode of generator.fountain()) { - clearConsole() - process.stdout.write(`${blockQRCode}\n`) - process.stdout.write(`${meta.filename} (${meta.contentType}) | size: ${data.length} bytes`) - await new Promise(resolve => setTimeout(resolve, 1000 / fps)) - } -} - -// Parse command line arguments -const args = process.argv.slice(2) -if (args.length < 1) { - console.error('Usage: qr-file-transfer [slice-size]') - process.exit(1) -} - -const [filePath, sliceSizeStr, fpsStr] = args -const sliceSize = sliceSizeStr ? Number.parseInt(sliceSizeStr, 10) : undefined -const fps = fpsStr ? Number.parseInt(fpsStr, 10) : undefined - -if (!filePath) { - console.error('File path is required') - process.exit(1) -} - -generateQRCodes(filePath, sliceSize, fps) +console.warn('qifi is a cli package, it should not be imported in other packages') +export default function () {} diff --git a/packages/cli/src/parse-args.ts b/packages/cli/src/parse-args.ts new file mode 100644 index 0000000..7c703a5 --- /dev/null +++ b/packages/cli/src/parse-args.ts @@ -0,0 +1,144 @@ +import type { QrCodeGenerateSvgOptions } from 'uqr' +import process from 'node:process' +import cac from 'cac' +import { version } from '../package.json' +import { ExitCode, handleArgsError } from './shared' + +export const { + values: { + sliceSize, + fps, + prefix, + unicode, + compact, + white, + black, + invert, + contentType, + ecc, + maskPattern, + boostEcc, + minVersion, + maxVersion, + border, + }, + positional, +} = getArgs() + +function getArgs() { + const cli = cac('qifi') + cli + .version(version) + .usage('[file]') + .option('-S, --slice-size ', 'Size of each slice of the file, default is 80') + .option('-F, --fps ', 'Frames per second, default is 10') + .option('-P, --prefix ', 'URL prefix to use for the QR code, default is https://qrss.netlify.app/#') + .option('-U, --unicode', 'Render QR Code to Unicode string for each pixel. By default it uses █ and ░ to represent black and white pixels, and it can be customizable.', { default: false }) + .option('-C, --compact', 'Render QR Code with two rows into one line with unicode ▀, ▄, █, . It is useful when you want to display QR Code in terminal with limited height.', { default: false }) + .option('--white ', 'Character to represent white pixel in Unicode rendering, need to set unicode to true') + .option('--black ', 'Character to represent black pixel in Unicode rendering, need to set unicode to true') + .option('-I, --invert', 'Invert black and white', { default: false }) + .option('--content-type ', 'Content type of the file, default is auto') + .option('--ecc ', [ + `Error correction level, default is 'L'`, + '\tL - Allows recovery of up to 7% data loss', + '\tM - Allows recovery of up to 15% data loss', + '\tQ - Allows recovery of up to 25% data loss', + '\tH - Allows recovery of up to 30% data loss', + ].join('\n')) + .option('--mask-pattern ', 'Mask pattern to use, default is -1 (auto)') + .option('--boost-ecc', 'Boost the error correction level to the maximum allowed by the version and size', { default: false }) + .option('--min-version ', 'Minimum version of the QR code (1-40), default is 1') + .option('--max-version ', 'Maximum version of the QR code (1-40), default is 40') + .option('--border ', 'Border around the QR code, default is 1') + .help() + + const result = cli.parse(process.argv) + + const { options, args } = result + + if (options.help || options.version) { + process.exit(ExitCode.Success) + } + + if (args.length !== 1 || !args[0]) { + handleArgsError(new TypeError('Usage: qifi [options] ')) + } + const positional = args[0]! + + let sliceSize = 80 + if (options.sliceSize) { + sliceSize = Number.parseInt(options.sliceSize) + if (Number.isNaN(sliceSize)) { + handleArgsError(new TypeError('Invalid slice size')) + } + } + + let fps = 10 + if (options.fps) { + fps = Number.parseInt(options.fps) + if (Number.isNaN(fps)) { + handleArgsError(new TypeError('Invalid fps')) + } + } + + let prefix = 'https://qrss.netlify.app/#' + if (options.prefix != null) { + prefix = options.prefix + } + + let maskPattern = -1 + if (options.maskPattern) { + maskPattern = Number.parseInt(options.maskPattern) + if (Number.isNaN(maskPattern)) { + handleArgsError(new TypeError('Invalid mask pattern')) + } + } + + let minVersion = 1 + if (options.minVersion) { + minVersion = Number.parseInt(options.minVersion) + if (Number.isNaN(minVersion)) { + handleArgsError(new TypeError('Invalid min version')) + } + } + + let maxVersion = 40 + if (options.maxVersion) { + maxVersion = Number.parseInt(options.maxVersion) + if (Number.isNaN(maxVersion)) { + handleArgsError(new TypeError('Invalid max version')) + } + } + + let border = 1 + if (options.border) { + border = Number.parseInt(options.border) + if (Number.isNaN(border)) { + handleArgsError(new TypeError('Invalid border')) + } + } + + const ecc = options.ecc as QrCodeGenerateSvgOptions['ecc'] + + return { + positional, + values: { + sliceSize, + fps, + prefix, + unicode: options.unicode as boolean, + compact: options.compact as boolean, + white: options.white as string, + black: options.black as string, + invert: options.invert as boolean, + contentType: options.contentType as string, + ecc, + maskPattern, + boostEcc: options.boostEcc as boolean, + minVersion, + maxVersion, + border, + }, + } +} diff --git a/packages/cli/src/shared.ts b/packages/cli/src/shared.ts new file mode 100644 index 0000000..93fe8a3 --- /dev/null +++ b/packages/cli/src/shared.ts @@ -0,0 +1,12 @@ +import process from 'node:process' + +export enum ExitCode { + Success = 0, + FatalError = 1, + InvalidArgument = 9, +} + +export function handleArgsError(error: Error): never { + console.error(error.message) + return process.exit(ExitCode.InvalidArgument) +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1238020..e0f6e62 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -78,6 +78,9 @@ importers: pinia: specifier: ^2.2.4 version: 2.2.4(typescript@5.6.2)(vue@3.5.6(typescript@5.6.2)) + tsx: + specifier: ^4.19.1 + version: 4.19.1 typescript: specifier: ^5.6.2 version: 5.6.2 @@ -96,12 +99,12 @@ importers: '@qifi/generate': specifier: workspace:* version: link:../generate + cac: + specifier: ^6.7.14 + version: 6.7.14 mime: specifier: ^4.0.4 version: 4.0.4 - tsx: - specifier: ^4.19.1 - version: 4.19.1 packages/generate: dependencies: @@ -5778,11 +5781,6 @@ packages: tslib@2.6.3: resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==} - tsx@4.17.0: - resolution: {integrity: sha512-eN4mnDA5UMKDt4YZixo9tBioibaMBpoxBkD+rIPAjVmYERSG0/dWEY1CEFuV89CgASlKL499q8AhmkMnnjtOJg==} - engines: {node: '>=18.0.0'} - hasBin: true - tsx@4.19.1: resolution: {integrity: sha512-0flMz1lh74BR4wOvBjuh9olbnwqCPc35OOlfyzHba0Dc+QNUeWX/Gq2YTbnwcWPO3BMd8fkzRVrHcsR+a7z7rA==} engines: {node: '>=18.0.0'} @@ -11038,7 +11036,7 @@ snapshots: jiti-v1: jiti@1.21.6 pathe: 1.1.2 pkg-types: 1.2.0 - tsx: 4.17.0 + tsx: 4.19.1 transitivePeerDependencies: - supports-color @@ -13128,13 +13126,6 @@ snapshots: tslib@2.6.3: {} - tsx@4.17.0: - dependencies: - esbuild: 0.23.1 - get-tsconfig: 4.7.6 - optionalDependencies: - fsevents: 2.3.3 - tsx@4.19.1: dependencies: esbuild: 0.23.1