✨ chore: migrate bar widgets to ags v3 and gtk4
This commit is contained in:
@@ -1,20 +0,0 @@
|
|||||||
import { Gtk, Widget } from "astal/gtk3";
|
|
||||||
import { tr } from "../../i18n/intl";
|
|
||||||
import { Windows } from "../../windows";
|
|
||||||
import { bind } from "astal";
|
|
||||||
|
|
||||||
export function Apps(): Gtk.Widget {
|
|
||||||
return new Widget.EventBox({
|
|
||||||
onClickRelease: () => Windows.open("apps-window"),
|
|
||||||
className: bind(Windows, "openWindows").as((openWindows) =>
|
|
||||||
Object.hasOwn(openWindows, "apps-window") ? "apps open" : "apps"),
|
|
||||||
child: new Widget.Box({
|
|
||||||
child: new Widget.Icon({
|
|
||||||
tooltipText: tr("apps"),
|
|
||||||
icon: "applications-other-symbolic",
|
|
||||||
halign: Gtk.Align.CENTER,
|
|
||||||
hexpand: true
|
|
||||||
} as Widget.IconProps)
|
|
||||||
} as Widget.BoxProps)
|
|
||||||
} as Widget.EventBoxProps);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { Gtk } from "ags/gtk4";
|
||||||
|
import { Windows } from "../../windows";
|
||||||
|
import { createBinding } from "ags";
|
||||||
|
import { tr } from "../../i18n/intl";
|
||||||
|
|
||||||
|
export const Apps = () =>
|
||||||
|
<Gtk.Button class={createBinding(Windows.getDefault(), "openWindows").as((openWindows) =>
|
||||||
|
`apps ${Object.hasOwn(openWindows, "apps-window") ? "open" : ""}`
|
||||||
|
)} $={(self) => {
|
||||||
|
const conns: Array<number> = [
|
||||||
|
self.connect("clicked", (_) => Windows.getDefault().open("apps-window")),
|
||||||
|
self.connect("destroy", (_) => conns.forEach(id => self.disconnect(id)))
|
||||||
|
];
|
||||||
|
}} iconName={"applications-other-symbolic"} halign={Gtk.Align.CENTER}
|
||||||
|
hexpand={true} tooltipText={tr("apps")}
|
||||||
|
/>;
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { Gtk, Widget } from "astal/gtk3";
|
|
||||||
import { getDateTime } from "../../scripts/time";
|
|
||||||
import { bind, GLib } from "astal";
|
|
||||||
import { Windows } from "../../windows";
|
|
||||||
import { Config } from "../../scripts/config";
|
|
||||||
|
|
||||||
export function Clock(): Gtk.Widget {
|
|
||||||
return new Widget.Box({
|
|
||||||
className: bind(Windows, "openWindows").as((openWins) =>
|
|
||||||
Object.hasOwn(openWins, "center-window") ? "open clock" : "clock"),
|
|
||||||
child: new Widget.Button({
|
|
||||||
onClick: () => Windows.toggle("center-window"),
|
|
||||||
label: getDateTime().as((dateTime: GLib.DateTime) =>
|
|
||||||
dateTime.format(Config.getDefault().getProperty("clock.date_format", "string") as string))
|
|
||||||
} as Widget.ButtonProps)
|
|
||||||
} as Widget.BoxProps);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { Gtk } from "ags/gtk4";
|
||||||
|
import { Windows } from "../../windows";
|
||||||
|
import { createBinding } from "ags";
|
||||||
|
import { time } from "../../scripts/utils";
|
||||||
|
import { Config } from "../../scripts/config";
|
||||||
|
|
||||||
|
export const Clock = () =>
|
||||||
|
<Gtk.Button class={createBinding(Windows.getDefault(), "openWindows").as((wins) =>
|
||||||
|
`clock ${Object.hasOwn(wins, "center-window") ? "open" : ""}`)}
|
||||||
|
$={(self) => {
|
||||||
|
const conns: Array<number> = [
|
||||||
|
self.connect("clicked", (_) => Windows.getDefault().toggle("center-window")),
|
||||||
|
self.connect("destroy", (_) => conns.forEach(id => self.disconnect(id)))
|
||||||
|
];
|
||||||
|
}}
|
||||||
|
label={time((dt) => dt.format(
|
||||||
|
Config.getDefault().getProperty("clock.date_format", "string"))
|
||||||
|
?? "An error occurred"
|
||||||
|
)}
|
||||||
|
/>;
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import { bind } from "astal";
|
|
||||||
import { Gtk, Widget } from "astal/gtk3";
|
|
||||||
import AstalHyprland from "gi://AstalHyprland";
|
|
||||||
import { getAppIcon } from "../../scripts/apps";
|
|
||||||
|
|
||||||
const hyprland = AstalHyprland.get_default();
|
|
||||||
|
|
||||||
export function FocusedClient(): Gtk.Widget {
|
|
||||||
return new Widget.Box({
|
|
||||||
className: "focused-client",
|
|
||||||
visible: bind(hyprland, "focusedClient").as(fClient =>
|
|
||||||
!fClient ? false : (fClient?.initialClass == null ? false : true)),
|
|
||||||
children: bind(hyprland, "focusedClient").as(focusedClient => focusedClient ? [
|
|
||||||
new Widget.Icon({
|
|
||||||
className: "icon",
|
|
||||||
vexpand: true,
|
|
||||||
css: ".icon { font-size: 18px; }",
|
|
||||||
icon: bind(focusedClient, "class").as(clss =>
|
|
||||||
getAppIcon(clss) ?? "application-x-executable-symbolic")
|
|
||||||
}),
|
|
||||||
new Widget.Box({
|
|
||||||
className: "text-content",
|
|
||||||
orientation: Gtk.Orientation.VERTICAL,
|
|
||||||
homogeneous: false,
|
|
||||||
valign: Gtk.Align.CENTER,
|
|
||||||
children: [
|
|
||||||
new Widget.Label({
|
|
||||||
className: "class",
|
|
||||||
xalign: 0,
|
|
||||||
visible: bind(focusedClient, "class").as(Boolean),
|
|
||||||
maxWidthChars: 55,
|
|
||||||
truncate: true,
|
|
||||||
tooltipText: bind(focusedClient, "class").as(clientClass =>
|
|
||||||
clientClass ?? ""),
|
|
||||||
label: bind(focusedClient, "class").as(clientClass =>
|
|
||||||
clientClass ?? "no_class")
|
|
||||||
} as Widget.LabelProps),
|
|
||||||
new Widget.Label({
|
|
||||||
className: "title",
|
|
||||||
xalign: 0,
|
|
||||||
maxWidthChars: 50,
|
|
||||||
visible: bind(focusedClient, "title").as(Boolean),
|
|
||||||
truncate: true,
|
|
||||||
tooltipText: bind(focusedClient, "title").as((clientTitle: string) =>
|
|
||||||
clientTitle ?? ""),
|
|
||||||
label: bind(focusedClient, "title").as(title =>
|
|
||||||
title ?? "")
|
|
||||||
} as Widget.LabelProps)
|
|
||||||
]
|
|
||||||
})
|
|
||||||
]: [])
|
|
||||||
} as Widget.BoxProps);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { Gtk } from "ags/gtk4";
|
||||||
|
import AstalHyprland from "gi://AstalHyprland";
|
||||||
|
import { createBinding, With } from "ags";
|
||||||
|
import { variableToBoolean } from "../../scripts/utils";
|
||||||
|
import { getAppIcon, getSymbolicIcon } from "../../scripts/apps";
|
||||||
|
import Pango from "gi://Pango?version=1.0";
|
||||||
|
|
||||||
|
const hyprland = AstalHyprland.get_default();
|
||||||
|
|
||||||
|
export const FocusedClient = () => {
|
||||||
|
const focusedClient = createBinding(hyprland, "focusedClient");
|
||||||
|
|
||||||
|
return <Gtk.Box class={"focused-client"}
|
||||||
|
visible={variableToBoolean(createBinding(hyprland, "focusedClient"))}>
|
||||||
|
<With value={focusedClient}>
|
||||||
|
{(focusedClient) => focusedClient && <Gtk.Box>
|
||||||
|
<Gtk.Image iconName={
|
||||||
|
createBinding(focusedClient, "class").as((clss) =>
|
||||||
|
getSymbolicIcon(clss) ?? getAppIcon(clss) ??
|
||||||
|
getAppIcon(focusedClient.initialClass) ??
|
||||||
|
"application-x-executable-symbolic")
|
||||||
|
} css={"font-size: 18px;"} vexpand={true} />
|
||||||
|
|
||||||
|
<Gtk.Box valign={Gtk.Align.CENTER} class={"text-content"}
|
||||||
|
orientation={Gtk.Orientation.VERTICAL}>
|
||||||
|
|
||||||
|
<Gtk.Label class={"class"} xalign={0} maxWidthChars={55}
|
||||||
|
ellipsize={Pango.EllipsizeMode.END}
|
||||||
|
label={createBinding(focusedClient, "class")}
|
||||||
|
tooltipText={createBinding(focusedClient, "class")}/>
|
||||||
|
<Gtk.Label class={"title"} xalign={0} maxWidthChars={50}
|
||||||
|
ellipsize={Pango.EllipsizeMode.END}
|
||||||
|
label={createBinding(focusedClient, "class")}
|
||||||
|
tooltipText={createBinding(focusedClient, "class")}/>
|
||||||
|
</Gtk.Box>
|
||||||
|
</Gtk.Box>}
|
||||||
|
</With>
|
||||||
|
</Gtk.Box>;
|
||||||
|
}
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
import { bind, exec } from "astal";
|
|
||||||
import { Gtk, Widget } from "astal/gtk3";
|
|
||||||
import AstalMpris from "gi://AstalMpris";
|
|
||||||
import { getSymbolicIcon } from "../../scripts/apps";
|
|
||||||
import { Separator, SeparatorProps } from "../Separator";
|
|
||||||
import { Windows } from "../../windows";
|
|
||||||
import { Clipboard } from "../../scripts/clipboard";
|
|
||||||
|
|
||||||
export function Media(): Gtk.Widget {
|
|
||||||
const connections: Array<number> = [];
|
|
||||||
|
|
||||||
const mediaControlsRevealer: Widget.Revealer = new Widget.Revealer({
|
|
||||||
transitionType: Gtk.RevealerTransitionType.SLIDE_RIGHT,
|
|
||||||
transitionDuration: 260,
|
|
||||||
revealChild: false,
|
|
||||||
child: new Widget.Box({
|
|
||||||
className: "media-controls button-row",
|
|
||||||
expand: false,
|
|
||||||
homogeneous: false,
|
|
||||||
children: bind(AstalMpris.get_default(), "players").as((players: Array<AstalMpris.Player>) =>
|
|
||||||
players[0] ? [
|
|
||||||
new Widget.Button({
|
|
||||||
className: "link",
|
|
||||||
image: new Widget.Icon({
|
|
||||||
icon: "edit-paste-symbolic"
|
|
||||||
} as Widget.IconProps),
|
|
||||||
tooltipText: "Copy link to Clipboard",
|
|
||||||
// AstalMpris.Player.metadata works only sometimes, so I'm not using it
|
|
||||||
visible: bind(players[0], "metadata").as(Boolean),
|
|
||||||
onClick: async () => {
|
|
||||||
const link = exec(`playerctl --player=${
|
|
||||||
players[0].busName.replace(/^org\.mpris\.MediaPlayer2\./i, "")
|
|
||||||
} metadata xesam:url`);
|
|
||||||
|
|
||||||
link && Clipboard.getDefault().copyAsync(link);
|
|
||||||
}
|
|
||||||
} as Widget.ButtonProps),
|
|
||||||
new Widget.Button({
|
|
||||||
className: "previous",
|
|
||||||
image: new Widget.Icon({
|
|
||||||
icon: "media-skip-backward-symbolic"
|
|
||||||
} as Widget.IconProps),
|
|
||||||
tooltipText: "Previous",
|
|
||||||
onClick: () => players[0].canGoPrevious && players[0].previous()
|
|
||||||
} as Widget.ButtonProps),
|
|
||||||
new Widget.Button({
|
|
||||||
className: "play-pause",
|
|
||||||
tooltipText: bind(players[0], "playback_status").as((status) =>
|
|
||||||
status === AstalMpris.PlaybackStatus.PLAYING ?
|
|
||||||
"Pause"
|
|
||||||
: "Play"),
|
|
||||||
image: new Widget.Icon({
|
|
||||||
icon: bind(players[0], "playbackStatus").as((status: AstalMpris.PlaybackStatus) =>
|
|
||||||
status === AstalMpris.PlaybackStatus.PLAYING ?
|
|
||||||
"media-playback-pause-symbolic"
|
|
||||||
: "media-playback-start-symbolic")
|
|
||||||
} as Widget.IconProps),
|
|
||||||
onClick: () => players[0].playbackStatus === AstalMpris.PlaybackStatus.PAUSED ?
|
|
||||||
players[0].play()
|
|
||||||
: players[0].pause()
|
|
||||||
} as Widget.ButtonProps),
|
|
||||||
new Widget.Button({
|
|
||||||
className: "next",
|
|
||||||
image: new Widget.Icon({
|
|
||||||
icon: "media-skip-forward-symbolic"
|
|
||||||
} as Widget.IconProps),
|
|
||||||
tooltipText: "Next",
|
|
||||||
onClick: () => players[0].canGoNext && players[0].next()
|
|
||||||
} as Widget.ButtonProps)
|
|
||||||
] : new Widget.Label({
|
|
||||||
label: "Don't Stop The Music!"
|
|
||||||
} as Widget.LabelProps)
|
|
||||||
)
|
|
||||||
} as Widget.BoxProps)
|
|
||||||
} as Widget.RevealerProps);
|
|
||||||
|
|
||||||
const mediaWidget = new Widget.EventBox({
|
|
||||||
className: "media-eventbox",
|
|
||||||
visible: bind(AstalMpris.get_default(), "players").as((players: Array<AstalMpris.Player>) =>
|
|
||||||
players[0] && players[0].get_available()),
|
|
||||||
onDestroy: (_) => connections.map(id => _.disconnect(id)),
|
|
||||||
onClick: () => Windows.toggle("center-window"),
|
|
||||||
child: new Widget.Box({
|
|
||||||
className: "media",
|
|
||||||
children: [
|
|
||||||
new Widget.Box({
|
|
||||||
spacing: 4,
|
|
||||||
children: bind(AstalMpris.get_default(), "players").as((players: Array<AstalMpris.Player>) =>
|
|
||||||
players[0] ? [
|
|
||||||
new Widget.Icon({
|
|
||||||
icon: bind(players[0], "busName").as((busName: string) => {
|
|
||||||
const splitName = busName.split('.').filter(str => str !== "" && !str.toLowerCase().includes('instance'));
|
|
||||||
if (getSymbolicIcon(splitName[splitName.length - 1])) {
|
|
||||||
return getSymbolicIcon(splitName[splitName.length - 1]);
|
|
||||||
} else {
|
|
||||||
return "folder-music-symbolic"
|
|
||||||
};
|
|
||||||
})
|
|
||||||
} as Widget.IconProps),
|
|
||||||
new Widget.Label({
|
|
||||||
className: "title",
|
|
||||||
label: bind(players[0], "title").as((title: string) => title || "No Title"),
|
|
||||||
maxWidthChars: 20,
|
|
||||||
truncate: true
|
|
||||||
} as Widget.LabelProps),
|
|
||||||
Separator({
|
|
||||||
orientation: Gtk.Orientation.HORIZONTAL,
|
|
||||||
size: 1,
|
|
||||||
margin: 5,
|
|
||||||
//cssColor: `rgb(180, 180, 180)`,
|
|
||||||
alpha: .3
|
|
||||||
} as SeparatorProps),
|
|
||||||
new Widget.Label({
|
|
||||||
className: "artist",
|
|
||||||
label: bind(players[0], "artist").as((artist: string) => artist || "No Artist"),
|
|
||||||
maxWidthChars: 18,
|
|
||||||
truncate: true
|
|
||||||
} as Widget.LabelProps)
|
|
||||||
] : new Widget.Label({
|
|
||||||
label: "Crazy to think this widget haven't disappeared yet!"
|
|
||||||
} as Widget.LabelProps)
|
|
||||||
)
|
|
||||||
} as Widget.BoxProps),
|
|
||||||
mediaControlsRevealer
|
|
||||||
]
|
|
||||||
} as Widget.BoxProps)
|
|
||||||
} as Widget.EventBoxProps);
|
|
||||||
|
|
||||||
connections.push(
|
|
||||||
mediaWidget.connect("hover", () => {
|
|
||||||
mediaControlsRevealer.set_reveal_child(true);
|
|
||||||
mediaWidget.className = mediaWidget.className + " reveal";
|
|
||||||
}),
|
|
||||||
mediaWidget.connect("hover-lost", (_) => {
|
|
||||||
mediaControlsRevealer.set_reveal_child(false);
|
|
||||||
_.className = mediaWidget.className.replaceAll(" reveal", "");
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return mediaWidget;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
import { createBinding, createState, With } from "ags";
|
||||||
|
import { execAsync } from "ags/process";
|
||||||
|
import { Gtk } from "ags/gtk4";
|
||||||
|
import { getSymbolicIcon } from "../../scripts/apps";
|
||||||
|
import { Separator } from "../Separator";
|
||||||
|
import { Windows } from "../../windows";
|
||||||
|
import { Clipboard } from "../../scripts/clipboard";
|
||||||
|
|
||||||
|
import GObject from "ags/gobject";
|
||||||
|
import AstalMpris from "gi://AstalMpris";
|
||||||
|
import Pango from "gi://Pango?version=1.0";
|
||||||
|
|
||||||
|
|
||||||
|
export const dummyPlayer = AstalMpris.Player.new("colorshellDummy");
|
||||||
|
|
||||||
|
export let [player, setPlayer] = createState(dummyPlayer);
|
||||||
|
|
||||||
|
export const Media = () => {
|
||||||
|
const connections: Map<GObject.Object, Array<number>|number> = new Map();
|
||||||
|
|
||||||
|
if(AstalMpris.get_default().players[0] && player.get() !== dummyPlayer)
|
||||||
|
setPlayer(AstalMpris.get_default().players[0]);
|
||||||
|
|
||||||
|
connections.set(AstalMpris.get_default(), [
|
||||||
|
AstalMpris.get_default().connect("player-added", (_, player) =>
|
||||||
|
player.available && setPlayer(player)),
|
||||||
|
|
||||||
|
AstalMpris.get_default().connect("player-closed", (_, closedPlayer) => {
|
||||||
|
if(player.get()?.busName !== closedPlayer.busName)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const players = AstalMpris.get_default().players.filter(pl => pl?.available);
|
||||||
|
|
||||||
|
if(players.length > 0) {
|
||||||
|
setPlayer(players[0]);
|
||||||
|
return;
|
||||||
|
} else setPlayer(dummyPlayer);
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
return <Gtk.Box class={"media"} visible={player((pl) => pl.available)}
|
||||||
|
$={(self) => {
|
||||||
|
const gestureClick = Gtk.GestureClick.new(),
|
||||||
|
controllerMotion = Gtk.EventControllerMotion.new(),
|
||||||
|
controllerScroll = Gtk.EventControllerScroll.new(
|
||||||
|
Gtk.EventControllerScrollFlags.VERTICAL);
|
||||||
|
|
||||||
|
self.add_controller(gestureClick);
|
||||||
|
self.add_controller(controllerMotion);
|
||||||
|
self.add_controller(controllerScroll);
|
||||||
|
|
||||||
|
connections.set(gestureClick, gestureClick.connect("released", () =>
|
||||||
|
Windows.getDefault().toggle("center-window")));
|
||||||
|
|
||||||
|
connections.set(controllerScroll,
|
||||||
|
controllerScroll.connect("scroll", (_, _dx, dy) => {
|
||||||
|
if(AstalMpris.get_default().players.length === 1 &&
|
||||||
|
player.get()?.busName === AstalMpris.get_default().players[0].busName)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
const players = AstalMpris.get_default().players;
|
||||||
|
|
||||||
|
for(let i = 0; i < players.length; i++) {
|
||||||
|
const pl = players[i];
|
||||||
|
|
||||||
|
if(pl.busName !== player.get().busName)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if(dy > 0 && players[i-1]) {
|
||||||
|
setPlayer(players[i-1]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(dy < 0 && players[i+1]) {
|
||||||
|
setPlayer(players[i+1]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
connections.set(controllerMotion, [
|
||||||
|
controllerMotion.connect("enter", () => {
|
||||||
|
const revealer = self.get_last_child() as Gtk.Revealer;
|
||||||
|
revealer.set_reveal_child(true);
|
||||||
|
}),
|
||||||
|
controllerMotion.connect("leave", () => {
|
||||||
|
const revealer = self.get_last_child() as Gtk.Revealer;
|
||||||
|
revealer.set_reveal_child(false);
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
connections.set(self, self.connect("destroy", () =>
|
||||||
|
connections.forEach((ids, obj) => Array.isArray(ids) ?
|
||||||
|
ids.forEach(id => obj.disconnect(id))
|
||||||
|
: obj.disconnect(ids))
|
||||||
|
));
|
||||||
|
}}>
|
||||||
|
|
||||||
|
<Gtk.Box spacing={4} visible={player(pl => pl.available)}>
|
||||||
|
<With value={player(pl => pl.available)}>
|
||||||
|
{(available: boolean) => available && <Gtk.Box>
|
||||||
|
<Gtk.Image iconName={createBinding(player.get(), "busName").as((busName) => {
|
||||||
|
const splitName = busName.split('.').filter(str => str !== "" && !str.toLowerCase().includes('instance'));
|
||||||
|
return getSymbolicIcon(splitName[splitName.length - 1]) ?
|
||||||
|
getSymbolicIcon(splitName[splitName.length - 1])!
|
||||||
|
: "folder-music-symbolic";
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<Gtk.Label class={"title"} label={createBinding(player.get(), "title").as(title =>
|
||||||
|
title ?? "No Title")} maxWidthChars={20} ellipsize={Pango.EllipsizeMode.END}
|
||||||
|
/>
|
||||||
|
<Separator orientation={Gtk.Orientation.HORIZONTAL} size={1} margin={5}
|
||||||
|
alpha={.3} />
|
||||||
|
<Gtk.Label class={"artist"} label={createBinding(player.get(), "artist").as(artist =>
|
||||||
|
artist ?? "No Artist")} maxWidthChars={18} ellipsize={Pango.EllipsizeMode.END}
|
||||||
|
/>
|
||||||
|
</Gtk.Box>}
|
||||||
|
</With>
|
||||||
|
</Gtk.Box>
|
||||||
|
<Gtk.Revealer transitionType={Gtk.RevealerTransitionType.SLIDE_RIGHT} transitionDuration={260}
|
||||||
|
revealChild={false}>
|
||||||
|
|
||||||
|
<With value={player(pl => pl.available)}>
|
||||||
|
{(available: boolean) => available && <Gtk.Box class={"media-controls button-row"}>
|
||||||
|
<Gtk.Button class={"link"} iconName={"edit-paste-symbolic"}
|
||||||
|
tooltipText={"Copy link to Clipboard"} onClicked={() => {
|
||||||
|
execAsync(`playerctl --player=${
|
||||||
|
player.get().busName.replace(/^org\.mpris\.MediaPlayer2\./i, "")
|
||||||
|
} metadata xesam:url`).then(link => {
|
||||||
|
Clipboard.getDefault().copyAsync(link);
|
||||||
|
}).catch((e: Error) => {
|
||||||
|
console.error(`Media: couldn't copy media link. Stderr: \n${e.message}\n${e.stack}`);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Gtk.Button class={"previous"} iconName={"media-skip-backward-symbolic"}
|
||||||
|
tooltipText={"Previous"} onClicked={() =>
|
||||||
|
player.get().canGoPrevious && player.get().previous()}
|
||||||
|
/>
|
||||||
|
<Gtk.Button class={"play-pause"} iconName={createBinding(player.get(), "playbackStatus").as(status =>
|
||||||
|
status === AstalMpris.PlaybackStatus.PAUSED ?
|
||||||
|
"media-playback-start-symbolic"
|
||||||
|
: "media-playback-pause-symbolic")}
|
||||||
|
tooltipText={
|
||||||
|
createBinding(player.get(), "playbackStatus").as(status =>
|
||||||
|
status === AstalMpris.PlaybackStatus.PAUSED ? "Play" : "Pause")
|
||||||
|
} onClicked={player.get().play_pause}
|
||||||
|
/>
|
||||||
|
<Gtk.Button class={"next"} iconName={"media-skip-forward-symbolic"}
|
||||||
|
tooltipText={"Next"} onClicked={() => player.get().canGoNext &&
|
||||||
|
player.get().next()}
|
||||||
|
/>
|
||||||
|
</Gtk.Box>}
|
||||||
|
</With>
|
||||||
|
</Gtk.Revealer>
|
||||||
|
</Gtk.Box>
|
||||||
|
}
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
import AstalBluetooth from "gi://AstalBluetooth";
|
|
||||||
import AstalNetwork from "gi://AstalNetwork";
|
|
||||||
import AstalWp from "gi://AstalWp";
|
|
||||||
|
|
||||||
import { bind, Binding, Variable } from "astal";
|
|
||||||
import { Gtk, Widget } from "astal/gtk3";
|
|
||||||
import { Wireplumber } from "../../scripts/volume";
|
|
||||||
import { Notifications } from "../../scripts/notifications";
|
|
||||||
import { Windows } from "../../windows";
|
|
||||||
import { Recording } from "../../scripts/recording";
|
|
||||||
import { getDateTime } from "../../scripts/time";
|
|
||||||
import { tr } from "../../i18n/intl";
|
|
||||||
|
|
||||||
|
|
||||||
export function Status(): Gtk.Widget {
|
|
||||||
const recordingTimer: Variable<string> = Variable.derive([
|
|
||||||
bind(Recording.getDefault(), "recording"),
|
|
||||||
getDateTime()
|
|
||||||
], (recording, dateTime) => {
|
|
||||||
if(!recording || !Recording.getDefault().startedAt)
|
|
||||||
return "...";
|
|
||||||
|
|
||||||
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 new Widget.EventBox({
|
|
||||||
className: bind(Windows, "openWindows").as((openWins) =>
|
|
||||||
Object.hasOwn(openWins, "control-center") ? "open status" : "status"),
|
|
||||||
onClick: () => Windows.toggle("control-center"),
|
|
||||||
child: new Widget.Box({
|
|
||||||
children: [
|
|
||||||
new Widget.Box({
|
|
||||||
className: "volume-indicators",
|
|
||||||
spacing: 5,
|
|
||||||
children: [
|
|
||||||
volumeStatus({
|
|
||||||
className: "sink",
|
|
||||||
endpoint: Wireplumber.getDefault().getDefaultSink(),
|
|
||||||
icon: bind(Wireplumber.getDefault().getDefaultSink(), "volumeIcon").as(icon =>
|
|
||||||
!Wireplumber.getDefault().isMutedSink() && Wireplumber.getDefault().getSinkVolume() > 0 ?
|
|
||||||
icon : "audio-volume-muted-symbolic"),
|
|
||||||
}),
|
|
||||||
volumeStatus({
|
|
||||||
className: "source",
|
|
||||||
endpoint: Wireplumber.getDefault().getDefaultSource(),
|
|
||||||
icon: bind(Wireplumber.getDefault().getDefaultSource(), "volumeIcon").as(icon =>
|
|
||||||
!Wireplumber.getDefault().isMutedSource() && Wireplumber.getDefault().getSourceVolume() > 0 ?
|
|
||||||
icon : "microphone-sensitivity-muted-symbolic"),
|
|
||||||
})
|
|
||||||
]
|
|
||||||
} as Widget.BoxProps),
|
|
||||||
new Widget.Revealer({
|
|
||||||
revealChild: bind(Recording.getDefault(), "recording"),
|
|
||||||
transitionDuration: 500,
|
|
||||||
transitionType: Gtk.RevealerTransitionType.SLIDE_LEFT,
|
|
||||||
onDestroy: () => recordingTimer.drop(),
|
|
||||||
child: new Widget.EventBox({
|
|
||||||
onClick: () => Recording.getDefault().recording &&
|
|
||||||
Recording.getDefault().stopRecording(),
|
|
||||||
tooltipText: tr("control_center.tiles.recording.enabled_desc"),
|
|
||||||
child: new Widget.Box({
|
|
||||||
children: [
|
|
||||||
new Widget.Icon({
|
|
||||||
className: "recording state",
|
|
||||||
icon: "media-record-symbolic",
|
|
||||||
css: "margin-right: 4px;"
|
|
||||||
} as Widget.IconProps),
|
|
||||||
new Widget.Label({
|
|
||||||
className: "rec-time",
|
|
||||||
label: recordingTimer()
|
|
||||||
} as Widget.LabelProps)
|
|
||||||
]
|
|
||||||
} as Widget.BoxProps)
|
|
||||||
} as Widget.EventBoxProps)
|
|
||||||
} as Widget.RevealerProps),
|
|
||||||
StatusIcons()
|
|
||||||
]
|
|
||||||
} as Widget.BoxProps)
|
|
||||||
} as Widget.EventBoxProps);
|
|
||||||
}
|
|
||||||
|
|
||||||
function volumeStatus(props: { className?: string, endpoint: AstalWp.Endpoint, icon?: (string|Binding<string>) }): Gtk.Widget {
|
|
||||||
return new Widget.EventBox({
|
|
||||||
className: props.className,
|
|
||||||
onScroll: (_, event) =>
|
|
||||||
event.delta_y > 0 ?
|
|
||||||
Wireplumber.getDefault().decreaseEndpointVolume(props.endpoint, 5)
|
|
||||||
: Wireplumber.getDefault().increaseEndpointVolume(props.endpoint, 5),
|
|
||||||
child: new Widget.Box({
|
|
||||||
spacing: 2,
|
|
||||||
children: [
|
|
||||||
new Widget.Icon({
|
|
||||||
visible: props.icon,
|
|
||||||
icon: props.icon,
|
|
||||||
} as Widget.IconProps),
|
|
||||||
new Widget.Label({
|
|
||||||
className: "volume",
|
|
||||||
label: bind(props.endpoint, "volume").as((volume: number) =>
|
|
||||||
Math.floor(volume * 100) + "%")
|
|
||||||
} as Widget.LabelProps),
|
|
||||||
]
|
|
||||||
} as Widget.BoxProps)
|
|
||||||
} as Widget.EventBoxProps)
|
|
||||||
}
|
|
||||||
|
|
||||||
function StatusIcons(): Gtk.Widget {
|
|
||||||
const bluetoothIcon: Variable<string> = Variable.derive([
|
|
||||||
bind(AstalBluetooth.get_default(), "isPowered"),
|
|
||||||
bind(AstalBluetooth.get_default(), "isConnected")
|
|
||||||
], (powered, connected) => {
|
|
||||||
return powered ? (
|
|
||||||
connected ?
|
|
||||||
"bluetooth-active-symbolic"
|
|
||||||
: "bluetooth-symbolic"
|
|
||||||
) : "bluetooth-disabled-symbolic"
|
|
||||||
});
|
|
||||||
|
|
||||||
const networkIcon: Variable<string> = Variable.derive([
|
|
||||||
bind(AstalNetwork.get_default(), "primary"),
|
|
||||||
],
|
|
||||||
(primary) => {
|
|
||||||
switch(primary) {
|
|
||||||
case AstalNetwork.Primary.WIRED: return AstalNetwork.get_default().wired.get_icon_name();
|
|
||||||
|
|
||||||
case AstalNetwork.Primary.WIFI: return AstalNetwork.get_default().wifi.get_icon_name();
|
|
||||||
}
|
|
||||||
|
|
||||||
return "network-no-route-symbolic";
|
|
||||||
});
|
|
||||||
|
|
||||||
return new Widget.Box({
|
|
||||||
className: "status-icons",
|
|
||||||
spacing: 8,
|
|
||||||
children: [
|
|
||||||
new Widget.Icon({
|
|
||||||
className: "bluetooth state",
|
|
||||||
visible: bind(AstalBluetooth.get_default(), "adapter").as(Boolean),
|
|
||||||
icon: bluetoothIcon(),
|
|
||||||
onDestroy: () => bluetoothIcon.drop()
|
|
||||||
} as Widget.IconProps),
|
|
||||||
new Widget.Icon({
|
|
||||||
className: "network state",
|
|
||||||
icon: networkIcon(),
|
|
||||||
onDestroy: () => networkIcon.drop()
|
|
||||||
} as Widget.IconProps),
|
|
||||||
new Widget.Box({
|
|
||||||
children: [
|
|
||||||
new Widget.Icon({
|
|
||||||
className: "bell state",
|
|
||||||
icon: bind(Notifications.getDefault().getNotifd(), "dontDisturb").as((dnd) =>
|
|
||||||
dnd ? "minus-circle-filled-symbolic"
|
|
||||||
: "preferences-system-notifications-symbolic")
|
|
||||||
} as Widget.IconProps),
|
|
||||||
new Widget.Icon({
|
|
||||||
className: "notification-count",
|
|
||||||
visible: bind(Notifications.getDefault(), "history").as(history =>
|
|
||||||
history.length > 0),
|
|
||||||
icon: "circle-filled-symbolic"
|
|
||||||
} as Widget.IconProps)
|
|
||||||
]
|
|
||||||
} as Widget.BoxProps)
|
|
||||||
]
|
|
||||||
} as Widget.BoxProps);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import { Gtk } from "ags/gtk4";
|
||||||
|
import { Wireplumber } from "../../scripts/volume";
|
||||||
|
import { Notifications } from "../../scripts/notifications";
|
||||||
|
import { Windows } from "../../windows";
|
||||||
|
import { Recording } from "../../scripts/recording";
|
||||||
|
import { Accessor, createBinding, createComputed } from "ags";
|
||||||
|
import { time, variableToBoolean } from "../../scripts/utils";
|
||||||
|
|
||||||
|
import AstalBluetooth from "gi://AstalBluetooth";
|
||||||
|
import AstalNetwork from "gi://AstalNetwork";
|
||||||
|
import AstalWp from "gi://AstalWp";
|
||||||
|
import GObject from "gi://GObject?version=2.0";
|
||||||
|
|
||||||
|
|
||||||
|
export const Status = () =>
|
||||||
|
<Gtk.Button class={createBinding(Windows.getDefault(), "openWindows").as((openWins) =>
|
||||||
|
Object.hasOwn(openWins, "control-center") ? "open status" : "status")}
|
||||||
|
onClicked={() => Windows.getDefault().toggle("control-center")}>
|
||||||
|
|
||||||
|
<Gtk.Box>
|
||||||
|
<Gtk.Box class={"volume-indicators"} spacing={5}>
|
||||||
|
<VolumeStatus class="sink" endpoint={Wireplumber.getDefault().getDefaultSink()}
|
||||||
|
icon={createBinding(Wireplumber.getDefault().getDefaultSink(), "volumeIcon").as(icon =>
|
||||||
|
!Wireplumber.getDefault().isMutedSink() &&
|
||||||
|
Wireplumber.getDefault().getSinkVolume() > 0 ?
|
||||||
|
icon
|
||||||
|
: "audio-volume-muted-symbolic")
|
||||||
|
} />
|
||||||
|
|
||||||
|
<VolumeStatus class="source" endpoint={Wireplumber.getDefault().getDefaultSource()}
|
||||||
|
icon={createBinding(Wireplumber.getDefault().getDefaultSource(), "volumeIcon").as(icon =>
|
||||||
|
!Wireplumber.getDefault().isMutedSource() &&
|
||||||
|
Wireplumber.getDefault().getSourceVolume() > 0 ?
|
||||||
|
icon
|
||||||
|
: "audio-volume-muted-symbolic")
|
||||||
|
} />
|
||||||
|
</Gtk.Box>
|
||||||
|
<Gtk.Revealer revealChild={createBinding(Recording.getDefault(), "recording")}
|
||||||
|
transitionDuration={500} transitionType={Gtk.RevealerTransitionType.SLIDE_LEFT}>
|
||||||
|
|
||||||
|
<Gtk.Box>
|
||||||
|
<Gtk.Image class={"recording state"} iconName={"media-record-symbolic"}
|
||||||
|
css={"margin-right: 6px;"} />
|
||||||
|
|
||||||
|
<Gtk.Label class={"rec-time"} label={createComputed([
|
||||||
|
createBinding(Recording.getDefault(), "recording"),
|
||||||
|
time
|
||||||
|
], (recording, dateTime) => {
|
||||||
|
if(!recording || !Recording.getDefault().startedAt)
|
||||||
|
return "...";
|
||||||
|
|
||||||
|
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 }`;
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Gtk.Box>
|
||||||
|
</Gtk.Revealer>
|
||||||
|
<StatusIcons />
|
||||||
|
</Gtk.Box>
|
||||||
|
</Gtk.Button> as Gtk.Button;
|
||||||
|
|
||||||
|
function VolumeStatus(props: { class?: string, endpoint: AstalWp.Endpoint, icon?: (string|Accessor<string>) }) {
|
||||||
|
return <Gtk.Box spacing={2} class={props.class} $={(self) => {
|
||||||
|
const conns: Map<GObject.Object, number> = new Map();
|
||||||
|
const controllerScroll = Gtk.EventControllerScroll.new(
|
||||||
|
Gtk.EventControllerScrollFlags.VERTICAL);
|
||||||
|
|
||||||
|
conns.set(controllerScroll, controllerScroll.connect("scroll", (_, _dx, dy) => {
|
||||||
|
(dy > 0) ?
|
||||||
|
Wireplumber.getDefault().decreaseEndpointVolume(props.endpoint, 5)
|
||||||
|
: Wireplumber.getDefault().increaseEndpointVolume(props.endpoint, 5);
|
||||||
|
}));
|
||||||
|
|
||||||
|
conns.set(self, self.connect("destroy", () => conns.forEach((id, obj) =>
|
||||||
|
obj.disconnect(id))));
|
||||||
|
}}>
|
||||||
|
|
||||||
|
{props.icon && <Gtk.Image iconName={props.icon} />}
|
||||||
|
<Gtk.Label class={"volume"} label={createBinding(props.endpoint, "volume").as(vol =>
|
||||||
|
`${Math.floor(vol * 100)}%`)} />
|
||||||
|
</Gtk.Box> as Gtk.Box;
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatusIcons() {
|
||||||
|
return <Gtk.Box class={"status-icons"} spacing={8}>
|
||||||
|
<Gtk.Image iconName={createComputed([
|
||||||
|
createBinding(AstalBluetooth.get_default(), "isPowered"),
|
||||||
|
createBinding(AstalBluetooth.get_default(), "isConnected")
|
||||||
|
], (powered, connected) => {
|
||||||
|
return powered ? (
|
||||||
|
connected ?
|
||||||
|
"bluetooth-active-symbolic"
|
||||||
|
: "bluetooth-symbolic"
|
||||||
|
) : "bluetooth-disabled-symbolic"
|
||||||
|
})} class={"bluetooth state"} visible={
|
||||||
|
createBinding(AstalBluetooth.get_default(), "adapter").as(Boolean)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Gtk.Image iconName={createBinding(AstalNetwork.get_default(), "primary").as(primary => {
|
||||||
|
switch(primary) {
|
||||||
|
case AstalNetwork.Primary.WIRED: return AstalNetwork.get_default().wired.get_icon_name();
|
||||||
|
|
||||||
|
case AstalNetwork.Primary.WIFI: return AstalNetwork.get_default().wifi.get_icon_name();
|
||||||
|
}
|
||||||
|
|
||||||
|
return "network-no-route-symbolic";
|
||||||
|
})} class={"network state"}
|
||||||
|
visible={createBinding(AstalNetwork.get_default(), "primary").as(primary =>
|
||||||
|
primary !== AstalNetwork.Primary.UNKNOWN)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Gtk.Box>
|
||||||
|
<Gtk.Image class={"bell state"} iconName={createBinding(
|
||||||
|
Notifications.getDefault().getNotifd(), "dontDisturb").as(dnd => dnd ?
|
||||||
|
"minus-circle-filled-symbolic"
|
||||||
|
: "preferences-system-notifications-symbolic")
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Gtk.Image iconName={"circle-filled-symbolic"} class={"notification-count"}
|
||||||
|
visible={variableToBoolean(createBinding(Notifications.getDefault(), "history"))}
|
||||||
|
/>
|
||||||
|
</Gtk.Box>
|
||||||
|
</Gtk.Box>
|
||||||
|
}
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import { bind, Gio, Variable } from "astal";
|
|
||||||
import { Astal, Gdk, Gtk, Widget } from "astal/gtk3";
|
|
||||||
import AstalTray from "gi://AstalTray"
|
|
||||||
|
|
||||||
const astalTray = AstalTray.get_default();
|
|
||||||
|
|
||||||
function menuFromModel(model: Gio.MenuModel, actionGroup: Gio.ActionGroup | null): Gtk.Menu {
|
|
||||||
const menu = Gtk.Menu.new_from_model(model);
|
|
||||||
menu.insert_action_group("dbusmenu", actionGroup)
|
|
||||||
|
|
||||||
return menu;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Tray(): Gtk.Widget {
|
|
||||||
return new Widget.Box({
|
|
||||||
className: "tray",
|
|
||||||
visible: bind(astalTray, "items").as((items: Array<AstalTray.TrayItem>) => items.length > 0),
|
|
||||||
children: bind(astalTray, "items").as((items: Array<AstalTray.TrayItem>) => items
|
|
||||||
.filter(item => item?.gicon)
|
|
||||||
.map((item: AstalTray.TrayItem) =>
|
|
||||||
new Widget.Box({
|
|
||||||
className: "item",
|
|
||||||
child: Variable.derive(
|
|
||||||
[ bind(item, "menuModel"), bind(item, "actionGroup") ],
|
|
||||||
(menuModel: Gio.MenuModel, actionGroup: Gio.ActionGroup) => {
|
|
||||||
const menu = menuFromModel(menuModel, actionGroup);
|
|
||||||
|
|
||||||
return new Widget.Button({
|
|
||||||
className: "item-button",
|
|
||||||
tooltipMarkup: bind(item, "tooltipMarkup"),
|
|
||||||
onClick: (_, event: Astal.ClickEvent) => {
|
|
||||||
if(event.button === Astal.MouseButton.SECONDARY) {
|
|
||||||
item.about_to_show();
|
|
||||||
menu.popup_at_widget(_, Gdk.Gravity.NORTH, Gdk.Gravity.SOUTH_WEST, null);
|
|
||||||
} else if(event.button === Astal.MouseButton.PRIMARY)
|
|
||||||
item.activate(event.x, event.y);
|
|
||||||
},
|
|
||||||
halign: Gtk.Align.CENTER,
|
|
||||||
child: new Widget.Icon({
|
|
||||||
gIcon: bind(item, "gicon")
|
|
||||||
})
|
|
||||||
} as Widget.ButtonProps)
|
|
||||||
}
|
|
||||||
)()
|
|
||||||
} as Widget.BoxProps)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} as Widget.BoxProps);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { createBinding, createComputed, For, With } from "ags";
|
||||||
|
import { Gdk, Gtk } from "ags/gtk4";
|
||||||
|
|
||||||
|
import AstalTray from "gi://AstalTray"
|
||||||
|
import Gio from "gi://Gio?version=2.0";
|
||||||
|
import { variableToBoolean } from "../../scripts/utils";
|
||||||
|
import GObject from "gi://GObject?version=2.0";
|
||||||
|
|
||||||
|
|
||||||
|
const astalTray = AstalTray.get_default();
|
||||||
|
|
||||||
|
function popoverFromModel(model: Gio.MenuModel, actionGroup: Gio.ActionGroup | null): Gtk.PopoverMenu {
|
||||||
|
const menu = Gtk.PopoverMenu.new_from_model(model);
|
||||||
|
menu.insert_action_group("dbusmenu", actionGroup)
|
||||||
|
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Tray = () => {
|
||||||
|
const items = createBinding(astalTray, "items").as(items => items.filter(item => item?.gicon));
|
||||||
|
|
||||||
|
return <Gtk.Box class={"tray"} visible={variableToBoolean(items)}>
|
||||||
|
<For each={items}>
|
||||||
|
{(item: AstalTray.TrayItem) => <Gtk.Box class={"item"}>
|
||||||
|
|
||||||
|
<With value={createComputed([
|
||||||
|
createBinding(item, "actionGroup"),
|
||||||
|
createBinding(item, "menuModel")
|
||||||
|
])}>
|
||||||
|
{([actionGroup, menuModel]: [Gio.ActionGroup, Gio.MenuModel]) => {
|
||||||
|
const popover = popoverFromModel(menuModel, actionGroup);
|
||||||
|
|
||||||
|
return <Gtk.MenuButton class={"item-button"} tooltipMarkup={
|
||||||
|
createBinding(item, "tooltipMarkup")} tooltipText={
|
||||||
|
createBinding(item, "tooltipText")} popover={popover}
|
||||||
|
$={(self) => {
|
||||||
|
const conns: Map<GObject.Object, number> = new Map();
|
||||||
|
const gestureClick = Gtk.GestureClick.new();
|
||||||
|
|
||||||
|
self.add_controller(gestureClick);
|
||||||
|
|
||||||
|
conns.set(gestureClick, gestureClick.connect("released", (gesture, _, x, y) => {
|
||||||
|
if(gesture.get_current_button() === Gdk.BUTTON_PRIMARY) {
|
||||||
|
item.activate(x, y);
|
||||||
|
return;
|
||||||
|
} else if(gesture.get_current_button() === Gdk.BUTTON_SECONDARY) {
|
||||||
|
item.about_to_show();
|
||||||
|
self.popup();
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}}>
|
||||||
|
|
||||||
|
<Gtk.Image gicon={createBinding(item, "gicon")} pixelSize={16} />
|
||||||
|
</Gtk.MenuButton>
|
||||||
|
}}
|
||||||
|
</With>
|
||||||
|
</Gtk.Box>}
|
||||||
|
</For>
|
||||||
|
</Gtk.Box>
|
||||||
|
}
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
import { bind, Variable } from "astal";
|
|
||||||
import { Gtk, Widget } from "astal/gtk3";
|
|
||||||
import AstalHyprland from "gi://AstalHyprland";
|
|
||||||
import { getAppIcon, getSymbolicIcon } from "../../scripts/apps";
|
|
||||||
import { Windows } from "../../windows";
|
|
||||||
import { Config } from "../../scripts/config";
|
|
||||||
import { Separator, SeparatorProps } from "../Separator";
|
|
||||||
|
|
||||||
let showWsNum: (Variable<boolean>|undefined);
|
|
||||||
export const showWorkspaceNumber = (show: boolean) =>
|
|
||||||
showWsNum?.set(show);
|
|
||||||
|
|
||||||
|
|
||||||
export function Workspaces(): Gtk.Widget {
|
|
||||||
showWsNum ??= new Variable<boolean>(false);
|
|
||||||
|
|
||||||
return new Widget.Box({
|
|
||||||
className: "workspaces-row",
|
|
||||||
orientation: Gtk.Orientation.HORIZONTAL,
|
|
||||||
children: [
|
|
||||||
new Widget.EventBox({
|
|
||||||
className: "special",
|
|
||||||
visible: bind(AstalHyprland.get_default(), "workspaces").as((workspaces) =>
|
|
||||||
workspaces.filter(ws => ws.id < 0).sort((a, b) => a.id - b.id).length > 0),
|
|
||||||
child: new Widget.Box({
|
|
||||||
className: "special-workspaces",
|
|
||||||
spacing: 4,
|
|
||||||
children: bind(AstalHyprland.get_default(), "workspaces").as((workspaces) =>
|
|
||||||
workspaces.filter(ws => ws.id < 0).sort((a, b) => a.id - b.id).map((workspace) =>
|
|
||||||
new Widget.EventBox({
|
|
||||||
className: bind(AstalHyprland.get_default(), "focusedWorkspace").as(focusWs =>
|
|
||||||
`${focusWs.id === workspace.id ? "focus" : ""}`),
|
|
||||||
tooltipText: bind(workspace, "name").as((name) => {
|
|
||||||
name = name.replace(/^special\:/, "");
|
|
||||||
return name.charAt(0).toUpperCase().concat(name.substring(1, name.length));
|
|
||||||
}),
|
|
||||||
child: new Widget.Box({
|
|
||||||
hexpand: true,
|
|
||||||
child: bind(workspace, "lastClient").as(lastClient =>
|
|
||||||
new Widget.Icon({
|
|
||||||
className: "last-app-icon",
|
|
||||||
halign: Gtk.Align.CENTER,
|
|
||||||
visible: Variable.derive([
|
|
||||||
bind(workspace, "lastClient"),
|
|
||||||
bind(AstalHyprland.get_default(), "focusedWorkspace")
|
|
||||||
], (lastClient, focusedWorkspace) => focusedWorkspace?.id === workspace.id ?
|
|
||||||
false : Boolean(lastClient))(),
|
|
||||||
icon: bind(lastClient, "initialClass").as((initialClass) =>
|
|
||||||
getSymbolicIcon(initialClass) ?? getAppIcon(initialClass) ??
|
|
||||||
"application-x-executable-symbolic")
|
|
||||||
} as Widget.IconProps)
|
|
||||||
)
|
|
||||||
} as Widget.BoxProps),
|
|
||||||
onClickRelease: () => AstalHyprland.get_default().dispatch(
|
|
||||||
"togglespecialworkspace", workspace.name.replace(/^special\:/, "")
|
|
||||||
)
|
|
||||||
} as Widget.EventBoxProps)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} as Widget.BoxProps)
|
|
||||||
} as Widget.EventBoxProps),
|
|
||||||
Separator({
|
|
||||||
alpha: .2,
|
|
||||||
orientation: Gtk.Orientation.HORIZONTAL,
|
|
||||||
margin: 12,
|
|
||||||
spacing: 8,
|
|
||||||
visible: bind(AstalHyprland.get_default(), "workspaces").as(wss =>
|
|
||||||
wss.filter(ws => ws.id < 0).length > 0)
|
|
||||||
} as SeparatorProps),
|
|
||||||
new Widget.EventBox({
|
|
||||||
onScroll: (_, event) =>
|
|
||||||
event.delta_y > 0 ?
|
|
||||||
AstalHyprland.get_default().dispatch("workspace", "e-1")
|
|
||||||
: AstalHyprland.get_default().dispatch("workspace", "e+1"),
|
|
||||||
onHover: () => showWorkspaceNumber(true),
|
|
||||||
onHoverLost: () => showWorkspaceNumber(false),
|
|
||||||
onDestroy: () => {
|
|
||||||
// check if the current widgets is from the only bar
|
|
||||||
if((Windows.openWindows["bar"] as (Array<Widget.Window>|undefined))?.length === 1) {
|
|
||||||
showWsNum?.drop();
|
|
||||||
showWsNum = undefined;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: new Widget.Box({
|
|
||||||
className: "workspaces",
|
|
||||||
spacing: 4,
|
|
||||||
children: bind(AstalHyprland.get_default(), "workspaces").as((workspaces) =>
|
|
||||||
workspaces.filter((ws) => ws.id > 0).sort((a, b) => a.id - b.id).map((workspace, wsIndex, workspaces) => {
|
|
||||||
|
|
||||||
const showIds: Variable<boolean> = Variable.derive([
|
|
||||||
Config.getDefault().bindProperty("workspaces.always_show_id", "boolean").as(Boolean),
|
|
||||||
Config.getDefault().bindProperty("workspaces.enable_helper", "boolean").as(Boolean),
|
|
||||||
showWsNum!()
|
|
||||||
], (alwaysShowIds, enableHelper, showIds) => {
|
|
||||||
if(enableHelper && !alwaysShowIds) {
|
|
||||||
const previousWorkspace = workspaces[wsIndex-1];
|
|
||||||
const nextWorkspace = workspaces[wsIndex+1];
|
|
||||||
|
|
||||||
if((workspaces.filter((_, i) => i < wsIndex).length > 0 &&
|
|
||||||
previousWorkspace?.id < (workspace.id-1)) ||
|
|
||||||
(workspaces.filter((_, i) => i > wsIndex).length > 0 &&
|
|
||||||
nextWorkspace?.id > (workspace.id+1))) {
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return alwaysShowIds || showIds;
|
|
||||||
});
|
|
||||||
|
|
||||||
const className = Variable.derive([
|
|
||||||
bind(AstalHyprland.get_default(), "focusedWorkspace"),
|
|
||||||
showIds!()
|
|
||||||
], (focusedWs, showWsNumbers) =>
|
|
||||||
`${focusedWs.id === workspace.id ? "focus" : ""} ${
|
|
||||||
showWsNumbers ? "show" : ""}`
|
|
||||||
);
|
|
||||||
|
|
||||||
const tooltipText = Variable.derive([
|
|
||||||
bind(workspace, "lastClient"),
|
|
||||||
bind(AstalHyprland.get_default(), "focusedWorkspace")
|
|
||||||
], (lastClient, focusWs) => focusWs.id === workspace.id ? "" :
|
|
||||||
`Workspace ${workspace.id}${ lastClient ? ` - ${
|
|
||||||
!lastClient.title.toLowerCase().includes(lastClient.class) ?
|
|
||||||
`${lastClient.get_class()}: `
|
|
||||||
: ""
|
|
||||||
} ${lastClient.title}` : "" }`
|
|
||||||
);
|
|
||||||
|
|
||||||
return new Widget.EventBox({
|
|
||||||
className: className(),
|
|
||||||
onClickRelease: () => workspace.focus(),
|
|
||||||
tooltipText: tooltipText(),
|
|
||||||
onDestroy: () => {
|
|
||||||
showIds.drop();
|
|
||||||
className.drop();
|
|
||||||
tooltipText.drop();
|
|
||||||
},
|
|
||||||
child: new Widget.Box({
|
|
||||||
hexpand: true,
|
|
||||||
children: bind(workspace, "lastClient").as((lastClient) => {
|
|
||||||
const widgets: Array<Gtk.Widget> = [
|
|
||||||
new Widget.Revealer({
|
|
||||||
transitionDuration: 200,
|
|
||||||
transitionType: Gtk.RevealerTransitionType.SLIDE_LEFT,
|
|
||||||
revealChild: showIds!(),
|
|
||||||
hexpand: true,
|
|
||||||
child: new Widget.Label({
|
|
||||||
label: bind(workspace, "id").as(String),
|
|
||||||
className: "id",
|
|
||||||
} as Widget.LabelProps)
|
|
||||||
} as Widget.RevealerProps),
|
|
||||||
];
|
|
||||||
|
|
||||||
if(lastClient) {
|
|
||||||
widgets.push(new Widget.Icon({
|
|
||||||
className: "last-app-icon",
|
|
||||||
halign: Gtk.Align.CENTER,
|
|
||||||
expand: true,
|
|
||||||
visible: bind(AstalHyprland.get_default(), "focusedWorkspace").as(focusedWorkspace =>
|
|
||||||
workspace.id === focusedWorkspace.id ?
|
|
||||||
false
|
|
||||||
: Boolean(lastClient)),
|
|
||||||
icon: lastClient ?
|
|
||||||
bind(lastClient, "initialClass").as((clss) =>
|
|
||||||
getSymbolicIcon(clss) ?? getAppIcon(clss) ?? "application-x-executable-symbolic")
|
|
||||||
: undefined
|
|
||||||
} as Widget.IconProps));
|
|
||||||
}
|
|
||||||
|
|
||||||
return widgets;
|
|
||||||
})
|
|
||||||
} as Widget.BoxProps)
|
|
||||||
} as Widget.EventBoxProps);
|
|
||||||
})
|
|
||||||
)
|
|
||||||
} as Widget.BoxProps)
|
|
||||||
} as Widget.EventBoxProps)
|
|
||||||
]
|
|
||||||
} as Widget.BoxProps);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
import { Gtk } from "ags/gtk4";
|
||||||
|
import AstalHyprland from "gi://AstalHyprland";
|
||||||
|
import { getAppIcon, getSymbolicIcon } from "../../scripts/apps";
|
||||||
|
import { Config } from "../../scripts/config";
|
||||||
|
import { Separator } from "../Separator";
|
||||||
|
import { createBinding, createComputed, createState, For, With } from "ags";
|
||||||
|
import GObject from "gi://GObject?version=2.0";
|
||||||
|
import { variableToBoolean } from "../../scripts/utils";
|
||||||
|
|
||||||
|
const [showNumbers, setShowNumbers] = createState(false);
|
||||||
|
export const showWorkspaceNumber = (show: boolean) =>
|
||||||
|
setShowNumbers(show);
|
||||||
|
|
||||||
|
|
||||||
|
export const Workspaces = () => {
|
||||||
|
const workspaces = createBinding(AstalHyprland.get_default(), "workspaces"),
|
||||||
|
defaultWorkspaces = workspaces.as(wss =>
|
||||||
|
wss.filter(ws => ws.id > 0).sort((a, b) => a.id - b.id)),
|
||||||
|
specialWorkspaces = workspaces.as(wss =>
|
||||||
|
wss.filter(ws => ws.id < 0).sort((a, b) => a.id - b.id));
|
||||||
|
|
||||||
|
|
||||||
|
return <Gtk.Box class={"workspaces-row"}>
|
||||||
|
<Gtk.Box class={"special-workspaces"} spacing={4}>
|
||||||
|
<For each={specialWorkspaces}>
|
||||||
|
{(ws: AstalHyprland.Workspace) =>
|
||||||
|
<Gtk.Button class={"workspace"}
|
||||||
|
tooltipText={createBinding(ws, "name").as(name => {
|
||||||
|
name = name.replace(/^special\:/, "");
|
||||||
|
return name.charAt(0).toUpperCase().concat(name.substring(1, name.length));
|
||||||
|
})} onClicked={() => AstalHyprland.get_default().dispatch(
|
||||||
|
"togglespecialworkspace", ws.name.replace(/^special[:]/, "")
|
||||||
|
)}>
|
||||||
|
|
||||||
|
<With value={createBinding(ws, "lastClient")}>
|
||||||
|
{(lastClient: AstalHyprland.Client|null) => lastClient &&
|
||||||
|
<Gtk.Image class="last-client" iconName={
|
||||||
|
createBinding(lastClient, "initialClass").as(initialClass =>
|
||||||
|
getSymbolicIcon(initialClass) ?? getAppIcon(initialClass) ??
|
||||||
|
"application-x-executable-symbolic")}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</With>
|
||||||
|
</Gtk.Button>
|
||||||
|
}
|
||||||
|
</For>
|
||||||
|
</Gtk.Box>
|
||||||
|
<Separator alpha={.2} orientation={Gtk.Orientation.HORIZONTAL}
|
||||||
|
margin={12} spacing={8} visible={variableToBoolean(specialWorkspaces)}
|
||||||
|
/>
|
||||||
|
<Gtk.Box class={"default-workspaces"} spacing={4} $={(self) => {
|
||||||
|
const conns: Map<GObject.Object, Array<number>|number> = new Map();
|
||||||
|
const controllerScroll = Gtk.EventControllerScroll.new(
|
||||||
|
Gtk.EventControllerScrollFlags.VERTICAL
|
||||||
|
), controllerMotion = Gtk.EventControllerMotion.new();
|
||||||
|
|
||||||
|
self.add_controller(controllerScroll);
|
||||||
|
self.add_controller(controllerMotion);
|
||||||
|
|
||||||
|
conns.set(controllerScroll, controllerScroll.connect("scroll", (_, _dx, dy) => {
|
||||||
|
dy > 0 ?
|
||||||
|
AstalHyprland.get_default().dispatch("workspace", "e-1")
|
||||||
|
: AstalHyprland.get_default().dispatch("workspace", "e+1");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}));
|
||||||
|
|
||||||
|
conns.set(controllerMotion, [
|
||||||
|
controllerMotion.connect("enter", () => setShowNumbers(true)),
|
||||||
|
controllerMotion.connect("leave", () => setShowNumbers(false))
|
||||||
|
]);
|
||||||
|
|
||||||
|
conns.set(self, self.connect("destroy", () => conns.forEach((ids, obj) =>
|
||||||
|
Array.isArray(ids) ?
|
||||||
|
ids.forEach(id => obj.disconnect(id))
|
||||||
|
: obj.disconnect(ids)
|
||||||
|
)));
|
||||||
|
}}>
|
||||||
|
<For each={defaultWorkspaces}>
|
||||||
|
{(ws: AstalHyprland.Workspace, i) => {
|
||||||
|
const showId = createComputed([
|
||||||
|
Config.getDefault().bindProperty("workspaces.always_show_id", "boolean").as(Boolean),
|
||||||
|
Config.getDefault().bindProperty("workspaces.enable_helper", "boolean").as(Boolean),
|
||||||
|
showNumbers
|
||||||
|
], (alwaysShowIds, enableHelper, showIds) => {
|
||||||
|
if(enableHelper && !alwaysShowIds) {
|
||||||
|
const previousWorkspace = defaultWorkspaces.get()[i.get()-1];
|
||||||
|
const nextWorkspace = defaultWorkspaces.get()[i.get()+1];
|
||||||
|
|
||||||
|
if((defaultWorkspaces.get().filter((_, ii) => ii < i.get()).length > 0 &&
|
||||||
|
previousWorkspace?.id < (ws.id-1)) ||
|
||||||
|
(defaultWorkspaces.get().filter((_, ii) => ii > i.get()).length > 0 &&
|
||||||
|
nextWorkspace?.id > (ws.id+1))) {
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return alwaysShowIds || showIds;
|
||||||
|
});
|
||||||
|
|
||||||
|
return <Gtk.Button class={createComputed([
|
||||||
|
createBinding(AstalHyprland.get_default(), "focusedWorkspace"),
|
||||||
|
showId
|
||||||
|
], (focusedWs, showWsNumbers) =>
|
||||||
|
`workspace ${focusedWs.id === ws.id ? "focus" : ""} ${
|
||||||
|
showWsNumbers ? "show" : ""}`
|
||||||
|
)} tooltipText={createComputed([
|
||||||
|
createBinding(ws, "lastClient"),
|
||||||
|
createBinding(AstalHyprland.get_default(), "focusedWorkspace")
|
||||||
|
], (lastClient, focusWs) => focusWs.id === ws.id ? "" :
|
||||||
|
`workspace ${ws.id}${ lastClient ? ` - ${
|
||||||
|
!lastClient.title.toLowerCase().includes(lastClient.class) ?
|
||||||
|
`${lastClient.get_class()}: `
|
||||||
|
: ""
|
||||||
|
} ${lastClient.title}` : "" }`
|
||||||
|
)} onClicked={ws.focus}>
|
||||||
|
|
||||||
|
|
||||||
|
<With value={createBinding(ws, "lastClient")}>
|
||||||
|
{(lastClient: AstalHyprland.Client) =>
|
||||||
|
<Gtk.Box class={"last-client"}>
|
||||||
|
<Gtk.Revealer transitionDuration={200} revealChild={showId}
|
||||||
|
transitionType={Gtk.RevealerTransitionType.SLIDE_LEFT}
|
||||||
|
hexpand={true}>
|
||||||
|
|
||||||
|
<Gtk.Label label={createBinding(ws, "id").as(String)}
|
||||||
|
class={"id"} />
|
||||||
|
</Gtk.Revealer>
|
||||||
|
{lastClient && <Gtk.Image class={"last-client-icon"} iconName={
|
||||||
|
createBinding(lastClient, "initialClass").as(initialClass =>
|
||||||
|
getSymbolicIcon(initialClass) ?? getAppIcon(initialClass) ??
|
||||||
|
"application-x-executable-symbolic")}
|
||||||
|
hexpand={true} vexpand={true}
|
||||||
|
/>}
|
||||||
|
</Gtk.Box>
|
||||||
|
}
|
||||||
|
</With>
|
||||||
|
</Gtk.Button>
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</Gtk.Box>
|
||||||
|
</Gtk.Box>
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user