feat: initial commit

This commit is contained in:
tux
2025-05-31 14:00:56 +05:30
commit ec117cea00
19 changed files with 603 additions and 0 deletions

View File

@@ -0,0 +1,103 @@
import { Variable, Gio } from "astal";
import { App, Astal, Gdk, Gtk, hook } from "astal/gtk4";
import AstalApps from "gi://AstalApps";
import { Picture } from "../common";
export const WINDOW_NAME = "app-launcher";
const apps = new AstalApps.Apps();
const text = Variable("");
const hide = () => {
App.get_window(WINDOW_NAME)?.set_visible(false);
};
const AppButton = ({ app }: { app: AstalApps.Application }) => {
return (
<button
cssClasses={["app-button"]}
onClicked={() => {
hide();
app.launch();
}}
>
<box spacing={6} halign={Gtk.Align.START} valign={Gtk.Align.CENTER}>
<image iconName={app.iconName} iconSize={Gtk.IconSize.LARGE} />
<label cssClasses={["name"]} xalign={0} label={app.name} />
</box>
</button>
);
};
const AppList = () => {
const appList = text((text) => apps.fuzzy_query(text));
return (
<Gtk.ScrolledWindow vexpand heightRequest={500} widthRequest={300}>
<box vertical spacing={6} cssClasses={["app-list"]}>
{appList.as((list) => list.map((app) => <AppButton app={app} />))}
</box>
</Gtk.ScrolledWindow>
);
};
const AppSearch = () => {
const onEnter = () => {
apps.fuzzy_query(text.get())?.[0].launch();
hide();
};
return (
<overlay cssClasses={["entry-overlay"]} heightRequest={100}>
<Gtk.ScrolledWindow heightRequest={100}>
<Picture
file={Gio.file_new_for_path("/home/tux/Wallpapers/5m5kLI9.png")}
contentFit={Gtk.ContentFit.COVER}
overflow={Gtk.Overflow.HIDDEN}
/>
</Gtk.ScrolledWindow>
<entry
type="overlay"
vexpand
text={text.get()}
primaryIconName={"system-search-symbolic"}
placeholderText="Search..."
onChanged={(self) => text.set(self.text)}
onActivate={onEnter}
setup={(self) => {
hook(self, App, "window-toggled", (_, win) => {
const winName = win.name;
const visible = win.visible;
if (winName == WINDOW_NAME && visible) {
text.set("");
self.set_text("");
self.grab_focus();
}
});
}}
/>
</overlay>
);
};
export const AppLauncher = (gdkmonitor: Gdk.Monitor) => {
return (
<window
name={WINDOW_NAME}
cssClasses={["app-launcher"]}
gdkmonitor={gdkmonitor}
exclusivity={Astal.Exclusivity.EXCLUSIVE}
application={App}
keymode={Astal.Keymode.ON_DEMAND}
onKeyPressed={(_, keyval) => {
if (keyval === Gdk.KEY_Escape) {
App.toggle_window(WINDOW_NAME);
}
}}
>
<box vertical spacing={6}>
<AppSearch />
<AppList />
</box>
</window>
);
};

15
widget/bar/battery.tsx Normal file
View File

@@ -0,0 +1,15 @@
import { bind } from "astal";
import AstalBattery from "gi://AstalBattery";
export const Battery = () => {
const battery = AstalBattery.get_default();
return (
<box visible={bind(battery, "isPresent")}>
<image iconName={bind(battery, "batteryIconName")} />
<label
label={bind(battery, "percentage").as((p) => `${Math.floor(p * 100)}%`)}
/>
</box>
);
};

45
widget/bar/index.tsx Normal file
View File

@@ -0,0 +1,45 @@
import { App, Astal, Gdk } from "astal/gtk4";
import { FocusedClient, WorkspaceButton } from "./workspace";
import { Battery } from "./battery";
import { Launcher } from "./launcher";
export const WINDOW_NAME = "bar";
export const Bar = (gdkmonitor: Gdk.Monitor) => {
const { TOP, LEFT, RIGHT } = Astal.WindowAnchor;
return (
<window
name={WINDOW_NAME}
visible
cssClasses={["Bar"]}
gdkmonitor={gdkmonitor}
exclusivity={Astal.Exclusivity.EXCLUSIVE}
anchor={TOP | LEFT | RIGHT}
application={App}
>
<centerbox>
<Start />
<Center />
<End />
</centerbox>
</window>
);
};
const Start = () => {
return (
<box spacing={15}>
<Launcher />
<WorkspaceButton />
</box>
);
};
const Center = () => {
return <FocusedClient />;
};
const End = () => {
return <Battery />;
};

22
widget/bar/launcher.tsx Normal file
View File

@@ -0,0 +1,22 @@
import { App, Gtk } from "astal/gtk4";
import { Gio } from "astal";
import { Picture } from "../common";
import { WINDOW_NAME } from "../app-launcher";
export const Launcher = () => {
return (
<Gtk.ScrolledWindow
heightRequest={30}
widthRequest={30}
cssClasses={["launcher"]}
>
<button onClicked={() => App.toggle_window(WINDOW_NAME)}>
<Picture
file={Gio.file_new_for_path("/home/tux/Wallpapers/avatar.png")}
contentFit={Gtk.ContentFit.CONTAIN}
overflow={Gtk.Overflow.HIDDEN}
/>
</button>
</Gtk.ScrolledWindow>
);
};

56
widget/bar/workspace.tsx Normal file
View File

@@ -0,0 +1,56 @@
import { Variable, bind } from "astal";
import { Gtk } from "astal/gtk4";
import { ButtonProps } from "astal/gtk4/widget";
import AstalHyprland from "gi://AstalHyprland";
type WsButtonProps = ButtonProps & {
ws: AstalHyprland.Workspace;
};
const Workspace = ({ ws, ...props }: WsButtonProps) => {
const hyprland = AstalHyprland.get_default();
const classNames = Variable.derive(
[bind(hyprland, "focusedWorkspace"), bind(hyprland, "clients")],
(fws, _) => {
const classes = ["workspace-button"];
const active = fws.id == ws.id;
active && classes.push("active");
const occupied = hyprland.get_workspace(ws.id)?.get_clients().length > 0;
occupied && classes.push("occupied");
return classes;
},
);
return (
<button
cssClasses={classNames()}
onDestroy={() => classNames.drop()}
valign={Gtk.Align.CENTER}
halign={Gtk.Align.CENTER}
onClicked={() => ws.focus()}
{...props}
/>
);
};
export const WorkspaceButton = () => {
const range = Array.from({ length: 4 + 1 }, (_, i) => i);
return (
<box cssClasses={["workspace-container"]} spacing={4}>
{range.map((i) => (
<Workspace ws={AstalHyprland.Workspace.dummy(i + 1, null)} />
))}
</box>
);
};
export const FocusedClient = () => {
const hyprland = AstalHyprland.get_default();
const title = bind(hyprland, "focusedClient")
.as((fcsClient) => fcsClient.title)
.toString();
return <label>{title}</label>;
};

1
widget/common/index.ts Normal file
View File

@@ -0,0 +1 @@
export * from "./picture";

11
widget/common/picture.tsx Normal file
View File

@@ -0,0 +1,11 @@
import { ConstructProps } from "astal/gtk4";
import { astalify, Gtk } from "astal/gtk4";
export type PictureProps = ConstructProps<
Gtk.Picture,
Gtk.Picture.ConstructorProps
>;
export const Picture = astalify<Gtk.Picture, Gtk.Picture.ConstructorProps>(
Gtk.Picture,
);