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 @@
-
+
-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