Minor: Refactor the code-base for better organization and compartmentalization. (#934)

* Clean up unused code

* Fix media player formatting issue for labels with new line characteres.

* Refactor the media player handlers into a class.

* More code cleanup and organize shared weather utils into distinct classes.

* Flatten some nesting.

* Move weather manager in dedicated class and build HTTP Utility class for Rest API calling.

* Remove logs

* Rebase master merge

* Reorg code (WIP)

* More reorg

* Delete utility scripts

* Reorg options

* Finish moving all options over

* Fix typescript issues

* Update options imports to default

* missed update

* Screw barrel files honestly, work of the devil.

* Only initialize power profiles if power-profiles-daemon is running.

* Fix window positioning and weather service naming

* style dir

* More organization

* Restructure types to be closer to their source

* Remove lib types and constants

* Update basic weather object to be saner with extensibility.

* Service updates

* Fix initialization strategy for services.

* Fix Config Manager to only emit changed objects and added missing temp converters.

* Update storage service to handle unit changes.

* Added cpu temp sensor auto-discovery

* Added missing JSDocs to services

* remove unused

* Migrate to network service.

* Fix network password issue.

* Move out password input into helper

* Rename password mask constant to be less double-negativey.

* Dropdown menu rename

* Added a component to edit JSON in the settings dialog (rough/WIP)

* Align settings

* Add and style JSON Editor.

* Adjust padding

* perf(shortcuts):  avoid unnecessary polling when shortcuts are disabled

Stops the recording poller when shortcuts are disabled, preventing redundant polling and reducing resource usage.

* Fix types and return value if shortcut not enabled.

* Move the swww daemon checking process outside of the wallpaper service into a dedicated deamon lifecyle processor.

* Add more string formatters and use title case for weather status (as it was).

* Fix startup errors.

* Rgba fix

* Remove zod from dependencies

---------

Co-authored-by: KernelDiego <gonzalezdiego.contact@gmail.com>
This commit is contained in:
Jas Singh
2025-05-26 19:45:11 -07:00
committed by GitHub
parent 436dcbfcf2
commit 8cf5806766
532 changed files with 13134 additions and 8669 deletions

View File

@@ -2,7 +2,7 @@ import { Gtk } from 'astal/gtk3';
import { ActiveDevices } from './devices/index.js';
import { ActivePlaybacks } from './playbacks/index.js';
import { bind, Variable } from 'astal';
import { isPrimaryClick } from 'src/lib/utils.js';
import { isPrimaryClick } from 'src/lib/events/mouse';
export enum ActiveDeviceMenu {
DEVICES = 'devices',

View File

@@ -1,8 +1,9 @@
import { bind } from 'astal';
import { Gdk, Gtk } from 'astal/gtk3';
import AstalWp from 'gi://AstalWp?version=0.1';
import { capitalizeFirstLetter, isScrollDown, isScrollUp } from 'src/lib/utils';
import options from 'src/options';
import options from 'src/configuration';
import { isScrollUp, isScrollDown } from 'src/lib/events/mouse';
import { capitalizeFirstLetter } from 'src/lib/string/formatters';
const { raiseMaximumVolume } = options.menus.volume;

View File

@@ -1,8 +1,8 @@
import { bind, Variable } from 'astal';
import { Gtk } from 'astal/gtk3';
import { isPrimaryClick } from 'src/lib/utils';
import { getIcon } from '../../utils';
import AstalWp from 'gi://AstalWp?version=0.1';
import { isPrimaryClick } from 'src/lib/events/mouse';
export const SliderIcon = ({ type, device }: SliderIconProps): JSX.Element => {
const iconBinding = Variable.derive([bind(device, 'volume'), bind(device, 'mute')], (volume, isMuted) => {

View File

@@ -1,7 +1,7 @@
import { Gtk } from 'astal/gtk3';
import AstalWp from 'gi://AstalWp?version=0.1';
import { isPrimaryClick } from 'src/lib/utils';
import { bind } from 'astal';
import { isPrimaryClick } from 'src/lib/events/mouse';
const DeviceIcon = ({ device, type, icon }: AudioDeviceProps): JSX.Element => {
return (

View File

@@ -1,10 +1,10 @@
import DropdownMenu from '../shared/dropdown/index.js';
import { VolumeSliders } from './active/index.js';
import options from 'src/options.js';
import { bind } from 'astal';
import { Gtk } from 'astal/gtk3';
import { AvailableDevices } from './available/index.js';
import { RevealerTransitionMap } from 'src/lib/constants/options.js';
import { RevealerTransitionMap } from 'src/components/settings/constants.js';
import options from 'src/configuration';
export default (): JSX.Element => {
return (

View File

@@ -1,7 +1,7 @@
import { bind } from 'astal';
import { ActionButton } from './ActionButton';
import { isPrimaryClick } from 'src/lib/utils';
import AstalBluetooth from 'gi://AstalBluetooth?version=0.1';
import { isPrimaryClick } from 'src/lib/events/mouse';
export const ConnectButton = ({ device }: ConnectButtonProps): JSX.Element => {
return (

View File

@@ -1,7 +1,7 @@
import { ActionButton } from './ActionButton';
import { isPrimaryClick } from 'src/lib/utils';
import AstalBluetooth from 'gi://AstalBluetooth?version=0.1';
import { forgetBluetoothDevice } from '../helpers';
import { isPrimaryClick } from 'src/lib/events/mouse';
export const ForgetButton = ({ device }: ForgetButtonProps): JSX.Element => {
return (

View File

@@ -1,7 +1,7 @@
import { bind } from 'astal';
import { ActionButton } from './ActionButton';
import { isPrimaryClick } from 'src/lib/utils';
import AstalBluetooth from 'gi://AstalBluetooth?version=0.1';
import { isPrimaryClick } from 'src/lib/events/mouse';
export const PairButton = ({ device }: PairButtonProps): JSX.Element => {
return (

View File

@@ -1,7 +1,7 @@
import { bind } from 'astal';
import { ActionButton } from './ActionButton';
import { isPrimaryClick } from 'src/lib/utils';
import AstalBluetooth from 'gi://AstalBluetooth?version=0.1';
import { isPrimaryClick } from 'src/lib/events/mouse';
export const TrustButton = ({ device }: TrustButtonProps): JSX.Element => {
return (

View File

@@ -1,11 +1,11 @@
import { Gtk } from 'astal/gtk3';
import AstalBluetooth from 'gi://AstalBluetooth?version=0.1';
import Spinner from 'src/components/shared/Spinner';
import { isPrimaryClick } from 'src/lib/utils';
import { bind } from 'astal';
import { DeviceIcon } from './DeviceIcon';
import { DeviceName } from './DeviceName';
import { DeviceStatus } from './DeviceStatus';
import { isPrimaryClick } from 'src/lib/events/mouse';
export const BluetoothDevice = ({ device, connectedDevices }: BluetoothDeviceProps): JSX.Element => {
const IsConnectingSpinner = (): JSX.Element => {

View File

@@ -1,8 +1,8 @@
import { Gtk } from 'astal/gtk3';
import { isPrimaryClick } from 'src/lib/utils';
import { bind, timeout } from 'astal';
import { isDiscovering } from './helper';
import AstalBluetooth from 'gi://AstalBluetooth?version=0.1';
import { isPrimaryClick } from 'src/lib/events/mouse';
const bluetoothService = AstalBluetooth.get_default();

View File

@@ -1,10 +1,10 @@
import options from 'src/configuration';
import DropdownMenu from '../shared/dropdown/index.js';
import { BluetoothDevices } from './devices/index.js';
import { Header } from './header/index.js';
import options from 'src/options.js';
import { bind } from 'astal';
import { Gtk } from 'astal/gtk3';
import { RevealerTransitionMap } from 'src/lib/constants/options.js';
import { RevealerTransitionMap } from 'src/components/settings/constants.js';
export default (): JSX.Element => {
return (

View File

@@ -2,9 +2,9 @@ import DropdownMenu from '../shared/dropdown/index.js';
import { TimeWidget } from './time/index';
import { CalendarWidget } from './CalendarWidget.js';
import { WeatherWidget } from './weather/index';
import options from 'src/options';
import { bind } from 'astal';
import { RevealerTransitionMap } from 'src/lib/constants/options.js';
import { RevealerTransitionMap } from 'src/components/settings/constants.js';
import options from 'src/configuration';
const { transition } = options.menus;
const { enabled: weatherEnabled } = options.menus.clock.weather;

View File

@@ -1,7 +1,7 @@
import options from 'src/options';
import { bind, Variable } from 'astal';
import { Gtk } from 'astal/gtk3';
import { systemTime } from 'src/shared/time';
import options from 'src/configuration';
import { systemTime } from 'src/lib/units/time';
const { military, hideSeconds } = options.menus.clock.time;

View File

@@ -1,7 +1,7 @@
import options from 'src/options';
import { bind, GLib, Variable } from 'astal';
import { Gtk } from 'astal/gtk3';
import { systemTime } from 'src/shared/time';
import options from 'src/configuration';
import { systemTime } from 'src/lib/units/time';
const { military, hideSeconds } = options.menus.clock.time;

View File

@@ -1,64 +1,51 @@
import { Weather, WeatherIconTitle } from 'src/lib/types/weather.types';
import { isValidWeatherIconTitle } from 'src/shared/weather';
import { Weather, WeatherIcon, WeatherStatus } from 'src/services/weather/types';
/**
* Retrieves the next epoch time for weather data.
* Calculates the target hour for weather data lookup
*
* This function calculates the next epoch time based on the current weather data and the specified number of hours from now.
* It ensures that the prediction remains within the current day by rewinding the time if necessary.
*
* @param wthr The current weather data.
* @param hoursFromNow The number of hours from now to calculate the next epoch time.
*
* @returns The next epoch time as a number.
* @param baseTime - The base time to calculate from
* @param hoursFromNow - Number of hours to add
* @returns A Date object set to the start of the target hour
*/
export const getNextEpoch = (wthr: Weather, hoursFromNow: number): number => {
const currentEpoch = wthr.location.localtime_epoch;
const epochAtHourStart = currentEpoch - (currentEpoch % 3600);
let nextEpoch = 3600 * hoursFromNow + epochAtHourStart;
export const getTargetHour = (baseTime: Date, hoursFromNow: number): Date => {
const targetTime = new Date(baseTime);
const newHour = targetTime.getHours() + hoursFromNow;
targetTime.setHours(newHour);
targetTime.setMinutes(0, 0, 0);
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;
const currentHour = baseTime.getHours();
if (currentHour > 19) {
const hoursToRewind = currentHour - 19;
targetTime.setHours(targetTime.getHours() - hoursToRewind);
}
return nextEpoch;
return targetTime;
};
/**
* Retrieves the weather icon query for a specific time in the future.
* Retrieves the weather icon for a specific hour in the future
*
* This function calculates the next epoch time and retrieves the corresponding weather data.
* It then generates a weather icon query based on the weather condition and time of day.
*
* @param weather The current weather data.
* @param hoursFromNow The number of hours from now to calculate the weather icon query.
*
* @returns The weather icon query as a string.
* @param weather - The current weather data
* @param hoursFromNow - Number of hours from now to get the icon for
* @returns The appropriate weather icon
*/
export const getIconQuery = (weather: Weather, hoursFromNow: number): WeatherIconTitle => {
const nextEpoch = getNextEpoch(weather, hoursFromNow);
const weatherAtEpoch = weather.forecast.forecastday[0].hour.find((h) => h.time_epoch === nextEpoch);
if (weatherAtEpoch === undefined) {
return 'warning';
export const getHourlyWeatherIcon = (weather: Weather, hoursFromNow: number): WeatherIcon => {
if (!weather?.forecast?.[0]?.hourly) {
return WeatherIcon.WARNING;
}
let iconQuery = weatherAtEpoch.condition.text.trim().toLowerCase().replaceAll(' ', '_');
const targetHour = getTargetHour(weather.lastUpdated, hoursFromNow);
const targetTime = targetHour.getTime();
if (!weatherAtEpoch?.is_day && iconQuery === 'partly_cloudy') {
iconQuery = 'partly_cloudy_night';
const weatherAtHour = weather.forecast[0].hourly.find((hour) => {
const hourTime = hour.time.getTime();
return hourTime === targetTime;
});
if (!weatherAtHour) {
return WeatherIcon.WARNING;
}
if (isValidWeatherIconTitle(iconQuery)) {
return iconQuery;
} else {
return 'warning';
}
const iconQuery: WeatherStatus = weatherAtHour.condition?.text ?? 'WARNING';
return WeatherIcon[iconQuery];
};

View File

@@ -1,18 +1,18 @@
import { bind } from 'astal';
import { globalWeatherVar } from 'src/shared/weather';
import { Gtk } from 'astal/gtk3';
import { weatherIcons } from 'src/lib/icons/weather.js';
import { getIconQuery } from '../helpers';
import WeatherService from 'src/services/weather';
import { getHourlyWeatherIcon } from '../helpers';
const weatherService = WeatherService.getInstance();
export const HourlyIcon = ({ hoursFromNow }: HourlyIconProps): JSX.Element => {
return (
<box halign={Gtk.Align.CENTER}>
<label
className={'hourly-weather-icon txt-icon'}
label={bind(globalWeatherVar).as((weather) => {
const iconQuery = getIconQuery(weather, hoursFromNow);
const weatherIcn = weatherIcons[iconQuery] || weatherIcons['warning'];
return weatherIcn;
label={bind(weatherService.weatherData).as((weather) => {
const weatherIcon = getHourlyWeatherIcon(weather, hoursFromNow);
return weatherIcon;
})}
halign={Gtk.Align.CENTER}
/>

View File

@@ -1,24 +1,33 @@
import options from 'src/options';
import { globalWeatherVar } from 'src/shared/weather';
import { getNextEpoch } from '../helpers';
import { bind, Variable } from 'astal';
import options from 'src/configuration';
import WeatherService from 'src/services/weather';
import { getTargetHour } from '../helpers';
import { TemperatureConverter } from 'src/lib/units/temperature';
const weatherService = WeatherService.getInstance();
const { unit } = options.menus.clock.weather;
export const HourlyTemp = ({ hoursFromNow }: HourlyTempProps): JSX.Element => {
const weatherBinding = Variable.derive([bind(globalWeatherVar), bind(unit)], (weather, unitType) => {
if (!Object.keys(weather).length) {
return '-';
}
const weatherBinding = Variable.derive(
[bind(weatherService.weatherData), bind(unit)],
(weather, unitType) => {
if (!Object.keys(weather).length || !weather?.forecast?.[0]?.hourly) {
return '-';
}
const nextEpoch = getNextEpoch(weather, hoursFromNow);
const weatherAtEpoch = weather.forecast.forecastday[0].hour.find((h) => h.time_epoch === nextEpoch);
const targetHour = getTargetHour(new Date(), hoursFromNow);
const weatherAtTargetHour = weather.forecast[0].hourly.find(
(h) => h.time.getTime() === targetHour.getTime(),
);
const temperatureAtTargetHour = weatherAtTargetHour?.temperature ?? 0;
if (unitType === 'imperial') {
return `${weatherAtEpoch ? Math.ceil(weatherAtEpoch.temp_f) : '-'}° F`;
}
return `${weatherAtEpoch ? Math.ceil(weatherAtEpoch.temp_c) : '-'}° C`;
});
const tempConverter = TemperatureConverter.fromCelsius(temperatureAtTargetHour);
const isImperial = unitType === 'imperial';
return isImperial ? tempConverter.formatFahrenheit() : tempConverter.formatCelsius();
},
);
return (
<label

View File

@@ -1,29 +1,32 @@
import options from 'src/options';
import { globalWeatherVar } from 'src/shared/weather';
import { getNextEpoch } from '../helpers';
import { bind, Variable } from 'astal';
import options from 'src/configuration';
import WeatherService from 'src/services/weather';
import { getTargetHour } from '../helpers';
const weatherService = WeatherService.getInstance();
const { military } = options.menus.clock.time;
export const HourlyTime = ({ hoursFromNow }: HourlyTimeProps): JSX.Element => {
const weatherBinding = Variable.derive([bind(globalWeatherVar), bind(military)], (weather, military) => {
if (!Object.keys(weather).length) {
return '-';
}
const weatherBinding = Variable.derive(
[bind(weatherService.weatherData), bind(military)],
(weather, military) => {
if (!Object.keys(weather).length) {
return '-';
}
const nextEpoch = getNextEpoch(weather, hoursFromNow);
const dateAtEpoch = new Date(nextEpoch * 1000);
const targetHour = getTargetHour(new Date(), hoursFromNow);
let hours = dateAtEpoch.getHours();
let hours = targetHour.getHours();
if (military) {
return `${hours}:00`;
}
if (military) {
return `${hours}:00`;
}
const ampm = hours >= 12 ? 'PM' : 'AM';
hours = hours % 12 || 12;
return `${hours}${ampm}`;
});
const ampm = hours >= 12 ? 'PM' : 'AM';
hours = hours % 12 || 12;
return `${hours}${ampm}`;
},
);
return (
<label

View File

@@ -1,6 +1,8 @@
import { bind } from 'astal';
import { Gtk } from 'astal/gtk3';
import { getWeatherStatusTextIcon, globalWeatherVar } from 'src/shared/weather';
import WeatherService from 'src/services/weather';
const weatherService = WeatherService.getInstance();
export const TodayIcon = (): JSX.Element => {
return (
@@ -11,7 +13,7 @@ export const TodayIcon = (): JSX.Element => {
>
<label
className={'calendar-menu-weather today icon txt-icon'}
label={bind(globalWeatherVar).as((weather) => getWeatherStatusTextIcon(weather))}
label={bind(weatherService.statusIcon)}
/>
</box>
);

View File

@@ -1,31 +1,27 @@
import { getTemperature, globalWeatherVar } from 'src/shared/weather';
import options from 'src/options';
import { getRainChance } from 'src/shared/weather';
import { Gtk } from 'astal/gtk3';
import { bind, Variable } from 'astal';
import { bind } from 'astal';
import WeatherService from 'src/services/weather';
const { unit } = options.menus.clock.weather;
const weatherService = WeatherService.getInstance();
export const TodayStats = (): JSX.Element => {
const temperatureBinding = Variable.derive([bind(globalWeatherVar), bind(unit)], getTemperature);
return (
<box
className={'calendar-menu-weather today stats container'}
halign={Gtk.Align.END}
valign={Gtk.Align.CENTER}
vertical
onDestroy={() => {
temperatureBinding.drop();
}}
>
<box className={'weather wind'}>
<label className={'weather wind icon txt-icon'} label={''} />
<label className={'weather wind label'} label={temperatureBinding()} />
<label className={'weather wind label'} label={bind(weatherService.windCondition)} />
</box>
<box className={'weather precip'}>
<label className={'weather precip icon txt-icon'} label={''} />
<label className={'weather precip label'} label={bind(globalWeatherVar).as(getRainChance)} />
<label
className={'weather precip label'}
label={bind(weatherService.rainChance).as((chanceOfRain) => `${chanceOfRain}%`)}
/>
</box>
</box>
);

View File

@@ -1,43 +1,48 @@
import options from 'src/options';
import { globalWeatherVar } from 'src/shared/weather';
import { getTemperature, getWeatherIcon } from 'src/shared/weather';
import { Gtk } from 'astal/gtk3';
import { bind, Variable } from 'astal';
import { bind } from 'astal';
import WeatherService from 'src/services/weather';
import options from 'src/configuration';
import { toTitleCase } from 'src/lib/string/formatters';
const { unit } = options.menus.clock.weather;
const weatherService = WeatherService.getInstance();
unit.subscribe((unitType) => (weatherService.unit = unitType));
const WeatherStatus = (): JSX.Element => {
return (
<box halign={Gtk.Align.CENTER}>
<label
className={bind(globalWeatherVar).as(
(weather) =>
`calendar-menu-weather today condition label ${getWeatherIcon(Math.ceil(weather.current.temp_f)).color}`,
className={bind(weatherService.gaugeIcon).as(
(gauge) => `calendar-menu-weather today condition label ${gauge.color}`,
)}
label={bind(weatherService.weatherData).as((weather) =>
toTitleCase(weather.current.condition.text),
)}
label={bind(globalWeatherVar).as((weather) => weather.current.condition.text)}
truncate
tooltipText={bind(globalWeatherVar).as((weather) => weather.current.condition.text)}
tooltipText={bind(weatherService.weatherData).as((weather) => weather.current.condition.text)}
/>
</box>
);
};
const Temperature = (): JSX.Element => {
const labelBinding = Variable.derive([bind(globalWeatherVar), bind(unit)], getTemperature);
const TemperatureLabel = (): JSX.Element => {
return <label className={'calendar-menu-weather today temp label'} label={labelBinding()} />;
return (
<label
className={'calendar-menu-weather today temp label'}
label={bind(weatherService.temperature)}
/>
);
};
const ThermometerIcon = (): JSX.Element => {
return (
<label
className={bind(globalWeatherVar).as(
(weather) =>
`calendar-menu-weather today temp label icon txt-icon ${getWeatherIcon(Math.ceil(weather.current.temp_f)).color}`,
)}
label={bind(globalWeatherVar).as(
(weather) => getWeatherIcon(Math.ceil(weather.current.temp_f)).icon,
className={bind(weatherService.gaugeIcon).as(
(gauge) => `calendar-menu-weather today temp label icon txt-icon ${gauge.color}`,
)}
label={bind(weatherService.gaugeIcon).as((gauge) => gauge.icon)}
/>
);
};
@@ -47,9 +52,6 @@ const Temperature = (): JSX.Element => {
className={'calendar-menu-weather today temp container'}
valign={Gtk.Align.CENTER}
vertical={false}
onDestroy={() => {
labelBinding.drop();
}}
hexpand
>
<box halign={Gtk.Align.CENTER} hexpand>

View File

@@ -1,10 +1,10 @@
import { bind } from 'astal';
import { isPrimaryClick } from 'src/lib/utils';
import { isWifiEnabled } from './helpers';
import AstalNotifd from 'gi://AstalNotifd?version=0.1';
import AstalBluetooth from 'gi://AstalBluetooth?version=0.1';
import AstalNetwork from 'gi://AstalNetwork?version=0.1';
import AstalWp from 'gi://AstalWp?version=0.1';
import { isPrimaryClick } from 'src/lib/events/mouse';
const wireplumber = AstalWp.get_default() as AstalWp.Wp;
const audioService = wireplumber.audio;

View File

@@ -6,8 +6,9 @@ import {
PlaybackButton,
WifiButton,
} from './ControlButtons';
import { JSXElement } from 'src/core/types';
export const Controls = ({ isEnabled }: ControlsProps): JSX.Element => {
export const Controls = ({ isEnabled }: ControlsProps): JSXElement => {
if (!isEnabled) {
return null;
}

View File

@@ -1,7 +1,7 @@
import { bind, execAsync, Variable } from 'astal';
import { App, Gtk, Widget } from 'astal/gtk3';
import { isPrimaryClick } from 'src/lib/utils';
import options from 'src/options';
import options from 'src/configuration';
import { isPrimaryClick } from 'src/lib/events/mouse';
const { left, right } = options.menus.dashboard.directories;

View File

@@ -1,8 +1,9 @@
import { Gtk } from 'astal/gtk3';
import { LeftSection, RightSection } from './Sections';
import { LeftLink1, LeftLink2, LeftLink3, RightLink1, RightLink2, RightLink3 } from './DirectoryLinks';
import { JSXElement } from 'src/core/types';
export const Directories = ({ isEnabled }: DirectoriesProps): JSX.Element => {
export const Directories = ({ isEnabled }: DirectoriesProps): JSXElement => {
if (!isEnabled) {
return null;
}

View File

@@ -4,9 +4,9 @@ import { Shortcuts } from './shortcuts/index.js';
import { Controls } from './controls/index.js';
import { Stats } from './stats/index.js';
import { Directories } from './directories/index.js';
import options from 'src/options.js';
import { bind, Variable } from 'astal';
import { RevealerTransitionMap } from 'src/lib/constants/options.js';
import { RevealerTransitionMap } from 'src/components/settings/constants.js';
import options from 'src/configuration';
const { controls, shortcuts, stats, directories } = options.menus.dashboard;
const { transition } = options.menus;

View File

@@ -1,6 +1,6 @@
import { isPrimaryClick } from 'src/lib/utils';
import { isPrimaryClick } from 'src/lib/events/mouse';
import { handleClick } from './helpers';
import { PowerOptions } from 'src/lib/options/options.types';
import { PowerOptions } from 'src/lib/options/types';
const PowerActionButton = (icon: string, tooltip: string, action: PowerOptions): JSX.Element => {
return (

View File

@@ -1,7 +1,8 @@
import { bind, GLib } from 'astal';
import { Gtk } from 'astal/gtk3';
import options from 'src/options.js';
import { normalizePath, isAnImage } from 'src/lib/utils.js';
import options from 'src/configuration';
import { normalizeToAbsolutePath } from 'src/lib/path/helpers';
import { isAnImage } from 'src/lib/validation/images';
const { image, name } = options.menus.dashboard.powermenu.avatar;
@@ -12,7 +13,7 @@ const ProfilePicture = (): JSX.Element => {
halign={Gtk.Align.CENTER}
css={bind(image).as((img) => {
if (isAnImage(img)) {
return `background-image: url("${normalizePath(img)}")`;
return `background-image: url("${normalizeToAbsolutePath(img)}")`;
}
return `background-image: url("${SRC_DIR}/assets/hyprpanel.png")`;

View File

@@ -1,8 +1,8 @@
import { App } from 'astal/gtk3';
import powermenu from '../../power/helpers/actions.js';
import { execAsync } from 'astal';
import options from 'src/options';
import { PowerOptions } from 'src/lib/options/options.types.js';
import { PowerOptions } from 'src/lib/options/types.js';
import options from 'src/configuration';
const { confirmation, shutdown, logout, sleep, reboot } = options.menus.dashboard.powermenu;

View File

@@ -1,8 +1,8 @@
import { Widget } from 'astal/gtk3';
import { isPrimaryClick } from 'src/lib/utils';
import { handleClick, hasCommand } from '../helpers';
import options from 'src/options';
import { ShortcutVariable } from 'src/lib/types/dashboard.types';
import options from 'src/configuration';
import { isPrimaryClick } from 'src/lib/events/mouse';
import { ShortcutVariable } from '../types';
const { left, right } = options.menus.dashboard.shortcuts;

View File

@@ -1,11 +1,16 @@
import { bind, execAsync, timeout, Variable } from 'astal';
import { App } from 'astal/gtk3';
import options from 'src/configuration';
import { BashPoller } from 'src/lib/poller/BashPoller';
import { ShortcutVariable } from 'src/lib/types/dashboard.types';
import options from 'src/options';
import { ShortcutVariable } from './types';
const { left } = options.menus.dashboard.shortcuts;
/**
* A variable representing the polling interval in milliseconds.
*/
const pollingInterval = Variable(1000);
/**
* Retrieves the latest recording path from options.
*
@@ -27,19 +32,6 @@ export const executeCommand = async (command: string): Promise<void> => {
}
};
/**
* Handles the recorder status based on the command output.
*
* This function checks if the command output indicates that recording is in progress.
*
* @param commandOutput The output of the command to check.
*
* @returns True if the command output is 'recording', false otherwise.
*/
export const handleRecorder = (commandOutput: string): boolean => {
return commandOutput === 'recording';
};
/**
* Handles the click action for a shortcut.
*
@@ -82,11 +74,6 @@ export const leftCardHidden = Variable(
),
);
/**
* A variable representing the polling interval in milliseconds.
*/
export const pollingInterval = Variable(1000);
/**
* A variable indicating whether recording is in progress.
*/
@@ -105,3 +92,16 @@ export const recordingPoller = new BashPoller<boolean, []>(
`${SRC_DIR}/scripts/screen_record.sh status`,
handleRecorder,
);
/**
* Handles the recorder status based on the command output.
*
* This function checks if the command output indicates that recording is in progress.
*
* @param commandOutput The output of the command to check.
*
* @returns True if the command output is 'recording', false otherwise.
*/
function handleRecorder(commandOutput: string): boolean {
return commandOutput === 'recording';
}

View File

@@ -1,13 +1,14 @@
import { Gtk } from 'astal/gtk3';
import { LeftShortcuts, RightShortcuts } from './sections/Section';
import { recordingPoller } from './helpers';
import { JSXElement } from 'src/core/types';
export const Shortcuts = ({ isEnabled }: ShortcutsProps): JSX.Element => {
recordingPoller.initialize();
export const Shortcuts = ({ isEnabled }: ShortcutsProps): JSXElement => {
if (!isEnabled) {
recordingPoller.stop();
return null;
}
recordingPoller.initialize();
return (
<box className={'shortcuts-container'} halign={Gtk.Align.FILL} hexpand>

View File

@@ -1,5 +1,4 @@
import { bind, Variable } from 'astal';
import options from 'src/options';
import { hasCommand, isRecording, leftCardHidden } from '../helpers';
import {
LeftShortcut1,
@@ -12,6 +11,7 @@ import {
import { LeftColumn, RightColumn } from './Column';
import { SettingsButton } from '../buttons/SettingsButton';
import { RecordingButton } from '../buttons/RecordingButton';
import options from 'src/configuration';
const { left, right } = options.menus.dashboard.shortcuts;

View File

@@ -0,0 +1,8 @@
import { Variable } from 'astal';
export type ShortcutVariable = {
tooltip: Variable<string>;
command: Variable<string>;
icon: Variable<string>;
configurable?: true;
};

View File

@@ -1,11 +1,10 @@
import { bind } from 'astal';
import { Gtk } from 'astal/gtk3';
import { isPrimaryClick } from 'src/lib/utils';
import options from 'src/options';
import { handleClick } from './helpers';
import { cpuService, gpuService, handleClick, ramService, storageService } from './helpers';
import { Binding } from 'astal';
import { cpuService, gpuService, ramService, storageService } from '.';
import { renderResourceLabel } from 'src/components/bar/utils/helpers';
import { renderResourceLabel } from 'src/components/bar/utils/systemResource';
import options from 'src/configuration';
import { isPrimaryClick } from 'src/lib/events/mouse';
const { enable_gpu } = options.menus.dashboard.stats;
@@ -41,12 +40,14 @@ export const GpuStat = (): JSX.Element => {
return <box />;
}
gpuService.initialize();
return (
<StatBar
icon={'󰢮'}
stat={'gpu'}
value={bind(gpuService.gpuUsage)}
label={bind(gpuService.gpuUsage).as((gpuUsage) => `${Math.floor(gpuUsage * 100)}%`)}
value={bind(gpuService.gpu)}
label={bind(gpuService.gpu).as((gpuUsage) => `${Math.floor(gpuUsage * 100)}%`)}
/>
);
})}
@@ -55,6 +56,8 @@ export const GpuStat = (): JSX.Element => {
};
export const CpuStat = (): JSX.Element => {
cpuService.initialize();
return (
<StatBar
icon={''}
@@ -66,6 +69,8 @@ export const CpuStat = (): JSX.Element => {
};
export const RamStat = (): JSX.Element => {
ramService.initialize();
return (
<StatBar
icon={''}
@@ -79,6 +84,8 @@ export const RamStat = (): JSX.Element => {
};
export const StorageStat = (): JSX.Element => {
storageService.initialize();
return (
<StatBar
icon={'󰋊'}

View File

@@ -1,13 +1,19 @@
import { execAsync } from 'astal';
import { App } from 'astal/gtk3';
import options from 'src/options';
import Cpu from 'src/services/Cpu';
import Gpu from 'src/services/Gpu';
import Ram from 'src/services/Ram';
import Storage from 'src/services/Storage';
import options from 'src/configuration';
import CpuUsageService from 'src/services/system/cpuUsage';
import GpuUsageService from 'src/services/system/gpuUsage';
import RamUsageService from 'src/services/system/ramUsage';
import StorageService from 'src/services/system/storage';
const { terminal } = options;
const { interval, enabled, enable_gpu } = options.menus.dashboard.stats;
const { paths } = options.bar.customModules.storage;
export const gpuService = new GpuUsageService();
export const cpuService = new CpuUsageService();
export const ramService = new RamUsageService();
export const storageService = new StorageService({ pathsToMonitor: paths });
/**
* Handles the click event for the dashboard menu.
@@ -24,16 +30,12 @@ export const handleClick = (): void => {
* Monitors the interval for updating CPU, RAM, and storage services.
*
* This function subscribes to the interval setting and updates the timers for the CPU, RAM, and storage services accordingly.
*
* @param cpuService The CPU service instance.
* @param ramService The RAM service instance.
* @param storageService The storage service instance.
*/
const monitorInterval = (cpuService: Cpu, ramService: Ram, storageService: Storage): void => {
const monitorInterval = (): void => {
interval.subscribe(() => {
ramService.updateTimer(interval.get());
cpuService.updateTimer(interval.get());
storageService.updateTimer(interval.get());
storageService.frequency = interval.get();
});
};
@@ -41,18 +43,8 @@ const monitorInterval = (cpuService: Cpu, ramService: Ram, storageService: Stora
* Monitors the enabled state for CPU, RAM, GPU, and storage services.
*
* This function subscribes to the enabled setting and starts or stops the pollers for the CPU, RAM, GPU, and storage services based on the enabled state.
*
* @param cpuService The CPU service instance.
* @param ramService The RAM service instance.
* @param gpuService The GPU service instance.
* @param storageService The storage service instance.
*/
const monitorStatsEnabled = (
cpuService: Cpu,
ramService: Ram,
gpuService: Gpu,
storageService: Storage,
): void => {
const monitorStatsEnabled = (): void => {
enabled.subscribe(() => {
if (!enabled.get()) {
ramService.stopPoller();
@@ -76,10 +68,8 @@ const monitorStatsEnabled = (
* Monitors the GPU tracking enabled state.
*
* This function subscribes to the GPU tracking enabled setting and starts or stops the GPU poller based on the enabled state.
*
* @param gpuService The GPU service instance.
*/
const monitorGpuTrackingEnabled = (gpuService: Gpu): void => {
const monitorGpuTrackingEnabled = (): void => {
enable_gpu.subscribe((gpuEnabled) => {
if (gpuEnabled) {
return gpuService.startPoller();
@@ -90,24 +80,12 @@ const monitorGpuTrackingEnabled = (gpuService: Gpu): void => {
};
/**
* Initializes the pollers for CPU, RAM, GPU, and storage services.
* Sets up dashboard monitoring for CPU, RAM, GPU, and storage services.
*
* This function sets up the initial state for the CPU, RAM, GPU, and storage services, including starting the pollers if enabled.
* It also sets up monitoring for interval changes, enabled state changes, and GPU tracking enabled state.
*
* @param cpuService The CPU service instance.
* @param ramService The RAM service instance.
* @param gpuService The GPU service instance.
* @param storageService The storage service instance.
* This function sets up the initial state for the services and monitoring for interval changes, enabled state changes, and GPU tracking enabled state.
*/
export const initializePollers = (
cpuService: Cpu,
ramService: Ram,
gpuService: Gpu,
storageService: Storage,
): void => {
ramService.setShouldRound(true);
storageService.setShouldRound(true);
export const setupDashboardMonitoring = (): void => {
storageService.round = true;
if (enabled.get()) {
ramService.startPoller();
@@ -121,7 +99,7 @@ export const initializePollers = (
gpuService.stopPoller();
}
monitorInterval(cpuService, ramService, storageService);
monitorStatsEnabled(cpuService, ramService, gpuService, storageService);
monitorGpuTrackingEnabled(gpuService);
monitorInterval();
monitorStatsEnabled();
monitorGpuTrackingEnabled();
};

View File

@@ -1,23 +1,15 @@
import { Gtk } from 'astal/gtk3';
import { CpuStat, GpuStat, RamStat, StorageStat } from './StatBars';
import { initializePollers } from './helpers';
import Gpu from 'src/services/Gpu';
import Ram from 'src/services/Ram';
import Cpu from 'src/services/Cpu';
import Storage from 'src/services/Storage';
import { setupDashboardMonitoring } from './helpers';
import { JSXElement } from 'src/core/types';
export const ramService = new Ram();
export const cpuService = new Cpu();
export const storageService = new Storage();
export const gpuService = new Gpu();
initializePollers(cpuService, ramService, gpuService, storageService);
export const Stats = ({ isEnabled }: StatsProps): JSX.Element => {
export const Stats = ({ isEnabled }: StatsProps): JSXElement => {
if (!isEnabled) {
return null;
}
setupDashboardMonitoring();
return (
<box
className={'dashboard-card stats-container'}

View File

@@ -1,8 +1,8 @@
import { bind } from 'astal';
import { Gtk } from 'astal/gtk3';
import Brightness from 'src/services/Brightness';
import BrightnessService from 'src/services/system/brightness';
const brightnessService = Brightness.get_default();
const brightnessService = BrightnessService.getInstance();
export const BrightnessPercentage = (): JSX.Element => {
return (

View File

@@ -1,8 +1,8 @@
import { bind } from 'astal';
import { Gtk } from 'astal/gtk3';
import Brightness from 'src/services/Brightness';
import BrightnessService from 'src/services/system/brightness';
const brightnessService = Brightness.get_default();
const brightnessService = BrightnessService.getInstance();
export const BrightnessSlider = (): JSX.Element => {
return (

View File

@@ -1,10 +1,10 @@
import DropdownMenu from '../shared/dropdown/index.js';
import { EnergyProfiles } from './profiles/index.js';
import { Brightness } from './brightness/index.js';
import options from 'src/options.js';
import { bind } from 'astal';
import { Gtk } from 'astal/gtk3';
import { RevealerTransitionMap } from 'src/lib/constants/options.js';
import { RevealerTransitionMap } from 'src/components/settings/constants.js';
import options from 'src/configuration';
const { transition } = options.menus;

View File

@@ -1,7 +1,7 @@
import { bind } from 'astal';
import { Gtk } from 'astal/gtk3';
import { uptime } from 'src/lib/variables.js';
import { renderUptime } from './helpers';
import { uptime } from 'src/services/system/uptime';
export const PowerProfileHeader = (): JSX.Element => {
return (

View File

@@ -1,13 +1,12 @@
import { bind } from 'astal';
import { Gtk } from 'astal/gtk3';
import AstalPowerProfiles from 'gi://AstalPowerProfiles?version=0.1';
import { isPrimaryClick } from 'src/lib/events/mouse';
import icons from 'src/lib/icons/icons';
import { ProfileType } from 'src/lib/types/powerprofiles.types';
import { isPrimaryClick } from 'src/lib/utils';
const powerProfilesService = AstalPowerProfiles.get_default();
import { ProfileType } from './types';
export const PowerProfiles = (): JSX.Element => {
const powerProfilesService = AstalPowerProfiles.get_default();
const powerProfiles = powerProfilesService.get_profiles();
return (

View File

@@ -0,0 +1 @@
export type ProfileType = 'balanced' | 'power-saver' | 'performance';

View File

@@ -1,12 +1,10 @@
import icons from 'src/lib/icons/icons';
import { Astal, Gtk, Widget } from 'astal/gtk3';
import { isPrimaryClick } from 'src/lib/utils';
import { bind } from 'astal';
import { isLoopActive, isShuffleActive, loopIconMap, loopTooltipMap } from './helpers';
import AstalMpris from 'gi://AstalMpris?version=0.1';
import { activePlayer, loopStatus, shuffleStatus } from 'src/shared/media';
export type LoopStatus = 'none' | 'track' | 'playlist';
import { loopStatus, activePlayer, shuffleStatus } from 'src/services/media';
import { isPrimaryClick } from 'src/lib/events/mouse';
export const Loop = (): JSX.Element => {
const className = bind(loopStatus).as((status) => {

View File

@@ -1,9 +1,9 @@
import { Astal, Gtk, Widget } from 'astal/gtk3';
import { isPrimaryClick } from 'src/lib/utils';
import { bind } from 'astal';
import { getPlaybackIcon } from './helpers';
import AstalMpris from 'gi://AstalMpris?version=0.1';
import { activePlayer, canPlay, playbackStatus } from 'src/shared/media';
import { canPlay, playbackStatus, activePlayer } from 'src/services/media';
import { isPrimaryClick } from 'src/lib/events/mouse';
export const PlayPause = (): JSX.Element => {
const className = bind(canPlay).as((canPlay) => {

View File

@@ -1,8 +1,8 @@
import { bind } from 'astal';
import { Astal, Gtk, Widget } from 'astal/gtk3';
import { isPrimaryClick } from 'src/lib/utils';
import { getNextPlayer, getPreviousPlayer } from './helpers';
import AstalMpris from 'gi://AstalMpris?version=0.1';
import { isPrimaryClick } from 'src/lib/events/mouse';
const mprisService = AstalMpris.get_default();

View File

@@ -1,8 +1,8 @@
import icons from 'src/lib/icons/icons';
import { Astal, Gtk, Widget } from 'astal/gtk3';
import { isPrimaryClick } from 'src/lib/utils';
import { bind } from 'astal';
import { activePlayer, canGoNext, canGoPrevious } from 'src/shared/media';
import { canGoNext, activePlayer, canGoPrevious } from 'src/services/media';
import { isPrimaryClick } from 'src/lib/events/mouse';
export const NextTrack = (): JSX.Element => {
const className = bind(canGoNext).as((skippable) => {

View File

@@ -1,7 +1,7 @@
import AstalMpris from 'gi://AstalMpris?version=0.1';
import { activePlayer } from 'src/shared/media';
import icons2 from 'src/lib/icons/icons2';
import { PlaybackIconMap } from 'src/lib/types/mpris.types';
import icons2 from 'src/lib/icons/icons';
import { activePlayer } from 'src/services/media';
import { PlaybackIconMap } from './types';
const mprisService = AstalMpris.get_default();

View File

@@ -0,0 +1,5 @@
import AstalMpris from 'gi://AstalMpris';
export type PlaybackIconMap = {
[key in AstalMpris.PlaybackStatus]: string;
};

View File

@@ -1,13 +1,23 @@
import { Binding } from 'astal';
import { bind, Variable } from 'astal';
import AstalMpris from 'gi://AstalMpris?version=0.1';
import { mediaArtUrl } from 'src/shared/media';
import options from 'src/options';
import options from 'src/configuration';
import { mediaArtUrl } from 'src/services/media';
const mprisService = AstalMpris.get_default();
const { tint, color } = options.theme.bar.menus.menu.media.card;
const curPlayer = Variable('');
/**
* Retrieves the background binding for the media card.
*
* This function sets up a derived variable that updates the background CSS for the media card
* based on the current theme settings for color, tint, and media art URL.
*
* @returns A Binding<string> representing the background CSS for the media card.
*/
export const getBackground = (): Binding<string> => {
return Variable.derive([bind(color), bind(tint), bind(mediaArtUrl)], (_, __, artUrl) => {
return generateAlbumArt(artUrl);
})();
};
/**
* Generates CSS for album art with a tinted background.
@@ -19,7 +29,7 @@ const curPlayer = Variable('');
*
* @returns A CSS string for the album art background.
*/
export const generateAlbumArt = (imageUrl: string): string => {
function generateAlbumArt(imageUrl: string): string {
const userTint = tint.get();
const userHexColor = color.get();
@@ -36,50 +46,4 @@ export const generateAlbumArt = (imageUrl: string): string => {
), url("${imageUrl}");`;
return css;
};
/**
* Initializes the active player hook.
*
* This function sets up a listener for changes in the MPRIS service.
* It updates the current player based on the playback status and the order of players.
*/
export const initializeActivePlayerHook = (): void => {
mprisService.connect('changed', () => {
const statusOrder = {
[AstalMpris.PlaybackStatus.PLAYING]: 1,
[AstalMpris.PlaybackStatus.PAUSED]: 2,
[AstalMpris.PlaybackStatus.STOPPED]: 3,
};
const isPlaying = mprisService
.get_players()
.find((p) => p['playbackStatus'] === AstalMpris.PlaybackStatus.PLAYING);
const playerStillExists = mprisService
.get_players()
.some((player) => curPlayer.get() === player.busName);
const nextPlayerUp = mprisService
.get_players()
.sort((a, b) => statusOrder[a.playbackStatus] - statusOrder[b.playbackStatus])[0].bus_name;
if (isPlaying || !playerStillExists) {
curPlayer.set(nextPlayerUp);
}
});
};
/**
* Retrieves the background binding for the media card.
*
* This function sets up a derived variable that updates the background CSS for the media card
* based on the current theme settings for color, tint, and media art URL.
*
* @returns A Binding<string> representing the background CSS for the media card.
*/
export const getBackground = (): Binding<string> => {
return Variable.derive([bind(color), bind(tint), bind(mediaArtUrl)], (_, __, artUrl) => {
return generateAlbumArt(artUrl);
})();
};
}

View File

@@ -27,7 +27,7 @@ export const getTimeStamp = (position: number, totalLength: number): string => {
*
* @returns A formatted time string in the format "HH:MM:SS" or "MM:SS".
*/
export const getFormattedTime = (time: number): string => {
function getFormattedTime(time: number): string {
const curHour = Math.floor(time / 3600);
const curMin = Math.floor((time % 3600) / 60);
const curSec = Math.floor(time % 60);
@@ -41,4 +41,4 @@ export const getFormattedTime = (time: number): string => {
};
return `${formatHour(curHour)}${formatTime(curMin)}:${formatTime(curSec)}`;
};
}

View File

@@ -1,7 +1,7 @@
import options from 'src/options';
import { bind, Variable } from 'astal';
import { Widget } from 'astal/gtk3';
import { activePlayer, currentPosition, timeStamp } from 'src/shared/media';
import options from 'src/configuration';
import { activePlayer, currentPosition, timeStamp } from 'src/services/media';
const { displayTimeTooltip } = options.menus.media;

View File

@@ -1,6 +1,6 @@
import options from 'src/options';
import { bind } from 'astal';
import { timeStamp } from 'src/shared/media';
import options from 'src/configuration';
import { timeStamp } from 'src/services/media';
const { displayTime } = options.menus.media;

View File

@@ -1,7 +1,7 @@
import options from 'src/options';
import { Gtk } from 'astal/gtk3';
import { bind } from 'astal';
import { mediaAlbum } from 'src/shared/media';
import { mediaAlbum } from 'src/services/media';
import options from 'src/configuration';
const { hideAlbum } = options.menus.media;

View File

@@ -1,7 +1,7 @@
import options from 'src/options';
import { Gtk } from 'astal/gtk3';
import { bind } from 'astal';
import { mediaArtist } from 'src/shared/media';
import { mediaArtist } from 'src/services/media';
import options from 'src/configuration';
const { hideAuthor } = options.menus.media;

View File

@@ -1,6 +1,6 @@
import { Gtk } from 'astal/gtk3';
import { bind } from 'astal';
import { mediaTitle } from 'src/shared/media';
import { mediaTitle } from 'src/services/media';
export const SongName = (): JSX.Element => {
return (

View File

@@ -1,12 +1,12 @@
import { bind } from 'astal';
import DropdownMenu from '../shared/dropdown/index.js';
import options from 'src/options.js';
import options from 'src/configuration';
import { MediaContainer } from './components/MediaContainer.js';
import { MediaInfo } from './components/title/index.js';
import { MediaControls } from './components/controls/index.js';
import { MediaSlider } from './components/timebar/index.js';
import { MediaTimeStamp } from './components/timelabel/index.js';
import { RevealerTransitionMap } from 'src/lib/constants/options.js';
import { RevealerTransitionMap } from 'src/components/settings/constants.js';
const { transition } = options.menus;

View File

@@ -1,105 +0,0 @@
import { bind, Variable } from 'astal';
import AstalNetwork from 'gi://AstalNetwork?version=0.1';
const networkService = AstalNetwork.get_default();
/*******************************************
* Values *
*******************************************/
export const wiredState: Variable<AstalNetwork.DeviceState> = Variable(AstalNetwork.DeviceState.UNKNOWN);
export const wiredInternet: Variable<AstalNetwork.Internet> = Variable(AstalNetwork.Internet.DISCONNECTED);
export const wiredIcon: Variable<string> = Variable('');
export const wiredSpeed: Variable<number> = Variable(0);
/*******************************************
* Bindings *
*******************************************/
let wiredStateBinding: Variable<void> | undefined;
let wiredInternetBinding: Variable<void> | undefined;
let wiredIconBinding: Variable<void> | undefined;
let wiredSpeedBinding: Variable<void> | undefined;
/**
* Retrieves the current state of the wired network.
*
* This function sets up a binding to the `state` property of the wired network service.
* If the wired network service is available, it updates the `wiredState` variable with the current state.
*/
const getWiredState = (): void => {
wiredStateBinding?.drop();
wiredStateBinding = undefined;
if (networkService.wired === null) {
wiredState.set(AstalNetwork.DeviceState.UNAVAILABLE);
return;
}
wiredStateBinding = Variable.derive([bind(networkService.wired, 'state')], (state) => {
wiredState.set(state);
});
};
/**
* Retrieves the current internet status of the wired network.
*
* This function sets up a binding to the `internet` property of the wired network service.
* If the wired network service is available, it updates the `wiredInternet` variable with the current internet status.
*/
const getWiredInternet = (): void => {
wiredInternetBinding?.drop();
wiredInternetBinding = undefined;
if (networkService.wired === null) {
return;
}
wiredInternetBinding = Variable.derive([bind(networkService.wired, 'internet')], (internet) => {
wiredInternet.set(internet);
});
};
/**
* Retrieves the current icon for the wired network.
*
* This function sets up a binding to the `iconName` property of the wired network service.
* If the wired network service is available, it updates the `wiredIcon` variable with the current icon name.
*/
const getWiredIcon = (): void => {
wiredIconBinding?.drop();
wiredIconBinding = undefined;
if (networkService.wired === null) {
wiredIcon.set('network-wired-symbolic');
return;
}
wiredIconBinding = Variable.derive([bind(networkService.wired, 'iconName')], (icon) => {
wiredIcon.set(icon);
});
};
/**
* Retrieves the current speed of the wired network.
*
* This function sets up a binding to the `speed` property of the wired network service.
* If the wired network service is available, it updates the `wiredSpeed` variable with the current speed.
*/
const getWiredSpeed = (): void => {
wiredSpeedBinding?.drop();
wiredSpeedBinding = undefined;
if (networkService.wired === null) {
return;
}
wiredSpeedBinding = Variable.derive([bind(networkService.wired, 'speed')], (speed) => {
wiredSpeed.set(speed);
});
};
Variable.derive([bind(networkService, 'wired')], () => {
getWiredState();
getWiredInternet();
getWiredIcon();
getWiredSpeed();
});

View File

@@ -1,8 +1,10 @@
import { Gtk } from 'astal/gtk3';
import { bind } from 'astal/binding';
import AstalNetwork from 'gi://AstalNetwork?version=0.1';
import { DEVICE_STATES } from 'src/lib/constants/network';
import { wiredIcon, wiredInternet, wiredSpeed, wiredState } from './helpers';
import { DEVICE_STATES } from 'src/services/network/types';
import { NetworkService } from 'src/services/network';
const networkService = NetworkService.getInstance();
export const Ethernet = (): JSX.Element => {
return (
@@ -15,13 +17,13 @@ export const Ethernet = (): JSX.Element => {
<box className={'network-element-item'}>
<box halign={Gtk.Align.START}>
<icon
className={bind(wiredState).as((state) => {
className={bind(networkService.ethernet.wiredState).as((state) => {
return `network-icon ethernet ${state === AstalNetwork.DeviceState.ACTIVATED ? 'active' : ''}`;
})}
tooltipText={bind(wiredInternet).as((internet) => {
tooltipText={bind(networkService.ethernet.wiredInternet).as((internet) => {
return internet.toString();
})}
icon={bind(wiredIcon)}
icon={bind(networkService.ethernet.wiredIcon)}
/>
<box className={'connection-container'} vertical>
<label
@@ -29,7 +31,7 @@ export const Ethernet = (): JSX.Element => {
halign={Gtk.Align.START}
truncate
wrap
label={bind(wiredSpeed).as((speed) => {
label={bind(networkService.ethernet.wiredSpeed).as((speed) => {
return `Ethernet Connection (${speed} Mbps)`;
})}
/>
@@ -38,7 +40,7 @@ export const Ethernet = (): JSX.Element => {
halign={Gtk.Align.START}
truncate
wrap
label={bind(wiredState).as((state) => {
label={bind(networkService.ethernet.wiredState).as((state) => {
return DEVICE_STATES[state];
})}
/>

View File

@@ -1,11 +1,11 @@
import DropdownMenu from '../shared/dropdown/index.js';
import { Ethernet } from './ethernet/index.js';
import { Wifi } from './wifi/index.js';
import options from 'src/options.js';
import { bind } from 'astal';
import { NoWifi } from './wifi/WirelessAPs/NoWifi.js';
import { RevealerTransitionMap } from 'src/lib/constants/options.js';
import { RevealerTransitionMap } from 'src/components/settings/constants.js';
import AstalNetwork from 'gi://AstalNetwork?version=0.1';
import options from 'src/configuration';
const networkService = AstalNetwork.get_default();

View File

@@ -0,0 +1 @@
export type WifiIcon = '󰤩' | '󰤨' | '󰤪' | '󰤨' | '󰤩' | '󰤮' | '󰤨' | '󰤥' | '󰤢' | '󰤟' | '󰤯';

View File

@@ -1,36 +0,0 @@
import { WifiIcon } from 'src/lib/types/network.types';
/**
* Retrieves the appropriate WiFi icon based on the provided icon name.
*
* This function returns a WiFi icon based on the given icon name. If the icon name is not provided,
* it returns a default icon. It uses a predefined mapping of device icon names to WiFi icons.
*
* @param iconName The name of the icon to look up. If not provided, a default icon is returned.
*
* @returns The corresponding WiFi icon as a string.
*/
const getWifiIcon = (iconName?: string): WifiIcon => {
if (iconName === undefined) {
return '󰤫' as WifiIcon;
}
const deviceIconMap: [string, WifiIcon][] = [
['network-wireless-acquiring', '󰤩'],
['network-wireless-connected', '󰤨'],
['network-wireless-encrypted', '󰤪'],
['network-wireless-hotspot', '󰤨'],
['network-wireless-no-route', '󰤩'],
['network-wireless-offline', '󰤮'],
['network-wireless-signal-excellent', '󰤨'],
['network-wireless-signal-good', '󰤥'],
['network-wireless-signal-ok', '󰤢'],
['network-wireless-signal-weak', '󰤟'],
['network-wireless-signal-none', '󰤯'],
];
const foundMatch = deviceIconMap.find((icon) => RegExp(icon[0]).test(iconName.toLowerCase()));
return foundMatch ? foundMatch[1] : '󰤨';
};
export { getWifiIcon };

View File

@@ -1,10 +1,11 @@
import { Gdk, Gtk } from 'astal/gtk3';
import { isPrimaryClick, Notify } from 'src/lib/utils';
import { Gtk } from 'astal/gtk3';
import AstalNetwork from 'gi://AstalNetwork?version=0.1';
import { execAsync, Variable } from 'astal';
import { Variable } from 'astal';
import { isPrimaryClick } from 'src/lib/events/mouse';
import { handlePasswordInput } from './helpers';
export const PasswordInput = ({ connecting, staging }: PasswordInputProps): JSX.Element => {
const shouldMaskPassword = true;
const showPassword = true;
return (
<box className="network-password-input-container" halign={Gtk.Align.FILL} hexpand>
@@ -12,33 +13,13 @@ export const PasswordInput = ({ connecting, staging }: PasswordInputProps): JSX.
className="network-password-input"
hexpand
halign={Gtk.Align.START}
visibility={shouldMaskPassword}
visibility={!showPassword}
placeholderText="Enter Password"
onKeyPressEvent={(self, event) => {
const keyPressed = event.get_keyval()[1];
if (keyPressed === Gdk.KEY_Return) {
connecting.set(staging.get()?.bssid ?? '');
const connectCommand = `nmcli device wifi connect "${staging.get()?.ssid}" password "${self.text}"`;
execAsync(connectCommand)
.catch((err) => {
connecting.set('');
Notify({
summary: 'Network',
body: err.message,
});
})
.then(() => {
connecting.set('');
staging.set({} as AstalNetwork.AccessPoint);
});
self.text = '';
}
handlePasswordInput(self, event, staging);
}}
setup={(self) => {
setTimeout(() => self.grab_focus(), 100);
}}
/>
<button
@@ -47,7 +28,7 @@ export const PasswordInput = ({ connecting, staging }: PasswordInputProps): JSX.
onClick={(_, event) => {
if (isPrimaryClick(event)) {
connecting.set('');
staging.set({} as AstalNetwork.AccessPoint);
staging.set(undefined);
}
}}
>

View File

@@ -0,0 +1,35 @@
import { Variable } from 'astal';
import { Gdk } from 'astal/gtk3';
import { Entry } from 'astal/gtk3/widget';
import AstalNetwork from 'gi://AstalNetwork?version=0.1';
import { NetworkService } from 'src/services/network';
import { SystemUtilities } from 'src/core/system/SystemUtilities';
const networkService = NetworkService.getInstance();
export function handlePasswordInput(
self: Entry,
event: Gdk.Event,
staging: Variable<AstalNetwork.AccessPoint | undefined>,
): void {
const keyPressed = event.get_keyval()[1];
const accessPoint = staging.get();
const password = self.text;
if (keyPressed !== Gdk.KEY_Return || password.length === 0 || !accessPoint) {
return;
}
networkService.wifi.connectToAPWithPassword(accessPoint, password).catch((err) => {
if (self.is_visible() && self.get_realized()) {
self.grab_focus();
}
SystemUtilities.notify({
summary: 'Network',
body: err.message,
});
self.text = '';
});
}

View File

@@ -2,24 +2,34 @@ import { bind } from 'astal/binding';
import { Variable } from 'astal';
import { AccessPoint } from './AccessPoint';
import { PasswordInput } from './PasswordInput';
import { connecting, staging } from '../WirelessAPs/helpers';
import AstalNetwork from 'gi://AstalNetwork?version=0.1';
import { NetworkService } from 'src/services/network';
const networkService = AstalNetwork.get_default();
const networkService = NetworkService.getInstance();
const astalNetwork = AstalNetwork.get_default();
export const APStaging = (): JSX.Element => {
const stagingBinding = Variable.derive([bind(networkService, 'wifi'), bind(staging)], () => {
if (staging.get()?.ssid === undefined) {
return <box />;
}
const stagingBinding = Variable.derive(
[bind(astalNetwork, 'wifi'), bind(networkService.wifi.staging)],
() => {
if (networkService.wifi.staging.get()?.ssid === undefined) {
return <box />;
}
return (
<box className="network-element-item staging" vertical>
<AccessPoint connecting={connecting} staging={staging} />
<PasswordInput connecting={connecting} staging={staging} />
</box>
);
});
return (
<box className="network-element-item staging" vertical>
<AccessPoint
connecting={networkService.wifi.connecting}
staging={networkService.wifi.staging}
/>
<PasswordInput
connecting={networkService.wifi.connecting}
staging={networkService.wifi.staging}
/>
</box>
);
},
);
return (
<box
className="wap-staging"

View File

@@ -1,10 +1,11 @@
import { Gtk } from 'astal/gtk3';
import { bind } from 'astal';
import { isPrimaryClick } from 'src/lib/utils';
import { isScanning } from './helpers';
import AstalNetwork from 'gi://AstalNetwork?version=0.1';
import { isPrimaryClick } from 'src/lib/events/mouse';
import { NetworkService } from 'src/services/network';
const networkService = AstalNetwork.get_default();
const networkService = NetworkService.getInstance();
const astalNetwork = AstalNetwork.get_default();
export const RefreshButton = (): JSX.Element => {
return (
@@ -14,12 +15,14 @@ export const RefreshButton = (): JSX.Element => {
halign={Gtk.Align.END}
onClick={(_, event) => {
if (isPrimaryClick(event)) {
networkService.wifi?.scan();
astalNetwork.wifi?.scan();
}
}}
>
<icon
className={bind(isScanning).as((scanning) => (scanning ? 'spinning-icon' : ''))}
className={bind(networkService.wifi.isScanning).as((scanning) =>
scanning ? 'spinning-icon' : '',
)}
icon="view-refresh-symbolic"
/>
</button>

View File

@@ -1,20 +0,0 @@
import { bind, Variable } from 'astal';
import AstalNetwork from 'gi://AstalNetwork?version=0.1';
const networkService = AstalNetwork.get_default();
export const isScanning: Variable<boolean> = Variable(false);
let scanningBinding: Variable<void> | undefined;
Variable.derive([bind(networkService, 'wifi')], () => {
scanningBinding?.drop();
scanningBinding = undefined;
if (networkService.wifi === null) {
return;
}
scanningBinding = Variable.derive([bind(networkService.wifi, 'scanning')], (scanning) => {
isScanning.set(scanning);
});
});

View File

@@ -1,19 +1,19 @@
import { Variable } from 'astal';
import AstalNetwork from 'gi://AstalNetwork?version=0.1';
import { getWifiIcon } from '../../utils';
import { connectToAP, getWifiStatus, isDisconnecting, isApEnabled, isApActive } from './helpers';
import { NetworkService } from 'src/services/network';
import { Gtk } from 'astal/gtk3';
import Spinner from 'src/components/shared/Spinner';
const networkService = AstalNetwork.get_default();
const networkService = NetworkService.getInstance();
const astalNetwork = AstalNetwork.get_default();
export const AccessPoint = ({ connecting, accessPoint }: AccessPointProps): JSX.Element => {
const ConnectionIcon = (): JSX.Element => {
return (
<label
valign={Gtk.Align.START}
className={`network-icon wifi ${isApActive(accessPoint) ? 'active' : ''} txt-icon`}
label={getWifiIcon(accessPoint.iconName)}
className={`network-icon wifi ${networkService.wifi.isApActive(accessPoint) ? 'active' : ''} txt-icon`}
label={networkService.getWifiIcon(accessPoint.iconName)}
/>
);
};
@@ -28,11 +28,16 @@ export const AccessPoint = ({ connecting, accessPoint }: AccessPointProps): JSX.
wrap
label={accessPoint.ssid ?? ''}
/>
<revealer revealChild={isApActive(accessPoint) && isApEnabled(networkService.wifi?.state)}>
<revealer
revealChild={
networkService.wifi.isApActive(accessPoint) &&
networkService.wifi.isApEnabled(astalNetwork.wifi?.state)
}
>
<label
className="connection-status dim"
halign={Gtk.Align.START}
label={getWifiStatus()}
label={networkService.wifi.getWifiStatus()}
/>
</revealer>
</box>
@@ -44,7 +49,9 @@ export const AccessPoint = ({ connecting, accessPoint }: AccessPointProps): JSX.
<revealer
halign={Gtk.Align.END}
valign={Gtk.Align.CENTER}
revealChild={accessPoint.bssid === connecting.get() || isDisconnecting(accessPoint)}
revealChild={
accessPoint.bssid === connecting.get() || networkService.wifi.isDisconnecting(accessPoint)
}
>
<Spinner
className="spinner wap"
@@ -62,7 +69,7 @@ export const AccessPoint = ({ connecting, accessPoint }: AccessPointProps): JSX.
<button
className="network-element-item"
onClick={(_, event) => {
connectToAP(accessPoint, event);
networkService.wifi.connectToAP(accessPoint, event);
}}
>
<box hexpand>

View File

@@ -1,7 +1,9 @@
import { Variable } from 'astal';
import { disconnectFromAP, forgetAP, isApActive } from './helpers';
import AstalNetwork from 'gi://AstalNetwork?version=0.1';
import { Gtk } from 'astal/gtk3';
import { NetworkService } from 'src/services/network';
const networkService = NetworkService.getInstance();
export const Controls = ({ connecting, accessPoint }: ControlsProps): JSX.Element => {
const DisconnectButton = (): JSX.Element => {
@@ -9,7 +11,7 @@ export const Controls = ({ connecting, accessPoint }: ControlsProps): JSX.Elemen
<button
className="menu-icon-button network disconnect"
onClick={(_, event) => {
disconnectFromAP(accessPoint, event);
networkService.wifi.disconnectFromAP(accessPoint, event);
}}
>
<label
@@ -27,7 +29,7 @@ export const Controls = ({ connecting, accessPoint }: ControlsProps): JSX.Elemen
className="menu-icon-button network disconnect"
tooltipText="Delete/Forget Network"
onClick={(_, event) => {
forgetAP(accessPoint, event);
networkService.wifi.forgetAP(accessPoint, event);
}}
>
<label className="txt-icon delete-network" label="󰚃" />
@@ -37,7 +39,9 @@ export const Controls = ({ connecting, accessPoint }: ControlsProps): JSX.Elemen
return (
<revealer
revealChild={accessPoint.bssid !== connecting.get() && isApActive(accessPoint)}
revealChild={
accessPoint.bssid !== connecting.get() && networkService.wifi.isApActive(accessPoint)
}
valign={Gtk.Align.START}
>
<box className={'network-element-controls-container'}>

View File

@@ -1,318 +0,0 @@
import { bind, execAsync, Variable } from 'astal';
import { Astal } from 'astal/gtk3';
import AstalNetwork from 'gi://AstalNetwork?version=0.1';
import { DEVICE_STATES } from 'src/lib/constants/network';
import { isPrimaryClick, Notify } from 'src/lib/utils';
const networkService = AstalNetwork.get_default();
export const isWifiEnabled: Variable<boolean> = Variable(false);
export const wifiAccessPoints: Variable<AstalNetwork.AccessPoint[]> = Variable([]);
let wifiEnabledBinding: Variable<void> | undefined;
let accessPointBinding: Variable<void> | undefined;
export const staging = Variable<AstalNetwork.AccessPoint | undefined>(undefined);
export const connecting = Variable<string>('');
/**
* Checks if WiFi is enabled and updates the `isWifiEnabled` variable.
*
* This function sets up a binding to the `enabled` property of the WiFi service.
* If the WiFi service is available, it updates the `isWifiEnabled` variable based on the enabled state.
*/
const wifiEnabled = (): void => {
wifiEnabledBinding?.drop();
wifiEnabledBinding = undefined;
if (networkService.wifi === null) {
return;
}
wifiEnabledBinding = Variable.derive([bind(networkService.wifi, 'enabled')], (isEnabled) => {
isWifiEnabled.set(isEnabled);
});
};
/**
* Updates the list of WiFi access points.
*
* This function sets up a binding to the `accessPoints` property of the WiFi service.
* If the WiFi service is available, it updates the `wifiAccessPoints` variable with the list of access points.
*/
const accessPoints = (): void => {
accessPointBinding?.drop();
accessPointBinding = undefined;
if (networkService.wifi === null) {
return;
}
Variable.derive([bind(networkService.wifi, 'accessPoints')], (axsPoints) => {
wifiAccessPoints.set(axsPoints);
});
};
/**
* Removes duplicate access points based on their SSID.
*
* This function iterates through the list of access points and removes duplicates based on their SSID.
* It returns an array of deduplicated access points.
*
* @returns An array of deduplicated access points.
*/
const dedupeWAPs = (): AstalNetwork.AccessPoint[] => {
if (networkService.wifi === null) {
return [];
}
const WAPs = networkService.wifi.get_access_points();
const dedupMap: Record<string, AstalNetwork.AccessPoint> = {};
WAPs.forEach((item: AstalNetwork.AccessPoint) => {
if (item.ssid !== null && !Object.prototype.hasOwnProperty.call(dedupMap, item.ssid)) {
dedupMap[item.ssid] = item;
}
});
return Object.keys(dedupMap).map((itm) => dedupMap[itm]);
};
/**
* Determines if a given access point is currently in the staging area.
*
* This function checks if the provided access point is in the staging area by comparing its BSSID with the BSSID of the staged access point.
*
* @param wap The access point to check.
*
* @returns True if the access point is in staging; otherwise, false.
*/
const isInStaging = (wap: AstalNetwork.AccessPoint): boolean => {
const wapInStaging = staging.get();
if (wapInStaging === undefined) {
return false;
}
return wap.bssid === wapInStaging.bssid;
};
/**
* Retrieves a list of filtered wireless access points by removing duplicates and excluding specific entries.
*
* This function filters the list of access points by removing duplicates and excluding access points with the SSID 'Unknown' or those in the staging area.
* It also sorts the access points by their signal strength and active status.
*
* @returns A filtered array of wireless access points.
*/
export const getFilteredWirelessAPs = (): AstalNetwork.AccessPoint[] => {
const dedupedWAPs = dedupeWAPs();
const filteredWAPs = dedupedWAPs
.filter((ap: AstalNetwork.AccessPoint) => {
return ap.ssid !== 'Unknown' && !isInStaging(ap);
})
.sort((a: AstalNetwork.AccessPoint, b: AstalNetwork.AccessPoint) => {
if (isApActive(a)) {
return -1;
}
if (isApActive(b)) {
return 1;
}
return b.strength - a.strength;
});
return filteredWAPs;
};
/**
* Determines whether the device is in an active state.
*
* This function checks if the provided device state is active by comparing it with the disconnected, unavailable, and failed states.
*
* @param state The current state of the device.
*
* @returns True if the device is in an active state; otherwise, false.
*/
export const isApEnabled = (state: AstalNetwork.DeviceState | undefined): boolean => {
if (state == null) {
return false;
}
return !(
state === AstalNetwork.DeviceState.DISCONNECTED ||
state === AstalNetwork.DeviceState.UNAVAILABLE ||
state === AstalNetwork.DeviceState.FAILED
);
};
/**
* Checks if the given access point is the currently active one.
*
* This function compares the SSID of the provided access point with the SSID of the active access point in the WiFi service.
*
* @param accessPoint The access point to check.
*
* @returns True if the access point is active; otherwise, false.
*/
export const isApActive = (accessPoint: AstalNetwork.AccessPoint): boolean => {
return accessPoint.ssid === networkService.wifi?.activeAccessPoint?.ssid;
};
/**
* Checks if the specified access point is in the process of disconnecting.
*
* This function checks if the provided access point is the active one and if the WiFi service state is deactivating.
*
* @param accessPoint The access point to check.
*
* @returns True if the access point is disconnecting; otherwise, false.
*/
export const isDisconnecting = (accessPoint: AstalNetwork.AccessPoint): boolean => {
if (isApActive(accessPoint)) {
return networkService.wifi?.state === AstalNetwork.DeviceState.DEACTIVATING;
}
return false;
};
/**
* Extracts the connection ID associated with a given SSID from the `nmcli` command output.
*
* This function parses the output of the `nmcli` command to find the connection ID associated with the provided SSID.
*
* @param ssid The SSID of the network.
* @param nmcliOutput The output string from the `nmcli` command.
*
* @returns The connection ID if found; otherwise, undefined.
*/
export const getIdFromSsid = (ssid: string, nmcliOutput: string): string | undefined => {
const lines = nmcliOutput.trim().split('\n');
for (const line of lines) {
const columns = line.trim().split(/\s{2,}/);
if (columns[0].includes(ssid)) {
return columns[1];
}
}
};
/**
* Retrieves the current Wi-Fi status based on the network service state.
*
* This function returns a string representing the current Wi-Fi status based on the state of the WiFi service.
*
* @returns A string representing the current Wi-Fi status.
*/
export const getWifiStatus = (): string => {
const wifiState = networkService.wifi?.state;
if (wifiState !== null) {
return DEVICE_STATES[wifiState];
}
return DEVICE_STATES[AstalNetwork.DeviceState.UNKNOWN];
};
/**
* Initiates a connection to the specified access point.
*
* This function attempts to connect to the provided access point using the `nmcli` command.
* It handles connection attempts, updates the `connecting` variable, and manages errors.
*
* @param accessPoint The access point to connect to.
* @param event The click event triggering the connection.
*/
export const connectToAP = (accessPoint: AstalNetwork.AccessPoint, event: Astal.ClickEvent): void => {
if (accessPoint.bssid === connecting.get() || isApActive(accessPoint) || !isPrimaryClick(event)) {
return;
}
connecting.set(accessPoint.bssid || '');
execAsync(`nmcli device wifi connect ${accessPoint.bssid}`)
.then(() => {
connecting.set('');
staging.set({} as AstalNetwork.AccessPoint);
})
.catch((err: Error) => {
connecting.set('');
if (err.message.toLowerCase().includes('secrets were required, but not provided')) {
staging.set(accessPoint);
} else {
Notify({
summary: 'Network',
body: err.message,
});
}
});
};
/**
* Disconnects from the specified access point.
*
* This function attempts to disconnect from the provided access point using the `nmcli` command.
* It handles disconnection attempts, updates the `connecting` variable, and manages errors.
*
* @param accessPoint The access point to disconnect from.
* @param event The click event triggering the disconnection.
*/
export const disconnectFromAP = (accessPoint: AstalNetwork.AccessPoint, event: Astal.ClickEvent): void => {
if (!isPrimaryClick(event)) {
return;
}
connecting.set(accessPoint.bssid || '');
execAsync('nmcli connection show --active').then((res: string) => {
const connectionId = getIdFromSsid(accessPoint.ssid || '', res);
if (connectionId === undefined) {
console.error(`Error while disconnecting "${accessPoint.ssid}": Connection ID not found`);
return;
}
execAsync(`nmcli connection down ${connectionId} "${accessPoint.ssid}"`)
.then(() => {
connecting.set('');
})
.catch((err: unknown) => {
connecting.set('');
console.error(`Error while disconnecting "${accessPoint.ssid}": ${err}`);
});
});
};
/**
* Forgets the specified access point by deleting its connection.
*
* This function attempts to forget the provided access point by deleting its connection using the `nmcli` command.
* It handles the forget action, updates the `connecting` variable, and manages errors.
*
* @param accessPoint The access point to forget.
* @param event The click event triggering the forget action.
*/
export const forgetAP = (accessPoint: AstalNetwork.AccessPoint, event: Astal.ClickEvent): void => {
if (!isPrimaryClick(event)) {
return;
}
connecting.set(accessPoint.bssid || '');
execAsync('nmcli connection show --active').then((res: string) => {
const connectionId = getIdFromSsid(accessPoint.ssid || '', res);
if (connectionId === undefined) {
console.error(`Error while forgetting "${accessPoint.ssid}": Connection ID not found`);
return;
}
execAsync(`nmcli connection delete ${connectionId} "${accessPoint.ssid}"`)
.then(() => {
connecting.set('');
})
.catch((err: unknown) => {
connecting.set('');
console.error(`Error while forgetting "${accessPoint.ssid}": ${err}`);
});
});
};
Variable.derive([bind(networkService, 'wifi')], () => {
wifiEnabled();
accessPoints();
});

View File

@@ -1,18 +1,25 @@
import { Gtk } from 'astal/gtk3';
import { bind } from 'astal/binding';
import AstalNetwork from 'gi://AstalNetwork?version=0.1';
import { connecting, getFilteredWirelessAPs, isWifiEnabled, staging, wifiAccessPoints } from './helpers';
import { AccessPoint } from './AccessPoint';
import { Controls } from './Controls';
import { Variable } from 'astal';
import { NetworkService } from 'src/services/network';
const networkService = NetworkService.getInstance();
export const WirelessAPs = (): JSX.Element => {
const wapBinding = Variable.derive(
[bind(staging), bind(connecting), bind(wifiAccessPoints), bind(isWifiEnabled)],
[
bind(networkService.wifi.staging),
bind(networkService.wifi.connecting),
bind(networkService.wifi.wifiAccessPoints),
bind(networkService.wifi.isWifiEnabled),
],
() => {
const filteredWAPs = getFilteredWirelessAPs();
const filteredWAPs = networkService.wifi.getFilteredWirelessAPs();
if (filteredWAPs.length <= 0 && staging.get() === undefined) {
if (filteredWAPs.length <= 0 && networkService.wifi.staging.get() === undefined) {
return (
<label
className={'waps-not-found dim'}
@@ -30,8 +37,11 @@ export const WirelessAPs = (): JSX.Element => {
{filteredWAPs.map((ap: AstalNetwork.AccessPoint) => {
return (
<box className={'network-element-item'}>
<AccessPoint connecting={connecting} accessPoint={ap} />
<Controls connecting={connecting} accessPoint={ap} />
<AccessPoint
connecting={networkService.wifi.connecting}
accessPoint={ap}
/>
<Controls connecting={networkService.wifi.connecting} accessPoint={ap} />
</box>
);
})}

View File

@@ -1,8 +1,8 @@
import { bind } from 'astal';
import AstalNotifd from 'gi://AstalNotifd?version=0.1';
import { clearNotifications, removingNotifications } from 'src/shared/notification';
import { isPrimaryClick } from 'src/lib/utils';
import options from 'src/options';
import options from 'src/configuration';
import { isPrimaryClick } from 'src/lib/events/mouse';
import { clearNotifications, removingNotifications } from 'src/lib/shared/notifications';
const notifdService = AstalNotifd.get_default();

View File

@@ -1,6 +1,6 @@
import { bind, Variable } from 'astal';
import AstalNotifd from 'gi://AstalNotifd?version=0.1';
import options from 'src/options';
import options from 'src/configuration';
const { displayedTotal } = options.notifications;
const notifdService = AstalNotifd.get_default();

View File

@@ -2,10 +2,10 @@ import DropdownMenu from '../shared/dropdown/index.js';
import { Controls } from './controls/index.js';
import { NotificationsContainer } from './notification/index.js';
import { NotificationPager } from './pager/index.js';
import options from 'src/options.js';
import { handlePageBoundaries } from './helpers.js';
import { bind, Variable } from 'astal';
import { RevealerTransitionMap } from 'src/lib/constants/options.js';
import { RevealerTransitionMap } from 'src/components/settings/constants.js';
import options from 'src/configuration';
const { transition } = options.menus;

View File

@@ -1,10 +1,10 @@
import options from 'src/options.js';
import { filterNotifications } from 'src/lib/shared/notifications.js';
import AstalNotifd from 'gi://AstalNotifd?version=0.1';
import { Gtk } from 'astal/gtk3';
import { bind, Variable } from 'astal';
import { NotificationCard } from 'src/components/notifications/Notification.js';
import { Placeholder } from './Placeholder';
import { NotificationCard } from 'src/components/notifications/Notification';
import options from 'src/configuration';
import { filterNotifications } from 'src/lib/shared/notifications';
const notifdService = AstalNotifd.get_default();

View File

@@ -1,7 +1,7 @@
import { Variable } from 'astal';
import { Gtk } from 'astal/gtk3';
import AstalNotifd from 'gi://AstalNotifd?version=0.1';
import { isPrimaryClick } from 'src/lib/utils';
import { isPrimaryClick } from 'src/lib/events/mouse';
export const FirstPageButton = ({ curPage, currentPage }: FirstPageButtonProps): JSX.Element => {
return (

View File

@@ -1,8 +1,8 @@
import { bind, Variable } from 'astal';
import { Gtk } from 'astal/gtk3';
import AstalNotifd from 'gi://AstalNotifd?version=0.1';
import options from 'src/options';
import { FirstPageButton, LastPageButton, NextPageButton, PreviousPageButton } from './Buttons';
import options from 'src/configuration';
const notifdService = AstalNotifd.get_default();

View File

@@ -1,7 +1,7 @@
import options from 'src/options';
import { execAsync, GObject, property, register } from 'astal';
import { App } from 'astal/gtk3';
import { Action } from 'src/lib/types/power.types';
import options from 'src/configuration';
import { Action } from '../types';
const { sleep, reboot, logout, shutdown } = options.menus.dashboard.powermenu;
@register({ GTypeName: 'PowerMenu' })

View File

@@ -1,12 +1,12 @@
import PopupWindow from '../shared/popup/index.js';
import powermenu from './helpers/actions.js';
import options from 'src/options.js';
import { isPrimaryClick } from 'src/lib/utils.js';
import icons from 'src/lib/icons/icons.js';
import { bind } from 'astal';
import { Gtk } from 'astal/gtk3';
import { RevealerTransitionMap } from 'src/lib/constants/options.js';
import { Action } from 'src/lib/types/power.types.js';
import { RevealerTransitionMap } from 'src/components/settings/constants.js';
import options from 'src/configuration';
import { isPrimaryClick } from 'src/lib/events/mouse';
import { Action } from './types';
const { transition } = options.menus;

View File

@@ -0,0 +1 @@
export type Action = 'sleep' | 'reboot' | 'logout' | 'shutdown';

View File

@@ -1,9 +1,9 @@
import { capitalizeFirstLetter } from 'src/lib/utils';
import options from 'src/options';
import options from 'src/configuration';
import powermenu from '../power/helpers/actions';
import { App, Gtk } from 'astal/gtk3';
import { bind, execAsync } from 'astal';
import { PowerOptions } from 'src/lib/options/options.types';
import { PowerOptions } from 'src/lib/options/types';
import { capitalizeFirstLetter } from 'src/lib/string/formatters';
const { confirmation, shutdown, logout, sleep, reboot, showLabel } = options.menus.power;

View File

@@ -1,8 +1,8 @@
import { bind } from 'astal';
import DropdownMenu from '../shared/dropdown/index.js';
import { PowerButton } from './button.js';
import options from 'src/options.js';
import { RevealerTransitionMap } from 'src/lib/constants/options.js';
import options from 'src/configuration';
import { RevealerTransitionMap } from 'src/components/settings/constants.js';
export default (): JSX.Element => {
return (

View File

@@ -1,5 +1,5 @@
import { App } from 'astal/gtk3';
import { BarEventMarginsProps, EventBoxPaddingProps } from '../types';
import { BarEventMarginsProps, EventBoxPaddingProps } from './types';
const EventBoxPadding = ({ className, windowName }: EventBoxPaddingProps): JSX.Element => {
return (

View File

@@ -1,6 +1,6 @@
import { DropdownMenuList } from 'src/lib/options/options.types';
import { calculateMenuPosition } from './locationHandler';
import { DropdownMenuList } from 'src/lib/options/types';
import { App, Gtk } from 'astal/gtk3';
import { calculateMenuPosition } from './locationHandler';
/**
* Handles the realization of a dropdown menu.

View File

@@ -1,5 +1,5 @@
import options from 'src/options';
import { globalEventBoxes } from 'src/shared/dropdown';
import options from 'src/configuration';
import { globalEventBoxes } from 'src/lib/events/dropdown';
import { GLib } from 'astal';
import { EventBox } from 'astal/gtk3/widget';
import AstalHyprland from 'gi://AstalHyprland?version=0.1';

View File

@@ -0,0 +1,11 @@
import { BarLocation } from 'src/lib/options/types';
export type EventBoxPaddingProps = {
className: string;
windowName: string;
};
export type BarEventMarginsProps = {
windowName: string;
location?: BarLocation;
};

View File

@@ -1,11 +1,10 @@
import options from 'src/options';
import { BarEventMargins } from './eventBoxes/index';
import { globalEventBoxes } from 'src/shared/dropdown';
import options from 'src/configuration';
import { BarEventMargins } from './helpers/eventBoxes';
import { globalEventBoxes } from 'src/lib/events/dropdown';
import { bind } from 'astal';
import { App, Astal, Gdk, Gtk } from 'astal/gtk3';
import { Revealer } from 'astal/gtk3/widget';
import { locationMap } from 'src/lib/types/defaults/bar.types';
import { DropdownMenuProps } from 'src/lib/types/dropdownmenu.types';
import { DropdownMenuProps, LocationMap } from './types';
const { location } = options.theme.bar;
@@ -16,6 +15,11 @@ export default ({
exclusivity = Astal.Exclusivity.IGNORE,
...props
}: DropdownMenuProps): JSX.Element => {
const locationMap: LocationMap = {
top: Astal.WindowAnchor.TOP,
bottom: Astal.WindowAnchor.BOTTOM,
};
return (
<window
name={name}

View File

@@ -1,11 +1,17 @@
import { BarLocation } from 'src/lib/options/options.types';
import { Astal, Gtk } from 'astal/gtk3';
import { Binding } from 'astal';
import { BarLocation } from 'src/lib/options/types';
export type EventBoxPaddingProps = {
className: string;
windowName: string;
};
export interface DropdownMenuProps {
name: string;
child?: JSX.Element | JSX.Element[] | Binding<JSX.Element | undefined>;
layout?: string;
transition?: Gtk.RevealerTransitionType | Binding<Gtk.RevealerTransitionType>;
exclusivity?: Astal.Exclusivity;
fixed?: boolean;
onDestroy?: () => void;
}
export type BarEventMarginsProps = {
windowName: string;
location?: BarLocation;
export type LocationMap = {
[key in BarLocation]: Astal.WindowAnchor;
};

View File

@@ -1,15 +1,9 @@
import { App, Astal, Gdk, Gtk } from 'astal/gtk3';
import { WINDOW_LAYOUTS } from 'src/shared/window';
import { POPUP_LAYOUTS } from 'src/components/menus/shared/popup/layouts';
import { EventBox, Revealer } from 'astal/gtk3/widget';
import {
PaddingProps,
PopupRevealerProps,
LayoutFunction,
PopupWindowProps,
} from 'src/lib/types/popupwindow.types';
import { Layouts } from 'src/lib/types/widget.types';
import { LayoutFunction, Layouts, PaddingProps, PopupRevealerProps, PopupWindowProps } from './types';
export const Padding = ({ name, opts }: PaddingProps): JSX.Element => (
const Padding = ({ name, opts }: PaddingProps): JSX.Element => (
<eventbox
className={opts?.className ?? ''}
hexpand
@@ -119,7 +113,7 @@ const Layout: LayoutFunction = (name, child, transition) => ({
});
const isValidLayout = (layout: string): layout is Layouts => {
return WINDOW_LAYOUTS.includes(layout);
return POPUP_LAYOUTS.includes(layout);
};
export default ({

View File

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

View File

@@ -0,0 +1,52 @@
import { Binding } from 'astal';
import { Gtk, Astal } from 'astal/gtk3';
import { WindowProps } from 'astal/gtk3/widget';
export type Layouts =
| 'center'
| 'top'
| 'top-right'
| 'top-center'
| 'top-left'
| 'bottom-left'
| 'bottom-center'
| 'bottom-right';
export interface PopupWindowProps extends WindowProps {
name: string;
child?: JSX.Element;
layout?: Layouts;
transition?: Gtk.RevealerTransitionType | Binding<Gtk.RevealerTransitionType>;
exclusivity?: Astal.Exclusivity;
}
export type LayoutFunction = (
name: string,
child: JSX.Element,
transition: Gtk.RevealerTransitionType | Binding<Gtk.RevealerTransitionType>,
) => {
center: () => JSX.Element;
top: () => JSX.Element;
'top-right': () => JSX.Element;
'top-center': () => JSX.Element;
'top-left': () => JSX.Element;
'bottom-left': () => JSX.Element;
'bottom-center': () => JSX.Element;
'bottom-right': () => JSX.Element;
};
type Opts = {
className: string;
vexpand: boolean;
};
export type PaddingProps = {
name: string;
opts?: Opts;
};
export type PopupRevealerProps = {
name: string;
child: JSX.Element;
transition: Gtk.RevealerTransitionType | Binding<Gtk.RevealerTransitionType>;
};