feat(LocalPicture): support CLI args for path/URL and golden ratio width
Browse filesAllow passing an image path or URL as a command-line argument. Default
falls back to the original wallpaper. Display width changed from 40% to
16.18% (golden ratio) for a more aesthetically balanced default.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
src/ink-picture/__tests__/LocalPicture.test.tsx
CHANGED
|
@@ -1,13 +1,13 @@
|
|
| 1 |
#!/usr/bin/env bun
|
| 2 |
/**
|
| 3 |
-
* Visual test: display a local image using ink-picture + Ink.
|
| 4 |
-
*
|
| 5 |
-
* Height is auto-calculated from the original aspect ratio:
|
| 6 |
-
* only width is fixed (60% of terminal columns).
|
| 7 |
*
|
| 8 |
* Usage:
|
| 9 |
-
* bun run src/ink-picture/__tests__/LocalPicture.test.tsx
|
| 10 |
-
*
|
|
|
|
|
|
|
|
|
|
| 11 |
*/
|
| 12 |
|
| 13 |
import { Jimp } from "jimp";
|
|
@@ -15,42 +15,57 @@ import React, { useEffect, useState } from "react";
|
|
| 15 |
import { render, Box, Text, useApp } from "ink";
|
| 16 |
import Image, { InkPictureProvider } from "../index.ts";
|
| 17 |
|
| 18 |
-
const IMAGE_PATH = "/home/yuki/Pictures/Wallpapers/3god.jpg";
|
| 19 |
-
|
| 20 |
// 终端字符尺寸(像素)
|
| 21 |
const CELL_WIDTH = 8;
|
| 22 |
const CELL_HEIGHT = 16;
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
function App() {
|
| 25 |
const { exit } = useApp();
|
| 26 |
const [dimensions, setDimensions] = useState<{
|
| 27 |
-
width: number;
|
| 28 |
-
height: number;
|
| 29 |
-
pixelWidth: number;
|
| 30 |
-
pixelHeight: number;
|
| 31 |
} | null>(null);
|
| 32 |
const [err, setErr] = useState(false);
|
| 33 |
|
| 34 |
useEffect(() => {
|
| 35 |
(async () => {
|
| 36 |
try {
|
| 37 |
-
const image = await
|
| 38 |
const origW = image.bitmap.width;
|
| 39 |
const origH = image.bitmap.height;
|
| 40 |
const cols = process.stdout.columns ?? 80;
|
| 41 |
|
| 42 |
-
|
| 43 |
-
const targetW_chars = Math.floor(cols * 0.4);
|
| 44 |
-
|
| 45 |
-
// 2. 转为像素尺寸(用于图片缩放)
|
| 46 |
const targetW_pixels = targetW_chars * CELL_WIDTH;
|
| 47 |
const targetH_pixels = Math.floor(targetW_pixels * (origH / origW));
|
| 48 |
-
|
| 49 |
-
// 3. 确保最小高度(至少 3 行字符)
|
| 50 |
const minH_pixels = 3 * CELL_HEIGHT;
|
| 51 |
const finalH_pixels = Math.max(targetH_pixels, minH_pixels);
|
| 52 |
-
|
| 53 |
-
// 4. 转回字符行数(用于 Ink 占位)
|
| 54 |
const targetH_chars = Math.ceil(finalH_pixels / CELL_HEIGHT);
|
| 55 |
|
| 56 |
setDimensions({
|
|
@@ -59,23 +74,21 @@ function App() {
|
|
| 59 |
pixelWidth: targetW_pixels,
|
| 60 |
pixelHeight: finalH_pixels,
|
| 61 |
});
|
| 62 |
-
} catch {
|
|
|
|
| 63 |
setErr(true);
|
| 64 |
}
|
| 65 |
})();
|
| 66 |
}, []);
|
| 67 |
|
| 68 |
-
// Ctrl+C 直接退出,不清理图片
|
| 69 |
useEffect(() => {
|
| 70 |
-
const handleSigint = () =>
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
process.on('SIGINT', handleSigint);
|
| 74 |
-
return () => process.off('SIGINT', handleSigint);
|
| 75 |
}, [exit]);
|
| 76 |
|
| 77 |
if (err) {
|
| 78 |
-
return <Text color="red">Failed to load image</Text>;
|
| 79 |
}
|
| 80 |
|
| 81 |
if (!dimensions) {
|
|
@@ -87,11 +100,11 @@ function App() {
|
|
| 87 |
<InkPictureProvider>
|
| 88 |
<Image
|
| 89 |
src={IMAGE_PATH}
|
| 90 |
-
width={dimensions.width}
|
| 91 |
-
height={dimensions.height}
|
| 92 |
-
pixelWidth={dimensions.pixelWidth}
|
| 93 |
-
pixelHeight={dimensions.pixelHeight}
|
| 94 |
-
alt="
|
| 95 |
/>
|
| 96 |
</InkPictureProvider>
|
| 97 |
</Box>
|
|
|
|
| 1 |
#!/usr/bin/env bun
|
| 2 |
/**
|
| 3 |
+
* Visual test: display a local image or URL using ink-picture + Ink.
|
|
|
|
|
|
|
|
|
|
| 4 |
*
|
| 5 |
* Usage:
|
| 6 |
+
* bun run src/ink-picture/__tests__/LocalPicture.test.tsx [path|url]
|
| 7 |
+
*
|
| 8 |
+
* Examples:
|
| 9 |
+
* bun run src/ink-picture/__tests__/LocalPicture.test.tsx ~/Pictures/IMAGE/image.png
|
| 10 |
+
* bun run src/ink-picture/__tests__/LocalPicture.test.tsx https://example.com/image.jpg
|
| 11 |
*/
|
| 12 |
|
| 13 |
import { Jimp } from "jimp";
|
|
|
|
| 15 |
import { render, Box, Text, useApp } from "ink";
|
| 16 |
import Image, { InkPictureProvider } from "../index.ts";
|
| 17 |
|
|
|
|
|
|
|
| 18 |
// 终端字符尺寸(像素)
|
| 19 |
const CELL_WIDTH = 8;
|
| 20 |
const CELL_HEIGHT = 16;
|
| 21 |
|
| 22 |
+
// 获取命令行参数
|
| 23 |
+
const args = process.argv.slice(2);
|
| 24 |
+
const IMAGE_PATH = args[0] || "/home/yuki/Pictures/Wallpapers/3god.jpg";
|
| 25 |
+
|
| 26 |
+
// 判断是 URL 还是本地路径
|
| 27 |
+
const isUrl = IMAGE_PATH.startsWith("http://") || IMAGE_PATH.startsWith("https://");
|
| 28 |
+
|
| 29 |
+
// 加载图片(支持本地和 URL)
|
| 30 |
+
async function loadImage(path: string) {
|
| 31 |
+
if (isUrl) {
|
| 32 |
+
// URL:先下载,再用 Jimp 读取
|
| 33 |
+
const response = await fetch(path);
|
| 34 |
+
if (!response.ok) {
|
| 35 |
+
throw new Error(`Failed to fetch: ${response.status}`);
|
| 36 |
+
}
|
| 37 |
+
const arrayBuffer = await response.arrayBuffer();
|
| 38 |
+
const buffer = Buffer.from(arrayBuffer);
|
| 39 |
+
return Jimp.fromBuffer(buffer);
|
| 40 |
+
} else {
|
| 41 |
+
// 本地路径:直接用 Jimp.read
|
| 42 |
+
return Jimp.read(path);
|
| 43 |
+
}
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
function App() {
|
| 47 |
const { exit } = useApp();
|
| 48 |
const [dimensions, setDimensions] = useState<{
|
| 49 |
+
width: number;
|
| 50 |
+
height: number;
|
| 51 |
+
pixelWidth: number;
|
| 52 |
+
pixelHeight: number;
|
| 53 |
} | null>(null);
|
| 54 |
const [err, setErr] = useState(false);
|
| 55 |
|
| 56 |
useEffect(() => {
|
| 57 |
(async () => {
|
| 58 |
try {
|
| 59 |
+
const image = await loadImage(IMAGE_PATH);
|
| 60 |
const origW = image.bitmap.width;
|
| 61 |
const origH = image.bitmap.height;
|
| 62 |
const cols = process.stdout.columns ?? 80;
|
| 63 |
|
| 64 |
+
const targetW_chars = Math.floor(cols * 0.1618);
|
|
|
|
|
|
|
|
|
|
| 65 |
const targetW_pixels = targetW_chars * CELL_WIDTH;
|
| 66 |
const targetH_pixels = Math.floor(targetW_pixels * (origH / origW));
|
|
|
|
|
|
|
| 67 |
const minH_pixels = 3 * CELL_HEIGHT;
|
| 68 |
const finalH_pixels = Math.max(targetH_pixels, minH_pixels);
|
|
|
|
|
|
|
| 69 |
const targetH_chars = Math.ceil(finalH_pixels / CELL_HEIGHT);
|
| 70 |
|
| 71 |
setDimensions({
|
|
|
|
| 74 |
pixelWidth: targetW_pixels,
|
| 75 |
pixelHeight: finalH_pixels,
|
| 76 |
});
|
| 77 |
+
} catch (e) {
|
| 78 |
+
console.error("Error loading image:", e);
|
| 79 |
setErr(true);
|
| 80 |
}
|
| 81 |
})();
|
| 82 |
}, []);
|
| 83 |
|
|
|
|
| 84 |
useEffect(() => {
|
| 85 |
+
const handleSigint = () => exit();
|
| 86 |
+
process.on("SIGINT", handleSigint);
|
| 87 |
+
return () => process.off("SIGINT", handleSigint);
|
|
|
|
|
|
|
| 88 |
}, [exit]);
|
| 89 |
|
| 90 |
if (err) {
|
| 91 |
+
return <Text color="red">Failed to load image: {IMAGE_PATH}</Text>;
|
| 92 |
}
|
| 93 |
|
| 94 |
if (!dimensions) {
|
|
|
|
| 100 |
<InkPictureProvider>
|
| 101 |
<Image
|
| 102 |
src={IMAGE_PATH}
|
| 103 |
+
width={dimensions.width}
|
| 104 |
+
height={dimensions.height}
|
| 105 |
+
pixelWidth={dimensions.pixelWidth}
|
| 106 |
+
pixelHeight={dimensions.pixelHeight}
|
| 107 |
+
alt={isUrl ? "url-image" : "local-image"}
|
| 108 |
/>
|
| 109 |
</InkPictureProvider>
|
| 110 |
</Box>
|