refactor: move ts to src

Also move popupwindow to own file
This commit is contained in:
2 * r + 2 * t 2025-01-16 16:35:37 +11:00
parent 476fbe69d3
commit 02fd2e97f2
20 changed files with 209 additions and 147 deletions

12
app.tsx
View file

@ -1,11 +1,11 @@
import { execAsync, GLib, writeFileAsync } from "astal"; import { execAsync, GLib, writeFileAsync } from "astal";
import { App } from "astal/gtk3"; import { App } from "astal/gtk3";
import Bar from "./modules/bar"; import Bar from "./src/modules/bar";
import Launcher from "./modules/launcher"; import Launcher from "./src/modules/launcher";
import NotifPopups from "./modules/notifpopups"; import NotifPopups from "./src/modules/notifpopups";
import Osds from "./modules/osds"; import Osds from "./src/modules/osds";
import Monitors from "./services/monitors"; import Monitors from "./src/services/monitors";
import Players from "./services/players"; import Players from "./src/services/players";
const loadStyleAsync = async () => { const loadStyleAsync = async () => {
if (!GLib.file_test(`${SRC}/scss/scheme/_index.scss`, GLib.FileTest.EXISTS)) if (!GLib.file_test(`${SRC}/scss/scheme/_index.scss`, GLib.FileTest.EXISTS))

View file

@ -25,7 +25,7 @@
padding-top: lib.s(10); padding-top: lib.s(10);
} }
.popup { .notification {
@include lib.rounded(8, $tr: 0, $br: 0); @include lib.rounded(8, $tr: 0, $br: 0);
@include lib.shadow; @include lib.shadow;
@include font.main; @include font.main;

View file

@ -7,7 +7,7 @@ import AstalNetwork from "gi://AstalNetwork";
import AstalNotifd from "gi://AstalNotifd"; import AstalNotifd from "gi://AstalNotifd";
import AstalTray from "gi://AstalTray"; import AstalTray from "gi://AstalTray";
import AstalWp01 from "gi://AstalWp"; import AstalWp01 from "gi://AstalWp";
import { bar as config } from "../config"; import { bar as config } from "../../config";
import type { Monitor } from "../services/monitors"; import type { Monitor } from "../services/monitors";
import Players from "../services/players"; import Players from "../services/players";
import Updates from "../services/updates"; import Updates from "../services/updates";

View file

@ -3,12 +3,13 @@ import { Astal, Gtk, Widget } from "astal/gtk3";
import fuzzysort from "fuzzysort"; import fuzzysort from "fuzzysort";
import type AstalApps from "gi://AstalApps"; import type AstalApps from "gi://AstalApps";
import AstalHyprland from "gi://AstalHyprland"; import AstalHyprland from "gi://AstalHyprland";
import { launcher as config } from "../config"; import { launcher as config } from "../../config";
import { Apps } from "../services/apps"; import { Apps } from "../services/apps";
import Math, { type HistoryItem } from "../services/math"; import Math, { type HistoryItem } from "../services/math";
import { getAppCategoryIcon } from "../utils/icons"; import { getAppCategoryIcon } from "../utils/icons";
import { launch } from "../utils/system"; import { launch } from "../utils/system";
import { PopupWindow, setupCustomTooltip } from "../utils/widgets"; import { setupCustomTooltip } from "../utils/widgets";
import PopupWindow from "../widgets/popupwindow";
type Mode = "apps" | "files" | "math"; type Mode = "apps" | "files" | "math";

View file

@ -0,0 +1,57 @@
import { Gtk } from "astal/gtk3";
import AstalNotifd from "gi://AstalNotifd";
import { PopupWindow, setupChildClickthrough } from "../utils/widgets";
const List = () => (
<box
vertical
valign={Gtk.Align.START}
className="list"
setup={self => {
const notifd = AstalNotifd.get_default();
const map = new Map<number, NotifPopup>();
self.hook(notifd, "notified", (self, id) => {
const notification = notifd.get_notification(id);
const popup = (<NotifPopup notification={notification} />) as NotifPopup;
popup.connect("destroy", () => map.get(notification.id) === popup && map.delete(notification.id));
map.get(notification.id)?.destroyWithAnims();
map.set(notification.id, popup);
self.add(
<eventbox
// Dismiss on middle click
onClick={(_, event) => event.button === Astal.MouseButton.MIDDLE && notification.dismiss()}
// Close on hover lost
onHoverLost={() => popup.destroyWithAnims()}
>
{popup}
</eventbox>
);
// Limit number of popups
if (config.maxPopups > 0 && self.children.length > config.maxPopups)
map.values().next().value?.destroyWithAnims();
});
self.hook(notifd, "resolved", (_, id) => map.get(id)?.destroyWithAnims());
// Change input region to child region so can click through empty space
setupChildClickthrough(self);
}}
/>
);
export default class Notifications extends PopupWindow {
constructor() {
super({
name: "notifications",
child: (
<box>
<List />
</box>
),
});
setupChildClickthrough(self);
}
}

View file

@ -0,0 +1,49 @@
import { Astal, Gtk } from "astal/gtk3";
import AstalNotifd from "gi://AstalNotifd";
import { notifpopups as config } from "../../config";
import { setupChildClickthrough } from "../utils/widgets";
import Notification from "../widgets/notification";
export default () => (
<window
namespace="caelestia-notifpopups"
anchor={Astal.WindowAnchor.TOP | Astal.WindowAnchor.RIGHT | Astal.WindowAnchor.BOTTOM}
>
<box
vertical
valign={Gtk.Align.START}
className="notifpopups"
setup={self => {
const notifd = AstalNotifd.get_default();
const map = new Map<number, Notification>();
self.hook(notifd, "notified", (self, id) => {
const notification = notifd.get_notification(id);
const popup = (<Notification popup notification={notification} />) as Notification;
popup.connect("destroy", () => map.get(notification.id) === popup && map.delete(notification.id));
map.get(notification.id)?.destroyWithAnims();
map.set(notification.id, popup);
self.add(
<eventbox
// Dismiss on middle click
onClick={(_, event) => event.button === Astal.MouseButton.MIDDLE && notification.dismiss()}
// Close on hover lost
onHoverLost={() => popup.destroyWithAnims()}
>
{popup}
</eventbox>
);
// Limit number of popups
if (config.maxPopups > 0 && self.children.length > config.maxPopups)
map.values().next().value?.destroyWithAnims();
});
self.hook(notifd, "resolved", (_, id) => map.get(id)?.destroyWithAnims());
// Change input region to child region so can click through empty space
setupChildClickthrough(self);
}}
/>
</window>
);

View file

@ -5,9 +5,9 @@ import AstalWp from "gi://AstalWp";
import Cairo from "gi://cairo"; import Cairo from "gi://cairo";
import Pango from "gi://Pango"; import Pango from "gi://Pango";
import PangoCairo from "gi://PangoCairo"; import PangoCairo from "gi://PangoCairo";
import { osds as config } from "../config"; import { osds as config } from "../../config";
import Monitors, { type Monitor } from "../services/monitors"; import Monitors, { type Monitor } from "../services/monitors";
import { PopupWindow } from "../utils/widgets"; import PopupWindow from "../widgets/popupwindow";
const getStyle = (context: Gtk.StyleContext, prop: string) => context.get_property(prop, Gtk.StateFlags.NORMAL); const getStyle = (context: Gtk.StyleContext, prop: string) => context.get_property(prop, Gtk.StateFlags.NORMAL);
const getNumStyle = (context: Gtk.StyleContext, prop: string) => getStyle(context, prop) as number; const getNumStyle = (context: Gtk.StyleContext, prop: string) => getStyle(context, prop) as number;

View file

@ -1,5 +1,5 @@
import { execAsync, GLib, GObject, property, readFileAsync, register, writeFileAsync } from "astal"; import { execAsync, GLib, GObject, property, readFileAsync, register, writeFileAsync } from "astal";
import { updates as config } from "../config"; import { updates as config } from "../../config";
interface Update { interface Update {
name: string; name: string;

45
src/utils/widgets.ts Normal file
View file

@ -0,0 +1,45 @@
import { Binding } from "astal";
import { Astal, Widget } from "astal/gtk3";
import AstalHyprland from "gi://AstalHyprland";
export const setupCustomTooltip = (self: any, text: string | Binding<string>) => {
if (!text) return null;
const window = new Widget.Window({
visible: false,
namespace: "caelestia-tooltip",
keymode: Astal.Keymode.NONE,
exclusivity: Astal.Exclusivity.IGNORE,
anchor: Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT,
child: new Widget.Label({ className: "tooltip", label: text }),
});
self.set_tooltip_window(window);
let dirty = true;
let lastX = 0;
self.connect("size-allocate", () => (dirty = true));
window.connect("size-allocate", () => {
window.marginLeft = lastX + (self.get_allocated_width() - window.get_preferred_width()[1]) / 2;
});
if (text instanceof Binding) self.hook(text, (_: any, v: string) => !v && window.hide());
self.connect("query-tooltip", (_: any, x: number, y: number) => {
if (text instanceof Binding && !text.get()) return false;
if (dirty) {
const { width, height } = self.get_allocation();
const { x: cx, y: cy } = AstalHyprland.get_default().get_cursor_position();
window.marginLeft = cx + ((width - window.get_preferred_width()[1]) / 2 - x);
window.marginTop = cy + (height - y);
lastX = cx - x;
dirty = false;
}
return true;
});
self.connect("destroy", () => window.destroy());
return window;
};
export const setupChildClickthrough = (self: any) =>
self.connect("size-allocate", () => self.get_window()?.set_child_input_shapes());

View file

@ -1,9 +1,8 @@
import { GLib, register, timeout } from "astal"; import { GLib, register, timeout } from "astal";
import { Astal, Gtk, Widget } from "astal/gtk3"; import { Astal, Gtk, Widget } from "astal/gtk3";
import AstalNotifd from "gi://AstalNotifd"; import AstalNotifd from "gi://AstalNotifd";
import { notifpopups as config } from "../config"; import { notifpopups as config } from "../../config";
import { desktopEntrySubs } from "../utils/icons"; import { desktopEntrySubs } from "../utils/icons";
import { setupChildClickthrough } from "../utils/widgets";
const urgencyToString = (urgency: AstalNotifd.Urgency) => { const urgencyToString = (urgency: AstalNotifd.Urgency) => {
switch (urgency) { switch (urgency) {
@ -53,17 +52,17 @@ const Image = ({ icon }: { icon: string }) => {
}; };
@register() @register()
class NotifPopup extends Widget.Box { export default class Notification extends Widget.Box {
readonly #revealer; readonly #revealer;
#destroyed = false; #destroyed = false;
constructor({ notification }: { notification: AstalNotifd.Notification }) { constructor({ notification, popup }: { notification: AstalNotifd.Notification; popup?: boolean }) {
super(); super();
this.#revealer = ( this.#revealer = (
<revealer revealChild transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN} transitionDuration={150}> <revealer revealChild transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN} transitionDuration={150}>
<box className="wrapper"> <box className="wrapper">
<box vertical className={`popup ${urgencyToString(notification.urgency)}`}> <box vertical className={`notification ${urgencyToString(notification.urgency)}`}>
<box className="header"> <box className="header">
<AppIcon appIcon={notification.appIcon} desktopEntry={notification.appName} /> <AppIcon appIcon={notification.appIcon} desktopEntry={notification.appName} />
<label className="app-name" label={notification.appName ?? "Unknown"} /> <label className="app-name" label={notification.appName ?? "Unknown"} />
@ -106,7 +105,7 @@ class NotifPopup extends Widget.Box {
}); });
// Close popup after timeout if transient or expire enabled in config // Close popup after timeout if transient or expire enabled in config
if (config.expire || notification.transient) if (popup && (config.expire || notification.transient))
timeout( timeout(
notification.expireTimeout > 0 notification.expireTimeout > 0
? notification.expireTimeout ? notification.expireTimeout
@ -131,47 +130,3 @@ class NotifPopup extends Widget.Box {
}); });
} }
} }
export default () => (
<window
namespace="caelestia-notifpopups"
anchor={Astal.WindowAnchor.TOP | Astal.WindowAnchor.RIGHT | Astal.WindowAnchor.BOTTOM}
>
<box
vertical
valign={Gtk.Align.START}
className="notifpopups"
setup={self => {
const notifd = AstalNotifd.get_default();
const map = new Map<number, NotifPopup>();
self.hook(notifd, "notified", (self, id) => {
const notification = notifd.get_notification(id);
const popup = (<NotifPopup notification={notification} />) as NotifPopup;
popup.connect("destroy", () => map.get(notification.id) === popup && map.delete(notification.id));
map.get(notification.id)?.destroyWithAnims();
map.set(notification.id, popup);
self.add(
<eventbox
// Dismiss on middle click
onClick={(_, event) => event.button === Astal.MouseButton.MIDDLE && notification.dismiss()}
// Close on hover lost
onHoverLost={() => popup.destroyWithAnims()}
>
{popup}
</eventbox>
);
// Limit number of popups
if (config.maxPopups > 0 && self.children.length > config.maxPopups)
map.values().next().value?.destroyWithAnims();
});
self.hook(notifd, "resolved", (_, id) => map.get(id)?.destroyWithAnims());
// Change input region to child region so can click through empty space
setupChildClickthrough(self);
}}
/>
</window>
);

View file

@ -0,0 +1,39 @@
import { Binding, register } from "astal";
import { App, Astal, Gdk, Widget } from "astal/gtk3";
import AstalHyprland from "gi://AstalHyprland?version=0.1";
const extendProp = <T>(
prop: T | Binding<T | undefined> | undefined,
override: (prop: T | undefined) => T | undefined
) => prop && (prop instanceof Binding ? prop.as(override) : override(prop));
@register()
export default class PopupWindow extends Widget.Window {
constructor(props: Widget.WindowProps) {
super({
keymode: Astal.Keymode.ON_DEMAND,
exclusivity: Astal.Exclusivity.IGNORE,
...props,
visible: false,
application: App,
name: props.monitor ? extendProp(props.name, n => (n ? n + props.monitor : undefined)) : props.name,
namespace: extendProp(props.name, n => `caelestia-${n}`),
onKeyPressEvent: (self, event) => {
// Close window on escape
if (event.get_keyval()[1] === Gdk.KEY_Escape) self.hide();
return props.onKeyPressEvent?.(self, event);
},
borderWidth: 20, // To allow shadow, cause if not it gets cut off
});
}
popup_at_widget(widget: JSX.Element, event: Gdk.Event) {
const { width, height } = widget.get_allocation();
const [_, x, y] = event.get_coords();
const { x: cx, y: cy } = AstalHyprland.get_default().get_cursor_position();
this.marginLeft = cx + ((width - this.get_preferred_width()[1]) / 2 - x);
this.marginTop = cy + (height - y);
this.show();
}
}

View file

@ -1,84 +0,0 @@
import { Binding, register } from "astal";
import { App, Astal, Gdk, Widget } from "astal/gtk3";
import AstalHyprland from "gi://AstalHyprland";
export const setupCustomTooltip = (self: any, text: string | Binding<string>) => {
if (!text) return null;
const window = (
<window
visible={false}
namespace="caelestia-tooltip"
keymode={Astal.Keymode.NONE}
exclusivity={Astal.Exclusivity.IGNORE}
anchor={Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT}
>
<label className="tooltip" label={text} />
</window>
) as Widget.Window;
self.set_tooltip_window(window);
let dirty = true;
let lastX = 0;
self.connect("size-allocate", () => (dirty = true));
window.connect("size-allocate", () => {
window.marginLeft = lastX + (self.get_allocated_width() - window.get_preferred_width()[1]) / 2;
});
if (text instanceof Binding) self.hook(text, (_: any, v: string) => !v && window.hide());
self.connect("query-tooltip", (_: any, x: number, y: number) => {
if (text instanceof Binding && !text.get()) return false;
if (dirty) {
const { width, height } = self.get_allocation();
const { x: cx, y: cy } = AstalHyprland.get_default().get_cursor_position();
window.marginLeft = cx + ((width - window.get_preferred_width()[1]) / 2 - x);
window.marginTop = cy + (height - y);
lastX = cx - x;
dirty = false;
}
return true;
});
self.connect("destroy", () => window.destroy());
return window;
};
export const setupChildClickthrough = (self: any) =>
self.connect("size-allocate", () => self.get_window()?.set_child_input_shapes());
const extendProp = <T,>(
prop: T | Binding<T | undefined> | undefined,
override: (prop: T | undefined) => T | undefined
) => prop && (prop instanceof Binding ? prop.as(override) : override(prop));
@register()
export class PopupWindow extends Widget.Window {
constructor(props: Widget.WindowProps) {
super({
keymode: Astal.Keymode.ON_DEMAND,
exclusivity: Astal.Exclusivity.IGNORE,
...props,
visible: false,
application: App,
name: props.monitor ? extendProp(props.name, n => (n ? n + props.monitor : undefined)) : props.name,
namespace: extendProp(props.name, n => `caelestia-${n}`),
onKeyPressEvent: (self, event) => {
// Close window on escape
if (event.get_keyval()[1] === Gdk.KEY_Escape) self.hide();
return props.onKeyPressEvent?.(self, event);
},
borderWidth: 20, // To allow shadow, cause if not it gets cut off
});
}
popup_at_widget(widget: JSX.Element, event: Gdk.Event) {
const { width, height } = widget.get_allocation();
const [_, x, y] = event.get_coords();
const { x: cx, y: cy } = AstalHyprland.get_default().get_cursor_position();
this.marginLeft = cx + ((width - this.get_preferred_width()[1]) / 2 - x);
this.marginTop = cy + (height - y);
this.show();
}
}