This commit is contained in:
Nico
2024-10-31 10:33:46 +01:00
commit ad6d893cb0
237 changed files with 19793 additions and 0 deletions

View File

@@ -0,0 +1,56 @@
import BatteryBar from "./buttons/BatteryBar.js"
import ColorPicker from "./buttons/ColorPicker.js"
import Date from "./buttons/Date.js"
import Launcher from "./buttons/Launcher.js"
import Media from "./buttons/Media.js"
import PowerMenu from "./buttons/PowerMenu.js"
import SysTray from "./buttons/SysTray.js"
import SystemIndicators from "./buttons/SystemIndicators.js"
import Taskbar from "./buttons/Taskbar.js"
import Workspaces from "./buttons/Workspaces.js"
import ScreenRecord from "./buttons/ScreenRecord.js"
import Messages from "./buttons/Messages.js"
import options from "../../options.js"
const { start, center, end } = options.bar.layout
const pos = options.bar.position
const widget = {
battery: BatteryBar,
colorpicker: ColorPicker,
date: Date,
launcher: Launcher,
media: Media,
powermenu: PowerMenu,
systray: SysTray,
system: SystemIndicators,
taskbar: Taskbar,
workspaces: Workspaces,
screenrecord: ScreenRecord,
messages: Messages,
expander: () => Widget.Box({ expand: true }),
}
export default (monitor) => Widget.Window({
monitor,
class_name: "bar",
name: `bar${monitor}`,
exclusivity: "exclusive",
layer: "top",
anchor: [pos, "right", "left"],
child: Widget.CenterBox({
css: "min-width: 2px; min-height: 2px;",
startWidget: Widget.Box({
hexpand: true,
children: start.map(w => widget[w]()),
}),
centerWidget: Widget.Box({
hpack: "center",
children: center.map(w => widget[w]()),
}),
endWidget: Widget.Box({
hexpand: true,
children: end.map(w => widget[w]()),
}),
}),
})

View File

@@ -0,0 +1,38 @@
import options from "../../options.js"
export default ({
window = "",
flat,
child,
setup,
...rest
}) => Widget.Button({
child: Widget.Box({ child }),
setup: self => {
let open = false
self.toggleClassName("panel-button")
self.toggleClassName(window)
self.toggleClassName("flat", flat ?? options.bar.flatButtons)
self.hook(App, (_, win, visible) => {
if (win !== window)
return
if (open && !visible) {
open = false
self.toggleClassName("active", false)
}
if (visible) {
open = true
self.toggleClassName("active")
}
})
if (setup)
setup(self)
},
...rest,
})

View File

@@ -0,0 +1,25 @@
import options from "../../options.js"
const { corners } = options.bar
export default (monitor) => Widget.Window({
monitor,
name: `corner${monitor}`,
class_name: "screen-corner",
anchor: ["top", "bottom", "right", "left"],
click_through: true,
child: Widget.Box({
class_name: "shadow",
child: Widget.Box({
class_name: "border",
expand: true,
child: Widget.Box({
class_name: "corner",
expand: true,
}),
}),
}),
setup: self => {
self.toggleClassName("corners", corners)
},
})

View File

@@ -0,0 +1,234 @@
@use 'sass:color';
$bar-spacing: $spacing * .3;
$button-radius: $radius;
@mixin panel-button($flat: true, $reactive: true) {
@include accs-button($flat, $reactive);
>* {
border-radius: $button-radius;
margin: $bar-spacing;
}
label,
image {
font-weight: bold;
}
>* {
padding: $padding * 0.4 $padding * 0.8;
}
}
.bar {
background-color: $bg;
.panel-button {
@include panel-button;
&:not(.flat) {
@include accs-button($flat: false);
}
}
.launcher {
.colored {
color: transparentize($primary-bg, 0.2);
}
&:hover .colored {
color: $primary-bg;
}
&:active .colored,
&.active .colored {
color: $primary-fg;
}
}
.workspaces {
label {
font-size: 0;
min-width: 5pt;
min-height: 5pt;
border-radius: $radius*.6;
box-shadow: inset 0 0 0 $border-width $border-color;
margin: 0 $padding * .5;
transition: $transition* .5;
background-color: transparentize($fg, .8);
&.occupied {
background-color: transparentize($fg, .2);
min-width: 7pt;
min-height: 7pt;
}
&.active {
// background-color: $primary-bg;
background-image: $active-gradient;
min-width: 20pt;
min-height: 12pt;
}
}
&.active,
&:active {
label {
background-color: transparentize($primary-fg, .3);
&.occupied {
background-color: transparentize($primary-fg, .15);
}
&.active {
background-color: $primary-fg;
}
}
}
}
.media label {
margin: 0 ($spacing * .5)
}
.taskbar .indicator.active {
background-color: $primary-bg;
border-radius: $radius;
min-height: 4pt;
min-width: 6pt;
margin: 2pt;
}
.powermenu.colored,
.recorder {
image {
color: transparentize($error-bg, 0.3);
}
&:hover image {
color: transparentize($error-bg, 0.15);
}
&:active image {
color: $primary-fg;
}
}
.quicksettings>box>box {
@include spacing($spacing: if($bar-spacing==0, $padding / 2, $bar-spacing));
}
.quicksettings:not(.active):not(:active) {
.bluetooth {
color: $primary-bg;
label {
font-size: $font-size * .7;
color: $fg;
text-shadow: $text-shadow;
}
}
}
.battery-bar {
>* {
padding: 0;
}
&.bar-hidden>box {
padding: 0 $spacing * .5;
image {
margin: 0;
}
}
levelbar * {
all: unset;
transition: $transition;
}
.whole {
@if $shadows {
image {
-gtk-icon-shadow: $text-shadow;
}
label {
text-shadow: $text-shadow;
}
}
}
.regular image {
margin-left: $spacing * .5;
}
trough {
@include widget;
min-height: 12pt;
min-width: 12pt;
}
.regular trough {
margin-right: $spacing * .5;
}
block {
margin: 0;
&:last-child {
border-radius: 0 $button-radius $button-radius 0;
}
&:first-child {
border-radius: $button-radius 0 0 $button-radius;
}
}
.vertical {
block {
&:last-child {
border-radius: 0 0 $button-radius $button-radius;
}
&:first-child {
border-radius: $button-radius $button-radius 0 0;
}
}
}
@for $i from 1 through $bar-battery-blocks {
block:nth-child(#{$i}).filled {
background-color: color.mix($bg, $primary-bg, $i*3)
}
&.low block:nth-child(#{$i}).filled {
background-color: color.mix($bg, $error-bg, $i*3)
}
&.charging block:nth-child(#{$i}).filled {
background-color: color.mix($bg, $charging-bg, $i*3)
}
&:active .regular block:nth-child(#{$i}).filled {
background-color: color.mix($bg, $primary-fg, $i*3)
}
}
&.low image {
color: $error-bg
}
&.charging image {
color: $charging-bg
}
&:active image {
color: $primary-fg
}
}
}

View File

@@ -0,0 +1,94 @@
import icons from "../../../lib/icons.js"
import options from "../../../options.js"
import PanelButton from "../PanelButton.js"
const battery = await Service.import("battery")
let { bar, percentage, blocks, width, low } = options.bar.battery
percentage = Variable(percentage, {})
const Indicator = () => Widget.Icon({
setup: self => self.hook(battery, () => {
self.icon = battery.charging || battery.charged
? icons.battery.charging
: battery.icon_name
}),
})
const PercentLabel = () => Widget.Revealer({
transition: "slide_right",
click_through: true,
reveal_child: percentage.bind(),
child: Widget.Label({
label: battery.bind("percent").as(p => `${p}%`),
}),
})
const LevelBar = () => {
const level = Widget.LevelBar({
mode: 1,
max_value: blocks,
visible: bar !== "hidden",
value: battery.bind("percent").as(p => (p / 100) * blocks),
})
const update = () => {
level.value = (battery.percent / 100) * blocks
level.css = `block { min-width: ${width / blocks}pt; }`
}
return level
// .hook(width, update)
// .hook(blocks, update)
// .hook(bar, () => {
// level.vpack = bar.value === "whole" ? "fill" : "center"
// level.hpack = bar.value === "whole" ? "fill" : "center"
// })
}
const WholeButton = () => Widget.Overlay({
vexpand: true,
child: LevelBar(),
class_name: "whole",
pass_through: true,
overlay: Widget.Box({
hpack: "center",
children: [
Widget.Icon({
icon: icons.battery.charging,
visible: Utils.merge([
battery.bind("charging"),
battery.bind("charged"),
], (ing, ed) => ing || ed),
}),
Widget.Box({
hpack: "center",
vpack: "center",
child: PercentLabel(),
}),
],
}),
})
const Regular = () => Widget.Box({
class_name: "regular",
children: [
Indicator(),
PercentLabel(),
LevelBar(),
],
})
export default () => PanelButton({
class_name: "battery-bar",
hexpand: false,
on_clicked: () => { percentage.value = !percentage.value },
child: Widget.Box({
expand: true,
visible: battery.bind("available"),
child: bar === "whole" ? WholeButton() : Regular(),
}),
setup: self => {
self.toggleClassName("bar-hidden", bar === "hidden")
self.toggleClassName("charging", battery.charging || battery.charged)
self.toggleClassName("low", battery.percent < low)
}
})

View File

@@ -0,0 +1,37 @@
import PanelButton from "../PanelButton.js"
import colorpicker from "../../../services/colorpicker.js"
import Gdk from "gi://Gdk"
const css = (color) => `
* {
background-color: ${color};
color: transparent;
}
*:hover {
color: white;
text-shadow: 2px 2px 3px rgba(0,0,0,.8);
}`
export default () => {
const menu = Widget.Menu({
class_name: "colorpicker",
children: colorpicker.bind("colors").as(c => c.map(color => Widget.MenuItem({
child: Widget.Label(color),
css: css(color),
on_activate: () => colorpicker.wlCopy(color),
}))),
})
return PanelButton({
class_name: "color-picker",
child: Widget.Icon("color-select-symbolic"),
tooltip_text: colorpicker.bind("colors").as(v => `${v.length} colors`),
on_clicked: colorpicker.pick,
on_secondary_click: self => {
if (colorpicker.colors.length === 0)
return
menu.popup_at_widget(self, Gdk.Gravity.SOUTH, Gdk.Gravity.NORTH, null)
},
})
}

View File

@@ -0,0 +1,19 @@
import clock from "../../../services/clock.js"
import PanelButton from "../PanelButton.js"
import options from "../../../options.js"
const { format, action } = options.bar.date
// const time = Utils.derive([clock], (c) => {
// c.format(format) || ""
// })
// const time = Variable('', {
// poll: [1000, `date "+${format}"`],
// });
export default () => PanelButton({
window: "dashboard",
on_clicked: action,
child: Widget.Label({ label: clock.bind('time').as(t => `${t.format(format)}`) }),
})

View File

@@ -0,0 +1,21 @@
import PanelButton from "../PanelButton.js"
import options from "../../../options.js"
const { icon, label, action } = options.bar.launcher
export default () => PanelButton({
window: "launcher",
on_clicked: action,
child: Widget.Box([
Widget.Icon({
class_name: icon.colored ? "colored" : "",
visible: !!icon.icon,
icon: icon.icon,
}),
Widget.Label({
class_name: label.colored ? "colored" : "",
visible: !!label.label,
label: label.label,
}),
]),
})

View File

@@ -0,0 +1,81 @@
import PanelButton from "../PanelButton.js"
import options from "../../../options.js"
import icons from "../../../lib/icons.js"
import { icon } from "../../../lib/utils.js"
const mpris = await Service.import("mpris")
const { length, direction, preferred, monochrome } = options.bar.media
const getPlayer = (name = preferred) =>
mpris.getPlayer(name) || mpris.players[0] || null
const Content = (player) => {
const revealer = Widget.Revealer({
click_through: true,
visible: (length > 0),
transition: `slide_${direction}`,
setup: self => {
let current = ""
self.hook(player, () => {
if (current === player.track_title)
return
current = player.track_title
self.reveal_child = true
Utils.timeout(3000, () => {
!self.is_destroyed && (self.reveal_child = false)
})
})
},
child: Widget.Label({
truncate: "end",
max_width_chars: length,
label: player.bind("track_title").as(() =>
`${player.track_artists.join(", ")} - ${player.track_title}`),
}),
})
const playericon = Widget.Icon({
icon: player.bind("entry").as(entry => {
const name = `${entry}${monochrome ? "-symbolic" : ""}`
return icon(name, icons.fallback.audio)
}),
})
return Widget.Box({
attribute: { revealer },
children: direction === "right"
? [playericon, revealer] : [revealer, playericon],
})
}
export default () => {
let player = getPlayer()
const btn = PanelButton({
class_name: "media",
child: Widget.Icon(icons.fallback.audio),
})
const update = () => {
player = getPlayer()
btn.visible = !!player
if (!player)
return
const content = Content(player)
const { revealer } = content.attribute
btn.child = content
btn.on_primary_click = () => { player.playPause() }
btn.on_secondary_click = () => { player.playPause() }
btn.on_scroll_up = () => { player.next() }
btn.on_scroll_down = () => { player.previous() }
btn.on_hover = () => { revealer.reveal_child = true }
btn.on_hover_lost = () => { revealer.reveal_child = false }
}
return btn
// .hook(preferred, update)
.hook(mpris, update, "notify::players")
}

View File

@@ -0,0 +1,16 @@
import icons from "../../../lib/icons.js"
import PanelButton from "../PanelButton.js"
import options from "../../../options.js"
const n = await Service.import("notifications")
const notifs = n.bind("notifications")
const action = options.bar.messages.action
export default () => PanelButton({
class_name: "messages",
on_clicked: action,
visible: notifs.as(n => n.length > 0),
child: Widget.Box([
Widget.Icon(icons.notifications.message),
]),
})

View File

@@ -0,0 +1,15 @@
import icons from "../../../lib/icons.js"
import PanelButton from "../PanelButton.js"
import options from "../../../options.js"
const { monochrome, action } = options.bar.powermenu
export default () => PanelButton({
window: "powermenu",
on_clicked: action,
child: Widget.Icon(icons.powermenu.shutdown),
setup: self => {
self.toggleClassName("colored", !monochrome)
self.toggleClassName("box")
},
})

View File

@@ -0,0 +1,21 @@
import PanelButton from "../PanelButton.js"
import screenrecord from "../../../services/screenrecord.js"
import icons from "../../../lib/icons.js"
export default () => PanelButton({
class_name: "recorder",
on_clicked: () => screenrecord.stop(),
visible: screenrecord.bind("recording"),
child: Widget.Box({
children: [
Widget.Icon(icons.recorder.recording),
Widget.Label({
label: screenrecord.bind("timer").as(time => {
const sec = time % 60
const min = Math.floor(time / 60)
return `${min}:${sec < 10 ? "0" + sec : sec}`
}),
}),
],
}),
})

View File

@@ -0,0 +1,39 @@
import PanelButton from "../PanelButton.js"
import Gdk from "gi://Gdk"
import options from "../../../options.js"
const systemtray = await Service.import("systemtray")
const { ignore } = options.bar.systray
const SysTrayItem = (item) => PanelButton({
class_name: "tray-item",
child: Widget.Icon({ icon: item.bind("icon") }),
tooltip_markup: item.bind("tooltip_markup"),
setup: self => {
const menu = item.menu
if (!menu)
return
const id = item.menu?.connect("popped-up", () => {
self.toggleClassName("active")
menu.connect("notify::visible", () => {
self.toggleClassName("active", menu.visible)
})
menu.disconnect(id)
})
if (id)
self.connect("destroy", () => item.menu?.disconnect(id))
},
on_primary_click: btn => item.menu?.popup_at_widget(
btn, Gdk.Gravity.SOUTH, Gdk.Gravity.NORTH, null),
on_secondary_click: btn => item.menu?.popup_at_widget(
btn, Gdk.Gravity.SOUTH, Gdk.Gravity.NORTH, null),
})
export default () => Widget.Box()
.bind("children", systemtray, "items", i => i
.filter(({ id }) => !ignore.includes(id))
.map(SysTrayItem))

View File

@@ -0,0 +1,79 @@
import PanelButton from "../PanelButton.js"
import icons from "../../../lib/icons.js"
import asusctl from "../../../services/asusctl.js"
const notifications = await Service.import("notifications")
const bluetooth = await Service.import("bluetooth")
const audio = await Service.import("audio")
const network = await Service.import("network")
const ProfileIndicator = () => Widget.Icon()
.bind("visible", asusctl, "profile", p => p !== "Balanced")
.bind("icon", asusctl, "profile", p => icons.asusctl.profile[p])
const ModeIndicator = () => Widget.Icon()
.bind("visible", asusctl, "mode", m => m !== "Hybrid")
.bind("icon", asusctl, "mode", m => icons.asusctl.mode[m])
const MicrophoneIndicator = () => Widget.Icon()
.hook(audio, self => self.visible =
audio.recorders.length > 0
|| audio.microphone.stream?.is_muted
|| audio.microphone.is_muted)
.hook(audio.microphone, self => {
const vol = audio.microphone.stream.is_muted ? 0 : audio.microphone.volume
const { muted, low, medium, high } = icons.audio.mic
const cons = [[67, high], [34, medium], [1, low], [0, muted]]
self.icon = cons.find(([n]) => n <= vol * 100)?.[1] || ""
})
const DNDIndicator = () => Widget.Icon({
visible: notifications.bind("dnd"),
icon: icons.notifications.silent,
})
const BluetoothIndicator = () => Widget.Overlay({
class_name: "bluetooth",
passThrough: true,
child: Widget.Icon({
icon: icons.bluetooth.enabled,
visible: bluetooth.bind("enabled"),
}),
overlay: Widget.Label({
hpack: "end",
vpack: "start",
label: bluetooth.bind("connected_devices").as(c => `${c.length}`),
visible: bluetooth.bind("connected_devices").as(c => c.length > 0),
}),
})
const NetworkIndicator = () => Widget.Icon().hook(network, self => {
const icon = network[network.primary || "wifi"]?.icon_name
self.icon = icon || ""
self.visible = !!icon
})
const AudioIndicator = () => Widget.Icon({
icon: audio.speaker.bind("volume").as(vol => {
const { muted, low, medium, high, overamplified } = icons.audio.volume
const cons = [[101, overamplified], [67, high], [34, medium], [1, low], [0, muted]]
const icon = cons.find(([n]) => n <= vol * 100)?.[1] || ""
return audio.speaker.is_muted ? muted : icon
}),
})
export default () => PanelButton({
class_name: "quicksettings panel-button",
on_clicked: () => App.toggleWindow("quicksettings"),
on_scroll_up: () => audio.speaker.volume += 0.02,
on_scroll_down: () => audio.speaker.volume -= 0.02,
child: Widget.Box([
asusctl?.available && ProfileIndicator(),
asusctl?.available && ModeIndicator(),
DNDIndicator(),
BluetoothIndicator(),
NetworkIndicator(),
AudioIndicator(),
MicrophoneIndicator(),
]),
})

View File

@@ -0,0 +1,86 @@
import { launchApp, icon } from "../../../lib/utils.js"
import icons from "../../../lib/icons.js"
import options from "../../../options.js"
import { watch } from "../../../lib/experiments.js"
import PanelButton from "../PanelButton.js"
const hyprland = await Service.import("hyprland")
const apps = await Service.import("applications")
const { monochrome, exclusive } = options.bar.taskbar
const { position } = options.bar
const focus = (address) => hyprland.messageAsync(
`dispatch focuswindow address:${address}`)
const DummyItem = (address) => Widget.Box({
attribute: { address },
visible: false,
})
const AppItem = (address) => {
const client = hyprland.getClient(address)
if (!client || client.class === "")
return DummyItem(address)
const app = apps.list.find(app => app.match(client.class))
const btn = PanelButton({
class_name: "panel-button",
tooltip_text: client.title,
on_primary_click: () => focus(address),
on_middle_click: () => app && launchApp(app),
visible: watch(true, [hyprland], () => {
return exclusive
? hyprland.active.workspace.id === client.workspace.id
: true
}),
child: Widget.Icon({
icon: icon(
(app?.icon_name || client.class) + (monochrome ? "-symbolic" : ""),
icons.fallback.executable,
),
}),
})
return Widget.Box(
{ attribute: { address } },
Widget.Overlay({
child: btn,
pass_through: true,
overlay: Widget.Box({
className: "indicator",
hpack: "center",
vpack: (position === "top" ? "start" : "end"),
setup: w => w.hook(hyprland, () => {
w.toggleClassName("active", hyprland.active.client.address === address)
}),
}),
}),
)
}
function sortItems(arr) {
return arr.sort(({ attribute: a }, { attribute: b }) => {
const aclient = hyprland.getClient(a.address)
const bclient = hyprland.getClient(b.address)
return aclient.workspace.id - bclient.workspace.id
})
}
export default () => Widget.Box({
class_name: "taskbar",
children: sortItems(hyprland.clients.map(c => AppItem(c.address))),
setup: w => w
.hook(hyprland, (w, address) => {
if (typeof address === "string")
w.children = w.children.filter(ch => ch.attribute.address !== address)
}, "client-removed")
.hook(hyprland, (w, address) => {
if (typeof address === "string")
w.children = sortItems([...w.children, AppItem(address)])
}, "client-added")
.hook(hyprland, (w, event) => {
if (event === "movewindow")
w.children = sortItems(w.children)
}, "event"),
})

View File

@@ -0,0 +1,38 @@
import PanelButton from "../PanelButton.js"
import options from "../../../options.js"
import { sh, range } from "../../../lib/utils.js"
const hyprland = await Service.import("hyprland")
const { workspaces } = options.bar.workspaces
const dispatch = (arg) => {
sh(`hyprctl dispatch workspace ${arg}`)
}
const Workspaces = (ws) => Widget.Box({
children: range(ws || 20).map(i => Widget.Label({
attribute: i,
vpack: "center",
label: `${i}`,
setup: self => self.hook(hyprland, () => {
self.toggleClassName("active", hyprland.active.workspace.id === i)
self.toggleClassName("occupied", (hyprland.getWorkspace(i)?.windows || 0) > 0)
}),
})),
setup: box => {
if (ws === 0) {
box.hook(hyprland.active.workspace, () => box.children.map(btn => {
btn.visible = hyprland.workspaces.some(ws => ws.id === btn.attribute)
}))
}
},
})
export default () => PanelButton({
window: "overview",
class_name: "workspaces",
on_scroll_up: () => dispatch("m+1"),
on_scroll_down: () => dispatch("m-1"),
on_clicked: () => App.toggleWindow("overview"),
child: Workspaces(workspaces),
})

View File

@@ -0,0 +1,50 @@
$_shadow-size: $padding;
$_radius: $radius * $hyprland-gaps-multiplier;
$_margin: 99px;
window.screen-corner {
box.shadow {
margin-right: $_margin * -1;
margin-left: $_margin * -1;
@if $shadows {
box-shadow: inset 0 0 $_shadow-size 0 $shadow-color;
}
@if $bar-position =="top" {
margin-bottom: $_margin * -1;
}
@if $bar-position =="bottom" {
margin-top: $_margin * -1;
}
}
box.border {
@if $bar-position =="top" {
border-top: $border-width solid $bg;
}
@if $bar-position =="bottom" {
border-bottom: $border-width solid $bg;
}
margin-right: $_margin;
margin-left: $_margin;
}
box.corner {
box-shadow: 0 0 0 $border-width $border-color;
}
&.corners {
box.border {
border-radius: if($radius>0, $radius * $hyprland-gaps-multiplier, 0);
box-shadow: 0 0 0 $_radius $bg;
}
box.corner {
border-radius: if($radius>0, $radius * $hyprland-gaps-multiplier, 0);
}
}
}