chore: control-center and center-window widgets to gtk4 and ags v3

This commit is contained in:
retrozinndev
2025-07-06 19:56:09 -03:00
parent 7b758bd298
commit 9db1d6fc12
37 changed files with 1477 additions and 1867 deletions
@@ -1,35 +0,0 @@
import { bind, Variable } from "astal";
import { Tile, TileProps } from "./Tile";
import AstalBluetooth from "gi://AstalBluetooth";
import { BluetoothPage } from "../pages/Bluetooth";
import { TilesPages } from "../Tiles";
export const TileBluetooth = () => {
const icon: Variable<string> = Variable.derive([
bind(AstalBluetooth.get_default(), "isPowered"),
bind(AstalBluetooth.get_default(), "isConnected")
],
(powered: boolean, isConnected: boolean) =>
powered ? ( isConnected ?
"bluetooth-active-symbolic"
: "bluetooth-symbolic"
) : "bluetooth-disabled-symbolic"
);
return Tile({
title: "Bluetooth",
visible: bind(AstalBluetooth.get_default(), "adapter").as(Boolean),
description: bind(AstalBluetooth.get_default(), "isConnected").as((connected) => {
const connectedDev = AstalBluetooth.get_default().devices.filter(dev => dev.connected)?.[0];
return connected && connectedDev ? connectedDev.get_alias() : ""
}),
onDestroy: () => icon.drop(),
onToggledOn: () => AstalBluetooth.get_default().adapter?.set_powered(true),
onToggledOff: () => AstalBluetooth.get_default().adapter?.set_powered(false),
onClickMore: () => TilesPages?.toggle(BluetoothPage()),
enableOnClickMore: true,
icon: icon(),
iconSize: 16,
toggleState: bind(AstalBluetooth.get_default(), "isPowered")
} as TileProps)();
}
@@ -0,0 +1,28 @@
import { Tile } from "./Tile";
import AstalBluetooth from "gi://AstalBluetooth";
import { BluetoothPage } from "../pages/Bluetooth";
import { TilesPages } from "../Tiles";
import { createBinding, createComputed } from "ags";
export const TileBluetooth = () =>
<Tile title={"Bluetooth"} visible={
createBinding(AstalBluetooth.get_default(), "adapter").as(Boolean)
} description={createBinding(AstalBluetooth.get_default(), "isConnected").as((connected) => {
const connectedDev = AstalBluetooth.get_default().devices.filter(dev => dev.connected)?.[0];
return connected && connectedDev ? connectedDev.get_alias() : ""
})} onToggledOn={() => AstalBluetooth.get_default().adapter?.set_powered(true)}
onToggledOff={() => AstalBluetooth.get_default().adapter?.set_powered(false)}
onClickMore={() => TilesPages?.toggle(BluetoothPage())}
enableOnClickMore={true} iconSize={16}
toggleState={createBinding(AstalBluetooth.get_default(), "isPowered")}
icon={createComputed([
createBinding(AstalBluetooth.get_default(), "isPowered"),
createBinding(AstalBluetooth.get_default(), "isConnected")
],
(powered: boolean, isConnected: boolean) =>
powered ? ( isConnected ?
"bluetooth-active-symbolic"
: "bluetooth-symbolic"
) : "bluetooth-disabled-symbolic")}
/>;
@@ -1,15 +0,0 @@
import { bind } from "astal";
import { Notifications } from "../../../scripts/notifications";
import { Tile } from "./Tile";
import { tr } from "../../../i18n/intl";
export const TileDND = Tile({
title: tr("control_center.tiles.dnd.title"),
description: bind(Notifications.getDefault().getNotifd(), "dontDisturb").as(
(dnd: boolean) => dnd ? tr("control_center.tiles.enabled") : tr("control_center.tiles.disabled")),
onToggledOff: () => Notifications.getDefault().getNotifd().dontDisturb = false,
onToggledOn: () => Notifications.getDefault().getNotifd().dontDisturb = true,
icon: "minus-circle-filled-symbolic",
iconSize: 16,
toggleState: Notifications.getDefault().getNotifd().dontDisturb
});
@@ -0,0 +1,15 @@
import { Notifications } from "../../../scripts/notifications";
import { Tile } from "./Tile";
import { tr } from "../../../i18n/intl";
import { createBinding } from "ags";
export const TileDND = () =>
<Tile title={tr("control_center.tiles.dnd.title")}
description={createBinding(Notifications.getDefault().getNotifd(), "dontDisturb").as(
(dnd: boolean) => dnd ? tr("control_center.tiles.enabled") : tr("control_center.tiles.disabled"))}
onToggledOff={() => Notifications.getDefault().getNotifd().dontDisturb = false}
onToggledOn={() => Notifications.getDefault().getNotifd().dontDisturb = true}
icon={"minus-circle-filled-symbolic"}
iconSize={16}
toggleState={Notifications.getDefault().getNotifd().dontDisturb}
/>;
@@ -1,86 +0,0 @@
import { bind, execAsync, Variable } from "astal";
import { Tile, TileProps } from "./Tile";
import AstalNetwork from "gi://AstalNetwork";
import { Widget } from "astal/gtk3";
import { PageNetwork } from "../pages/Network";
import { tr } from "../../../i18n/intl";
import { TilesPages } from "../Tiles";
export const TileNetwork = () => new Widget.Box({
child: Variable.derive([
bind(AstalNetwork.get_default(), "primary"),
bind(AstalNetwork.get_default(), "wired"),
bind(AstalNetwork.get_default(), "wifi")
],
(primary: AstalNetwork.Primary, wired: AstalNetwork.Wired, wifi: AstalNetwork.Wifi) => {
if(primary === AstalNetwork.Primary.WIFI) {
return Tile({
title: tr("control_center.tiles.network.wireless"),
description: Variable.derive(
[ bind(wifi, "ssid"), bind(wifi, "internet") ],
(ssid: string, internet: AstalNetwork.Internet) =>
ssid ? ssid : (() => {
switch(internet) {
case AstalNetwork.Internet.CONNECTED:
return tr("connected");
case AstalNetwork.Internet.DISCONNECTED:
return tr("disconnected");
case AstalNetwork.Internet.CONNECTING:
return tr("connecting") + "...";
}
})()
)(),
onToggledOn: () => wifi.set_enabled(true),
onToggledOff: () => wifi.set_enabled(false),
onClickMore: () => TilesPages?.toggle(PageNetwork()),
icon: "network-wireless-signal-excellent-symbolic",
toggleState: bind(wifi, "enabled")
} as TileProps)();
} else if(primary === AstalNetwork.Primary.WIRED) {
return Tile({
title: tr("control_center.tiles.network.wired") || "Wired",
description: bind(wired, "internet").as((internet: AstalNetwork.Internet) => {
switch(internet) {
case AstalNetwork.Internet.CONNECTED:
return tr("connected");
case AstalNetwork.Internet.DISCONNECTED:
return tr("disconnected");
case AstalNetwork.Internet.CONNECTING:
return tr("connecting") + "...";
}
}),
onToggledOn: () => execAsync("nmcli n on"),
onToggledOff: () => execAsync("nmcli n off"),
onClickMore: () => TilesPages?.toggle(PageNetwork()),
icon: bind(wired, "internet").as((internet: AstalNetwork.Internet) => {
switch(internet) {
case AstalNetwork.Internet.CONNECTED:
return "network-wired-symbolic";
case AstalNetwork.Internet.DISCONNECTED:
return "network-wired-disconnected-symbolic";
}
return "network-wired-no-route-symbolic";
}),
iconSize: 16,
toggleState: bind(wired, "internet").as((internet: AstalNetwork.Internet) =>
internet === AstalNetwork.Internet.CONNECTING
|| internet === AstalNetwork.Internet.CONNECTED
)
} as TileProps)();
}
return Tile({
title: tr("control_center.tiles.network.network"),
description: tr("disconnected"),
onToggledOn: () => execAsync("nmcli n on"),
onToggledOff: () => execAsync("nmcli n off"),
onClickMore: () => TilesPages?.toggle(PageNetwork()),
icon: "network-wired-disconnected-symbolic",
iconSize: 16,
toggleState: bind(wired, "internet").as((internet: AstalNetwork.Internet) =>
internet === AstalNetwork.Internet.CONNECTING || internet === AstalNetwork.Internet.CONNECTED)
} as TileProps)();
})()
} as Widget.BoxProps);
@@ -0,0 +1,85 @@
import { execAsync } from "ags/process";
import { Tile } from "./Tile";
import AstalNetwork from "gi://AstalNetwork";
import { PageNetwork } from "../pages/Network";
import { tr } from "../../../i18n/intl";
import { TilesPages } from "../Tiles";
import { Gtk } from "ags/gtk4";
import { createBinding, createComputed, With } from "ags";
export const TileNetwork = () => <Gtk.Box>
<With value={createComputed([
createBinding(AstalNetwork.get_default(), "primary"),
createBinding(AstalNetwork.get_default(), "wired"),
createBinding(AstalNetwork.get_default(), "wifi")
])}>
{([primary, wired, wifi]: [AstalNetwork.Primary, AstalNetwork.Wired, AstalNetwork.Wifi]) => {
if(primary === AstalNetwork.Primary.WIFI) {
return <Tile title={tr("control_center.tiles.network.wireless")}
description={createComputed([
createBinding(wifi, "ssid"), createBinding(wifi, "internet")
], (ssid, internet) => ssid ? ssid : (() => {
switch(internet) {
case AstalNetwork.Internet.CONNECTED:
return tr("connected");
case AstalNetwork.Internet.DISCONNECTED:
return tr("disconnected");
case AstalNetwork.Internet.CONNECTING:
return tr("connecting") + "...";
}
})()
)} onToggledOn={() => wifi.set_enabled(true)}
onToggledOff={() => wifi.set_enabled(false)}
onClickMore={() => TilesPages?.toggle(PageNetwork())}
icon={"network-wireless-signal-excellent-symbolic"}
toggleState={createBinding(wifi, "enabled")}
/>
} else if(primary === AstalNetwork.Primary.WIRED) {
return <Tile title={tr("control_center.tiles.network.wired")}
description={createBinding(wired, "internet").as((internet: AstalNetwork.Internet) => {
switch(internet) {
case AstalNetwork.Internet.CONNECTED:
return tr("connected");
case AstalNetwork.Internet.DISCONNECTED:
return tr("disconnected");
case AstalNetwork.Internet.CONNECTING:
return tr("connecting") + "...";
}
})}
onToggledOn={() => execAsync("nmcli n on")}
onToggledOff={() => execAsync("nmcli n off")}
onClickMore={() => TilesPages?.toggle(PageNetwork())}
icon={createBinding(wired, "internet").as((internet: AstalNetwork.Internet) => {
switch(internet) {
case AstalNetwork.Internet.CONNECTED:
return "network-wired-symbolic";
case AstalNetwork.Internet.DISCONNECTED:
return "network-wired-disconnected-symbolic";
}
return "network-wired-no-route-symbolic";
})}
iconSize={16}
toggleState={createBinding(wired, "internet").as((internet: AstalNetwork.Internet) =>
internet === AstalNetwork.Internet.CONNECTING
|| internet === AstalNetwork.Internet.CONNECTED
)}
/>
}
return <Tile
title={tr("control_center.tiles.network.network")}
description={tr("disconnected")}
onToggledOn={() => execAsync("nmcli n on")}
onToggledOff={() => execAsync("nmcli n off")}
onClickMore={() => TilesPages?.toggle(PageNetwork())}
icon={"network-wired-disconnected-symbolic"}
iconSize={16}
toggleState={createBinding(wired, "internet").as((internet: AstalNetwork.Internet) =>
internet === AstalNetwork.Internet.CONNECTING || internet === AstalNetwork.Internet.CONNECTED)}
/>
}}
</With>
</Gtk.Box> as Gtk.Box;
@@ -1,26 +0,0 @@
import { bind, Variable } from "astal";
import { Tile, TileProps } from "./Tile";
import { NightLight } from "../../../scripts/nightlight";
import { PageNightLight } from "../pages/NightLight";
import { tr } from "../../../i18n/intl";
import { TilesPages } from "../Tiles";
import { isInstalled } from "../../../scripts/utils";
import { Widget } from "astal/gtk3";
export const TileNightLight = () => isInstalled("hyprsunset") ? Tile({
title: tr("control_center.tiles.night_light.title"),
icon: "weather-clear-night-symbolic",
description: Variable.derive([
bind(NightLight.getDefault(), "temperature"),
bind(NightLight.getDefault(), "gamma")
], (temp, gamma) => `${temp === NightLight.getDefault().identityTemperature ?
tr("control_center.tiles.night_light.default_desc") : `${temp}K`} ${
gamma < NightLight.getDefault().maxGamma ? `(${gamma}%)` : ""}`
)(),
onToggledOff: () => NightLight.getDefault().identity = true,
onToggledOn: () => NightLight.getDefault().identity = false,
enableOnClickMore: true,
onClickMore: () => TilesPages?.toggle(PageNightLight()),
toggleState: bind(NightLight.getDefault(), "identity").as(identity => !identity)
} as TileProps)()
: new Widget.Box({ visible: false } as Widget.BoxProps);
@@ -0,0 +1,25 @@
import { Tile } from "./Tile";
import { NightLight } from "../../../scripts/nightlight";
import { PageNightLight } from "../pages/NightLight";
import { tr } from "../../../i18n/intl";
import { TilesPages } from "../Tiles";
import { isInstalled } from "../../../scripts/utils";
import { createBinding, createComputed } from "ags";
export const TileNightLight = () =>
<Tile title={tr("control_center.tiles.night_light.title")}
icon={"weather-clear-night-symbolic"}
description={createComputed([
createBinding(NightLight.getDefault(), "temperature"),
createBinding(NightLight.getDefault(), "gamma")
], (temp, gamma) => `${temp === NightLight.getDefault().identityTemperature ?
tr("control_center.tiles.night_light.default_desc") : `${temp}K`} ${
gamma < NightLight.getDefault().maxGamma ? `(${gamma}%)` : ""}`
)}
visible={isInstalled("hyprsunset")}
onToggledOff={() => NightLight.getDefault().identity = true}
onToggledOn={() => NightLight.getDefault().identity = false}
enableOnClickMore={true}
onClickMore={() => TilesPages?.toggle(PageNightLight())}
toggleState={createBinding(NightLight.getDefault(), "identity").as(identity => !identity)}
/>
@@ -1,38 +0,0 @@
import { Tile, TileProps } from "./Tile";
import { Recording } from "../../../scripts/recording";
import { bind, Variable } from "astal";
import { tr } from "../../../i18n/intl";
import { getDateTime } from "../../../scripts/time";
import { isInstalled } from "../../../scripts/utils";
const wfRecorderInstalled = isInstalled("wf-recorder");
export const TileRecording = () => {
const description: Variable<string> = Variable.derive([
bind(Recording.getDefault(), "recording"),
getDateTime()
], (recording, dateTime) => {
if(!recording || !Recording.getDefault().startedAt)
return tr("control_center.tiles.recording.disabled_desc") || "Start recording";
const startedAtSeconds = dateTime.to_unix() - Recording.getDefault().startedAt!.to_unix();
if(startedAtSeconds <= 0) return "00:00";
const minutes = Math.floor(startedAtSeconds / 60);
const seconds = Math.floor(startedAtSeconds % 60);
return `${ minutes < 10 ? `0${minutes}` : minutes }:${ seconds < 10 ? `0${seconds}` : seconds }`;
});
return Tile({
title: tr("control_center.tiles.recording.title") || "Screen Recording",
description: description(),
icon: "media-record-symbolic",
visible: wfRecorderInstalled,
onDestroy: () => description.drop(),
onToggledOff: () => Recording.getDefault().stopRecording(),
onToggledOn: () => Recording.getDefault().startRecording(),
toggleState: bind(Recording.getDefault(), "recording"),
iconSize: 16
} as TileProps)();
}
@@ -0,0 +1,32 @@
import { Tile } from "./Tile";
import { Recording } from "../../../scripts/recording";
import { tr } from "../../../i18n/intl";
import { isInstalled, time } from "../../../scripts/utils";
import { createBinding, createComputed } from "ags";
import { Gtk } from "ags/gtk4";
export const TileRecording = () =>
<Tile title={tr("control_center.tiles.recording.title")}
description={createComputed([
createBinding(Recording.getDefault(), "recording"),
time
], (recording, dateTime) => {
if(!recording || !Recording.getDefault().startedAt)
return tr("control_center.tiles.recording.disabled_desc") || "Start recording";
const startedAtSeconds = dateTime.to_unix() - Recording.getDefault().startedAt!;
if(startedAtSeconds <= 0) return "00:00";
const minutes = Math.floor(startedAtSeconds / 60);
const seconds = Math.floor(startedAtSeconds % 60);
return `${ minutes < 10 ? `0${minutes}` : minutes }:${ seconds < 10 ? `0${seconds}` : seconds }`;
})}
icon={"media-record-symbolic"}
visible={isInstalled("wf-recorder")}
onToggledOff={() => Recording.getDefault().stopRecording()}
onToggledOn={() => Recording.getDefault().startRecording()}
toggleState={createBinding(Recording.getDefault(), "recording")}
iconSize={16}
/> as Gtk.Widget;
-129
View File
@@ -1,129 +0,0 @@
import { Binding, Variable } from "astal";
import { Gtk, Widget } from "astal/gtk3";
import { tr } from "../../../i18n/intl";
export type TileProps = {
className?: string | Binding<string | undefined>;
icon?: string | Binding<string | undefined>;
visible?: boolean | Binding<boolean | undefined>;
iconSize?: number | Binding<number | undefined>;
title: string | Binding<string | undefined>;
description?: string | Binding<string | undefined>;
toggleState?: boolean | Binding<boolean | undefined>;
enableOnClickMore?: boolean | Binding<boolean | undefined>;
onDestroy?: () => void;
onToggledOn: () => void;
onToggledOff: () => void;
onClickMore?: () => void;
}
export function Tile(props: TileProps): (() => Gtk.Widget) {
const subs: Array<() => void> = [];
const toggled = new Variable<boolean>(((props.toggleState instanceof Binding) ?
props.toggleState.get()
: props.toggleState) ?? false);
if(props?.toggleState instanceof Binding)
subs.push(props.toggleState.subscribe((state) =>
toggled.set(state ?? false)
));
return () => new Widget.Box({
className: (props.className instanceof Binding) ?
Variable.derive([
props.className,
toggled()
], (className, isToggled) =>
`tile ${className} ${isToggled ? "toggled" : ""} ${
props.onClickMore ? "has-more" : ""
}`
)()
: toggled().as((state: boolean) =>
`tile${state ? " toggled" : ""}${
props.onClickMore ? " has-more" : ""
}`
),
expand: true,
visible: props.visible,
onDestroy: () => {
subs.map(sub => sub?.());
props.onDestroy?.();
},
children: [
new Widget.Button({
className: "toggle-button",
onClick: () => {
if(toggled.get()) {
toggled.set(false);
props.onToggledOff && props.onToggledOff();
return;
}
toggled.set(true);
props.onToggledOn && props.onToggledOn();
},
child: new Widget.Box({
className: "content",
expand: true,
hexpand: true,
children: [
new Widget.Icon({
className: "icon",
icon: props.icon,
visible: (props.icon instanceof Binding) ?
props.icon.as(Boolean)
: Boolean(props.icon),
css: `font-size: ${props.iconSize ?? 16}px;`
} as Widget.IconProps),
new Widget.Box({
className: "text",
orientation: Gtk.Orientation.VERTICAL,
vexpand: true,
hexpand: true,
valign: Gtk.Align.CENTER,
children: [
new Widget.Label({
className: "title",
xalign: 0,
halign: Gtk.Align.START,
truncate: true,
label: props.title
} as Widget.LabelProps),
new Widget.Label({
className: "description",
visible: (props.description instanceof Binding) ?
props.description.as(Boolean)
: Boolean(props.description),
halign: Gtk.Align.START,
truncate: true,
xalign: 0,
label: (props.description instanceof Binding) ?
props.description.as((desc) => desc ? desc : "")
: (props.description || "")
} as Widget.LabelProps)
]
} as Widget.BoxProps)
]
} as Widget.BoxProps)
} as Widget.ButtonProps),
new Widget.Button({
className: "more icon",
visible: props.onClickMore !== undefined,
halign: Gtk.Align.END,
tooltipText: tr("control_center.tiles.more") || "More",
image: new Widget.Icon({
icon: "go-next-symbolic",
css: "icon { font-size: 16px; }"
}),
onClick: () => {
((props.enableOnClickMore instanceof Binding) ?
props.enableOnClickMore.get()
: props.enableOnClickMore) && props?.onToggledOn();
props.onClickMore && props?.onClickMore()
},
widthRequest: 32
})
]
});
}
+108
View File
@@ -0,0 +1,108 @@
import { Gdk, Gtk } from "ags/gtk4";
import { tr } from "../../../i18n/intl";
import { Accessor, createComputed, createState } from "ags";
import GObject from "gi://GObject?version=2.0";
import Pango from "gi://Pango?version=1.0";
import { variableToBoolean } from "../../../scripts/utils";
export type TileProps = {
class?: string | Accessor<string>;
icon?: string | Accessor<string>;
visible?: boolean | Accessor<boolean>;
iconSize?: number | Accessor<number>;
title: string | Accessor<string>;
description?: string | Accessor<string>;
toggleState?: boolean | Accessor<boolean>;
enableOnClickMore?: boolean | Accessor<boolean>;
onDestroy?: () => void;
onToggledOn: () => void;
onToggledOff: () => void;
onClickMore?: () => void;
}
export function Tile(props: TileProps): Gtk.Widget {
const subs: Array<() => void> = [];
const [toggled, setToggled] = createState(((props.toggleState instanceof Accessor) ?
props.toggleState.get()
: props.toggleState) ?? false);
(props.toggleState instanceof Accessor) && subs.push(
props.toggleState.subscribe(() =>
setToggled((props.toggleState as Accessor<boolean>).get() ?? false))
);
return <Gtk.Box class={
(props.class instanceof Accessor) ?
createComputed([props.class, toggled], (clss, isToggled) =>
`tile ${clss} ${isToggled ? "toggled" : ""} ${
props.onClickMore ? "has-more" : ""
}`
)
: toggled.as(isToggled =>
`tile ${props.class ? props.class : ""} ${isToggled ? "toggled" : ""} ${
props.onClickMore ? "has-more" : ""
}`
)
} hexpand={true} visible={props.visible} onDestroy={(_) => {
subs.forEach(sub => sub());
props.onDestroy?.();
}}>
<Gtk.Button class={"toggle-button"} $={(self) => {
const gestureClick = Gtk.GestureClick.new();
const conns: Map<GObject.Object, number> = new Map();
self.add_controller(gestureClick);
conns.set(gestureClick, gestureClick.connect("released", (gesture) => {
if(gesture.get_current_button() === Gdk.BUTTON_PRIMARY) {
if(toggled.get()) {
setToggled(false);
props.onToggledOff?.();
return;
}
setToggled(true);
props.onToggledOn?.();
}
}));
}}>
<Gtk.Box class={"content"} hexpand={true} vexpand={true}>
{props.icon && <Gtk.Image class={"icon"} iconName={props.icon} css={
(props.iconSize instanceof Accessor) ?
props.iconSize.as(size => `font-size: ${size}px;`)
: (props.iconSize ?
`font-size: ${props.iconSize ?? 16}px;`
: undefined)
} />}
<Gtk.Box orientation={Gtk.Orientation.VERTICAL} class={"text"} vexpand={true} hexpand={true}
valign={Gtk.Align.CENTER}>
<Gtk.Label class={"title"} xalign={0} halign={Gtk.Align.START} ellipsize={Pango.EllipsizeMode.END}
label={props.title} />
{props.description && <Gtk.Label class={"description"} ellipsize={Pango.EllipsizeMode.END}
visible={variableToBoolean(props.description)} xalign={0} label={
(props.description instanceof Accessor) ?
props.description.as(str => str ?? "")
: (props.description ?? "")
} halign={Gtk.Align.START}
/>}
</Gtk.Box>
</Gtk.Box>
</Gtk.Button>
<Gtk.Button class={"more icon"} iconName={"go-next-symbolic"} widthRequest={32}
visible={Boolean(props.onClickMore)} halign={Gtk.Align.END} onClicked={() => {
((props.enableOnClickMore instanceof Accessor) ?
props.enableOnClickMore.get()
: props.enableOnClickMore) && props.onToggledOn?.();
props.onClickMore?.();
}} tooltipText={tr("control_center.tiles.more")} />
</Gtk.Box> as Gtk.Widget;
}