diff --git a/.eslintrc.yml b/.eslintrc.yml new file mode 100644 index 0000000..ff96a83 --- /dev/null +++ b/.eslintrc.yml @@ -0,0 +1,130 @@ +env: + es2022: true +extends: + - "eslint:recommended" + - "plugin:@typescript-eslint/recommended" +parser: "@typescript-eslint/parser" +parserOptions: + ecmaVersion: 2022 + sourceType: "module" + project: "./tsconfig.json" + warnOnUnsupportedTypeScriptVersion: false +root: true +ignorePatterns: + - types/ +plugins: + - "@typescript-eslint" +rules: + "@typescript-eslint/ban-ts-comment": + - "off" + "@typescript-eslint/no-non-null-assertion": + - "off" + # "@typescript-eslint/no-explicit-any": + # - "off" + "@typescript-eslint/no-unused-vars": + - error + - varsIgnorePattern: (^unused|_$) + argsIgnorePattern: ^(unused|_) + "@typescript-eslint/no-empty-interface": + - "off" + + arrow-parens: + - error + - as-needed + comma-dangle: + - error + - always-multiline + comma-spacing: + - error + - before: false + after: true + comma-style: + - error + - last + curly: + - error + - multi-or-nest + - consistent + dot-location: + - error + - property + eol-last: + - error + eqeqeq: + - error + - always + indent: + - error + - 4 + - SwitchCase: 1 + keyword-spacing: + - error + - before: true + lines-between-class-members: + - error + - always + - exceptAfterSingleLine: true + padded-blocks: + - error + - never + - allowSingleLineBlocks: false + prefer-const: + - error + quotes: + - error + - double + - avoidEscape: true + semi: + - error + - never + nonblock-statement-body-position: + - error + - below + no-trailing-spaces: + - error + no-useless-escape: + - off + max-len: + - error + - code: 100 + func-call-spacing: + - error + array-bracket-spacing: + - error + space-before-function-paren: + - error + - anonymous: never + named: never + asyncArrow: ignore + space-before-blocks: + - error + key-spacing: + - error + object-curly-spacing: + - error + - always +globals: + Widget: readonly + Utils: readonly + App: readonly + Variable: readonly + Service: readonly + pkg: readonly + ARGV: readonly + Debugger: readonly + GIRepositoryGType: readonly + globalThis: readonly + imports: readonly + Intl: readonly + log: readonly + logError: readonly + print: readonly + printerr: readonly + window: readonly + TextEncoder: readonly + TextDecoder: readonly + console: readonly + setTimeout: readonly + setInterval: readonly + clearTimeout: readonly + clearInterval: readonly diff --git a/globals.js b/globals.ts similarity index 64% rename from globals.js rename to globals.ts index b706cdb..913a682 100644 --- a/globals.js +++ b/globals.ts @@ -1,4 +1,4 @@ -const globalMousePos = Variable([]); +const globalMousePos = Variable([0, 0]); globalThis["globalMousePos"] = globalMousePos; diff --git a/lib/types/defaults/weather.ts b/lib/types/defaults/weather.ts new file mode 100644 index 0000000..4e9d54f --- /dev/null +++ b/lib/types/defaults/weather.ts @@ -0,0 +1,1053 @@ +export const DEFAULT_WEATHER = { + "location": { + "name": "Tahiti", + "region": "Somewhere", + "country": "United States of America", + "lat": 0, + "lon": 0, + "tz_id": "Tahiti", + "localtime_epoch": 1721981457, + "localtime": "2024-07-26 1:10" + }, + "current": { + "last_updated_epoch": 1721980800, + "last_updated": "2024-07-26 01:00", + "temp_c": 30.7, + "temp_f": 87.3, + "is_day": 0, + "condition": { + "text": "Clear", + "icon": "//cdn.weatherapi.com/weather/64x64/night/113.png", + "code": 1000 + }, + "wind_mph": 13.2, + "wind_kph": 21.2, + "wind_degree": 311, + "wind_dir": "NW", + "pressure_mb": 1010.0, + "pressure_in": 29.84, + "precip_mm": 0.0, + "precip_in": 0.0, + "humidity": 17, + "cloud": 5, + "feelslike_c": 28.6, + "feelslike_f": 83.5, + "windchill_c": 30.7, + "windchill_f": 87.3, + "heatindex_c": 28.6, + "heatindex_f": 83.5, + "dewpoint_c": 2.9, + "dewpoint_f": 37.2, + "vis_km": 10.0, + "vis_miles": 6.0, + "uv": 1.0, + "gust_mph": 18.9, + "gust_kph": 30.4 + }, + "forecast": { + "forecastday": [ + { + "date": "2024-07-26", + "date_epoch": 1721952000, + "day": { + "maxtemp_c": 42.7, + "maxtemp_f": 108.9, + "mintemp_c": 26.3, + "mintemp_f": 79.3, + "avgtemp_c": 33.1, + "avgtemp_f": 91.5, + "maxwind_mph": 22.1, + "maxwind_kph": 35.6, + "totalprecip_mm": 0.0, + "totalprecip_in": 0.0, + "totalsnow_cm": 0.0, + "avgvis_km": 10.0, + "avgvis_miles": 6.0, + "avghumidity": 15, + "daily_will_it_rain": 0, + "daily_chance_of_rain": 0, + "daily_will_it_snow": 0, + "daily_chance_of_snow": 0, + "condition": { + "text": "Sunny", + "icon": "//cdn.weatherapi.com/weather/64x64/day/113.png", + "code": 1000 + }, + "uv": 11.0 + }, + "astro": { + "sunrise": "06:01 AM", + "sunset": "08:10 PM", + "moonrise": "11:32 PM", + "moonset": "12:01 PM", + "moon_phase": "Waning Gibbous", + "moon_illumination": 74, + "is_moon_up": 0, + "is_sun_up": 1 + }, + "hour": [ + { + "time_epoch": 1721977200, + "time": "2024-07-26 00:00", + "temp_c": 32.2, + "temp_f": 89.9, + "is_day": 0, + "condition": { + "text": "Clear ", + "icon": "//cdn.weatherapi.com/weather/64x64/night/113.png", + "code": 1000 + }, + "wind_mph": 14.1, + "wind_kph": 22.7, + "wind_degree": 313, + "wind_dir": "NW", + "pressure_mb": 1011.0, + "pressure_in": 29.84, + "precip_mm": 0.0, + "precip_in": 0.0, + "snow_cm": 0.0, + "humidity": 15, + "cloud": 5, + "feelslike_c": 30.1, + "feelslike_f": 86.2, + "windchill_c": 32.2, + "windchill_f": 89.9, + "heatindex_c": 30.1, + "heatindex_f": 86.2, + "dewpoint_c": 2.3, + "dewpoint_f": 36.1, + "will_it_rain": 0, + "chance_of_rain": 0, + "will_it_snow": 0, + "chance_of_snow": 0, + "vis_km": 10.0, + "vis_miles": 6.0, + "gust_mph": 20.4, + "gust_kph": 32.9, + "uv": 0 + }, + { + "time_epoch": 1721980800, + "time": "2024-07-26 01:00", + "temp_c": 30.7, + "temp_f": 87.3, + "is_day": 0, + "condition": { + "text": "Clear ", + "icon": "//cdn.weatherapi.com/weather/64x64/night/113.png", + "code": 1000 + }, + "wind_mph": 13.2, + "wind_kph": 21.2, + "wind_degree": 311, + "wind_dir": "NW", + "pressure_mb": 1010.0, + "pressure_in": 29.84, + "precip_mm": 0.0, + "precip_in": 0.0, + "snow_cm": 0.0, + "humidity": 17, + "cloud": 5, + "feelslike_c": 28.6, + "feelslike_f": 83.5, + "windchill_c": 30.7, + "windchill_f": 87.3, + "heatindex_c": 28.6, + "heatindex_f": 83.5, + "dewpoint_c": 2.9, + "dewpoint_f": 37.2, + "will_it_rain": 0, + "chance_of_rain": 0, + "will_it_snow": 0, + "chance_of_snow": 0, + "vis_km": 10.0, + "vis_miles": 6.0, + "gust_mph": 18.9, + "gust_kph": 30.4, + "uv": 0 + }, + { + "time_epoch": 1721984400, + "time": "2024-07-26 02:00", + "temp_c": 29.5, + "temp_f": 85.1, + "is_day": 0, + "condition": { + "text": "Clear ", + "icon": "//cdn.weatherapi.com/weather/64x64/night/113.png", + "code": 1000 + }, + "wind_mph": 13.2, + "wind_kph": 21.2, + "wind_degree": 311, + "wind_dir": "NW", + "pressure_mb": 1010.0, + "pressure_in": 29.83, + "precip_mm": 0.0, + "precip_in": 0.0, + "snow_cm": 0.0, + "humidity": 19, + "cloud": 4, + "feelslike_c": 27.4, + "feelslike_f": 81.4, + "windchill_c": 29.5, + "windchill_f": 85.1, + "heatindex_c": 27.4, + "heatindex_f": 81.4, + "dewpoint_c": 3.1, + "dewpoint_f": 37.5, + "will_it_rain": 0, + "chance_of_rain": 0, + "will_it_snow": 0, + "chance_of_snow": 0, + "vis_km": 10.0, + "vis_miles": 6.0, + "gust_mph": 19.0, + "gust_kph": 30.6, + "uv": 0 + }, + { + "time_epoch": 1721988000, + "time": "2024-07-26 03:00", + "temp_c": 28.6, + "temp_f": 83.5, + "is_day": 0, + "condition": { + "text": "Clear ", + "icon": "//cdn.weatherapi.com/weather/64x64/night/113.png", + "code": 1000 + }, + "wind_mph": 11.2, + "wind_kph": 18.0, + "wind_degree": 318, + "wind_dir": "NW", + "pressure_mb": 1010.0, + "pressure_in": 29.82, + "precip_mm": 0.0, + "precip_in": 0.0, + "snow_cm": 0.0, + "humidity": 20, + "cloud": 4, + "feelslike_c": 26.6, + "feelslike_f": 79.9, + "windchill_c": 28.6, + "windchill_f": 83.5, + "heatindex_c": 26.6, + "heatindex_f": 79.9, + "dewpoint_c": 3.1, + "dewpoint_f": 37.6, + "will_it_rain": 0, + "chance_of_rain": 0, + "will_it_snow": 0, + "chance_of_snow": 0, + "vis_km": 10.0, + "vis_miles": 6.0, + "gust_mph": 16.4, + "gust_kph": 26.4, + "uv": 0 + }, + { + "time_epoch": 1721991600, + "time": "2024-07-26 04:00", + "temp_c": 27.9, + "temp_f": 82.2, + "is_day": 0, + "condition": { + "text": "Clear ", + "icon": "//cdn.weatherapi.com/weather/64x64/night/113.png", + "code": 1000 + }, + "wind_mph": 8.7, + "wind_kph": 14.0, + "wind_degree": 319, + "wind_dir": "NW", + "pressure_mb": 1010.0, + "pressure_in": 29.83, + "precip_mm": 0.0, + "precip_in": 0.0, + "snow_cm": 0.0, + "humidity": 20, + "cloud": 3, + "feelslike_c": 26.0, + "feelslike_f": 78.8, + "windchill_c": 27.9, + "windchill_f": 82.2, + "heatindex_c": 26.0, + "heatindex_f": 78.8, + "dewpoint_c": 2.9, + "dewpoint_f": 37.2, + "will_it_rain": 0, + "chance_of_rain": 0, + "will_it_snow": 0, + "chance_of_snow": 0, + "vis_km": 10.0, + "vis_miles": 6.0, + "gust_mph": 12.7, + "gust_kph": 20.5, + "uv": 0 + }, + { + "time_epoch": 1721995200, + "time": "2024-07-26 05:00", + "temp_c": 27.3, + "temp_f": 81.1, + "is_day": 0, + "condition": { + "text": "Clear ", + "icon": "//cdn.weatherapi.com/weather/64x64/night/113.png", + "code": 1000 + }, + "wind_mph": 7.6, + "wind_kph": 12.2, + "wind_degree": 314, + "wind_dir": "NW", + "pressure_mb": 1010.0, + "pressure_in": 29.84, + "precip_mm": 0.0, + "precip_in": 0.0, + "snow_cm": 0.0, + "humidity": 20, + "cloud": 4, + "feelslike_c": 25.5, + "feelslike_f": 77.9, + "windchill_c": 27.3, + "windchill_f": 81.1, + "heatindex_c": 25.5, + "heatindex_f": 77.9, + "dewpoint_c": 2.6, + "dewpoint_f": 36.7, + "will_it_rain": 0, + "chance_of_rain": 0, + "will_it_snow": 0, + "chance_of_snow": 0, + "vis_km": 10.0, + "vis_miles": 6.0, + "gust_mph": 11.6, + "gust_kph": 18.7, + "uv": 0 + }, + { + "time_epoch": 1721998800, + "time": "2024-07-26 06:00", + "temp_c": 27.3, + "temp_f": 81.1, + "is_day": 0, + "condition": { + "text": "Clear ", + "icon": "//cdn.weatherapi.com/weather/64x64/night/113.png", + "code": 1000 + }, + "wind_mph": 5.8, + "wind_kph": 9.4, + "wind_degree": 314, + "wind_dir": "NW", + "pressure_mb": 1011.0, + "pressure_in": 29.85, + "precip_mm": 0.0, + "precip_in": 0.0, + "snow_cm": 0.0, + "humidity": 21, + "cloud": 6, + "feelslike_c": 25.5, + "feelslike_f": 77.8, + "windchill_c": 27.3, + "windchill_f": 81.1, + "heatindex_c": 25.5, + "heatindex_f": 77.8, + "dewpoint_c": 2.4, + "dewpoint_f": 36.4, + "will_it_rain": 0, + "chance_of_rain": 0, + "will_it_snow": 0, + "chance_of_snow": 0, + "vis_km": 10.0, + "vis_miles": 6.0, + "gust_mph": 9.1, + "gust_kph": 14.7, + "uv": 0 + }, + { + "time_epoch": 1722002400, + "time": "2024-07-26 07:00", + "temp_c": 28.1, + "temp_f": 82.7, + "is_day": 1, + "condition": { + "text": "Sunny", + "icon": "//cdn.weatherapi.com/weather/64x64/day/113.png", + "code": 1000 + }, + "wind_mph": 2.9, + "wind_kph": 4.7, + "wind_degree": 310, + "wind_dir": "NW", + "pressure_mb": 1011.0, + "pressure_in": 29.86, + "precip_mm": 0.0, + "precip_in": 0.0, + "snow_cm": 0.0, + "humidity": 20, + "cloud": 8, + "feelslike_c": 26.2, + "feelslike_f": 79.1, + "windchill_c": 28.1, + "windchill_f": 82.7, + "heatindex_c": 26.2, + "heatindex_f": 79.1, + "dewpoint_c": 2.3, + "dewpoint_f": 36.2, + "will_it_rain": 0, + "chance_of_rain": 0, + "will_it_snow": 0, + "chance_of_snow": 0, + "vis_km": 10.0, + "vis_miles": 6.0, + "gust_mph": 4.1, + "gust_kph": 6.6, + "uv": 7.0 + }, + { + "time_epoch": 1722006000, + "time": "2024-07-26 08:00", + "temp_c": 29.5, + "temp_f": 85.2, + "is_day": 1, + "condition": { + "text": "Sunny", + "icon": "//cdn.weatherapi.com/weather/64x64/day/113.png", + "code": 1000 + }, + "wind_mph": 2.0, + "wind_kph": 3.2, + "wind_degree": 273, + "wind_dir": "W", + "pressure_mb": 1012.0, + "pressure_in": 29.87, + "precip_mm": 0.0, + "precip_in": 0.0, + "snow_cm": 0.0, + "humidity": 18, + "cloud": 8, + "feelslike_c": 27.4, + "feelslike_f": 81.4, + "windchill_c": 29.5, + "windchill_f": 85.2, + "heatindex_c": 27.4, + "heatindex_f": 81.4, + "dewpoint_c": 2.4, + "dewpoint_f": 36.3, + "will_it_rain": 0, + "chance_of_rain": 0, + "will_it_snow": 0, + "chance_of_snow": 0, + "vis_km": 10.0, + "vis_miles": 6.0, + "gust_mph": 2.3, + "gust_kph": 3.7, + "uv": 7.0 + }, + { + "time_epoch": 1722009600, + "time": "2024-07-26 09:00", + "temp_c": 31.3, + "temp_f": 88.4, + "is_day": 1, + "condition": { + "text": "Sunny", + "icon": "//cdn.weatherapi.com/weather/64x64/day/113.png", + "code": 1000 + }, + "wind_mph": 2.7, + "wind_kph": 4.3, + "wind_degree": 267, + "wind_dir": "W", + "pressure_mb": 1011.0, + "pressure_in": 29.86, + "precip_mm": 0.0, + "precip_in": 0.0, + "snow_cm": 0.0, + "humidity": 16, + "cloud": 7, + "feelslike_c": 29.2, + "feelslike_f": 84.6, + "windchill_c": 31.3, + "windchill_f": 88.4, + "heatindex_c": 29.2, + "heatindex_f": 84.6, + "dewpoint_c": 2.2, + "dewpoint_f": 36.0, + "will_it_rain": 0, + "chance_of_rain": 0, + "will_it_snow": 0, + "chance_of_snow": 0, + "vis_km": 10.0, + "vis_miles": 6.0, + "gust_mph": 3.1, + "gust_kph": 5.0, + "uv": 8.0 + }, + { + "time_epoch": 1722013200, + "time": "2024-07-26 10:00", + "temp_c": 33.3, + "temp_f": 91.9, + "is_day": 1, + "condition": { + "text": "Sunny", + "icon": "//cdn.weatherapi.com/weather/64x64/day/113.png", + "code": 1000 + }, + "wind_mph": 4.3, + "wind_kph": 6.8, + "wind_degree": 287, + "wind_dir": "WNW", + "pressure_mb": 1011.0, + "pressure_in": 29.84, + "precip_mm": 0.0, + "precip_in": 0.0, + "snow_cm": 0.0, + "humidity": 14, + "cloud": 7, + "feelslike_c": 31.4, + "feelslike_f": 88.5, + "windchill_c": 33.3, + "windchill_f": 91.9, + "heatindex_c": 31.4, + "heatindex_f": 88.5, + "dewpoint_c": 2.0, + "dewpoint_f": 35.5, + "will_it_rain": 0, + "chance_of_rain": 0, + "will_it_snow": 0, + "chance_of_snow": 0, + "vis_km": 10.0, + "vis_miles": 6.0, + "gust_mph": 4.9, + "gust_kph": 7.9, + "uv": 8.0 + }, + { + "time_epoch": 1722016800, + "time": "2024-07-26 11:00", + "temp_c": 35.4, + "temp_f": 95.7, + "is_day": 1, + "condition": { + "text": "Sunny", + "icon": "//cdn.weatherapi.com/weather/64x64/day/113.png", + "code": 1000 + }, + "wind_mph": 5.8, + "wind_kph": 9.4, + "wind_degree": 304, + "wind_dir": "NW", + "pressure_mb": 1010.0, + "pressure_in": 29.83, + "precip_mm": 0.0, + "precip_in": 0.0, + "snow_cm": 0.0, + "humidity": 12, + "cloud": 6, + "feelslike_c": 33.8, + "feelslike_f": 92.9, + "windchill_c": 35.4, + "windchill_f": 95.7, + "heatindex_c": 33.8, + "heatindex_f": 92.9, + "dewpoint_c": 1.3, + "dewpoint_f": 34.4, + "will_it_rain": 0, + "chance_of_rain": 0, + "will_it_snow": 0, + "chance_of_snow": 0, + "vis_km": 10.0, + "vis_miles": 6.0, + "gust_mph": 6.7, + "gust_kph": 10.8, + "uv": 9.0 + }, + { + "time_epoch": 1722020400, + "time": "2024-07-26 12:00", + "temp_c": 37.4, + "temp_f": 99.4, + "is_day": 1, + "condition": { + "text": "Sunny", + "icon": "//cdn.weatherapi.com/weather/64x64/day/113.png", + "code": 1000 + }, + "wind_mph": 6.0, + "wind_kph": 9.7, + "wind_degree": 309, + "wind_dir": "NW", + "pressure_mb": 1009.0, + "pressure_in": 29.81, + "precip_mm": 0.0, + "precip_in": 0.0, + "snow_cm": 0.0, + "humidity": 10, + "cloud": 6, + "feelslike_c": 36.5, + "feelslike_f": 97.7, + "windchill_c": 37.4, + "windchill_f": 99.4, + "heatindex_c": 36.5, + "heatindex_f": 97.7, + "dewpoint_c": 0.4, + "dewpoint_f": 32.7, + "will_it_rain": 0, + "chance_of_rain": 0, + "will_it_snow": 0, + "chance_of_snow": 0, + "vis_km": 10.0, + "vis_miles": 6.0, + "gust_mph": 7.0, + "gust_kph": 11.2, + "uv": 9.0 + }, + { + "time_epoch": 1722024000, + "time": "2024-07-26 13:00", + "temp_c": 39.3, + "temp_f": 102.8, + "is_day": 1, + "condition": { + "text": "Sunny", + "icon": "//cdn.weatherapi.com/weather/64x64/day/113.png", + "code": 1000 + }, + "wind_mph": 4.5, + "wind_kph": 7.2, + "wind_degree": 303, + "wind_dir": "WNW", + "pressure_mb": 1008.0, + "pressure_in": 29.78, + "precip_mm": 0.0, + "precip_in": 0.0, + "snow_cm": 0.0, + "humidity": 9, + "cloud": 5, + "feelslike_c": 39.1, + "feelslike_f": 102.5, + "windchill_c": 39.3, + "windchill_f": 102.8, + "heatindex_c": 39.1, + "heatindex_f": 102.5, + "dewpoint_c": 0.1, + "dewpoint_f": 32.1, + "will_it_rain": 0, + "chance_of_rain": 0, + "will_it_snow": 0, + "chance_of_snow": 0, + "vis_km": 10.0, + "vis_miles": 6.0, + "gust_mph": 5.1, + "gust_kph": 8.3, + "uv": 9.0 + }, + { + "time_epoch": 1722027600, + "time": "2024-07-26 14:00", + "temp_c": 40.8, + "temp_f": 105.5, + "is_day": 1, + "condition": { + "text": "Sunny", + "icon": "//cdn.weatherapi.com/weather/64x64/day/113.png", + "code": 1000 + }, + "wind_mph": 2.9, + "wind_kph": 4.7, + "wind_degree": 284, + "wind_dir": "WNW", + "pressure_mb": 1007.0, + "pressure_in": 29.75, + "precip_mm": 0.0, + "precip_in": 0.0, + "snow_cm": 0.0, + "humidity": 8, + "cloud": 4, + "feelslike_c": 41.3, + "feelslike_f": 106.4, + "windchill_c": 40.8, + "windchill_f": 105.5, + "heatindex_c": 41.3, + "heatindex_f": 106.4, + "dewpoint_c": 1.0, + "dewpoint_f": 33.9, + "will_it_rain": 0, + "chance_of_rain": 0, + "will_it_snow": 0, + "chance_of_snow": 0, + "vis_km": 10.0, + "vis_miles": 6.0, + "gust_mph": 3.4, + "gust_kph": 5.4, + "uv": 10.0 + }, + { + "time_epoch": 1722031200, + "time": "2024-07-26 15:00", + "temp_c": 41.8, + "temp_f": 107.2, + "is_day": 1, + "condition": { + "text": "Sunny", + "icon": "//cdn.weatherapi.com/weather/64x64/day/113.png", + "code": 1000 + }, + "wind_mph": 1.6, + "wind_kph": 2.5, + "wind_degree": 229, + "wind_dir": "SW", + "pressure_mb": 1006.0, + "pressure_in": 29.71, + "precip_mm": 0.0, + "precip_in": 0.0, + "snow_cm": 0.0, + "humidity": 9, + "cloud": 1, + "feelslike_c": 42.7, + "feelslike_f": 108.9, + "windchill_c": 41.8, + "windchill_f": 107.2, + "heatindex_c": 42.7, + "heatindex_f": 108.9, + "dewpoint_c": 2.5, + "dewpoint_f": 36.6, + "will_it_rain": 0, + "chance_of_rain": 0, + "will_it_snow": 0, + "chance_of_snow": 0, + "vis_km": 10.0, + "vis_miles": 6.0, + "gust_mph": 1.8, + "gust_kph": 2.9, + "uv": 10.0 + }, + { + "time_epoch": 1722034800, + "time": "2024-07-26 16:00", + "temp_c": 42.2, + "temp_f": 108.0, + "is_day": 1, + "condition": { + "text": "Sunny", + "icon": "//cdn.weatherapi.com/weather/64x64/day/113.png", + "code": 1000 + }, + "wind_mph": 2.5, + "wind_kph": 4.0, + "wind_degree": 119, + "wind_dir": "ESE", + "pressure_mb": 1005.0, + "pressure_in": 29.68, + "precip_mm": 0.0, + "precip_in": 0.0, + "snow_cm": 0.0, + "humidity": 9, + "cloud": 0, + "feelslike_c": 43.4, + "feelslike_f": 110.1, + "windchill_c": 42.2, + "windchill_f": 108.0, + "heatindex_c": 43.4, + "heatindex_f": 110.1, + "dewpoint_c": 3.4, + "dewpoint_f": 38.1, + "will_it_rain": 0, + "chance_of_rain": 0, + "will_it_snow": 0, + "chance_of_snow": 0, + "vis_km": 10.0, + "vis_miles": 6.0, + "gust_mph": 2.8, + "gust_kph": 4.6, + "uv": 10.0 + }, + { + "time_epoch": 1722038400, + "time": "2024-07-26 17:00", + "temp_c": 40.4, + "temp_f": 104.7, + "is_day": 1, + "condition": { + "text": "Sunny", + "icon": "//cdn.weatherapi.com/weather/64x64/day/113.png", + "code": 1000 + }, + "wind_mph": 7.4, + "wind_kph": 11.9, + "wind_degree": 215, + "wind_dir": "SW", + "pressure_mb": 1004.0, + "pressure_in": 29.65, + "precip_mm": 0.0, + "precip_in": 0.0, + "snow_cm": 0.0, + "humidity": 8, + "cloud": 0, + "feelslike_c": 40.6, + "feelslike_f": 105.2, + "windchill_c": 40.4, + "windchill_f": 104.7, + "heatindex_c": 40.6, + "heatindex_f": 105.2, + "dewpoint_c": 1.6, + "dewpoint_f": 35.0, + "will_it_rain": 0, + "chance_of_rain": 0, + "will_it_snow": 0, + "chance_of_snow": 0, + "vis_km": 10.0, + "vis_miles": 6.0, + "gust_mph": 8.5, + "gust_kph": 13.7, + "uv": 10.0 + }, + { + "time_epoch": 1722042000, + "time": "2024-07-26 18:00", + "temp_c": 38.0, + "temp_f": 100.4, + "is_day": 1, + "condition": { + "text": "Sunny", + "icon": "//cdn.weatherapi.com/weather/64x64/day/113.png", + "code": 1000 + }, + "wind_mph": 22.1, + "wind_kph": 35.6, + "wind_degree": 306, + "wind_dir": "NW", + "pressure_mb": 1005.0, + "pressure_in": 29.67, + "precip_mm": 0.0, + "precip_in": 0.0, + "snow_cm": 0.0, + "humidity": 10, + "cloud": 0, + "feelslike_c": 37.3, + "feelslike_f": 99.1, + "windchill_c": 38.0, + "windchill_f": 100.4, + "heatindex_c": 37.3, + "heatindex_f": 99.1, + "dewpoint_c": 1.0, + "dewpoint_f": 33.8, + "will_it_rain": 0, + "chance_of_rain": 0, + "will_it_snow": 0, + "chance_of_snow": 0, + "vis_km": 10.0, + "vis_miles": 6.0, + "gust_mph": 25.5, + "gust_kph": 41.0, + "uv": 9.0 + }, + { + "time_epoch": 1722045600, + "time": "2024-07-26 19:00", + "temp_c": 35.1, + "temp_f": 95.2, + "is_day": 1, + "condition": { + "text": "Sunny", + "icon": "//cdn.weatherapi.com/weather/64x64/day/113.png", + "code": 1000 + }, + "wind_mph": 18.8, + "wind_kph": 30.2, + "wind_degree": 312, + "wind_dir": "NW", + "pressure_mb": 1006.0, + "pressure_in": 29.69, + "precip_mm": 0.0, + "precip_in": 0.0, + "snow_cm": 0.0, + "humidity": 11, + "cloud": 0, + "feelslike_c": 33.7, + "feelslike_f": 92.6, + "windchill_c": 35.1, + "windchill_f": 95.3, + "heatindex_c": 33.7, + "heatindex_f": 92.6, + "dewpoint_c": 1.0, + "dewpoint_f": 33.9, + "will_it_rain": 0, + "chance_of_rain": 0, + "will_it_snow": 0, + "chance_of_snow": 0, + "vis_km": 10.0, + "vis_miles": 6.0, + "gust_mph": 21.6, + "gust_kph": 34.8, + "uv": 9.0 + }, + { + "time_epoch": 1722049200, + "time": "2024-07-26 20:00", + "temp_c": 32.6, + "temp_f": 90.7, + "is_day": 1, + "condition": { + "text": "Sunny", + "icon": "//cdn.weatherapi.com/weather/64x64/day/113.png", + "code": 1000 + }, + "wind_mph": 17.2, + "wind_kph": 27.7, + "wind_degree": 326, + "wind_dir": "NNW", + "pressure_mb": 1007.0, + "pressure_in": 29.73, + "precip_mm": 0.0, + "precip_in": 0.0, + "snow_cm": 0.0, + "humidity": 13, + "cloud": 0, + "feelslike_c": 30.8, + "feelslike_f": 87.4, + "windchill_c": 32.6, + "windchill_f": 90.7, + "heatindex_c": 30.8, + "heatindex_f": 87.4, + "dewpoint_c": 0.3, + "dewpoint_f": 32.6, + "will_it_rain": 0, + "chance_of_rain": 0, + "will_it_snow": 0, + "chance_of_snow": 0, + "vis_km": 10.0, + "vis_miles": 6.0, + "gust_mph": 20.4, + "gust_kph": 32.8, + "uv": 8.0 + }, + { + "time_epoch": 1722052800, + "time": "2024-07-26 21:00", + "temp_c": 30.2, + "temp_f": 86.3, + "is_day": 0, + "condition": { + "text": "Clear ", + "icon": "//cdn.weatherapi.com/weather/64x64/night/113.png", + "code": 1000 + }, + "wind_mph": 15.7, + "wind_kph": 25.2, + "wind_degree": 326, + "wind_dir": "NNW", + "pressure_mb": 1008.0, + "pressure_in": 29.76, + "precip_mm": 0.0, + "precip_in": 0.0, + "snow_cm": 0.0, + "humidity": 16, + "cloud": 0, + "feelslike_c": 28.3, + "feelslike_f": 82.9, + "windchill_c": 30.2, + "windchill_f": 86.3, + "heatindex_c": 28.3, + "heatindex_f": 82.9, + "dewpoint_c": 1.6, + "dewpoint_f": 35.0, + "will_it_rain": 0, + "chance_of_rain": 0, + "will_it_snow": 0, + "chance_of_snow": 0, + "vis_km": 10.0, + "vis_miles": 6.0, + "gust_mph": 20.1, + "gust_kph": 32.4, + "uv": 0 + }, + { + "time_epoch": 1722056400, + "time": "2024-07-26 22:00", + "temp_c": 28.2, + "temp_f": 82.8, + "is_day": 0, + "condition": { + "text": "Clear ", + "icon": "//cdn.weatherapi.com/weather/64x64/night/113.png", + "code": 1000 + }, + "wind_mph": 14.8, + "wind_kph": 23.8, + "wind_degree": 329, + "wind_dir": "NNW", + "pressure_mb": 1009.0, + "pressure_in": 29.79, + "precip_mm": 0.0, + "precip_in": 0.0, + "snow_cm": 0.0, + "humidity": 20, + "cloud": 0, + "feelslike_c": 26.7, + "feelslike_f": 80.0, + "windchill_c": 28.2, + "windchill_f": 82.8, + "heatindex_c": 26.7, + "heatindex_f": 80.0, + "dewpoint_c": 2.6, + "dewpoint_f": 36.7, + "will_it_rain": 0, + "chance_of_rain": 0, + "will_it_snow": 0, + "chance_of_snow": 0, + "vis_km": 10.0, + "vis_miles": 6.0, + "gust_mph": 20.1, + "gust_kph": 32.3, + "uv": 0 + }, + { + "time_epoch": 1722060000, + "time": "2024-07-26 23:00", + "temp_c": 26.8, + "temp_f": 80.2, + "is_day": 0, + "condition": { + "text": "Clear ", + "icon": "//cdn.weatherapi.com/weather/64x64/night/113.png", + "code": 1000 + }, + "wind_mph": 13.0, + "wind_kph": 20.9, + "wind_degree": 323, + "wind_dir": "NW", + "pressure_mb": 1009.0, + "pressure_in": 29.8, + "precip_mm": 0.0, + "precip_in": 0.0, + "snow_cm": 0.0, + "humidity": 24, + "cloud": 0, + "feelslike_c": 25.6, + "feelslike_f": 78.1, + "windchill_c": 26.8, + "windchill_f": 80.2, + "heatindex_c": 25.6, + "heatindex_f": 78.1, + "dewpoint_c": 4.2, + "dewpoint_f": 39.5, + "will_it_rain": 0, + "chance_of_rain": 0, + "will_it_snow": 0, + "chance_of_snow": 0, + "vis_km": 10.0, + "vis_miles": 6.0, + "gust_mph": 18.2, + "gust_kph": 29.3, + "uv": 0 + } + ] + } + ] + } +} diff --git a/lib/types/options.d.ts b/lib/types/options.d.ts new file mode 100644 index 0000000..e89b98c --- /dev/null +++ b/lib/types/options.d.ts @@ -0,0 +1 @@ +export type Unit = "imperial" | "metric"; diff --git a/lib/types/weather.d.ts b/lib/types/weather.d.ts new file mode 100644 index 0000000..44eaa0d --- /dev/null +++ b/lib/types/weather.d.ts @@ -0,0 +1,107 @@ +export type Weather = { + location: Location; + current: Current; + forecast: Forecast; +} + +export type Current = { + last_updated_epoch?: number; + last_updated?: string; + temp_c: number; + temp_f: number; + is_day: number; + condition: Condition; + wind_mph: number; + wind_kph: number; + wind_degree: number; + wind_dir: string; + pressure_mb: number; + pressure_in: number; + precip_mm: number; + precip_in: number; + humidity: number; + cloud: number; + feelslike_c: number; + feelslike_f: number; + windchill_c: number; + windchill_f: number; + heatindex_c: number; + heatindex_f: number; + dewpoint_c: number; + dewpoint_f: number; + vis_km: number; + vis_miles: number; + uv: number; + gust_mph: number; + gust_kph: number; + time_epoch?: number; + time?: string; + snow_cm?: number; + will_it_rain?: number; + chance_of_rain?: number; + will_it_snow?: number; + chance_of_snow?: number; +} + +export type Condition = { + text: string; + icon: string; + code: number; +} + +export type Forecast = { + forecastday: Forecastday[]; +} + +export type Forecastday = { + date: string; + date_epoch: number; + day: Day; + astro: Astro; + hour: Current[]; +} + +export type Astro = { + sunrise: string; + sunset: string; + moonrise: string; + moonset: string; + moon_phase: string; + moon_illumination: number; + is_moon_up: number; + is_sun_up: number; +} + +export type Day = { + maxtemp_c: number; + maxtemp_f: number; + mintemp_c: number; + mintemp_f: number; + avgtemp_c: number; + avgtemp_f: number; + maxwind_mph: number; + maxwind_kph: number; + totalprecip_mm: number; + totalprecip_in: number; + totalsnow_cm: number; + avgvis_km: number; + avgvis_miles: number; + avghumidity: number; + daily_will_it_rain: number; + daily_chance_of_rain: number; + daily_will_it_snow: number; + daily_chance_of_snow: number; + condition: Condition; + uv: number; +} + +export type Location = { + name: string; + region: string; + country: string; + lat: number; + lon: number; + tz_id: string; + localtime_epoch: number; + localtime: string; +} diff --git a/lib/types/workspace.d.ts b/lib/types/workspace.d.ts new file mode 100644 index 0000000..1117291 --- /dev/null +++ b/lib/types/workspace.d.ts @@ -0,0 +1,8 @@ +export type WorkspaceRule = { + workspaceString: string, + monitor: string, +} + +export type WorkspaceMap = { + [key: string]: number[], +} diff --git a/modules/bar/battery/index.js b/modules/bar/battery/index.js deleted file mode 100644 index c028929..0000000 --- a/modules/bar/battery/index.js +++ /dev/null @@ -1,82 +0,0 @@ -const battery = await Service.import("battery"); -import { openMenu } from "../utils.js"; -import options from "options"; - -const { label: show_label } = options.bar.battery; - -const BatteryLabel = () => { - const isVis = Variable(battery.available); - - const icon = () => - battery - .bind("percent") - .as((p) => `battery-level-${Math.floor(p / 10) * 10}-symbolic`); - - battery.connect("changed", ({ available }) => { - isVis.value = available; - }); - - const formatTime = (seconds) => { - const hours = Math.floor(seconds / 3600); - const minutes = Math.floor((seconds % 3600) / 60); - return { hours, minutes }; - }; - - const generateTooltip = (timeSeconds, isCharging, isCharged) => { - if (isCharged) { - return "Fully Charged!!!"; - } - - const { hours, minutes } = formatTime(timeSeconds); - if (isCharging) { - return `${hours} hours ${minutes} minutes until full`; - } else { - return `${hours} hours ${minutes} minutes left`; - } - }; - - return { - component: Widget.Box({ - class_name: "battery", - visible: battery.bind("available"), - tooltip_text: battery.bind("time_remaining").as((t) => t.toString()), - children: Utils.merge( - [battery.bind("available"), show_label.bind("value")], - (batAvail, showLabel) => { - if (batAvail && showLabel) { - return [ - Widget.Icon({ icon: icon() }), - Widget.Label({ - label: battery.bind("percent").as((p) => ` ${p}%`), - }), - ]; - } else if (batAvail && !showLabel) { - return [Widget.Icon({ icon: icon() })]; - } else { - return []; - } - }, - ), - setup: (self) => { - self.hook(battery, () => { - if (battery.available) { - self.tooltip_text = generateTooltip( - battery.time_remaining, - battery.charging, - battery.charged, - ); - } - }); - }, - }), - isVis, - boxClass: "battery", - props: { - on_primary_click: (clicked, event) => { - openMenu(clicked, event, "energymenu"); - }, - }, - }; -}; - -export { BatteryLabel }; diff --git a/modules/bar/battery/index.ts b/modules/bar/battery/index.ts new file mode 100644 index 0000000..977a2a8 --- /dev/null +++ b/modules/bar/battery/index.ts @@ -0,0 +1,84 @@ +const battery = await Service.import("battery"); +import Gdk from 'gi://Gdk?version=3.0'; +import EventHandler from 'types/widgets/button.ts' +import { openMenu } from "../utils.js"; +import options from "options"; + +const { label: show_label } = options.bar.battery; + +const BatteryLabel = () => { + const isVis = Variable(battery.available); + + const icon = () => + battery + .bind("percent") + .as((p) => `battery-level-${Math.floor(p / 10) * 10}-symbolic`); + + battery.connect("changed", ({ available }) => { + isVis.value = available; + }); + + const formatTime = (seconds: number) => { + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + return { hours, minutes }; + }; + + const generateTooltip = (timeSeconds: number, isCharging: boolean, isCharged: boolean) => { + if (isCharged) { + return "Fully Charged!!!"; + } + + const { hours, minutes } = formatTime(timeSeconds); + if (isCharging) { + return `${hours} hours ${minutes} minutes until full`; + } else { + return `${hours} hours ${minutes} minutes left`; + } + }; + + return { + component: Widget.Box({ + class_name: "battery", + visible: battery.bind("available"), + tooltip_text: battery.bind("time_remaining").as((t) => t.toString()), + children: Utils.merge( + [battery.bind("available"), show_label.bind("value")], + (batAvail, showLabel) => { + if (batAvail && showLabel) { + return [ + Widget.Icon({ icon: icon() }), + Widget.Label({ + label: battery.bind("percent").as((p) => ` ${p}%`), + }), + ]; + } else if (batAvail && !showLabel) { + return [Widget.Icon({ icon: icon() })]; + } else { + return []; + } + }, + ), + setup: (self) => { + self.hook(battery, () => { + if (battery.available) { + self.tooltip_text = generateTooltip( + battery.time_remaining, + battery.charging, + battery.charged, + ); + } + }); + }, + }), + isVis, + boxClass: "battery", + props: { + on_primary_click: (clicked: any, event: Gdk.Event) => { + openMenu(clicked, event, "energymenu"); + }, + }, + }; +}; + +export { BatteryLabel }; diff --git a/modules/bar/bluetooth/index.js b/modules/bar/bluetooth/index.js deleted file mode 100644 index f7747a0..0000000 --- a/modules/bar/bluetooth/index.js +++ /dev/null @@ -1,38 +0,0 @@ -const bluetooth = await Service.import('bluetooth') -import options from "options"; -import { openMenu } from "../utils.js"; - -const Bluetooth = () => { - const btIcon = Widget.Label({ - label: bluetooth.bind("enabled").as((v) => v ? "󰂯" : "󰂲"), - class_name: "bar-bt_icon", - }); - - const btText = Widget.Label({ - label: Utils.merge([bluetooth.bind("enabled"), options.bar.bluetooth.label.bind("value")], (btEnabled, showLabel) => { - if (showLabel) { - return btEnabled ? " On" : " Off" - } - return ""; - - }), - class_name: "bar-bt_label", - }); - - return { - component: Widget.Box({ - class_name: "volume", - children: [btIcon, btText], - }), - isVisible: true, - boxClass: "bluetooth", - props: { - on_primary_click: (clicked, event) => { - openMenu(clicked, event, "bluetoothmenu"); - }, - }, - }; - -} - -export { Bluetooth } diff --git a/modules/bar/bluetooth/index.ts b/modules/bar/bluetooth/index.ts new file mode 100644 index 0000000..402af23 --- /dev/null +++ b/modules/bar/bluetooth/index.ts @@ -0,0 +1,39 @@ +const bluetooth = await Service.import('bluetooth') +import Gdk from 'gi://Gdk?version=3.0'; +import options from "options"; +import { openMenu } from "../utils.js"; + +const Bluetooth = () => { + const btIcon = Widget.Label({ + label: bluetooth.bind("enabled").as((v) => v ? "󰂯" : "󰂲"), + class_name: "bar-bt_icon", + }); + + const btText = Widget.Label({ + label: Utils.merge([bluetooth.bind("enabled"), options.bar.bluetooth.label.bind("value")], (btEnabled, showLabel) => { + if (showLabel) { + return btEnabled ? " On" : " Off" + } + return ""; + + }), + class_name: "bar-bt_label", + }); + + return { + component: Widget.Box({ + class_name: "volume", + children: [btIcon, btText], + }), + isVisible: true, + boxClass: "bluetooth", + props: { + on_primary_click: (clicked: any, event: Gdk.Event) => { + openMenu(clicked, event, "bluetoothmenu"); + }, + }, + }; + +} + +export { Bluetooth } diff --git a/modules/bar/clock/index.js b/modules/bar/clock/index.js deleted file mode 100644 index 27823a8..0000000 --- a/modules/bar/clock/index.js +++ /dev/null @@ -1,27 +0,0 @@ -import GLib from "gi://GLib"; -import { openMenu } from "../utils.js"; -import options from "options"; -const { format } = options.bar.clock; - -const date = Variable(GLib.DateTime.new_now_local(), { - poll: [1000, () => GLib.DateTime.new_now_local()], -}); -const time = Utils.derive([date, format], (c, f) => c.format(f) || ""); - -const Clock = () => { - return { - component: Widget.Label({ - class_name: "clock", - label: time.bind(), - }), - isVisible: true, - boxClass: "clock", - props: { - on_primary_click: (clicked, event) => { - openMenu(clicked, event, "calendarmenu"); - }, - }, - }; -}; - -export { Clock }; diff --git a/modules/bar/clock/index.ts b/modules/bar/clock/index.ts new file mode 100644 index 0000000..dcab26d --- /dev/null +++ b/modules/bar/clock/index.ts @@ -0,0 +1,28 @@ +import Gdk from 'gi://Gdk?version=3.0'; +import GLib from "gi://GLib"; +import { openMenu } from "../utils.js"; +import options from "options"; +const { format } = options.bar.clock; + +const date = Variable(GLib.DateTime.new_now_local(), { + poll: [1000, () => GLib.DateTime.new_now_local()], +}); +const time = Utils.derive([date, format], (c, f) => c.format(f) || ""); + +const Clock = () => { + return { + component: Widget.Label({ + class_name: "clock", + label: time.bind(), + }), + isVisible: true, + boxClass: "clock", + props: { + on_primary_click: (clicked: any, event: Gdk.Event) => { + openMenu(clicked, event, "calendarmenu"); + }, + }, + }; +}; + +export { Clock }; diff --git a/modules/bar/media/index.js b/modules/bar/media/index.js deleted file mode 100644 index cfe571a..0000000 --- a/modules/bar/media/index.js +++ /dev/null @@ -1,101 +0,0 @@ -const mpris = await Service.import("mpris"); -import { openMenu } from "../utils.js"; - -const Media = () => { - const activePlayer = Variable(mpris.players[0]); - - mpris.connect("changed", (value) => { - const statusOrder = { - Playing: 1, - Paused: 2, - Stopped: 3, - }; - - if (value.players.length === 0) { - activePlayer.value = mpris.players[0]; - return; - } - - const isPlaying = value.players.find( - (p) => p["play-back-status"] === "Playing", - ); - - if (isPlaying) { - activePlayer.value = value.players.sort( - (a, b) => - statusOrder[a["play-back-status"]] - - statusOrder[b["play-back-status"]], - )[0]; - } - }); - - const getIconForPlayer = (playerName) => { - const windowTitleMap = [ - ["Mozilla Firefox", "󰈹 "], - ["Microsoft Edge", "󰇩 "], - ["(.*)Discord(.*)", " "], - ["Plex", "󰚺 "], - ["(.*) Spotify Free", "󰓇 "], - ["(.*)Spotify Premium", "󰓇 "], - ["Spotify", "󰓇 "], - ["(.*)", "󰝚 "], - ]; - - const foundMatch = windowTitleMap.find((wt) => - RegExp(wt[0]).test(playerName), - ); - - return foundMatch ? foundMatch[1] : "󰝚"; - }; - - const songIcon = Variable(""); - - const label = Utils.watch("󰎇 Media 󰎇", mpris, "changed", () => { - if (activePlayer.value) { - const { track_title, identity } = activePlayer.value; - songIcon.value = getIconForPlayer(identity); - return track_title.length === 0 - ? ` No media playing...` - : ` ${track_title}`; - } else { - songIcon.value = ""; - return "󰎇 Media 󰎇"; - } - }); - - return { - component: Widget.Box({ - visible: false, - child: Widget.Box({ - class_name: "media", - child: Widget.Box({ - children: [ - Widget.Label({ - class_name: "bar-media_icon", - label: songIcon.bind("value"), - maxWidthChars: 30, - }), - Widget.Label({ - label, - truncate: "end", - wrap: true, - maxWidthChars: 30, - }), - ], - }), - }), - }), - isVisible: false, - boxClass: "media", - name: "media", - props: { - on_scroll_up: () => mpris.getPlayer("")?.next(), - on_scroll_down: () => mpris.getPlayer("")?.previous(), - on_primary_click: (clicked, event) => { - openMenu(clicked, event, "mediamenu"); - }, - }, - }; -}; - -export { Media }; diff --git a/modules/bar/media/index.ts b/modules/bar/media/index.ts new file mode 100644 index 0000000..7b48255 --- /dev/null +++ b/modules/bar/media/index.ts @@ -0,0 +1,102 @@ +import Gdk from 'gi://Gdk?version=3.0'; +const mpris = await Service.import("mpris"); +import { openMenu } from "../utils.js"; + +const Media = () => { + const activePlayer = Variable(mpris.players[0]); + + mpris.connect("changed", (value) => { + const statusOrder = { + Playing: 1, + Paused: 2, + Stopped: 3, + }; + + if (value.players.length === 0) { + activePlayer.value = mpris.players[0]; + return; + } + + const isPlaying = value.players.find( + (p) => p["play-back-status"] === "Playing", + ); + + if (isPlaying) { + activePlayer.value = value.players.sort( + (a, b) => + statusOrder[a["play-back-status"]] - + statusOrder[b["play-back-status"]], + )[0]; + } + }); + + const getIconForPlayer = (playerName: string) => { + const windowTitleMap = [ + ["Mozilla Firefox", "󰈹 "], + ["Microsoft Edge", "󰇩 "], + ["(.*)Discord(.*)", " "], + ["Plex", "󰚺 "], + ["(.*) Spotify Free", "󰓇 "], + ["(.*)Spotify Premium", "󰓇 "], + ["Spotify", "󰓇 "], + ["(.*)", "󰝚 "], + ]; + + const foundMatch = windowTitleMap.find((wt) => + RegExp(wt[0]).test(playerName), + ); + + return foundMatch ? foundMatch[1] : "󰝚"; + }; + + const songIcon = Variable(""); + + const label = Utils.watch("󰎇 Media 󰎇", mpris, "changed", () => { + if (activePlayer.value) { + const { track_title, identity } = activePlayer.value; + songIcon.value = getIconForPlayer(identity); + return track_title.length === 0 + ? ` No media playing...` + : ` ${track_title}`; + } else { + songIcon.value = ""; + return "󰎇 Media 󰎇"; + } + }); + + return { + component: Widget.Box({ + visible: false, + child: Widget.Box({ + class_name: "media", + child: Widget.Box({ + children: [ + Widget.Label({ + class_name: "bar-media_icon", + label: songIcon.bind("value"), + maxWidthChars: 30, + }), + Widget.Label({ + label, + truncate: "end", + wrap: true, + maxWidthChars: 30, + }), + ], + }), + }), + }), + isVisible: false, + boxClass: "media", + name: "media", + props: { + on_scroll_up: () => mpris.getPlayer("")?.next(), + on_scroll_down: () => mpris.getPlayer("")?.previous(), + on_primary_click: (clicked: any, event: Gdk.Event) => { + openMenu(clicked, event, "mediamenu"); + }, + }, + }; +}; + +export { Media }; diff --git a/modules/bar/menu/index.js b/modules/bar/menu/index.js deleted file mode 100644 index 16a406f..0000000 --- a/modules/bar/menu/index.js +++ /dev/null @@ -1,22 +0,0 @@ -import { openMenu } from "../utils.js"; -import options from "options"; - -const Menu = () => { - return { - component: Widget.Box({ - child: Widget.Label({ - class_name: "bar-menu_label", - label: options.bar.launcher.icon.bind("value"), - }), - }), - isVisible: true, - boxClass: "dashboard", - props: { - on_primary_click: (clicked, event) => { - openMenu(clicked, event, "dashboardmenu"); - }, - }, - }; -}; - -export { Menu }; diff --git a/modules/bar/menu/index.ts b/modules/bar/menu/index.ts new file mode 100644 index 0000000..2e29e17 --- /dev/null +++ b/modules/bar/menu/index.ts @@ -0,0 +1,23 @@ +import Gdk from 'gi://Gdk?version=3.0'; +import { openMenu } from "../utils.js"; +import options from "options"; + +const Menu = () => { + return { + component: Widget.Box({ + child: Widget.Label({ + class_name: "bar-menu_label", + label: options.bar.launcher.icon.bind("value"), + }), + }), + isVisible: true, + boxClass: "dashboard", + props: { + on_primary_click: (clicked: any, event: Gdk.Event) => { + openMenu(clicked, event, "dashboardmenu"); + }, + }, + }; +}; + +export { Menu }; diff --git a/modules/bar/network/index.js b/modules/bar/network/index.ts similarity index 94% rename from modules/bar/network/index.js rename to modules/bar/network/index.ts index 61bb16b..6d02ef7 100644 --- a/modules/bar/network/index.js +++ b/modules/bar/network/index.ts @@ -1,3 +1,4 @@ +import Gdk from 'gi://Gdk?version=3.0'; const network = await Service.import("network"); import options from "options"; import { openMenu } from "../utils.js"; @@ -52,7 +53,7 @@ const Network = () => { isVisible: true, boxClass: "network", props: { - on_primary_click: (clicked, event) => { + on_primary_click: (clicked: any, event: Gdk.Event) => { openMenu(clicked, event, "networkmenu"); }, }, diff --git a/modules/bar/notifications/index.js b/modules/bar/notifications/index.js deleted file mode 100644 index 2b7b997..0000000 --- a/modules/bar/notifications/index.js +++ /dev/null @@ -1,46 +0,0 @@ -import { openMenu } from "../utils.js"; -import options from "options"; - -const { show_total } = options.bar.notifications; - -const notifs = await Service.import("notifications"); - -export const Notifications = () => { - return { - component: Widget.Box({ - hpack: "start", - child: Widget.Box({ - hpack: "start", - class_name: "bar-notifications", - children: Utils.merge( - [notifs.bind("notifications"), notifs.bind("dnd"), show_total.bind("value")], - (notif, dnd, showTotal) => { - const notifIcon = Widget.Label({ - hpack: "center", - class_name: "bar-notifications-label", - label: dnd ? "󰂛" : notif.length > 0 ? "󱅫" : "󰂚", - }); - - const notifLabel = Widget.Label({ - hpack: "center", - class_name: "bar-notifications-total", - label: notif.length.toString(), - }); - - if (showTotal) { - return [notifIcon, notifLabel]; - } - return [notifIcon]; - }, - ), - }), - }), - isVisible: true, - boxClass: "notifications", - props: { - on_primary_click: (clicked, event) => { - openMenu(clicked, event, "notificationsmenu"); - }, - }, - }; -}; diff --git a/modules/bar/notifications/index.ts b/modules/bar/notifications/index.ts new file mode 100644 index 0000000..ec5e1cd --- /dev/null +++ b/modules/bar/notifications/index.ts @@ -0,0 +1,47 @@ +import Gdk from 'gi://Gdk?version=3.0'; +import { openMenu } from "../utils.js"; +import options from "options"; + +const { show_total } = options.bar.notifications; + +const notifs = await Service.import("notifications"); + +export const Notifications = () => { + return { + component: Widget.Box({ + hpack: "start", + child: Widget.Box({ + hpack: "start", + class_name: "bar-notifications", + children: Utils.merge( + [notifs.bind("notifications"), notifs.bind("dnd"), show_total.bind("value")], + (notif, dnd, showTotal) => { + const notifIcon = Widget.Label({ + hpack: "center", + class_name: "bar-notifications-label", + label: dnd ? "󰂛" : notif.length > 0 ? "󱅫" : "󰂚", + }); + + const notifLabel = Widget.Label({ + hpack: "center", + class_name: "bar-notifications-total", + label: notif.length.toString(), + }); + + if (showTotal) { + return [notifIcon, notifLabel]; + } + return [notifIcon]; + }, + ), + }), + }), + isVisible: true, + boxClass: "notifications", + props: { + on_primary_click: (clicked: any, event: Gdk.Event) => { + openMenu(clicked, event, "notificationsmenu"); + }, + }, + }; +}; diff --git a/modules/bar/systray/index.js b/modules/bar/systray/index.js deleted file mode 100644 index eb9dc3c..0000000 --- a/modules/bar/systray/index.js +++ /dev/null @@ -1,47 +0,0 @@ -const systemtray = await Service.import("systemtray"); -import { bash } from "lib/utils"; -import options from "options"; - -const { ignore } = options.bar.systray; - -const SysTray = () => { - const isVis = Variable(false); - - const items = Utils.merge( - [systemtray.bind("items"), ignore.bind("value")], - (items, ignored) => { - const filteredTray = items.filter(({ id }) => !ignored.includes(id)); - - isVis.value = filteredTray.length > 0; - - return filteredTray.map((item) => { - if (item.menu !== undefined) { - item.menu["class_name"] = "systray-menu"; - } - - return Widget.Button({ - cursor: "pointer", - child: Widget.Icon({ - class_name: "systray-icon", - icon: item.bind("icon"), - }), - on_primary_click: (_, event) => item.activate(event), - on_secondary_click: (_, event) => item.openMenu(event), - tooltip_markup: item.bind("tooltip_markup"), - }); - }); - }, - ); - - return { - component: Widget.Box({ - class_name: "systray", - children: items, - }), - isVisible: true, - boxClass: "systray", - isVis, - }; -}; - -export { SysTray }; diff --git a/modules/bar/systray/index.ts b/modules/bar/systray/index.ts new file mode 100644 index 0000000..7b44612 --- /dev/null +++ b/modules/bar/systray/index.ts @@ -0,0 +1,47 @@ +import Gdk from 'gi://Gdk?version=3.0'; +const systemtray = await Service.import("systemtray"); +import options from "options"; + +const { ignore } = options.bar.systray; + +const SysTray = () => { + const isVis = Variable(false); + + const items = Utils.merge( + [systemtray.bind("items"), ignore.bind("value")], + (items, ignored) => { + const filteredTray = items.filter(({ id }) => !ignored.includes(id)); + + isVis.value = filteredTray.length > 0; + + return filteredTray.map((item) => { + if (item.menu !== undefined) { + item.menu["class_name"] = "systray-menu"; + } + + return Widget.Button({ + cursor: "pointer", + child: Widget.Icon({ + class_name: "systray-icon", + icon: item.bind("icon"), + }), + on_primary_click: (_: any, event: Gdk.Event) => item.activate(event), + on_secondary_click: (_, event) => item.openMenu(event), + tooltip_markup: item.bind("tooltip_markup"), + }); + }); + }, + ); + + return { + component: Widget.Box({ + class_name: "systray", + children: items, + }), + isVisible: true, + boxClass: "systray", + isVis, + }; +}; + +export { SysTray }; diff --git a/modules/bar/utils.js b/modules/bar/utils.js deleted file mode 100644 index 6adfdfe..0000000 --- a/modules/bar/utils.js +++ /dev/null @@ -1,47 +0,0 @@ -export const closeAllMenus = () => { - const menuWindows = App.windows - .filter((w) => { - if (w.name) { - return /.*menu/.test(w.name); - } - - return false; - }) - .map((w) => w.name); - - menuWindows.forEach((w) => { - if (w) { - App.closeWindow(w); - } - }); -}; - -export const openMenu = (clicked, event, window) => { - /* - * NOTE: We have to make some adjustments so the menu pops up relatively - * to the center of the button clicked. We don't want the menu to spawn - * offcenter dependending on which edge of the button you click on. - * ------------- - * To fix this, we take the x coordinate of the click within the button's bounds. - * If you click the left edge of a 100 width button, then the x axis will be 0 - * and if you click the right edge then the x axis will be 100. - * ------------- - * Then we divide the width of the button by 2 to get the center of the button and then get - * the offset by subtracting the clicked x coordinate. Then we can apply that offset - * to the x coordinate of the click relative to the screen to get the center of the - * icon click. - */ - - const middleOfButton = Math.floor(clicked.get_allocated_width() / 2); - const xAxisOfButtonClick = clicked.get_pointer()[0]; - const middleOffset = middleOfButton - xAxisOfButtonClick; - - const clickPos = event.get_root_coords(); - const adjustedXCoord = clickPos[1] + middleOffset; - const coords = [adjustedXCoord, clickPos[2]]; - - globalMousePos.value = coords; - - closeAllMenus(); - App.toggleWindow(window); -}; diff --git a/modules/bar/utils.ts b/modules/bar/utils.ts new file mode 100644 index 0000000..d1db2be --- /dev/null +++ b/modules/bar/utils.ts @@ -0,0 +1,51 @@ +import Gdk from 'gi://Gdk?version=3.0'; + +import { globalMousePos } from 'globals'; + +export const closeAllMenus = () => { + const menuWindows = App.windows + .filter((w) => { + if (w.name) { + return /.*menu/.test(w.name); + } + + return false; + }) + .map((w) => w.name); + + menuWindows.forEach((w) => { + if (w) { + App.closeWindow(w); + } + }); +}; + +export const openMenu = (clicked: any, event: Gdk.Event, window: string) => { + /* + * NOTE: We have to make some adjustments so the menu pops up relatively + * to the center of the button clicked. We don't want the menu to spawn + * offcenter dependending on which edge of the button you click on. + * ------------- + * To fix this, we take the x coordinate of the click within the button's bounds. + * If you click the left edge of a 100 width button, then the x axis will be 0 + * and if you click the right edge then the x axis will be 100. + * ------------- + * Then we divide the width of the button by 2 to get the center of the button and then get + * the offset by subtracting the clicked x coordinate. Then we can apply that offset + * to the x coordinate of the click relative to the screen to get the center of the + * icon click. + */ + + const middleOfButton = Math.floor(clicked.get_allocated_width() / 2); + const xAxisOfButtonClick = clicked.get_pointer()[0]; + const middleOffset = middleOfButton - xAxisOfButtonClick; + + const clickPos = event.get_root_coords(); + const adjustedXCoord = clickPos[1] + middleOffset; + const coords = [adjustedXCoord, clickPos[2]]; + + globalMousePos.value = coords; + + closeAllMenus(); + App.toggleWindow(window); +}; diff --git a/modules/bar/volume/index.js b/modules/bar/volume/index.js deleted file mode 100644 index 4985424..0000000 --- a/modules/bar/volume/index.js +++ /dev/null @@ -1,62 +0,0 @@ -const audio = await Service.import("audio"); -import { openMenu } from "../utils.js"; -import options from "options"; - -import { globalMousePos } from "globals.js"; - -const Volume = () => { - const icons = { - 101: "󰕾", - 66: "󰕾", - 34: "󰖀", - 1: "󰕿", - 0: "󰝟", - }; - - const getIcon = () => { - const icon = Utils.merge( - [audio.speaker.bind("is_muted"), audio.speaker.bind("volume")], - (isMuted, vol) => { - return isMuted - ? 0 - : [101, 66, 34, 1, 0].find((threshold) => threshold <= vol * 100); - }, - ); - - return icon.as((i) => icons[i]); - }; - - const volIcn = Widget.Label({ - vpack: "center", - label: getIcon(), - class_name: "bar-volume_icon", - }); - - const volPct = Widget.Label({ - vpack: "center", - label: audio.speaker.bind("volume").as((v) => ` ${Math.floor(v * 100)}%`), - class_name: "bar-volume_percentage", - }); - - return { - component: Widget.Box({ - vpack: "center", - class_name: "volume", - children: options.bar.volume.label.bind("value").as((showLabel) => { - if (showLabel) { - return [volIcn, volPct]; - } - return [volIcn]; - }), - }), - isVisible: true, - boxClass: "volume", - props: { - on_primary_click: (clicked, event) => { - openMenu(clicked, event, "audiomenu"); - }, - }, - }; -}; - -export { Volume }; diff --git a/modules/bar/volume/index.ts b/modules/bar/volume/index.ts new file mode 100644 index 0000000..b5e065c --- /dev/null +++ b/modules/bar/volume/index.ts @@ -0,0 +1,63 @@ +import Gdk from 'gi://Gdk?version=3.0'; +const audio = await Service.import("audio"); +import { openMenu } from "../utils.js"; +import options from "options"; + +import { globalMousePos } from "globals.js"; + +const Volume = () => { + const icons = { + 101: "󰕾", + 66: "󰕾", + 34: "󰖀", + 1: "󰕿", + 0: "󰝟", + }; + + const getIcon = () => { + const icon = Utils.merge( + [audio.speaker.bind("is_muted"), audio.speaker.bind("volume")], + (isMuted, vol) => { + return isMuted + ? 0 + : [101, 66, 34, 1, 0].find((threshold) => threshold <= vol * 100); + }, + ); + + return icon.as((i) => i !== undefined ? icons[i] : 101); + }; + + const volIcn = Widget.Label({ + vpack: "center", + label: getIcon(), + class_name: "bar-volume_icon", + }); + + const volPct = Widget.Label({ + vpack: "center", + label: audio.speaker.bind("volume").as((v) => ` ${Math.floor(v * 100)}%`), + class_name: "bar-volume_percentage", + }); + + return { + component: Widget.Box({ + vpack: "center", + class_name: "volume", + children: options.bar.volume.label.bind("value").as((showLabel) => { + if (showLabel) { + return [volIcn, volPct]; + } + return [volIcn]; + }), + }), + isVisible: true, + boxClass: "volume", + props: { + on_primary_click: (clicked: any, event: Gdk.Event) => { + openMenu(clicked, event, "audiomenu"); + }, + }, + }; +}; + +export { Volume }; diff --git a/modules/bar/window_title/index.js b/modules/bar/window_title/index.js deleted file mode 100644 index ba96487..0000000 --- a/modules/bar/window_title/index.js +++ /dev/null @@ -1,36 +0,0 @@ -const hyprland = await Service.import("hyprland"); - -const filterTitle = (windowtitle) => { - const windowTitleMap = [ - ["kitty", "󰄛 Kitty Terminal"], - ["firefox", "󰈹 Firefox"], - ["microsoft-edge", "󰇩 Edge"], - ["discord", " Discord"], - ["org.kde.dolphin", " Dolphin"], - ["plex", "󰚺 Plex"], - ["steam", " Steam"], - ["spotify", "󰓇 Spotify"], - ["obsidian", "󱓧 Obsidian"], - ["^$", "󰇄 Desktop"], - ["(.+)", `󰣆 ${windowtitle.class.charAt(0).toUpperCase() + windowtitle.class.slice(1)}`], - ]; - - const foundMatch = windowTitleMap.find((wt) => - RegExp(wt[0]).test(windowtitle.class.toLowerCase()), - ); - - return foundMatch ? foundMatch[1] : windowtitle.class; -}; - -const ClientTitle = () => { - return { - component: Widget.Label({ - class_name: "window_title", - label: hyprland.active.bind("client").as((v) => filterTitle(v)), - }), - isVisible: true, - boxClass: "windowtitle", - }; -}; - -export { ClientTitle }; diff --git a/modules/bar/window_title/index.ts b/modules/bar/window_title/index.ts new file mode 100644 index 0000000..c8b6953 --- /dev/null +++ b/modules/bar/window_title/index.ts @@ -0,0 +1,37 @@ +const hyprland = await Service.import("hyprland"); +import { ActiveClient } from 'types/service/hyprland' + +const filterTitle = (windowtitle: ActiveClient) => { + const windowTitleMap = [ + ["kitty", "󰄛 Kitty Terminal"], + ["firefox", "󰈹 Firefox"], + ["microsoft-edge", "󰇩 Edge"], + ["discord", " Discord"], + ["org.kde.dolphin", " Dolphin"], + ["plex", "󰚺 Plex"], + ["steam", " Steam"], + ["spotify", "󰓇 Spotify"], + ["obsidian", "󱓧 Obsidian"], + ["^$", "󰇄 Desktop"], + ["(.+)", `󰣆 ${windowtitle.class.charAt(0).toUpperCase() + windowtitle.class.slice(1)}`], + ]; + + const foundMatch = windowTitleMap.find((wt) => + RegExp(wt[0]).test(windowtitle.class.toLowerCase()), + ); + + return foundMatch ? foundMatch[1] : windowtitle.class; +}; + +const ClientTitle = () => { + return { + component: Widget.Label({ + class_name: "window_title", + label: hyprland.active.bind("client").as((v) => filterTitle(v)), + }), + isVisible: true, + boxClass: "windowtitle", + }; +}; + +export { ClientTitle }; diff --git a/modules/bar/workspaces/index.js b/modules/bar/workspaces/index.ts similarity index 94% rename from modules/bar/workspaces/index.js rename to modules/bar/workspaces/index.ts index ab4458c..d817803 100644 --- a/modules/bar/workspaces/index.js +++ b/modules/bar/workspaces/index.ts @@ -1,14 +1,15 @@ const hyprland = await Service.import("hyprland"); +import { WorkspaceRule, WorkspaceMap } from "lib/types/workspace"; import options from "options"; const { workspaces, monitorSpecific } = options.bar.workspaces; -function range(length, start = 1) { +function range(length: number, start = 1) { return Array.from({ length }, (_, i) => i + start); } const Workspaces = (monitor = -1, ws = 8) => { - const getWorkspacesForMonitor = (curWs, wsRules) => { + const getWorkspacesForMonitor = (curWs: number, wsRules: WorkspaceMap) => { if (!wsRules || !Object.keys(wsRules).length) { return true; } @@ -20,13 +21,13 @@ const Workspaces = (monitor = -1, ws = 8) => { return wsRules[currentMonitorName].includes(curWs); }; - const getWorkspaceRules = () => { + const getWorkspaceRules = (): WorkspaceMap => { try { const rules = Utils.exec("hyprctl workspacerules -j"); const workspaceRules = {}; - JSON.parse(rules).forEach((rule, index) => { + JSON.parse(rules).forEach((rule: WorkspaceRule, index: number) => { if (Object.hasOwnProperty.call(workspaceRules, rule.monitor)) { workspaceRules[rule.monitor].push(index + 1); } else { @@ -37,6 +38,7 @@ const Workspaces = (monitor = -1, ws = 8) => { return workspaceRules; } catch (err) { console.error(err); + return {}; } }; diff --git a/modules/icons/index.js b/modules/icons/index.ts similarity index 99% rename from modules/icons/index.js rename to modules/icons/index.ts index f22fd66..a573875 100644 --- a/modules/icons/index.js +++ b/modules/icons/index.ts @@ -143,6 +143,7 @@ export default { light: "light-mode-symbolic", }, weather: { + warning: "dialog-warning-symbolic", sunny: "weather-clear-symbolic", clear: "weather-clear-night-symbolic", partly_cloudy: "weather-few-clouds-symbolic", diff --git a/modules/menus/DropdownMenu.js b/modules/menus/DropdownMenu.js index 93d3911..c8b5364 100644 --- a/modules/menus/DropdownMenu.js +++ b/modules/menus/DropdownMenu.js @@ -1,64 +1,65 @@ const hyprland = await Service.import("hyprland"); +import { globalMousePos } from "globals"; export const Padding = (name) => - Widget.EventBox({ - hexpand: true, - vexpand: true, - can_focus: true, - child: Widget.Box(), - setup: (w) => w.on("button-press-event", () => App.toggleWindow(name)), - }); + Widget.EventBox({ + hexpand: true, + vexpand: true, + can_focus: true, + child: Widget.Box(), + setup: (w) => w.on("button-press-event", () => App.toggleWindow(name)), + }); const moveBoxToCursor = (self, fixed) => { - if (fixed) { - return; - } - - globalMousePos.connect("changed", ({ value }) => { - const currentWidth = self.child.get_allocation().width; - - let monWidth = hyprland.monitors[hyprland.active.monitor.id].width; - let monHeight = hyprland.monitors[hyprland.active.monitor.id].height; - - // If GDK Scaling is applied, then get divide width by scaling - // to get the proper coordinates. - // Ex: On a 2860px wide monitor... if scaling is set to 2, then the right - // end of the monitor is the 1430th pixel. - const gdkScale = Utils.exec('bash -c "echo $GDK_SCALE"'); - - if (/^\d+(.\d+)?$/.test(gdkScale)) { - const scale = parseFloat(gdkScale); - monWidth = monWidth / scale; - monHeight = monHeight / scale; + if (fixed) { + return; } - // If monitor is vertical (transform = 1 || 3) swap height and width - if (hyprland.monitors[hyprland.active.monitor.id].transform % 2 !== 0) { - [monWidth, monHeight] = [monHeight, monWidth]; - } + globalMousePos.connect("changed", ({ value }) => { + const currentWidth = self.child.get_allocation().width; - let marginRight = monWidth - currentWidth / 2; - marginRight = fixed ? marginRight - monWidth / 2 : marginRight - value[0]; - let marginLeft = monWidth - currentWidth - marginRight; + let monWidth = hyprland.monitors[hyprland.active.monitor.id].width; + let monHeight = hyprland.monitors[hyprland.active.monitor.id].height; - const minimumMargin = 0; + // If GDK Scaling is applied, then get divide width by scaling + // to get the proper coordinates. + // Ex: On a 2860px wide monitor... if scaling is set to 2, then the right + // end of the monitor is the 1430th pixel. + const gdkScale = Utils.exec('bash -c "echo $GDK_SCALE"'); - if (marginRight < minimumMargin) { - marginRight = minimumMargin; - marginLeft = monWidth - currentWidth - minimumMargin; - } + if (/^\d+(.\d+)?$/.test(gdkScale)) { + const scale = parseFloat(gdkScale); + monWidth = monWidth / scale; + monHeight = monHeight / scale; + } - if (marginLeft < minimumMargin) { - marginLeft = minimumMargin; - marginRight = monWidth - currentWidth - minimumMargin; - } + // If monitor is vertical (transform = 1 || 3) swap height and width + if (hyprland.monitors[hyprland.active.monitor.id].transform % 2 !== 0) { + [monWidth, monHeight] = [monHeight, monWidth]; + } - const marginTop = 45; - const marginBottom = monHeight - marginTop; - self.set_margin_left(marginLeft); - self.set_margin_right(marginRight); - self.set_margin_bottom(marginBottom); - }); + let marginRight = monWidth - currentWidth / 2; + marginRight = fixed ? marginRight - monWidth / 2 : marginRight - value[0]; + let marginLeft = monWidth - currentWidth - marginRight; + + const minimumMargin = 0; + + if (marginRight < minimumMargin) { + marginRight = minimumMargin; + marginLeft = monWidth - currentWidth - minimumMargin; + } + + if (marginLeft < minimumMargin) { + marginLeft = minimumMargin; + marginRight = monWidth - currentWidth - minimumMargin; + } + + const marginTop = 45; + const marginBottom = monHeight - marginTop; + self.set_margin_left(marginLeft); + self.set_margin_right(marginRight); + self.set_margin_bottom(marginBottom); + }); }; // NOTE: We make the window visible for 2 seconds (on startup) so the child @@ -68,78 +69,78 @@ const moveBoxToCursor = (self, fixed) => { const initRender = Variable(true); setTimeout(() => { - initRender.value = false; + initRender.value = false; }, 2000); export default ({ - name, - child, - layout = "center", - transition, - exclusivity = "ignore", - fixed = false, - ...props -}) => - Widget.Window({ name, - class_names: [name, "dropdown-menu"], - setup: (w) => w.keybind("Escape", () => App.closeWindow(name)), - visible: initRender.bind("value"), - keymode: "on-demand", - exclusivity, - layer: "top", - anchor: ["top", "left"], - child: Widget.EventBox({ - class_name: "parent-event", - on_primary_click: () => App.closeWindow(name), - on_secondary_click: () => App.closeWindow(name), - child: Widget.Box({ - class_name: "top-eb", - vertical: true, - children: [ - Widget.EventBox({ - class_name: "mid-eb event-top-padding", - hexpand: true, - vexpand: false, - can_focus: false, - child: Widget.Box(), - setup: (w) => { - w.on("button-press-event", () => App.toggleWindow(name)); - w.set_margin_top(1); - }, - }), - Widget.EventBox({ - class_name: "in-eb menu-event-box", - on_primary_click: () => { - return true; - }, - on_secondary_click: () => { - return true; - }, - setup: (self) => { - moveBoxToCursor(self, fixed); - }, + child, + layout = "center", + transition, + exclusivity = "ignore", + fixed = false, + ...props +}) => + Widget.Window({ + name, + class_names: [name, "dropdown-menu"], + setup: (w) => w.keybind("Escape", () => App.closeWindow(name)), + visible: initRender.bind("value"), + keymode: "on-demand", + exclusivity, + layer: "top", + anchor: ["top", "left"], + child: Widget.EventBox({ + class_name: "parent-event", + on_primary_click: () => App.closeWindow(name), + on_secondary_click: () => App.closeWindow(name), child: Widget.Box({ - class_name: "dropdown-menu-container", - css: "padding: 1px; margin: -1px;", - child: Widget.Revealer({ - revealChild: false, - setup: (self) => - self.hook(App, (_, wname, visible) => { - if (wname === name) self.reveal_child = visible; - }), - transition: "crossfade", - transitionDuration: 350, - child: Widget.Box({ - class_name: "dropdown-menu-container", - can_focus: true, - children: [child], - }), - }), + class_name: "top-eb", + vertical: true, + children: [ + Widget.EventBox({ + class_name: "mid-eb event-top-padding", + hexpand: true, + vexpand: false, + can_focus: false, + child: Widget.Box(), + setup: (w) => { + w.on("button-press-event", () => App.toggleWindow(name)); + w.set_margin_top(1); + }, + }), + Widget.EventBox({ + class_name: "in-eb menu-event-box", + on_primary_click: () => { + return true; + }, + on_secondary_click: () => { + return true; + }, + setup: (self) => { + moveBoxToCursor(self, fixed); + }, + child: Widget.Box({ + class_name: "dropdown-menu-container", + css: "padding: 1px; margin: -1px;", + child: Widget.Revealer({ + revealChild: false, + setup: (self) => + self.hook(App, (_, wname, visible) => { + if (wname === name) self.reveal_child = visible; + }), + transition: "crossfade", + transitionDuration: 350, + child: Widget.Box({ + class_name: "dropdown-menu-container", + can_focus: true, + children: [child], + }), + }), + }), + }), + ], }), - }), - ], - }), - }), - ...props, - }); + }), + ...props, + }); diff --git a/modules/menus/audio/active/SelectedInput.js b/modules/menus/audio/active/SelectedInput.ts similarity index 100% rename from modules/menus/audio/active/SelectedInput.js rename to modules/menus/audio/active/SelectedInput.ts diff --git a/modules/menus/audio/active/SelectedPlayback.js b/modules/menus/audio/active/SelectedPlayback.ts similarity index 100% rename from modules/menus/audio/active/SelectedPlayback.js rename to modules/menus/audio/active/SelectedPlayback.ts diff --git a/modules/menus/audio/active/index.js b/modules/menus/audio/active/index.ts similarity index 100% rename from modules/menus/audio/active/index.js rename to modules/menus/audio/active/index.ts diff --git a/modules/menus/audio/available/InputDevices.js b/modules/menus/audio/available/InputDevices.js deleted file mode 100644 index 99c3856..0000000 --- a/modules/menus/audio/available/InputDevices.js +++ /dev/null @@ -1,58 +0,0 @@ -const audio = await Service.import("audio"); - -const renderInputDevices = (inputDevices) => { - if (!inputDevices.length) { - return [ - Widget.Box({ - class_name: `menu-unfound-button input`, - child: Widget.Box({ - children: [ - Widget.Label({ - class_name: "menu-button-name input", - label: "No input devices found...", - }), - ], - }), - }), - ]; - } - return inputDevices.map((device) => { - return Widget.Button({ - on_primary_click: () => (audio.microphone = device), - class_name: `menu-button audio input ${device}`, - child: Widget.Box({ - children: [ - Widget.Box({ - hpack: "start", - children: [ - Widget.Label({ - class_name: audio.microphone - .bind("description") - .as((v) => - device.description === v - ? "menu-button-icon active input" - : "menu-button-icon input", - ), - label: "", - }), - Widget.Label({ - truncate: "end", - wrap: true, - class_name: audio.microphone - .bind("description") - .as((v) => - device.description === v - ? "menu-button-name active input" - : "menu-button-name input", - ), - label: device.description, - }), - ], - }), - ], - }), - }); - }); -}; - -export { renderInputDevices }; diff --git a/modules/menus/audio/available/InputDevices.ts b/modules/menus/audio/available/InputDevices.ts new file mode 100644 index 0000000..aac0685 --- /dev/null +++ b/modules/menus/audio/available/InputDevices.ts @@ -0,0 +1,65 @@ +const audio = await Service.import("audio"); +import { Stream } from "types/service/audio"; + +const renderInputDevices = (inputDevices: Stream[]) => { + if (inputDevices.length === 0) { + return [ + Widget.Button({ + class_name: `menu-unfound-button input`, + child: Widget.Box({ + children: [ + Widget.Box({ + hpack: "start", + children: [ + Widget.Label({ + class_name: "menu-button-name input", + label: "No input devices found...", + }), + ], + }), + ], + }), + }), + ]; + } + return inputDevices.map((device) => { + return Widget.Button({ + on_primary_click: () => (audio.microphone = device), + class_name: `menu-button audio input ${device}`, + child: Widget.Box({ + children: [ + Widget.Box({ + hpack: "start", + children: [ + Widget.Label({ + wrap: true, + class_name: audio.microphone + .bind("description") + .as((v) => + device.description === v + ? "menu-button-icon active input" + : "menu-button-icon input", + ), + label: "", + }), + Widget.Label({ + truncate: "end", + wrap: true, + class_name: audio.microphone + .bind("description") + .as((v) => + device.description === v + ? "menu-button-name active input" + : "menu-button-name input", + ), + label: device.description, + }), + ], + }), + ], + }), + }); + }); +}; + +export { renderInputDevices }; diff --git a/modules/menus/audio/available/PlaybackDevices.js b/modules/menus/audio/available/PlaybackDevices.js deleted file mode 100644 index efe232e..0000000 --- a/modules/menus/audio/available/PlaybackDevices.js +++ /dev/null @@ -1,58 +0,0 @@ -const audio = await Service.import("audio"); - -const renderPlaybacks = (playbackDevices) => { - return playbackDevices.map((device) => { - if (device.description === "Dummy Output") { - return Widget.Box({ - class_name: "menu-unfound-button playback", - child: Widget.Box({ - children: [ - Widget.Label({ - class_name: "menu-button-name playback", - label: "No playback devices found...", - }), - ], - }), - }); - } - return Widget.Button({ - class_name: `menu-button audio playback ${device}`, - on_primary_click: () => (audio.speaker = device), - child: Widget.Box({ - children: [ - Widget.Box({ - hpack: "start", - children: [ - Widget.Label({ - truncate: "end", - wrap: true, - class_name: audio.speaker - .bind("description") - .as((v) => - device.description === v - ? "menu-button-icon active playback" - : "menu-button-icon playback", - ), - label: "", - }), - Widget.Label({ - class_name: audio.speaker - .bind("description") - .as((v) => - device.description === v - ? "menu-button-name active playback" - : "menu-button-name playback", - ), - truncate: "end", - wrap: true, - label: device.description, - }), - ], - }), - ], - }), - }); - }); -}; - -export { renderPlaybacks }; diff --git a/modules/menus/audio/available/PlaybackDevices.ts b/modules/menus/audio/available/PlaybackDevices.ts new file mode 100644 index 0000000..07c3fad --- /dev/null +++ b/modules/menus/audio/available/PlaybackDevices.ts @@ -0,0 +1,59 @@ +const audio = await Service.import("audio"); +import { Stream } from "types/service/audio"; + +const renderPlaybacks = (playbackDevices: Stream[]) => { + return playbackDevices.map((device) => { + if (device.description === "Dummy Output") { + return Widget.Box({ + class_name: "menu-unfound-button playback", + child: Widget.Box({ + children: [ + Widget.Label({ + class_name: "menu-button-name playback", + label: "No playback devices found...", + }), + ], + }), + }); + } + return Widget.Button({ + class_name: `menu-button audio playback ${device}`, + on_primary_click: () => (audio.speaker = device), + child: Widget.Box({ + children: [ + Widget.Box({ + hpack: "start", + children: [ + Widget.Label({ + truncate: "end", + wrap: true, + class_name: audio.speaker + .bind("description") + .as((v) => + device.description === v + ? "menu-button-icon active playback" + : "menu-button-icon playback", + ), + label: "", + }), + Widget.Label({ + truncate: "end", + wrap: true, + class_name: audio.speaker + .bind("description") + .as((v) => + device.description === v + ? "menu-button-name active playback" + : "menu-button-name playback", + ), + label: device.description, + }), + ], + }), + ], + }), + }); + }); +}; + +export { renderPlaybacks }; diff --git a/modules/menus/audio/available/index.js b/modules/menus/audio/available/index.js deleted file mode 100644 index ce486b1..0000000 --- a/modules/menus/audio/available/index.js +++ /dev/null @@ -1,75 +0,0 @@ -const audio = await Service.import("audio"); -import { renderInputDevices } from "./InputDevices.js"; -import { renderPlaybacks } from "./PlaybackDevices.js"; - -const availableDevices = () => { - return Widget.Box({ - vertical: true, - children: [ - Widget.Box({ - class_name: "menu-section-container playback", - vertical: true, - children: [ - Widget.Box({ - class_name: "menu-label-container playback", - hpack: "fill", - child: Widget.Label({ - class_name: "menu-label audio playback", - hexpand: true, - hpack: "start", - label: "Playback Devices", - }), - }), - Widget.Box({ - class_name: "menu-items-section playback", - vertical: true, - children: [ - Widget.Box({ - class_name: "menu-container playback", - vertical: true, - children: [ - Widget.Box({ - vertical: true, - children: audio - .bind("speakers") - .as((v) => renderPlaybacks(v)), - }), - ], - }), - ], - }), - Widget.Box({ - class_name: "menu-label-container input", - hpack: "fill", - child: Widget.Label({ - class_name: "menu-label audio input", - hexpand: true, - hpack: "start", - label: "Input Devices", - }), - }), - Widget.Box({ - class_name: "menu-items-section input", - vertical: true, - children: [ - Widget.Box({ - class_name: "menu-container input", - vertical: true, - children: [ - Widget.Box({ - vertical: true, - children: audio - .bind("microphones") - .as((v) => renderInputDevices(v)), - }), - ], - }), - ], - }), - ], - }), - ], - }); -}; - -export { availableDevices }; diff --git a/modules/menus/audio/available/index.ts b/modules/menus/audio/available/index.ts new file mode 100644 index 0000000..1dca28d --- /dev/null +++ b/modules/menus/audio/available/index.ts @@ -0,0 +1,75 @@ +const audio = await Service.import("audio"); +import { renderInputDevices } from "./InputDevices.js"; +import { renderPlaybacks } from "./PlaybackDevices.js"; + +const availableDevices = () => { + return Widget.Box({ + vertical: true, + children: [ + Widget.Box({ + class_name: "menu-section-container playback", + vertical: true, + children: [ + Widget.Box({ + class_name: "menu-label-container playback", + hpack: "fill", + child: Widget.Label({ + class_name: "menu-label audio playback", + hexpand: true, + hpack: "start", + label: "Playback Devices", + }), + }), + Widget.Box({ + class_name: "menu-items-section playback", + vertical: true, + children: [ + Widget.Box({ + class_name: "menu-container playback", + vertical: true, + children: [ + Widget.Box({ + vertical: true, + children: audio + .bind("speakers") + .as((v) => renderPlaybacks(v)), + }), + ], + }), + ], + }), + Widget.Box({ + class_name: "menu-label-container input", + hpack: "fill", + child: Widget.Label({ + class_name: "menu-label audio input", + hexpand: true, + hpack: "start", + label: "Input Devices", + }), + }), + Widget.Box({ + class_name: "menu-items-section input", + vertical: true, + children: [ + Widget.Box({ + class_name: "menu-container input", + vertical: true, + children: [ + Widget.Box({ + vertical: true, + children: audio + .bind("microphones") + .as((v) => renderInputDevices(v)), + }), + ], + }), + ], + }), + ], + }), + ], + }); +}; + +export { availableDevices }; diff --git a/modules/menus/audio/index.js b/modules/menus/audio/index.ts similarity index 100% rename from modules/menus/audio/index.js rename to modules/menus/audio/index.ts diff --git a/modules/menus/audio/utils.js b/modules/menus/audio/utils.ts similarity index 100% rename from modules/menus/audio/utils.js rename to modules/menus/audio/utils.ts diff --git a/modules/menus/bluetooth/devices/connectedControls.js b/modules/menus/bluetooth/devices/connectedControls.js deleted file mode 100644 index f32fd0b..0000000 --- a/modules/menus/bluetooth/devices/connectedControls.js +++ /dev/null @@ -1,76 +0,0 @@ -const connectedControls = (dev, connectedDevices) => { - if (!connectedDevices.includes(dev.address)) { - return Widget.Box({}); - } - - return Widget.Box({ - vpack: "start", - class_name: "bluetooth-controls", - children: [ - Widget.Button({ - class_name: "menu-icon-button unpair bluetooth", - child: Widget.Label({ - tooltip_text: dev.paired ? "Unpair" : "Pair", - class_name: "menu-icon-button-label unpair bluetooth", - label: dev.paired ? "" : "", - }), - on_primary_click: () => - Utils.execAsync([ - "bash", - "-c", - `bluetoothctl ${dev.paired ? "unpair" : "pair"} ${dev.address}`, - ]).catch((err) => - console.error( - `bluetoothctl ${dev.paired ? "unpair" : "pair"} ${dev.address}`, - err, - ), - ), - }), - Widget.Button({ - class_name: "menu-icon-button disconnect bluetooth", - child: Widget.Label({ - tooltip_text: dev.connected ? "Disconnect" : "Connect", - class_name: "menu-icon-button-label disconnect bluetooth", - label: dev.connected ? "󱘖" : "", - }), - on_primary_click: () => dev.setConnection(!dev.connected), - }), - Widget.Button({ - class_name: "menu-icon-button untrust bluetooth", - child: Widget.Label({ - tooltip_text: dev.trusted ? "Untrust" : "Trust", - class_name: "menu-icon-button-label untrust bluetooth", - label: dev.trusted ? "" : "󱖡", - }), - on_primary_click: () => - Utils.execAsync([ - "bash", - "-c", - `bluetoothctl ${dev.trusted ? "untrust" : "trust"} ${dev.address}`, - ]).catch((err) => - console.error( - `bluetoothctl ${dev.trusted ? "untrust" : "trust"} ${dev.address}`, - err, - ), - ), - }), - Widget.Button({ - class_name: "menu-icon-button delete bluetooth", - child: Widget.Label({ - tooltip_text: "Forget", - class_name: "menu-icon-button-label delete bluetooth", - label: "󰆴", - }), - on_primary_click: () => { - Utils.execAsync([ - "bash", - "-c", - `bluetoothctl remove ${dev.address}`, - ]).catch((err) => console.error("Bluetooth Remove", err)); - }, - }), - ], - }); -}; - -export { connectedControls }; diff --git a/modules/menus/bluetooth/devices/connectedControls.ts b/modules/menus/bluetooth/devices/connectedControls.ts new file mode 100644 index 0000000..c241c0b --- /dev/null +++ b/modules/menus/bluetooth/devices/connectedControls.ts @@ -0,0 +1,78 @@ +import { BluetoothDevice } from "types/service/bluetooth"; + +const connectedControls = (dev: BluetoothDevice, connectedDevices: BluetoothDevice[]) => { + if (!connectedDevices.includes(dev.address)) { + return Widget.Box({}); + } + + return Widget.Box({ + vpack: "start", + class_name: "bluetooth-controls", + children: [ + Widget.Button({ + class_name: "menu-icon-button unpair bluetooth", + child: Widget.Label({ + tooltip_text: dev.paired ? "Unpair" : "Pair", + class_name: "menu-icon-button-label unpair bluetooth", + label: dev.paired ? "" : "", + }), + on_primary_click: () => + Utils.execAsync([ + "bash", + "-c", + `bluetoothctl ${dev.paired ? "unpair" : "pair"} ${dev.address}`, + ]).catch((err) => + console.error( + `bluetoothctl ${dev.paired ? "unpair" : "pair"} ${dev.address}`, + err, + ), + ), + }), + Widget.Button({ + class_name: "menu-icon-button disconnect bluetooth", + child: Widget.Label({ + tooltip_text: dev.connected ? "Disconnect" : "Connect", + class_name: "menu-icon-button-label disconnect bluetooth", + label: dev.connected ? "󱘖" : "", + }), + on_primary_click: () => dev.setConnection(!dev.connected), + }), + Widget.Button({ + class_name: "menu-icon-button untrust bluetooth", + child: Widget.Label({ + tooltip_text: dev.trusted ? "Untrust" : "Trust", + class_name: "menu-icon-button-label untrust bluetooth", + label: dev.trusted ? "" : "󱖡", + }), + on_primary_click: () => + Utils.execAsync([ + "bash", + "-c", + `bluetoothctl ${dev.trusted ? "untrust" : "trust"} ${dev.address}`, + ]).catch((err) => + console.error( + `bluetoothctl ${dev.trusted ? "untrust" : "trust"} ${dev.address}`, + err, + ), + ), + }), + Widget.Button({ + class_name: "menu-icon-button delete bluetooth", + child: Widget.Label({ + tooltip_text: "Forget", + class_name: "menu-icon-button-label delete bluetooth", + label: "󰆴", + }), + on_primary_click: () => { + Utils.execAsync([ + "bash", + "-c", + `bluetoothctl remove ${dev.address}`, + ]).catch((err) => console.error("Bluetooth Remove", err)); + }, + }), + ], + }); +}; + +export { connectedControls }; diff --git a/modules/menus/bluetooth/devices/devicelist.js b/modules/menus/bluetooth/devices/devicelist.js deleted file mode 100644 index 7511bd7..0000000 --- a/modules/menus/bluetooth/devices/devicelist.js +++ /dev/null @@ -1,137 +0,0 @@ -import { connectedControls } from "./connectedControls.js"; -import { getBluetoothIcon } from "../utils.js"; - -const devices = (bluetooth, self) => { - return self.hook(bluetooth, () => { - if (!bluetooth.enabled) { - return (self.child = Widget.Box({ - class_name: "bluetooth-items", - vertical: true, - expand: true, - vpack: "center", - hpack: "center", - children: [ - Widget.Label({ - class_name: "dim", - hexpand: true, - label: "Bluetooth is disabled", - }), - ], - })); - } - - const availableDevices = bluetooth.devices - .filter( - (btDev) => btDev.name !== null, - ) - .sort((a, b) => { - if (a.connected || a.paired) { - return -1; - } - - if (b.connected || b.paired) { - return 1; - } - - return b.name - a.name; - }); - - const conDevNames = availableDevices - .filter((d) => d.connected || d.paired) - .map((d) => d.address); - - if (!availableDevices.length) { - return (self.child = Widget.Box({ - class_name: "bluetooth-items", - vertical: true, - expand: true, - vpack: "center", - hpack: "center", - children: [ - Widget.Label({ - class_name: "no-bluetooth-devices dim", - hexpand: true, - label: "No devices currently found", - }), - Widget.Label({ - class_name: "search-bluetooth-label dim", - hexpand: true, - label: "Press '󰑐' to search", - }), - ], - })); - } - - return (self.child = Widget.Box({ - vertical: true, - children: availableDevices.map((device) => { - return Widget.Box({ - children: [ - Widget.Button({ - hexpand: true, - class_name: `bluetooth-element-item ${device}`, - on_primary_click: () => { - if (!conDevNames.includes(device.address)) - device.setConnection(true); - }, - child: Widget.Box({ - hexpand: true, - children: [ - Widget.Box({ - hexpand: true, - hpack: "start", - class_name: "menu-button-container", - children: [ - Widget.Label({ - vpack: "start", - class_name: `menu-button-icon bluetooth ${conDevNames.includes(device.address) ? "active" : ""}`, - label: getBluetoothIcon(`${device["icon-name"]}-symbolic`), - }), - Widget.Box({ - vertical: true, - vpack: "center", - children: [ - Widget.Label({ - vpack: "center", - hpack: "start", - class_name: "menu-button-name bluetooth", - truncate: "end", - wrap: true, - label: device.alias, - }), - Widget.Revealer({ - hpack: "start", - reveal_child: device.connected || device.paired, - child: Widget.Label({ - hpack: "start", - class_name: "connection-status dim", - label: device.connected ? "Connected" : "Paired", - }), - }), - ], - }), - ], - }), - Widget.Box({ - hpack: "end", - children: device.connecting - ? [ - Widget.Spinner({ - vpack: "start", - class_name: "spinner bluetooth", - }), - ] - : [], - }), - ], - }), - }), - connectedControls(device, conDevNames), - ], - }); - }), - })); - }); -}; - -export { devices }; diff --git a/modules/menus/bluetooth/devices/devicelist.ts b/modules/menus/bluetooth/devices/devicelist.ts new file mode 100644 index 0000000..2066857 --- /dev/null +++ b/modules/menus/bluetooth/devices/devicelist.ts @@ -0,0 +1,139 @@ +import { Bluetooth } from "types/service/bluetooth.js"; +import Box from "types/widgets/box.js"; +import { connectedControls } from "./connectedControls.js"; +import { getBluetoothIcon } from "../utils.js"; + +const devices = (bluetooth: Bluetooth, self: Box) => { + return self.hook(bluetooth, () => { + if (!bluetooth.enabled) { + return (self.child = Widget.Box({ + class_name: "bluetooth-items", + vertical: true, + expand: true, + vpack: "center", + hpack: "center", + children: [ + Widget.Label({ + class_name: "dim", + hexpand: true, + label: "Bluetooth is disabled", + }), + ], + })); + } + + const availableDevices = bluetooth.devices + .filter( + (btDev) => btDev.name !== null, + ) + .sort((a, b) => { + if (a.connected || a.paired) { + return -1; + } + + if (b.connected || b.paired) { + return 1; + } + + return b.name - a.name; + }); + + const conDevNames = availableDevices + .filter((d) => d.connected || d.paired) + .map((d) => d.address); + + if (!availableDevices.length) { + return (self.child = Widget.Box({ + class_name: "bluetooth-items", + vertical: true, + expand: true, + vpack: "center", + hpack: "center", + children: [ + Widget.Label({ + class_name: "no-bluetooth-devices dim", + hexpand: true, + label: "No devices currently found", + }), + Widget.Label({ + class_name: "search-bluetooth-label dim", + hexpand: true, + label: "Press '󰑐' to search", + }), + ], + })); + } + + return (self.child = Widget.Box({ + vertical: true, + children: availableDevices.map((device) => { + return Widget.Box({ + children: [ + Widget.Button({ + hexpand: true, + class_name: `bluetooth-element-item ${device}`, + on_primary_click: () => { + if (!conDevNames.includes(device.address)) + device.setConnection(true); + }, + child: Widget.Box({ + hexpand: true, + children: [ + Widget.Box({ + hexpand: true, + hpack: "start", + class_name: "menu-button-container", + children: [ + Widget.Label({ + vpack: "start", + class_name: `menu-button-icon bluetooth ${conDevNames.includes(device.address) ? "active" : ""}`, + label: getBluetoothIcon(`${device["icon-name"]}-symbolic`), + }), + Widget.Box({ + vertical: true, + vpack: "center", + children: [ + Widget.Label({ + vpack: "center", + hpack: "start", + class_name: "menu-button-name bluetooth", + truncate: "end", + wrap: true, + label: device.alias, + }), + Widget.Revealer({ + hpack: "start", + reveal_child: device.connected || device.paired, + child: Widget.Label({ + hpack: "start", + class_name: "connection-status dim", + label: device.connected ? "Connected" : "Paired", + }), + }), + ], + }), + ], + }), + Widget.Box({ + hpack: "end", + children: device.connecting + ? [ + Widget.Spinner({ + vpack: "start", + class_name: "spinner bluetooth", + }), + ] + : [], + }), + ], + }), + }), + connectedControls(device, conDevNames), + ], + }); + }), + })); + }); +}; + +export { devices }; diff --git a/modules/menus/bluetooth/devices/index.js b/modules/menus/bluetooth/devices/index.js deleted file mode 100644 index 750570c..0000000 --- a/modules/menus/bluetooth/devices/index.js +++ /dev/null @@ -1,27 +0,0 @@ -const bluetooth = await Service.import("bluetooth"); -import { label } from "./label.js"; -import { devices } from "./devicelist.js"; - -const Devices = () => { - return Widget.Box({ - class_name: "menu-section-container", - vertical: true, - children: [ - label(bluetooth), - Widget.Box({ - class_name: "menu-items-section", - // hscroll: 'never', - // vscroll: 'always', - child: Widget.Box({ - class_name: "menu-content", - vertical: true, - setup: (self) => { - devices(bluetooth, self); - }, - }), - }), - ], - }); -}; - -export { Devices }; diff --git a/modules/menus/bluetooth/devices/index.ts b/modules/menus/bluetooth/devices/index.ts new file mode 100644 index 0000000..9b7d3a1 --- /dev/null +++ b/modules/menus/bluetooth/devices/index.ts @@ -0,0 +1,25 @@ +const bluetooth = await Service.import("bluetooth"); +import { label } from "./label.js"; +import { devices } from "./devicelist.js"; + +const Devices = () => { + return Widget.Box({ + class_name: "menu-section-container", + vertical: true, + children: [ + label(bluetooth), + Widget.Box({ + class_name: "menu-items-section", + child: Widget.Box({ + class_name: "menu-content", + vertical: true, + setup: (self) => { + devices(bluetooth, self); + }, + }), + }), + ], + }); +}; + +export { Devices }; diff --git a/modules/menus/bluetooth/devices/label.js b/modules/menus/bluetooth/devices/label.js deleted file mode 100644 index 2ffa608..0000000 --- a/modules/menus/bluetooth/devices/label.js +++ /dev/null @@ -1,74 +0,0 @@ -const label = (bluetooth) => { - const searchInProgress = Variable(false); - const startRotation = () => { - searchInProgress.value = true; - setTimeout(() => { - searchInProgress.value = false; - }, 10 * 1000); - }; - - return Widget.Box({ - class_name: "menu-label-container", - hpack: "fill", - vpack: "start", - children: [ - Widget.Label({ - class_name: "menu-label", - vpack: "center", - hpack: "start", - label: "Bluetooth", - }), - Widget.Box({ - class_name: "controls-container", - vpack: "start", - children: [ - Widget.Switch({ - class_name: "menu-switch bluetooth", - hexpand: true, - hpack: "end", - active: bluetooth.bind("enabled"), - on_activate: ({ active }) => { - searchInProgress.value = false; - Utils.execAsync([ - "bash", - "-c", - `bluetoothctl power ${active ? "on" : "off"}`, - ]).catch((err) => - console.error( - `bluetoothctl power ${active ? "on" : "off"}`, - err, - ), - ); - }, - }), - Widget.Separator({ - class_name: "menu-separator bluetooth", - }), - Widget.Button({ - vpack: "center", - class_name: "menu-icon-button search", - on_primary_click: () => { - startRotation(); - Utils.execAsync([ - "bash", - "-c", - "bluetoothctl --timeout 120 scan on", - ]).catch((err) => { - searchInProgress.value = false; - console.error("bluetoothctl --timeout 120 scan on", err); - }); - }, - child: Widget.Icon({ - class_name: searchInProgress - .bind("value") - .as((v) => (v ? "spinning" : "")), - icon: "view-refresh-symbolic", - }), - }), - ], - }), - ], - }); -}; - -export { label }; diff --git a/modules/menus/bluetooth/devices/label.ts b/modules/menus/bluetooth/devices/label.ts new file mode 100644 index 0000000..d5b8555 --- /dev/null +++ b/modules/menus/bluetooth/devices/label.ts @@ -0,0 +1,75 @@ +import { Bluetooth } from "types/service/bluetooth"; +const label = (bluetooth: Bluetooth) => { + const searchInProgress = Variable(false); + const startRotation = () => { + searchInProgress.value = true; + setTimeout(() => { + searchInProgress.value = false; + }, 10 * 1000); + }; + + return Widget.Box({ + class_name: "menu-label-container", + hpack: "fill", + vpack: "start", + children: [ + Widget.Label({ + class_name: "menu-label", + vpack: "center", + hpack: "start", + label: "Bluetooth", + }), + Widget.Box({ + class_name: "controls-container", + vpack: "start", + children: [ + Widget.Switch({ + class_name: "menu-switch bluetooth", + hexpand: true, + hpack: "end", + active: bluetooth.bind("enabled"), + on_activate: ({ active }) => { + searchInProgress.value = false; + Utils.execAsync([ + "bash", + "-c", + `bluetoothctl power ${active ? "on" : "off"}`, + ]).catch((err) => + console.error( + `bluetoothctl power ${active ? "on" : "off"}`, + err, + ), + ); + }, + }), + Widget.Separator({ + class_name: "menu-separator bluetooth", + }), + Widget.Button({ + vpack: "center", + class_name: "menu-icon-button search", + on_primary_click: () => { + startRotation(); + Utils.execAsync([ + "bash", + "-c", + "bluetoothctl --timeout 120 scan on", + ]).catch((err) => { + searchInProgress.value = false; + console.error("bluetoothctl --timeout 120 scan on", err); + }); + }, + child: Widget.Icon({ + class_name: searchInProgress + .bind("value") + .as((v) => (v ? "spinning" : "")), + icon: "view-refresh-symbolic", + }), + }), + ], + }), + ], + }); +}; + +export { label }; diff --git a/modules/menus/bluetooth/index.js b/modules/menus/bluetooth/index.ts similarity index 100% rename from modules/menus/bluetooth/index.js rename to modules/menus/bluetooth/index.ts diff --git a/modules/menus/bluetooth/utils.js b/modules/menus/bluetooth/utils.js deleted file mode 100644 index 6cb3866..0000000 --- a/modules/menus/bluetooth/utils.js +++ /dev/null @@ -1,31 +0,0 @@ -const getBluetoothIcon = (iconName) => { - const deviceIconMap = [ - ["^audio-card*", "󰎄"], - ["^audio-headphones*", "󰋋"], - ["^audio-headset*", "󰋎"], - ["^audio-input*", "󰍬"], - ["^audio-speakers*", "󰓃"], - ["^bluetooth*", "󰂯"], - ["^camera*", "󰄀"], - ["^computer*", "󰟀"], - ["^input-gaming*", "󰍬"], - ["^input-keyboard*", "󰌌"], - ["^input-mouse*", "󰍽"], - ["^input-tablet*", "󰓶"], - ["^media*", "󱛟"], - ["^modem*", "󱂇"], - ["^network*", "󱂇"], - ["^phone*", "󰄞"], - ["^printer*", "󰐪"], - ["^scanner*", "󰚫"], - ["^video-camera*", "󰕧"], - ]; - - const foundMatch = deviceIconMap.find((icon) => - RegExp(icon[0]).test(iconName.toLowerCase()), - ); - - return foundMatch ? foundMatch[1] : "󰂯"; -}; - -export { getBluetoothIcon }; diff --git a/modules/menus/bluetooth/utils.ts b/modules/menus/bluetooth/utils.ts new file mode 100644 index 0000000..2024f8b --- /dev/null +++ b/modules/menus/bluetooth/utils.ts @@ -0,0 +1,31 @@ +const getBluetoothIcon = (iconName: string) => { + const deviceIconMap = [ + ["^audio-card*", "󰎄"], + ["^audio-headphones*", "󰋋"], + ["^audio-headset*", "󰋎"], + ["^audio-input*", "󰍬"], + ["^audio-speakers*", "󰓃"], + ["^bluetooth*", "󰂯"], + ["^camera*", "󰄀"], + ["^computer*", "󰟀"], + ["^input-gaming*", "󰍬"], + ["^input-keyboard*", "󰌌"], + ["^input-mouse*", "󰍽"], + ["^input-tablet*", "󰓶"], + ["^media*", "󱛟"], + ["^modem*", "󱂇"], + ["^network*", "󱂇"], + ["^phone*", "󰄞"], + ["^printer*", "󰐪"], + ["^scanner*", "󰚫"], + ["^video-camera*", "󰕧"], + ]; + + const foundMatch = deviceIconMap.find((icon) => + RegExp(icon[0]).test(iconName.toLowerCase()), + ); + + return foundMatch ? foundMatch[1] : "󰂯"; +}; + +export { getBluetoothIcon }; diff --git a/modules/menus/calendar/calendar.js b/modules/menus/calendar/calendar.ts similarity index 100% rename from modules/menus/calendar/calendar.js rename to modules/menus/calendar/calendar.ts diff --git a/modules/menus/calendar/index.js b/modules/menus/calendar/index.ts similarity index 100% rename from modules/menus/calendar/index.js rename to modules/menus/calendar/index.ts diff --git a/modules/menus/calendar/time/index.js b/modules/menus/calendar/time/index.ts similarity index 100% rename from modules/menus/calendar/time/index.js rename to modules/menus/calendar/time/index.ts diff --git a/modules/menus/calendar/weather/hourly/icon/index.js b/modules/menus/calendar/weather/hourly/icon/index.ts similarity index 67% rename from modules/menus/calendar/weather/hourly/icon/index.js rename to modules/menus/calendar/weather/hourly/icon/index.ts index ceeac30..0f9517b 100644 --- a/modules/menus/calendar/weather/hourly/icon/index.js +++ b/modules/menus/calendar/weather/hourly/icon/index.ts @@ -1,6 +1,8 @@ +import { Weather } from "lib/types/weather.js"; +import { Variable } from "types/variable.js"; import icons from "../../../../../icons/index.js"; -export const HourlyIcon = (theWeather, getNextEpoch) => { +export const HourlyIcon = (theWeather: Variable, getNextEpoch: any) => { return Widget.Icon({ class_name: "hourly-weather-icon", icon: theWeather.bind("value").as((w) => { @@ -16,9 +18,11 @@ export const HourlyIcon = (theWeather, getNextEpoch) => { let iconQuery = weatherAtEpoch?.condition.text .trim() .toLowerCase() - .replaceAll(" ", "_"); + .replaceAll(" ", "_") + || "warning" + ; - if (!weatherAtEpoch?.isDay && iconQuery === "partly_cloudy") { + if (!weatherAtEpoch?.is_day && iconQuery === "partly_cloudy") { iconQuery = "partly_cloudy_night"; } diff --git a/modules/menus/calendar/weather/hourly/index.js b/modules/menus/calendar/weather/hourly/index.js deleted file mode 100644 index bfde498..0000000 --- a/modules/menus/calendar/weather/hourly/index.js +++ /dev/null @@ -1,44 +0,0 @@ -import { HourlyIcon } from "./icon/index.js"; -import { HourlyTemp } from "./temperature/index.js"; -import { HourlyTime } from "./time/index.js"; - -export const Hourly = (theWeather) => { - return Widget.Box({ - vertical: false, - hexpand: true, - hpack: "fill", - class_name: "hourly-weather-container", - children: [1, 2, 3, 4].map((hoursFromNow) => { - const getNextEpoch = (wthr) => { - const currentEpoch = wthr.location.localtime_epoch; - const epochAtHourStart = currentEpoch - (currentEpoch % 3600); - let nextEpoch = 3600 * hoursFromNow + epochAtHourStart; - - const curHour = new Date(currentEpoch * 1000).getHours(); - - /* - * NOTE: Since the API is only capable of showing the current day; if - * the hours left in the day are less than 4 (aka spilling into the next day), - * then rewind to contain the prediction within the current day. - */ - if (curHour > 19) { - const hoursToRewind = curHour - 19; - nextEpoch = - 3600 * hoursFromNow + epochAtHourStart - hoursToRewind * 3600; - } - return nextEpoch; - }; - - return Widget.Box({ - class_name: "hourly-weather-item", - hexpand: true, - vertical: true, - children: [ - HourlyTime(theWeather, getNextEpoch), - HourlyIcon(theWeather, getNextEpoch), - HourlyTemp(theWeather, getNextEpoch), - ], - }); - }), - }); -}; diff --git a/modules/menus/calendar/weather/hourly/index.ts b/modules/menus/calendar/weather/hourly/index.ts new file mode 100644 index 0000000..046bf92 --- /dev/null +++ b/modules/menus/calendar/weather/hourly/index.ts @@ -0,0 +1,46 @@ +import { Weather } from "lib/types/weather"; +import { Variable } from "types/variable"; +import { HourlyIcon } from "./icon/index.js"; +import { HourlyTemp } from "./temperature/index.js"; +import { HourlyTime } from "./time/index.js"; + +export const Hourly = (theWeather: Variable) => { + return Widget.Box({ + vertical: false, + hexpand: true, + hpack: "fill", + class_name: "hourly-weather-container", + children: [1, 2, 3, 4].map((hoursFromNow) => { + const getNextEpoch = (wthr: Weather) => { + const currentEpoch = wthr.location.localtime_epoch; + const epochAtHourStart = currentEpoch - (currentEpoch % 3600); + let nextEpoch = 3600 * hoursFromNow + epochAtHourStart; + + const curHour = new Date(currentEpoch * 1000).getHours(); + + /* + * NOTE: Since the API is only capable of showing the current day; if + * the hours left in the day are less than 4 (aka spilling into the next day), + * then rewind to contain the prediction within the current day. + */ + if (curHour > 19) { + const hoursToRewind = curHour - 19; + nextEpoch = + 3600 * hoursFromNow + epochAtHourStart - hoursToRewind * 3600; + } + return nextEpoch; + }; + + return Widget.Box({ + class_name: "hourly-weather-item", + hexpand: true, + vertical: true, + children: [ + HourlyTime(theWeather, getNextEpoch), + HourlyIcon(theWeather, getNextEpoch), + HourlyTemp(theWeather, getNextEpoch), + ], + }); + }), + }); +}; diff --git a/modules/menus/calendar/weather/hourly/temperature/index.js b/modules/menus/calendar/weather/hourly/temperature/index.js deleted file mode 100644 index 1ba4324..0000000 --- a/modules/menus/calendar/weather/hourly/temperature/index.js +++ /dev/null @@ -1,27 +0,0 @@ -import options from "options"; - -const { unit } = options.menus.clock.weather; - -export const HourlyTemp = (theWeather, getNextEpoch) => { - return Widget.Label({ - class_name: "hourly-weather-temp", - label: Utils.merge( - [theWeather.bind("value"), unit.bind("value")], - (wthr, unt) => { - if (!Object.keys(wthr).length) { - return "-"; - } - - const nextEpoch = getNextEpoch(wthr); - const weatherAtEpoch = wthr.forecast.forecastday[0].hour.find( - (h) => h.time_epoch === nextEpoch, - ); - - if (unt === "imperial") { - return `${weatherAtEpoch ? Math.ceil(weatherAtEpoch.temp_f) : "-"}° F`; - } - return `${weatherAtEpoch ? Math.ceil(weatherAtEpoch.temp_c) : "-"}° C`; - }, - ), - }); -}; diff --git a/modules/menus/calendar/weather/hourly/temperature/index.ts b/modules/menus/calendar/weather/hourly/temperature/index.ts new file mode 100644 index 0000000..d649358 --- /dev/null +++ b/modules/menus/calendar/weather/hourly/temperature/index.ts @@ -0,0 +1,29 @@ +import { Weather } from "lib/types/weather"; +import { Variable } from "types/variable"; +import options from "options"; + +const { unit } = options.menus.clock.weather; + +export const HourlyTemp = (theWeather: Variable, getNextEpoch: any) => { + return Widget.Label({ + class_name: "hourly-weather-temp", + label: Utils.merge( + [theWeather.bind("value"), unit.bind("value")], + (wthr, unt) => { + if (!Object.keys(wthr).length) { + return "-"; + } + + const nextEpoch = getNextEpoch(wthr); + const weatherAtEpoch = wthr.forecast.forecastday[0].hour.find( + (h) => h.time_epoch === nextEpoch, + ); + + if (unt === "imperial") { + return `${weatherAtEpoch ? Math.ceil(weatherAtEpoch.temp_f) : "-"}° F`; + } + return `${weatherAtEpoch ? Math.ceil(weatherAtEpoch.temp_c) : "-"}° C`; + }, + ), + }); +}; diff --git a/modules/menus/calendar/weather/hourly/time/index.js b/modules/menus/calendar/weather/hourly/time/index.js deleted file mode 100644 index d244602..0000000 --- a/modules/menus/calendar/weather/hourly/time/index.js +++ /dev/null @@ -1,18 +0,0 @@ -export const HourlyTime = (theWeather, getNextEpoch) => { - return Widget.Label({ - class_name: "hourly-weather-time", - label: theWeather.bind("value").as((w) => { - if (!Object.keys(w).length) { - return "-"; - } - - const nextEpoch = getNextEpoch(w); - const dateAtEpoch = new Date(nextEpoch * 1000); - let hours = dateAtEpoch.getHours(); - const ampm = hours >= 12 ? "PM" : "AM"; - hours = hours % 12 || 12; - - return `${hours}${ampm}`; - }), - }); -}; diff --git a/modules/menus/calendar/weather/hourly/time/index.ts b/modules/menus/calendar/weather/hourly/time/index.ts new file mode 100644 index 0000000..48e2ebe --- /dev/null +++ b/modules/menus/calendar/weather/hourly/time/index.ts @@ -0,0 +1,21 @@ +import { Weather } from "lib/types/weather"; +import { Variable } from "types/variable"; + +export const HourlyTime = (theWeather: Variable, getNextEpoch: any) => { + return Widget.Label({ + class_name: "hourly-weather-time", + label: theWeather.bind("value").as((w) => { + if (!Object.keys(w).length) { + return "-"; + } + + const nextEpoch = getNextEpoch(w); + const dateAtEpoch = new Date(nextEpoch * 1000); + let hours = dateAtEpoch.getHours(); + const ampm = hours >= 12 ? "PM" : "AM"; + hours = hours % 12 || 12; + + return `${hours}${ampm}`; + }), + }); +}; diff --git a/modules/menus/calendar/weather/icon/index.js b/modules/menus/calendar/weather/icon/index.js deleted file mode 100644 index 3d5371f..0000000 --- a/modules/menus/calendar/weather/icon/index.js +++ /dev/null @@ -1,25 +0,0 @@ -import icons from "../../../../icons/index.js"; - -export const TodayIcon = (theWeather) => { - return Widget.Box({ - vpack: "center", - hpack: "start", - class_name: "calendar-menu-weather today icon container", - children: [ - Widget.Icon({ - class_name: "calendar-menu-weather today icon", - icon: theWeather.bind("value").as((v) => { - let iconQuery = v.current.condition.text - .trim() - .toLowerCase() - .replaceAll(" ", "_"); - - if (!v.current.isDay && iconQuery === "partly_cloudy") { - iconQuery = "partly_cloudy_night"; - } - return icons.weather[iconQuery]; - }), - }), - ], - }); -}; diff --git a/modules/menus/calendar/weather/icon/index.ts b/modules/menus/calendar/weather/icon/index.ts new file mode 100644 index 0000000..f5a0fc7 --- /dev/null +++ b/modules/menus/calendar/weather/icon/index.ts @@ -0,0 +1,27 @@ +import { Weather } from "lib/types/weather"; +import { Variable } from "types/variable"; +import icons from "../../../../icons/index.js"; + +export const TodayIcon = (theWeather: Variable) => { + return Widget.Box({ + vpack: "center", + hpack: "start", + class_name: "calendar-menu-weather today icon container", + children: [ + Widget.Icon({ + class_name: "calendar-menu-weather today icon", + icon: theWeather.bind("value").as((v) => { + let iconQuery = v.current.condition.text + .trim() + .toLowerCase() + .replaceAll(" ", "_"); + + if (!v.current.is_day && iconQuery === "partly_cloudy") { + iconQuery = "partly_cloudy_night"; + } + return icons.weather[iconQuery]; + }), + }), + ], + }); +}; diff --git a/modules/menus/calendar/weather/index.js b/modules/menus/calendar/weather/index.js deleted file mode 100644 index 7c7a4dd..0000000 --- a/modules/menus/calendar/weather/index.js +++ /dev/null @@ -1,92 +0,0 @@ -import options from "options"; -import { TodayIcon } from "./icon/index.js"; -import { TodayStats } from "./stats/index.js"; -import { TodayTemperature } from "./temperature/index.js"; -import { Hourly } from "./hourly/index.js"; - -const { key, interval } = options.menus.clock.weather; - -const defaultWeather = { - location: { - localtime_epoch: 1719471600, - }, - current: { - temp_f: 0, - wind_mph: 0, - condition: { - text: "Clear", - }, - }, - forecast: { - forecastday: [ - { - day: { - daily_chance_of_rain: 0, - }, - hour: [ - { - time_epoch: 1719471600, - temp_f: 0, - condition: { - text: "Clear", - }, - }, - ], - }, - ], - }, -}; - -const theWeather = Variable(defaultWeather); - -const WeatherWidget = () => { - return Widget.Box({ - class_name: "calendar-menu-item-container weather", - child: Widget.Box({ - class_name: "weather-container-box", - setup: (self) => { - Utils.merge( - [key.bind("value"), interval.bind("value")], - (weatherKey, weatherInterval) => { - Utils.interval(weatherInterval, () => { - Utils.execAsync( - `curl "https://api.weatherapi.com/v1/forecast.json?key=${weatherKey}&q=93722&days=1&aqi=no&alerts=no"`, - ) - .then((res) => { - if (typeof res === "string") { - theWeather.value = JSON.parse(res); - } - }) - .catch((err) => { - console.error(`Failed to fetch weather: ${err}`); - theWeather.value = defaultWeather; - }); - }); - }, - ); - - return (self.child = Widget.Box({ - vertical: true, - hexpand: true, - children: [ - Widget.Box({ - class_name: "calendar-menu-weather today", - hexpand: true, - children: [ - TodayIcon(theWeather), - TodayTemperature(theWeather), - TodayStats(theWeather), - ], - }), - Widget.Separator({ - class_name: "menu-separator weather", - }), - Hourly(theWeather), - ], - })); - }, - }), - }); -}; - -export { WeatherWidget }; diff --git a/modules/menus/calendar/weather/index.ts b/modules/menus/calendar/weather/index.ts new file mode 100644 index 0000000..3fabe01 --- /dev/null +++ b/modules/menus/calendar/weather/index.ts @@ -0,0 +1,63 @@ +import options from "options"; +import { TodayIcon } from "./icon/index.js"; +import { TodayStats } from "./stats/index.js"; +import { TodayTemperature } from "./temperature/index.js"; +import { Hourly } from "./hourly/index.js"; +import { Weather } from "lib/types/weather.js"; +import { DEFAULT_WEATHER } from "lib/types/defaults/weather.js"; + +const { key, interval } = options.menus.clock.weather; + +const theWeather = Variable(DEFAULT_WEATHER); + +const WeatherWidget = () => { + return Widget.Box({ + class_name: "calendar-menu-item-container weather", + child: Widget.Box({ + class_name: "weather-container-box", + setup: (self) => { + Utils.merge( + [key.bind("value"), interval.bind("value")], + (weatherKey, weatherInterval) => { + Utils.interval(weatherInterval, () => { + Utils.execAsync( + `curl "https://api.weatherapi.com/v1/forecast.json?key=${weatherKey}&q=93722&days=1&aqi=no&alerts=no"`, + ) + .then((res) => { + if (typeof res === "string") { + theWeather.value = JSON.parse(res); + } + }) + .catch((err) => { + console.error(`Failed to fetch weather: ${err}`); + theWeather.value = DEFAULT_WEATHER; + }); + }); + }, + ); + + return (self.child = Widget.Box({ + vertical: true, + hexpand: true, + children: [ + Widget.Box({ + class_name: "calendar-menu-weather today", + hexpand: true, + children: [ + TodayIcon(theWeather), + TodayTemperature(theWeather), + TodayStats(theWeather), + ], + }), + Widget.Separator({ + class_name: "menu-separator weather", + }), + Hourly(theWeather), + ], + })); + }, + }), + }); +}; + +export { WeatherWidget }; diff --git a/modules/menus/calendar/weather/stats/index.js b/modules/menus/calendar/weather/stats/index.js deleted file mode 100644 index def90db..0000000 --- a/modules/menus/calendar/weather/stats/index.js +++ /dev/null @@ -1,52 +0,0 @@ -import options from "options"; - -const { unit } = options.menus.clock.weather; - -export const TodayStats = (theWeather) => { - return Widget.Box({ - class_name: "calendar-menu-weather today stats container", - hpack: "end", - vpack: "center", - vertical: true, - children: [ - Widget.Box({ - class_name: "weather wind", - children: [ - Widget.Label({ - class_name: "weather wind icon", - label: "", - }), - Widget.Label({ - class_name: "weather wind label", - label: Utils.merge( - [theWeather.bind("value"), unit.bind("value")], - (wthr, unt) => { - if (unt === "imperial") { - return `${Math.floor(wthr.current.wind_mph)} mph`; - } - return `${Math.floor(wthr.current.wind_kph)} kph`; - }, - ), - }), - ], - }), - Widget.Box({ - class_name: "weather precip", - children: [ - Widget.Label({ - class_name: "weather precip icon", - label: "", - }), - Widget.Label({ - class_name: "weather precip label", - label: theWeather - .bind("value") - .as( - (v) => `${v.forecast.forecastday[0].day.daily_chance_of_rain}%`, - ), - }), - ], - }), - ], - }); -}; diff --git a/modules/menus/calendar/weather/stats/index.ts b/modules/menus/calendar/weather/stats/index.ts new file mode 100644 index 0000000..c459c70 --- /dev/null +++ b/modules/menus/calendar/weather/stats/index.ts @@ -0,0 +1,55 @@ +import { Weather } from "lib/types/weather"; +import { Variable } from "types/variable"; +import options from "options"; +import { Unit } from "lib/types/options"; + +const { unit } = options.menus.clock.weather; + +export const TodayStats = (theWeather: Variable) => { + return Widget.Box({ + class_name: "calendar-menu-weather today stats container", + hpack: "end", + vpack: "center", + vertical: true, + children: [ + Widget.Box({ + class_name: "weather wind", + children: [ + Widget.Label({ + class_name: "weather wind icon", + label: "", + }), + Widget.Label({ + class_name: "weather wind label", + label: Utils.merge( + [theWeather.bind("value"), unit.bind("value")], + (wthr: Weather, unt: Unit) => { + if (unt === "imperial") { + return `${Math.floor(wthr.current.wind_mph)} mph`; + } + return `${Math.floor(wthr.current.wind_kph)} kph`; + }, + ), + }), + ], + }), + Widget.Box({ + class_name: "weather precip", + children: [ + Widget.Label({ + class_name: "weather precip icon", + label: "", + }), + Widget.Label({ + class_name: "weather precip label", + label: theWeather + .bind("value") + .as( + (v) => `${v.forecast.forecastday[0].day.daily_chance_of_rain}%`, + ), + }), + ], + }), + ], + }); +}; diff --git a/modules/menus/calendar/weather/temperature/index.js b/modules/menus/calendar/weather/temperature/index.js deleted file mode 100644 index f03a972..0000000 --- a/modules/menus/calendar/weather/temperature/index.js +++ /dev/null @@ -1,92 +0,0 @@ -import options from "options"; -const { unit } = options.menus.clock.weather; - -export const TodayTemperature = (theWeather) => { - const getIcon = (fahren) => { - const icons = { - 100: "", - 75: "", - 50: "", - 25: "", - 0: "", - }; - const colors = { - 100: "weather-color red", - 75: "weather-color orange", - 50: "weather-color lavender", - 25: "weather-color blue", - 0: "weather-color sky", - }; - - const threshold = - fahren < 0 - ? 0 - : [100, 75, 50, 25, 0].find((threshold) => threshold <= fahren); - - return { - icon: icons[threshold], - color: colors[threshold], - }; - }; - - return Widget.Box({ - hpack: "center", - vpack: "center", - vertical: true, - children: [ - Widget.Box({ - hexpand: true, - vpack: "center", - class_name: "calendar-menu-weather today temp container", - vertical: false, - children: [ - Widget.Box({ - hexpand: true, - hpack: "center", - children: [ - Widget.Label({ - class_name: "calendar-menu-weather today temp label", - label: Utils.merge( - [theWeather.bind("value"), unit.bind("value")], - (wthr, unt) => { - if (unt === "imperial") { - return `${Math.ceil(wthr.current.temp_f)}° F`; - } else { - return `${Math.ceil(wthr.current.temp_c)}° C`; - } - }, - ), - }), - Widget.Label({ - class_name: theWeather - .bind("value") - .as( - (v) => - `calendar-menu-weather today temp label icon ${getIcon(Math.ceil(v.current.temp_f)).color}`, - ), - label: theWeather - .bind("value") - .as((v) => getIcon(Math.ceil(v.current.temp_f)).icon), - }), - ], - }), - ], - }), - Widget.Box({ - hpack: "center", - child: Widget.Label({ - max_width_chars: 17, - truncate: "end", - lines: 2, - class_name: theWeather - .bind("value") - .as( - (v) => - `calendar-menu-weather today condition label ${getIcon(Math.ceil(v.current.temp_f)).color}`, - ), - label: theWeather.bind("value").as((v) => v.current.condition.text), - }), - }), - ], - }); -}; diff --git a/modules/menus/calendar/weather/temperature/index.ts b/modules/menus/calendar/weather/temperature/index.ts new file mode 100644 index 0000000..5577118 --- /dev/null +++ b/modules/menus/calendar/weather/temperature/index.ts @@ -0,0 +1,94 @@ +import { Weather } from "lib/types/weather"; +import { Variable } from "types/variable"; +import options from "options"; +const { unit } = options.menus.clock.weather; + +export const TodayTemperature = (theWeather: Variable) => { + const getIcon = (fahren: number) => { + const icons = { + 100: "", + 75: "", + 50: "", + 25: "", + 0: "", + }; + const colors = { + 100: "weather-color red", + 75: "weather-color orange", + 50: "weather-color lavender", + 25: "weather-color blue", + 0: "weather-color sky", + }; + + const threshold = + fahren < 0 + ? 0 + : [100, 75, 50, 25, 0].find((threshold) => threshold <= fahren); + + return { + icon: icons[threshold || 50], + color: colors[threshold || 50], + }; + }; + + return Widget.Box({ + hpack: "center", + vpack: "center", + vertical: true, + children: [ + Widget.Box({ + hexpand: true, + vpack: "center", + class_name: "calendar-menu-weather today temp container", + vertical: false, + children: [ + Widget.Box({ + hexpand: true, + hpack: "center", + children: [ + Widget.Label({ + class_name: "calendar-menu-weather today temp label", + label: Utils.merge( + [theWeather.bind("value"), unit.bind("value")], + (wthr, unt) => { + if (unt === "imperial") { + return `${Math.ceil(wthr.current.temp_f)}° F`; + } else { + return `${Math.ceil(wthr.current.temp_c)}° C`; + } + }, + ), + }), + Widget.Label({ + class_name: theWeather + .bind("value") + .as( + (v) => + `calendar-menu-weather today temp label icon ${getIcon(Math.ceil(v.current.temp_f)).color}`, + ), + label: theWeather + .bind("value") + .as((v) => getIcon(Math.ceil(v.current.temp_f)).icon), + }), + ], + }), + ], + }), + Widget.Box({ + hpack: "center", + child: Widget.Label({ + max_width_chars: 17, + truncate: "end", + lines: 2, + class_name: theWeather + .bind("value") + .as( + (v) => + `calendar-menu-weather today condition label ${getIcon(Math.ceil(v.current.temp_f)).color}`, + ), + label: theWeather.bind("value").as((v) => v.current.condition.text), + }), + }), + ], + }); +};