Merge branch 'master' into patch-1

This commit is contained in:
orangc
2024-12-24 16:33:42 +03:00
committed by GitHub
36 changed files with 540 additions and 101 deletions

View File

@@ -72,7 +72,13 @@
# Define .overlay to expose the package as pkgs.hyprpanel based on the system # Define .overlay to expose the package as pkgs.hyprpanel based on the system
overlay = final: prev: { overlay = final: prev: {
hyprpanel = self.packages.${prev.stdenv.system}.default; hyprpanel = prev.writeShellScriptBin "hyprpanel" ''
if [ "$#" -eq 0 ]; then
exec ${self.packages.${final.stdenv.system}.default}/bin/hyprpanel
else
exec ${astal.packages.${final.stdenv.system}.io}/bin/astal -i hyprpanel "$@"
fi
'';
}; };
}; };
} }

View File

@@ -3,14 +3,19 @@
check_arch_updates() { check_arch_updates() {
official_updates=0 official_updates=0
aur_updates=0 aur_updates=0
if command -v paru &> /dev/null; then
aur_helper="paru"
else
aur_helper="yay"
fi
if [ "$1" = "-y" ]; then if [ "$1" = "-y" ]; then
aur_updates=$(yay -Qum 2>/dev/null | wc -l) aur_updates=$($aur_helper -Qum 2>/dev/null | wc -l)
elif [ "$1" = "-p" ]; then elif [ "$1" = "-p" ]; then
official_updates=$(checkupdates 2>/dev/null | wc -l) official_updates=$(checkupdates 2>/dev/null | wc -l)
else else
official_updates=$(checkupdates 2>/dev/null | wc -l) official_updates=$(checkupdates 2>/dev/null | wc -l)
aur_updates=$(yay -Qum 2>/dev/null | wc -l) aur_updates=$($aur_helper -Qum 2>/dev/null | wc -l)
fi fi
total_updates=$((official_updates + aur_updates)) total_updates=$((official_updates + aur_updates))

View File

@@ -19,6 +19,8 @@ const {
rateUnit, rateUnit,
dynamicIcon, dynamicIcon,
icon, icon,
networkInLabel,
networkOutLabel,
round, round,
leftClick, leftClick,
rightClick, rightClick,
@@ -47,11 +49,11 @@ export const Netstat = (): BarBoxChild => {
const renderNetworkLabel = (lblType: NetstatLabelType, networkService: NetworkResourceData): string => { const renderNetworkLabel = (lblType: NetstatLabelType, networkService: NetworkResourceData): string => {
switch (lblType) { switch (lblType) {
case 'in': case 'in':
return ` ${networkService.in}`; return `${networkInLabel.get()} ${networkService.in}`;
case 'out': case 'out':
return ` ${networkService.out}`; return `${networkOutLabel.get()} ${networkService.out}`;
default: default:
return `${networkService.in} ${networkService.out}`; return `${networkInLabel.get()} ${networkService.in} ${networkOutLabel.get()} ${networkService.out}`;
} }
}; };

View File

@@ -10,6 +10,7 @@ const {
updateCommand, updateCommand,
label, label,
padZero, padZero,
autoHide,
pollingInterval, pollingInterval,
icon, icon,
leftClick, leftClick,
@@ -21,6 +22,7 @@ const {
const pendingUpdates: Variable<string> = Variable('0'); const pendingUpdates: Variable<string> = Variable('0');
const postInputUpdater = Variable(true); const postInputUpdater = Variable(true);
const isVis = Variable(!autoHide.get());
const processUpdateCount = (updateCount: string): string => { const processUpdateCount = (updateCount: string): string => {
if (!padZero.get()) return updateCount; if (!padZero.get()) return updateCount;
@@ -37,9 +39,14 @@ const updatesPoller = new BashPoller<string, []>(
updatesPoller.initialize('updates'); updatesPoller.initialize('updates');
Variable.derive([bind(autoHide)], (autoHideModule) => {
isVis.set(!autoHideModule || (autoHideModule && parseFloat(pendingUpdates.get()) > 0));
});
const updatesIcon = Variable.derive( const updatesIcon = Variable.derive(
[bind(icon.pending), bind(icon.updated), bind(pendingUpdates)], [bind(icon.pending), bind(icon.updated), bind(pendingUpdates)],
(pendingIcon, updatedIcon, pUpdates) => { (pendingIcon, updatedIcon, pUpdates) => {
isVis.set(!autoHide.get() || (autoHide.get() && parseFloat(pUpdates) > 0));
return parseFloat(pUpdates) === 0 ? updatedIcon : pendingIcon; return parseFloat(pUpdates) === 0 ? updatedIcon : pendingIcon;
}, },
); );
@@ -49,6 +56,7 @@ export const Updates = (): BarBoxChild => {
textIcon: updatesIcon(), textIcon: updatesIcon(),
tooltipText: bind(pendingUpdates).as((v) => `${v} updates available`), tooltipText: bind(pendingUpdates).as((v) => `${v} updates available`),
boxClass: 'updates', boxClass: 'updates',
isVis: isVis,
label: bind(pendingUpdates), label: bind(pendingUpdates),
showLabelBinding: bind(label), showLabelBinding: bind(label),
props: { props: {

View File

@@ -8,11 +8,11 @@ import AstalHyprland from 'gi://AstalHyprland?version=0.1';
* This function searches for a matching window title in the predefined `windowTitleMap` based on the class of the provided window. * This function searches for a matching window title in the predefined `windowTitleMap` based on the class of the provided window.
* If a match is found, it returns an object containing the icon and label for the window. If no match is found, it returns a default icon and label. * If a match is found, it returns an object containing the icon and label for the window. If no match is found, it returns a default icon and label.
* *
* @param windowtitle The window object containing the class information. * @param client The window object containing the class information.
* *
* @returns An object containing the icon and label for the window. * @returns An object containing the icon and label for the window.
*/ */
export const getWindowMatch = (windowtitle: AstalHyprland.Client): Record<string, string> => { export const getWindowMatch = (client: AstalHyprland.Client): Record<string, string> => {
const windowTitleMap = [ const windowTitleMap = [
// user provided values // user provided values
...options.bar.windowtitle.title_map.get(), ...options.bar.windowtitle.title_map.get(),
@@ -119,17 +119,17 @@ export const getWindowMatch = (windowtitle: AstalHyprland.Client): Record<string
['^$', '󰇄', 'Desktop'], ['^$', '󰇄', 'Desktop'],
// Fallback icon // Fallback icon
['(.+)', '󰣆', `${capitalizeFirstLetter(windowtitle?.class ?? 'Unknown')}`], ['(.+)', '󰣆', `${capitalizeFirstLetter(client?.class ?? 'Unknown')}`],
]; ];
if (!windowtitle?.class) { if (!client?.class) {
return { return {
icon: '󰇄', icon: '󰇄',
label: 'Desktop', label: 'Desktop',
}; };
} }
const foundMatch = windowTitleMap.find((wt) => RegExp(wt[0]).test(windowtitle?.class.toLowerCase())); const foundMatch = windowTitleMap.find((wt) => RegExp(wt[0]).test(client?.class.toLowerCase()));
if (!foundMatch || foundMatch.length !== 3) { if (!foundMatch || foundMatch.length !== 3) {
return { return {
@@ -157,13 +157,12 @@ export const getWindowMatch = (windowtitle: AstalHyprland.Client): Record<string
* @returns The title of the window as a string. * @returns The title of the window as a string.
*/ */
export const getTitle = (client: AstalHyprland.Client, useCustomTitle: boolean, useClassName: boolean): string => { export const getTitle = (client: AstalHyprland.Client, useCustomTitle: boolean, useClassName: boolean): string => {
if (client === null) return getWindowMatch(client).label; if (client === null || useCustomTitle) return getWindowMatch(client).label;
if (useCustomTitle) return getWindowMatch(client).label;
if (useClassName) return client.class;
const title = client.title; const title = client.title;
// If the title is empty or only filled with spaces, fallback to the class name
if (!title || useClassName) return client.class;
if (title.length === 0 || title.match(/^ *$/)) { if (title.length === 0 || title.match(/^ *$/)) {
return client.class; return client.class;
} }

View File

@@ -162,7 +162,7 @@ const navigateWorkspace = (
while (attempts < workspacesList.length) { while (attempts < workspacesList.length) {
const targetWS = workspacesList[newIndex]; const targetWS = workspacesList[newIndex];
if (!isWorkspaceIgnored(ignoredWorkspaces, targetWS)) { if (!isWorkspaceIgnored(ignoredWorkspaces, targetWS)) {
hyprlandService.message_async(`dispatch workspace ${targetWS}`); hyprlandService.dispatch('workspace', targetWS.toString());
return; return;
} }
newIndex = (newIndex + step + workspacesList.length) % workspacesList.length; newIndex = (newIndex + step + workspacesList.length) % workspacesList.length;

View File

@@ -1,10 +1,11 @@
import options from 'src/options'; import options from 'src/options';
import { createThrottledScrollHandlers, getCurrentMonitorWorkspaces } from './helpers'; import { createThrottledScrollHandlers, getCurrentMonitorWorkspaces } from './helpers';
import { BarBoxChild, SelfButton } from 'src/lib/types/bar'; import { BarBoxChild } from 'src/lib/types/bar';
import { WorkspaceModule } from './workspaces'; import { WorkspaceModule } from './workspaces';
import { bind, Variable } from 'astal'; import { bind, Variable } from 'astal';
import { GtkWidget } from 'src/lib/types/widget'; import { GtkWidget } from 'src/lib/types/widget';
import { Gdk } from 'astal/gtk3'; import { Astal, Gdk } from 'astal/gtk3';
import { isScrollDown, isScrollUp } from 'src/lib/utils';
const { workspaces, scroll_speed } = options.bar.workspaces; const { workspaces, scroll_speed } = options.bar.workspaces;
@@ -27,23 +28,27 @@ const Workspaces = (monitor = -1): BarBoxChild => {
boxClass: 'workspaces', boxClass: 'workspaces',
isBox: true, isBox: true,
props: { props: {
setup: (self: SelfButton): void => { setup: (self: Astal.EventBox): void => {
let scrollHandlers: number;
Variable.derive([bind(scroll_speed)], (scroll_speed) => { Variable.derive([bind(scroll_speed)], (scroll_speed) => {
if (scrollHandlers) {
self.disconnect(scrollHandlers);
}
const { throttledScrollUp, throttledScrollDown } = createThrottledScrollHandlers( const { throttledScrollUp, throttledScrollDown } = createThrottledScrollHandlers(
scroll_speed, scroll_speed,
currentMonitorWorkspaces, currentMonitorWorkspaces,
); );
const scrollHandlers = self.connect('scroll-event', (_: GtkWidget, event: Gdk.Event) => { scrollHandlers = self.connect('scroll-event', (_: GtkWidget, event: Gdk.Event) => {
const eventDirection = event.get_scroll_direction()[1]; if (isScrollUp(event)) {
if (eventDirection === Gdk.ScrollDirection.UP) {
throttledScrollUp();
} else if (eventDirection === Gdk.ScrollDirection.DOWN) {
throttledScrollDown(); throttledScrollDown();
} }
});
self.disconnect(scrollHandlers); if (isScrollDown(event)) {
throttledScrollUp();
}
});
}); });
}, },
}, },

View File

@@ -152,7 +152,7 @@ export const WorkspaceModule = ({ monitor }: WorkspaceModuleProps): JSX.Element
self.toggleClassName('active', activeWorkspace === wsId); self.toggleClassName('active', activeWorkspace === wsId);
self.toggleClassName( self.toggleClassName(
'occupied', 'occupied',
(hyprlandService.get_workspace(wsId)?.get_clients().length || 0) > 0, (hyprlandService.get_workspace(wsId)?.get_clients()?.length || 0) > 0,
); );
}} }}
/> />

View File

@@ -140,7 +140,10 @@ export const CustomModuleSettings = (): JSX.Element => {
<Option <Option
opt={options.bar.customModules.netstat.networkInterface} opt={options.bar.customModules.netstat.networkInterface}
title="Network Interface" title="Network Interface"
subtitle="Wiki: https://hyprpanel.com/configuration/panel.html#custom-modules" subtitle={
'Name of the network interface to poll.\n' +
"HINT: Get a list of interfaces with 'cat /proc/net/dev"
}
type="string" type="string"
/> />
<Option <Option
@@ -151,6 +154,12 @@ export const CustomModuleSettings = (): JSX.Element => {
/> />
<Option opt={options.bar.customModules.netstat.icon} title="Netstat Icon" type="string" /> <Option opt={options.bar.customModules.netstat.icon} title="Netstat Icon" type="string" />
<Option opt={options.bar.customModules.netstat.label} title="Show Label" type="boolean" /> <Option opt={options.bar.customModules.netstat.label} title="Show Label" type="boolean" />
<Option opt={options.bar.customModules.netstat.networkInLabel} title="Network In Label" type="string" />
<Option
opt={options.bar.customModules.netstat.networkOutLabel}
title="Network Out Label"
type="string"
/>
<Option <Option
opt={options.bar.customModules.netstat.rateUnit} opt={options.bar.customModules.netstat.rateUnit}
title="Rate Unit" title="Rate Unit"
@@ -167,7 +176,7 @@ export const CustomModuleSettings = (): JSX.Element => {
<Option opt={options.bar.customModules.netstat.round} title="Round" type="boolean" /> <Option opt={options.bar.customModules.netstat.round} title="Round" type="boolean" />
<Option <Option
opt={options.bar.customModules.netstat.pollingInterval} opt={options.bar.customModules.netstat.pollingInterval}
title="Polling Interval" title="Polling Interval (ms)"
type="number" type="number"
min={100} min={100}
max={60 * 24 * 1000} max={60 * 24 * 1000}
@@ -218,6 +227,12 @@ export const CustomModuleSettings = (): JSX.Element => {
/> />
<Option opt={options.bar.customModules.updates.icon.updated} title="No Updates Icon" type="string" /> <Option opt={options.bar.customModules.updates.icon.updated} title="No Updates Icon" type="string" />
<Option opt={options.bar.customModules.updates.label} title="Show Label" type="boolean" /> <Option opt={options.bar.customModules.updates.label} title="Show Label" type="boolean" />
<Option
opt={options.bar.customModules.updates.autoHide}
title="Auto Hide"
subtitle="Hides module when no updates are available."
type="boolean"
/>
<Option opt={options.bar.customModules.updates.padZero} title="Pad with 0" type="boolean" /> <Option opt={options.bar.customModules.updates.padZero} title="Pad with 0" type="boolean" />
<Option opt={options.theme.bar.buttons.modules.updates.spacing} title="Spacing" type="string" /> <Option opt={options.theme.bar.buttons.modules.updates.spacing} title="Spacing" type="string" />
<Option <Option
@@ -280,6 +295,79 @@ export const CustomModuleSettings = (): JSX.Element => {
<Option opt={options.bar.customModules.weather.middleClick} title="Middle Click" type="string" /> <Option opt={options.bar.customModules.weather.middleClick} title="Middle Click" type="string" />
<Option opt={options.bar.customModules.weather.scrollUp} title="Scroll Up" type="string" /> <Option opt={options.bar.customModules.weather.scrollUp} title="Scroll Up" type="string" />
<Option opt={options.bar.customModules.weather.scrollDown} title="Scroll Down" type="string" /> <Option opt={options.bar.customModules.weather.scrollDown} title="Scroll Down" type="string" />
{/* Hyprsunset Section */}
<Header title="Hyprsunset" />
<Option
opt={options.bar.customModules.hyprsunset.temperature}
title="Temperature"
subtitle="Ex: 1000k, 2000k, 5000k, etc."
type="string"
/>
<Option
opt={options.theme.bar.buttons.modules.hyprsunset.enableBorder}
title="Button Border"
type="boolean"
/>
<Option opt={options.bar.customModules.hyprsunset.onIcon} title="Enabled Icon" type="string" />
<Option opt={options.bar.customModules.hyprsunset.offIcon} title="Disabled Icon" type="string" />
<Option opt={options.bar.customModules.hyprsunset.onLabel} title="Enabled Label" type="string" />
<Option opt={options.bar.customModules.hyprsunset.offLabel} title="Disabled Label" type="string" />
<Option opt={options.bar.customModules.hyprsunset.label} title="Show Label" type="boolean" />
<Option opt={options.theme.bar.buttons.modules.hyprsunset.spacing} title="Spacing" type="string" />
<Option
opt={options.bar.customModules.hyprsunset.pollingInterval}
title="Polling Interval"
type="number"
min={100}
max={60 * 24 * 1000}
increment={1000}
/>
<Option opt={options.bar.customModules.hyprsunset.rightClick} title="Right Click" type="string" />
<Option opt={options.bar.customModules.hyprsunset.middleClick} title="Middle Click" type="string" />
<Option opt={options.bar.customModules.hyprsunset.scrollUp} title="Scroll Up" type="string" />
<Option opt={options.bar.customModules.hyprsunset.scrollDown} title="Scroll Down" type="string" />
{/* Hypridle Section */}
<Header title="Hypridle" />
<Option
opt={options.theme.bar.buttons.modules.hypridle.enableBorder}
title="Button Border"
type="boolean"
/>
<Option opt={options.bar.customModules.hypridle.onIcon} title="Enabled Icon" type="string" />
<Option opt={options.bar.customModules.hypridle.offIcon} title="Disabled Icon" type="string" />
<Option opt={options.bar.customModules.hypridle.onLabel} title="Enabled Label" type="string" />
<Option opt={options.bar.customModules.hypridle.offLabel} title="Disabled Label" type="string" />
<Option opt={options.bar.customModules.hypridle.label} title="Show Label" type="boolean" />
<Option opt={options.theme.bar.buttons.modules.hypridle.spacing} title="Spacing" type="string" />
<Option
opt={options.bar.customModules.hypridle.pollingInterval}
title="Polling Interval"
type="number"
min={100}
max={60 * 24 * 1000}
increment={1000}
/>
<Option opt={options.bar.customModules.hypridle.rightClick} title="Right Click" type="string" />
<Option opt={options.bar.customModules.hypridle.middleClick} title="Middle Click" type="string" />
<Option opt={options.bar.customModules.hypridle.scrollUp} title="Scroll Up" type="string" />
<Option opt={options.bar.customModules.hypridle.scrollDown} title="Scroll Down" type="string" />
{/* Power Section */}
<Header title="Power" />
<Option
opt={options.theme.bar.buttons.modules.power.enableBorder}
title="Button Border"
type="boolean"
/>
<Option opt={options.theme.bar.buttons.modules.power.spacing} title="Spacing" type="string" />
<Option opt={options.bar.customModules.power.icon} title="Power Button Icon" type="string" />
<Option opt={options.bar.customModules.power.leftClick} title="Left Click" type="string" />
<Option opt={options.bar.customModules.power.rightClick} title="Right Click" type="string" />
<Option opt={options.bar.customModules.power.middleClick} title="Middle Click" type="string" />
<Option opt={options.bar.customModules.power.scrollUp} title="Scroll Up" type="string" />
<Option opt={options.bar.customModules.power.scrollDown} title="Scroll Down" type="string" />
</box> </box>
</scrollable> </scrollable>
); );

View File

@@ -158,6 +158,56 @@ export const CustomModuleTheme = (): JSX.Element => {
type="color" type="color"
/> />
<Option opt={options.theme.bar.buttons.modules.weather.border} title="Border" type="color" /> <Option opt={options.theme.bar.buttons.modules.weather.border} title="Border" type="color" />
{/* Hyprsunset Module Section */}
<Header title="Hyprsunset" />
<Option opt={options.theme.bar.buttons.modules.hyprsunset.text} title="Text" type="color" />
<Option opt={options.theme.bar.buttons.modules.hyprsunset.icon} title="Icon" type="color" />
<Option
opt={options.theme.bar.buttons.modules.hyprsunset.background}
title="Label Background"
type="color"
/>
<Option
opt={options.theme.bar.buttons.modules.hyprsunset.icon_background}
title="Icon Background"
subtitle="Applies a background color to the icon section of the button.\nRequires 'split' button styling."
type="color"
/>
<Option opt={options.theme.bar.buttons.modules.hyprsunset.border} title="Border" type="color" />
{/* Hypridle Module Section */}
<Header title="Hypridle" />
<Option opt={options.theme.bar.buttons.modules.hypridle.text} title="Text" type="color" />
<Option opt={options.theme.bar.buttons.modules.hypridle.icon} title="Icon" type="color" />
<Option
opt={options.theme.bar.buttons.modules.hypridle.background}
title="Label Background"
type="color"
/>
<Option
opt={options.theme.bar.buttons.modules.hypridle.icon_background}
title="Icon Background"
subtitle="Applies a background color to the icon section of the button.\nRequires 'split' button styling."
type="color"
/>
<Option opt={options.theme.bar.buttons.modules.hypridle.border} title="Border" type="color" />
{/* Power Module Section */}
<Header title="Power" />
<Option opt={options.theme.bar.buttons.modules.power.icon} title="Icon" type="color" />
<Option
opt={options.theme.bar.buttons.modules.power.background}
title="Label Background"
type="color"
/>
<Option
opt={options.theme.bar.buttons.modules.power.icon_background}
title="Icon Background"
subtitle="Applies a background color to the icon section of the button.\nRequires 'split' button styling."
type="color"
/>
<Option opt={options.theme.bar.buttons.modules.power.border} title="Border" type="color" />
</box> </box>
</scrollable> </scrollable>
); );

View File

@@ -14,6 +14,7 @@ export const Module = ({
label, label,
tooltipText, tooltipText,
boxClass, boxClass,
isVis,
props = {}, props = {},
showLabelBinding = bind(undefinedVar), showLabelBinding = bind(undefinedVar),
showLabel, showLabel,
@@ -86,7 +87,7 @@ export const Module = ({
return { return {
component, component,
tooltip_text: tooltipText, tooltip_text: tooltipText,
isVisible: true, isVis: isVis,
boxClass, boxClass,
props, props,
}; };

View File

@@ -26,9 +26,9 @@ export const WidgetContainer = (child: BarBoxChild): JSX.Element => {
if (child.isBox) { if (child.isBox) {
return ( return (
<box className={buttonClassName} visible={computeVisible(child)}> <eventbox visible={computeVisible(child)} {...child.props}>
{child.component} <box className={buttonClassName}>{child.component}</box>
</box> </eventbox>
); );
} }

View File

@@ -0,0 +1,25 @@
import { BindableChild } from 'astal/gtk3/astalify';
import { audioService } from 'src/lib/constants/services';
import { SliderItem } from '../sliderItem/SliderItem';
import { ActiveDeviceMenu } from '..';
const ActiveDeviceContainer = ({ children }: ActiveDeviceContainerProps): JSX.Element => {
return (
<box className={'menu-items-section selected'} name={ActiveDeviceMenu.Devices} vertical>
{children}
</box>
);
};
export const ActiveDevices = (): JSX.Element => {
return (
<ActiveDeviceContainer>
<SliderItem type={'playback'} device={audioService.defaultSpeaker} />
<SliderItem type={'input'} device={audioService.defaultMicrophone} />
</ActiveDeviceContainer>
);
};
interface ActiveDeviceContainerProps {
children?: BindableChild | BindableChild[];
}

View File

@@ -1,34 +1,56 @@
import { Gtk } from 'astal/gtk3'; import { Gtk } from 'astal/gtk3';
import { ActiveDevice } from './device/index.js'; import { ActiveDevices } from './devices/index.js';
import { audioService } from 'src/lib/constants/services.js'; import Variable from 'astal/variable.js';
import { BindableChild } from 'astal/gtk3/astalify.js'; import { ActivePlaybacks } from './playbacks/index.js';
import { bind } from 'astal/binding.js';
import { isPrimaryClick } from 'src/lib/utils.js';
export enum ActiveDeviceMenu {
Devices = 'devices',
Playbacks = 'playbacks',
}
const activeMenu: Variable<ActiveDeviceMenu> = Variable(ActiveDeviceMenu.Devices);
export const SelectedDevices = (): JSX.Element => {
const Header = (): JSX.Element => ( const Header = (): JSX.Element => (
<box className={'menu-label-container volume selected'} halign={Gtk.Align.FILL}> <box className={'menu-label-container volume selected'} halign={Gtk.Align.FILL}>
<label className={'menu-label audio volume'} halign={Gtk.Align.START} hexpand label={'Volume'} /> <label className={'menu-label audio volume'} halign={Gtk.Align.START} hexpand label={'Volume'} />
<button
className={'menu-icon-button menu-label slider-toggle volume'}
onClick={(_, event) => {
if (!isPrimaryClick(event)) {
return;
}
if (activeMenu.get() === ActiveDeviceMenu.Devices) {
activeMenu.set(ActiveDeviceMenu.Playbacks);
} else {
activeMenu.set(ActiveDeviceMenu.Devices);
}
}}
halign={Gtk.Align.END}
hexpand
label={bind(activeMenu).as((menu) => (menu === ActiveDeviceMenu.Devices ? '' : '󰤽'))}
/>
</box> </box>
); );
const ActiveDeviceContainer = ({ children }: ActiveDeviceContainerProps): JSX.Element => { export const VolumeSliders = (): JSX.Element => {
return (
<box className={'menu-items-section selected'} vertical>
{children}
</box>
);
};
return ( return (
<box className={'menu-section-container volume'} vertical> <box className={'menu-section-container volume'} vertical>
<Header /> <Header />
<ActiveDeviceContainer> <revealer
<ActiveDevice type={'playback'} device={audioService.defaultSpeaker} /> transitionType={Gtk.RevealerTransitionType.NONE}
<ActiveDevice type={'input'} device={audioService.defaultMicrophone} /> revealChild={bind(activeMenu).as((curMenu) => curMenu === ActiveDeviceMenu.Devices)}
</ActiveDeviceContainer> >
<ActiveDevices />
</revealer>
<revealer
transitionType={Gtk.RevealerTransitionType.NONE}
revealChild={bind(activeMenu).as((curMenu) => curMenu === ActiveDeviceMenu.Playbacks)}
>
<ActivePlaybacks />
</revealer>
</box> </box>
); );
}; };
interface ActiveDeviceContainerProps {
children?: BindableChild | BindableChild[];
}

View File

@@ -0,0 +1,30 @@
import { bind } from 'astal';
import { audioService } from 'src/lib/constants/services';
import { SliderItem } from '../sliderItem/SliderItem';
import { ActiveDeviceMenu } from '..';
const NoStreams = (): JSX.Element => {
return <label className={'no-playbacks dim'} label={'No active playbacks found.'} expand />;
};
export const ActivePlaybacks = (): JSX.Element => {
return (
<box className={'menu-items-section selected'} name={ActiveDeviceMenu.Playbacks} vertical>
<scrollable className={'menu-scroller active-playbacks-scrollable'}>
<box vertical>
{bind(audioService, 'streams').as((streams) => {
if (!streams || streams.length === 0) {
return <NoStreams />;
}
const currentStreams = streams;
return currentStreams.map((stream) => {
return <SliderItem type={'playback'} device={stream} />;
});
})}
</box>
</scrollable>
</box>
);
};

View File

@@ -1,6 +1,7 @@
import { bind } from 'astal'; import { bind } from 'astal';
import { Gtk } from 'astal/gtk3'; import { Gdk, Gtk } from 'astal/gtk3';
import AstalWp from 'gi://AstalWp?version=0.1'; 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/options';
const { raiseMaximumVolume } = options.menus.volume; const { raiseMaximumVolume } = options.menus.volume;
@@ -12,9 +13,11 @@ export const Slider = ({ device, type }: SliderProps): JSX.Element => {
className={`menu-active ${type}`} className={`menu-active ${type}`}
halign={Gtk.Align.START} halign={Gtk.Align.START}
truncate truncate
expand hexpand
wrap wrap
label={bind(device, 'description').as((description) => description ?? `Unknown ${type} Device`)} label={bind(device, 'description').as((description) =>
capitalizeFirstLetter(description ?? `Unknown ${type} Device`),
)}
/> />
<slider <slider
value={bind(device, 'volume')} value={bind(device, 'volume')}
@@ -29,6 +32,19 @@ export const Slider = ({ device, type }: SliderProps): JSX.Element => {
device.mute = false; device.mute = false;
} }
}} }}
setup={(self) => {
self.connect('scroll-event', (_, event: Gdk.Event) => {
if (isScrollUp(event)) {
const newVolume = device.volume + 0.05;
device.set_volume(Math.min(newVolume, 1));
}
if (isScrollDown(event)) {
const newVolume = device.volume - 0.05;
device.set_volume(newVolume);
}
});
}}
/> />
</box> </box>
); );

View File

@@ -3,7 +3,7 @@ import { SliderIcon } from './SliderIcon';
import { Slider } from './Slider'; import { Slider } from './Slider';
import { SliderPercentage } from './SliderPercentage'; import { SliderPercentage } from './SliderPercentage';
export const ActiveDevice = ({ type, device }: ActiveDeviceProps): JSX.Element => { export const SliderItem = ({ type, device }: SliderItemProps): JSX.Element => {
return ( return (
<box className={`menu-active-container ${type}`} vertical> <box className={`menu-active-container ${type}`} vertical>
<box className={`menu-slider-container ${type}`}> <box className={`menu-slider-container ${type}`}>
@@ -15,7 +15,7 @@ export const ActiveDevice = ({ type, device }: ActiveDeviceProps): JSX.Element =
); );
}; };
interface ActiveDeviceProps { interface SliderItemProps {
type: 'playback' | 'input'; type: 'playback' | 'input';
device: AstalWp.Endpoint; device: AstalWp.Endpoint;
} }

View File

@@ -1,5 +1,5 @@
import DropdownMenu from '../shared/dropdown/index.js'; import DropdownMenu from '../shared/dropdown/index.js';
import { SelectedDevices } from './active/index.js'; import { VolumeSliders } from './active/index.js';
import options from 'src/options.js'; import options from 'src/options.js';
import { bind } from 'astal/binding.js'; import { bind } from 'astal/binding.js';
import { Gtk } from 'astal/gtk3'; import { Gtk } from 'astal/gtk3';
@@ -14,7 +14,7 @@ export default (): JSX.Element => {
> >
<box className={'menu-items audio'} halign={Gtk.Align.FILL} hexpand> <box className={'menu-items audio'} halign={Gtk.Align.FILL} hexpand>
<box className={'menu-items-container audio'} halign={Gtk.Align.FILL} vertical hexpand> <box className={'menu-items-container audio'} halign={Gtk.Align.FILL} vertical hexpand>
<SelectedDevices /> <VolumeSliders />
<AvailableDevices /> <AvailableDevices />
</box> </box>
</box> </box>

View File

@@ -6,7 +6,7 @@ import { isDiscovering } from './helper';
export const DiscoverButton = (): JSX.Element => ( export const DiscoverButton = (): JSX.Element => (
<button <button
className="menu-icon-button search" className="menu-icon-button search bluetooth"
valign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER}
onClick={(_, self) => { onClick={(_, self) => {
if (!isPrimaryClick(self)) { if (!isPrimaryClick(self)) {

View File

@@ -1,7 +1,7 @@
import { bind, exec } from 'astal'; import { bind, exec } from 'astal';
import GdkPixbuf from 'gi://GdkPixbuf';
import { Gtk } from 'astal/gtk3'; import { Gtk } from 'astal/gtk3';
import options from 'src/options.js'; import options from 'src/options.js';
import { normalizePath, isAnImage } from 'src/lib/utils.js';
const { image, name } = options.menus.dashboard.powermenu.avatar; const { image, name } = options.menus.dashboard.powermenu.avatar;
@@ -11,12 +11,11 @@ const ProfilePicture = (): JSX.Element => {
className={'profile-picture'} className={'profile-picture'}
halign={Gtk.Align.CENTER} halign={Gtk.Align.CENTER}
css={bind(image).as((img) => { css={bind(image).as((img) => {
try { if (isAnImage(img)) {
GdkPixbuf.Pixbuf.new_from_file(img); return `background-image: url("${normalizePath(img)}")`;
return `background-image: url("${img}")`;
} catch {
return `background-image: url("${SRC_DIR}/assets/hyprpanel.png")`;
} }
return `background-image: url("${SRC_DIR}/assets/hyprpanel.png")`;
})} })}
/> />
); );

View File

@@ -24,36 +24,60 @@ const ShortcutButton = ({ shortcut, ...props }: ShortcutButtonProps): JSX.Elemen
}; };
export const LeftShortcut1 = (): JSX.Element => { export const LeftShortcut1 = (): JSX.Element => {
if (!hasCommand(left.shortcut1)) {
return <box />;
}
return ( return (
<ShortcutButton <ShortcutButton
shortcut={left.shortcut1} shortcut={left.shortcut1}
className={`dashboard-button top-button ${hasCommand(left.shortcut1) ? 'paired' : ''}`} className={`dashboard-button top-button ${hasCommand(left.shortcut2) ? 'paired' : ''}`}
/> />
); );
}; };
export const LeftShortcut2 = (): JSX.Element => { export const LeftShortcut2 = (): JSX.Element => {
if (!hasCommand(left.shortcut2)) {
return <box />;
}
return <ShortcutButton shortcut={left.shortcut2} className={`dashboard-button`} />; return <ShortcutButton shortcut={left.shortcut2} className={`dashboard-button`} />;
}; };
export const LeftShortcut3 = (): JSX.Element => { export const LeftShortcut3 = (): JSX.Element => {
if (!hasCommand(left.shortcut3)) {
return <box />;
}
return ( return (
<ShortcutButton <ShortcutButton
shortcut={left.shortcut3} shortcut={left.shortcut3}
className={`dashboard-button top-button ${hasCommand(left.shortcut3) ? 'paired' : ''}`} className={`dashboard-button top-button ${hasCommand(left.shortcut4) ? 'paired' : ''}`}
/> />
); );
}; };
export const LeftShortcut4 = (): JSX.Element => { export const LeftShortcut4 = (): JSX.Element => {
if (!hasCommand(left.shortcut4)) {
return <box />;
}
return <ShortcutButton shortcut={left.shortcut4} className={`dashboard-button `} />; return <ShortcutButton shortcut={left.shortcut4} className={`dashboard-button `} />;
}; };
export const RightShortcut1 = (): JSX.Element => { export const RightShortcut1 = (): JSX.Element => {
if (!hasCommand(right.shortcut1)) {
return <box />;
}
return <ShortcutButton shortcut={right.shortcut1} className={`dashboard-button top-button paired`} />; return <ShortcutButton shortcut={right.shortcut1} className={`dashboard-button top-button paired`} />;
}; };
export const RightShortcut3 = (): JSX.Element => { export const RightShortcut3 = (): JSX.Element => {
if (!hasCommand(right.shortcut3)) {
return <box />;
}
return <ShortcutButton shortcut={right.shortcut3} className={`dashboard-button top-button paired`} />; return <ShortcutButton shortcut={right.shortcut3} className={`dashboard-button top-button paired`} />;
}; };

View File

@@ -1,9 +1,9 @@
import { BindableChild } from 'astal/gtk3/astalify'; import { BindableChild } from 'astal/gtk3/astalify';
export const LeftColumn = ({ visibleClass, children }: LeftColumnProps): JSX.Element => { export const LeftColumn = ({ isVisible, children }: LeftColumnProps): JSX.Element => {
return ( return (
<box className={`card-button-section-container ${visibleClass ? 'visible' : ''}`}> <box className={`card-button-section-container ${isVisible ? 'visible' : ''}`}>
{visibleClass ? ( {isVisible ? (
<box vertical hexpand vexpand> <box vertical hexpand vexpand>
{children} {children}
</box> </box>
@@ -25,7 +25,7 @@ export const RightColumn = ({ children }: RightColumnProps): JSX.Element => {
}; };
interface LeftColumnProps { interface LeftColumnProps {
visibleClass?: boolean; isVisible?: boolean;
children?: BindableChild | BindableChild[]; children?: BindableChild | BindableChild[];
} }

View File

@@ -57,7 +57,7 @@ export const LeftShortcuts = (): JSX.Element => {
return ( return (
<box className={'container most-used dashboard-card'}> <box className={'container most-used dashboard-card'}>
<LeftColumn visibleClass={isVisibleRight && isVisibleLeft}> <LeftColumn isVisible={isVisibleRight && isVisibleLeft}>
<LeftShortcut1 /> <LeftShortcut1 />
<LeftShortcut2 /> <LeftShortcut2 />
</LeftColumn> </LeftColumn>
@@ -78,7 +78,7 @@ export const RightShortcuts = (): JSX.Element => {
{Variable.derive(rightBindings, () => { {Variable.derive(rightBindings, () => {
return ( return (
<box className={`container utilities dashboard-card ${!leftCardHidden.get() ? 'paired' : ''}`}> <box className={`container utilities dashboard-card ${!leftCardHidden.get() ? 'paired' : ''}`}>
<LeftColumn visibleClass={!leftCardHidden.get()}> <LeftColumn isVisible={true}>
<RightShortcut1 /> <RightShortcut1 />
<SettingsButton /> <SettingsButton />
</LeftColumn> </LeftColumn>

View File

@@ -8,6 +8,14 @@ const { enable, duration, active_monitor, monitor } = options.theme.osd;
let count = 0; let count = 0;
/*
* So the OSD doesn't show on startup for no reason
*/
let isStartingUp = true;
timeout(3000, () => {
isStartingUp = false;
});
/** /**
* Handles the reveal state of a Widget.Revealer. * Handles the reveal state of a Widget.Revealer.
* *
@@ -69,6 +77,10 @@ export const handleRevealWindow = (self: Widget.Window, property: 'revealChild'
* @param property The property to check, either 'revealChild' or 'visible'. * @param property The property to check, either 'revealChild' or 'visible'.
*/ */
export const handleReveal = (self: Widget.Revealer | Widget.Window, property: 'revealChild' | 'visible'): void => { export const handleReveal = (self: Widget.Revealer | Widget.Window, property: 'revealChild' | 'visible'): void => {
if (isStartingUp) {
return;
}
if (self instanceof Widget.Revealer) { if (self instanceof Widget.Revealer) {
handleRevealRevealer(self, property); handleRevealRevealer(self, property);
} else if (self instanceof Widget.Window) { } else if (self instanceof Widget.Window) {

View File

@@ -145,6 +145,77 @@ export const BarTheme = (): JSX.Element => {
type="color" type="color"
/> />
<Option opt={options.theme.bar.buttons.volume.border} title="Border" type="color" /> <Option opt={options.theme.bar.buttons.volume.border} title="Border" type="color" />
{/* Network Section */}
<Header title="Network" />
<Option opt={options.theme.bar.buttons.network.background} title="Background" type="color" />
<Option opt={options.theme.bar.buttons.network.text} title="Text" type="color" />
<Option opt={options.theme.bar.buttons.network.icon} title="Icon" type="color" />
<Option
opt={options.theme.bar.buttons.network.icon_background}
title="Button Icon Background"
subtitle="Applies a background color to the icon section of the button.\nRequires 'split' button styling."
type="color"
/>
<Option opt={options.theme.bar.buttons.network.border} title="Border" type="color" />
{/* Bluetooth Section */}
<Header title="Bluetooth" />
<Option opt={options.theme.bar.buttons.bluetooth.background} title="Background" type="color" />
<Option opt={options.theme.bar.buttons.bluetooth.text} title="Text" type="color" />
<Option opt={options.theme.bar.buttons.bluetooth.icon} title="Icon" type="color" />
<Option
opt={options.theme.bar.buttons.bluetooth.icon_background}
title="Button Icon Background"
subtitle="Applies a background color to the icon section of the button.\nRequires 'split' button styling."
type="color"
/>
<Option opt={options.theme.bar.buttons.bluetooth.border} title="Border" type="color" />
{/* System Tray Section */}
<Header title="System Tray" />
<Option opt={options.theme.bar.buttons.systray.border} title="Border" type="color" />
<Option opt={options.theme.bar.buttons.systray.customIcon} title="Custom Icons" type="color" />
<Option opt={options.theme.bar.buttons.systray.background} title="Background" type="color" />
{/* Battery Section */}
<Header title="Battery" />
<Option opt={options.theme.bar.buttons.battery.background} title="Background" type="color" />
<Option opt={options.theme.bar.buttons.battery.text} title="Text" type="color" />
<Option opt={options.theme.bar.buttons.battery.icon} title="Icon" type="color" />
<Option
opt={options.theme.bar.buttons.battery.icon_background}
title="Button Icon Background"
subtitle="Applies a background color to the icon section of the button.\nRequires 'split' button styling."
type="color"
/>
<Option opt={options.theme.bar.buttons.battery.border} title="Border" type="color" />
{/* Clock Section */}
<Header title="Clock" />
<Option opt={options.theme.bar.buttons.clock.background} title="Background" type="color" />
<Option opt={options.theme.bar.buttons.clock.text} title="Text" type="color" />
<Option opt={options.theme.bar.buttons.clock.icon} title="Icon" type="color" />
<Option
opt={options.theme.bar.buttons.clock.icon_background}
title="Button Icon Background"
subtitle="Applies a background color to the icon section of the button.\nRequires 'split' button styling."
type="color"
/>
<Option opt={options.theme.bar.buttons.clock.border} title="Border" type="color" />
{/* Notifications Section */}
<Header title="Notifications" />
<Option opt={options.theme.bar.buttons.notifications.background} title="Background" type="color" />
<Option opt={options.theme.bar.buttons.notifications.total} title="Notification Count" type="color" />
<Option opt={options.theme.bar.buttons.notifications.icon} title="Icon" type="color" />
<Option
opt={options.theme.bar.buttons.notifications.icon_background}
title="Button Icon Background"
subtitle="Applies a background color to the icon section of the button.\nRequires 'split' button styling."
type="color"
/>
<Option opt={options.theme.bar.buttons.notifications.border} title="Border" type="color" />
</box> </box>
</scrollable> </scrollable>
); );

View File

@@ -29,19 +29,19 @@ export function warnOnLowBattery(): void {
// To avoid double notifications, we check each of the thresholds and set the correct `sentNotification`, but then // To avoid double notifications, we check each of the thresholds and set the correct `sentNotification`, but then
// combine them into one notification only // combine them into one notification only
let sendNotification = false; let sendNotification = false;
if (!sentLowNotification && batteryPercentage < lowThreshold) { if (!sentLowNotification && batteryPercentage <= lowThreshold) {
sentLowNotification = true; sentLowNotification = true;
sendNotification = true; sendNotification = true;
} }
if (!sentHalfLowNotification && batteryPercentage < lowThreshold / 2) { if (!sentHalfLowNotification && batteryPercentage <= lowThreshold / 2) {
sentHalfLowNotification = true; sentHalfLowNotification = true;
sendNotification = true; sendNotification = true;
} }
if (sendNotification) { if (sendNotification) {
Notify({ Notify({
summary: lowBatteryNotificationTitle.get().replace('$POWER_LEVEL', batteryPercentage.toString()), summary: lowBatteryNotificationTitle.get().replaceAll('$POWER_LEVEL', batteryPercentage.toString()),
body: lowBatteryNotificationText.get().replace('$POWER_LEVEL', batteryPercentage.toString()), body: lowBatteryNotificationText.get().replaceAll('$POWER_LEVEL', batteryPercentage.toString()),
iconName: icons.ui.warning, iconName: icons.ui.warning,
urgency: 'critical', urgency: 'critical',
}); });

View File

@@ -13,7 +13,7 @@ export type BarBoxChild = {
isBox?: boolean; isBox?: boolean;
boxClass: string; boxClass: string;
tooltip_text?: string | Binding<string>; tooltip_text?: string | Binding<string>;
} & ({ isBox: true; props: Widget.BoxProps } | { isBox?: false; props: Widget.ButtonProps }); } & ({ isBox: true; props: Widget.EventBoxProps } | { isBox?: false; props: Widget.ButtonProps });
export type SelfButton = Button<Child, Attribute>; export type SelfButton = Button<Child, Attribute>;
@@ -29,6 +29,7 @@ export type BarModule = {
boundLabel?: string; boundLabel?: string;
tooltipText?: string | Binding<string>; tooltipText?: string | Binding<string>;
boxClass: string; boxClass: string;
isVis?: Variable<boolean>;
props?: Widget.ButtonProps; props?: Widget.ButtonProps;
showLabel?: boolean; showLabel?: boolean;
showLabelBinding?: Binding; showLabelBinding?: Binding;

View File

@@ -11,7 +11,6 @@ import options from '../options';
import { Astal, Gdk, Gtk } from 'astal/gtk3'; import { Astal, Gdk, Gtk } from 'astal/gtk3';
import AstalApps from 'gi://AstalApps?version=0.1'; import AstalApps from 'gi://AstalApps?version=0.1';
import { exec, execAsync } from 'astal/process'; import { exec, execAsync } from 'astal/process';
import { Gio } from 'astal';
/** /**
* Handles errors by throwing a new Error with a message. * Handles errors by throwing a new Error with a message.
@@ -228,25 +227,40 @@ export function launchApp(app: AstalApps.Application): void {
* This function attempts to load an image from the specified filepath using GdkPixbuf. * This function attempts to load an image from the specified filepath using GdkPixbuf.
* If the image is successfully loaded, it returns true. Otherwise, it logs an error and returns false. * If the image is successfully loaded, it returns true. Otherwise, it logs an error and returns false.
* *
* Note: Unlike GdkPixbuf, this function will normalize the given path.
*
* @param imgFilePath The path to the image file. * @param imgFilePath The path to the image file.
* *
* @returns True if the filepath is a valid image, false otherwise. * @returns True if the filepath is a valid image, false otherwise.
*/ */
export function isAnImage(imgFilePath: string): boolean { export function isAnImage(imgFilePath: string): boolean {
try { try {
const file = Gio.File.new_for_path(imgFilePath); GdkPixbuf.Pixbuf.new_from_file(normalizePath(imgFilePath));
if (!file.query_exists(null)) {
return false;
}
GdkPixbuf.Pixbuf.new_from_file(imgFilePath);
return true; return true;
} catch (error) { } catch (error) {
console.error(error); console.info(error);
return false; return false;
} }
} }
/**
* Normalize a path to the absolute representation of the path.
*
* Note: This will only expand '~' if present. Path traversal is not supported.
*
* @param path The path to normalize.
*
* @returns The normalized path.
*/
export function normalizePath(path: string): string {
if (path.charAt(0) == '~') {
// Replace will only replace the first match, in this case, the first character
return path.replace('~', GLib.get_home_dir());
}
return path;
}
/** /**
* Sends a notification using the `notify-send` command. * Sends a notification using the `notify-send` command.
* *
@@ -399,7 +413,20 @@ export const isMiddleClick = (event: Astal.ClickEvent): boolean => event.button
* *
* @returns True if the event is a scroll up, false otherwise. * @returns True if the event is a scroll up, false otherwise.
*/ */
export const isScrollUp = (event: Astal.ScrollEvent): boolean => event.direction === Gdk.ScrollDirection.UP; export const isScrollUp = (event: Gdk.Event): boolean => {
const [directionSuccess, direction] = event.get_scroll_direction();
const [deltaSuccess, , yScroll] = event.get_scroll_deltas();
if (directionSuccess && direction === Gdk.ScrollDirection.UP) {
return true;
}
if (deltaSuccess && yScroll < 0) {
return true;
}
return false;
};
/** /**
* Checks if an event is a scroll down. * Checks if an event is a scroll down.
@@ -410,4 +437,17 @@ export const isScrollUp = (event: Astal.ScrollEvent): boolean => event.direction
* *
* @returns True if the event is a scroll down, false otherwise. * @returns True if the event is a scroll down, false otherwise.
*/ */
export const isScrollDown = (event: Astal.ScrollEvent): boolean => event.direction === Gdk.ScrollDirection.DOWN; export const isScrollDown = (event: Gdk.Event): boolean => {
const [directionSuccess, direction] = event.get_scroll_direction();
const [deltaSuccess, , yScroll] = event.get_scroll_deltas();
if (directionSuccess && direction === Gdk.ScrollDirection.DOWN) {
return true;
}
if (deltaSuccess && yScroll > 0) {
return true;
}
return false;
};

View File

@@ -527,6 +527,9 @@ const options = mkOptions(CONFIG, {
color: opt(colors.maroon), color: opt(colors.maroon),
}, },
text: opt(colors.text), text: opt(colors.text),
scroller: {
color: opt(colors.maroon),
},
listitems: { listitems: {
passive: opt(colors.text), passive: opt(colors.text),
active: opt(secondary_colors.maroon), active: opt(secondary_colors.maroon),
@@ -1069,6 +1072,8 @@ const options = mkOptions(CONFIG, {
networkInterface: opt(''), networkInterface: opt(''),
dynamicIcon: opt(false), dynamicIcon: opt(false),
icon: opt('󰖟'), icon: opt('󰖟'),
networkInLabel: opt('↓'),
networkOutLabel: opt('↑'),
round: opt(true), round: opt(true),
labelType: opt<NetstatLabelType>('full'), labelType: opt<NetstatLabelType>('full'),
rateUnit: opt<RateUnit>('auto'), rateUnit: opt<RateUnit>('auto'),
@@ -1091,6 +1096,7 @@ const options = mkOptions(CONFIG, {
updateCommand: opt(`${SRC_DIR}/scripts/checkUpdates.sh -arch`), updateCommand: opt(`${SRC_DIR}/scripts/checkUpdates.sh -arch`),
label: opt(true), label: opt(true),
padZero: opt(true), padZero: opt(true),
autoHide: opt(false),
icon: { icon: {
pending: opt('󰏗'), pending: opt('󰏗'),
updated: opt('󰏖'), updated: opt('󰏖'),
@@ -1196,7 +1202,7 @@ const options = mkOptions(CONFIG, {
logout: opt('hyprctl dispatch exit'), logout: opt('hyprctl dispatch exit'),
shutdown: opt('systemctl poweroff'), shutdown: opt('systemctl poweroff'),
avatar: { avatar: {
image: opt('$HOME/.face.icon'), image: opt('~/.face.icon'),
name: opt<'system' | string>('system'), name: opt<'system' | string>('system'),
}, },
}, },

View File

@@ -1,5 +1,5 @@
import icons from '../lib/icons/icons'; import icons from '../lib/icons/icons';
import { bash, dependencies, Notify, isAnImage } from '../lib/utils'; import { bash, dependencies, Notify, isAnImage, normalizePath } from '../lib/utils';
import options from '../options'; import options from '../options';
import Wallpaper from 'src/services/Wallpaper'; import Wallpaper from 'src/services/Wallpaper';
@@ -8,7 +8,7 @@ const { matugen } = options.theme;
const ensureMatugenWallpaper = (): void => { const ensureMatugenWallpaper = (): void => {
const wallpaperPath = options.wallpaper.image.get(); const wallpaperPath = options.wallpaper.image.get();
if (matugen.get() && (!options.wallpaper.image.get().length || !isAnImage(wallpaperPath))) { if (matugen.get() && (!wallpaperPath.length || !isAnImage(normalizePath(wallpaperPath)))) {
Notify({ Notify({
summary: 'Matugen Failed', summary: 'Matugen Failed',
body: "Please select a wallpaper in 'Theming > General' first.", body: "Please select a wallpaper in 'Theming > General' first.",

View File

@@ -31,6 +31,18 @@
color: if($bar-menus-monochrome, $bar-menus-icons-active, $bar-menus-menu-volume-icons-active); color: if($bar-menus-monochrome, $bar-menus-icons-active, $bar-menus-menu-volume-icons-active);
} }
.menu-icon-button.volume {
color: if($bar-menus-monochrome, $bar-menus-iconbuttons-passive, $bar-menus-menu-volume-iconbutton-passive);
padding: 0em 1em;
label {
font-size: 1.25em;
}
&:hover {
color: if($bar-menus-monochrome, $bar-menus-iconbuttons-active, $bar-menus-menu-volume-iconbutton-active);
}
}
.menu-slider.playback { .menu-slider.playback {
trough { trough {
background: if( background: if(
@@ -212,4 +224,21 @@
.menu-items-section.playback { .menu-items-section.playback {
border-radius: 0em; border-radius: 0em;
} }
.active-playbacks-scrollable {
min-height: 12.5em;
scrollbar contents trough slider {
background: $bar-menus-menu-volume-scroller-color;
}
.menu-active-percentage.playback,
.menu-active-percentage.input {
margin-right: 0.6em;
}
}
.no-playbacks.dim {
opacity: 0.5;
}
} }

View File

@@ -202,7 +202,7 @@
.menu-scroller.bluetooth { .menu-scroller.bluetooth {
min-height: 18em; min-height: 18em;
slider { scrollbar contents trough slider {
background: $bar-menus-menu-bluetooth-scroller-color; background: $bar-menus-menu-bluetooth-scroller-color;
} }
} }

View File

@@ -259,7 +259,7 @@
margin-right: 0em; margin-right: 0em;
min-width: 0.35em; min-width: 0.35em;
slider { scrollbar contents trough slider {
min-width: $bar-menus-scroller-width; min-width: $bar-menus-scroller-width;
border-radius: $bar-menus-scroller-radius; border-radius: $bar-menus-scroller-radius;
background: $bar-menus-text; background: $bar-menus-text;

View File

@@ -185,7 +185,7 @@
.menu-scroller.wap { .menu-scroller.wap {
min-height: 16em; min-height: 16em;
slider { scrollbar contents trough slider {
background: $bar-menus-menu-network-scroller-color; background: $bar-menus-menu-network-scroller-color;
} }
} }