- modifiers-napi: 使用 Bun FFI 调用 macOS CGEventSourceFlagsState 检测修饰键 - image-processor-napi: 集成 sharp 库,macOS 剪贴板图像读取 (osascript) - audio-capture-napi: 基于 SoX/arecord 的跨平台音频录制 - url-handler-napi: 完善函数签名(保持 null fallback) - 修复 image-processor 类型兼容性问题 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
126 lines
3.3 KiB
TypeScript
126 lines
3.3 KiB
TypeScript
import sharpModule from 'sharp'
|
|
|
|
export const sharp = sharpModule
|
|
|
|
interface NativeModule {
|
|
hasClipboardImage(): boolean
|
|
readClipboardImage(
|
|
maxWidth?: number,
|
|
maxHeight?: number,
|
|
): {
|
|
png: Buffer
|
|
width: number
|
|
height: number
|
|
originalWidth: number
|
|
originalHeight: number
|
|
} | null
|
|
}
|
|
|
|
function createDarwinNativeModule(): NativeModule {
|
|
return {
|
|
hasClipboardImage(): boolean {
|
|
try {
|
|
const result = Bun.spawnSync({
|
|
cmd: [
|
|
'osascript',
|
|
'-e',
|
|
'try\nthe clipboard as «class PNGf»\nreturn "yes"\non error\nreturn "no"\nend try',
|
|
],
|
|
stdout: 'pipe',
|
|
stderr: 'pipe',
|
|
})
|
|
const output = result.stdout.toString().trim()
|
|
return output === 'yes'
|
|
} catch {
|
|
return false
|
|
}
|
|
},
|
|
|
|
readClipboardImage(
|
|
maxWidth?: number,
|
|
maxHeight?: number,
|
|
) {
|
|
try {
|
|
// Use osascript to read clipboard image as PNG data and write to a temp file,
|
|
// then read the temp file back
|
|
const tmpPath = `/tmp/claude_clipboard_native_${Date.now()}.png`
|
|
const script = `
|
|
set png_data to (the clipboard as «class PNGf»)
|
|
set fp to open for access POSIX file "${tmpPath}" with write permission
|
|
write png_data to fp
|
|
close access fp
|
|
return "${tmpPath}"
|
|
`
|
|
const result = Bun.spawnSync({
|
|
cmd: ['osascript', '-e', script],
|
|
stdout: 'pipe',
|
|
stderr: 'pipe',
|
|
})
|
|
|
|
if (result.exitCode !== 0) {
|
|
return null
|
|
}
|
|
|
|
const file = Bun.file(tmpPath)
|
|
// Use synchronous read via Node compat
|
|
const fs = require('fs')
|
|
const buffer: Buffer = fs.readFileSync(tmpPath)
|
|
|
|
// Clean up temp file
|
|
try {
|
|
fs.unlinkSync(tmpPath)
|
|
} catch {
|
|
// ignore cleanup errors
|
|
}
|
|
|
|
if (buffer.length === 0) {
|
|
return null
|
|
}
|
|
|
|
// Read PNG dimensions from IHDR chunk
|
|
// PNG header: 8 bytes signature, then IHDR chunk
|
|
// IHDR starts at offset 8 (4 bytes length) + 4 bytes "IHDR" + 4 bytes width + 4 bytes height
|
|
let width = 0
|
|
let height = 0
|
|
if (buffer.length > 24 && buffer[12] === 0x49 && buffer[13] === 0x48 && buffer[14] === 0x44 && buffer[15] === 0x52) {
|
|
width = buffer.readUInt32BE(16)
|
|
height = buffer.readUInt32BE(20)
|
|
}
|
|
|
|
const originalWidth = width
|
|
const originalHeight = height
|
|
|
|
// If maxWidth/maxHeight are specified and the image exceeds them,
|
|
// we still return the full PNG - the caller handles resizing via sharp
|
|
// But we report the capped dimensions
|
|
if (maxWidth && maxHeight) {
|
|
if (width > maxWidth || height > maxHeight) {
|
|
const scale = Math.min(maxWidth / width, maxHeight / height)
|
|
width = Math.round(width * scale)
|
|
height = Math.round(height * scale)
|
|
}
|
|
}
|
|
|
|
return {
|
|
png: buffer,
|
|
width,
|
|
height,
|
|
originalWidth,
|
|
originalHeight,
|
|
}
|
|
} catch {
|
|
return null
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
export function getNativeModule(): NativeModule | null {
|
|
if (process.platform === 'darwin') {
|
|
return createDarwinNativeModule()
|
|
}
|
|
return null
|
|
}
|
|
|
|
export default sharp
|