From b0f857f0d1a1bec3d9c56a6e76d54932bfc6bf01 Mon Sep 17 00:00:00 2001
From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>
Date: Tue, 8 Apr 2025 16:01:36 +1000
Subject: feat: add navbar
For controlling sidebar panes + other stuff later
---
app.tsx | 2 +
package-lock.json | 9 +-
package.json | 2 +-
scss/navbar.scss | 64 ++++++++++++++
src/config/defaults.ts | 4 +
src/config/index.ts | 1 +
src/config/types.ts | 3 +
src/modules/bar.tsx | 11 +--
src/modules/navbar.tsx | 120 ++++++++++++++++++++++++++
src/modules/notifpopups.tsx | 17 ++--
src/modules/osds.tsx | 3 +-
src/modules/session.tsx | 2 +-
src/modules/sidebar/index.tsx | 46 ++++++++--
src/modules/sidebar/modules/notifications.tsx | 6 +-
src/services/players.ts | 2 +-
src/services/updates.ts | 3 +-
src/utils/strings.ts | 2 +
src/utils/widgets.ts | 7 --
style.scss | 1 +
19 files changed, 259 insertions(+), 46 deletions(-)
create mode 100644 scss/navbar.scss
create mode 100644 src/modules/navbar.tsx
diff --git a/app.tsx b/app.tsx
index 83bd378..f1dcde3 100644
--- a/app.tsx
+++ b/app.tsx
@@ -1,5 +1,6 @@
import Bar from "@/modules/bar";
import Launcher from "@/modules/launcher";
+import NavBar from "@/modules/navbar";
import NotifPopups from "@/modules/notifpopups";
import Osds from "@/modules/osds";
import ScreenCorners, { BarScreenCorners } from "@/modules/screencorners";
@@ -80,6 +81,7 @@ App.start({
;
Monitors.get_default().forEach(m => );
Monitors.get_default().forEach(m => );
+ Monitors.get_default().forEach(m => );
Monitors.get_default().forEach(m => );
Monitors.get_default().forEach(m => );
Monitors.get_default().forEach(m => );
diff --git a/package-lock.json b/package-lock.json
index 4745313..d0a50f8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4,7 +4,6 @@
"requires": true,
"packages": {
"": {
- "name": "shell",
"dependencies": {
"fuzzysort": "^3.1.0",
"ical.js": "^2.1.0",
@@ -12,7 +11,7 @@
},
"devDependencies": {
"esbuild": "^0.25.2",
- "typescript": "^5.7.3"
+ "typescript": "5.7.3"
}
},
"node_modules/@babel/runtime": {
@@ -600,9 +599,9 @@
}
},
"node_modules/typescript": {
- "version": "5.8.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
- "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
+ "version": "5.7.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
+ "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
diff --git a/package.json b/package.json
index 9e1994e..1c6d73e 100644
--- a/package.json
+++ b/package.json
@@ -6,6 +6,6 @@
},
"devDependencies": {
"esbuild": "^0.25.2",
- "typescript": "^5.7.3"
+ "typescript": "5.7.3"
}
}
diff --git a/scss/navbar.scss b/scss/navbar.scss
new file mode 100644
index 0000000..213fc48
--- /dev/null
+++ b/scss/navbar.scss
@@ -0,0 +1,64 @@
+@use "sass:color";
+@use "scheme";
+@use "lib";
+@use "font";
+
+.navbar {
+ @include font.mono;
+
+ background-color: scheme.$mantle;
+
+ button {
+ color: scheme.$subtext1;
+
+ &:hover,
+ &:focus {
+ color: scheme.$subtext0;
+ }
+
+ &:active {
+ color: color.change(scheme.$overlay2, $alpha: 1);
+ }
+
+ &.current {
+ .pane-button {
+ background-color: scheme.$primary;
+ color: color.change(scheme.$base, $alpha: 1);
+ }
+
+ &:hover .pane-button,
+ &:focus .pane-button {
+ background-color: color.mix(scheme.$primary, scheme.$base, 80%);
+ }
+
+ &:active .pane-button {
+ background-color: color.mix(scheme.$primary, scheme.$base, 70%);
+ }
+ }
+
+ &:first-child .pane-button {
+ margin-top: lib.s(10);
+ }
+
+ &:last-child .pane-button {
+ margin-bottom: lib.s(10);
+ }
+ }
+
+ .pane-button {
+ @include lib.rounded(20);
+ @include lib.element-decel;
+
+ padding: lib.s(10) lib.s(8);
+ margin: lib.s(5) lib.s(8);
+
+ .icon {
+ font-size: lib.s(28);
+ }
+
+ .label {
+ font-size: lib.s(12);
+ margin-bottom: lib.s(5);
+ }
+ }
+}
diff --git a/src/config/defaults.ts b/src/config/defaults.ts
index f256265..59cc215 100644
--- a/src/config/defaults.ts
+++ b/src/config/defaults.ts
@@ -112,6 +112,10 @@ export default {
sidebar: {
showOnStartup: false,
},
+ navbar: {
+ persistent: false, // Whether to show all the time or only on hover
+ appearWidth: 10, // The width in pixels of the hover area for the navbar to show up
+ },
// Services
math: {
maxHistory: 100,
diff --git a/src/config/index.ts b/src/config/index.ts
index 631aa1d..abdb094 100644
--- a/src/config/index.ts
+++ b/src/config/index.ts
@@ -10,6 +10,7 @@ export const {
notifpopups,
osds,
sidebar,
+ navbar,
math,
updates,
weather,
diff --git a/src/config/types.ts b/src/config/types.ts
index eea8738..68f1824 100644
--- a/src/config/types.ts
+++ b/src/config/types.ts
@@ -71,6 +71,9 @@ export default {
"osds.lock.num.hideDelay": NUM,
// Sidebar
"sidebar.showOnStartup": BOOL,
+ // Navbar
+ "navbar.persistent": BOOL,
+ "navbar.appearWidth": NUM,
// Services
"math.maxHistory": NUM,
"updates.interval": NUM,
diff --git a/src/modules/bar.tsx b/src/modules/bar.tsx
index 447b69d..5a0fee3 100644
--- a/src/modules/bar.tsx
+++ b/src/modules/bar.tsx
@@ -1,4 +1,3 @@
-import type SideBar from "@/modules/sidebar";
import type { Monitor } from "@/services/monitors";
import Players from "@/services/players";
import Updates from "@/services/updates";
@@ -18,6 +17,7 @@ import AstalNetwork from "gi://AstalNetwork";
import AstalNotifd from "gi://AstalNotifd";
import AstalTray from "gi://AstalTray";
import AstalWp from "gi://AstalWp";
+import { switchPane } from "./sidebar";
interface ClassNameProps {
beforeSpacer: boolean;
@@ -75,15 +75,6 @@ const hookFocusedClientProp = (
callback(lastClient);
};
-const switchPane = (monitor: Monitor, name: string) => {
- const sidebar = App.get_window(`sidebar${monitor.id}`) as SideBar | null;
- if (sidebar) {
- if (sidebar.visible && sidebar.shown.get() === name) sidebar.hide();
- else sidebar.show();
- sidebar.shown.set(name);
- }
-};
-
const getClassName = ({ beforeSpacer, afterSpacer, first, last }: ClassNameProps) =>
`${beforeSpacer ? "before-spacer" : ""} ${afterSpacer ? "after-spacer" : ""}` +
` ${first ? "first" : ""} ${last ? "last" : ""}`;
diff --git a/src/modules/navbar.tsx b/src/modules/navbar.tsx
new file mode 100644
index 0000000..b844ff4
--- /dev/null
+++ b/src/modules/navbar.tsx
@@ -0,0 +1,120 @@
+import type { Monitor } from "@/services/monitors";
+import type { AstalWidget } from "@/utils/types";
+import { Variable } from "astal";
+import { Astal, Gtk } from "astal/gtk3";
+import { navbar as config } from "config";
+import AstalHyprland from "gi://AstalHyprland";
+import SideBar, { awaitSidebar, paneNames, switchPane, type PaneName } from "./sidebar";
+
+const getPaneIcon = (name: PaneName) => {
+ if (name === "dashboard") return "dashboard";
+ if (name === "audio") return "tune";
+ if (name === "connectivity") return "settings_ethernet";
+ if (name === "packages") return "package_2";
+ if (name === "notifpane") return "notifications";
+ return "date_range";
+};
+
+const getPaneName = (name: PaneName) => {
+ if (name === "dashboard") return "Dash";
+ if (name === "audio") return "Audio";
+ if (name === "connectivity") return "Conn";
+ if (name === "packages") return "Pkgs";
+ if (name === "notifpane") return "Alrts";
+ return "Time";
+};
+
+const hookIsCurrent = (
+ self: AstalWidget,
+ sidebar: Variable,
+ name: PaneName,
+ callback: (isCurrent: boolean) => void
+) => {
+ const unsub = sidebar.subscribe(s => {
+ if (!s) return;
+ self.hook(s.shown, (_, v) => callback(s.visible && v === name));
+ self.hook(s, "notify::visible", () => callback(s.visible && s.shown.get() === name));
+ callback(s.visible && s.shown.get() === name);
+ unsub();
+ });
+};
+
+const PaneButton = ({
+ monitor,
+ name,
+ sidebar,
+}: {
+ monitor: Monitor;
+ name: PaneName;
+ sidebar: Variable;
+}) => (
+
+);
+
+export default ({ monitor }: { monitor: Monitor }) => {
+ const sidebar = Variable(null);
+ awaitSidebar(monitor).then(s => sidebar.set(s));
+
+ return (
+ {
+ const hyprland = AstalHyprland.get_default();
+ const visible = Variable(config.persistent.get());
+
+ visible.poll(100, () => {
+ const width = self.visible
+ ? Math.max(config.appearWidth.get(), self.get_allocated_width())
+ : config.appearWidth.get();
+ return hyprland.get_cursor_position().x < width;
+ });
+ if (config.persistent.get()) visible.stopPoll();
+
+ self.hook(config.persistent, (_, v) => {
+ if (v) {
+ visible.stopPoll();
+ visible.set(true);
+ } else visible.startPoll();
+ });
+
+ self.hook(visible, (_, v) => self.set_visible(v));
+ self.connect("destroy", () => visible.drop());
+ }}
+ >
+ {
+ const shown = sidebar.get()?.shown;
+ if (!shown) return;
+ const idx = paneNames.indexOf(shown.get());
+ if (event.delta_y > 0) shown.set(paneNames[Math.min(paneNames.length - 1, idx + 1)]);
+ else shown.set(paneNames[Math.max(0, idx - 1)]);
+ }}
+ >
+
+ {paneNames.map(n => (
+
+ ))}
+
+
+
+ );
+};
diff --git a/src/modules/notifpopups.tsx b/src/modules/notifpopups.tsx
index 57e2a64..c8f4e13 100644
--- a/src/modules/notifpopups.tsx
+++ b/src/modules/notifpopups.tsx
@@ -1,11 +1,11 @@
import type { Monitor } from "@/services/monitors";
-import { idle, timeout } from "astal";
-import { App, Astal, Gtk } from "astal/gtk3";
+import { setupChildClickthrough } from "@/utils/widgets";
+import Notification from "@/widgets/notification";
+import { Astal, Gtk } from "astal/gtk3";
import { notifpopups as config } from "config";
import AstalNotifd from "gi://AstalNotifd";
-import { setupChildClickthrough } from "../utils/widgets";
-import Notification from "../widgets/notification";
import type SideBar from "./sidebar";
+import { awaitSidebar } from "./sidebar";
export default ({ monitor }: { monitor: Monitor }) => (
(
});
self.hook(notifd, "resolved", (_, id) => map.get(id)?.destroyWithAnims());
- let sidebar: SideBar | null;
-
- const awaitSidebar = () => {
- sidebar = App.get_window(`sidebar${monitor.id}`) as SideBar | null;
- if (!sidebar) timeout(100, awaitSidebar);
- };
- idle(awaitSidebar);
+ let sidebar: SideBar | null = null;
+ awaitSidebar(monitor).then(s => (sidebar = s));
// Change input region to child region so can click through empty space
setupChildClickthrough(self);
diff --git a/src/modules/osds.tsx b/src/modules/osds.tsx
index 4bbc87b..0f38823 100644
--- a/src/modules/osds.tsx
+++ b/src/modules/osds.tsx
@@ -1,4 +1,5 @@
import Monitors, { type Monitor } from "@/services/monitors";
+import { capitalize } from "@/utils/strings";
import PopupWindow from "@/widgets/popupwindow";
import { bind, execAsync, register, timeout, Variable, type Time } from "astal";
import { App, Astal, Gtk, Widget } from "astal/gtk3";
@@ -282,7 +283,7 @@ class LockOsd extends Widget.Window {
this.add(
-
+
);
diff --git a/src/modules/session.tsx b/src/modules/session.tsx
index 4f4a987..40d3b31 100644
--- a/src/modules/session.tsx
+++ b/src/modules/session.tsx
@@ -1,6 +1,6 @@
+import PopupWindow from "@/widgets/popupwindow";
import { execAsync } from "astal";
import { App, Astal, Gtk } from "astal/gtk3";
-import PopupWindow from "../widgets/popupwindow";
const Item = ({ icon, label, cmd, isDefault }: { icon: string; label: string; cmd: string; isDefault?: boolean }) => (
diff --git a/src/modules/sidebar/index.tsx b/src/modules/sidebar/index.tsx
index 60675d6..55d635c 100644
--- a/src/modules/sidebar/index.tsx
+++ b/src/modules/sidebar/index.tsx
@@ -9,9 +9,42 @@ import NotifPane from "./notifpane";
import Packages from "./packages";
import Time from "./time";
+export const paneNames = ["dashboard", "audio", "connectivity", "packages", "notifpane", "time"] as const;
+export type PaneName = (typeof paneNames)[number];
+
+export const switchPane = (monitor: Monitor, name: PaneName) => {
+ const sidebar = App.get_window(`sidebar${monitor.id}`) as SideBar | null;
+ if (sidebar) {
+ if (sidebar.visible && sidebar.shown.get() === name) sidebar.hide();
+ else sidebar.show();
+ sidebar.shown.set(name);
+ }
+};
+
+export const awaitSidebar = (monitor: Monitor) =>
+ new Promise(resolve => {
+ let sidebar: SideBar | null = null;
+
+ const awaitSidebar = () => {
+ sidebar = App.get_window(`sidebar${monitor.id}`) as SideBar | null;
+ if (sidebar) resolve(sidebar);
+ else idle(awaitSidebar);
+ };
+ idle(awaitSidebar);
+ });
+
+const getPane = (name: PaneName) => {
+ if (name === "dashboard") return ;
+ if (name === "audio") return ;
+ if (name === "connectivity") return ;
+ if (name === "packages") return ;
+ if (name === "notifpane") return ;
+ return ;
+};
+
@register()
export default class SideBar extends Widget.Window {
- readonly shown: Variable;
+ readonly shown: Variable;
constructor({ monitor }: { monitor: Monitor }) {
super({
@@ -24,16 +57,15 @@ export default class SideBar extends Widget.Window {
visible: false,
});
- const panes = [, , , , , ];
- this.shown = Variable(panes[0].name);
+ this.shown = Variable(paneNames[0]);
this.add(
{
if (event.modifier & Gdk.ModifierType.BUTTON1_MASK) {
- const index = panes.findIndex(p => p.name === this.shown.get()) + (event.delta_y < 0 ? -1 : 1);
- if (index < 0 || index >= panes.length) return;
- this.shown.set(panes[index].name);
+ const index = paneNames.indexOf(this.shown.get()) + (event.delta_y < 0 ? -1 : 1);
+ if (index < 0 || index >= paneNames.length) return;
+ this.shown.set(paneNames[index]);
}
}}
>
@@ -44,7 +76,7 @@ export default class SideBar extends Widget.Window {
transitionDuration={200}
shown={bind(this.shown)}
>
- {panes}
+ {paneNames.map(getPane)}
diff --git a/src/modules/sidebar/modules/notifications.tsx b/src/modules/sidebar/modules/notifications.tsx
index 9a9f440..e9347ec 100644
--- a/src/modules/sidebar/modules/notifications.tsx
+++ b/src/modules/sidebar/modules/notifications.tsx
@@ -68,7 +68,11 @@ export default ({ compact }: { compact?: boolean }) => (
/>