diff options
| -rw-r--r-- | app.tsx | 2 | ||||
| -rw-r--r-- | package-lock.json | 9 | ||||
| -rw-r--r-- | package.json | 2 | ||||
| -rw-r--r-- | scss/navbar.scss | 64 | ||||
| -rw-r--r-- | src/config/defaults.ts | 4 | ||||
| -rw-r--r-- | src/config/index.ts | 1 | ||||
| -rw-r--r-- | src/config/types.ts | 3 | ||||
| -rw-r--r-- | src/modules/bar.tsx | 11 | ||||
| -rw-r--r-- | src/modules/navbar.tsx | 120 | ||||
| -rw-r--r-- | src/modules/notifpopups.tsx | 17 | ||||
| -rw-r--r-- | src/modules/osds.tsx | 3 | ||||
| -rw-r--r-- | src/modules/session.tsx | 2 | ||||
| -rw-r--r-- | src/modules/sidebar/index.tsx | 46 | ||||
| -rw-r--r-- | src/modules/sidebar/modules/notifications.tsx | 6 | ||||
| -rw-r--r-- | src/services/players.ts | 2 | ||||
| -rw-r--r-- | src/services/updates.ts | 3 | ||||
| -rw-r--r-- | src/utils/strings.ts | 2 | ||||
| -rw-r--r-- | src/utils/widgets.ts | 7 | ||||
| -rw-r--r-- | style.scss | 1 |
19 files changed, 259 insertions, 46 deletions
@@ -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({ <Session />; Monitors.get_default().forEach(m => <NotifPopups monitor={m} />); Monitors.get_default().forEach(m => <SideBar monitor={m} />); + Monitors.get_default().forEach(m => <NavBar monitor={m} />); Monitors.get_default().forEach(m => <Bar monitor={m} />); Monitors.get_default().forEach(m => <ScreenCorners monitor={m} />); Monitors.get_default().forEach(m => <BarScreenCorners monitor={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<SideBar | null>, + 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<SideBar | null>; +}) => ( + <button + cursor="pointer" + onClick={() => switchPane(monitor, name)} + setup={self => hookIsCurrent(self, sidebar, name, c => self.toggleClassName("current", c))} + > + <box vertical className="pane-button"> + <label className="icon" label={getPaneIcon(name)} /> + <revealer + transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN} + transitionDuration={150} + setup={self => hookIsCurrent(self, sidebar, name, c => self.set_reveal_child(c))} + > + <label className="label" label={getPaneName(name)} /> + </revealer> + </box> + </button> +); + +export default ({ monitor }: { monitor: Monitor }) => { + const sidebar = Variable<SideBar | null>(null); + awaitSidebar(monitor).then(s => sidebar.set(s)); + + return ( + <window + namespace="caelestia-navbar" + monitor={monitor.id} + anchor={Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.BOTTOM} + exclusivity={Astal.Exclusivity.EXCLUSIVE} + visible={config.persistent.get()} + setup={self => { + 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()); + }} + > + <eventbox + onScroll={(_, event) => { + 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)]); + }} + > + <box vertical className="navbar"> + {paneNames.map(n => ( + <PaneButton monitor={monitor} name={n} sidebar={sidebar} /> + ))} + </box> + </eventbox> + </window> + ); +}; 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 }) => ( <window @@ -61,13 +61,8 @@ 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( <box vertical halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className={`lock ${type}`}> <label vexpand className="icon" label={icon} /> - <label vexpand className="text" label={type.slice(0, 1).toUpperCase() + type.slice(1) + "lock"} /> + <label vexpand className="text" label={capitalize(type) + "lock"} /> </box> ); 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 }) => ( <box vertical className="item"> 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<SideBar>(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 <Dashboard />; + if (name === "audio") return <Audio />; + if (name === "connectivity") return <Connectivity />; + if (name === "packages") return <Packages />; + if (name === "notifpane") return <NotifPane />; + return <Time />; +}; + @register() export default class SideBar extends Widget.Window { - readonly shown: Variable<string>; + readonly shown: Variable<PaneName>; constructor({ monitor }: { monitor: Monitor }) { super({ @@ -24,16 +57,15 @@ export default class SideBar extends Widget.Window { visible: false, }); - const panes = [<Dashboard />, <Audio />, <Connectivity />, <Packages />, <NotifPane />, <Time />]; - this.shown = Variable(panes[0].name); + this.shown = Variable(paneNames[0]); this.add( <eventbox onScroll={(_, event) => { 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)} </stack> </box> </eventbox> 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 }) => ( /> <button cursor="pointer" - onClicked={() => AstalNotifd.get_default().notifications.forEach(n => n.dismiss())} + onClicked={() => + AstalNotifd.get_default() + .get_notifications() + .forEach(n => n.dismiss()) + } label=" Clear" /> </box> diff --git a/src/services/players.ts b/src/services/players.ts index afac78b..1ae526d 100644 --- a/src/services/players.ts +++ b/src/services/players.ts @@ -1,6 +1,6 @@ +import { isRealPlayer } from "@/utils/mpris"; import { GLib, GObject, property, readFile, register, writeFileAsync } from "astal"; import AstalMpris from "gi://AstalMpris"; -import { isRealPlayer } from "../utils/mpris"; @register({ GTypeName: "Players" }) export default class Players extends GObject.Object { diff --git a/src/services/updates.ts b/src/services/updates.ts index b58609b..f8993c0 100644 --- a/src/services/updates.ts +++ b/src/services/updates.ts @@ -1,3 +1,4 @@ +import { capitalize } from "@/utils/strings"; import { execAsync, GLib, GObject, property, readFileAsync, register, writeFileAsync } from "astal"; import { updates as config } from "config"; @@ -124,7 +125,7 @@ export default class Updates extends GObject.Object { repo: await this.getRepo(r), updates: [], icon: this.getRepoIcon(r), - name: r[0].toUpperCase() + r.slice(1) + " repository", + name: capitalize(r) + " repository", })) ); diff --git a/src/utils/strings.ts b/src/utils/strings.ts index 4786c6b..77608e8 100644 --- a/src/utils/strings.ts +++ b/src/utils/strings.ts @@ -9,3 +9,5 @@ export const pathToFileName = (path: string, ext?: string) => { const dir = path.slice(start, path.lastIndexOf("/")).replaceAll("/", "-"); return `${dir}-${basename(path, ext !== undefined)}${ext ? `.${ext}` : ""}`; }; + +export const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1); diff --git a/src/utils/widgets.ts b/src/utils/widgets.ts index e0e512d..9fb1e29 100644 --- a/src/utils/widgets.ts +++ b/src/utils/widgets.ts @@ -72,13 +72,6 @@ export class MenuItem extends astalify(Gtk.MenuItem) { } @register() -export class Calendar extends astalify(Gtk.Calendar) { - constructor(props: ConstructProps<Calendar, Gtk.Calendar.ConstructorProps>) { - super(props as any); - } -} - -@register() export class FlowBox extends astalify(Gtk.FlowBox) { constructor(props: ConstructProps<FlowBox, Gtk.FlowBox.ConstructorProps>) { super(props as any); @@ -12,6 +12,7 @@ @use "scss/osds"; @use "scss/session"; @use "scss/sidebar"; +@use "scss/navbar"; * { all: unset; // Remove GTK theme styles |