@ltcode/crosshot
Version:
Cross-platform desktop screenshot utility
486 lines (424 loc) • 18.1 kB
JavaScript
import { exec } from 'child_process';
import { join, dirname } from 'path';
import { existsSync, statSync, mkdirSync, readFileSync } from 'fs';
import { platform } from 'os';
import { fileURLToPath } from 'url';
import chalk from 'chalk';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
export function takeScreenshot(destinationDir = "./", customName = null, options = {}) {
return new Promise((resolve, reject) => {
const config = {
silent: false,
verbose: false,
format: 'png',
quality: 100,
returnBase64: false,
...options
};
const normalizedFormat = config.format.toLowerCase();
const validFormats = ['png', 'jpg', 'jpeg', 'bmp', 'webp'];
if (!validFormats.includes(normalizedFormat)) {
reject({
success: false,
error: `Unsupported format: ${config.format}. Supported formats: ${validFormats.join(', ')}`,
platform: platform(),
timestamp: new Date().toISOString(),
suggestions: [`Use one of these formats: ${validFormats.join(', ')}`]
});
return;
}
const fileExtension = normalizedFormat === 'jpeg' ? 'jpg' : normalizedFormat;
const filename = customName ? `${customName}.${fileExtension}` : `screenshot-${Date.now()}.${fileExtension}`;
const filepath = join(destinationDir, filename);
const currentPlatform = platform();
const log = (...args) => !config.silent && config.verbose && console.log(...args);
const logError = (...args) => !config.silent && console.error(...args);
log(chalk.cyan(`Platform detected: ${currentPlatform}`));
log(chalk.blue('Taking screenshot...'));
let commands = [];
if (currentPlatform === 'win32') {
const formatMap = {
'png': 'Png',
'jpg': 'Jpeg',
'jpeg': 'Jpeg',
'bmp': 'Bmp',
'webp': 'Png'
};
const psFormat = formatMap[normalizedFormat];
commands = [
`powershell -Command "Add-Type -AssemblyName System.Windows.Forms; $screen = [System.Windows.Forms.Screen]::PrimaryScreen; $bitmap = New-Object System.Drawing.Bitmap($screen.Bounds.Width, $screen.Bounds.Height); $graphics = [System.Drawing.Graphics]::FromImage($bitmap); $graphics.CopyFromScreen($screen.Bounds.X, $screen.Bounds.Y, 0, 0, $screen.Bounds.Size); $bitmap.Save('${filepath.replace(/\\/g, '/')}', [System.Drawing.Imaging.ImageFormat]::${psFormat}); $graphics.Dispose(); $bitmap.Dispose()"`,
`nircmd savescreenshot "${filepath}"`,
`screencapture "${filepath}"`
];
} else if (currentPlatform === 'darwin') {
const macFormats = ['png', 'jpg', 'jpeg'];
if (macFormats.includes(normalizedFormat)) {
const formatFlag = normalizedFormat === 'png' ? '' : ' -t jpg';
commands = [
`screencapture${formatFlag} "${filepath}"`,
`screencapture -x${formatFlag} "${filepath}"`
];
} else {
const fallbackPath = filepath.replace(new RegExp(`\\.${fileExtension}$`), '.png');
commands = [
`screencapture "${fallbackPath}"`,
`screencapture -x "${fallbackPath}"`
];
}
} else {
commands = [];
if (['png', 'jpg', 'jpeg', 'webp'].includes(normalizedFormat)) {
const grimFormat = normalizedFormat === 'jpeg' ? 'jpg' : normalizedFormat;
commands.push(`grim -t ${grimFormat} "${filepath}"`);
}
commands.push(
`gnome-screenshot -f "${filepath}"`,
`spectacle -b -n -o "${filepath}"`,
`wayshot -f "${filepath}"`,
`flameshot full -p "${dirname(filepath)}" -d 0`,
`scrot "${filepath}"`,
`maim "${filepath}"`
);
}
function tryCommand(index) {
if (index >= commands.length) {
const errorMessage = 'No screenshot tools found!';
logError(chalk.red.bold(`ERROR: ${errorMessage}`));
if (currentPlatform === 'win32') {
log(chalk.yellow('\nFor Windows:'));
log(chalk.white(' * PowerShell already tried (native to Windows)'));
log(chalk.white(' * Install NirCmd: ') + chalk.cyan('https://www.nirsoft.net/utils/nircmd.html'));
log(chalk.white(' * Or use npm: ') + chalk.green('npm install screenshot-desktop'));
} else if (currentPlatform === 'darwin') {
log(chalk.yellow('\nFor macOS:'));
log(chalk.white(' * screencapture is native to macOS'));
log(chalk.white(' * Check screen recording permissions in System Preferences'));
} else {
log(chalk.yellow('\nFor Linux systems, install one of these tools:'));
log(chalk.green(' * grim') + chalk.gray(' (recommended for Wayland)'));
log(chalk.green(' * gnome-screenshot') + chalk.gray(' (for GNOME environments)'));
log(chalk.green(' * spectacle') + chalk.gray(' (for KDE Plasma)'));
log(chalk.green(' * wayshot') + chalk.gray(' (alternative for Wayland)'));
log(chalk.green(' * flameshot') + chalk.gray(' (GUI with extra features)'));
log(chalk.green(' * scrot') + chalk.gray(' (for X11 systems)'));
log(chalk.green(' * maim') + chalk.gray(' (alternative for X11)'));
log(chalk.magenta('\nInstall using your system package manager ') + chalk.cyan('(apt, pacman, dnf, etc.)'));
}
reject({
success: false,
error: errorMessage,
platform: currentPlatform,
availableTools: commands.map(cmd => cmd.split(' ')[0]),
timestamp: new Date().toISOString(),
suggestions: getSuggestions(currentPlatform)
});
return;
}
const command = commands[index];
const toolName = command.split(' ')[0];
exec(command, (error, stdout, stderr) => {
if (error) {
log(chalk.yellow(`WARNING: ${toolName} not available, trying next...`));
if (config.verbose) {
log(chalk.gray(`Command failed: ${command}`));
log(chalk.gray(`Error: ${error.message}`));
}
tryCommand(index + 1);
} else {
if (existsSync(filepath)) {
log(chalk.green.bold(`SUCCESS: Screenshot captured with ${toolName}!`));
log(chalk.blue('File saved at: ') + chalk.white.underline(filepath));
const stats = statSync(filepath);
const sizeKB = (stats.size / 1024).toFixed(2);
const sizeMB = (stats.size / (1024 * 1024)).toFixed(2);
log(chalk.magenta('Size: ') + chalk.cyan(`${sizeKB} KB`));
const result = {
success: true,
filename: filename,
filepath: filepath,
absolutePath: join(process.cwd(), filepath),
directory: destinationDir,
size: {
bytes: stats.size,
kb: parseFloat(sizeKB),
mb: parseFloat(sizeMB)
},
tool: toolName,
platform: currentPlatform,
format: normalizedFormat,
timestamp: new Date().toISOString(),
metadata: {
created: stats.birthtime,
modified: stats.mtime,
permissions: stats.mode
}
};
if (config.returnBase64) {
try {
const imageBuffer = readFileSync(filepath);
const base64Data = imageBuffer.toString('base64');
const mimeType = getMimeType(normalizedFormat);
result.base64 = `data:${mimeType};base64,${base64Data}`;
result.base64Raw = base64Data;
log(chalk.blue('Base64 data generated'));
} catch (base64Error) {
log(chalk.yellow(`Warning: Could not generate base64: ${base64Error.message}`));
}
}
resolve(result);
} else {
log(chalk.red(`ERROR: Failed to save with ${toolName}, trying next...`));
tryCommand(index + 1);
}
}
});
}
tryCommand(0);
});
}
export default takeScreenshot;
function getMimeType(format) {
const mimeTypes = {
'png': 'image/png',
'jpg': 'image/jpeg',
'jpeg': 'image/jpeg',
'webp': 'image/webp',
'bmp': 'image/bmp'
};
return mimeTypes[format] || 'image/png';
}
function getVersion() {
try {
const packagePath = join(__dirname, 'package.json');
const packageContent = readFileSync(packagePath, 'utf8');
const packageInfo = JSON.parse(packageContent);
return packageInfo.version || 'unknown';
} catch (error) {
return 'unknown';
}
}
function showVersion() {
const version = getVersion();
const currentPlatform = platform();
console.log(chalk.cyan.bold('Crosshot'));
console.log(chalk.white(`Version: ${version}`));
console.log(chalk.gray(`Platform: ${currentPlatform}`));
console.log(chalk.gray('Cross-platform desktop screenshot utility'));
console.log(chalk.blue('\nRepository: ') + chalk.underline('https://github.com/ltcodedev/crosshot'));
console.log(chalk.green('License: MIT'));
}
function getSuggestions(currentPlatform) {
if (currentPlatform === 'win32') {
return [
'PowerShell (native to Windows)',
'NirCmd from https://www.nirsoft.net/utils/nircmd.html',
'npm install screenshot-desktop'
];
} else if (currentPlatform === 'darwin') {
return [
'screencapture (native to macOS)',
'Check screen recording permissions in System Preferences'
];
} else {
return [
'grim (recommended for Wayland)',
'gnome-screenshot (for GNOME environments)',
'spectacle (for KDE Plasma)',
'wayshot (alternative for Wayland)',
'flameshot (GUI with extra features)',
'scrot (for X11 systems)',
'maim (alternative for X11)'
];
}
}
export async function captureScreen(options = {}) {
const {
outputDir = process.cwd(),
filename = null,
silent = true,
verbose = false,
createDir = true,
format = 'png',
quality = 100,
returnBase64 = false
} = options;
try {
if (!existsSync(outputDir)) {
if (createDir) {
mkdirSync(outputDir, { recursive: true });
} else {
throw {
success: false,
error: `Directory does not exist: ${outputDir}`,
platform: platform(),
timestamp: new Date().toISOString(),
suggestions: ['Set createDir: true to automatically create directories', 'Create the directory manually before taking screenshot']
};
}
}
const result = await takeScreenshot(outputDir, filename, {
silent,
verbose,
format,
quality,
returnBase64
});
return result;
} catch (error) {
throw error;
}
}
export function getAvailableTools() {
return new Promise((resolve) => {
const currentPlatform = platform();
let commands = [];
if (currentPlatform === 'win32') {
commands = ['powershell', 'nircmd', 'screencapture'];
} else if (currentPlatform === 'darwin') {
commands = ['screencapture'];
} else {
commands = ['grim', 'gnome-screenshot', 'spectacle', 'wayshot', 'flameshot', 'scrot', 'maim'];
}
const availableTools = [];
let checkedTools = 0;
commands.forEach(tool => {
exec(`which ${tool}`, (error) => {
checkedTools++;
if (!error) {
availableTools.push(tool);
}
if (checkedTools === commands.length) {
resolve({
platform: currentPlatform,
available: availableTools,
total: commands.length,
hasTools: availableTools.length > 0
});
}
});
});
});
}
export function getLibraryVersion() {
return {
version: getVersion(),
platform: platform(),
name: 'crosshot',
description: 'Cross-platform desktop screenshot utility'
};
}
function parseArguments() {
const args = process.argv.slice(2);
const options = {};
args.forEach(arg => {
if (arg.startsWith('--name=') || arg.startsWith('-n=')) {
options.name = arg.split('=')[1].replace(/["']/g, '');
} else if (arg.startsWith('-o=') || arg.startsWith('--output=')) {
const outputPath = arg.split('=')[1].replace(/["']/g, '');
options.output = outputPath.startsWith('~/')
? join(process.env.HOME || process.env.USERPROFILE, outputPath.slice(2))
: outputPath;
} else if (arg.startsWith('-f=') || arg.startsWith('--format=')) {
options.format = arg.split('=')[1].replace(/["']/g, '').toLowerCase();
} else if (arg.startsWith('-q=') || arg.startsWith('--quality=')) {
options.quality = parseInt(arg.split('=')[1]) || 100;
} else if (arg.startsWith('--help') || arg.startsWith('-h')) {
options.help = true;
} else if (arg.startsWith('--verbose')) {
options.verbose = true;
} else if (arg.startsWith('--version') || arg === '-v') {
options.version = true;
}
});
return options;
}
function showHelp() {
console.log(chalk.cyan.bold(`
Crosshot - Cross-Platform Screenshot Utility
`));
console.log(chalk.white.bold('Usage:'));
console.log(chalk.gray(' crosshot [options]') + chalk.dim(' (global installation)'));
console.log(chalk.gray(' node index.js [options]') + chalk.dim(' (local/development)'));
console.log(chalk.white.bold('\nOptions:'));
console.log(chalk.green(' -n, --name=<filename>') + chalk.gray(' Set custom filename for the screenshot (without extension)'));
console.log(chalk.gray(' Example: ') + chalk.yellow('-n="my-capture"') + chalk.gray(' or ') + chalk.yellow('--name=desktop-screenshot'));
console.log(chalk.green(' -o, --output=<path>') + chalk.gray(' Set output directory for the screenshot'));
console.log(chalk.gray(' Example: ') + chalk.yellow('-o="~/Images/Screenshots/"') + chalk.gray(' or ') + chalk.yellow('--output="./"'));
console.log(chalk.gray(' Note: Directory will be created if it doesn\'t exist'));
console.log(chalk.green(' -f, --format=<type>') + chalk.gray(' Set output format (png, jpg, jpeg, bmp, webp)'));
console.log(chalk.gray(' Example: ') + chalk.yellow('-f="jpg"') + chalk.gray(' or ') + chalk.yellow('--format=webp'));
console.log(chalk.gray(' Default: png'));
console.log(chalk.green(' -q, --quality=<num>') + chalk.gray(' Set quality for lossy formats (1-100)'));
console.log(chalk.gray(' Example: ') + chalk.yellow('-q=85') + chalk.gray(' or ') + chalk.yellow('--quality=90'));
console.log(chalk.gray(' Default: 100 (only affects jpg/webp)'));
console.log(chalk.green(' --verbose') + chalk.gray(' Show detailed information and result object'));
console.log(chalk.green(' -v, --version') + chalk.gray(' Show version information'));
console.log(chalk.green(' -h, --help') + chalk.gray(' Show this help message'));
console.log(chalk.white.bold('\nExamples:'));
console.log(chalk.magenta('Global usage:'));
console.log(chalk.cyan(' crosshot'));
console.log(chalk.cyan(' crosshot ') + chalk.yellow('-n="important-capture"'));
console.log(chalk.cyan(' crosshot ') + chalk.yellow('-f="jpg" -q=85'));
console.log(chalk.cyan(' crosshot ') + chalk.yellow('-n="screenshot" -f="webp"'));
console.log(chalk.cyan(' crosshot ') + chalk.yellow('--verbose'));
console.log(chalk.magenta('\nLocal/Development usage:'));
console.log(chalk.cyan(' node index.js'));
console.log(chalk.cyan(' node index.js ') + chalk.yellow('-n="important-capture"'));
console.log(chalk.cyan(' node index.js ') + chalk.yellow('-o="~/Images/Screenshots/"'));
console.log(chalk.cyan(' node index.js ') + chalk.yellow('--help'));
console.log(chalk.magenta('\nSupported formats: PNG (default), JPG/JPEG, BMP, WebP'));
console.log(chalk.gray('Quality setting only affects lossy formats (JPG, WebP).'));
console.log(chalk.gray('Install globally: ') + chalk.white('npm install -g @ltcode/crosshot'));
console.log(chalk.gray('Tilde (~) expands to your home directory on Unix-like systems.'));
}
if (import.meta.url === `file://${process.argv[1]}`) {
const options = parseArguments();
if (options.help) {
showHelp();
process.exit(0);
}
if (options.version) {
showVersion();
process.exit(0);
}
const outputDir = options.output || "./";
try {
if (!existsSync(outputDir)) {
mkdirSync(outputDir, { recursive: true });
if (options.verbose) {
console.log(chalk.blue('Created directory: ') + chalk.white.underline(outputDir));
}
}
} catch (error) {
console.error(chalk.red.bold(`ERROR: Error creating directory: ${error.message}`));
process.exit(1);
}
takeScreenshot(outputDir, options.name, {
silent: false,
verbose: options.verbose || false,
format: options.format || 'png',
quality: options.quality || 100
})
.then(result => {
if (options.verbose) {
console.log(chalk.green.bold('Screenshot taken successfully!'));
console.log(chalk.gray('Detailed result:'));
console.log(JSON.stringify(result, null, 2));
} else {
console.log(chalk.green('✓ Success'));
console.log(chalk.white(result.filepath));
}
})
.catch(error => {
console.error(chalk.red.bold('ERROR:'), chalk.red(error.error || error.message));
if (options.verbose && error.suggestions) {
console.log(chalk.yellow('\nSuggestions:'));
error.suggestions.forEach(suggestion => {
console.log(chalk.gray(' - ') + chalk.white(suggestion));
});
}
process.exit(1);
});
}