Upgrade to Agsv2 + Astal (#533)

* migrate to astal

* Reorganize project structure.

* progress

* Migrate Dashboard and Window Title modules.

* Migrate clock and notification bar modules.

* Remove unused code

* Media menu

* Rework network and volume modules

* Finish custom modules.

* Migrate battery bar module.

* Update battery module and organize helpers.

* Migrate workspace module.

* Wrap up bar modules.

* Checkpoint before I inevitbly blow something up.

* Updates

* Fix event propagation logic.

* Type fixes

* More type fixes

* Fix padding for event boxes.

* Migrate volume menu and refactor scroll event handlers.

* network module WIP

* Migrate network service.

* Migrate bluetooth menu

* Updates

* Migrate notifications

* Update scrolling behavior for custom modules.

* Improve popup notifications and add timer functionality.

* Migration notifications menu header/controls.

* Migrate notifications menu and consolidate notifications menu code.

* Migrate power menu.

* Dashboard progress

* Migrate dashboard

* Migrate media menu.

* Reduce media menu nesting.

* Finish updating media menu bindings to navigate active player.

* Migrate battery menu

* Consolidate code

* Migrate calendar menu

* Fix workspace logic to update on client add/change/remove and consolidate code.

* Migrate osd

* Consolidate hyprland service connections.

* Implement startup dropdown menu position allocation.

* Migrate settings menu (WIP)

* Settings dialo menu fixes

* Finish Dashboard menu

* Type updates

* update submoldule for types

* update github ci

* ci

* Submodule update

* Ci updates

* Remove type checking for now.

* ci fix

* Fix a bunch of stuff, losing track... need rest. Brb coffee

* Validate dropdown menu before render.

* Consolidate code and add auto-hide functionality.

* Improve auto-hide behavior.

* Consolidate audio menu code

* Organize bluetooth code

* Improve active player logic

* Properly dismiss a notification on action button resolution.

* Implement CLI command engine and migrate CLI commands.

* Handle variable disposal

* Bar component fixes and add hyprland startup rules.

* Handle potentially null bindings network and bluetooth bindings.

* Handle potentially null wired adapter.

* Fix GPU stats

* Handle poller for GPU

* Fix gpu bar logic.

* Clean up logic for stat bars.

* Handle wifi and wired bar icon bindings.

* Fix battery percentages

* Fix switch behavior

* Wifi staging fixes

* Reduce redundant hyprland service calls.

* Code cleanup

* Document the option code and reduce redundant calls to optimize performance.

* Remove outdated comment.

* Add JSDocs

* Add meson to build hyprpanel

* Consistency updates

* Organize commands

* Fix images not showing up on notifications.

* Remove todo

* Move hyprpanel configuration to the ~/.config/hyprpanel directory and add utility commands.

* Handle SRC directory for the bundled/built hyprpanel.

* Add namespaces to all windows

* Migrate systray

* systray updates

* Update meson to include ts, tsx and scss files.

* Remove log from meson

* Fix file choose path and make it float.

* Added a command to check the dependency status

* Update dep names.

* Get scale directly from env

* Add todo
This commit is contained in:
Jas Singh
2024-12-20 18:10:10 -08:00
committed by GitHub
parent 955eed6c60
commit 2ffd602910
605 changed files with 19543 additions and 15999 deletions

6
src/globals/dropdown.ts Normal file
View File

@@ -0,0 +1,6 @@
import Variable from 'astal/variable';
type GlobalEventBoxes = {
[key: string]: unknown;
};
export const globalEventBoxes: Variable<GlobalEventBoxes> = Variable({});

327
src/globals/media.ts Normal file
View File

@@ -0,0 +1,327 @@
import { bind, Variable } from 'astal';
import AstalMpris from 'gi://AstalMpris?version=0.1';
import { getTimeStamp } from 'src/components/menus/media/components/timebar/helpers';
import { mprisService } from 'src/lib/constants/services';
import options from 'src/options';
const { noMediaText } = options.menus.media;
export const activePlayer = Variable<AstalMpris.Player | undefined>(mprisService.players[0]);
const forceUpdate = Variable(false);
mprisService.connect('player-closed', (_, closedPlayer) => {
if (mprisService.get_players().length === 1 && closedPlayer.busName === mprisService.get_players()[0]?.busName) {
return activePlayer.set(undefined);
}
if (closedPlayer.busName === activePlayer.get()?.busName) {
const nextPlayer = mprisService.players.find((player) => player.busName !== closedPlayer.busName);
activePlayer.set(nextPlayer);
}
});
mprisService.connect('player-added', (_, addedPlayer) => {
if (activePlayer.get() === undefined) {
activePlayer.set(addedPlayer);
}
});
export const timeStamp = Variable('00:00');
export const currentPosition = Variable(0);
export const loopStatus = Variable(AstalMpris.Loop.NONE);
export const shuffleStatus = Variable(AstalMpris.Shuffle.OFF);
export const canPlay = Variable(false);
export const playbackStatus = Variable(AstalMpris.PlaybackStatus.STOPPED);
export const canGoNext = Variable(false);
export const canGoPrevious = Variable(false);
export const mediaTitle = Variable(noMediaText.get());
export const mediaAlbum = Variable('-----');
export const mediaArtist = Variable('-----');
export const mediaArtUrl = Variable('');
let positionUnsub: Variable<void>;
let loopUnsub: Variable<void>;
let shuffleUnsub: Variable<void>;
let canPlayUnsub: Variable<void>;
let playbackStatusUnsub: Variable<void>;
let canGoNextUnsub: Variable<void>;
let canGoPreviousUnsub: Variable<void>;
let titleUnsub: Variable<void>;
let albumUnsub: Variable<void>;
let artistUnsub: Variable<void>;
let artUrlUnsub: Variable<void>;
const updatePosition = (player: AstalMpris.Player | undefined): void => {
if (positionUnsub) {
positionUnsub();
positionUnsub.drop();
}
if (player === undefined) {
timeStamp.set('00:00');
currentPosition.set(0);
return;
}
const loopBinding = bind(player, 'position');
positionUnsub = Variable.derive([bind(loopBinding), bind(player, 'playbackStatus')], (pos) => {
if (player?.length > 0) {
timeStamp.set(getTimeStamp(pos, player.length));
currentPosition.set(pos);
} else {
timeStamp.set('00:00');
currentPosition.set(0);
}
});
const initialPos = loopBinding.get();
timeStamp.set(getTimeStamp(initialPos, player.length));
currentPosition.set(initialPos);
};
const updateLoop = (player: AstalMpris.Player | undefined): void => {
if (loopUnsub) {
loopUnsub();
loopUnsub.drop();
}
if (player === undefined) {
loopStatus.set(AstalMpris.Loop.NONE);
return;
}
const loopBinding = bind(player, 'loopStatus');
loopUnsub = Variable.derive([bind(loopBinding), bind(player, 'playbackStatus')], (status) => {
if (player?.length > 0) {
loopStatus.set(status);
} else {
currentPosition.set(AstalMpris.Loop.NONE);
}
});
const initialStatus = loopBinding.get();
loopStatus.set(initialStatus);
};
const updateShuffle = (player: AstalMpris.Player | undefined): void => {
if (shuffleUnsub) {
shuffleUnsub();
shuffleUnsub.drop();
}
if (player === undefined) {
shuffleStatus.set(AstalMpris.Shuffle.OFF);
return;
}
const shuffleBinding = bind(player, 'shuffleStatus');
shuffleUnsub = Variable.derive([bind(shuffleBinding), bind(player, 'playbackStatus')], (status) => {
shuffleStatus.set(status ?? AstalMpris.Shuffle.OFF);
});
const initialStatus = shuffleBinding.get();
shuffleStatus.set(initialStatus);
};
const updateCanPlay = (player: AstalMpris.Player | undefined): void => {
if (canPlayUnsub) {
canPlayUnsub();
canPlayUnsub.drop();
}
if (player === undefined) {
canPlay.set(false);
return;
}
const canPlayBinding = bind(player, 'canPlay');
canPlayUnsub = Variable.derive([canPlayBinding, bind(player, 'playbackStatus')], (playable) => {
canPlay.set(playable ?? false);
});
const initialCanPlay = canPlay.get();
canPlay.set(initialCanPlay);
};
const updatePlaybackStatus = (player: AstalMpris.Player | undefined): void => {
if (playbackStatusUnsub) {
playbackStatusUnsub();
playbackStatusUnsub.drop();
}
if (player === undefined) {
playbackStatus.set(AstalMpris.PlaybackStatus.STOPPED);
return;
}
const playbackStatusBinding = bind(player, 'playbackStatus');
playbackStatusUnsub = Variable.derive([playbackStatusBinding], (status) => {
playbackStatus.set(status ?? AstalMpris.PlaybackStatus.STOPPED);
});
const initialStatus = playbackStatus.get();
playbackStatus.set(initialStatus);
};
const updateCanGoNext = (player: AstalMpris.Player | undefined): void => {
if (canGoNextUnsub) {
canGoNextUnsub();
canGoNextUnsub.drop();
}
if (player === undefined) {
canGoNext.set(false);
return;
}
const canGoNextBinding = bind(player, 'canGoNext');
canGoNextUnsub = Variable.derive([canGoNextBinding, bind(player, 'playbackStatus')], (canNext) => {
canGoNext.set(canNext ?? false);
});
const initialCanNext = canGoNext.get();
canGoNext.set(initialCanNext);
};
const updateCanGoPrevious = (player: AstalMpris.Player | undefined): void => {
if (canGoPreviousUnsub) {
canGoPreviousUnsub();
canGoPreviousUnsub.drop();
}
if (player === undefined) {
canGoPrevious.set(false);
return;
}
const canGoPreviousBinding = bind(player, 'canGoPrevious');
canGoPreviousUnsub = Variable.derive([canGoPreviousBinding, bind(player, 'playbackStatus')], (canPrev) => {
canGoPrevious.set(canPrev ?? false);
});
const initialCanPrev = canGoPrevious.get();
canGoPrevious.set(initialCanPrev);
};
const updateTitle = (player: AstalMpris.Player | undefined): void => {
if (titleUnsub) {
titleUnsub();
titleUnsub.drop();
}
if (player === undefined) {
mediaTitle.set(noMediaText.get());
return;
}
const titleBinding = bind(player, 'title');
titleUnsub = Variable.derive([titleBinding, bind(player, 'playbackStatus')], (newTitle, pbStatus) => {
if (pbStatus === AstalMpris.PlaybackStatus.STOPPED) {
return mediaTitle.set(noMediaText.get() ?? '-----');
}
mediaTitle.set(newTitle.length > 0 ? newTitle : '-----');
});
const initialTitle = mediaTitle.get();
mediaTitle.set(initialTitle.length > 0 ? initialTitle : '-----');
};
const updateAlbum = (player: AstalMpris.Player | undefined): void => {
if (albumUnsub) {
albumUnsub();
albumUnsub.drop();
}
if (player === undefined) {
mediaAlbum.set('-----');
return;
}
albumUnsub = Variable.derive([bind(player, 'album'), bind(player, 'playbackStatus')], (newAlbum) => {
mediaAlbum.set(newAlbum?.length > 0 ? newAlbum : '-----');
});
const initialAlbum = mediaAlbum.get();
mediaAlbum.set(initialAlbum.length > 0 ? initialAlbum : '-----');
};
const updateArtist = (player: AstalMpris.Player | undefined): void => {
if (artistUnsub) {
artistUnsub();
artistUnsub.drop();
}
if (player === undefined) {
mediaArtist.set('-----');
return;
}
const artistBinding = bind(player, 'artist');
artistUnsub = Variable.derive([artistBinding, bind(player, 'playbackStatus')], (newArtist) => {
mediaArtist.set(newArtist?.length > 0 ? newArtist : '-----');
});
const initialArtist = mediaArtist.get();
mediaArtist.set(initialArtist?.length > 0 ? initialArtist : '-----');
};
const updateArtUrl = (player: AstalMpris.Player | undefined): void => {
if (artUrlUnsub) {
artUrlUnsub();
artUrlUnsub.drop();
}
if (player === undefined) {
mediaArtUrl.set('');
return;
}
const artUrlBinding = bind(player, 'artUrl');
artUrlUnsub = Variable.derive([artUrlBinding, bind(player, 'playbackStatus')], (newArtUrl) => {
mediaArtUrl.set(newArtUrl ?? '');
});
const initialArtUrl = mediaArtUrl.get();
mediaArtUrl.set(initialArtUrl);
};
Variable.derive([bind(activePlayer), bind(forceUpdate)], (player) => {
updatePosition(player);
updateLoop(player);
updateShuffle(player);
updateCanPlay(player);
updatePlaybackStatus(player);
updateCanGoNext(player);
updateCanGoPrevious(player);
updateTitle(player);
updateAlbum(player);
updateArtist(player);
updateArtUrl(player);
});

View File

@@ -0,0 +1,48 @@
import { notifdService } from 'src/lib/constants/services';
import icons from 'src/lib/icons/icons2';
import options from 'src/options';
import { errorHandler, lookUpIcon } from 'src/lib/utils';
import { Variable } from 'astal';
import AstalNotifd from 'gi://AstalNotifd?version=0.1';
const { clearDelay } = options.notifications;
export const removingNotifications = Variable<boolean>(false);
export const getNotificationIcon = (app_name: string, app_icon: string, app_entry: string): string => {
let icon: string = icons.fallback.notification;
if (lookUpIcon(app_name) || lookUpIcon(app_name.toLowerCase() || '')) {
icon = lookUpIcon(app_name) ? app_name : lookUpIcon(app_name.toLowerCase()) ? app_name.toLowerCase() : '';
}
if (lookUpIcon(app_icon) && icon === '') {
icon = app_icon;
}
if (lookUpIcon(app_entry || '') && icon === '') {
icon = app_entry || '';
}
return icon;
};
export const clearNotifications = async (notifications: AstalNotifd.Notification[], delay: number): Promise<void> => {
removingNotifications.set(true);
for (const notification of notifications) {
notification.dismiss();
await new Promise((resolve) => setTimeout(resolve, delay));
}
removingNotifications.set(false);
};
const clearAllNotifications = async (): Promise<void> => {
try {
clearNotifications(notifdService.notifications, clearDelay.get());
} catch (error) {
errorHandler(error);
}
};
globalThis['removingNotifications'] = removingNotifications;
globalThis['clearAllNotifications'] = clearAllNotifications;

18
src/globals/systray.ts Normal file
View File

@@ -0,0 +1,18 @@
import AstalTray from 'gi://AstalTray';
import { errorHandler } from 'src/lib/utils';
const systemtray = AstalTray.get_default();
globalThis.getSystrayItems = (): string => {
try {
const items = systemtray
.get_items()
.map((systrayItem) => systrayItem.id)
.join('\n');
return items;
} catch (error) {
errorHandler(error);
}
};
export { getSystrayItems };

6
src/globals/time.ts Normal file
View File

@@ -0,0 +1,6 @@
import { GLib, Variable } from 'astal';
export const systemTime = Variable(GLib.DateTime.new_now_local()).poll(
1000,
(): GLib.DateTime => GLib.DateTime.new_now_local(),
);

40
src/globals/useTheme.ts Normal file
View File

@@ -0,0 +1,40 @@
import options from '../options';
import Gio from 'gi://Gio';
import { bash, errorHandler } from '../lib/utils';
import { filterConfigForThemeOnly, loadJsonFile, saveConfigToFile } from '../components/settings/shared/FileChooser';
const { restartCommand } = options.hyprpanel;
export const hexColorPattern = /^#([0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/;
globalThis.useTheme = (filePath: string): void => {
try {
const importedConfig = loadJsonFile(filePath);
if (!importedConfig) {
return;
}
const tmpConfigFile = Gio.File.new_for_path(`${TMP}/config.json`);
const optionsConfigFile = Gio.File.new_for_path(CONFIG);
const [tmpSuccess, tmpContent] = tmpConfigFile.load_contents(null);
const [optionsSuccess, optionsContent] = optionsConfigFile.load_contents(null);
if (!tmpSuccess || !optionsSuccess) {
throw new Error('Failed to load theme file.');
}
let tmpConfig = JSON.parse(new TextDecoder('utf-8').decode(tmpContent));
let optionsConfig = JSON.parse(new TextDecoder('utf-8').decode(optionsContent));
const filteredConfig = filterConfigForThemeOnly(importedConfig);
tmpConfig = { ...tmpConfig, ...filteredConfig };
optionsConfig = { ...optionsConfig, ...filteredConfig };
saveConfigToFile(tmpConfig, `${TMP}/config.json`);
saveConfigToFile(optionsConfig, CONFIG);
bash(restartCommand.get());
} catch (error) {
errorHandler(error);
}
};

24
src/globals/utilities.ts Normal file
View File

@@ -0,0 +1,24 @@
import { App } from 'astal/gtk3';
import options from '../options';
import { BarLayouts } from 'src/lib/types/options';
globalThis.isWindowVisible = (windowName: string): boolean => {
const appWindow = App.get_window(windowName);
if (appWindow === undefined || appWindow === null) {
throw new Error(`Window with name "${windowName}" not found.`);
}
return appWindow.visible;
};
globalThis.setLayout = (layout: BarLayouts): string => {
try {
const { layouts } = options.bar;
layouts.set(layout);
return 'Successfully updated layout.';
} catch (error) {
return `Failed to set layout: ${error}`;
}
};

29
src/globals/variables.ts Normal file
View File

@@ -0,0 +1,29 @@
import { Opt } from '../lib/option';
import { HexColor, MatugenTheme, RecursiveOptionsObject } from 'src/lib/types/options';
export const isOpt = <T>(value: unknown): value is Opt<T> =>
typeof value === 'object' && value !== null && 'value' in value && value instanceof Opt;
export const isOptString = (value: unknown): value is Opt<string> => {
return value instanceof Opt && typeof value.get() === 'string';
};
export const isOptNumber = (value: unknown): value is Opt<number> => {
return value instanceof Opt && typeof value.get() === 'number';
};
export const isOptBoolean = (value: unknown): value is Opt<boolean> => {
return value instanceof Opt && typeof value.get() === 'boolean';
};
export const isOptMatugenTheme = (value: unknown): value is Opt<MatugenTheme> => {
return value instanceof Opt && typeof value.get() === 'object' && 'specificProperty' in value.get();
};
export const isRecursiveOptionsObject = (value: unknown): value is RecursiveOptionsObject => {
return typeof value === 'object' && value !== null && !(value instanceof Opt);
};
export const isHexColor = (val: unknown): val is HexColor => {
return typeof val === 'string' && /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(val);
};

27
src/globals/wallpaper.ts Normal file
View File

@@ -0,0 +1,27 @@
import GLib from 'gi://GLib?version=2.0';
import options from '../options';
import Wallpaper from '../services/Wallpaper';
const { EXISTS, IS_REGULAR } = GLib.FileTest;
const { enable: enableWallpaper, image } = options.wallpaper;
globalThis.setWallpaper = (filePath: string): void => {
if (!(GLib.file_test(filePath, EXISTS) && GLib.file_test(filePath, IS_REGULAR))) {
throw new Error('The input file is not a valid wallpaper.');
}
image.set(filePath);
if (!enableWallpaper.get()) {
return;
}
try {
Wallpaper.setWallpaper(filePath);
} catch (error) {
if (error instanceof Error) {
throw new Error(error.message);
} else {
throw new Error(`An error occurred while setting the wallpaper: ${error}`);
}
}
};

220
src/globals/weather.ts Normal file
View File

@@ -0,0 +1,220 @@
import options from 'src/options';
import { UnitType, Weather, WeatherIconTitle, WeatherIcon } from 'src/lib/types/weather.js';
import { DEFAULT_WEATHER } from 'src/lib/types/defaults/weather.js';
import GLib from 'gi://GLib?version=2.0';
import { weatherIcons } from 'src/lib/icons/weather.js';
import { AstalIO, bind, execAsync, interval, Variable } from 'astal';
const { EXISTS, IS_REGULAR } = GLib.FileTest;
const { key, interval: weatherInterval, location } = options.menus.clock.weather;
export const globalWeatherVar = Variable<Weather>(DEFAULT_WEATHER);
let weatherIntervalInstance: null | AstalIO.Time = null;
key.subscribe(() => {
const fetchedKey = getWeatherKey(key.get());
weatherApiKey.set(fetchedKey);
});
/**
* Retrieves the weather API key from a file if it exists and is valid.
*
* @param apiKey - The path to the file containing the weather API key.
* @returns - The weather API key if found, otherwise the original apiKey.
*/
const getWeatherKey = (apiKey: string): string => {
const weatherKey = apiKey;
if (GLib.file_test(weatherKey, EXISTS) && GLib.file_test(weatherKey, IS_REGULAR)) {
try {
const fileContentArray = GLib.file_get_contents(weatherKey)[1];
const fileContent = new TextDecoder().decode(fileContentArray);
if (!fileContent) {
console.error('File content is empty');
return '';
}
const parsedContent = JSON.parse(fileContent);
if (parsedContent.weather_api_key !== undefined) {
return parsedContent.weather_api_key;
} else {
console.error('weather_api_key is missing in the JSON content');
return '';
}
} catch (error) {
console.error(`Failed to read or parse weather key file: ${error}`);
return '';
}
}
return apiKey;
};
const fetchedApiKey = getWeatherKey(key.get());
const weatherApiKey = Variable(fetchedApiKey);
/**
* Sets up a weather update interval function.
*
* @param weatherInterval - The interval in milliseconds at which to fetch weather updates.
* @param loc - The location for which to fetch weather data.
* @param weatherKey - The API key for accessing the weather service.
*/
const weatherIntervalFn = (weatherInterval: number, loc: string, weatherKey: string): void => {
if (weatherIntervalInstance !== null) {
weatherIntervalInstance.cancel();
}
const formattedLocation = loc.replace(' ', '%20');
weatherIntervalInstance = interval(weatherInterval, () => {
execAsync(
`curl "https://api.weatherapi.com/v1/forecast.json?key=${weatherKey}&q=${formattedLocation}&days=1&aqi=no&alerts=no"`,
)
.then((res) => {
try {
if (typeof res !== 'string') {
return globalWeatherVar.set(DEFAULT_WEATHER);
}
const parsedWeather = JSON.parse(res);
if (Object.keys(parsedWeather).includes('error')) {
return globalWeatherVar.set(DEFAULT_WEATHER);
}
return globalWeatherVar.set(parsedWeather);
} catch (error) {
globalWeatherVar.set(DEFAULT_WEATHER);
console.warn(`Failed to parse weather data: ${error}`);
}
})
.catch((err) => {
console.error(`Failed to fetch weather: ${err}`);
globalWeatherVar.set(DEFAULT_WEATHER);
});
});
};
Variable.derive([bind(weatherApiKey), bind(weatherInterval), bind(location)], (weatherKey, weatherInterval, loc) => {
if (!weatherKey) {
return globalWeatherVar.set(DEFAULT_WEATHER);
}
weatherIntervalFn(weatherInterval, loc, weatherKey);
})();
/**
* Gets the temperature from the weather data in the specified unit.
*
* @param weatherData - The weather data object.
* @param unitType - The unit type, either 'imperial' or 'metric'.
* @returns - The temperature formatted as a string with the appropriate unit.
*/
export const getTemperature = (weatherData: Weather, unitType: UnitType): string => {
if (unitType === 'imperial') {
return `${Math.ceil(weatherData.current.temp_f)}° F`;
} else {
return `${Math.ceil(weatherData.current.temp_c)}° C`;
}
};
/**
* Returns the appropriate weather icon and color class based on the temperature in Fahrenheit.
*
* @param fahrenheit - The temperature in Fahrenheit.
* @returns - An object containing the weather icon and color class.
*/
export const getWeatherIcon = (fahrenheit: number): Record<string, string> => {
const icons = {
100: '',
75: '',
50: '',
25: '',
0: '',
} as const;
const colors = {
100: 'weather-color red',
75: 'weather-color orange',
50: 'weather-color lavender',
25: 'weather-color blue',
0: 'weather-color sky',
} as const;
type IconKeys = keyof typeof icons;
const threshold: IconKeys =
fahrenheit < 0 ? 0 : ([100, 75, 50, 25, 0] as IconKeys[]).find((threshold) => threshold <= fahrenheit) || 0;
const icon = icons[threshold || 50];
const color = colors[threshold || 50];
return {
icon,
color,
};
};
/**
* Gets the wind conditions from the weather data in the specified unit.
*
* @param weatherData - The weather data object.
* @param unitType - The unit type, either 'imperial' or 'metric'.
* @returns - The wind conditions formatted as a string with the appropriate unit.
*/
export const getWindConditions = (weatherData: Weather, unitType: UnitType): string => {
if (unitType === 'imperial') {
return `${Math.floor(weatherData.current.wind_mph)} mph`;
}
return `${Math.floor(weatherData.current.wind_kph)} kph`;
};
/**
* Gets the chance of rain from the weather forecast data.
*
* @param weatherData - The weather data object.
* @returns - The chance of rain formatted as a percentage string.
*/
export const getRainChance = (weatherData: Weather): string =>
`${weatherData.forecast.forecastday[0].day.daily_chance_of_rain}%`;
/**
* Type Guard
* Checks if the given title is a valid weather icon title.
*
* @param title - The weather icon title to check.
* @returns - True if the title is a valid weather icon title, false otherwise.
*/
export const isValidWeatherIconTitle = (title: string): title is WeatherIconTitle => {
return title in weatherIcons;
};
/**
* Gets the appropriate weather icon based on the weather status text.
*
* @param weatherData - The weather data object.
* @returns - The weather icon corresponding to the weather status text.
*/
export const getWeatherStatusTextIcon = (weatherData: Weather): WeatherIcon => {
let iconQuery = weatherData.current.condition.text.trim().toLowerCase().replaceAll(' ', '_');
if (!weatherData.current.is_day && iconQuery === 'partly_cloudy') {
iconQuery = 'partly_cloudy_night';
}
if (isValidWeatherIconTitle(iconQuery)) {
return weatherIcons[iconQuery];
} else {
console.warn(`Unknown weather icon title: ${iconQuery}`);
return weatherIcons['warning'];
}
};
export const convertCelsiusToFahrenheit = (celsiusValue: number): number => {
return (celsiusValue * 9) / 5 + 32;
};
globalThis['globalWeatherVar'] = globalWeatherVar;

10
src/globals/window.ts Normal file
View File

@@ -0,0 +1,10 @@
export const WINDOW_LAYOUTS: string[] = [
'center',
'top',
'top-right',
'top-center',
'top-left',
'bottom-left',
'bottom-center',
'bottom-right',
];