UNPKG

weather-cli-16bit

Version:

Command-line weather tool with formatted display and Tokyo Night theme

472 lines (411 loc) 18 kB
import chalk from 'chalk'; import boxen from 'boxen'; import { theme, getTemperatureColor, getWeatherConditionColor, getAQIColor, getWindColor, boxTheme } from './theme.js'; // Weather emoji mapping (removed - no longer using emojis) // Format temperature with Tokyo Night colors function formatTemp(temp, displayUnit) { const unit = displayUnit === 'fahrenheit' ? '°F' : '°C'; const tempColor = getTemperatureColor(temp, displayUnit); return tempColor(`${Math.round(temp)}${unit}`); } // Format time function formatTime(timestamp) { return new Date(timestamp * 1000).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true }); } // Get air quality description function getAirQualityDescription(aqi) { const descriptions = { 1: 'Good', 2: 'Fair', 3: 'Moderate', 4: 'Poor', 5: 'Very Poor' }; return descriptions[aqi] || 'Unknown'; } // Display alerts with Tokyo Night colors function displayAlerts(data) { if (!data.pollution?.list?.[0]?.main?.aqi) return; const aqi = data.pollution.list[0].main.aqi; const description = getAirQualityDescription(aqi); const aqiColor = getAQIColor(aqi); console.log(aqiColor(`\nAir Quality: ${description} (AQI: ${aqi})`)); } // Display sun times with Tokyo Night colors function displaySunTimes(data) { const { sunrise, sunset } = data.current.sys; console.log(theme.sunrise(`\nSunrise: ${formatTime(sunrise)}`)); console.log(theme.sunset(`Sunset: ${formatTime(sunset)}`)); } // Display ASCII art function displayASCIIArt(weatherMain) { const art = { Clear: ' \\ / \n .-. \n ― ( ) ― \n `-´ \n / \\ ', Clouds: ' .--. \n .-( ). \n (___.__)__) ', Rain: ' .--. \n .-( ). \n (___.__)__) \n /|/|/ ', Snow: ' .--. \n .-( ). \n (___.__)__) \n * * * * ', Thunderstorm: ' .--. \n .-( ). \n (___.__)__) \n ⚡⚡⚡ ' }; return art[weatherMain] || art.Clouds; } // Get terminal width function getTerminalWidth() { return process.stdout.columns || 80; } // Display current weather with new formatted layout function displayCurrentWeather(data, displayUnit) { const weather = data.current; const layout = createFormattedLayout(weather, data, displayUnit); // Create the box with Tokyo Night styling console.log(boxen( layout, { padding: boxTheme.padding, margin: boxTheme.margin, borderStyle: boxTheme.borderStyle, borderColor: '#3b4261', // dark blue-gray border width: 60 } )); } // Create formatted layout according to v0.3.6 specifications with Tokyo Night colors function createFormattedLayout(weather, data, displayUnit) { const { sunrise, sunset } = weather.sys; const aqi = data.pollution?.list?.[0]?.main?.aqi; const airQualityDesc = aqi ? getAirQualityDescription(aqi) : 'Moderate'; const unit = displayUnit === 'fahrenheit' ? '°F' : '°C'; // Tokyo Night color definitions const locationColor = chalk.hex('#7dcfff'); // cyan for location const conditionColor = chalk.hex('#9d7cd8'); // purple for weather condition const labelColor = chalk.hex('#a9b1d6'); // light gray-blue for labels const timeColor = chalk.hex('#bb9af7'); // magenta for time values const dimColor = chalk.hex('#565f89'); // dimmed gray-blue const separatorColor = chalk.hex('#3b4261'); // dark blue-gray for separators const tealColor = chalk.hex('#73daca'); // teal for atmospheric data const cyanColor = chalk.hex('#7dcfff'); // cyan for wind direction const orangeColor = chalk.hex('#ff9e64'); // orange // Temperature color function const getTempColorValue = (temp) => { const fahrenheitTemp = displayUnit === 'fahrenheit' ? temp : (temp * 9/5) + 32; if (fahrenheitTemp > 80) return chalk.hex('#f7768e'); // hot - red if (fahrenheitTemp > 70) return chalk.hex('#ff9e64'); // warm - orange if (fahrenheitTemp >= 60) return chalk.hex('#9ece6a'); // mild - green return chalk.hex('#7aa2f7'); // cool - blue }; // AQI color function const getAQIColorValue = (aqi, desc) => { if (!aqi) return dimColor; switch(aqi) { case 1: return chalk.hex('#9ece6a'); // Good - green case 2: return chalk.hex('#9ece6a'); // Fair - green case 3: return chalk.hex('#e0af68'); // Moderate - yellow case 4: case 5: return chalk.hex('#f7768e'); // USG/Unhealthy - red default: return dimColor; } }; // Header Section (2 lines) const headerLine1 = locationColor.bold(`${weather.name}, ${weather.sys.country}`); const headerLine2 = conditionColor(weather.weather[0].description); // Primary Temperature Section (1 line) with color based on value const tempColor = getTempColorValue(weather.main.temp); const feelsLikeColor = getTempColorValue(weather.main.feels_like); const tempLine = `${tempColor.bold(Math.round(weather.main.temp) + unit)} ${dimColor('(Feels like:')} ${feelsLikeColor(Math.round(weather.main.feels_like) + unit)}${dimColor(')')}`; // Calculate daily min/max from forecast data (today's actual low/high) const today = new Date().toDateString(); const todayForecasts = data.forecast?.list?.filter(item => { const forecastDate = new Date(item.dt * 1000).toDateString(); return forecastDate === today; }) || []; let dailyMin, dailyMax; if (todayForecasts.length > 0) { // Get the actual min/max from all today's forecast entries const allTemps = todayForecasts.flatMap(f => [f.main.temp, f.main.temp_min, f.main.temp_max]); dailyMin = Math.min(...allTemps, weather.main.temp); // Include current temp dailyMax = Math.max(...allTemps, weather.main.temp); // Include current temp } else { // Fallback: use first day of forecast if today's data not available const firstDayForecasts = data.forecast?.list?.slice(0, 8) || []; // First 8 entries (24 hours) if (firstDayForecasts.length > 0) { const allTemps = firstDayForecasts.flatMap(f => [f.main.temp, f.main.temp_min, f.main.temp_max]); dailyMin = Math.min(...allTemps, weather.main.temp); dailyMax = Math.max(...allTemps, weather.main.temp); } else { // Final fallback to current weather min/max dailyMin = weather.main.temp_min; dailyMax = weather.main.temp_max; } } // Time & Environmental Grid (2-column aligned) with colors const sunriseStr = timeColor(formatTime(sunrise)); const sunsetStr = timeColor(formatTime(sunset)); const minTempStr = getTempColorValue(dailyMin)(`${Math.round(dailyMin)}${unit}`); const maxTempStr = getTempColorValue(dailyMax)(`${Math.round(dailyMax)}${unit}`); const aqiColoredDesc = getAQIColorValue(aqi)(airQualityDesc); const aqiIndexStr = aqi ? getAQIColorValue(aqi)(aqi.toString()) : dimColor('3'); const timeEnvGrid = [ formatColoredGridRow(labelColor('Sunrise:'), sunriseStr, labelColor('Sunset:'), sunsetStr, separatorColor), formatColoredGridRow(labelColor('Min Temp:'), minTempStr, labelColor('Max Temp:'), maxTempStr, separatorColor), formatColoredGridRow(labelColor('Air Quality:'), aqiColoredDesc, labelColor('AQI Index:'), aqiIndexStr, separatorColor) ]; // Atmospheric Conditions Grid (2-column aligned) with colors const humidityStr = tealColor(`${weather.main.humidity}%`); const pressureStr = tealColor(`${weather.main.pressure} hPa`); const windSpeedStr = tealColor(`${weather.wind.speed} ${displayUnit === 'fahrenheit' ? 'mph' : 'm/s'}`); const windDirStr = cyanColor(`${weather.wind.deg || 0}°`); const visibilityStr = tealColor(`${(weather.visibility / 1000)} km`); const uvIndexStr = dimColor('N/A'); // Dimmed when N/A const atmosphericGrid = [ formatColoredGridRow(labelColor('Humidity:'), humidityStr, labelColor('Pressure:'), pressureStr, separatorColor), formatColoredGridRow(labelColor('Wind Speed:'), windSpeedStr, labelColor('Wind Dir:'), windDirStr, separatorColor), formatColoredGridRow(labelColor('Visibility:'), visibilityStr, labelColor('UV Index:'), uvIndexStr, separatorColor) ]; // Combine all sections return [ headerLine1, headerLine2, '', tempLine, '', ...timeEnvGrid, '', ...atmosphericGrid ].join('\n'); } // Format colored grid row with consistent column widths function formatColoredGridRow(label1, value1, label2, value2, separatorColor) { const labelWidth = 12; const valueWidth = 10; const separator = separatorColor(' | '); // Strip ANSI codes for length calculation const stripAnsi = (str) => { return str.replace(/\x1b\[[0-9;]*m/g, ''); }; // Pad based on actual string length without ANSI codes const padEndWithColor = (str, width) => { const actualLength = stripAnsi(str).length; const padding = Math.max(0, width - actualLength); return str + ' '.repeat(padding); }; const padStartWithColor = (str, width) => { const actualLength = stripAnsi(str).length; const padding = Math.max(0, width - actualLength); return ' '.repeat(padding) + str; }; const formattedLabel1 = padEndWithColor(label1, labelWidth); const formattedValue1 = padStartWithColor(value1, valueWidth); const formattedLabel2 = padEndWithColor(label2, labelWidth); const formattedValue2 = padStartWithColor(value2, valueWidth); return `${formattedLabel1}${formattedValue1}${separator}${formattedLabel2}${formattedValue2}`; } // Legacy display function for backward compatibility function displayCurrentWeatherLegacy(data, displayUnit) { const weather = data.current; const terminalWidth = getTerminalWidth(); // Responsive design based on terminal width let layout; if (terminalWidth < 60) { // Ultra compact layout for very small terminals layout = createUltraCompactLayout(weather, data, displayUnit); } else if (terminalWidth < 80) { // Compact layout for small terminals layout = createCompactLayout(weather, data, displayUnit); } else if (terminalWidth < 120) { // Medium layout layout = createMediumLayout(weather, data, displayUnit); } else { // Full horizontal layout layout = createFullLayout(weather, data, displayUnit); } // Create the box with Tokyo Night styling console.log(boxen( layout, { padding: boxTheme.padding, margin: boxTheme.margin, borderStyle: boxTheme.borderStyle, borderColor: boxTheme.borderColor, width: Math.min(terminalWidth - 2, 120) } )); } // Ultra compact layout for very small terminals function createUltraCompactLayout(weather, data, displayUnit) { const conditionColor = getWeatherConditionColor(weather.weather[0].description); return [ theme.location(weather.name), '', formatTemp(weather.main.temp, displayUnit), conditionColor(weather.weather[0].description), '', `Feels like: ${formatTemp(weather.main.feels_like, displayUnit)}`, `Humidity: ${theme.humidity(weather.main.humidity + '%')}`, `Wind: ${weather.wind.speed} ${displayUnit === 'fahrenheit' ? 'mph' : 'm/s'}` ].join('\n'); } // Compact layout for small terminals with Tokyo Night colors function createCompactLayout(weather, data, displayUnit) { const { sunrise, sunset } = weather.sys; const aqi = data.pollution?.list?.[0]?.main?.aqi; const airQualityDesc = aqi ? getAirQualityDescription(aqi) : 'N/A'; const conditionColor = getWeatherConditionColor(weather.weather[0].description); const windColor = getWindColor(weather.wind.speed, displayUnit); const aqiColor = getAQIColor(aqi); return [ theme.location(`${weather.name}, ${weather.sys.country}`), '', conditionColor(weather.weather[0].description), '', `Temperature: ${formatTemp(weather.main.temp, displayUnit)}`, `Feels like: ${formatTemp(weather.main.feels_like, displayUnit)}`, '', `Humidity: ${theme.humidity(weather.main.humidity + '%')}`, `Wind: ${windColor(weather.wind.speed + ' ' + (displayUnit === 'fahrenheit' ? 'mph' : 'm/s'))}`, '', `Sunrise: ${theme.sunrise(formatTime(sunrise))}`, `Sunset: ${theme.sunset(formatTime(sunset))}`, '', `Air Quality: ${aqiColor(airQualityDesc + (aqi ? ` (AQI: ${aqi})` : ''))}` ].join('\n'); } // Medium layout with Tokyo Night colors function createMediumLayout(weather, data, displayUnit) { const { sunrise, sunset } = weather.sys; const aqi = data.pollution?.list?.[0]?.main?.aqi; const airQualityDesc = aqi ? getAirQualityDescription(aqi) : 'N/A'; const conditionColor = getWeatherConditionColor(weather.weather[0].description); const windColor = getWindColor(weather.wind.speed, displayUnit); const aqiColor = getAQIColor(aqi); const leftSection = [ theme.location(`${weather.name}, ${weather.sys.country}`), '', conditionColor(weather.weather[0].description), '', formatTemp(weather.main.temp, displayUnit), `${theme.text('Feels like:')} ${formatTemp(weather.main.feels_like, displayUnit)}`, '', `${theme.text('Humidity:')} ${theme.humidity(weather.main.humidity + '%')}` ]; const rightSection = [ '', '', `${theme.text('Sunrise:')} ${theme.sunrise(formatTime(sunrise))}`, `${theme.text('Sunset:')} ${theme.sunset(formatTime(sunset))}`, '', `${theme.text('Air Quality:')} ${aqiColor(airQualityDesc + (aqi ? ` (AQI: ${aqi})` : ''))}`, '', `${theme.text('Wind:')} ${windColor(weather.wind.speed + ' ' + (displayUnit === 'fahrenheit' ? 'mph' : 'm/s'))}` ]; return createHorizontalLayout(leftSection, rightSection, 35, 40); } // Full horizontal layout with Tokyo Night colors function createFullLayout(weather, data, displayUnit) { const { sunrise, sunset } = weather.sys; const aqi = data.pollution?.list?.[0]?.main?.aqi; const airQualityDesc = aqi ? getAirQualityDescription(aqi) : 'N/A'; const conditionColor = getWeatherConditionColor(weather.weather[0].description); const windColor = getWindColor(weather.wind.speed, displayUnit); const aqiColor = getAQIColor(aqi); const leftSection = [ theme.location(`${weather.name}, ${weather.sys.country}`), '', conditionColor(weather.weather[0].description), '', formatTemp(weather.main.temp, displayUnit), '', `${theme.text('Feels like:')} ${formatTemp(weather.main.feels_like, displayUnit)}`, `${theme.text('Humidity:')} ${theme.humidity(weather.main.humidity + '%')}`, `${theme.text('Pressure:')} ${theme.pressure(weather.main.pressure + ' hPa')}`, `${theme.text('Wind:')} ${windColor(weather.wind.speed + ' ' + (displayUnit === 'fahrenheit' ? 'mph' : 'm/s'))}` ]; const rightSection = [ '', '', `${theme.text('Sunrise:')} ${theme.sunrise(formatTime(sunrise))}`, `${theme.text('Sunset:')} ${theme.sunset(formatTime(sunset))}`, '', `${theme.text('Air Quality:')} ${aqiColor(airQualityDesc + (aqi ? ` (AQI: ${aqi})` : ''))}`, `${theme.text('Min:')} ${formatTemp(weather.main.temp_min, displayUnit)}`, `${theme.text('Max:')} ${formatTemp(weather.main.temp_max, displayUnit)}`, `${theme.text('Wind Dir:')} ${theme.info(weather.wind.deg + '°')}`, `${theme.text('Visibility:')} ${theme.info((weather.visibility / 1000) + 'km')}` ]; return createHorizontalLayout(leftSection, rightSection, 50, 60); } // Helper function to create horizontal layout function createHorizontalLayout(leftSection, rightSection, leftWidth, rightWidth) { const maxLines = Math.max(leftSection.length, rightSection.length); let layout = ''; for (let i = 0; i < maxLines; i++) { const leftLine = leftSection[i] || ''; const rightLine = rightSection[i] || ''; const paddedLeft = leftLine.padEnd(leftWidth); const paddedRight = rightLine.padEnd(rightWidth); layout += `${paddedLeft} ${paddedRight}\n`; } return layout; } // Display 5-day forecast with Tokyo Night colors function display5DayForecast(data, displayUnit) { console.log(theme.header('\n5-Day Forecast:')); const dailyData = {}; data.forecast.list.forEach(item => { const date = new Date(item.dt * 1000).toDateString(); if (!dailyData[date]) { dailyData[date] = { temps: [], minTemps: [], maxTemps: [], descriptions: [] }; } // Collect all temperature data points dailyData[date].temps.push(item.main.temp); dailyData[date].minTemps.push(item.main.temp_min); dailyData[date].maxTemps.push(item.main.temp_max); dailyData[date].descriptions.push(item.weather[0].description); }); Object.entries(dailyData).forEach(([date, info]) => { // Calculate the actual min and max from all data points for the day const dayMin = Math.min(...info.temps, ...info.minTemps); const dayMax = Math.max(...info.temps, ...info.maxTemps); const mostCommonDesc = info.descriptions.sort((a, b) => info.descriptions.filter(v => v === a).length - info.descriptions.filter(v => v === b).length ).pop(); // Format with both min and max temperatures const minTempStr = formatTemp(dayMin, displayUnit); const maxTempStr = formatTemp(dayMax, displayUnit); console.log(theme.text(`${date}: Min: ${minTempStr} / Max: ${maxTempStr} - ${mostCommonDesc}`)); }); } // Display 24-hour forecast function display24HourForecast(data, displayUnit) { console.log(theme.header('\n24-Hour Forecast:')); const next24Hours = data.forecast.list.slice(0, 8); // 3-hour intervals next24Hours.forEach(item => { const time = new Date(item.dt * 1000).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true }); console.log(theme.text(`${time}: ${formatTemp(item.main.temp, displayUnit)} - ${item.weather[0].description}`)); }); } export { formatTemp, formatTime, getAirQualityDescription, displayAlerts, displaySunTimes, displayASCIIArt, displayCurrentWeather, displayCurrentWeatherLegacy, display5DayForecast, display24HourForecast, createFormattedLayout, formatColoredGridRow };