chore!: split-up in components and widgets
The project has been split up in "components" and "widgets". Here's what's meant by both: - Components: big building blocks to coquille shell (i.e. the bar itself, the app launcher...) - Widgets: smaller building blocks, contained in components and composing them (i.e. workspaces widget, wifi status...)
This commit is contained in:
parent
eb7c276914
commit
0bb9279c61
14 changed files with 272 additions and 245 deletions
2
app.ts
2
app.ts
|
@ -1,6 +1,6 @@
|
|||
import { App } from "astal/gtk4";
|
||||
import style from "./style.scss";
|
||||
import Bar from "@/widget/Bar";
|
||||
import Bar from "@/components/Bar";
|
||||
|
||||
App.start({
|
||||
css: style,
|
||||
|
|
31
src/components/Bar/index.tsx
Normal file
31
src/components/Bar/index.tsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { Astal, Gtk, Gdk } from "astal/gtk4";
|
||||
import QuickSettings from "@/components/QuickSettings";
|
||||
import { Workspaces } from "./widgets/workspaces";
|
||||
import { Time } from "./widgets/time";
|
||||
|
||||
export default function Bar(monitor: Gdk.Monitor) {
|
||||
const { TOP, LEFT, RIGHT } = Astal.WindowAnchor;
|
||||
|
||||
return (
|
||||
<window
|
||||
visible
|
||||
namespace={"bar"}
|
||||
cssClasses={["Bar"]}
|
||||
gdkmonitor={monitor}
|
||||
exclusivity={Astal.Exclusivity.EXCLUSIVE}
|
||||
anchor={TOP | LEFT | RIGHT}
|
||||
>
|
||||
<centerbox shrinkCenterLast>
|
||||
<box hexpand halign={Gtk.Align.START}>
|
||||
<Workspaces />
|
||||
</box>
|
||||
<box halign={Gtk.Align.CENTER}>
|
||||
<Time />
|
||||
</box>
|
||||
<box halign={Gtk.Align.END}>
|
||||
<QuickSettings />
|
||||
</box>
|
||||
</centerbox>
|
||||
</window>
|
||||
);
|
||||
}
|
20
src/components/Bar/widgets/time.tsx
Normal file
20
src/components/Bar/widgets/time.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { Variable, GLib } from "astal";
|
||||
import { Gtk } from "astal/gtk4";
|
||||
|
||||
export function Time({ format = "%H:%M - %A %e." }) {
|
||||
const time = Variable<string>("").poll(
|
||||
1000,
|
||||
() => GLib.DateTime.new_now_local().format(format)!,
|
||||
);
|
||||
|
||||
return (
|
||||
<box cssClasses={["Time"]}>
|
||||
<menubutton>
|
||||
<label label={time()} />
|
||||
<popover hasArrow={false}>
|
||||
<Gtk.Calendar />
|
||||
</popover>
|
||||
</menubutton>
|
||||
</box>
|
||||
);
|
||||
}
|
25
src/components/Bar/widgets/workspaces.tsx
Normal file
25
src/components/Bar/widgets/workspaces.tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { bind } from "astal";
|
||||
import Hyprland from "gi://AstalHyprland";
|
||||
import Gtk from "gi://Gtk";
|
||||
|
||||
export function Workspaces() {
|
||||
const hypr = Hyprland.get_default();
|
||||
return (
|
||||
<box cssClasses={["Workspaces"]}>
|
||||
{bind(hypr, "workspaces").as((wss) =>
|
||||
wss
|
||||
.filter((ws) => !(ws.id >= -99 && ws.id <= -2)) // filter out special workspaces
|
||||
.sort((a, b) => a.id - b.id)
|
||||
.map((ws) => (
|
||||
<button
|
||||
valign={Gtk.Align.CENTER}
|
||||
cssClasses={bind(hypr, "focusedWorkspace").as((fw) =>
|
||||
ws === fw ? ["focused"] : [""],
|
||||
)}
|
||||
onClicked={() => ws.focus()}
|
||||
></button>
|
||||
)),
|
||||
)}
|
||||
</box>
|
||||
);
|
||||
}
|
39
src/components/QuickSettings/index.tsx
Normal file
39
src/components/QuickSettings/index.tsx
Normal file
|
@ -0,0 +1,39 @@
|
|||
import { Gtk } from "astal/gtk4";
|
||||
import { WifiStatus, WifiToggle } from "./widgets/wifi";
|
||||
import { AudioStatus, AudioSlider } from "./widgets/audio";
|
||||
import { BatteryStatus } from "./widgets/battery";
|
||||
import { IdleInhibitor } from "./widgets/idle";
|
||||
import { BluetoothToggle } from "./widgets/bluetooth";
|
||||
import { BrightnessSlider } from "./widgets/brightness";
|
||||
|
||||
export default function QuickSettings() {
|
||||
return (
|
||||
<box cssClasses={["QuickSettings"]}>
|
||||
<menubutton>
|
||||
<box>
|
||||
<AudioStatus />
|
||||
<WifiStatus />
|
||||
<BatteryStatus />
|
||||
</box>
|
||||
<popover hasArrow={false}>
|
||||
<box vertical>
|
||||
<box
|
||||
cssClasses={["Toggles"]}
|
||||
spacing={30}
|
||||
halign={Gtk.Align.CENTER}
|
||||
hexpand
|
||||
>
|
||||
<WifiToggle />
|
||||
<BluetoothToggle />
|
||||
<IdleInhibitor />
|
||||
</box>
|
||||
<box vertical>
|
||||
<AudioSlider />
|
||||
<BrightnessSlider />
|
||||
</box>
|
||||
</box>
|
||||
</popover>
|
||||
</menubutton>
|
||||
</box>
|
||||
);
|
||||
}
|
28
src/components/QuickSettings/widgets/audio.tsx
Normal file
28
src/components/QuickSettings/widgets/audio.tsx
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { bind } from "astal";
|
||||
import Wp from "gi://AstalWp";
|
||||
|
||||
const speaker = Wp.get_default()?.audio.defaultSpeaker!;
|
||||
|
||||
export function AudioStatus() {
|
||||
return (
|
||||
<box cssClasses={["Audio"]}>
|
||||
<image iconName={bind(speaker, "volumeIcon")} />
|
||||
</box>
|
||||
);
|
||||
}
|
||||
|
||||
export function AudioSlider() {
|
||||
return (
|
||||
<box cssClasses={["AudioSlider"]}>
|
||||
<image iconName={bind(speaker, "volumeIcon")} />
|
||||
<slider
|
||||
hexpand
|
||||
widthRequest={100}
|
||||
onChangeValue={({ value }) => {
|
||||
speaker.volume = value;
|
||||
}}
|
||||
value={bind(speaker, "volume")}
|
||||
/>
|
||||
</box>
|
||||
);
|
||||
}
|
15
src/components/QuickSettings/widgets/battery.tsx
Normal file
15
src/components/QuickSettings/widgets/battery.tsx
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { bind } from "astal";
|
||||
import Battery from "gi://AstalBattery";
|
||||
|
||||
export function BatteryStatus() {
|
||||
const bat = Battery.get_default();
|
||||
|
||||
return (
|
||||
<box cssClasses={["Battery"]} visible={bind(bat, "isPresent")}>
|
||||
<image iconName={bind(bat, "batteryIconName")} />
|
||||
<label
|
||||
label={bind(bat, "percentage").as((p) => `${Math.floor(p * 100)} %`)}
|
||||
/>
|
||||
</box>
|
||||
);
|
||||
}
|
18
src/components/QuickSettings/widgets/bluetooth.tsx
Normal file
18
src/components/QuickSettings/widgets/bluetooth.tsx
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { Gtk } from "astal/gtk4";
|
||||
import { bind } from "astal";
|
||||
import Bluetooth from "gi://AstalBluetooth";
|
||||
|
||||
const bluetooth = Bluetooth.get_default();
|
||||
|
||||
export function BluetoothToggle() {
|
||||
return (
|
||||
<box visible={bluetooth.adapter != null} halign={Gtk.Align.CENTER}>
|
||||
<button
|
||||
onClicked={() => bluetooth.toggle()}
|
||||
iconName={bind(bluetooth, "is_powered").as((p) =>
|
||||
p ? "bluetooth-symbolic" : "bluetooth-disabled-symbolic",
|
||||
)}
|
||||
/>
|
||||
</box>
|
||||
);
|
||||
}
|
20
src/components/QuickSettings/widgets/brightness.tsx
Normal file
20
src/components/QuickSettings/widgets/brightness.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { bind } from "astal";
|
||||
import Brightness from "@/services/brightness";
|
||||
|
||||
const brightness = Brightness.get_default();
|
||||
|
||||
export function BrightnessSlider() {
|
||||
return (
|
||||
<box cssClasses={["BrightnessSlider"]}>
|
||||
<image iconName={"display-brightness-symbolic"} />
|
||||
<slider
|
||||
hexpand
|
||||
widthRequest={100}
|
||||
onChangeValue={({ value }) => {
|
||||
brightness.screen = value;
|
||||
}}
|
||||
value={bind(brightness, "screen")}
|
||||
/>
|
||||
</box>
|
||||
);
|
||||
}
|
41
src/components/QuickSettings/widgets/idle.tsx
Normal file
41
src/components/QuickSettings/widgets/idle.tsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
import { Variable, bind, exec } from "astal";
|
||||
import { dependencies } from "@/lib/utils";
|
||||
|
||||
type IdleState = "active" | "inactive" | "unknown";
|
||||
export function IdleInhibitor() {
|
||||
/*
|
||||
* matcha needs additional checking to ensure the daemon is properly running
|
||||
*/
|
||||
function isDaemonRunning() {
|
||||
try {
|
||||
exec("matcha --status");
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!dependencies("matcha") || !isDaemonRunning()) return <></>;
|
||||
|
||||
const state = Variable<IdleState>("unknown");
|
||||
|
||||
function toggle() {
|
||||
exec("matcha --toggle");
|
||||
const response = exec("matcha --status");
|
||||
const enabled = response.match(/on/g);
|
||||
state.set(enabled ? "active" : "inactive");
|
||||
}
|
||||
|
||||
return (
|
||||
<box cssName="IdleInhibitor">
|
||||
<button
|
||||
onClicked={() => toggle()}
|
||||
iconName={bind(state).as((s) =>
|
||||
s === "active"
|
||||
? "my-caffeine-on-symbolic"
|
||||
: "my-caffeine-off-symbolic",
|
||||
)}
|
||||
/>
|
||||
</box>
|
||||
);
|
||||
}
|
34
src/components/QuickSettings/widgets/wifi.tsx
Normal file
34
src/components/QuickSettings/widgets/wifi.tsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { bind } from "astal";
|
||||
import Network from "gi://AstalNetwork";
|
||||
|
||||
const network = Network.get_default();
|
||||
const wifi = bind(network, "wifi");
|
||||
|
||||
export function WifiStatus() {
|
||||
return (
|
||||
<box visible={wifi.as(Boolean)}>
|
||||
{wifi.as(
|
||||
(wifi) =>
|
||||
wifi && (
|
||||
<image cssClasses={["Wifi"]} iconName={bind(wifi, "iconName")} />
|
||||
),
|
||||
)}
|
||||
</box>
|
||||
);
|
||||
}
|
||||
|
||||
export function WifiToggle() {
|
||||
return (
|
||||
<box visible={wifi.as(Boolean)} cssClasses={["WifiButton"]}>
|
||||
{wifi.as(
|
||||
(wifi) =>
|
||||
wifi && (
|
||||
<button
|
||||
onClicked={() => (wifi.enabled = !wifi.enabled)}
|
||||
iconName={bind(wifi, "iconName")}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</box>
|
||||
);
|
||||
}
|
|
@ -1,242 +0,0 @@
|
|||
import { Astal, Gtk, Gdk } from "astal/gtk4";
|
||||
import { Variable, GLib, bind, exec } from "astal";
|
||||
import Battery from "gi://AstalBattery";
|
||||
import Bluetooth from "gi://AstalBluetooth";
|
||||
import Wp from "gi://AstalWp";
|
||||
import Network from "gi://AstalNetwork";
|
||||
import Hyprland from "gi://AstalHyprland";
|
||||
import Brightness from "@/service/brightness";
|
||||
import { dependencies } from "@/lib/utils";
|
||||
|
||||
const network = Network.get_default();
|
||||
const wifi = bind(network, "wifi");
|
||||
const hypr = Hyprland.get_default();
|
||||
const bluetooth = Bluetooth.get_default();
|
||||
|
||||
function Wifi() {
|
||||
return (
|
||||
<box visible={wifi.as(Boolean)}>
|
||||
{wifi.as(
|
||||
(wifi) =>
|
||||
wifi && (
|
||||
<image cssClasses={["Wifi"]} iconName={bind(wifi, "iconName")} />
|
||||
),
|
||||
)}
|
||||
</box>
|
||||
);
|
||||
}
|
||||
|
||||
function Audio() {
|
||||
const speaker = Wp.get_default()?.audio.defaultSpeaker!;
|
||||
|
||||
return (
|
||||
<box cssClasses={["Audio"]}>
|
||||
<image iconName={bind(speaker, "volumeIcon")} />
|
||||
</box>
|
||||
);
|
||||
}
|
||||
|
||||
function AudioSlider() {
|
||||
const speaker = Wp.get_default()?.audio.defaultSpeaker!;
|
||||
|
||||
return (
|
||||
<box cssClasses={["AudioSlider"]}>
|
||||
<image iconName={bind(speaker, "volumeIcon")} />
|
||||
<slider
|
||||
hexpand
|
||||
widthRequest={100}
|
||||
onChangeValue={({ value }) => {
|
||||
speaker.volume = value;
|
||||
}}
|
||||
value={bind(speaker, "volume")}
|
||||
/>
|
||||
</box>
|
||||
);
|
||||
}
|
||||
|
||||
function BrightnessSlider() {
|
||||
const brightness = Brightness.get_default();
|
||||
|
||||
return (
|
||||
<box cssClasses={["BrightnessSlider"]}>
|
||||
<image iconName={"display-brightness-symbolic"} />
|
||||
<slider
|
||||
hexpand
|
||||
widthRequest={100}
|
||||
onChangeValue={({ value }) => {
|
||||
brightness.screen = value;
|
||||
}}
|
||||
value={bind(brightness, "screen")}
|
||||
/>
|
||||
</box>
|
||||
);
|
||||
}
|
||||
|
||||
function BatteryLevel() {
|
||||
const bat = Battery.get_default();
|
||||
|
||||
return (
|
||||
<box cssClasses={["Battery"]} visible={bind(bat, "isPresent")}>
|
||||
<image iconName={bind(bat, "batteryIconName")} />
|
||||
<label
|
||||
label={bind(bat, "percentage").as((p) => `${Math.floor(p * 100)} %`)}
|
||||
/>
|
||||
</box>
|
||||
);
|
||||
}
|
||||
|
||||
function Time({ format = "%H:%M - %A %e." }) {
|
||||
const time = Variable<string>("").poll(
|
||||
1000,
|
||||
() => GLib.DateTime.new_now_local().format(format)!,
|
||||
);
|
||||
|
||||
return (
|
||||
<box cssClasses={["Time"]}>
|
||||
<menubutton>
|
||||
<label label={time()} />
|
||||
<popover hasArrow={false}>
|
||||
<Gtk.Calendar />
|
||||
</popover>
|
||||
</menubutton>
|
||||
</box>
|
||||
);
|
||||
}
|
||||
|
||||
type IdleState = "active" | "inactive" | "unknown";
|
||||
|
||||
function IdleInhibitor() {
|
||||
/*
|
||||
* matcha needs additional checking to ensure the daemon is properly running
|
||||
*/
|
||||
function isDaemonRunning() {
|
||||
try {
|
||||
exec("matcha --status");
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!dependencies("matcha") || !isDaemonRunning()) return <></>;
|
||||
|
||||
const state = Variable<IdleState>("unknown");
|
||||
|
||||
function toggle() {
|
||||
exec("matcha --toggle");
|
||||
const response = exec("matcha --status");
|
||||
const enabled = response.match(/on/g);
|
||||
state.set(enabled ? "active" : "inactive");
|
||||
}
|
||||
|
||||
return (
|
||||
<box cssName="IdleInhibitor">
|
||||
<button
|
||||
onClicked={() => toggle()}
|
||||
iconName={bind(state).as((s) =>
|
||||
s === "active"
|
||||
? "my-caffeine-on-symbolic"
|
||||
: "my-caffeine-off-symbolic",
|
||||
)}
|
||||
/>
|
||||
</box>
|
||||
);
|
||||
}
|
||||
function QuickSettings() {
|
||||
return (
|
||||
<box cssClasses={["QuickSettings"]}>
|
||||
<menubutton>
|
||||
<box>
|
||||
<Audio />
|
||||
<Wifi />
|
||||
<BatteryLevel />
|
||||
</box>
|
||||
<popover hasArrow={false}>
|
||||
<box vertical>
|
||||
<box
|
||||
cssClasses={["Toggles"]}
|
||||
spacing={30}
|
||||
halign={Gtk.Align.CENTER}
|
||||
hexpand
|
||||
>
|
||||
<box visible={wifi.as(Boolean)} cssClasses={["WifiButton"]}>
|
||||
{wifi.as(
|
||||
(wifi) =>
|
||||
wifi && (
|
||||
<button
|
||||
onClicked={() => (wifi.enabled = !wifi.enabled)}
|
||||
iconName={bind(wifi, "iconName")}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</box>
|
||||
<box
|
||||
visible={bluetooth.adapter != null}
|
||||
halign={Gtk.Align.CENTER}
|
||||
>
|
||||
<button
|
||||
onClicked={() => bluetooth.toggle()}
|
||||
iconName={bind(bluetooth, "is_powered").as((p) =>
|
||||
p ? "bluetooth-symbolic" : "bluetooth-disabled-symbolic",
|
||||
)}
|
||||
/>
|
||||
</box>
|
||||
<IdleInhibitor />
|
||||
</box>
|
||||
<box vertical>
|
||||
<AudioSlider />
|
||||
<BrightnessSlider />
|
||||
</box>
|
||||
</box>
|
||||
</popover>
|
||||
</menubutton>
|
||||
</box>
|
||||
);
|
||||
}
|
||||
|
||||
function Workspaces() {
|
||||
return (
|
||||
<box cssClasses={["Workspaces"]}>
|
||||
{bind(hypr, "workspaces").as((wss) =>
|
||||
wss
|
||||
.filter((ws) => !(ws.id >= -99 && ws.id <= -2)) // filter out special workspaces
|
||||
.sort((a, b) => a.id - b.id)
|
||||
.map((ws) => (
|
||||
<button
|
||||
valign={Gtk.Align.CENTER}
|
||||
cssClasses={bind(hypr, "focusedWorkspace").as((fw) =>
|
||||
ws === fw ? ["focused"] : [""],
|
||||
)}
|
||||
onClicked={() => ws.focus()}
|
||||
></button>
|
||||
)),
|
||||
)}
|
||||
</box>
|
||||
);
|
||||
}
|
||||
export default function Bar(monitor: Gdk.Monitor) {
|
||||
const { TOP, LEFT, RIGHT } = Astal.WindowAnchor;
|
||||
|
||||
return (
|
||||
<window
|
||||
visible
|
||||
namespace={"bar"}
|
||||
cssClasses={["Bar"]}
|
||||
gdkmonitor={monitor}
|
||||
exclusivity={Astal.Exclusivity.EXCLUSIVE}
|
||||
anchor={TOP | LEFT | RIGHT}
|
||||
>
|
||||
<centerbox shrinkCenterLast>
|
||||
<box hexpand halign={Gtk.Align.START}>
|
||||
<Workspaces />
|
||||
</box>
|
||||
<box halign={Gtk.Align.CENTER}>
|
||||
<Time />
|
||||
</box>
|
||||
<box halign={Gtk.Align.END}>
|
||||
<QuickSettings />
|
||||
</box>
|
||||
</centerbox>
|
||||
</window>
|
||||
);
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
import { bind } from "astal";
|
||||
import Hyprland from "gi://AstalHyprland";
|
Loading…
Add table
Reference in a new issue