diff options
Diffstat (limited to '.config/ags/widget/bar')
| -rw-r--r-- | .config/ags/widget/bar/Bar.ts | 57 | ||||
| -rw-r--r-- | .config/ags/widget/bar/PanelButton.ts | 46 | ||||
| -rw-r--r-- | .config/ags/widget/bar/ScreenCorners.ts | 25 | ||||
| -rw-r--r-- | .config/ags/widget/bar/bar.scss | 242 | ||||
| -rw-r--r-- | .config/ags/widget/bar/buttons/BatteryBar.ts | 94 | ||||
| -rw-r--r-- | .config/ags/widget/bar/buttons/ColorPicker.ts | 37 | ||||
| -rw-r--r-- | .config/ags/widget/bar/buttons/Date.ts | 15 | ||||
| -rw-r--r-- | .config/ags/widget/bar/buttons/Launcher.ts | 49 | ||||
| -rw-r--r-- | .config/ags/widget/bar/buttons/Media.ts | 92 | ||||
| -rw-r--r-- | .config/ags/widget/bar/buttons/Messages.ts | 16 | ||||
| -rw-r--r-- | .config/ags/widget/bar/buttons/PowerMenu.ts | 15 | ||||
| -rw-r--r-- | .config/ags/widget/bar/buttons/ScreenRecord.ts | 21 | ||||
| -rw-r--r-- | .config/ags/widget/bar/buttons/SysTray.ts | 39 | ||||
| -rw-r--r-- | .config/ags/widget/bar/buttons/SystemIndicators.ts | 107 | ||||
| -rw-r--r-- | .config/ags/widget/bar/buttons/Taskbar.ts | 90 | ||||
| -rw-r--r-- | .config/ags/widget/bar/buttons/Workspaces.ts | 66 | ||||
| -rw-r--r-- | .config/ags/widget/bar/screencorner.scss | 51 |
17 files changed, 1062 insertions, 0 deletions
diff --git a/.config/ags/widget/bar/Bar.ts b/.config/ags/widget/bar/Bar.ts new file mode 100644 index 0000000..9343a36 --- /dev/null +++ b/.config/ags/widget/bar/Bar.ts @@ -0,0 +1,57 @@ +import BatteryBar from "./buttons/BatteryBar" +import ColorPicker from "./buttons/ColorPicker" +import Date from "./buttons/Date" +import Launcher from "./buttons/Launcher" +import Media from "./buttons/Media" +import PowerMenu from "./buttons/PowerMenu" +import SysTray from "./buttons/SysTray" +import SystemIndicators from "./buttons/SystemIndicators" +import Taskbar from "./buttons/Taskbar" +import Workspaces from "./buttons/Workspaces" +import ScreenRecord from "./buttons/ScreenRecord" +import Messages from "./buttons/Messages" +import options from "options" + +const { start, center, end } = options.bar.layout +const pos = options.bar.position.bind() + +export type BarWidget = keyof typeof widget + +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: number) => Widget.Window({ + monitor, + class_name: "bar", + name: `bar${monitor}`, + exclusivity: "exclusive", + anchor: pos.as(pos => [pos, "right", "left"]), + child: Widget.CenterBox({ + css: "min-width: 2px; min-height: 2px;", + startWidget: Widget.Box({ + hexpand: true, + children: start.bind().as(s => s.map(w => widget[w]())), + }), + centerWidget: Widget.Box({ + hpack: "center", + children: center.bind().as(c => c.map(w => widget[w]())), + }), + endWidget: Widget.Box({ + hexpand: true, + children: end.bind().as(e => e.map(w => widget[w]())), + }), + }), +}) diff --git a/.config/ags/widget/bar/PanelButton.ts b/.config/ags/widget/bar/PanelButton.ts new file mode 100644 index 0000000..332b46d --- /dev/null +++ b/.config/ags/widget/bar/PanelButton.ts @@ -0,0 +1,46 @@ +import options from "options" +import { ButtonProps } from "types/widgets/button" + +type PanelButtonProps = ButtonProps & { + window?: string, + flat?: boolean +} + +export default ({ + window = "", + flat, + child, + setup, + ...rest +}: PanelButtonProps) => Widget.Button({ + child: Widget.Box({ child }), + setup: self => { + let open = false + + self.toggleClassName("panel-button") + self.toggleClassName(window) + + self.hook(options.bar.flatButtons, () => { + self.toggleClassName("flat", flat ?? options.bar.flatButtons.value) + }) + + 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, +}) diff --git a/.config/ags/widget/bar/ScreenCorners.ts b/.config/ags/widget/bar/ScreenCorners.ts new file mode 100644 index 0000000..1b35e50 --- /dev/null +++ b/.config/ags/widget/bar/ScreenCorners.ts @@ -0,0 +1,25 @@ +import options from "options" + +const { corners } = options.bar + +export default (monitor: number) => 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.hook(corners, () => { + self.toggleClassName("corners", corners.value) + }), +}) diff --git a/.config/ags/widget/bar/bar.scss b/.config/ags/widget/bar/bar.scss new file mode 100644 index 0000000..5c6c2cd --- /dev/null +++ b/.config/ags/widget/bar/bar.scss @@ -0,0 +1,242 @@ +@use 'sass:color'; + +$bar-spacing: $spacing * 0.3; +$button-radius: $radius; + +@mixin panel-button($flat: true, $reactive: true) { + @include accs-button($flat, $reactive); + + >* { + border-radius: $button-radius; + margin: $bar-spacing; + background-color: $bg; + } + + label, + image { + font-weight: bold; + } + + >* { + padding: $padding * 0.8 $padding * 1.2; + } +} + +.bar { + .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 * 0.6; + box-shadow: inset 0 0 0 $border-width $border-color; + margin: 0 $padding * 0.5; + transition: $transition * 0.5; + background-color: transparentize($fg, 0.8); + + &.occupied { + background-color: transparentize($fg, 0.2); + min-width: 7pt; + min-height: 7pt; + } + + &.active { + // background-color: $primary-bg; + background-image: $active-gradient; + min-width: 20pt; + min-height: 12pt; + } + + &.inctive { + // background-color: $primary-bg; + background-image: $active-gradient; + min-width: 20pt; + min-height: 12pt; + } + } + + &.inactive, + &:active { + label { + background-color: transparentize($primary-fg, 0.3); + + &.occupied { + background-color: transparentize($primary-fg, 0.15); + } + + &.active { + background-color: $primary-fg; + } + + &.inactive { + background-color: $primary-fg; + } + } + } + } + + .media label { + margin: 0 ($spacing * 0.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 * 0.7; + color: $fg; + text-shadow: $text-shadow; + } + } + } + + .battery-bar { + >* { + padding: 0; + } + + &.bar-hidden>box { + padding: 0 $spacing * 0.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 * 0.5; + } + + trough { + @include widget; + min-height: 12pt; + min-width: 12pt; + } + + .regular trough { + margin-right: $spacing * 0.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; + } + } +} diff --git a/.config/ags/widget/bar/buttons/BatteryBar.ts b/.config/ags/widget/bar/buttons/BatteryBar.ts new file mode 100644 index 0000000..18de329 --- /dev/null +++ b/.config/ags/widget/bar/buttons/BatteryBar.ts @@ -0,0 +1,94 @@ +import icons from "lib/icons" +import options from "options" +import PanelButton from "../PanelButton" + +const battery = await Service.import("battery") +const { bar, percentage, blocks, width, low } = options.bar.battery + +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({ + bar_mode: "discrete", + max_value: blocks.bind(), + visible: bar.bind().as(b => b !== "hidden"), + value: battery.bind("percent").as(p => (p / 100) * blocks.value), + }) + const update = () => { + level.value = (battery.percent / 100) * blocks.value + level.css = `block { min-width: ${width.value / blocks.value}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 }, + visible: battery.bind("available"), + child: Widget.Box({ + expand: true, + visible: battery.bind("available"), + child: bar.bind().as(b => b === "whole" ? WholeButton() : Regular()), + }), + setup: self => self + .hook(bar, w => w.toggleClassName("bar-hidden", bar.value === "hidden")) + .hook(battery, w => { + w.toggleClassName("charging", battery.charging || battery.charged) + w.toggleClassName("low", battery.percent < low.value) + }), +}) diff --git a/.config/ags/widget/bar/buttons/ColorPicker.ts b/.config/ags/widget/bar/buttons/ColorPicker.ts new file mode 100644 index 0000000..5b1f3f6 --- /dev/null +++ b/.config/ags/widget/bar/buttons/ColorPicker.ts @@ -0,0 +1,37 @@ +import PanelButton from "../PanelButton" +import colorpicker from "service/colorpicker" +import Gdk from "gi://Gdk" + +const css = (color: string) => ` +* { + 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) + }, + }) +} diff --git a/.config/ags/widget/bar/buttons/Date.ts b/.config/ags/widget/bar/buttons/Date.ts new file mode 100644 index 0000000..44c2540 --- /dev/null +++ b/.config/ags/widget/bar/buttons/Date.ts @@ -0,0 +1,15 @@ +import { clock } from "lib/variables" +import PanelButton from "../PanelButton" +import options from "options" + +const { format, action } = options.bar.date +const time = Utils.derive([clock, format], (c, f) => c.format(f) || "") + +export default () => PanelButton({ + window: "datemenu", + on_clicked: action.bind(), + child: Widget.Label({ + justification: "center", + label: time.bind(), + }), +}) diff --git a/.config/ags/widget/bar/buttons/Launcher.ts b/.config/ags/widget/bar/buttons/Launcher.ts new file mode 100644 index 0000000..f3fee6b --- /dev/null +++ b/.config/ags/widget/bar/buttons/Launcher.ts @@ -0,0 +1,49 @@ +import PanelButton from "../PanelButton" +import options from "options" +import nix from "service/nix" + +const { icon, label, action } = options.bar.launcher + +function Spinner() { + const child = Widget.Icon({ + icon: icon.icon.bind(), + class_name: Utils.merge([ + icon.colored.bind(), + nix.bind("ready"), + ], (c, r) => `${c ? "colored" : ""} ${r ? "" : "spinning"}`), + css: ` + @keyframes spin { + to { -gtk-icon-transform: rotate(1turn); } + } + + image.spinning { + animation-name: spin; + animation-duration: 1s; + animation-timing-function: linear; + animation-iteration-count: infinite; + } + `, + }) + + return Widget.Revealer({ + transition: "slide_left", + child, + reveal_child: Utils.merge([ + icon.icon.bind(), + nix.bind("ready"), + ], (i, r) => Boolean(i || r)), + }) +} + +export default () => PanelButton({ + window: "launcher", + on_clicked: action.bind(), + child: Widget.Box([ + Spinner(), + Widget.Label({ + class_name: label.colored.bind().as(c => c ? "colored" : ""), + visible: label.label.bind().as(v => !!v), + label: label.label.bind(), + }), + ]), +}) diff --git a/.config/ags/widget/bar/buttons/Media.ts b/.config/ags/widget/bar/buttons/Media.ts new file mode 100644 index 0000000..b3aab61 --- /dev/null +++ b/.config/ags/widget/bar/buttons/Media.ts @@ -0,0 +1,92 @@ +import { type MprisPlayer } from "types/service/mpris" +import PanelButton from "../PanelButton" +import options from "options" +import icons from "lib/icons" +import { icon } from "lib/utils" + +const mpris = await Service.import("mpris") +const { length, direction, preferred, monochrome, format } = options.bar.media + +const getPlayer = (name = preferred.value) => + mpris.getPlayer(name) || mpris.players[0] || null + +const Content = (player: MprisPlayer) => { + const revealer = Widget.Revealer({ + click_through: true, + visible: length.bind().as(l => l > 0), + transition: direction.bind().as(d => `slide_${d}` as const), + 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.bind().as(n => n > 0 ? n : -1), + label: Utils.merge([ + player.bind("track_title"), + player.bind("track_artists"), + format.bind(), + ], () => `${format}` + .replace("{title}", player.track_title) + .replace("{artists}", player.track_artists.join(", ")) + .replace("{artist}", player.track_artists[0] || "") + .replace("{album}", player.track_album) + .replace("{name}", player.name) + .replace("{identity}", player.identity), + ), + }), + }) + + const playericon = Widget.Icon({ + icon: Utils.merge([player.bind("entry"), monochrome.bind()], (entry => { + const name = `${entry}${monochrome.value ? "-symbolic" : ""}` + return icon(name, icons.fallback.audio) + })), + }) + + return Widget.Box({ + attribute: { revealer }, + children: direction.bind().as(d => d === "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") +} diff --git a/.config/ags/widget/bar/buttons/Messages.ts b/.config/ags/widget/bar/buttons/Messages.ts new file mode 100644 index 0000000..a8971e9 --- /dev/null +++ b/.config/ags/widget/bar/buttons/Messages.ts @@ -0,0 +1,16 @@ +import icons from "lib/icons" +import PanelButton from "../PanelButton" +import options from "options" + +const n = await Service.import("notifications") +const notifs = n.bind("notifications") +const action = options.bar.messages.action.bind() + +export default () => PanelButton({ + class_name: "messages", + on_clicked: action, + visible: notifs.as(n => n.length > 0), + child: Widget.Box([ + Widget.Icon(icons.notifications.message), + ]), +}) diff --git a/.config/ags/widget/bar/buttons/PowerMenu.ts b/.config/ags/widget/bar/buttons/PowerMenu.ts new file mode 100644 index 0000000..4432ade --- /dev/null +++ b/.config/ags/widget/bar/buttons/PowerMenu.ts @@ -0,0 +1,15 @@ +import icons from "lib/icons" +import PanelButton from "../PanelButton" +import options from "options" + +const { monochrome, action } = options.bar.powermenu + +export default () => PanelButton({ + window: "powermenu", + on_clicked: action.bind(), + child: Widget.Icon(icons.powermenu.shutdown), + setup: self => self.hook(monochrome, () => { + self.toggleClassName("colored", !monochrome.value) + self.toggleClassName("box") + }), +}) diff --git a/.config/ags/widget/bar/buttons/ScreenRecord.ts b/.config/ags/widget/bar/buttons/ScreenRecord.ts new file mode 100644 index 0000000..1d6eb36 --- /dev/null +++ b/.config/ags/widget/bar/buttons/ScreenRecord.ts @@ -0,0 +1,21 @@ +import PanelButton from "../PanelButton" +import screenrecord from "service/screenrecord" +import icons from "lib/icons" + +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}` + }), + }), + ], + }), +}) diff --git a/.config/ags/widget/bar/buttons/SysTray.ts b/.config/ags/widget/bar/buttons/SysTray.ts new file mode 100644 index 0000000..9f569d1 --- /dev/null +++ b/.config/ags/widget/bar/buttons/SysTray.ts @@ -0,0 +1,39 @@ +import { type TrayItem } from "types/service/systemtray" +import PanelButton from "../PanelButton" +import Gdk from "gi://Gdk" +import options from "options" + +const systemtray = await Service.import("systemtray") +const { ignore } = options.bar.systray + +const SysTrayItem = (item: TrayItem) => PanelButton({ + class_name: "tray-item", + child: Widget.Icon({ icon: item.bind("icon") }), + tooltip_markup: item.bind("tooltip_markup"), + setup: self => { + const { menu } = item + if (!menu) + return + + const id = menu.connect("popped-up", () => { + self.toggleClassName("active") + menu.connect("notify::visible", () => { + self.toggleClassName("active", menu.visible) + }) + menu.disconnect(id!) + }) + + self.connect("destroy", () => 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.value.includes(id)) + .map(SysTrayItem)) diff --git a/.config/ags/widget/bar/buttons/SystemIndicators.ts b/.config/ags/widget/bar/buttons/SystemIndicators.ts new file mode 100644 index 0000000..cc98548 --- /dev/null +++ b/.config/ags/widget/bar/buttons/SystemIndicators.ts @@ -0,0 +1,107 @@ +import PanelButton from '../PanelButton'; +import icons from 'lib/icons'; +import asusctl from 'service/asusctl'; + +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 powerprof = await Service.import('powerprofiles'); + +const ProfileIndicator = () => { + const visible = asusctl.available ? asusctl.bind('profile').as(p => p !== 'Balanced') : powerprof.bind('active_profile').as(p => p !== 'balanced'); + + const icon = asusctl.available ? asusctl.bind('profile').as(p => icons.asusctl.profile[p]) : powerprof.bind('active_profile').as(p => icons.powerprofile[p]); + + return Widget.Icon({ visible, icon }); +}; + +const ModeIndicator = () => { + if (!asusctl.available) { + return Widget.Icon({ + setup(self) { + Utils.idle(() => (self.visible = false)); + }, + }); + } + + return Widget.Icon({ + visible: asusctl.bind('mode').as(m => m !== 'Hybrid'), + icon: asusctl.bind('mode').as(m => icons.asusctl.mode[m]), + }); +}; + +const MicrophoneIndicator = () => + Widget.Icon() + .hook(audio, self => (self.visible = audio.recorders.length > 0 || audio.microphone.is_muted || false)) + .hook(audio.microphone, self => { + const vol = audio.microphone.is_muted ? 0 : audio.microphone.volume; + const { muted, low, medium, high } = icons.audio.mic; + const cons = [ + [67, high], + [34, medium], + [1, low], + [0, muted], + ] as const; + 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().hook(audio.speaker, self => { + const vol = audio.speaker.is_muted ? 0 : audio.speaker.volume; + const { muted, low, medium, high, overamplified } = icons.audio.volume; + const cons = [ + [101, overamplified], + [67, high], + [34, medium], + [1, low], + [0, muted], + ] as const; + self.icon = cons.find(([n]) => n <= vol * 100)?.[1] || ''; + }); + +export default () => + PanelButton({ + window: 'quicksettings', + on_clicked: () => App.toggleWindow('quicksettings'), + on_scroll_up: () => (audio.speaker.volume += 0.02), + on_scroll_down: () => (audio.speaker.volume -= 0.02), + child: Widget.Box([ + //ProfileIndicator(), + ModeIndicator(), + DNDIndicator(), + BluetoothIndicator(), + MicrophoneIndicator(), + AudioIndicator(), + NetworkIndicator(), + ]), + }); diff --git a/.config/ags/widget/bar/buttons/Taskbar.ts b/.config/ags/widget/bar/buttons/Taskbar.ts new file mode 100644 index 0000000..b9c65fa --- /dev/null +++ b/.config/ags/widget/bar/buttons/Taskbar.ts @@ -0,0 +1,90 @@ +import { launchApp, icon } from "lib/utils" +import icons from "lib/icons" +import options from "options" +import PanelButton from "../PanelButton" + +const hyprland = await Service.import("hyprland") +const apps = await Service.import("applications") +const { monochrome, exclusive, iconSize } = options.bar.taskbar +const { position } = options.bar + +const focus = (address: string) => hyprland.messageAsync( + `dispatch focuswindow address:${address}`) + +const DummyItem = (address: string) => Widget.Box({ + attribute: { address }, + visible: false, +}) + +const AppItem = (address: string) => { + 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: Utils.watch(client.title, hyprland, () => + hyprland.getClient(address)?.title || "", + ), + on_primary_click: () => focus(address), + on_middle_click: () => app && launchApp(app), + child: Widget.Icon({ + size: iconSize.bind(), + icon: monochrome.bind().as(m => icon( + (app?.icon_name || client.class) + (m ? "-symbolic" : ""), + icons.fallback.executable + (m ? "-symbolic" : ""), + )), + }), + }) + + return Widget.Box( + { + attribute: { address }, + visible: Utils.watch(true, [exclusive, hyprland], () => { + return exclusive.value + ? hyprland.active.workspace.id === client.workspace.id + : true + }), + }, + Widget.Overlay({ + child: btn, + pass_through: true, + overlay: Widget.Box({ + className: "indicator", + hpack: "center", + vpack: position.bind().as(p => p === "top" ? "start" : "end"), + setup: w => w.hook(hyprland, () => { + w.toggleClassName("active", hyprland.active.client.address === address) + }), + }), + }), + ) +} + +function sortItems<T extends { attribute: { address: string } }>(arr: T[]) { + 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?: string) => { + if (typeof address === "string") + w.children = w.children.filter(ch => ch.attribute.address !== address) + }, "client-removed") + .hook(hyprland, (w, address?: string) => { + if (typeof address === "string") + w.children = sortItems([...w.children, AppItem(address)]) + }, "client-added") + .hook(hyprland, (w, event?: string) => { + if (event === "movewindow") + w.children = sortItems(w.children) + }, "event"), +}) diff --git a/.config/ags/widget/bar/buttons/Workspaces.ts b/.config/ags/widget/bar/buttons/Workspaces.ts new file mode 100644 index 0000000..a59f61b --- /dev/null +++ b/.config/ags/widget/bar/buttons/Workspaces.ts @@ -0,0 +1,66 @@ +import PanelButton from '../PanelButton'; +import options from 'options'; +import { sh, range } from 'lib/utils'; + +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 => { + const updateState = () => { + const monitorData = JSON.parse(hyprland.message('j/monitors')); + const activeWorkspaceId = monitorData[0]?.activeWorkspace?.id; + const workspaceData = hyprland.getWorkspace(i); + + if (activeWorkspaceId !== undefined) { + self.toggleClassName('active', activeWorkspaceId === i); + } + self.toggleClassName('occupied', (workspaceData?.windows || 0) > 0); + }; + + // Hook to Hyprland for updates + self.hook(hyprland, updateState); + + // Initial update + updateState(); + }, + }), + ), + setup: box => { + box.hook(hyprland, () => { + const monitorData = JSON.parse(hyprland.message('j/monitors')); + const activeWorkspaceId = monitorData[0]?.activeWorkspace?.id; + + if (activeWorkspaceId !== undefined) { + for (const btn of box.children) { + const workspaceId = btn.attribute; + btn.toggleClassName('active', workspaceId === activeWorkspaceId); + + if (ws === 0) { + btn.visible = hyprland.workspaces.some(workspace => workspace.id === workspaceId); + } + } + } + }); + }, + }); + +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.bind().as(Workspaces), + }); diff --git a/.config/ags/widget/bar/screencorner.scss b/.config/ags/widget/bar/screencorner.scss new file mode 100644 index 0000000..93cd459 --- /dev/null +++ b/.config/ags/widget/bar/screencorner.scss @@ -0,0 +1,51 @@ +$_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 transparent; + } + + @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 none $bg; + //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); + } + } +} |
