Compare commits

...

17 Commits

Author SHA1 Message Date
tux
c946acacf3 fix: profile and ping stdout 2026-08-22 13:09:24 +05:30
tux
14d3510746 chore(network): replace nmtui with impala 2026-08-22 13:09:24 +05:30
tux
9b8c4df596 feat: increase workspace 2026-02-24 14:48:23 +05:30
tux
00e8ee6d8b feat: remove launcher and wallpaper manager 2026-02-17 17:56:13 +05:30
tux
69b93ca32f feat: increase workspace 2026-02-12 11:40:48 +05:30
tux
18eb55eb05 feat(flake): add symlink cleanup 2026-02-12 11:33:32 +05:30
tux
bd05b722f5 style: adjust scss for consistent styling 2026-02-04 02:55:13 +05:30
tux
38b6cfb89a feat: update wallpaper command 2026-01-10 05:45:01 +05:30
tux
b41e5706e1 feat: add multi-monitor support 2025-12-29 21:47:59 +05:30
tux
f46e52d427 fix: active profile not displaying 2025-12-29 11:47:53 +05:30
tux
b0d22ef4bc fix(network): fix ssid length issue 2025-12-25 03:45:54 +05:30
tux
ed56ef463e feat: update native inputs 2025-12-17 01:10:38 +05:30
tux
292a7ad786 chore: update nix flake input 2025-12-17 01:10:16 +05:30
tux
4b7ae63bc8 feat: add dnd toggle button 2025-10-12 06:12:35 +05:30
tux
0c7610355f Merge pull request #1 from kkanden/patch-1
Fix typo in README
2025-10-08 17:50:04 +05:30
oliwia
0c1ac689cf fix: typo 2025-10-07 13:35:06 +02:00
tux
98203af3e4 feat: add notification timeout manager 2025-10-07 03:25:48 +05:30
27 changed files with 234 additions and 337 deletions

View File

@@ -34,5 +34,5 @@ environment.systemPackages = [ inputs.tpanel.packages.${system}.default ];
# Add this in your HomeManager config
home.packages = [ inputs.tpanel.packages.${system}.default ];
```
## Ooutdated Screenshot
## Outdated Screenshot
![full](https://raw.githubusercontent.com/tuxdotrs/tpanel/refs/heads/main/assets/gallery/full.png)

6
app.ts
View File

@@ -1,14 +1,12 @@
import app from "ags/gtk4/app";
import GLib from "gi://GLib";
import style from "./style/main.scss";
import windows from "./windows";
import { Main } from "./main";
const icons = `${GLib.get_user_config_dir()}/tpanel/assets/icons`;
app.start({
css: style,
icons: icons,
main() {
windows.map((win) => app.get_monitors().map(win));
},
main: Main,
});

View File

@@ -0,0 +1 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M8.35179 20.2418C9.19288 21.311 10.5142 22 12 22C13.4858 22 14.8071 21.311 15.6482 20.2418C13.2264 20.57 10.7736 20.57 8.35179 20.2418Z" fill="#000"></path> <path fill-rule="evenodd" clip-rule="evenodd" d="M18.7491 9V9.7041C18.7491 10.5491 18.9903 11.3752 19.4422 12.0782L20.5496 13.8012C21.5612 15.3749 20.789 17.5139 19.0296 18.0116C14.4273 19.3134 9.57274 19.3134 4.97036 18.0116C3.21105 17.5139 2.43882 15.3749 3.45036 13.8012L4.5578 12.0782C5.00972 11.3752 5.25087 10.5491 5.25087 9.7041V9C5.25087 5.13401 8.27256 2 12 2C15.7274 2 18.7491 5.13401 18.7491 9ZM10.0717 9.75C9.67233 9.75 9.34857 9.41421 9.34857 9C9.34857 8.58578 9.67233 8.25 10.0717 8.25H13.9283C14.2208 8.25 14.4845 8.43273 14.5964 8.71299C14.7083 8.99324 14.6465 9.31583 14.4397 9.53033L11.8175 12.25H13.9283C14.3277 12.25 14.6515 12.5858 14.6515 13C14.6515 13.4142 14.3277 13.75 13.9283 13.75H10.0717C9.77922 13.75 9.51554 13.5673 9.40362 13.287C9.29169 13.0068 9.35356 12.6842 9.56037 12.4697L12.1826 9.75H10.0717Z" fill="#000"></path> </g></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M8.35179 20.2418C9.19288 21.311 10.5142 22 12 22C13.4858 22 14.8071 21.311 15.6482 20.2418C13.2264 20.57 10.7736 20.57 8.35179 20.2418Z" fill="#000"></path> <path fill-rule="evenodd" clip-rule="evenodd" d="M18.7491 9.7041V9C18.7491 5.13401 15.7274 2 12 2C8.27256 2 5.25087 5.13401 5.25087 9V9.7041C5.25087 10.5491 5.00972 11.3752 4.5578 12.0782L3.45036 13.8012C2.43882 15.3749 3.21105 17.5139 4.97036 18.0116C9.57274 19.3134 14.4273 19.3134 19.0296 18.0116C20.789 17.5139 21.5612 15.3749 20.5496 13.8012L19.4422 12.0782C18.9903 11.3752 18.7491 10.5491 18.7491 9.7041ZM12 5.25C12.4142 5.25 12.75 5.58579 12.75 6V10C12.75 10.4142 12.4142 10.75 12 10.75C11.5858 10.75 11.25 10.4142 11.25 10V6C11.25 5.58579 11.5858 5.25 12 5.25Z" fill="#000"></path> </g></svg>

After

Width:  |  Height:  |  Size: 996 B

View File

@@ -0,0 +1 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M18.2211 19.6431C18.6981 18.7396 19.1627 17.7065 19.4613 16.6623C19.8722 15.2247 20.0207 13.8751 20.0629 12.8451L18.5105 11.2926L12.7073 5.48944L11.1549 3.93701C10.1248 3.97917 8.77531 4.12767 7.33767 4.53865C6.29348 4.83716 5.26037 5.30183 4.35693 5.77885C2.10098 6.96998 1.42721 9.71071 2.49716 11.8068L2.51021 11.8324L3.20923 12.9815C5.15002 16.1718 7.82804 18.8499 11.0184 20.7907L12.1675 21.4898L12.1931 21.5028C14.2892 22.5728 17.0299 21.899 18.2211 19.6431Z" fill="#000"></path> <path d="M21.7747 3.31343C22.0751 3.01296 22.0751 2.52581 21.7747 2.22535C21.4742 1.92488 20.987 1.92488 20.6866 2.22535L19.0118 3.90018C17.3118 2.66569 14.9941 2.66575 13.2942 3.9002L14.4027 5.00866L18.9915 9.59749L20.0999 10.7059C21.3344 9.00597 21.3343 6.68821 20.0998 4.98826L21.7747 3.31343Z" fill="#000"></path> </g></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

18
flake.lock generated
View File

@@ -8,11 +8,11 @@
]
},
"locked": {
"lastModified": 1758577685,
"narHash": "sha256-iHT0kvsQJG+Z89quGi7rNCXEg2e3DBGfuuCMu/UwiIo=",
"lastModified": 1764289441,
"narHash": "sha256-ak+lgFiYE5PHByN1/BRkO5JP498hno6Ix24C1Qf/vec=",
"owner": "aylur",
"repo": "ags",
"rev": "aa7a8a2dd6e54aaeb4e13a73ed3bc2283995090b",
"rev": "e169694390548dfd38ff40f1ef2163d6c3ffe3ea",
"type": "github"
},
"original": {
@@ -29,11 +29,11 @@
]
},
"locked": {
"lastModified": 1756474652,
"narHash": "sha256-iiBU6itpEqE0spXeNJ3uJTfioSyKYjt5bNepykpDXTE=",
"lastModified": 1764173295,
"narHash": "sha256-Jh4VtPcK2Ov+RTcV9FtyQRsxiJmXFQGfqX6jjM7/mgc=",
"owner": "aylur",
"repo": "astal",
"rev": "20bd8318e4136fbd3d4eb2d64dbabc3acbc915dd",
"rev": "7d1fac8a4b2a14954843a978d2ddde86168c75ef",
"type": "github"
},
"original": {
@@ -44,11 +44,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1758427187,
"narHash": "sha256-pHpxZ/IyCwoTQPtFIAG2QaxuSm8jWzrzBGjwQZIttJc=",
"lastModified": 1765779637,
"narHash": "sha256-KJ2wa/BLSrTqDjbfyNx70ov/HdgNBCBBSQP3BIzKnv4=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "554be6495561ff07b6c724047bdd7e0716aa7b46",
"rev": "1306659b587dc277866c7b69eb97e5f07864d8c4",
"type": "github"
},
"original": {

View File

@@ -46,7 +46,7 @@
src = ./.;
nativeBuildInputs = with pkgs; [
wrapGAppsHook
wrapGAppsHook3
gobject-introspection
ags.packages.${system}.default
];
@@ -63,6 +63,11 @@
runHook postInstall
'';
# Remove any broken symlinks
postFixup = ''
find $out -type l ! -exec test -e {} \; -delete 2>/dev/null || true
'';
};
in
pkgs.runCommand "tpanel" {

21
main.tsx Normal file
View File

@@ -0,0 +1,21 @@
import { For, This, createBinding } from "ags";
import app from "ags/gtk4/app";
import { Bar } from "./widgets/bar";
import { Notifications } from "./widgets/notifications";
import { ControlCenter } from "./widgets/control-center";
export const Main = () => {
const monitors = createBinding(app, "monitors");
return (
<For each={monitors}>
{(monitor) => (
<This this={app}>
<Bar gdkmonitor={monitor} />
<Notifications gdkmonitor={monitor} />
<ControlCenter gdkmonitor={monitor} />
</This>
)}
</For>
);
};

View File

@@ -15,7 +15,7 @@ window.Bar {
padding: 8px;
image {
-gtk-icon-size: 1.5rem;
-gtk-icon-size: 1.2rem;
}
&:hover {
@@ -73,13 +73,13 @@ window.Bar {
padding: 8px;
image {
-gtk-icon-size: 1.5rem;
-gtk-icon-size: 1.2rem;
}
}
.tray {
image {
-gtk-icon-size: 1.3rem;
-gtk-icon-size: 1rem;
}
menubutton {

View File

@@ -15,7 +15,7 @@ window.control-center {
color: $fg-color;
background-color: $inactive-color;
border-radius: calc($rounded / 1.5);
padding: 20px;
padding: 15px;
image {
-gtk-icon-size: 2rem;
@@ -32,19 +32,19 @@ window.control-center {
.header {
.name {
color: rgba($fg-color, 0.8);
font-size: 1.8rem;
font-size: 1.5rem;
}
.username {
color: rgba($fg-color, 0.5);
font-size: 1.2rem;
font-size: 1rem;
}
.controls {
button {
background-color: $inactive-color;
border-radius: calc($rounded / 1.5);
padding: 10px;
padding: 6px;
transition: color 100ms;
&:hover {
@@ -56,7 +56,28 @@ window.control-center {
}
image {
-gtk-icon-size: 2rem;
-gtk-icon-size: 1.5rem;
}
}
}
.notification-list {
.focus {
background: $accent;
}
button {
background-color: $inactive-color;
border-radius: calc($rounded / 1.5);
padding: 10px;
transition: color 100ms;
&:hover {
background-color: rgba($accent, 1);
}
image {
-gtk-icon-size: 1.2rem;
}
}
}

View File

@@ -1,40 +0,0 @@
@use "./_variable.scss" as *;
window.launcher {
background: $bg-color;
border-radius: $rounded;
padding: 10px;
font-size: 1.2rem;
.search {
min-width: 300px;
padding: 10px;
color: rgba($fg-color, 0.5);
}
separator {
border: 1px solid rgba($inactive-color, 0.5);
}
.list {
background: $bg-color;
}
.button {
border-radius: $rounded;
padding: 10px;
&:hover,
&:focus {
background: $inactive-color;
}
image {
-gtk-icon-size: 2rem;
}
}
.name {
color: $fg-color;
}
}

View File

@@ -3,4 +3,4 @@ $fg-color: #fff;
$bg-color: #101213;
$inactive-color: #1b1d1e;
$rounded: 14px;
$font-size: 1rem;
$font-size: 0.8rem;

View File

@@ -1,30 +0,0 @@
@use "./_variable.scss" as *;
@use "sass:math";
@use "sass:list";
window.wallpaper-manager {
background: $bg-color;
color: $fg-color;
min-width: 400px;
margin: 10px;
padding: 20px;
border-radius: $rounded;
.button {
border-radius: $rounded;
opacity: 0.5;
&:hover,
&:focus {
opacity: 1;
}
}
picture {
border-radius: $rounded;
&:hover {
background: $inactive-color;
}
}
}

View File

@@ -4,14 +4,8 @@
// bar
@use "./_bar.scss";
// launcher
@use "./_launcher.scss";
// notifications
@use "./_notification.scss";
// control center
@use "./_control-center.scss";
// wallpaper manager
@use "./_wallpaper-manager.scss";

View File

@@ -8,10 +8,12 @@ import { Tray } from "./tray";
import { WorkspaceButton } from "./workspace";
import { Bluetooth } from "./bluetooth";
import { Cava } from "./cava";
import { onCleanup } from "gnim";
import { execAsync } from "ags/process";
export const WINDOW_NAME = "bar";
export const Bar = (gdkmonitor: Gdk.Monitor) => {
export const Bar = ({ gdkmonitor }: { gdkmonitor: Gdk.Monitor }) => {
const { TOP } = Astal.WindowAnchor;
return (
@@ -24,10 +26,14 @@ export const Bar = (gdkmonitor: Gdk.Monitor) => {
anchor={TOP}
widthRequest={1200}
application={app}
$={(self) => onCleanup(() => self.destroy())}
>
<centerbox>
<box spacing={10} $type="start">
<Launcher windowName="launcher" icon="nix-symbolic" />
<Launcher
icon="nix-symbolic"
onClicked={() => execAsync("vicinae toggle")}
/>
<Network />
<Battery />
</box>
@@ -42,7 +48,10 @@ export const Bar = (gdkmonitor: Gdk.Monitor) => {
<Bluetooth />
<Tray />
<Time />
<Launcher windowName="control-center" icon="fa-ghost-symbolic" />
<Launcher
icon="fa-ghost-symbolic"
onClicked={() => app.toggle_window("control-center")}
/>
</box>
</centerbox>
</window>

View File

@@ -1,20 +1,15 @@
import { Gdk } from "ags/gtk4";
import app from "ags/gtk4/app";
import { WINDOW_NAME as APP_LAUNCHER_WINDOW_NAME } from "../launcher";
import { WINDOW_NAME as CONTROL_CENTER_WINDOW_NAME } from "../control-center";
import { Gdk, Gtk } from "ags/gtk4";
type Props = {
icon: string;
windowName:
| typeof APP_LAUNCHER_WINDOW_NAME
| typeof CONTROL_CENTER_WINDOW_NAME;
onClicked?: ((source: Gtk.Button) => void) | undefined;
};
export const Launcher = ({ icon, windowName }: Props) => {
export const Launcher = ({ icon, onClicked }: Props) => {
return (
<button
cssClasses={["launcher"]}
onClicked={() => app.toggle_window(windowName)}
onClicked={onClicked}
cursor={Gdk.Cursor.new_from_name("pointer", null)}
>
<image iconName={icon} />

View File

@@ -21,8 +21,7 @@ export const Network = () => {
const wifi = net.wifi;
switch (wifi.internet) {
case 0:
const ssid = wifi.ssid;
ssid ? ssid : "";
let ssid = wifi.ssid ? wifi.ssid : "";
let text = ssid.length > 10 ? ssid.substring(0, 10) + "..." : ssid;
return text;
case 1:
@@ -39,7 +38,7 @@ export const Network = () => {
cssClasses={["pill"]}
cursor={Gdk.Cursor.new_from_name("pointer", null)}
onClicked={() =>
execAsync(`wezterm start --class wezterm-floating nmtui`)
execAsync(`wezterm start --class wezterm-floating impala`)
}
>
<box spacing={5}>

View File

@@ -33,7 +33,7 @@ const Workspace = ({ ws }: WsButtonProps) => {
};
export const WorkspaceButton = () => {
const range = Array.from({ length: 4 + 1 }, (_, i) => i);
const range = Array.from({ length: 6 + 1 }, (_, i) => i);
return (
<box cssClasses={["workspace-container"]} spacing={4}>
{range.map((i) => (

View File

@@ -5,10 +5,11 @@ import { Header } from "./header";
import { NotificationList } from "./notification-list";
import { SlidingControls } from "./sliding-controls";
import { SystemInfo } from "./system-info";
import { onCleanup } from "ags";
export const WINDOW_NAME = "control-center";
export const ControlCenter = (gdkmonitor: Gdk.Monitor) => {
export const ControlCenter = ({ gdkmonitor }: { gdkmonitor: Gdk.Monitor }) => {
const { TOP, BOTTOM, RIGHT } = Astal.WindowAnchor;
const { VERTICAL } = Gtk.Orientation;
@@ -20,6 +21,7 @@ export const ControlCenter = (gdkmonitor: Gdk.Monitor) => {
application={app}
keymode={Astal.Keymode.ON_DEMAND}
anchor={TOP | BOTTOM | RIGHT}
$={(self) => onCleanup(() => self.destroy())}
>
<Gtk.EventControllerKey onKeyPressed={onKey} />
<box vexpand orientation={VERTICAL} spacing={20}>

View File

@@ -1,11 +1,53 @@
import { Gtk } from "ags/gtk4";
import { Gdk, Gtk } from "ags/gtk4";
import Notifd from "gi://AstalNotifd";
import { createComputed, createBinding } from "ags";
export const NotificationList = () => {
const { VERTICAL } = Gtk.Orientation;
const notifd = Notifd.get_default();
const isDndMode = createBinding(notifd, "dont-disturb");
const toggleDndMode = () => {
const currentDnd = notifd.get_dont_disturb();
notifd.set_dont_disturb(!currentDnd);
};
const notificationIcon = createComputed([isDndMode], (dndMode) => {
if (dndMode) {
return "fa-bell-off-symbolic";
}
return "fa-bell-symbolic";
});
return (
<box vexpand orientation={VERTICAL} spacing={20}>
<box
vexpand
hexpand
orientation={VERTICAL}
spacing={20}
cssClasses={["notification-list"]}
>
<box hexpand>
<label label="Notifications" />
<box hexpand halign={Gtk.Align.END} spacing={10}>
<button
onClicked={toggleDndMode}
tooltipText={isDndMode((dnd) =>
dnd ? "Disable Do Not Disturb" : "Enable Do Not Disturb",
)}
cursor={Gdk.Cursor.new_from_name("pointer", null)}
class={isDndMode((dnd) => (dnd ? "focus" : ""))}
>
<image iconName={notificationIcon} />
</button>
<button cursor={Gdk.Cursor.new_from_name("pointer", null)}>
<image iconName="fa-broom-symbolic" />
</button>
</box>
</box>
<box vexpand cssClasses={["pill"]}></box>
</box>
);

View File

@@ -4,8 +4,8 @@ import { createPoll } from "ags/time";
export const SystemInfo = () => {
const cpu = PollCMD("cat /sys/class/thermal/thermal_zone*/temp");
const gpu = PollCMD("supergfxctl -g");
const profile = PollCMD("asusctl profile -p");
const tailscale = PollCMD("tailscale ping homelab");
const profile = PollCMD("asusctl profile get");
const home = PollCMD("ping 100.64.0.3 -c 1");
const cpuUsage = PollCMD(
`awk '{u=$2+$4; t=$2+$3+$4+$5+$6+$7+$8; if(NR==1){u1=u; t1=t}else{printf "%.1f%%",(u-u1)*100/(t-t1)}}' <(grep '^cpu ' /proc/stat; sleep 1; grep '^cpu ' /proc/stat)`,
2000,
@@ -66,8 +66,9 @@ export const SystemInfo = () => {
<image iconName="fa-speed-symbolic" />
<label
label={profile((val) => {
const data = val.split(" ");
return data[data.length - 1];
const match = val.match(/^Active profile:\s*(.+)$/m);
const activeProfile = match?.[1]?.trim() ?? "NA";
return activeProfile;
})}
/>
</box>
@@ -80,9 +81,9 @@ export const SystemInfo = () => {
<image iconName="fa-home-symbolic" />
<label
label={tailscale((val) => {
const data = val.split(" ");
return data[data.length - 1];
label={home((val) => {
const time = `${parseInt(val.match(/time=([\d.]+)\s*ms/)?.[1] ?? "0", 10)}ms`;
return time;
})}
/>
</box>

View File

@@ -1,97 +0,0 @@
import { createState, For } from "ags";
import { Astal, Gdk, Gtk } from "ags/gtk4";
import app from "ags/gtk4/app";
import AstalApps from "gi://AstalApps";
export const WINDOW_NAME = "launcher";
let searchEntry: Gtk.Entry;
const apps = new AstalApps.Apps();
const [searchText, setSearchText] = createState("");
const hide = () => {
app.get_window(WINDOW_NAME)?.set_visible(false);
};
const AppButton = ({ app }: { app: AstalApps.Application }) => {
return (
<button
cssClasses={["button"]}
onClicked={() => {
hide();
app.launch();
}}
cursor={Gdk.Cursor.new_from_name("pointer", null)}
>
<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 = searchText((text) => apps.fuzzy_query(text));
return (
<Gtk.ScrolledWindow vexpand heightRequest={500} widthRequest={350}>
<box
spacing={5}
orientation={Gtk.Orientation.VERTICAL}
cssClasses={["list"]}
>
<For each={appList}>{(app) => <AppButton app={app} />}</For>
</box>
</Gtk.ScrolledWindow>
);
};
const AppSearch = () => {
return (
<entry
$={(ref) => (searchEntry = ref)}
cssClasses={["search"]}
text={searchText}
placeholderText="Search..."
onNotifyText={({ text }) => setSearchText(text)}
/>
);
};
export const Launcher = (gdkmonitor: Gdk.Monitor) => {
return (
<window
name={WINDOW_NAME}
cssClasses={["launcher"]}
gdkmonitor={gdkmonitor}
application={app}
exclusivity={Astal.Exclusivity.EXCLUSIVE}
keymode={Astal.Keymode.ON_DEMAND}
onNotifyVisible={({ visible }) => {
if (visible) searchEntry.grab_focus();
else searchEntry.set_text("");
}}
>
<Gtk.EventControllerKey onKeyPressed={onKey} />
<box orientation={Gtk.Orientation.VERTICAL} spacing={5}>
<AppSearch />
<Gtk.Separator orientation={Gtk.Orientation.HORIZONTAL} />
<AppList />
</box>
</window>
);
};
const onKey = (
_e: Gtk.EventControllerKey,
keyval: number,
_: number,
mod: number,
) => {
if (keyval === Gdk.KEY_Escape) {
app.toggle_window(WINDOW_NAME);
return;
}
};

View File

@@ -1,12 +1,19 @@
import { For, createState, onCleanup } from "ags";
import {
For,
createBinding,
createComputed,
createState,
onCleanup,
} from "ags";
import { Astal, Gdk, Gtk } from "ags/gtk4";
import AstalNotifd from "gi://AstalNotifd";
import { Notification } from "./notification";
export const WINDOW_NAME = "notifications";
export const Notifications = (gdkmonitor: Gdk.Monitor) => {
export const Notifications = ({ gdkmonitor }: { gdkmonitor: Gdk.Monitor }) => {
const notifd = AstalNotifd.get_default();
const isDndMode = createBinding(notifd, "dont-disturb");
const { TOP, RIGHT } = Astal.WindowAnchor;
@@ -33,13 +40,20 @@ export const Notifications = (gdkmonitor: Gdk.Monitor) => {
notifd.disconnect(resolvedHandler);
});
const shouldShowWindow = createComputed(
[notifications, isDndMode],
(notifs, dndEnabled) => {
return !dndEnabled && notifs.length > 0;
},
);
return (
<window
$={(self) => onCleanup(() => self.destroy())}
name={WINDOW_NAME}
cssClasses={["notifications"]}
gdkmonitor={gdkmonitor}
visible={notifications((ns) => ns.length > 0)}
visible={shouldShowWindow}
anchor={TOP | RIGHT}
>
<box orientation={Gtk.Orientation.VERTICAL} spacing={10}>

View File

@@ -0,0 +1,50 @@
import GLib from "gi://GLib";
type TimeoutManager = {
setupTimeout: () => void;
clearTimeout: () => void;
handleHover: () => void;
handleHoverLost: () => void;
cleanup: () => void;
};
export const createTimeoutManager = (
dismissCallback: () => void,
timeoutDelay: number,
): TimeoutManager => {
let isHovered = false;
let timeoutId: number | null = null;
const clearTimeout = () => {
if (timeoutId !== null) {
GLib.source_remove(timeoutId);
timeoutId = null;
}
};
const setupTimeout = () => {
clearTimeout();
if (!isHovered) {
timeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, timeoutDelay, () => {
clearTimeout();
dismissCallback();
return GLib.SOURCE_REMOVE;
});
}
};
return {
setupTimeout,
clearTimeout,
handleHover: () => {
isHovered = true;
clearTimeout();
},
handleHoverLost: () => {
isHovered = false;
setupTimeout();
},
cleanup: clearTimeout,
};
};

View File

@@ -3,6 +3,8 @@ import Adw from "gi://Adw";
import AstalNotifd from "gi://AstalNotifd";
import GLib from "gi://GLib";
import Pango from "gi://Pango";
import { createTimeoutManager } from "./manager";
import { onCleanup, onMount } from "ags";
const isIcon = (icon?: string | null) => {
const iconTheme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default()!);
@@ -33,7 +35,20 @@ const urgency = (n: AstalNotifd.Notification) => {
const { VERTICAL } = Gtk.Orientation;
const { START, END, CENTER } = Gtk.Align;
// Keep track of notification validity
const TIMEOUT_DELAY = 3000;
export const Notification = ({ n }: { n: AstalNotifd.Notification }) => {
const timeoutManager = createTimeoutManager(() => n.dismiss(), TIMEOUT_DELAY);
onMount(() => {
timeoutManager.setupTimeout();
});
onCleanup(() => {
timeoutManager.cleanup();
});
return (
<Adw.Clamp maximumSize={400}>
<box
@@ -42,6 +57,11 @@ export const Notification = ({ n }: { n: AstalNotifd.Notification }) => {
orientation={VERTICAL}
spacing={20}
>
<Gtk.EventControllerMotion
onEnter={() => timeoutManager.handleHover()}
onLeave={() => timeoutManager.handleHoverLost()}
/>
<box class="header" spacing={10}>
{(n.appIcon || isIcon(n.desktopEntry)) && (
<image

View File

@@ -1,103 +0,0 @@
import { Astal, Gdk, Gtk } from "ags/gtk4";
import app from "ags/gtk4/app";
import { exec } from "ags/process";
import Gio from "gi://Gio";
import GLib from "gi://GLib";
export const WINDOW_NAME = "wallpaper-manager";
// @TODO wallpaper cache
const wallpaperPath = `/home/tux/Wallpapers/new`;
const imageFormats = [".jpeg", ".jpg", ".webp", ".png"];
export const WallpaperManager = (gdkmonitor: Gdk.Monitor) => {
const { TOP, BOTTOM, LEFT } = Astal.WindowAnchor;
const { VERTICAL } = Gtk.Orientation;
const wallpaperList = getWallpaperList(wallpaperPath);
return (
<window
name={WINDOW_NAME}
cssClasses={["wallpaper-manager"]}
gdkmonitor={gdkmonitor}
application={app}
keymode={Astal.Keymode.ON_DEMAND}
anchor={TOP | BOTTOM | LEFT}
>
<Gtk.EventControllerKey onKeyPressed={onKey} />
<scrolledwindow>
<box orientation={VERTICAL} spacing={20}>
{wallpaperList.map((wall) => (
<button
cssClasses={["button"]}
cursor={Gdk.Cursor.new_from_name("pointer", null)}
onClicked={() => setWallpaper(wall)}
>
<Gtk.Picture
file={Gio.file_new_for_path(`${wallpaperPath}/${wall}`)}
heightRequest={200}
widthRequest={200}
hexpand
vexpand
cssClasses={["wallpaper-picture"]}
contentFit={Gtk.ContentFit.COVER}
/>
</button>
))}
</box>
</scrolledwindow>
</window>
);
};
const onKey = (
_e: Gtk.EventControllerKey,
keyval: number,
_: number,
mod: number,
) => {
if (keyval === Gdk.KEY_Escape) {
app.toggle_window(WINDOW_NAME);
return;
}
};
function getWallpaperList(path: string) {
const dir = Gio.file_new_for_path(path);
const fileEnum = dir.enumerate_children(
"standard::name",
Gio.FileQueryInfoFlags.NONE,
null,
);
const files: string[] = [];
let i = fileEnum.next_file(null);
while (i) {
let fileName = i.get_name();
if (imageFormats.some((fmt) => fileName.endsWith(fmt))) {
files.push(fileName);
}
i = fileEnum.next_file(null);
}
return files;
}
const setWallpaper = (name: string) => {
const hyprctl = GLib.find_program_in_path("hyprctl");
const imagePath = `${wallpaperPath}/${name}`;
if (!hyprctl) return;
const preloadedWalls = exec([hyprctl, "hyprpaper", "listloaded"]);
const nWall = preloadedWalls.split("\n").length;
console.log(nWall);
if (nWall >= 5) {
exec([hyprctl, "hyprpaper", "unload", "all"]);
}
exec([hyprctl, "hyprpaper", "preload", imagePath]);
exec([hyprctl, "hyprpaper", "wallpaper", `,${imagePath}`]);
app.toggle_window(WINDOW_NAME);
};

View File

@@ -1,7 +0,0 @@
import { Bar } from "./widgets/bar";
import { Launcher } from "./widgets/launcher";
import { Notifications } from "./widgets/notifications";
import { ControlCenter } from "./widgets/control-center";
import { WallpaperManager } from "./widgets/wallpaper-manager";
export default [Bar, Launcher, Notifications, ControlCenter, WallpaperManager];