Upload to github
This commit is contained in:
46
widget/Bar.tsx
Normal file
46
widget/Bar.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { App, Astal, Gtk, Gdk, Widget } from "astal/gtk3"
|
||||
import { Variable } from "astal"
|
||||
import Hyprland from "gi://AstalHyprland"
|
||||
import battery from "./battery"
|
||||
import workspaces from "./workspaces"
|
||||
import volume from "./volume"
|
||||
import brightness from "./brightness"
|
||||
import client from "./client"
|
||||
import player from "./player"
|
||||
import bluetooth from "./bluetooth"
|
||||
|
||||
const time = Variable("").poll(1000, "date +'%a %b %d · %H:%M'")
|
||||
|
||||
export default function Bar(monitor: Hyprland.Monitor) {
|
||||
const { TOP, LEFT, RIGHT } = Astal.WindowAnchor
|
||||
|
||||
return <window
|
||||
className="bar"
|
||||
namespace="bar"
|
||||
monitor={monitor.id}
|
||||
exclusivity={Astal.Exclusivity.IGNORE}
|
||||
anchor={TOP | LEFT | RIGHT}
|
||||
heightRequest={36}
|
||||
layer={Astal.Layer.TOP}
|
||||
application={App}>
|
||||
<centerbox>
|
||||
<box>
|
||||
{battery()}
|
||||
{volume()}
|
||||
{brightness()}
|
||||
{bluetooth()}
|
||||
</box>
|
||||
{workspaces(monitor)}
|
||||
<box halign={Gtk.Align.END}>
|
||||
{player()}
|
||||
{client()}
|
||||
<button><box>
|
||||
<label
|
||||
className="innerButton"
|
||||
label={time()}
|
||||
/>
|
||||
</box></button>
|
||||
</box>
|
||||
</centerbox>
|
||||
</window>
|
||||
}
|
||||
154
widget/battery.tsx
Normal file
154
widget/battery.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
import Battery from "gi://AstalBattery"
|
||||
import PowerProfiles from "gi://AstalPowerProfiles"
|
||||
import Hyprland from "gi://AstalHyprland"
|
||||
import { popup } from "./popup"
|
||||
import { Astal, Gtk, Widget } from "astal/gtk3"
|
||||
import { mergeBindings } from "/usr/share/astal/gjs/_astal"
|
||||
import { bind } from "astal"
|
||||
|
||||
const hyprland = Hyprland.get_default()
|
||||
const batt = Battery.get_default()
|
||||
const powerProfiles = PowerProfiles.get_default()
|
||||
|
||||
const percentage = bind(batt, "percentage").as((p) => `${Math.round(p*100)}%`)
|
||||
const charging = bind(batt, "charging")
|
||||
const toFull = bind(batt, "timeToFull").as((t) => t.toString())
|
||||
const toEmpty = bind(batt, "timeToEmpty").as((t) => t.toString())
|
||||
|
||||
function profileButton(label: string, profile: string) {
|
||||
return <button className="toggleButton" onClicked={(self) => {
|
||||
powerProfiles.activeProfile = profile
|
||||
}}>
|
||||
<overlay overlay={new Widget.Box({
|
||||
halign: Gtk.Align.END,
|
||||
valign: Gtk.Align.CENTER,
|
||||
className: bind(powerProfiles, "activeProfile").as((p) =>
|
||||
(p === profile)
|
||||
? "toggleIndicator active"
|
||||
: "toggleIndicator inactive"
|
||||
)
|
||||
})}>
|
||||
<label halign={Gtk.Align.START}>{label}</label>
|
||||
</overlay>
|
||||
</button>
|
||||
}
|
||||
|
||||
function BattWindow() {
|
||||
const { TOP, LEFT } = Astal.WindowAnchor
|
||||
return <window
|
||||
className="popupWindow"
|
||||
namespace="lazerpopup"
|
||||
monitor={hyprland.focusedMonitor.id}
|
||||
anchor={TOP | LEFT}
|
||||
layer={Astal.Layer.TOP}>
|
||||
<box
|
||||
className="popupBattery"
|
||||
vertical={true}
|
||||
spacing={8}>
|
||||
<box spacing={10}>
|
||||
<overlay overlay={new Widget.Label({
|
||||
halign: Gtk.Align.CENTER,
|
||||
valign: Gtk.Align.CENTER,
|
||||
className: "percentage",
|
||||
label: bind(batt, "energy").as(e => `${e}Wh`),
|
||||
})}>
|
||||
<levelbar
|
||||
className="batteryLevel"
|
||||
minValue={0}
|
||||
maxValue={1}
|
||||
inverted={true}
|
||||
widthRequest={96}
|
||||
vertical={true}
|
||||
value={bind(batt, "percentage").as(c => c)}
|
||||
/>
|
||||
</overlay>
|
||||
<box vertical={true} spacing={10}>
|
||||
<overlay overlay={new Widget.Box({
|
||||
halign: Gtk.Align.CENTER,
|
||||
valign: Gtk.Align.CENTER,
|
||||
vertical: true,
|
||||
children: [
|
||||
new Widget.Label({
|
||||
className: "bigText",
|
||||
label: bind(batt, "energyRate").as((w) => `${w}W`),
|
||||
}),
|
||||
new Widget.Label({
|
||||
className: "smallText",
|
||||
label: bind(batt, "charging").as(
|
||||
(c) => c ? "Charging" : "Discharging"
|
||||
),
|
||||
}),
|
||||
]
|
||||
})}>
|
||||
<box className="info" />
|
||||
</overlay>
|
||||
<overlay overlay={new Widget.Box({
|
||||
halign: Gtk.Align.CENTER,
|
||||
valign: Gtk.Align.CENTER,
|
||||
vertical: true,
|
||||
children: [
|
||||
new Widget.Label({
|
||||
className: "status",
|
||||
label: mergeBindings([charging, toFull, toEmpty]).as(
|
||||
(b) => {
|
||||
let s = (b[0] ? b[1] : b[2])
|
||||
return `${Math.floor(s/3600)}H ${Math.floor((s%3600)/60)}M`
|
||||
}
|
||||
),
|
||||
}),
|
||||
new Widget.Label({
|
||||
className: "title",
|
||||
label: charging.as(c => c ? "To full" : "To empty"),
|
||||
}),
|
||||
]
|
||||
})}>
|
||||
<box className="info" />
|
||||
</overlay>
|
||||
</box>
|
||||
</box>
|
||||
<box
|
||||
vertical={true}>
|
||||
{profileButton("Power Saver", "power-saver")}
|
||||
{profileButton("Balanced", "balanced")}
|
||||
{profileButton("Performance", "performance")}
|
||||
</box>
|
||||
</box>
|
||||
</window>
|
||||
}
|
||||
|
||||
export default function battery() {
|
||||
return <button
|
||||
className="battery"
|
||||
halign={Gtk.Align.START}
|
||||
onClicked={(self) => {
|
||||
if (popup.state !== "battery") {
|
||||
if (popup.window !== null) {
|
||||
popup.window.destroy()
|
||||
popup.window = null
|
||||
}
|
||||
popup.window = BattWindow();
|
||||
popup.state = "battery"
|
||||
self.toggleClassName("selected", true)
|
||||
self.hook(popup.window, "destroy", () => {
|
||||
self.toggleClassName("selected", false)
|
||||
})
|
||||
} else {
|
||||
popup.window.destroy()
|
||||
popup.window = null
|
||||
popup.state = ""
|
||||
}}}
|
||||
><box>
|
||||
<icon
|
||||
valign={Gtk.Align.CENTER}
|
||||
css="font-size: 24px; margin-right: 2px;"
|
||||
icon={mergeBindings([charging, bind(powerProfiles, "activeProfile")]).as(
|
||||
b => {
|
||||
if (b[0]) return "battery-charging-symbolic"
|
||||
return `${b[1]}-symbolic`
|
||||
}
|
||||
)}>
|
||||
</icon>
|
||||
<label css="margin-top: 2px;">{percentage}</label>
|
||||
</box>
|
||||
</button>
|
||||
}
|
||||
67
widget/bluetooth.tsx
Normal file
67
widget/bluetooth.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { bind } from "astal"
|
||||
import Bluetooth from "gi://AstalBluetooth"
|
||||
import { popup } from "./popup"
|
||||
import { Astal, Gtk, Widget } from "astal/gtk3"
|
||||
|
||||
const { TOP, LEFT } = Astal.WindowAnchor
|
||||
|
||||
const bluetooth = Bluetooth.get_default()
|
||||
|
||||
function bluetoothItem(device: Bluetooth.Device) {
|
||||
return <button className="toggleButton">
|
||||
<overlay overlay={new Widget.Box({
|
||||
className: "toggleIndicator",
|
||||
halign: Gtk.Align.END,
|
||||
valign: Gtk.Align.CENTER,
|
||||
})}>
|
||||
<box>
|
||||
<icon icon={device.icon} css="font-size: 32px;" />
|
||||
<label>{device.alias ?? device.name}</label>
|
||||
</box>
|
||||
</overlay>
|
||||
</button>
|
||||
}
|
||||
|
||||
function bluetoothWindow() {
|
||||
return <window
|
||||
anchor={ TOP | LEFT }
|
||||
marginLeft={170}
|
||||
namespace="lazerpopup"
|
||||
className="popupWindow">
|
||||
<box vertical={true}>
|
||||
{bluetooth.devices.map(dev => bluetoothItem(dev))}
|
||||
</box>
|
||||
</window>
|
||||
}
|
||||
|
||||
export default function bluetoothWidget() {
|
||||
return <button
|
||||
onClicked={(self) => {
|
||||
if (popup.state !== "bluetooth") {
|
||||
if (popup.window) {
|
||||
popup.window.destroy()
|
||||
popup.window = null
|
||||
}
|
||||
popup.window = bluetoothWindow();
|
||||
popup.state = "bluetooth"
|
||||
self.toggleClassName("selected", true)
|
||||
self.hook(popup.window, "destroy", () => {
|
||||
self.toggleClassName("selected", false)
|
||||
})
|
||||
} else {
|
||||
if (popup.window) {
|
||||
popup.window.destroy()
|
||||
popup.window = null
|
||||
}
|
||||
popup.state = ""
|
||||
}
|
||||
}}
|
||||
><box>
|
||||
<icon
|
||||
icon={bind(bluetooth, "isConnected").as(c =>
|
||||
c ? "lazer-bluetooth-symbolic" : "nobluetooth-symbolic"
|
||||
)}
|
||||
css="font-size: 24px;"
|
||||
/>
|
||||
</box></button>
|
||||
}
|
||||
125
widget/brightness.tsx
Normal file
125
widget/brightness.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import { bind, exec, execAsync, monitorFile, readFile, writeFile } from "astal"
|
||||
import { Astal, Gtk, Gdk, Widget } from "astal/gtk3";
|
||||
import { Box } from "astal/gtk3/widget";
|
||||
import { popup } from "./popup"
|
||||
const { TOP, LEFT } = Astal.WindowAnchor
|
||||
|
||||
const backlight = "/sys/class/backlight/intel_backlight/brightness"
|
||||
|
||||
function brightnessSlider(display) {
|
||||
let slider: Widget.Slider = <box vertical={true} className="volSlider">
|
||||
<box>
|
||||
<icon
|
||||
icon="display"
|
||||
css="font-size: 32px; margin-right: 6px;"
|
||||
/>
|
||||
<label>{display}</label>
|
||||
</box>
|
||||
<slider
|
||||
halign={Gtk.Align.START}
|
||||
valign={Gtk.Align.CENTER}
|
||||
min={0}
|
||||
max={19393}
|
||||
value={Number(readFile(backlight))}
|
||||
onDragged={(self) => {
|
||||
exec(`brightnessctl s ${self.value}`)
|
||||
}}
|
||||
setup={(self) => {
|
||||
/*
|
||||
monitorFile(backlight, (file, event) => {
|
||||
self.value = Number(readFile(file))
|
||||
})
|
||||
*/
|
||||
}}
|
||||
/>
|
||||
</box>
|
||||
return slider
|
||||
}
|
||||
|
||||
function sunset() {
|
||||
try {
|
||||
exec("pgrep -x hyprsunset")
|
||||
return true
|
||||
}
|
||||
catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function BrightWindow() {
|
||||
let toggleIndicator: Box = <box
|
||||
halign={Gtk.Align.END}
|
||||
valign={Gtk.Align.CENTER}
|
||||
className={(sunset()) ? "toggleIndicator active" : "toggleIndicator inactive"}
|
||||
/>
|
||||
return <window
|
||||
className="popupWindow"
|
||||
anchor={TOP | LEFT}
|
||||
namespace="lazerpopup">
|
||||
<box
|
||||
vertical={true}
|
||||
css="margin-left: 130px;">
|
||||
{brightnessSlider("DP-1")}
|
||||
<button
|
||||
className="toggleButton"
|
||||
onClicked={(self) => {
|
||||
toggleIndicator.toggleClassName("active", !sunset())
|
||||
execAsync("/home/protoshark/.config/scripts/sunset-toggle.sh")
|
||||
.then((out) => (console.log(out)))
|
||||
.catch((out) => (console.log(out)))
|
||||
}}
|
||||
><overlay overlay={toggleIndicator}>
|
||||
<label halign={Gtk.Align.START}>Night Light</label>
|
||||
</overlay></button>
|
||||
</box>
|
||||
</window>
|
||||
}
|
||||
|
||||
export default function brightness() {
|
||||
let brightnessBar = <levelbar
|
||||
valign={Gtk.Align.CENTER}
|
||||
vertical={true}
|
||||
inverted={true}
|
||||
minValue={0}
|
||||
maxValue={19393}
|
||||
heightRequest={24}
|
||||
value={Number(readFile("/sys/class/backlight/intel_backlight/brightness"))}
|
||||
/>
|
||||
{monitorFile("/sys/class/backlight/intel_backlight/brightness", (file, event) => {
|
||||
brightnessBar.value = readFile(file)
|
||||
})}
|
||||
return <button
|
||||
className="scroller"
|
||||
onScroll={(self, diff) => {
|
||||
brightnessBar.value -= diff.delta_y*50
|
||||
exec(`brightnessctl s ${brightnessBar.value}`)
|
||||
}}
|
||||
onClicked={(self) => {
|
||||
if (popup.state !== "brightness") {
|
||||
if (popup.window) {
|
||||
popup.window.destroy()
|
||||
popup.window = null
|
||||
}
|
||||
popup.window = BrightWindow();
|
||||
popup.state = "brightness"
|
||||
self.toggleClassName("selected", true)
|
||||
self.hook(popup.window, "destroy", () => {
|
||||
self.toggleClassName("selected", false)
|
||||
})
|
||||
} else {
|
||||
if (popup.window) {
|
||||
popup.window.destroy()
|
||||
popup.window = null
|
||||
}
|
||||
popup.state = ""
|
||||
}
|
||||
}}>
|
||||
<box>
|
||||
<icon
|
||||
icon="brightness-symbolic"
|
||||
css="font-size: 22px; color: white;"
|
||||
/>
|
||||
{brightnessBar}
|
||||
</box>
|
||||
</button>
|
||||
}
|
||||
54
widget/client.tsx
Normal file
54
widget/client.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { bind } from "astal"
|
||||
import Hyprland from "gi://AstalHyprland"
|
||||
import Apps from "gi://AstalApps"
|
||||
import { Gtk, Widget } from "astal/gtk3"
|
||||
import { Overlay } from "astal/gtk3/widget"
|
||||
const hyprland = Hyprland.get_default()
|
||||
const apps = new Apps.Apps()
|
||||
|
||||
function NewClient(client: Hyprland.Client) {
|
||||
apps.entryMultiplier = 10
|
||||
let hyprClass = (client) ? apps.exact_query(client.class)[0] : null
|
||||
let b = <box halign={Gtk.Align.END} className="clientLabel" visible={true}>
|
||||
<icon
|
||||
icon={(client) ? hyprClass?.iconName : "archlinux"}
|
||||
css="font-size: 24px; margin-right: 4px;"
|
||||
/>
|
||||
<label css="font-family: comfortaa; font-size: 14px;">{
|
||||
(client) ? hyprClass?.name : "Hummingbird"
|
||||
}</label>
|
||||
</box>
|
||||
return b
|
||||
}
|
||||
|
||||
export default function client() {
|
||||
let clientContainer = <box />
|
||||
let clientOverlay: Overlay
|
||||
clientOverlay = <overlay
|
||||
overlays={[new Widget.Box(), NewClient(hyprland.focusedClient)]}
|
||||
css={bind(hyprland, "focusedClient").as(c => {
|
||||
let newClient = NewClient(c)
|
||||
clientContainer.css = `min-width: ${newClient.get_preferred_width()[0]}px;`
|
||||
if (clientOverlay) {
|
||||
let os = clientOverlay.overlays
|
||||
if (os[os.length-1].children[1].label === newClient.children[1].label) {
|
||||
return ""
|
||||
}
|
||||
clientOverlay.add_overlay(newClient)
|
||||
if (os.length > 2) {
|
||||
os[os.length-1].css = "margin-top: 50px; opacity: 0;"
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (os.length > 2)
|
||||
clientOverlay.remove(os[os.length-1])
|
||||
}, 500)
|
||||
}
|
||||
return ""
|
||||
})}
|
||||
passThrough={true}>
|
||||
{clientContainer}
|
||||
</overlay>
|
||||
return <button>
|
||||
{clientOverlay}
|
||||
</button>
|
||||
}
|
||||
178
widget/player.tsx
Normal file
178
widget/player.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
import { bind } from "astal"
|
||||
import { Astal, Gtk, Widget } from "astal/gtk3"
|
||||
import { Box, Button, Overlay } from "astal/gtk3/widget"
|
||||
import { popup } from "./popup"
|
||||
import Mpris from "gi://AstalMpris"
|
||||
|
||||
const mpris = Mpris.get_default()
|
||||
const { TOP, RIGHT } = Astal.WindowAnchor
|
||||
|
||||
let playerWindow
|
||||
|
||||
function NewCover(coverPath: string) {
|
||||
let b = <box
|
||||
className="coverArt"
|
||||
halign={Gtk.Align.CENTER}
|
||||
valign={Gtk.Align.CENTER}
|
||||
css={`
|
||||
background-image: url("${coverPath}");
|
||||
background-size: contain;
|
||||
background-color: transparent;
|
||||
`}
|
||||
>
|
||||
</box>
|
||||
return b
|
||||
}
|
||||
|
||||
function cover(player: Mpris.Player) {
|
||||
let coverContainer = <box />
|
||||
let coverOverlay: Overlay
|
||||
coverOverlay = <overlay
|
||||
passThrough={true}
|
||||
overlay={NewCover(player.coverArt)}
|
||||
css={bind(player, "coverArt").as(coverPath => {
|
||||
let newCover = NewCover(coverPath)
|
||||
coverContainer.widthRequest = newCover.get_preferred_width()[0]
|
||||
if (coverOverlay) {
|
||||
coverOverlay.overlay = NewCover(player.coverArt)
|
||||
}
|
||||
return ""
|
||||
})}
|
||||
>
|
||||
<box widthRequest={24} heightRequest={24}/>
|
||||
</overlay>
|
||||
return coverOverlay
|
||||
}
|
||||
|
||||
function wrap(str: string) {
|
||||
if (str.length > 40) {
|
||||
str = str.substring(0, 40)
|
||||
}
|
||||
if (str.length > 20) {
|
||||
return str.substring(0, 20) + "\n" + str.substring(20)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
function playerBox(player: Mpris.Player) {
|
||||
return <box
|
||||
vertical={true}
|
||||
spacing={4}
|
||||
>
|
||||
<box
|
||||
className="playerButton"
|
||||
halign={Gtk.Align.CENTER}>
|
||||
<button onClicked={() => {player.previous()}}>
|
||||
<box>
|
||||
<icon icon="lazer-previous-symbolic" css="font-size: 24px" />
|
||||
</box>
|
||||
</button>
|
||||
<button onClicked={() => {player.play_pause()}}>
|
||||
<box>
|
||||
<icon icon={bind(player, "playbackStatus").as(p =>
|
||||
(p === Mpris.PlaybackStatus.PLAYING)
|
||||
? "lazer-pause-symbolic"
|
||||
: "lazer-play-symbolic"
|
||||
)} css="font-size: 24px;" />
|
||||
</box>
|
||||
</button>
|
||||
<button onClicked={() => {player.next()}}>
|
||||
<box>
|
||||
<icon icon="lazer-next-symbolic" css="font-size: 24px;" />
|
||||
</box>
|
||||
</button>
|
||||
</box>
|
||||
<overlay overlay={
|
||||
new Widget.Box({
|
||||
vertical: true,
|
||||
halign: Gtk.Align.CENTER,
|
||||
valign: Gtk.Align.CENTER,
|
||||
className: "playerLabel",
|
||||
children: [
|
||||
new Widget.Label({
|
||||
className: "playerTitle",
|
||||
justify: Gtk.Justification.CENTER,
|
||||
label: bind(player, "title").as(str => wrap(str))
|
||||
}),
|
||||
new Widget.Label({
|
||||
className: "playerArtist",
|
||||
justify: Gtk.Justification.CENTER,
|
||||
label: bind(player, "artist").as(str => wrap(str))
|
||||
})
|
||||
]
|
||||
})
|
||||
}>
|
||||
<overlay overlay={
|
||||
new Widget.Box({
|
||||
halign: Gtk.Align.CENTER,
|
||||
valign: Gtk.Align.CENTER,
|
||||
className: "bigCoverShadow",
|
||||
})
|
||||
}>
|
||||
<box
|
||||
className="bigCoverArt"
|
||||
css={bind(player, "coverArt").as(c => {
|
||||
return `background-image: url("${c}");`
|
||||
})}>
|
||||
</box>
|
||||
</overlay>
|
||||
</overlay>
|
||||
</box>
|
||||
}
|
||||
|
||||
function PlayerWindow() {
|
||||
return <window
|
||||
className="popupWindow"
|
||||
anchor = {TOP | RIGHT}
|
||||
namespace="lazerpopup"
|
||||
marginRight={160}>
|
||||
<box
|
||||
vertical={true}
|
||||
spacing={12}
|
||||
css="margin-left: 40px; margin-right: 40px; padding-top: 4px"
|
||||
>
|
||||
{mpris.players.map(p => playerBox(p))}
|
||||
</box>
|
||||
</window>
|
||||
}
|
||||
|
||||
export default function player() {
|
||||
let fillerBox = <box/>
|
||||
let playerContainer = <label widthRequest={24} />
|
||||
let playerButton: Button
|
||||
playerButton = <button
|
||||
visible={bind(mpris, "players").as(players => (players.length > 0))}
|
||||
css="padding: 0 8px;"
|
||||
onClicked={(self) => {
|
||||
if (popup.state !== "player") {
|
||||
if (popup.window) {
|
||||
popup.window.destroy()
|
||||
popup.window = null
|
||||
}
|
||||
popup.window = PlayerWindow();
|
||||
popup.state = "player"
|
||||
self.toggleClassName("selected", true)
|
||||
self.hook(popup.window, "destroy", () => {
|
||||
self.toggleClassName("selected", false)
|
||||
})
|
||||
} else {
|
||||
if (popup.window) {
|
||||
popup.window.destroy()
|
||||
popup.window = null
|
||||
}
|
||||
popup.state = ""
|
||||
}
|
||||
}}
|
||||
>
|
||||
<box spacing={6}>
|
||||
<icon icon="player-symbolic"
|
||||
css="font-size: 24px;"/>
|
||||
<overlay passThrough={true} overlay={bind(mpris, "players").as((ps) => {
|
||||
return (ps.length > 0) ? cover(ps[0]) : fillerBox
|
||||
})}>
|
||||
{playerContainer}
|
||||
</overlay>
|
||||
</box>
|
||||
</button>
|
||||
return playerButton
|
||||
}
|
||||
8
widget/popup.tsx
Normal file
8
widget/popup.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Widget } from "astal/gtk3";
|
||||
|
||||
let popup = {
|
||||
window: null,
|
||||
state: "",
|
||||
}
|
||||
|
||||
export { popup }
|
||||
99
widget/volume.tsx
Normal file
99
widget/volume.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import Wp from "gi://AstalWp"
|
||||
import Hyprland from "gi://AstalHyprland";
|
||||
import { bind } from "astal"
|
||||
import { Astal, Gtk, Gdk } from "astal/gtk3";
|
||||
import { popup } from "./popup"
|
||||
const { TOP, LEFT } = Astal.WindowAnchor
|
||||
|
||||
const audio = Wp.get_default()?.audio;
|
||||
const hyprland = Hyprland.get_default()
|
||||
|
||||
function speakerIcon(icon: string): string {
|
||||
switch (icon) {
|
||||
case "audio-headset-bluetooth":
|
||||
return "audio-headset"
|
||||
case "audio-card-analog-pci":
|
||||
return "audio-speakers"
|
||||
default:
|
||||
return "speakers"
|
||||
}
|
||||
}
|
||||
|
||||
function speakerVolume(speaker: Wp.Endpoint) {
|
||||
return <box vertical={true} className="volSlider">
|
||||
<box>
|
||||
<icon
|
||||
icon={speakerIcon(speaker.icon)}
|
||||
css="font-size: 32px;"
|
||||
/>
|
||||
<label>{speaker.description}</label>
|
||||
</box>
|
||||
<slider
|
||||
halign={Gtk.Align.START}
|
||||
valign={Gtk.Align.CENTER}
|
||||
min={0}
|
||||
max={1}
|
||||
value={bind(speaker, "volume").as((s) => {
|
||||
return s
|
||||
})}
|
||||
onDragged={(self) => {
|
||||
speaker.volume = self.value
|
||||
}}
|
||||
/>
|
||||
</box>
|
||||
}
|
||||
|
||||
function VolWindow() {
|
||||
return <window
|
||||
className="popupWindow"
|
||||
anchor={TOP | LEFT}
|
||||
namespace="lazerpopup">
|
||||
<box
|
||||
vertical={true}
|
||||
css="margin-left: 70px;">
|
||||
{bind(audio, "speakers").as(speakers => speakers.map(s => speakerVolume(s)))}
|
||||
</box>
|
||||
</window>
|
||||
}
|
||||
|
||||
export default function volume() {
|
||||
return <button
|
||||
className="scroller"
|
||||
onScroll={(self, diff) => {
|
||||
audio.defaultSpeaker.volume -= diff.delta_y/20
|
||||
}}
|
||||
onClicked={(self) => {
|
||||
if (popup.state !== "volume") {
|
||||
if (popup.window !== null) {
|
||||
popup.window.destroy()
|
||||
popup.window = null
|
||||
}
|
||||
popup.window = VolWindow();
|
||||
popup.state = "volume"
|
||||
self.toggleClassName("selected", true)
|
||||
self.hook(popup.window, "destroy", () => {
|
||||
self.toggleClassName("selected", false)
|
||||
})
|
||||
} else {
|
||||
popup.window.destroy()
|
||||
popup.window = null
|
||||
popup.state = ""
|
||||
}}}
|
||||
>
|
||||
<box>
|
||||
<icon
|
||||
icon="myvolume-symbolic"
|
||||
css="font-size: 24px; color: white;"
|
||||
/>
|
||||
<levelbar
|
||||
valign={Gtk.Align.CENTER}
|
||||
vertical={true}
|
||||
inverted={true}
|
||||
minValue={0}
|
||||
maxValue={1}
|
||||
heightRequest={24}
|
||||
value={bind(audio.defaultSpeaker, "volume").as((v) => v)}
|
||||
/>
|
||||
</box>
|
||||
</button>
|
||||
}
|
||||
59
widget/workspaces.tsx
Normal file
59
widget/workspaces.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { bind } from "astal"
|
||||
import { Gdk, Gtk, Widget } from "astal/gtk3"
|
||||
import Hyprland from "gi://AstalHyprland"
|
||||
|
||||
const hyprland = Hyprland.get_default()
|
||||
|
||||
function newWorkspace(ws: number, monitor: number) {
|
||||
return <overlay
|
||||
halign={Gtk.Align.START}
|
||||
overlays={[
|
||||
new Widget.Button({
|
||||
className: "workspaceButton",
|
||||
visible: true,
|
||||
onClick: () => {
|
||||
hyprland.dispatch("workspace", ws.toString())
|
||||
}
|
||||
}, new Widget.Box({
|
||||
halign: Gtk.Align.FILL,
|
||||
valign: Gtk.Align.FILL,
|
||||
}, new Widget.Box({
|
||||
valign: Gtk.Align.CENTER,
|
||||
halign: Gtk.Align.CENTER,
|
||||
className: bind(hyprland, "workspaces").as(wss => {
|
||||
let myws = wss.find(w => w.id === ws)
|
||||
return (myws !== undefined && myws.monitor.id === monitor)
|
||||
? "active" : "inactive"
|
||||
}),
|
||||
css: bind(hyprland, "focusedWorkspace").as(fws => {
|
||||
return (fws.id === ws)
|
||||
? "border-color: #00ffaa"
|
||||
: "border-color: #ffffff"
|
||||
})
|
||||
}))),
|
||||
]}>
|
||||
<box className="workspaceContainer" />
|
||||
</overlay>
|
||||
}
|
||||
|
||||
export default function workspaces(monitor: Hyprland.Monitor) {
|
||||
return <overlay
|
||||
overlay={new Widget.Box({
|
||||
halign: Gtk.Align.START,
|
||||
valign: Gtk.Align.END,
|
||||
className: "workspaceIndicator",
|
||||
css: bind(hyprland, "focusedWorkspace").as(ws => {
|
||||
return "margin-left: " + (24+(ws.id-1)*32) + "px;"
|
||||
})
|
||||
})}>
|
||||
<box
|
||||
className="workspaces"
|
||||
halign={Gtk.Align.START}
|
||||
visible={true}
|
||||
setup={(self) => {
|
||||
for (let i = 1; i <= 9; i++) {
|
||||
self.add(newWorkspace(i, monitor.id))
|
||||
}
|
||||
}}/>
|
||||
</overlay>
|
||||
}
|
||||
Reference in New Issue
Block a user