Added ags
TODO copy ags config
This commit is contained in:
156
roles/ags/files/widget/PopupWindow.ts
Normal file
156
roles/ags/files/widget/PopupWindow.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import { type WindowProps } from "types/widgets/window"
|
||||
import { type RevealerProps } from "types/widgets/revealer"
|
||||
import { type EventBoxProps } from "types/widgets/eventbox"
|
||||
import type Gtk from "gi://Gtk?version=3.0"
|
||||
import options from "options"
|
||||
|
||||
type Transition = RevealerProps["transition"]
|
||||
type Child = WindowProps["child"]
|
||||
|
||||
type PopupWindowProps = Omit<WindowProps, "name"> & {
|
||||
name: string
|
||||
layout?: keyof ReturnType<typeof Layout>
|
||||
transition?: Transition,
|
||||
}
|
||||
|
||||
export const Padding = (name: string, {
|
||||
css = "",
|
||||
hexpand = true,
|
||||
vexpand = true,
|
||||
}: EventBoxProps = {}) => Widget.EventBox({
|
||||
hexpand,
|
||||
vexpand,
|
||||
can_focus: false,
|
||||
child: Widget.Box({ css }),
|
||||
setup: w => w.on("button-press-event", () => App.toggleWindow(name)),
|
||||
})
|
||||
|
||||
const PopupRevealer = (
|
||||
name: string,
|
||||
child: Child,
|
||||
transition: Transition = "slide_down",
|
||||
) => Widget.Box(
|
||||
{ css: "padding: 1px;" },
|
||||
Widget.Revealer({
|
||||
transition,
|
||||
child: Widget.Box({
|
||||
class_name: "window-content",
|
||||
child,
|
||||
}),
|
||||
transitionDuration: options.transition.bind(),
|
||||
setup: self => self.hook(App, (_, wname, visible) => {
|
||||
if (wname === name)
|
||||
self.reveal_child = visible
|
||||
}),
|
||||
}),
|
||||
)
|
||||
|
||||
const Layout = (name: string, child: Child, transition?: Transition) => ({
|
||||
"center": () => Widget.CenterBox({},
|
||||
Padding(name),
|
||||
Widget.CenterBox(
|
||||
{ vertical: true },
|
||||
Padding(name),
|
||||
PopupRevealer(name, child, transition),
|
||||
Padding(name),
|
||||
),
|
||||
Padding(name),
|
||||
),
|
||||
"top": () => Widget.CenterBox({},
|
||||
Padding(name),
|
||||
Widget.Box(
|
||||
{ vertical: true },
|
||||
PopupRevealer(name, child, transition),
|
||||
Padding(name),
|
||||
),
|
||||
Padding(name),
|
||||
),
|
||||
"top-right": () => Widget.Box({},
|
||||
Padding(name),
|
||||
Widget.Box(
|
||||
{
|
||||
hexpand: false,
|
||||
vertical: true,
|
||||
},
|
||||
PopupRevealer(name, child, transition),
|
||||
Padding(name),
|
||||
),
|
||||
),
|
||||
"top-center": () => Widget.Box({},
|
||||
Padding(name),
|
||||
Widget.Box(
|
||||
{
|
||||
hexpand: false,
|
||||
vertical: true,
|
||||
},
|
||||
PopupRevealer(name, child, transition),
|
||||
Padding(name),
|
||||
),
|
||||
Padding(name),
|
||||
),
|
||||
"top-left": () => Widget.Box({},
|
||||
Widget.Box(
|
||||
{
|
||||
hexpand: false,
|
||||
vertical: true,
|
||||
},
|
||||
PopupRevealer(name, child, transition),
|
||||
Padding(name),
|
||||
),
|
||||
Padding(name),
|
||||
),
|
||||
"bottom-left": () => Widget.Box({},
|
||||
Widget.Box(
|
||||
{
|
||||
hexpand: false,
|
||||
vertical: true,
|
||||
},
|
||||
Padding(name),
|
||||
PopupRevealer(name, child, transition),
|
||||
),
|
||||
Padding(name),
|
||||
),
|
||||
"bottom-center": () => Widget.Box({},
|
||||
Padding(name),
|
||||
Widget.Box(
|
||||
{
|
||||
hexpand: false,
|
||||
vertical: true,
|
||||
},
|
||||
Padding(name),
|
||||
PopupRevealer(name, child, transition),
|
||||
),
|
||||
Padding(name),
|
||||
),
|
||||
"bottom-right": () => Widget.Box({},
|
||||
Padding(name),
|
||||
Widget.Box(
|
||||
{
|
||||
hexpand: false,
|
||||
vertical: true,
|
||||
},
|
||||
Padding(name),
|
||||
PopupRevealer(name, child, transition),
|
||||
),
|
||||
),
|
||||
})
|
||||
|
||||
export default ({
|
||||
name,
|
||||
child,
|
||||
layout = "center",
|
||||
transition,
|
||||
exclusivity = "ignore",
|
||||
...props
|
||||
}: PopupWindowProps) => Widget.Window<Gtk.Widget>({
|
||||
name,
|
||||
class_names: [name, "popup-window"],
|
||||
setup: w => w.keybind("Escape", () => App.closeWindow(name)),
|
||||
visible: false,
|
||||
keymode: "on-demand",
|
||||
exclusivity,
|
||||
layer: "top",
|
||||
anchor: ["top", "bottom", "right", "left"],
|
||||
child: Layout(name, child, transition)[layout](),
|
||||
...props,
|
||||
})
|
||||
3
roles/ags/files/widget/RegularWindow.ts
Normal file
3
roles/ags/files/widget/RegularWindow.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import Gtk from "gi://Gtk?version=3.0"
|
||||
|
||||
export default Widget.subclass<typeof Gtk.Window, Gtk.Window.ConstructorProperties>(Gtk.Window)
|
||||
57
roles/ags/files/widget/bar/Bar.ts
Normal file
57
roles/ags/files/widget/bar/Bar.ts
Normal file
@@ -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]())),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
46
roles/ags/files/widget/bar/PanelButton.ts
Normal file
46
roles/ags/files/widget/bar/PanelButton.ts
Normal file
@@ -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,
|
||||
})
|
||||
25
roles/ags/files/widget/bar/ScreenCorners.ts
Normal file
25
roles/ags/files/widget/bar/ScreenCorners.ts
Normal file
@@ -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)
|
||||
}),
|
||||
})
|
||||
234
roles/ags/files/widget/bar/bar.scss
Normal file
234
roles/ags/files/widget/bar/bar.scss
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
94
roles/ags/files/widget/bar/buttons/BatteryBar.ts
Normal file
94
roles/ags/files/widget/bar/buttons/BatteryBar.ts
Normal file
@@ -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)
|
||||
}),
|
||||
})
|
||||
37
roles/ags/files/widget/bar/buttons/ColorPicker.ts
Normal file
37
roles/ags/files/widget/bar/buttons/ColorPicker.ts
Normal file
@@ -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)
|
||||
},
|
||||
})
|
||||
}
|
||||
15
roles/ags/files/widget/bar/buttons/Date.ts
Normal file
15
roles/ags/files/widget/bar/buttons/Date.ts
Normal file
@@ -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(),
|
||||
}),
|
||||
})
|
||||
49
roles/ags/files/widget/bar/buttons/Launcher.ts
Normal file
49
roles/ags/files/widget/bar/buttons/Launcher.ts
Normal file
@@ -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(),
|
||||
}),
|
||||
]),
|
||||
})
|
||||
92
roles/ags/files/widget/bar/buttons/Media.ts
Normal file
92
roles/ags/files/widget/bar/buttons/Media.ts
Normal file
@@ -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")
|
||||
}
|
||||
16
roles/ags/files/widget/bar/buttons/Messages.ts
Normal file
16
roles/ags/files/widget/bar/buttons/Messages.ts
Normal file
@@ -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),
|
||||
]),
|
||||
})
|
||||
15
roles/ags/files/widget/bar/buttons/PowerMenu.ts
Normal file
15
roles/ags/files/widget/bar/buttons/PowerMenu.ts
Normal file
@@ -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")
|
||||
}),
|
||||
})
|
||||
21
roles/ags/files/widget/bar/buttons/ScreenRecord.ts
Normal file
21
roles/ags/files/widget/bar/buttons/ScreenRecord.ts
Normal file
@@ -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}`
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
})
|
||||
39
roles/ags/files/widget/bar/buttons/SysTray.ts
Normal file
39
roles/ags/files/widget/bar/buttons/SysTray.ts
Normal file
@@ -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))
|
||||
98
roles/ags/files/widget/bar/buttons/SystemIndicators.ts
Normal file
98
roles/ags/files/widget/bar/buttons/SystemIndicators.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
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(),
|
||||
NetworkIndicator(),
|
||||
AudioIndicator(),
|
||||
MicrophoneIndicator(),
|
||||
]),
|
||||
})
|
||||
90
roles/ags/files/widget/bar/buttons/Taskbar.ts
Normal file
90
roles/ags/files/widget/bar/buttons/Taskbar.ts
Normal file
@@ -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"),
|
||||
})
|
||||
38
roles/ags/files/widget/bar/buttons/Workspaces.ts
Normal file
38
roles/ags/files/widget/bar/buttons/Workspaces.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
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: string | number) => {
|
||||
sh(`hyprctl dispatch workspace ${arg}`)
|
||||
}
|
||||
|
||||
const Workspaces = (ws: number) => 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.bind().as(Workspaces),
|
||||
})
|
||||
50
roles/ags/files/widget/bar/screencorner.scss
Normal file
50
roles/ags/files/widget/bar/screencorner.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
37
roles/ags/files/widget/datemenu/DateColumn.ts
Normal file
37
roles/ags/files/widget/datemenu/DateColumn.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { clock, uptime } from "lib/variables"
|
||||
|
||||
function up(up: number) {
|
||||
const h = Math.floor(up / 60)
|
||||
const m = Math.floor(up % 60)
|
||||
return `uptime: ${h}:${m < 10 ? "0" + m : m}`
|
||||
}
|
||||
|
||||
export default () => Widget.Box({
|
||||
vertical: true,
|
||||
class_name: "date-column vertical",
|
||||
children: [
|
||||
Widget.Box({
|
||||
class_name: "clock-box",
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Label({
|
||||
class_name: "clock",
|
||||
label: clock.bind().as(t => t.format("%H:%M")!),
|
||||
}),
|
||||
Widget.Label({
|
||||
class_name: "uptime",
|
||||
label: uptime.bind().as(up),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Widget.Box({
|
||||
class_name: "calendar",
|
||||
children: [
|
||||
Widget.Calendar({
|
||||
hexpand: true,
|
||||
hpack: "center",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
})
|
||||
36
roles/ags/files/widget/datemenu/DateMenu.ts
Normal file
36
roles/ags/files/widget/datemenu/DateMenu.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import PopupWindow from "widget/PopupWindow"
|
||||
import NotificationColumn from "./NotificationColumn"
|
||||
import DateColumn from "./DateColumn"
|
||||
import options from "options"
|
||||
|
||||
const { bar, datemenu } = options
|
||||
const pos = bar.position.bind()
|
||||
const layout = Utils.derive([bar.position, datemenu.position], (bar, qs) =>
|
||||
`${bar}-${qs}` as const,
|
||||
)
|
||||
|
||||
const Settings = () => Widget.Box({
|
||||
class_name: "datemenu horizontal",
|
||||
vexpand: false,
|
||||
children: [
|
||||
NotificationColumn(),
|
||||
Widget.Separator({ orientation: 1 }),
|
||||
DateColumn(),
|
||||
],
|
||||
})
|
||||
|
||||
const DateMenu = () => PopupWindow({
|
||||
name: "datemenu",
|
||||
exclusivity: "exclusive",
|
||||
transition: pos.as(pos => pos === "top" ? "slide_down" : "slide_up"),
|
||||
layout: layout.value,
|
||||
child: Settings(),
|
||||
})
|
||||
|
||||
export function setupDateMenu() {
|
||||
App.addWindow(DateMenu())
|
||||
layout.connect("changed", () => {
|
||||
App.removeWindow("datemenu")
|
||||
App.addWindow(DateMenu())
|
||||
})
|
||||
}
|
||||
113
roles/ags/files/widget/datemenu/NotificationColumn.ts
Normal file
113
roles/ags/files/widget/datemenu/NotificationColumn.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { type Notification as Notif } from "types/service/notifications"
|
||||
import Notification from "widget/notifications/Notification"
|
||||
import options from "options"
|
||||
import icons from "lib/icons"
|
||||
|
||||
const notifications = await Service.import("notifications")
|
||||
const notifs = notifications.bind("notifications")
|
||||
|
||||
const Animated = (n: Notif) => Widget.Revealer({
|
||||
transition_duration: options.transition.value,
|
||||
transition: "slide_down",
|
||||
child: Notification(n),
|
||||
setup: self => Utils.timeout(options.transition.value, () => {
|
||||
if (!self.is_destroyed)
|
||||
self.reveal_child = true
|
||||
}),
|
||||
})
|
||||
|
||||
const ClearButton = () => Widget.Button({
|
||||
on_clicked: notifications.clear,
|
||||
sensitive: notifs.as(n => n.length > 0),
|
||||
child: Widget.Box({
|
||||
children: [
|
||||
Widget.Label("Clear "),
|
||||
Widget.Icon({
|
||||
icon: notifs.as(n => icons.trash[n.length > 0 ? "full" : "empty"]),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
})
|
||||
|
||||
const Header = () => Widget.Box({
|
||||
class_name: "header",
|
||||
children: [
|
||||
Widget.Label({ label: "Notifications", hexpand: true, xalign: 0 }),
|
||||
ClearButton(),
|
||||
],
|
||||
})
|
||||
|
||||
const NotificationList = () => {
|
||||
const map: Map<number, ReturnType<typeof Animated>> = new Map
|
||||
const box = Widget.Box({
|
||||
vertical: true,
|
||||
children: notifications.notifications.map(n => {
|
||||
const w = Animated(n)
|
||||
map.set(n.id, w)
|
||||
return w
|
||||
}),
|
||||
visible: notifs.as(n => n.length > 0),
|
||||
})
|
||||
|
||||
function remove(_: unknown, id: number) {
|
||||
const n = map.get(id)
|
||||
if (n) {
|
||||
n.reveal_child = false
|
||||
Utils.timeout(options.transition.value, () => {
|
||||
n.destroy()
|
||||
map.delete(id)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return box
|
||||
.hook(notifications, remove, "closed")
|
||||
.hook(notifications, (_, id: number) => {
|
||||
if (id !== undefined) {
|
||||
if (map.has(id))
|
||||
remove(null, id)
|
||||
|
||||
const n = notifications.getNotification(id)!
|
||||
|
||||
const w = Animated(n)
|
||||
map.set(id, w)
|
||||
box.children = [w, ...box.children]
|
||||
}
|
||||
}, "notified")
|
||||
}
|
||||
|
||||
const Placeholder = () => Widget.Box({
|
||||
class_name: "placeholder",
|
||||
vertical: true,
|
||||
vpack: "center",
|
||||
hpack: "center",
|
||||
vexpand: true,
|
||||
hexpand: true,
|
||||
visible: notifs.as(n => n.length === 0),
|
||||
children: [
|
||||
Widget.Icon(icons.notifications.silent),
|
||||
Widget.Label("Your inbox is empty"),
|
||||
],
|
||||
})
|
||||
|
||||
export default () => Widget.Box({
|
||||
class_name: "notifications",
|
||||
css: options.notifications.width.bind().as(w => `min-width: ${w}px`),
|
||||
vertical: true,
|
||||
children: [
|
||||
Header(),
|
||||
Widget.Scrollable({
|
||||
vexpand: true,
|
||||
hscroll: "never",
|
||||
class_name: "notification-scrollable",
|
||||
child: Widget.Box({
|
||||
class_name: "notification-list vertical",
|
||||
vertical: true,
|
||||
children: [
|
||||
NotificationList(),
|
||||
Placeholder(),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
],
|
||||
})
|
||||
110
roles/ags/files/widget/datemenu/datemenu.scss
Normal file
110
roles/ags/files/widget/datemenu/datemenu.scss
Normal file
@@ -0,0 +1,110 @@
|
||||
@import "../notifications/notifications.scss";
|
||||
|
||||
@mixin calendar {
|
||||
@include widget;
|
||||
padding: $padding*2 $padding*2 0;
|
||||
|
||||
calendar {
|
||||
all: unset;
|
||||
|
||||
&.button {
|
||||
@include button($flat: true);
|
||||
}
|
||||
|
||||
&:selected {
|
||||
box-shadow: inset 0 -8px 0 0 transparentize($primary-bg, 0.5),
|
||||
inset 0 0 0 1px $primary-bg;
|
||||
border-radius: $radius*0.6;
|
||||
}
|
||||
|
||||
&.header {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: transparentize($fg, 0.5);
|
||||
}
|
||||
|
||||
&.highlight {
|
||||
background-color: transparent;
|
||||
color: transparentize($primary-bg, 0.5);
|
||||
}
|
||||
|
||||
&:indeterminate {
|
||||
color: transparentize($fg, 0.9);
|
||||
}
|
||||
|
||||
font-size: 1.1em;
|
||||
padding: .2em;
|
||||
}
|
||||
}
|
||||
|
||||
window#datemenu .datemenu {
|
||||
@include floating-widget;
|
||||
|
||||
.notifications {
|
||||
.header {
|
||||
margin-bottom: $spacing;
|
||||
margin-right: $spacing;
|
||||
|
||||
>label {
|
||||
margin-left: $radius * .5;
|
||||
}
|
||||
|
||||
button {
|
||||
@include button;
|
||||
padding: $padding*.7 $padding;
|
||||
}
|
||||
}
|
||||
|
||||
.notification-scrollable {
|
||||
@include scrollable($top: true, $bottom: true);
|
||||
}
|
||||
|
||||
.notification-list {
|
||||
margin-right: $spacing;
|
||||
}
|
||||
|
||||
.notification {
|
||||
@include notification;
|
||||
@include widget;
|
||||
padding: $padding;
|
||||
margin-bottom: $spacing;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
image {
|
||||
font-size: 7em;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
separator {
|
||||
background-color: $popover-border-color;
|
||||
border-radius: $radius;
|
||||
margin-right: $spacing;
|
||||
}
|
||||
|
||||
.datemenu {
|
||||
@include spacing;
|
||||
}
|
||||
|
||||
.clock-box {
|
||||
padding: $padding;
|
||||
|
||||
.clock {
|
||||
font-size: 5em;
|
||||
}
|
||||
|
||||
.uptime {
|
||||
color: transparentize($fg, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.calendar {
|
||||
@include calendar;
|
||||
}
|
||||
}
|
||||
40
roles/ags/files/widget/desktop/Desktop.ts
Normal file
40
roles/ags/files/widget/desktop/Desktop.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import options from "options"
|
||||
import { matugen } from "lib/matugen"
|
||||
const mpris = await Service.import("mpris")
|
||||
|
||||
const pref = () => options.bar.media.preferred.value
|
||||
|
||||
export default (monitor: number) => Widget.Window({
|
||||
monitor,
|
||||
layer: "bottom",
|
||||
name: `desktop${monitor}`,
|
||||
class_name: "desktop",
|
||||
anchor: ["top", "bottom", "left", "right"],
|
||||
child: Widget.Box({
|
||||
expand: true,
|
||||
css: options.theme.dark.primary.bg.bind().as(c => `
|
||||
transition: 500ms;
|
||||
background-color: ${c}`),
|
||||
child: Widget.Box({
|
||||
class_name: "wallpaper",
|
||||
expand: true,
|
||||
vpack: "center",
|
||||
hpack: "center",
|
||||
setup: self => self
|
||||
.hook(mpris, () => {
|
||||
const img = mpris.getPlayer(pref())!.cover_path
|
||||
matugen("image", img)
|
||||
Utils.timeout(500, () => self.css = `
|
||||
background-image: url('${img}');
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
transition: 200ms;
|
||||
min-width: 700px;
|
||||
min-height: 700px;
|
||||
border-radius: 30px;
|
||||
box-shadow: 25px 25px 30px 0 rgba(0,0,0,0.5);`,
|
||||
)
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
130
roles/ags/files/widget/launcher/AppLauncher.ts
Normal file
130
roles/ags/files/widget/launcher/AppLauncher.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { type Application } from "types/service/applications"
|
||||
import { launchApp, icon } from "lib/utils"
|
||||
import options from "options"
|
||||
import icons from "lib/icons"
|
||||
|
||||
const apps = await Service.import("applications")
|
||||
const { query } = apps
|
||||
const { iconSize } = options.launcher.apps
|
||||
|
||||
const QuickAppButton = (app: Application) => Widget.Button({
|
||||
hexpand: true,
|
||||
tooltip_text: app.name,
|
||||
on_clicked: () => {
|
||||
App.closeWindow("launcher")
|
||||
launchApp(app)
|
||||
},
|
||||
child: Widget.Icon({
|
||||
size: iconSize.bind(),
|
||||
icon: icon(app.icon_name, icons.fallback.executable),
|
||||
}),
|
||||
})
|
||||
|
||||
const AppItem = (app: Application) => {
|
||||
const title = Widget.Label({
|
||||
class_name: "title",
|
||||
label: app.name,
|
||||
hexpand: true,
|
||||
xalign: 0,
|
||||
vpack: "center",
|
||||
truncate: "end",
|
||||
})
|
||||
|
||||
const description = Widget.Label({
|
||||
class_name: "description",
|
||||
label: app.description || "",
|
||||
hexpand: true,
|
||||
wrap: true,
|
||||
max_width_chars: 30,
|
||||
xalign: 0,
|
||||
justification: "left",
|
||||
vpack: "center",
|
||||
})
|
||||
|
||||
const appicon = Widget.Icon({
|
||||
icon: icon(app.icon_name, icons.fallback.executable),
|
||||
size: iconSize.bind(),
|
||||
})
|
||||
|
||||
const textBox = Widget.Box({
|
||||
vertical: true,
|
||||
vpack: "center",
|
||||
children: app.description ? [title, description] : [title],
|
||||
})
|
||||
|
||||
return Widget.Button({
|
||||
class_name: "app-item",
|
||||
attribute: { app },
|
||||
child: Widget.Box({
|
||||
children: [appicon, textBox],
|
||||
}),
|
||||
on_clicked: () => {
|
||||
App.closeWindow("launcher")
|
||||
launchApp(app)
|
||||
},
|
||||
})
|
||||
}
|
||||
export function Favorites() {
|
||||
const favs = options.launcher.apps.favorites.bind()
|
||||
return Widget.Revealer({
|
||||
visible: favs.as(f => f.length > 0),
|
||||
child: Widget.Box({
|
||||
vertical: true,
|
||||
children: favs.as(favs => favs.flatMap(fs => [
|
||||
Widget.Separator(),
|
||||
Widget.Box({
|
||||
class_name: "quicklaunch horizontal",
|
||||
children: fs
|
||||
.map(f => query(f)?.[0])
|
||||
.filter(f => f)
|
||||
.map(QuickAppButton),
|
||||
}),
|
||||
])),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
export function Launcher() {
|
||||
const applist = Variable(query(""))
|
||||
const max = options.launcher.apps.max
|
||||
let first = applist.value[0]
|
||||
|
||||
function SeparatedAppItem(app: Application) {
|
||||
return Widget.Revealer(
|
||||
{ attribute: { app } },
|
||||
Widget.Box(
|
||||
{ vertical: true },
|
||||
Widget.Separator(),
|
||||
AppItem(app),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
const list = Widget.Box({
|
||||
vertical: true,
|
||||
children: applist.bind().as(list => list.map(SeparatedAppItem)),
|
||||
setup: self => self
|
||||
.hook(apps, () => applist.value = query(""), "notify::frequents"),
|
||||
})
|
||||
|
||||
return Object.assign(list, {
|
||||
filter(text: string | null) {
|
||||
first = query(text || "")[0]
|
||||
list.children.reduce((i, item) => {
|
||||
if (!text || i >= max.value) {
|
||||
item.reveal_child = false
|
||||
return i
|
||||
}
|
||||
if (item.attribute.app.match(text)) {
|
||||
item.reveal_child = true
|
||||
return ++i
|
||||
}
|
||||
item.reveal_child = false
|
||||
return i
|
||||
}, 0)
|
||||
},
|
||||
launchFirst() {
|
||||
launchApp(first)
|
||||
},
|
||||
})
|
||||
}
|
||||
139
roles/ags/files/widget/launcher/Launcher.ts
Normal file
139
roles/ags/files/widget/launcher/Launcher.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { type Binding } from "lib/utils"
|
||||
import PopupWindow, { Padding } from "widget/PopupWindow"
|
||||
import icons from "lib/icons"
|
||||
import options from "options"
|
||||
import nix from "service/nix"
|
||||
import * as AppLauncher from "./AppLauncher"
|
||||
import * as NixRun from "./NixRun"
|
||||
import * as ShRun from "./ShRun"
|
||||
|
||||
const { width, margin } = options.launcher
|
||||
const isnix = nix.available
|
||||
|
||||
function Launcher() {
|
||||
const favs = AppLauncher.Favorites()
|
||||
const applauncher = AppLauncher.Launcher()
|
||||
const sh = ShRun.ShRun()
|
||||
const shicon = ShRun.Icon()
|
||||
const nix = NixRun.NixRun()
|
||||
const nixload = NixRun.Spinner()
|
||||
|
||||
function HelpButton(cmd: string, desc: string | Binding<string>) {
|
||||
return Widget.Box(
|
||||
{ vertical: true },
|
||||
Widget.Separator(),
|
||||
Widget.Button(
|
||||
{
|
||||
class_name: "help",
|
||||
on_clicked: () => {
|
||||
entry.grab_focus()
|
||||
entry.text = `:${cmd} `
|
||||
entry.set_position(-1)
|
||||
},
|
||||
},
|
||||
Widget.Box([
|
||||
Widget.Label({
|
||||
class_name: "name",
|
||||
label: `:${cmd}`,
|
||||
}),
|
||||
Widget.Label({
|
||||
hexpand: true,
|
||||
hpack: "end",
|
||||
class_name: "description",
|
||||
label: desc,
|
||||
}),
|
||||
]),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
const help = Widget.Revealer({
|
||||
child: Widget.Box(
|
||||
{ vertical: true },
|
||||
HelpButton("sh", "run a binary"),
|
||||
isnix ? HelpButton("nx", options.launcher.nix.pkgs.bind().as(pkg =>
|
||||
`run a nix package from ${pkg}`,
|
||||
)) : Widget.Box(),
|
||||
),
|
||||
})
|
||||
|
||||
const entry = Widget.Entry({
|
||||
hexpand: true,
|
||||
primary_icon_name: icons.ui.search,
|
||||
on_accept: ({ text }) => {
|
||||
if (text?.startsWith(":nx"))
|
||||
nix.run(text.substring(3))
|
||||
else if (text?.startsWith(":sh"))
|
||||
sh.run(text.substring(3))
|
||||
else
|
||||
applauncher.launchFirst()
|
||||
|
||||
App.toggleWindow("launcher")
|
||||
entry.text = ""
|
||||
},
|
||||
on_change: ({ text }) => {
|
||||
text ||= ""
|
||||
favs.reveal_child = text === ""
|
||||
help.reveal_child = text.split(" ").length === 1 && text?.startsWith(":")
|
||||
|
||||
if (text?.startsWith(":nx"))
|
||||
nix.filter(text.substring(3))
|
||||
else
|
||||
nix.filter("")
|
||||
|
||||
if (text?.startsWith(":sh"))
|
||||
sh.filter(text.substring(3))
|
||||
else
|
||||
sh.filter("")
|
||||
|
||||
if (!text?.startsWith(":"))
|
||||
applauncher.filter(text)
|
||||
},
|
||||
})
|
||||
|
||||
function focus() {
|
||||
entry.text = "Search"
|
||||
entry.set_position(-1)
|
||||
entry.select_region(0, -1)
|
||||
entry.grab_focus()
|
||||
favs.reveal_child = true
|
||||
}
|
||||
|
||||
const layout = Widget.Box({
|
||||
css: width.bind().as(v => `min-width: ${v}pt;`),
|
||||
class_name: "launcher",
|
||||
vertical: true,
|
||||
vpack: "start",
|
||||
setup: self => self.hook(App, (_, win, visible) => {
|
||||
if (win !== "launcher")
|
||||
return
|
||||
|
||||
entry.text = ""
|
||||
if (visible)
|
||||
focus()
|
||||
}),
|
||||
children: [
|
||||
Widget.Box([entry, nixload, shicon]),
|
||||
favs,
|
||||
help,
|
||||
applauncher,
|
||||
nix,
|
||||
sh,
|
||||
],
|
||||
})
|
||||
|
||||
return Widget.Box(
|
||||
{ vertical: true, css: "padding: 1px" },
|
||||
Padding("applauncher", {
|
||||
css: margin.bind().as(v => `min-height: ${v}pt;`),
|
||||
vexpand: false,
|
||||
}),
|
||||
layout,
|
||||
)
|
||||
}
|
||||
|
||||
export default () => PopupWindow({
|
||||
name: "launcher",
|
||||
layout: "top",
|
||||
child: Launcher(),
|
||||
})
|
||||
118
roles/ags/files/widget/launcher/NixRun.ts
Normal file
118
roles/ags/files/widget/launcher/NixRun.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import icons from "lib/icons"
|
||||
import nix, { type Nixpkg } from "service/nix"
|
||||
|
||||
const iconVisible = Variable(false)
|
||||
|
||||
function Item(pkg: Nixpkg) {
|
||||
const name = Widget.Label({
|
||||
class_name: "name",
|
||||
label: pkg.name.split(".").at(-1),
|
||||
})
|
||||
|
||||
const subpkg = pkg.name.includes(".") ? Widget.Label({
|
||||
class_name: "description",
|
||||
hpack: "end",
|
||||
hexpand: true,
|
||||
label: ` ${pkg.name.split(".").slice(0, -1).join(".")}`,
|
||||
}) : null
|
||||
|
||||
const version = Widget.Label({
|
||||
class_name: "version",
|
||||
label: pkg.version,
|
||||
hexpand: true,
|
||||
hpack: "end",
|
||||
})
|
||||
|
||||
const description = pkg.description ? Widget.Label({
|
||||
class_name: "description",
|
||||
label: pkg.description,
|
||||
justification: "left",
|
||||
wrap: true,
|
||||
hpack: "start",
|
||||
max_width_chars: 40,
|
||||
}) : null
|
||||
|
||||
return Widget.Box(
|
||||
{
|
||||
attribute: { name: pkg.name },
|
||||
vertical: true,
|
||||
},
|
||||
Widget.Separator(),
|
||||
Widget.Button(
|
||||
{
|
||||
class_name: "nix-item",
|
||||
on_clicked: () => {
|
||||
nix.run(pkg.name)
|
||||
App.closeWindow("launcher")
|
||||
},
|
||||
},
|
||||
Widget.Box(
|
||||
{ vertical: true },
|
||||
Widget.Box([name, version]),
|
||||
Widget.Box([
|
||||
description as ReturnType<typeof Widget.Label>,
|
||||
subpkg as ReturnType<typeof Widget.Label>,
|
||||
]),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
export function Spinner() {
|
||||
const icon = Widget.Icon({
|
||||
icon: icons.nix.nix,
|
||||
class_name: "spinner",
|
||||
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;
|
||||
}
|
||||
`,
|
||||
setup: self => self.hook(nix, () => {
|
||||
self.toggleClassName("spinning", !nix.ready)
|
||||
}),
|
||||
})
|
||||
|
||||
return Widget.Revealer({
|
||||
transition: "slide_left",
|
||||
child: icon,
|
||||
reveal_child: Utils.merge([
|
||||
nix.bind("ready"),
|
||||
iconVisible.bind(),
|
||||
], (ready, show) => !ready || show),
|
||||
})
|
||||
}
|
||||
|
||||
export function NixRun() {
|
||||
const list = Widget.Box<ReturnType<typeof Item>>({
|
||||
vertical: true,
|
||||
})
|
||||
|
||||
const revealer = Widget.Revealer({
|
||||
child: list,
|
||||
})
|
||||
|
||||
async function filter(term: string) {
|
||||
iconVisible.value = Boolean(term)
|
||||
|
||||
if (!term)
|
||||
revealer.reveal_child = false
|
||||
|
||||
if (term.trim()) {
|
||||
const found = await nix.query(term)
|
||||
list.children = found.map(k => Item(nix.db[k]))
|
||||
revealer.reveal_child = true
|
||||
}
|
||||
}
|
||||
|
||||
return Object.assign(revealer, {
|
||||
filter,
|
||||
run: nix.run,
|
||||
})
|
||||
}
|
||||
89
roles/ags/files/widget/launcher/ShRun.ts
Normal file
89
roles/ags/files/widget/launcher/ShRun.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import icons from "lib/icons"
|
||||
import options from "options"
|
||||
import { bash, dependencies } from "lib/utils"
|
||||
|
||||
const iconVisible = Variable(false)
|
||||
|
||||
const MAX = options.launcher.sh.max
|
||||
const BINS = `${Utils.CACHE_DIR}/binaries`
|
||||
bash("{ IFS=:; ls -H $PATH; } | sort ")
|
||||
.then(bins => Utils.writeFile(bins, BINS))
|
||||
|
||||
async function query(filter: string) {
|
||||
if (!dependencies("fzf"))
|
||||
return [] as string[]
|
||||
|
||||
return bash(`cat ${BINS} | fzf -f ${filter} | head -n ${MAX}`)
|
||||
.then(str => Array.from(new Set(str.split("\n").filter(i => i)).values()))
|
||||
.catch(err => { print(err); return [] })
|
||||
}
|
||||
|
||||
function run(args: string) {
|
||||
Utils.execAsync(args)
|
||||
.then(out => {
|
||||
print(`:sh ${args.trim()}:`)
|
||||
print(out)
|
||||
})
|
||||
.catch(err => {
|
||||
Utils.notify("ShRun Error", err, icons.app.terminal)
|
||||
})
|
||||
}
|
||||
|
||||
function Item(bin: string) {
|
||||
return Widget.Box(
|
||||
{
|
||||
attribute: { bin },
|
||||
vertical: true,
|
||||
},
|
||||
Widget.Separator(),
|
||||
Widget.Button({
|
||||
child: Widget.Label({
|
||||
label: bin,
|
||||
hpack: "start",
|
||||
}),
|
||||
class_name: "sh-item",
|
||||
on_clicked: () => {
|
||||
Utils.execAsync(bin)
|
||||
App.closeWindow("launcher")
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export function Icon() {
|
||||
const icon = Widget.Icon({
|
||||
icon: icons.app.terminal,
|
||||
class_name: "spinner",
|
||||
})
|
||||
|
||||
return Widget.Revealer({
|
||||
transition: "slide_left",
|
||||
child: icon,
|
||||
reveal_child: iconVisible.bind(),
|
||||
})
|
||||
}
|
||||
|
||||
export function ShRun() {
|
||||
const list = Widget.Box<ReturnType<typeof Item>>({
|
||||
vertical: true,
|
||||
})
|
||||
|
||||
const revealer = Widget.Revealer({
|
||||
child: list,
|
||||
})
|
||||
|
||||
async function filter(term: string) {
|
||||
iconVisible.value = Boolean(term)
|
||||
|
||||
if (!term)
|
||||
revealer.reveal_child = false
|
||||
|
||||
if (term.trim()) {
|
||||
const found = await query(term)
|
||||
list.children = found.map(Item)
|
||||
revealer.reveal_child = true
|
||||
}
|
||||
}
|
||||
|
||||
return Object.assign(revealer, { filter, run })
|
||||
}
|
||||
143
roles/ags/files/widget/launcher/launcher.scss
Normal file
143
roles/ags/files/widget/launcher/launcher.scss
Normal file
@@ -0,0 +1,143 @@
|
||||
@use "sass:math";
|
||||
@use "sass:color";
|
||||
|
||||
window#launcher .launcher {
|
||||
@include floating_widget;
|
||||
|
||||
.quicklaunch {
|
||||
@include spacing;
|
||||
|
||||
button {
|
||||
@include button($flat: true);
|
||||
padding: $padding;
|
||||
}
|
||||
}
|
||||
|
||||
entry {
|
||||
@include button;
|
||||
padding: $padding;
|
||||
margin: $spacing;
|
||||
|
||||
selection {
|
||||
color: color.mix($fg, $bg, 50%);
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
label,
|
||||
image {
|
||||
color: $fg;
|
||||
}
|
||||
}
|
||||
|
||||
image.spinner {
|
||||
color: $primary-bg;
|
||||
margin-right: $spacing;
|
||||
}
|
||||
|
||||
separator {
|
||||
margin: 4pt 0;
|
||||
background-color: $popover-border-color;
|
||||
}
|
||||
|
||||
button.app-item {
|
||||
@include button($flat: true, $reactive: false);
|
||||
|
||||
>box {
|
||||
@include spacing(0.5);
|
||||
}
|
||||
|
||||
transition: $transition;
|
||||
padding: $padding;
|
||||
|
||||
label {
|
||||
transition: $transition;
|
||||
|
||||
&.title {
|
||||
color: $fg;
|
||||
}
|
||||
|
||||
&.description {
|
||||
color: transparentize($fg, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
image {
|
||||
transition: $transition;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
.title {
|
||||
color: $primary-bg;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: transparentize($primary-bg, .4);
|
||||
}
|
||||
|
||||
image {
|
||||
-gtk-icon-shadow: 2px 2px $primary-bg;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: transparentize($primary-bg, 0.5);
|
||||
border-radius: $radius;
|
||||
box-shadow: inset 0 0 0 $border-width $border-color;
|
||||
|
||||
.title {
|
||||
color: $fg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button.help,
|
||||
button.nix-item {
|
||||
@include button($flat: true, $reactive: false);
|
||||
padding: 0 ($padding * .5);
|
||||
|
||||
label {
|
||||
transition: $transition;
|
||||
color: $fg;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: transparentize($fg, .3)
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
label {
|
||||
text-shadow: $text-shadow;
|
||||
}
|
||||
|
||||
.name,
|
||||
.version {
|
||||
color: $primary-bg;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: transparentize($primary-bg, .3)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button.sh-item {
|
||||
@include button($flat: true, $reactive: false);
|
||||
padding: 0 ($padding * .5);
|
||||
|
||||
transition: $transition;
|
||||
color: $fg;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: $primary-bg;
|
||||
text-shadow: $text-shadow;
|
||||
}
|
||||
}
|
||||
}
|
||||
138
roles/ags/files/widget/notifications/Notification.ts
Normal file
138
roles/ags/files/widget/notifications/Notification.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { type Notification } from "types/service/notifications"
|
||||
import GLib from "gi://GLib"
|
||||
import icons from "lib/icons"
|
||||
|
||||
const time = (time: number, format = "%H:%M") => GLib.DateTime
|
||||
.new_from_unix_local(time)
|
||||
.format(format)
|
||||
|
||||
const NotificationIcon = ({ app_entry, app_icon, image }: Notification) => {
|
||||
if (image) {
|
||||
return Widget.Box({
|
||||
vpack: "start",
|
||||
hexpand: false,
|
||||
class_name: "icon img",
|
||||
css: `
|
||||
background-image: url("${image}");
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
min-width: 78px;
|
||||
min-height: 78px;
|
||||
`,
|
||||
})
|
||||
}
|
||||
|
||||
let icon = icons.fallback.notification
|
||||
if (Utils.lookUpIcon(app_icon))
|
||||
icon = app_icon
|
||||
|
||||
if (Utils.lookUpIcon(app_entry || ""))
|
||||
icon = app_entry || ""
|
||||
|
||||
return Widget.Box({
|
||||
vpack: "start",
|
||||
hexpand: false,
|
||||
class_name: "icon",
|
||||
css: `
|
||||
min-width: 78px;
|
||||
min-height: 78px;
|
||||
`,
|
||||
child: Widget.Icon({
|
||||
icon,
|
||||
size: 58,
|
||||
hpack: "center", hexpand: true,
|
||||
vpack: "center", vexpand: true,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
export default (notification: Notification) => {
|
||||
const content = Widget.Box({
|
||||
class_name: "content",
|
||||
children: [
|
||||
NotificationIcon(notification),
|
||||
Widget.Box({
|
||||
hexpand: true,
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
children: [
|
||||
Widget.Label({
|
||||
class_name: "title",
|
||||
xalign: 0,
|
||||
justification: "left",
|
||||
hexpand: true,
|
||||
max_width_chars: 24,
|
||||
truncate: "end",
|
||||
wrap: true,
|
||||
label: notification.summary.trim(),
|
||||
use_markup: true,
|
||||
}),
|
||||
Widget.Label({
|
||||
class_name: "time",
|
||||
vpack: "start",
|
||||
label: time(notification.time),
|
||||
}),
|
||||
Widget.Button({
|
||||
class_name: "close-button",
|
||||
vpack: "start",
|
||||
child: Widget.Icon("window-close-symbolic"),
|
||||
on_clicked: notification.close,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Widget.Label({
|
||||
class_name: "description",
|
||||
hexpand: true,
|
||||
use_markup: true,
|
||||
xalign: 0,
|
||||
justification: "left",
|
||||
label: notification.body.trim(),
|
||||
max_width_chars: 24,
|
||||
wrap: true,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
const actionsbox = notification.actions.length > 0 ? Widget.Revealer({
|
||||
transition: "slide_down",
|
||||
child: Widget.EventBox({
|
||||
child: Widget.Box({
|
||||
class_name: "actions horizontal",
|
||||
children: notification.actions.map(action => Widget.Button({
|
||||
class_name: "action-button",
|
||||
on_clicked: () => notification.invoke(action.id),
|
||||
hexpand: true,
|
||||
child: Widget.Label(action.label),
|
||||
})),
|
||||
}),
|
||||
}),
|
||||
}) : null
|
||||
|
||||
const eventbox = Widget.EventBox({
|
||||
vexpand: false,
|
||||
on_primary_click: notification.dismiss,
|
||||
on_hover() {
|
||||
if (actionsbox)
|
||||
actionsbox.reveal_child = true
|
||||
},
|
||||
on_hover_lost() {
|
||||
if (actionsbox)
|
||||
actionsbox.reveal_child = true
|
||||
|
||||
notification.dismiss()
|
||||
},
|
||||
child: Widget.Box({
|
||||
vertical: true,
|
||||
children: actionsbox ? [content, actionsbox] : [content],
|
||||
}),
|
||||
})
|
||||
|
||||
return Widget.Box({
|
||||
class_name: `notification ${notification.urgency}`,
|
||||
child: eventbox,
|
||||
})
|
||||
}
|
||||
90
roles/ags/files/widget/notifications/NotificationPopups.ts
Normal file
90
roles/ags/files/widget/notifications/NotificationPopups.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import Notification from "./Notification"
|
||||
import options from "options"
|
||||
|
||||
const notifications = await Service.import("notifications")
|
||||
const { transition } = options
|
||||
const { position } = options.notifications
|
||||
const { timeout, idle } = Utils
|
||||
|
||||
function Animated(id: number) {
|
||||
const n = notifications.getNotification(id)!
|
||||
const widget = Notification(n)
|
||||
|
||||
const inner = Widget.Revealer({
|
||||
transition: "slide_left",
|
||||
transition_duration: transition.value,
|
||||
child: widget,
|
||||
})
|
||||
|
||||
const outer = Widget.Revealer({
|
||||
transition: "slide_down",
|
||||
transition_duration: transition.value,
|
||||
child: inner,
|
||||
})
|
||||
|
||||
const box = Widget.Box({
|
||||
hpack: "end",
|
||||
child: outer,
|
||||
})
|
||||
|
||||
idle(() => {
|
||||
outer.reveal_child = true
|
||||
timeout(transition.value, () => {
|
||||
inner.reveal_child = true
|
||||
})
|
||||
})
|
||||
|
||||
return Object.assign(box, {
|
||||
dismiss() {
|
||||
inner.reveal_child = false
|
||||
timeout(transition.value, () => {
|
||||
outer.reveal_child = false
|
||||
timeout(transition.value, () => {
|
||||
box.destroy()
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function PopupList() {
|
||||
const map: Map<number, ReturnType<typeof Animated>> = new Map
|
||||
const box = Widget.Box({
|
||||
hpack: "end",
|
||||
vertical: true,
|
||||
css: options.notifications.width.bind().as(w => `min-width: ${w}px;`),
|
||||
})
|
||||
|
||||
function remove(_: unknown, id: number) {
|
||||
map.get(id)?.dismiss()
|
||||
map.delete(id)
|
||||
}
|
||||
|
||||
return box
|
||||
.hook(notifications, (_, id: number) => {
|
||||
if (id !== undefined) {
|
||||
if (map.has(id))
|
||||
remove(null, id)
|
||||
|
||||
if (notifications.dnd)
|
||||
return
|
||||
|
||||
const w = Animated(id)
|
||||
map.set(id, w)
|
||||
box.children = [w, ...box.children]
|
||||
}
|
||||
}, "notified")
|
||||
.hook(notifications, remove, "dismissed")
|
||||
.hook(notifications, remove, "closed")
|
||||
}
|
||||
|
||||
export default (monitor: number) => Widget.Window({
|
||||
monitor,
|
||||
name: `notifications${monitor}`,
|
||||
anchor: position.bind(),
|
||||
class_name: "notifications",
|
||||
child: Widget.Box({
|
||||
css: "padding: 2px;",
|
||||
child: PopupList(),
|
||||
}),
|
||||
})
|
||||
79
roles/ags/files/widget/notifications/notifications.scss
Normal file
79
roles/ags/files/widget/notifications/notifications.scss
Normal file
@@ -0,0 +1,79 @@
|
||||
@mixin notification() {
|
||||
&.critical {
|
||||
box-shadow: inset 0 0 .5em 0 $error-bg;
|
||||
}
|
||||
|
||||
&:hover button.close-button {
|
||||
@include button-hover;
|
||||
background-color: transparentize($error-bg, .5);
|
||||
}
|
||||
|
||||
.content {
|
||||
.title {
|
||||
margin-right: $spacing;
|
||||
color: $fg;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.time {
|
||||
color: transparentize($fg, .2);
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: .9em;
|
||||
color: transparentize($fg, .2);
|
||||
}
|
||||
|
||||
.icon {
|
||||
border-radius: $radius*0.8;
|
||||
margin-right: $spacing;
|
||||
|
||||
&.img {
|
||||
border: $border;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
box.actions {
|
||||
@include spacing(0.5);
|
||||
margin-top: $spacing;
|
||||
|
||||
button {
|
||||
@include button;
|
||||
border-radius: $radius*0.8;
|
||||
font-size: 1.2em;
|
||||
padding: $padding * 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
button.close-button {
|
||||
@include button($flat: true);
|
||||
margin-left: $spacing / 2;
|
||||
border-radius: $radius*0.8;
|
||||
min-width: 1.2em;
|
||||
min-height: 1.2em;
|
||||
|
||||
&:hover {
|
||||
background-color: transparentize($error-bg, .2);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-image: none;
|
||||
background-color: $error-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.notifications {
|
||||
@include unset;
|
||||
|
||||
.notification {
|
||||
@include notification;
|
||||
@include floating-widget;
|
||||
border-radius: $radius;
|
||||
|
||||
.description {
|
||||
min-width: 350px;
|
||||
}
|
||||
}
|
||||
}
|
||||
111
roles/ags/files/widget/osd/OSD.ts
Normal file
111
roles/ags/files/widget/osd/OSD.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { icon } from "lib/utils"
|
||||
import icons from "lib/icons"
|
||||
import Progress from "./Progress"
|
||||
import brightness from "service/brightness"
|
||||
import options from "options"
|
||||
|
||||
const audio = await Service.import("audio")
|
||||
const { progress, microphone } = options.osd
|
||||
|
||||
const DELAY = 2500
|
||||
|
||||
function OnScreenProgress(vertical: boolean) {
|
||||
const indicator = Widget.Icon({
|
||||
size: 42,
|
||||
vpack: "start",
|
||||
})
|
||||
const progress = Progress({
|
||||
vertical,
|
||||
width: vertical ? 42 : 300,
|
||||
height: vertical ? 300 : 42,
|
||||
child: indicator,
|
||||
})
|
||||
|
||||
const revealer = Widget.Revealer({
|
||||
transition: "slide_left",
|
||||
child: progress,
|
||||
})
|
||||
|
||||
let count = 0
|
||||
function show(value: number, icon: string) {
|
||||
revealer.reveal_child = true
|
||||
indicator.icon = icon
|
||||
progress.setValue(value)
|
||||
count++
|
||||
Utils.timeout(DELAY, () => {
|
||||
count--
|
||||
|
||||
if (count === 0)
|
||||
revealer.reveal_child = false
|
||||
})
|
||||
}
|
||||
|
||||
return revealer
|
||||
.hook(brightness, () => show(
|
||||
brightness.screen,
|
||||
icons.brightness.screen,
|
||||
), "notify::screen")
|
||||
.hook(brightness, () => show(
|
||||
brightness.kbd,
|
||||
icons.brightness.keyboard,
|
||||
), "notify::kbd")
|
||||
.hook(audio.speaker, () => show(
|
||||
audio.speaker.volume,
|
||||
icon(audio.speaker.icon_name || "", icons.audio.type.speaker),
|
||||
), "notify::volume")
|
||||
}
|
||||
|
||||
function MicrophoneMute() {
|
||||
const icon = Widget.Icon({
|
||||
class_name: "microphone",
|
||||
})
|
||||
|
||||
const revealer = Widget.Revealer({
|
||||
transition: "slide_up",
|
||||
child: icon,
|
||||
})
|
||||
|
||||
let count = 0
|
||||
let mute = audio.microphone.stream?.is_muted ?? false
|
||||
|
||||
return revealer.hook(audio.microphone, () => Utils.idle(() => {
|
||||
if (mute !== audio.microphone.stream?.is_muted) {
|
||||
mute = audio.microphone.stream!.is_muted
|
||||
icon.icon = icons.audio.mic[mute ? "muted" : "high"]
|
||||
revealer.reveal_child = true
|
||||
count++
|
||||
|
||||
Utils.timeout(DELAY, () => {
|
||||
count--
|
||||
if (count === 0)
|
||||
revealer.reveal_child = false
|
||||
})
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
export default (monitor: number) => Widget.Window({
|
||||
monitor,
|
||||
name: `indicator${monitor}`,
|
||||
class_name: "indicator",
|
||||
layer: "overlay",
|
||||
click_through: true,
|
||||
anchor: ["right", "left", "top", "bottom"],
|
||||
child: Widget.Box({
|
||||
css: "padding: 2px;",
|
||||
expand: true,
|
||||
child: Widget.Overlay(
|
||||
{ child: Widget.Box({ expand: true }) },
|
||||
Widget.Box({
|
||||
hpack: progress.pack.h.bind(),
|
||||
vpack: progress.pack.v.bind(),
|
||||
child: progress.vertical.bind().as(OnScreenProgress),
|
||||
}),
|
||||
Widget.Box({
|
||||
hpack: microphone.pack.h.bind(),
|
||||
vpack: microphone.pack.v.bind(),
|
||||
child: MicrophoneMute(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
})
|
||||
74
roles/ags/files/widget/osd/Progress.ts
Normal file
74
roles/ags/files/widget/osd/Progress.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import type Gtk from "gi://Gtk?version=3.0"
|
||||
import GLib from "gi://GLib?version=2.0"
|
||||
import { range } from "lib/utils"
|
||||
import options from "options"
|
||||
|
||||
type ProgressProps = {
|
||||
height?: number
|
||||
width?: number
|
||||
vertical?: boolean
|
||||
child: Gtk.Widget
|
||||
}
|
||||
|
||||
export default ({
|
||||
height = 18,
|
||||
width = 180,
|
||||
vertical = false,
|
||||
child,
|
||||
}: ProgressProps) => {
|
||||
const fill = Widget.Box({
|
||||
class_name: "fill",
|
||||
hexpand: vertical,
|
||||
vexpand: !vertical,
|
||||
hpack: vertical ? "fill" : "start",
|
||||
vpack: vertical ? "end" : "fill",
|
||||
child,
|
||||
})
|
||||
|
||||
const container = Widget.Box({
|
||||
class_name: "progress",
|
||||
child: fill,
|
||||
css: `
|
||||
min-width: ${width}px;
|
||||
min-height: ${height}px;
|
||||
`,
|
||||
})
|
||||
|
||||
let fill_size = 0
|
||||
let animations: number[] = []
|
||||
|
||||
return Object.assign(container, {
|
||||
setValue(value: number) {
|
||||
if (value < 0)
|
||||
return
|
||||
|
||||
if (animations.length > 0) {
|
||||
for (const id of animations)
|
||||
GLib.source_remove(id)
|
||||
|
||||
animations = []
|
||||
}
|
||||
|
||||
const axis = vertical ? "height" : "width"
|
||||
const axisv = vertical ? height : width
|
||||
const min = vertical ? width : height
|
||||
const preferred = (axisv - min) * value + min
|
||||
|
||||
if (!fill_size) {
|
||||
fill_size = preferred
|
||||
fill.css = `min-${axis}: ${preferred}px;`
|
||||
return
|
||||
}
|
||||
|
||||
const frames = options.transition.value / 10
|
||||
const goal = preferred - fill_size
|
||||
const step = goal / frames
|
||||
|
||||
animations = range(frames, 0).map(i => Utils.timeout(5 * i, () => {
|
||||
fill_size += step
|
||||
fill.css = `min-${axis}: ${fill_size}px`
|
||||
animations.shift()
|
||||
}))
|
||||
},
|
||||
})
|
||||
}
|
||||
26
roles/ags/files/widget/osd/osd.scss
Normal file
26
roles/ags/files/widget/osd/osd.scss
Normal file
@@ -0,0 +1,26 @@
|
||||
window.indicator {
|
||||
.progress {
|
||||
@include floating-widget;
|
||||
padding: $padding * .5;
|
||||
border-radius: if($radius >0, calc($radius + $padding*.5), 0);
|
||||
@debug $radius;
|
||||
|
||||
.fill {
|
||||
border-radius: $radius;
|
||||
background-color: $primary-bg;
|
||||
color: $primary-fg;
|
||||
|
||||
image {
|
||||
-gtk-icon-transform: scale(0.7);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.microphone {
|
||||
@include floating-widget;
|
||||
margin: $spacing * 2;
|
||||
padding: $popover-padding * 2;
|
||||
font-size: 58px;
|
||||
color: transparentize($fg, .1)
|
||||
}
|
||||
}
|
||||
41
roles/ags/files/widget/overview/Overview.ts
Normal file
41
roles/ags/files/widget/overview/Overview.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import PopupWindow from "widget/PopupWindow"
|
||||
import Workspace from "./Workspace"
|
||||
import options from "options"
|
||||
import { range } from "lib/utils"
|
||||
|
||||
const hyprland = await Service.import("hyprland")
|
||||
|
||||
const Overview = (ws: number) => Widget.Box({
|
||||
class_name: "overview horizontal",
|
||||
children: ws > 0
|
||||
? range(ws).map(Workspace)
|
||||
: hyprland.workspaces
|
||||
.map(({ id }) => Workspace(id))
|
||||
.sort((a, b) => a.attribute.id - b.attribute.id),
|
||||
|
||||
setup: w => {
|
||||
if (ws > 0)
|
||||
return
|
||||
|
||||
w.hook(hyprland, (w, id?: string) => {
|
||||
if (id === undefined)
|
||||
return
|
||||
|
||||
w.children = w.children
|
||||
.filter(ch => ch.attribute.id !== Number(id))
|
||||
}, "workspace-removed")
|
||||
w.hook(hyprland, (w, id?: string) => {
|
||||
if (id === undefined)
|
||||
return
|
||||
|
||||
w.children = [...w.children, Workspace(Number(id))]
|
||||
.sort((a, b) => a.attribute.id - b.attribute.id)
|
||||
}, "workspace-added")
|
||||
},
|
||||
})
|
||||
|
||||
export default () => PopupWindow({
|
||||
name: "overview",
|
||||
layout: "center",
|
||||
child: options.overview.workspaces.bind().as(Overview),
|
||||
})
|
||||
48
roles/ags/files/widget/overview/Window.ts
Normal file
48
roles/ags/files/widget/overview/Window.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { type Client } from "types/service/hyprland"
|
||||
import { createSurfaceFromWidget, icon } from "lib/utils"
|
||||
import Gdk from "gi://Gdk"
|
||||
import Gtk from "gi://Gtk?version=3.0"
|
||||
import options from "options"
|
||||
import icons from "lib/icons"
|
||||
|
||||
const monochrome = options.overview.monochromeIcon
|
||||
const TARGET = [Gtk.TargetEntry.new("text/plain", Gtk.TargetFlags.SAME_APP, 0)]
|
||||
const hyprland = await Service.import("hyprland")
|
||||
const apps = await Service.import("applications")
|
||||
const dispatch = (args: string) => hyprland.messageAsync(`dispatch ${args}`)
|
||||
|
||||
export default ({ address, size: [w, h], class: c, title }: Client) => Widget.Button({
|
||||
class_name: "client",
|
||||
attribute: { address },
|
||||
tooltip_text: `${title}`,
|
||||
child: Widget.Icon({
|
||||
css: options.overview.scale.bind().as(v => `
|
||||
min-width: ${(v / 100) * w}px;
|
||||
min-height: ${(v / 100) * h}px;
|
||||
`),
|
||||
icon: monochrome.bind().as(m => {
|
||||
const app = apps.list.find(app => app.match(c))
|
||||
if (!app)
|
||||
return icons.fallback.executable + (m ? "-symbolic" : "")
|
||||
|
||||
|
||||
return icon(
|
||||
app.icon_name + (m ? "-symbolic" : ""),
|
||||
icons.fallback.executable + (m ? "-symbolic" : ""),
|
||||
)
|
||||
}),
|
||||
}),
|
||||
on_secondary_click: () => dispatch(`closewindow address:${address}`),
|
||||
on_clicked: () => {
|
||||
dispatch(`focuswindow address:${address}`)
|
||||
App.closeWindow("overview")
|
||||
},
|
||||
setup: btn => btn
|
||||
.on("drag-data-get", (_w, _c, data) => data.set_text(address, address.length))
|
||||
.on("drag-begin", (_, context) => {
|
||||
Gtk.drag_set_icon_surface(context, createSurfaceFromWidget(btn))
|
||||
btn.toggleClassName("hidden", true)
|
||||
})
|
||||
.on("drag-end", () => btn.toggleClassName("hidden", false))
|
||||
.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, TARGET, Gdk.DragAction.COPY),
|
||||
})
|
||||
76
roles/ags/files/widget/overview/Workspace.ts
Normal file
76
roles/ags/files/widget/overview/Workspace.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import Window from "./Window"
|
||||
import Gdk from "gi://Gdk"
|
||||
import Gtk from "gi://Gtk?version=3.0"
|
||||
import options from "options"
|
||||
|
||||
const TARGET = [Gtk.TargetEntry.new("text/plain", Gtk.TargetFlags.SAME_APP, 0)]
|
||||
const scale = (size: number) => (options.overview.scale.value / 100) * size
|
||||
const hyprland = await Service.import("hyprland")
|
||||
|
||||
const dispatch = (args: string) => hyprland.messageAsync(`dispatch ${args}`)
|
||||
|
||||
const size = (id: number) => {
|
||||
const def = { h: 1080, w: 1920 }
|
||||
const ws = hyprland.getWorkspace(id)
|
||||
if (!ws)
|
||||
return def
|
||||
|
||||
const mon = hyprland.getMonitor(ws.monitorID)
|
||||
return mon ? { h: mon.height, w: mon.width } : def
|
||||
}
|
||||
|
||||
export default (id: number) => {
|
||||
const fixed = Widget.Fixed()
|
||||
|
||||
// TODO: early return if position is unchaged
|
||||
async function update() {
|
||||
const json = await hyprland.messageAsync("j/clients").catch(() => null)
|
||||
if (!json)
|
||||
return
|
||||
|
||||
fixed.get_children().forEach(ch => ch.destroy())
|
||||
const clients = JSON.parse(json) as typeof hyprland.clients
|
||||
clients
|
||||
.filter(({ workspace }) => workspace.id === id)
|
||||
.forEach(c => {
|
||||
const x = c.at[0] - (hyprland.getMonitor(c.monitor)?.x || 0)
|
||||
const y = c.at[1] - (hyprland.getMonitor(c.monitor)?.y || 0)
|
||||
c.mapped && fixed.put(Window(c), scale(x), scale(y))
|
||||
})
|
||||
fixed.show_all()
|
||||
}
|
||||
|
||||
return Widget.Box({
|
||||
attribute: { id },
|
||||
tooltipText: `${id}`,
|
||||
class_name: "workspace",
|
||||
vpack: "center",
|
||||
css: options.overview.scale.bind().as(v => `
|
||||
min-width: ${(v / 100) * size(id).w}px;
|
||||
min-height: ${(v / 100) * size(id).h}px;
|
||||
`),
|
||||
setup(box) {
|
||||
box.hook(options.overview.scale, update)
|
||||
box.hook(hyprland, update, "notify::clients")
|
||||
box.hook(hyprland.active.client, update)
|
||||
box.hook(hyprland.active.workspace, () => {
|
||||
box.toggleClassName("active", hyprland.active.workspace.id === id)
|
||||
})
|
||||
},
|
||||
child: Widget.EventBox({
|
||||
expand: true,
|
||||
on_primary_click: () => {
|
||||
App.closeWindow("overview")
|
||||
dispatch(`workspace ${id}`)
|
||||
},
|
||||
setup: eventbox => {
|
||||
eventbox.drag_dest_set(Gtk.DestDefaults.ALL, TARGET, Gdk.DragAction.COPY)
|
||||
eventbox.connect("drag-data-received", (_w, _c, _x, _y, data) => {
|
||||
const address = new TextDecoder().decode(data.get_data())
|
||||
dispatch(`movetoworkspacesilent ${id},address:${address}`)
|
||||
})
|
||||
},
|
||||
child: fixed,
|
||||
}),
|
||||
})
|
||||
}
|
||||
34
roles/ags/files/widget/overview/overview.scss
Normal file
34
roles/ags/files/widget/overview/overview.scss
Normal file
@@ -0,0 +1,34 @@
|
||||
window#overview .overview {
|
||||
@include floating-widget;
|
||||
@include spacing;
|
||||
|
||||
.workspace {
|
||||
&.active>widget {
|
||||
border-color: $primary-bg
|
||||
}
|
||||
|
||||
>widget {
|
||||
@include widget;
|
||||
border-radius: if($radius ==0, 0, $radius + $padding);
|
||||
|
||||
&:hover {
|
||||
background-color: $hover-bg;
|
||||
}
|
||||
|
||||
&:drop(active) {
|
||||
border-color: $primary-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.client {
|
||||
@include button;
|
||||
border-radius: $radius;
|
||||
margin: $padding;
|
||||
|
||||
&.hidden {
|
||||
@include hidden;
|
||||
transition: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
56
roles/ags/files/widget/powermenu/PowerMenu.ts
Normal file
56
roles/ags/files/widget/powermenu/PowerMenu.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import PopupWindow from "widget/PopupWindow"
|
||||
import powermenu, { type Action } from "service/powermenu"
|
||||
import icons from "lib/icons"
|
||||
import options from "options"
|
||||
import type Gtk from "gi://Gtk?version=3.0"
|
||||
|
||||
const { layout, labels } = options.powermenu
|
||||
|
||||
const SysButton = (action: Action, label: string) => Widget.Button({
|
||||
on_clicked: () => powermenu.action(action),
|
||||
child: Widget.Box({
|
||||
vertical: true,
|
||||
class_name: "system-button",
|
||||
children: [
|
||||
Widget.Icon(icons.powermenu[action]),
|
||||
Widget.Label({
|
||||
label,
|
||||
visible: labels.bind(),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
})
|
||||
|
||||
export default () => PopupWindow({
|
||||
name: "powermenu",
|
||||
transition: "crossfade",
|
||||
child: Widget.Box<Gtk.Widget>({
|
||||
class_name: "powermenu horizontal",
|
||||
setup: self => self.hook(layout, () => {
|
||||
self.toggleClassName("box", layout.value === "box")
|
||||
self.toggleClassName("line", layout.value === "line")
|
||||
}),
|
||||
children: layout.bind().as(layout => {
|
||||
switch (layout) {
|
||||
case "line": return [
|
||||
SysButton("shutdown", "Shutdown"),
|
||||
SysButton("logout", "Log Out"),
|
||||
SysButton("reboot", "Reboot"),
|
||||
SysButton("sleep", "Sleep"),
|
||||
]
|
||||
case "box": return [
|
||||
Widget.Box(
|
||||
{ vertical: true },
|
||||
SysButton("shutdown", "Shutdown"),
|
||||
SysButton("logout", "Log Out"),
|
||||
),
|
||||
Widget.Box(
|
||||
{ vertical: true },
|
||||
SysButton("reboot", "Reboot"),
|
||||
SysButton("sleep", "Sleep"),
|
||||
),
|
||||
]
|
||||
}
|
||||
}),
|
||||
}),
|
||||
})
|
||||
47
roles/ags/files/widget/powermenu/Verification.ts
Normal file
47
roles/ags/files/widget/powermenu/Verification.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import PopupWindow from "widget/PopupWindow"
|
||||
import powermenu from "service/powermenu"
|
||||
|
||||
export default () => PopupWindow({
|
||||
name: "verification",
|
||||
transition: "crossfade",
|
||||
child: Widget.Box({
|
||||
class_name: "verification",
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
class_name: "text-box",
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Label({
|
||||
class_name: "title",
|
||||
label: powermenu.bind("title"),
|
||||
}),
|
||||
Widget.Label({
|
||||
class_name: "desc",
|
||||
label: "Are you sure?",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Widget.Box({
|
||||
class_name: "buttons horizontal",
|
||||
vexpand: true,
|
||||
vpack: "end",
|
||||
homogeneous: true,
|
||||
children: [
|
||||
Widget.Button({
|
||||
child: Widget.Label("No"),
|
||||
on_clicked: () => App.toggleWindow("verification"),
|
||||
setup: self => self.hook(App, (_, name: string, visible: boolean) => {
|
||||
if (name === "verification" && visible)
|
||||
self.grab_focus()
|
||||
}),
|
||||
}),
|
||||
Widget.Button({
|
||||
child: Widget.Label("Yes"),
|
||||
on_clicked: () => Utils.exec(powermenu.cmd),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
})
|
||||
110
roles/ags/files/widget/powermenu/powermenu.scss
Normal file
110
roles/ags/files/widget/powermenu/powermenu.scss
Normal file
@@ -0,0 +1,110 @@
|
||||
window#powermenu,
|
||||
window#verification {
|
||||
// the fraction has to be more than hyprland ignorealpha
|
||||
background-color: rgba(0, 0, 0, .4);
|
||||
}
|
||||
|
||||
window#verification .verification {
|
||||
@include floating-widget;
|
||||
padding: $popover-padding * 1.5;
|
||||
min-width: 300px;
|
||||
min-height: 100px;
|
||||
|
||||
.text-box {
|
||||
margin-bottom: $spacing;
|
||||
|
||||
.title {
|
||||
font-size: 1.6em;
|
||||
}
|
||||
|
||||
.desc {
|
||||
color: transparentize($fg, 0.1);
|
||||
font-size: 1.1em;
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
@include spacing;
|
||||
margin-top: $padding;
|
||||
|
||||
button {
|
||||
@include button;
|
||||
font-size: 1.5em;
|
||||
padding: $padding;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window#powermenu .powermenu {
|
||||
@include floating-widget;
|
||||
|
||||
&.line {
|
||||
padding: $popover-padding * 1.5;
|
||||
|
||||
button {
|
||||
padding: $popover-padding;
|
||||
}
|
||||
|
||||
label {
|
||||
margin-bottom: $spacing * -.5;
|
||||
}
|
||||
}
|
||||
|
||||
&.box {
|
||||
padding: $popover-padding * 2;
|
||||
|
||||
button {
|
||||
padding: $popover-padding * 1.5;
|
||||
}
|
||||
|
||||
label {
|
||||
margin-bottom: $spacing * -1;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
@include unset;
|
||||
|
||||
image {
|
||||
@include button;
|
||||
border-radius: $radius + ($popover-padding * 1.4);
|
||||
min-width: 1.7em;
|
||||
min-height: 1.7em;
|
||||
font-size: 4em;
|
||||
}
|
||||
|
||||
label,
|
||||
image {
|
||||
color: transparentize($fg, 0.1);
|
||||
}
|
||||
|
||||
label {
|
||||
margin-top: $spacing * .3;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
image {
|
||||
@include button-hover;
|
||||
}
|
||||
|
||||
label {
|
||||
color: $fg;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus image {
|
||||
@include button-focus;
|
||||
}
|
||||
|
||||
&:active image {
|
||||
@include button-active;
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&:active {
|
||||
label {
|
||||
color: $primary-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
84
roles/ags/files/widget/quicksettings/QuickSettings.ts
Normal file
84
roles/ags/files/widget/quicksettings/QuickSettings.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import type Gtk from "gi://Gtk?version=3.0"
|
||||
import { ProfileSelector, ProfileToggle } from "./widgets/PowerProfile"
|
||||
import { Header } from "./widgets/Header"
|
||||
import { Volume, Microhone, SinkSelector, AppMixer } from "./widgets/Volume"
|
||||
import { Brightness } from "./widgets/Brightness"
|
||||
import { NetworkToggle, WifiSelection } from "./widgets/Network"
|
||||
import { BluetoothToggle, BluetoothDevices } from "./widgets/Bluetooth"
|
||||
import { DND } from "./widgets/DND"
|
||||
import { DarkModeToggle } from "./widgets/DarkMode"
|
||||
import { MicMute } from "./widgets/MicMute"
|
||||
import { Media } from "./widgets/Media"
|
||||
import PopupWindow from "widget/PopupWindow"
|
||||
import options from "options"
|
||||
|
||||
const { bar, quicksettings } = options
|
||||
const media = (await Service.import("mpris")).bind("players")
|
||||
const layout = Utils.derive([bar.position, quicksettings.position], (bar, qs) =>
|
||||
`${bar}-${qs}` as const,
|
||||
)
|
||||
|
||||
const Row = (
|
||||
toggles: Array<() => Gtk.Widget> = [],
|
||||
menus: Array<() => Gtk.Widget> = [],
|
||||
) => Widget.Box({
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
homogeneous: true,
|
||||
class_name: "row horizontal",
|
||||
children: toggles.map(w => w()),
|
||||
}),
|
||||
...menus.map(w => w()),
|
||||
],
|
||||
})
|
||||
|
||||
const Settings = () => Widget.Box({
|
||||
vertical: true,
|
||||
class_name: "quicksettings vertical",
|
||||
css: quicksettings.width.bind().as(w => `min-width: ${w}px;`),
|
||||
children: [
|
||||
Header(),
|
||||
Widget.Box({
|
||||
class_name: "sliders-box vertical",
|
||||
vertical: true,
|
||||
children: [
|
||||
Row(
|
||||
[Volume],
|
||||
[SinkSelector, AppMixer],
|
||||
),
|
||||
Microhone(),
|
||||
Brightness(),
|
||||
],
|
||||
}),
|
||||
Row(
|
||||
[NetworkToggle, BluetoothToggle],
|
||||
[WifiSelection, BluetoothDevices],
|
||||
),
|
||||
Row(
|
||||
[ProfileToggle, DarkModeToggle],
|
||||
[ProfileSelector],
|
||||
),
|
||||
Row([MicMute, DND]),
|
||||
Widget.Box({
|
||||
visible: media.as(l => l.length > 0),
|
||||
child: Media(),
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
const QuickSettings = () => PopupWindow({
|
||||
name: "quicksettings",
|
||||
exclusivity: "exclusive",
|
||||
transition: bar.position.bind().as(pos => pos === "top" ? "slide_down" : "slide_up"),
|
||||
layout: layout.value,
|
||||
child: Settings(),
|
||||
})
|
||||
|
||||
export function setupQuickSettings() {
|
||||
App.addWindow(QuickSettings())
|
||||
layout.connect("changed", () => {
|
||||
App.removeWindow("quicksettings")
|
||||
App.addWindow(QuickSettings())
|
||||
})
|
||||
}
|
||||
154
roles/ags/files/widget/quicksettings/ToggleButton.ts
Normal file
154
roles/ags/files/widget/quicksettings/ToggleButton.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import { type Props as IconProps } from "types/widgets/icon"
|
||||
import { type Props as LabelProps } from "types/widgets/label"
|
||||
import type GObject from "gi://GObject?version=2.0"
|
||||
import type Gtk from "gi://Gtk?version=3.0"
|
||||
import icons from "lib/icons"
|
||||
|
||||
export const opened = Variable("")
|
||||
App.connect("window-toggled", (_, name: string, visible: boolean) => {
|
||||
if (name === "quicksettings" && !visible)
|
||||
Utils.timeout(500, () => opened.value = "")
|
||||
})
|
||||
|
||||
export const Arrow = (name: string, activate?: false | (() => void)) => {
|
||||
let deg = 0
|
||||
let iconOpened = false
|
||||
const icon = Widget.Icon(icons.ui.arrow.right).hook(opened, () => {
|
||||
if (opened.value === name && !iconOpened || opened.value !== name && iconOpened) {
|
||||
const step = opened.value === name ? 10 : -10
|
||||
iconOpened = !iconOpened
|
||||
for (let i = 0; i < 9; ++i) {
|
||||
Utils.timeout(15 * i, () => {
|
||||
deg += step
|
||||
icon.setCss(`-gtk-icon-transform: rotate(${deg}deg);`)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
return Widget.Button({
|
||||
child: icon,
|
||||
class_name: "arrow",
|
||||
on_clicked: () => {
|
||||
opened.value = opened.value === name ? "" : name
|
||||
if (typeof activate === "function")
|
||||
activate()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
type ArrowToggleButtonProps = {
|
||||
name: string
|
||||
icon: IconProps["icon"]
|
||||
label: LabelProps["label"]
|
||||
activate: () => void
|
||||
deactivate: () => void
|
||||
activateOnArrow?: boolean
|
||||
connection: [GObject.Object, () => boolean]
|
||||
}
|
||||
export const ArrowToggleButton = ({
|
||||
name,
|
||||
icon,
|
||||
label,
|
||||
activate,
|
||||
deactivate,
|
||||
activateOnArrow = true,
|
||||
connection: [service, condition],
|
||||
}: ArrowToggleButtonProps) => Widget.Box({
|
||||
class_name: "toggle-button",
|
||||
setup: self => self.hook(service, () => {
|
||||
self.toggleClassName("active", condition())
|
||||
}),
|
||||
children: [
|
||||
Widget.Button({
|
||||
child: Widget.Box({
|
||||
hexpand: true,
|
||||
children: [
|
||||
Widget.Icon({
|
||||
class_name: "icon",
|
||||
icon,
|
||||
}),
|
||||
Widget.Label({
|
||||
class_name: "label",
|
||||
max_width_chars: 10,
|
||||
truncate: "end",
|
||||
label,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
on_clicked: () => {
|
||||
if (condition()) {
|
||||
deactivate()
|
||||
if (opened.value === name)
|
||||
opened.value = ""
|
||||
} else {
|
||||
activate()
|
||||
}
|
||||
},
|
||||
}),
|
||||
Arrow(name, activateOnArrow && activate),
|
||||
],
|
||||
})
|
||||
|
||||
type MenuProps = {
|
||||
name: string
|
||||
icon: IconProps["icon"]
|
||||
title: LabelProps["label"]
|
||||
content: Gtk.Widget[]
|
||||
}
|
||||
export const Menu = ({ name, icon, title, content }: MenuProps) => Widget.Revealer({
|
||||
transition: "slide_down",
|
||||
reveal_child: opened.bind().as(v => v === name),
|
||||
child: Widget.Box({
|
||||
class_names: ["menu", name],
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
class_name: "title-box",
|
||||
children: [
|
||||
Widget.Icon({
|
||||
class_name: "icon",
|
||||
icon,
|
||||
}),
|
||||
Widget.Label({
|
||||
class_name: "title",
|
||||
truncate: "end",
|
||||
label: title,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Widget.Separator(),
|
||||
Widget.Box({
|
||||
vertical: true,
|
||||
class_name: "content vertical",
|
||||
children: content,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
})
|
||||
|
||||
type SimpleToggleButtonProps = {
|
||||
icon: IconProps["icon"]
|
||||
label: LabelProps["label"]
|
||||
toggle: () => void
|
||||
connection: [GObject.Object, () => boolean]
|
||||
}
|
||||
export const SimpleToggleButton = ({
|
||||
icon,
|
||||
label,
|
||||
toggle,
|
||||
connection: [service, condition],
|
||||
}: SimpleToggleButtonProps) => Widget.Button({
|
||||
on_clicked: toggle,
|
||||
class_name: "simple-toggle",
|
||||
setup: self => self.hook(service, () => {
|
||||
self.toggleClassName("active", condition())
|
||||
}),
|
||||
child: Widget.Box([
|
||||
Widget.Icon({ icon }),
|
||||
Widget.Label({
|
||||
max_width_chars: 10,
|
||||
truncate: "end",
|
||||
label,
|
||||
}),
|
||||
]),
|
||||
})
|
||||
177
roles/ags/files/widget/quicksettings/quicksettings.scss
Normal file
177
roles/ags/files/widget/quicksettings/quicksettings.scss
Normal file
@@ -0,0 +1,177 @@
|
||||
window#quicksettings .quicksettings {
|
||||
@include floating-widget;
|
||||
@include spacing;
|
||||
|
||||
padding: $popover-padding * 1.4;
|
||||
|
||||
.avatar {
|
||||
@include widget;
|
||||
border-radius: $radius * 3;
|
||||
}
|
||||
|
||||
.header {
|
||||
@include spacing(.5);
|
||||
color: transparentize($fg, .15);
|
||||
|
||||
button {
|
||||
@include button;
|
||||
padding: $padding;
|
||||
|
||||
image {
|
||||
font-size: 1.4em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sliders-box {
|
||||
@include widget;
|
||||
padding: $padding;
|
||||
|
||||
button {
|
||||
@include button($flat: true);
|
||||
padding: $padding * .5;
|
||||
}
|
||||
|
||||
.volume button.arrow:last-child {
|
||||
margin-left: $spacing * .4;
|
||||
}
|
||||
|
||||
.volume,
|
||||
.brightness {
|
||||
padding: $padding * .5;
|
||||
}
|
||||
|
||||
scale {
|
||||
@include slider;
|
||||
margin: 0 ($spacing * .5);
|
||||
|
||||
&.muted highlight {
|
||||
background-image: none;
|
||||
background-color: transparentize($fg, $amount: .2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.row {
|
||||
@include spacing;
|
||||
}
|
||||
|
||||
.menu {
|
||||
@include unset;
|
||||
@include widget;
|
||||
padding: $padding;
|
||||
margin-top: $spacing;
|
||||
|
||||
.icon {
|
||||
margin: 0 ($spacing * .5);
|
||||
margin-left: $spacing * .2;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
separator {
|
||||
margin: ($radius * .5);
|
||||
background-color: $border-color;
|
||||
}
|
||||
|
||||
button {
|
||||
@include button($flat: true);
|
||||
padding: ($padding * .5);
|
||||
|
||||
image:first-child {
|
||||
margin-right: $spacing * .5;
|
||||
}
|
||||
}
|
||||
|
||||
.bluetooth-devices {
|
||||
@include spacing(.5);
|
||||
}
|
||||
|
||||
switch {
|
||||
@include switch;
|
||||
}
|
||||
}
|
||||
|
||||
.sliders-box .menu {
|
||||
margin: ($spacing * .5) 0;
|
||||
|
||||
&.app-mixer {
|
||||
.mixer-item {
|
||||
padding: $padding * .5;
|
||||
padding-left: 0;
|
||||
padding-right: $padding * 2;
|
||||
|
||||
scale {
|
||||
@include slider($width: .5em);
|
||||
}
|
||||
|
||||
image {
|
||||
font-size: 1.2em;
|
||||
margin: 0 $padding;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-button {
|
||||
@include button;
|
||||
font-weight: bold;
|
||||
|
||||
image {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
label {
|
||||
margin-left: $spacing * .3;
|
||||
}
|
||||
|
||||
button {
|
||||
@include button($flat: true);
|
||||
|
||||
&:first-child {
|
||||
padding: $padding * 1.2;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding: $padding * .5;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: $primary-bg;
|
||||
|
||||
label,
|
||||
image {
|
||||
color: $primary-fg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.simple-toggle {
|
||||
@include button;
|
||||
font-weight: bold;
|
||||
padding: $padding * 1.2;
|
||||
|
||||
label {
|
||||
margin-left: $spacing * .3;
|
||||
}
|
||||
|
||||
image {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
}
|
||||
|
||||
.media {
|
||||
@include spacing;
|
||||
|
||||
.player {
|
||||
@include media;
|
||||
}
|
||||
}
|
||||
}
|
||||
61
roles/ags/files/widget/quicksettings/widgets/Bluetooth.ts
Normal file
61
roles/ags/files/widget/quicksettings/widgets/Bluetooth.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { type BluetoothDevice } from "types/service/bluetooth"
|
||||
import { Menu, ArrowToggleButton } from "../ToggleButton"
|
||||
import icons from "lib/icons"
|
||||
|
||||
const bluetooth = await Service.import("bluetooth")
|
||||
|
||||
export const BluetoothToggle = () => ArrowToggleButton({
|
||||
name: "bluetooth",
|
||||
icon: bluetooth.bind("enabled").as(p => icons.bluetooth[p ? "enabled" : "disabled"]),
|
||||
label: Utils.watch("Disabled", bluetooth, () => {
|
||||
if (!bluetooth.enabled)
|
||||
return "Disabled"
|
||||
|
||||
if (bluetooth.connected_devices.length === 1)
|
||||
return bluetooth.connected_devices[0].alias
|
||||
|
||||
return `${bluetooth.connected_devices.length} Connected`
|
||||
}),
|
||||
connection: [bluetooth, () => bluetooth.enabled],
|
||||
deactivate: () => bluetooth.enabled = false,
|
||||
activate: () => bluetooth.enabled = true,
|
||||
})
|
||||
|
||||
const DeviceItem = (device: BluetoothDevice) => Widget.Box({
|
||||
children: [
|
||||
Widget.Icon(device.icon_name + "-symbolic"),
|
||||
Widget.Label(device.name),
|
||||
Widget.Label({
|
||||
label: `${device.battery_percentage}%`,
|
||||
visible: device.bind("battery_percentage").as(p => p > 0),
|
||||
}),
|
||||
Widget.Box({ hexpand: true }),
|
||||
Widget.Spinner({
|
||||
active: device.bind("connecting"),
|
||||
visible: device.bind("connecting"),
|
||||
}),
|
||||
Widget.Switch({
|
||||
active: device.connected,
|
||||
visible: device.bind("connecting").as(p => !p),
|
||||
setup: self => self.on("notify::active", () => {
|
||||
device.setConnection(self.active)
|
||||
}),
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
export const BluetoothDevices = () => Menu({
|
||||
name: "bluetooth",
|
||||
icon: icons.bluetooth.disabled,
|
||||
title: "Bluetooth",
|
||||
content: [
|
||||
Widget.Box({
|
||||
class_name: "bluetooth-devices",
|
||||
hexpand: true,
|
||||
vertical: true,
|
||||
children: bluetooth.bind("devices").as(ds => ds
|
||||
.filter(d => d.name)
|
||||
.map(DeviceItem)),
|
||||
}),
|
||||
],
|
||||
})
|
||||
23
roles/ags/files/widget/quicksettings/widgets/Brightness.ts
Normal file
23
roles/ags/files/widget/quicksettings/widgets/Brightness.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import icons from "lib/icons"
|
||||
import brightness from "service/brightness"
|
||||
|
||||
const BrightnessSlider = () => Widget.Slider({
|
||||
draw_value: false,
|
||||
hexpand: true,
|
||||
value: brightness.bind("screen"),
|
||||
on_change: ({ value }) => brightness.screen = value,
|
||||
})
|
||||
|
||||
export const Brightness = () => Widget.Box({
|
||||
class_name: "brightness",
|
||||
children: [
|
||||
Widget.Button({
|
||||
vpack: "center",
|
||||
child: Widget.Icon(icons.brightness.indicator),
|
||||
on_clicked: () => brightness.screen = 0,
|
||||
tooltip_text: brightness.bind("screen").as(v =>
|
||||
`Screen Brightness: ${Math.floor(v * 100)}%`),
|
||||
}),
|
||||
BrightnessSlider(),
|
||||
],
|
||||
})
|
||||
12
roles/ags/files/widget/quicksettings/widgets/DND.ts
Normal file
12
roles/ags/files/widget/quicksettings/widgets/DND.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { SimpleToggleButton } from "../ToggleButton"
|
||||
import icons from "lib/icons"
|
||||
|
||||
const n = await Service.import("notifications")
|
||||
const dnd = n.bind("dnd")
|
||||
|
||||
export const DND = () => SimpleToggleButton({
|
||||
icon: dnd.as(dnd => icons.notifications[dnd ? "silent" : "noisy"]),
|
||||
label: dnd.as(dnd => dnd ? "Silent" : "Noisy"),
|
||||
toggle: () => n.dnd = !n.dnd,
|
||||
connection: [n, () => n.dnd],
|
||||
})
|
||||
12
roles/ags/files/widget/quicksettings/widgets/DarkMode.ts
Normal file
12
roles/ags/files/widget/quicksettings/widgets/DarkMode.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { SimpleToggleButton } from "../ToggleButton"
|
||||
import icons from "lib/icons"
|
||||
import options from "options"
|
||||
|
||||
const { scheme } = options.theme
|
||||
|
||||
export const DarkModeToggle = () => SimpleToggleButton({
|
||||
icon: scheme.bind().as(s => icons.color[s]),
|
||||
label: scheme.bind().as(s => s === "dark" ? "Dark" : "Light"),
|
||||
toggle: () => scheme.value = scheme.value === "dark" ? "light" : "dark",
|
||||
connection: [scheme, () => scheme.value === "dark"],
|
||||
})
|
||||
63
roles/ags/files/widget/quicksettings/widgets/Header.ts
Normal file
63
roles/ags/files/widget/quicksettings/widgets/Header.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import icons from "lib/icons"
|
||||
import { uptime } from "lib/variables"
|
||||
import options from "options"
|
||||
import powermenu, { Action } from "service/powermenu"
|
||||
|
||||
const battery = await Service.import("battery")
|
||||
const { image, size } = options.quicksettings.avatar
|
||||
|
||||
function up(up: number) {
|
||||
const h = Math.floor(up / 60)
|
||||
const m = Math.floor(up % 60)
|
||||
return `${h}h ${m < 10 ? "0" + m : m}m`
|
||||
}
|
||||
|
||||
const Avatar = () => Widget.Box({
|
||||
class_name: "avatar",
|
||||
css: Utils.merge([image.bind(), size.bind()], (img, size) => `
|
||||
min-width: ${size}px;
|
||||
min-height: ${size}px;
|
||||
background-image: url('${img}');
|
||||
background-size: cover;
|
||||
`),
|
||||
})
|
||||
|
||||
const SysButton = (action: Action) => Widget.Button({
|
||||
vpack: "center",
|
||||
child: Widget.Icon(icons.powermenu[action]),
|
||||
on_clicked: () => powermenu.action(action),
|
||||
})
|
||||
|
||||
export const Header = () => Widget.Box(
|
||||
{ class_name: "header horizontal" },
|
||||
Avatar(),
|
||||
Widget.Box({
|
||||
vertical: true,
|
||||
vpack: "center",
|
||||
children: [
|
||||
Widget.Box({
|
||||
visible: battery.bind("available"),
|
||||
children: [
|
||||
Widget.Icon({ icon: battery.bind("icon_name") }),
|
||||
Widget.Label({ label: battery.bind("percent").as(p => `${p}%`) }),
|
||||
],
|
||||
}),
|
||||
Widget.Box([
|
||||
Widget.Icon({ icon: icons.ui.time }),
|
||||
Widget.Label({ label: uptime.bind().as(up) }),
|
||||
]),
|
||||
],
|
||||
}),
|
||||
Widget.Box({ hexpand: true }),
|
||||
Widget.Button({
|
||||
vpack: "center",
|
||||
child: Widget.Icon(icons.ui.settings),
|
||||
on_clicked: () => {
|
||||
App.closeWindow("quicksettings")
|
||||
App.closeWindow("settings-dialog")
|
||||
App.openWindow("settings-dialog")
|
||||
},
|
||||
}),
|
||||
SysButton("logout"),
|
||||
SysButton("shutdown"),
|
||||
)
|
||||
153
roles/ags/files/widget/quicksettings/widgets/Media.ts
Normal file
153
roles/ags/files/widget/quicksettings/widgets/Media.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import { type MprisPlayer } from "types/service/mpris"
|
||||
import icons from "lib/icons"
|
||||
import options from "options"
|
||||
import { icon } from "lib/utils"
|
||||
|
||||
const mpris = await Service.import("mpris")
|
||||
const players = mpris.bind("players")
|
||||
const { media } = options.quicksettings
|
||||
|
||||
function lengthStr(length: number) {
|
||||
const min = Math.floor(length / 60)
|
||||
const sec = Math.floor(length % 60)
|
||||
const sec0 = sec < 10 ? "0" : ""
|
||||
return `${min}:${sec0}${sec}`
|
||||
}
|
||||
|
||||
const Player = (player: MprisPlayer) => {
|
||||
const cover = Widget.Box({
|
||||
class_name: "cover",
|
||||
vpack: "start",
|
||||
css: Utils.merge([
|
||||
player.bind("cover_path"),
|
||||
player.bind("track_cover_url"),
|
||||
media.coverSize.bind(),
|
||||
], (path, url, size) => `
|
||||
min-width: ${size}px;
|
||||
min-height: ${size}px;
|
||||
background-image: url('${path || url}');
|
||||
`),
|
||||
})
|
||||
|
||||
const title = Widget.Label({
|
||||
class_name: "title",
|
||||
max_width_chars: 20,
|
||||
truncate: "end",
|
||||
hpack: "start",
|
||||
label: player.bind("track_title"),
|
||||
})
|
||||
|
||||
const artist = Widget.Label({
|
||||
class_name: "artist",
|
||||
max_width_chars: 20,
|
||||
truncate: "end",
|
||||
hpack: "start",
|
||||
label: player.bind("track_artists").as(a => a.join(", ")),
|
||||
})
|
||||
|
||||
const positionSlider = Widget.Slider({
|
||||
class_name: "position",
|
||||
draw_value: false,
|
||||
on_change: ({ value }) => player.position = value * player.length,
|
||||
setup: self => {
|
||||
const update = () => {
|
||||
const { length, position } = player
|
||||
self.visible = length > 0
|
||||
self.value = length > 0 ? position / length : 0
|
||||
}
|
||||
self.hook(player, update)
|
||||
self.hook(player, update, "position")
|
||||
self.poll(1000, update)
|
||||
},
|
||||
})
|
||||
|
||||
const positionLabel = Widget.Label({
|
||||
class_name: "position",
|
||||
hpack: "start",
|
||||
setup: self => {
|
||||
const update = (_: unknown, time?: number) => {
|
||||
self.label = lengthStr(time || player.position)
|
||||
self.visible = player.length > 0
|
||||
}
|
||||
self.hook(player, update, "position")
|
||||
self.poll(1000, update)
|
||||
},
|
||||
})
|
||||
|
||||
const lengthLabel = Widget.Label({
|
||||
class_name: "length",
|
||||
hpack: "end",
|
||||
visible: player.bind("length").as(l => l > 0),
|
||||
label: player.bind("length").as(lengthStr),
|
||||
})
|
||||
|
||||
const playericon = Widget.Icon({
|
||||
class_name: "icon",
|
||||
hexpand: true,
|
||||
hpack: "end",
|
||||
vpack: "start",
|
||||
tooltip_text: player.identity || "",
|
||||
icon: Utils.merge([player.bind("entry"), media.monochromeIcon.bind()], (e, s) => {
|
||||
const name = `${e}${s ? "-symbolic" : ""}`
|
||||
return icon(name, icons.fallback.audio)
|
||||
}),
|
||||
})
|
||||
|
||||
const playPause = Widget.Button({
|
||||
class_name: "play-pause",
|
||||
on_clicked: () => player.playPause(),
|
||||
visible: player.bind("can_play"),
|
||||
child: Widget.Icon({
|
||||
icon: player.bind("play_back_status").as(s => {
|
||||
switch (s) {
|
||||
case "Playing": return icons.mpris.playing
|
||||
case "Paused":
|
||||
case "Stopped": return icons.mpris.stopped
|
||||
}
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
const prev = Widget.Button({
|
||||
on_clicked: () => player.previous(),
|
||||
visible: player.bind("can_go_prev"),
|
||||
child: Widget.Icon(icons.mpris.prev),
|
||||
})
|
||||
|
||||
const next = Widget.Button({
|
||||
on_clicked: () => player.next(),
|
||||
visible: player.bind("can_go_next"),
|
||||
child: Widget.Icon(icons.mpris.next),
|
||||
})
|
||||
|
||||
return Widget.Box(
|
||||
{ class_name: "player", vexpand: false },
|
||||
cover,
|
||||
Widget.Box(
|
||||
{ vertical: true },
|
||||
Widget.Box([
|
||||
title,
|
||||
playericon,
|
||||
]),
|
||||
artist,
|
||||
Widget.Box({ vexpand: true }),
|
||||
positionSlider,
|
||||
Widget.CenterBox({
|
||||
class_name: "footer horizontal",
|
||||
start_widget: positionLabel,
|
||||
center_widget: Widget.Box([
|
||||
prev,
|
||||
playPause,
|
||||
next,
|
||||
]),
|
||||
end_widget: lengthLabel,
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
export const Media = () => Widget.Box({
|
||||
vertical: true,
|
||||
class_name: "media vertical",
|
||||
children: players.as(p => p.map(Player)),
|
||||
})
|
||||
18
roles/ags/files/widget/quicksettings/widgets/MicMute.ts
Normal file
18
roles/ags/files/widget/quicksettings/widgets/MicMute.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { SimpleToggleButton } from "../ToggleButton"
|
||||
import icons from "lib/icons"
|
||||
const { microphone } = await Service.import("audio")
|
||||
|
||||
const icon = () => microphone.is_muted || microphone.stream?.is_muted
|
||||
? icons.audio.mic.muted
|
||||
: icons.audio.mic.high
|
||||
|
||||
const label = () => microphone.is_muted || microphone.stream?.is_muted
|
||||
? "Muted"
|
||||
: "Unmuted"
|
||||
|
||||
export const MicMute = () => SimpleToggleButton({
|
||||
icon: Utils.watch(icon(), microphone, icon),
|
||||
label: Utils.watch(label(), microphone, label),
|
||||
toggle: () => microphone.is_muted = !microphone.is_muted,
|
||||
connection: [microphone, () => microphone?.is_muted || false],
|
||||
})
|
||||
61
roles/ags/files/widget/quicksettings/widgets/Network.ts
Normal file
61
roles/ags/files/widget/quicksettings/widgets/Network.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { Menu, ArrowToggleButton } from "../ToggleButton"
|
||||
import icons from "lib/icons.js"
|
||||
import { dependencies, sh } from "lib/utils"
|
||||
import options from "options"
|
||||
const { wifi } = await Service.import("network")
|
||||
|
||||
export const NetworkToggle = () => ArrowToggleButton({
|
||||
name: "network",
|
||||
icon: wifi.bind("icon_name"),
|
||||
label: wifi.bind("ssid").as(ssid => ssid || "Not Connected"),
|
||||
connection: [wifi, () => wifi.enabled],
|
||||
deactivate: () => wifi.enabled = false,
|
||||
activate: () => {
|
||||
wifi.enabled = true
|
||||
wifi.scan()
|
||||
},
|
||||
})
|
||||
|
||||
export const WifiSelection = () => Menu({
|
||||
name: "network",
|
||||
icon: wifi.bind("icon_name"),
|
||||
title: "Wifi Selection",
|
||||
content: [
|
||||
Widget.Box({
|
||||
vertical: true,
|
||||
setup: self => self.hook(wifi, () => self.children =
|
||||
wifi.access_points.map(ap => Widget.Button({
|
||||
on_clicked: () => {
|
||||
if (dependencies("nmcli"))
|
||||
Utils.execAsync(`nmcli device wifi connect ${ap.bssid}`)
|
||||
},
|
||||
child: Widget.Box({
|
||||
children: [
|
||||
Widget.Icon(ap.iconName),
|
||||
Widget.Label(ap.ssid || ""),
|
||||
Widget.Icon({
|
||||
icon: icons.ui.tick,
|
||||
hexpand: true,
|
||||
hpack: "end",
|
||||
setup: self => Utils.idle(() => {
|
||||
if (!self.is_destroyed)
|
||||
self.visible = ap.active
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
})),
|
||||
),
|
||||
}),
|
||||
Widget.Separator(),
|
||||
Widget.Button({
|
||||
on_clicked: () => sh(options.quicksettings.networkSettings.value),
|
||||
child: Widget.Box({
|
||||
children: [
|
||||
Widget.Icon(icons.ui.settings),
|
||||
Widget.Label("Network"),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
],
|
||||
})
|
||||
99
roles/ags/files/widget/quicksettings/widgets/PowerProfile.ts
Normal file
99
roles/ags/files/widget/quicksettings/widgets/PowerProfile.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { ArrowToggleButton, Menu } from "../ToggleButton"
|
||||
import icons from "lib/icons"
|
||||
|
||||
import asusctl from "service/asusctl"
|
||||
const asusprof = asusctl.bind("profile")
|
||||
|
||||
const AsusProfileToggle = () => ArrowToggleButton({
|
||||
name: "asusctl-profile",
|
||||
icon: asusprof.as(p => icons.asusctl.profile[p]),
|
||||
label: asusprof,
|
||||
connection: [asusctl, () => asusctl.profile !== "Balanced"],
|
||||
activate: () => asusctl.setProfile("Quiet"),
|
||||
deactivate: () => asusctl.setProfile("Balanced"),
|
||||
activateOnArrow: false,
|
||||
})
|
||||
|
||||
const AsusProfileSelector = () => Menu({
|
||||
name: "asusctl-profile",
|
||||
icon: asusprof.as(p => icons.asusctl.profile[p]),
|
||||
title: "Profile Selector",
|
||||
content: [
|
||||
Widget.Box({
|
||||
vertical: true,
|
||||
hexpand: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
vertical: true,
|
||||
children: asusctl.profiles.map(prof => Widget.Button({
|
||||
on_clicked: () => asusctl.setProfile(prof),
|
||||
child: Widget.Box({
|
||||
children: [
|
||||
Widget.Icon(icons.asusctl.profile[prof]),
|
||||
Widget.Label(prof),
|
||||
],
|
||||
}),
|
||||
})),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Widget.Separator(),
|
||||
Widget.Button({
|
||||
on_clicked: () => Utils.execAsync("rog-control-center"),
|
||||
child: Widget.Box({
|
||||
children: [
|
||||
Widget.Icon(icons.ui.settings),
|
||||
Widget.Label("Rog Control Center"),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
const pp = await Service.import("powerprofiles")
|
||||
const profile = pp.bind("active_profile")
|
||||
const profiles = pp.profiles.map(p => p.Profile)
|
||||
|
||||
const pretty = (str: string) => str
|
||||
.split("-")
|
||||
.map(str => `${str.at(0)?.toUpperCase()}${str.slice(1)}`)
|
||||
.join(" ")
|
||||
|
||||
const PowerProfileToggle = () => ArrowToggleButton({
|
||||
name: "asusctl-profile",
|
||||
icon: profile.as(p => icons.powerprofile[p]),
|
||||
label: profile.as(pretty),
|
||||
connection: [pp, () => pp.active_profile !== profiles[1]],
|
||||
activate: () => pp.active_profile = profiles[0],
|
||||
deactivate: () => pp.active_profile = profiles[1],
|
||||
activateOnArrow: false,
|
||||
})
|
||||
|
||||
const PowerProfileSelector = () => Menu({
|
||||
name: "asusctl-profile",
|
||||
icon: profile.as(p => icons.powerprofile[p]),
|
||||
title: "Profile Selector",
|
||||
content: [Widget.Box({
|
||||
vertical: true,
|
||||
hexpand: true,
|
||||
child: Widget.Box({
|
||||
vertical: true,
|
||||
children: profiles.map(prof => Widget.Button({
|
||||
on_clicked: () => pp.active_profile = prof,
|
||||
child: Widget.Box({
|
||||
children: [
|
||||
Widget.Icon(icons.powerprofile[prof]),
|
||||
Widget.Label(pretty(prof)),
|
||||
],
|
||||
}),
|
||||
})),
|
||||
}),
|
||||
})],
|
||||
})
|
||||
|
||||
export const ProfileToggle = asusctl.available
|
||||
? AsusProfileToggle : PowerProfileToggle
|
||||
|
||||
export const ProfileSelector = asusctl.available
|
||||
? AsusProfileSelector : PowerProfileSelector
|
||||
150
roles/ags/files/widget/quicksettings/widgets/Volume.ts
Normal file
150
roles/ags/files/widget/quicksettings/widgets/Volume.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { type Stream } from "types/service/audio"
|
||||
import { Arrow, Menu } from "../ToggleButton"
|
||||
import { dependencies, icon, sh } from "lib/utils"
|
||||
import icons from "lib/icons.js"
|
||||
const audio = await Service.import("audio")
|
||||
|
||||
type Type = "microphone" | "speaker"
|
||||
|
||||
const VolumeIndicator = (type: Type = "speaker") => Widget.Button({
|
||||
vpack: "center",
|
||||
on_clicked: () => audio[type].is_muted = !audio[type].is_muted,
|
||||
child: Widget.Icon({
|
||||
icon: audio[type].bind("icon_name")
|
||||
.as(i => icon(i || "", icons.audio.mic.high)),
|
||||
tooltipText: audio[type].bind("volume")
|
||||
.as(vol => `Volume: ${Math.floor(vol * 100)}%`),
|
||||
}),
|
||||
})
|
||||
|
||||
const VolumeSlider = (type: Type = "speaker") => Widget.Slider({
|
||||
hexpand: true,
|
||||
draw_value: false,
|
||||
on_change: ({ value, dragging }) => {
|
||||
if (dragging) {
|
||||
audio[type].volume = value
|
||||
audio[type].is_muted = false
|
||||
}
|
||||
},
|
||||
value: audio[type].bind("volume"),
|
||||
class_name: audio[type].bind("is_muted").as(m => m ? "muted" : ""),
|
||||
})
|
||||
|
||||
export const Volume = () => Widget.Box({
|
||||
class_name: "volume",
|
||||
children: [
|
||||
VolumeIndicator("speaker"),
|
||||
VolumeSlider("speaker"),
|
||||
Widget.Box({
|
||||
vpack: "center",
|
||||
child: Arrow("sink-selector"),
|
||||
}),
|
||||
Widget.Box({
|
||||
vpack: "center",
|
||||
child: Arrow("app-mixer"),
|
||||
visible: audio.bind("apps").as(a => a.length > 0),
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
export const Microhone = () => Widget.Box({
|
||||
class_name: "slider horizontal",
|
||||
visible: audio.bind("recorders").as(a => a.length > 0),
|
||||
children: [
|
||||
VolumeIndicator("microphone"),
|
||||
VolumeSlider("microphone"),
|
||||
],
|
||||
})
|
||||
|
||||
const MixerItem = (stream: Stream) => Widget.Box(
|
||||
{
|
||||
hexpand: true,
|
||||
class_name: "mixer-item horizontal",
|
||||
},
|
||||
Widget.Icon({
|
||||
tooltip_text: stream.bind("name").as(n => n || ""),
|
||||
icon: stream.bind("name").as(n => {
|
||||
return Utils.lookUpIcon(n || "")
|
||||
? (n || "")
|
||||
: icons.fallback.audio
|
||||
}),
|
||||
}),
|
||||
Widget.Box(
|
||||
{ vertical: true },
|
||||
Widget.Label({
|
||||
xalign: 0,
|
||||
truncate: "end",
|
||||
max_width_chars: 28,
|
||||
label: stream.bind("description").as(d => d || ""),
|
||||
}),
|
||||
Widget.Slider({
|
||||
hexpand: true,
|
||||
draw_value: false,
|
||||
value: stream.bind("volume"),
|
||||
on_change: ({ value }) => stream.volume = value,
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
const SinkItem = (stream: Stream) => Widget.Button({
|
||||
hexpand: true,
|
||||
on_clicked: () => audio.speaker = stream,
|
||||
child: Widget.Box({
|
||||
children: [
|
||||
Widget.Icon({
|
||||
icon: icon(stream.icon_name || "", icons.fallback.audio),
|
||||
tooltip_text: stream.icon_name || "",
|
||||
}),
|
||||
Widget.Label((stream.description || "").split(" ").slice(0, 4).join(" ")),
|
||||
Widget.Icon({
|
||||
icon: icons.ui.tick,
|
||||
hexpand: true,
|
||||
hpack: "end",
|
||||
visible: audio.speaker.bind("stream").as(s => s === stream.stream),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
})
|
||||
|
||||
const SettingsButton = () => Widget.Button({
|
||||
on_clicked: () => {
|
||||
if (dependencies("pavucontrol"))
|
||||
sh("pavucontrol")
|
||||
},
|
||||
hexpand: true,
|
||||
child: Widget.Box({
|
||||
children: [
|
||||
Widget.Icon(icons.ui.settings),
|
||||
Widget.Label("Settings"),
|
||||
],
|
||||
}),
|
||||
})
|
||||
|
||||
export const AppMixer = () => Menu({
|
||||
name: "app-mixer",
|
||||
icon: icons.audio.mixer,
|
||||
title: "App Mixer",
|
||||
content: [
|
||||
Widget.Box({
|
||||
vertical: true,
|
||||
class_name: "vertical mixer-item-box",
|
||||
children: audio.bind("apps").as(a => a.map(MixerItem)),
|
||||
}),
|
||||
Widget.Separator(),
|
||||
SettingsButton(),
|
||||
],
|
||||
})
|
||||
|
||||
export const SinkSelector = () => Menu({
|
||||
name: "sink-selector",
|
||||
icon: icons.audio.type.headset,
|
||||
title: "Sink Selector",
|
||||
content: [
|
||||
Widget.Box({
|
||||
vertical: true,
|
||||
children: audio.bind("speakers").as(a => a.map(SinkItem)),
|
||||
}),
|
||||
Widget.Separator(),
|
||||
SettingsButton(),
|
||||
],
|
||||
})
|
||||
34
roles/ags/files/widget/settings/Group.ts
Normal file
34
roles/ags/files/widget/settings/Group.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import icons from "lib/icons"
|
||||
import Row from "./Row"
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export default (title: string, ...rows: ReturnType<typeof Row<any>>[]) => Widget.Box(
|
||||
{
|
||||
class_name: "group",
|
||||
vertical: true,
|
||||
},
|
||||
Widget.Box([
|
||||
Widget.Label({
|
||||
hpack: "start",
|
||||
vpack: "end",
|
||||
class_name: "group-title",
|
||||
label: title,
|
||||
setup: w => Utils.idle(() => w.visible = !!title),
|
||||
}),
|
||||
title ? Widget.Button({
|
||||
hexpand: true,
|
||||
hpack: "end",
|
||||
child: Widget.Icon(icons.ui.refresh),
|
||||
class_name: "group-reset",
|
||||
sensitive: Utils.merge(
|
||||
rows.map(({ attribute: { opt } }) => opt.bind().as(v => v !== opt.initial)),
|
||||
(...values) => values.some(b => b),
|
||||
),
|
||||
on_clicked: () => rows.forEach(row => row.attribute.opt.reset()),
|
||||
}) : Widget.Box(),
|
||||
]),
|
||||
Widget.Box({
|
||||
vertical: true,
|
||||
children: rows,
|
||||
}),
|
||||
)
|
||||
19
roles/ags/files/widget/settings/Page.ts
Normal file
19
roles/ags/files/widget/settings/Page.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import Group from "./Group"
|
||||
|
||||
export default <T>(
|
||||
name: string,
|
||||
icon: string,
|
||||
...groups: ReturnType<typeof Group<T>>[]
|
||||
) => Widget.Box({
|
||||
class_name: "page",
|
||||
attribute: { name, icon },
|
||||
child: Widget.Scrollable({
|
||||
css: "min-height: 300px;",
|
||||
child: Widget.Box({
|
||||
class_name: "page-content",
|
||||
vexpand: true,
|
||||
vertical: true,
|
||||
children: groups,
|
||||
}),
|
||||
}),
|
||||
})
|
||||
55
roles/ags/files/widget/settings/Row.ts
Normal file
55
roles/ags/files/widget/settings/Row.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { Opt } from "lib/option"
|
||||
import Setter from "./Setter"
|
||||
import icons from "lib/icons"
|
||||
|
||||
export type RowProps<T> = {
|
||||
opt: Opt<T>
|
||||
title: string
|
||||
note?: string
|
||||
type?:
|
||||
| "number"
|
||||
| "color"
|
||||
| "float"
|
||||
| "object"
|
||||
| "string"
|
||||
| "enum"
|
||||
| "boolean"
|
||||
| "img"
|
||||
| "font"
|
||||
enums?: string[]
|
||||
max?: number
|
||||
min?: number
|
||||
}
|
||||
|
||||
export default <T>(props: RowProps<T>) => Widget.Box(
|
||||
{
|
||||
attribute: { opt: props.opt },
|
||||
class_name: "row",
|
||||
tooltip_text: props.note ? `note: ${props.note}` : "",
|
||||
},
|
||||
Widget.Box(
|
||||
{ vertical: true, vpack: "center" },
|
||||
Widget.Label({
|
||||
xalign: 0,
|
||||
class_name: "row-title",
|
||||
label: props.title,
|
||||
}),
|
||||
Widget.Label({
|
||||
xalign: 0,
|
||||
class_name: "id",
|
||||
label: props.opt.id,
|
||||
}),
|
||||
),
|
||||
Widget.Box({ hexpand: true }),
|
||||
Widget.Box(
|
||||
{ vpack: "center" },
|
||||
Setter(props),
|
||||
),
|
||||
Widget.Button({
|
||||
vpack: "center",
|
||||
class_name: "reset",
|
||||
child: Widget.Icon(icons.ui.refresh),
|
||||
on_clicked: () => props.opt.reset(),
|
||||
sensitive: props.opt.bind().as(v => v !== props.opt.initial),
|
||||
}),
|
||||
)
|
||||
93
roles/ags/files/widget/settings/Setter.ts
Normal file
93
roles/ags/files/widget/settings/Setter.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { type RowProps } from "./Row"
|
||||
import { Opt } from "lib/option"
|
||||
import icons from "lib/icons"
|
||||
import Gdk from "gi://Gdk"
|
||||
|
||||
function EnumSetter(opt: Opt<string>, values: string[]) {
|
||||
const lbl = Widget.Label({ label: opt.bind().as(v => `${v}`) })
|
||||
const step = (dir: 1 | -1) => {
|
||||
const i = values.findIndex(i => i === lbl.label)
|
||||
opt.setValue(dir > 0
|
||||
? i + dir > values.length - 1 ? values[0] : values[i + dir]
|
||||
: i + dir < 0 ? values[values.length - 1] : values[i + dir],
|
||||
)
|
||||
}
|
||||
const next = Widget.Button({
|
||||
child: Widget.Icon(icons.ui.arrow.right),
|
||||
on_clicked: () => step(+1),
|
||||
})
|
||||
const prev = Widget.Button({
|
||||
child: Widget.Icon(icons.ui.arrow.left),
|
||||
on_clicked: () => step(-1),
|
||||
})
|
||||
return Widget.Box({
|
||||
class_name: "enum-setter",
|
||||
children: [lbl, prev, next],
|
||||
})
|
||||
}
|
||||
|
||||
export default function Setter<T>({
|
||||
opt,
|
||||
type = typeof opt.value as RowProps<T>["type"],
|
||||
enums,
|
||||
max = 1000,
|
||||
min = 0,
|
||||
}: RowProps<T>) {
|
||||
switch (type) {
|
||||
case "number": return Widget.SpinButton({
|
||||
setup(self) {
|
||||
self.set_range(min, max)
|
||||
self.set_increments(1, 5)
|
||||
self.on("value-changed", () => opt.value = self.value as T)
|
||||
self.hook(opt, () => self.value = opt.value as number)
|
||||
},
|
||||
})
|
||||
|
||||
case "float":
|
||||
case "object": return Widget.Entry({
|
||||
on_accept: self => opt.value = JSON.parse(self.text || ""),
|
||||
setup: self => self.hook(opt, () => self.text = JSON.stringify(opt.value)),
|
||||
})
|
||||
|
||||
case "string": return Widget.Entry({
|
||||
on_accept: self => opt.value = self.text as T,
|
||||
setup: self => self.hook(opt, () => self.text = opt.value as string),
|
||||
})
|
||||
|
||||
case "enum": return EnumSetter(opt as unknown as Opt<string>, enums!)
|
||||
case "boolean": return Widget.Switch()
|
||||
.on("notify::active", self => opt.value = self.active as T)
|
||||
.hook(opt, self => self.active = opt.value as boolean)
|
||||
|
||||
case "img": return Widget.FileChooserButton({
|
||||
on_file_set: ({ uri }) => { opt.value = uri!.replace("file://", "") as T },
|
||||
})
|
||||
|
||||
case "font": return Widget.FontButton({
|
||||
show_size: false,
|
||||
use_size: false,
|
||||
setup: self => self
|
||||
.hook(opt, () => self.font = opt.value as string)
|
||||
.on("font-set", ({ font }) => opt.value = font!
|
||||
.split(" ").slice(0, -1).join(" ") as T),
|
||||
})
|
||||
|
||||
case "color": return Widget.ColorButton()
|
||||
.hook(opt, self => {
|
||||
const rgba = new Gdk.RGBA()
|
||||
rgba.parse(opt.value as string)
|
||||
self.rgba = rgba
|
||||
})
|
||||
.on("color-set", ({ rgba: { red, green, blue } }) => {
|
||||
const hex = (n: number) => {
|
||||
const c = Math.floor(255 * n).toString(16)
|
||||
return c.length === 1 ? `0${c}` : c
|
||||
}
|
||||
opt.value = `#${hex(red)}${hex(green)}${hex(blue)}` as T
|
||||
})
|
||||
|
||||
default: return Widget.Label({
|
||||
label: `no setter with type ${type}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
63
roles/ags/files/widget/settings/SettingsDialog.ts
Normal file
63
roles/ags/files/widget/settings/SettingsDialog.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import RegularWindow from "widget/RegularWindow"
|
||||
import layout from "./layout"
|
||||
import icons from "lib/icons"
|
||||
import options from "options"
|
||||
|
||||
const current = Variable(layout[0].attribute.name)
|
||||
|
||||
const Header = () => Widget.CenterBox({
|
||||
class_name: "header",
|
||||
start_widget: Widget.Button({
|
||||
class_name: "reset",
|
||||
on_clicked: options.reset,
|
||||
hpack: "start",
|
||||
vpack: "start",
|
||||
child: Widget.Icon(icons.ui.refresh),
|
||||
tooltip_text: "Reset",
|
||||
}),
|
||||
center_widget: Widget.Box({
|
||||
class_name: "pager horizontal",
|
||||
children: layout.map(({ attribute: { name, icon } }) => Widget.Button({
|
||||
xalign: 0,
|
||||
class_name: current.bind().as(v => `${v === name ? "active" : ""}`),
|
||||
on_clicked: () => current.value = name,
|
||||
child: Widget.Box([
|
||||
Widget.Icon(icon),
|
||||
Widget.Label(name),
|
||||
]),
|
||||
})),
|
||||
}),
|
||||
end_widget: Widget.Button({
|
||||
class_name: "close",
|
||||
hpack: "end",
|
||||
vpack: "start",
|
||||
child: Widget.Icon(icons.ui.close),
|
||||
on_clicked: () => App.closeWindow("settings-dialog"),
|
||||
}),
|
||||
})
|
||||
|
||||
const PagesStack = () => Widget.Stack({
|
||||
transition: "slide_left_right",
|
||||
children: layout.reduce((obj, page) => ({ ...obj, [page.attribute.name]: page }), {}),
|
||||
shown: current.bind() as never,
|
||||
})
|
||||
|
||||
export default () => RegularWindow({
|
||||
name: "settings-dialog",
|
||||
class_name: "settings-dialog",
|
||||
title: "Settings",
|
||||
setup(win) {
|
||||
win.on("delete-event", () => {
|
||||
win.hide()
|
||||
return true
|
||||
})
|
||||
win.set_default_size(500, 600)
|
||||
},
|
||||
child: Widget.Box({
|
||||
vertical: true,
|
||||
children: [
|
||||
Header(),
|
||||
PagesStack(),
|
||||
],
|
||||
}),
|
||||
})
|
||||
31
roles/ags/files/widget/settings/Wallpaper.ts
Normal file
31
roles/ags/files/widget/settings/Wallpaper.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import wallpaper from "service/wallpaper"
|
||||
|
||||
export default () => Widget.Box(
|
||||
{ class_name: "row wallpaper" },
|
||||
Widget.Box(
|
||||
{ vertical: true },
|
||||
Widget.Label({
|
||||
xalign: 0,
|
||||
class_name: "row-title",
|
||||
label: "Wallpaper",
|
||||
vpack: "start",
|
||||
}),
|
||||
Widget.Button({
|
||||
on_clicked: wallpaper.random,
|
||||
label: "Random",
|
||||
}),
|
||||
Widget.FileChooserButton({
|
||||
on_file_set: ({ uri }) => wallpaper.set(uri!.replace("file://", "")),
|
||||
}),
|
||||
),
|
||||
Widget.Box({ hexpand: true }),
|
||||
Widget.Box({
|
||||
class_name: "preview",
|
||||
css: wallpaper.bind("wallpaper").as(wp => `
|
||||
min-height: 120px;
|
||||
min-width: 200px;
|
||||
background-image: url('${wp}');
|
||||
background-size: cover;
|
||||
`),
|
||||
}),
|
||||
)
|
||||
147
roles/ags/files/widget/settings/layout.ts
Normal file
147
roles/ags/files/widget/settings/layout.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
/* eslint-disable max-len */
|
||||
import Row from "./Row"
|
||||
import Group from "./Group"
|
||||
import Page from "./Page"
|
||||
import Wallpaper from "./Wallpaper"
|
||||
import options from "options"
|
||||
import icons from "lib/icons"
|
||||
|
||||
const {
|
||||
autotheme: at,
|
||||
font,
|
||||
theme,
|
||||
bar: b,
|
||||
launcher: l,
|
||||
overview: ov,
|
||||
powermenu: pm,
|
||||
quicksettings: qs,
|
||||
osd,
|
||||
hyprland: h,
|
||||
} = options
|
||||
|
||||
const {
|
||||
dark,
|
||||
light,
|
||||
blur,
|
||||
scheme,
|
||||
padding,
|
||||
spacing,
|
||||
radius,
|
||||
shadows,
|
||||
widget,
|
||||
border,
|
||||
} = theme
|
||||
|
||||
export default [
|
||||
Page("Theme", icons.ui.themes,
|
||||
Group("",
|
||||
Wallpaper() as ReturnType<typeof Row>,
|
||||
Row({ opt: at, title: "Auto Generate Color Scheme" }),
|
||||
Row({ opt: scheme, title: "Color Scheme", type: "enum", enums: ["dark", "light"] }),
|
||||
),
|
||||
Group("Dark Colors",
|
||||
Row({ opt: dark.bg, title: "Background", type: "color" }),
|
||||
Row({ opt: dark.fg, title: "Foreground", type: "color" }),
|
||||
Row({ opt: dark.primary.bg, title: "Primary", type: "color" }),
|
||||
Row({ opt: dark.primary.fg, title: "On Primary", type: "color" }),
|
||||
Row({ opt: dark.error.bg, title: "Error", type: "color" }),
|
||||
Row({ opt: dark.error.fg, title: "On Error", type: "color" }),
|
||||
Row({ opt: dark.widget, title: "Widget", type: "color" }),
|
||||
Row({ opt: dark.border, title: "Border", type: "color" }),
|
||||
),
|
||||
Group("Light Colors",
|
||||
Row({ opt: light.bg, title: "Background", type: "color" }),
|
||||
Row({ opt: light.fg, title: "Foreground", type: "color" }),
|
||||
Row({ opt: light.primary.bg, title: "Primary", type: "color" }),
|
||||
Row({ opt: light.primary.fg, title: "On Primary", type: "color" }),
|
||||
Row({ opt: light.error.bg, title: "Error", type: "color" }),
|
||||
Row({ opt: light.error.fg, title: "On Error", type: "color" }),
|
||||
Row({ opt: light.widget, title: "Widget", type: "color" }),
|
||||
Row({ opt: light.border, title: "Border", type: "color" }),
|
||||
),
|
||||
Group("Theme",
|
||||
Row({ opt: shadows, title: "Shadows" }),
|
||||
Row({ opt: widget.opacity, title: "Widget Opacity", max: 100 }),
|
||||
Row({ opt: border.opacity, title: "Border Opacity", max: 100 }),
|
||||
Row({ opt: border.width, title: "Border Width" }),
|
||||
Row({ opt: blur, title: "Blur", note: "0 to disable", max: 70 }),
|
||||
),
|
||||
Group("UI",
|
||||
Row({ opt: padding, title: "Padding" }),
|
||||
Row({ opt: spacing, title: "Spacing" }),
|
||||
Row({ opt: radius, title: "Roundness" }),
|
||||
Row({ opt: font.size, title: "Font Size" }),
|
||||
Row({ opt: font.name, title: "Font Name", type: "font" }),
|
||||
),
|
||||
),
|
||||
Page("Bar", icons.ui.toolbars,
|
||||
Group("General",
|
||||
Row({ opt: b.flatButtons, title: "Flat Buttons" }),
|
||||
Row({ opt: b.position, title: "Position", type: "enum", enums: ["top", "bottom"] }),
|
||||
Row({ opt: b.corners, title: "Corners" }),
|
||||
),
|
||||
Group("Launcher",
|
||||
Row({ opt: b.launcher.icon.icon, title: "Icon" }),
|
||||
Row({ opt: b.launcher.icon.colored, title: "Colored Icon" }),
|
||||
Row({ opt: b.launcher.label.label, title: "Label" }),
|
||||
Row({ opt: b.launcher.label.colored, title: "Colored Label" }),
|
||||
),
|
||||
Group("Workspaces",
|
||||
Row({ opt: b.workspaces.workspaces, title: "Number of Workspaces", note: "0 to make it dynamic" }),
|
||||
),
|
||||
Group("Taskbar",
|
||||
Row({ opt: b.taskbar.iconSize, title: "Icon Size" }),
|
||||
Row({ opt: b.taskbar.monochrome, title: "Monochrome" }),
|
||||
Row({ opt: b.taskbar.exclusive, title: "Exclusive to workspaces" }),
|
||||
),
|
||||
Group("Date",
|
||||
Row({ opt: b.date.format, title: "Date Format" }),
|
||||
),
|
||||
Group("Media",
|
||||
Row({ opt: b.media.monochrome, title: "Monochrome" }),
|
||||
Row({ opt: b.media.preferred, title: "Preferred Player" }),
|
||||
Row({ opt: b.media.direction, title: "Slide Direction", type: "enum", enums: ["left", "right"] }),
|
||||
Row({ opt: b.media.format, title: "Format of the Label" }),
|
||||
Row({ opt: b.media.length, title: "Max Length of Label" }),
|
||||
),
|
||||
Group("Battery",
|
||||
Row({ opt: b.battery.bar, title: "Style", type: "enum", enums: ["hidden", "regular", "whole"] }),
|
||||
Row({ opt: b.battery.blocks, title: "Number of Blocks" }),
|
||||
Row({ opt: b.battery.width, title: "Width of Bar" }),
|
||||
Row({ opt: b.battery.charging, title: "Charging Color", type: "color" }),
|
||||
),
|
||||
Group("Powermenu",
|
||||
Row({ opt: b.powermenu.monochrome, title: "Monochrome" }),
|
||||
),
|
||||
),
|
||||
Page("General", icons.ui.settings,
|
||||
Group("Hyprland",
|
||||
Row({ opt: h.gapsWhenOnly, title: "Gaps When Only" }),
|
||||
),
|
||||
Group("Launcher",
|
||||
Row({ opt: l.width, title: "Width" }),
|
||||
Row({ opt: l.apps.iconSize, title: "Icon Size" }),
|
||||
Row({ opt: l.apps.max, title: "Max Items" }),
|
||||
),
|
||||
Group("Overview",
|
||||
Row({ opt: ov.scale, title: "Scale", max: 100 }),
|
||||
Row({ opt: ov.workspaces, title: "Workspaces", max: 11, note: "set this to 0 to make it dynamic" }),
|
||||
Row({ opt: ov.monochromeIcon, title: "Monochrome Icons" }),
|
||||
),
|
||||
Group("Powermenu",
|
||||
Row({ opt: pm.layout, title: "Layout", type: "enum", enums: ["box", "line"] }),
|
||||
Row({ opt: pm.labels, title: "Show Labels" }),
|
||||
),
|
||||
Group("Quicksettings",
|
||||
Row({ opt: qs.avatar.image, title: "Avatar", type: "img" }),
|
||||
Row({ opt: qs.avatar.size, title: "Avatar Size" }),
|
||||
Row({ opt: qs.media.monochromeIcon, title: "Media Monochrome Icons" }),
|
||||
Row({ opt: qs.media.coverSize, title: "Media Cover Art Size" }),
|
||||
),
|
||||
Group("On Screen Indicator",
|
||||
Row({ opt: osd.progress.vertical, title: "Vertical" }),
|
||||
Row({ opt: osd.progress.pack.h, title: "Horizontal Alignment", type: "enum", enums: ["start", "center", "end"] }),
|
||||
Row({ opt: osd.progress.pack.v, title: "Vertical Alignment", type: "enum", enums: ["start", "center", "end"] }),
|
||||
),
|
||||
),
|
||||
] as const
|
||||
144
roles/ags/files/widget/settings/settingsdialog.scss
Normal file
144
roles/ags/files/widget/settings/settingsdialog.scss
Normal file
@@ -0,0 +1,144 @@
|
||||
window.settings-dialog {
|
||||
background-color: $bg;
|
||||
color: $fg;
|
||||
|
||||
.header {
|
||||
.pager {
|
||||
@include spacing(.5);
|
||||
}
|
||||
|
||||
padding: $padding;
|
||||
|
||||
button {
|
||||
@include button;
|
||||
font-weight: bold;
|
||||
padding: $padding*.5 $padding;
|
||||
|
||||
box {
|
||||
@include spacing($spacing: .3em);
|
||||
}
|
||||
}
|
||||
|
||||
button.close {
|
||||
padding: $padding * .5;
|
||||
}
|
||||
|
||||
button.reset {
|
||||
@include button($flat: true);
|
||||
padding: $padding*.5;
|
||||
}
|
||||
}
|
||||
|
||||
.page {
|
||||
@include scrollable($top: true);
|
||||
|
||||
.page-content {
|
||||
padding: $padding*2;
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.group {
|
||||
.group-title {
|
||||
color: $primary-bg;
|
||||
margin-bottom: $spacing*.5;
|
||||
}
|
||||
|
||||
.group-reset {
|
||||
@include button($flat: true);
|
||||
margin: $spacing * .5;
|
||||
padding: $padding * .5;
|
||||
|
||||
&:disabled {
|
||||
color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-top: $spacing;
|
||||
}
|
||||
}
|
||||
|
||||
.row {
|
||||
background-color: $widget-bg;
|
||||
padding: $padding;
|
||||
border: $border;
|
||||
border-top: none;
|
||||
|
||||
&:first-child {
|
||||
border-radius: $radius $radius 0 0;
|
||||
border: $border;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-radius: 0 0 $radius $radius;
|
||||
}
|
||||
|
||||
&:first-child:last-child {
|
||||
border-radius: $radius;
|
||||
border: $border;
|
||||
}
|
||||
|
||||
button.reset {
|
||||
margin-left: $spacing;
|
||||
}
|
||||
|
||||
label.id,
|
||||
label.note {
|
||||
color: transparentize($fg, .4)
|
||||
}
|
||||
|
||||
entry,
|
||||
button {
|
||||
@include button;
|
||||
padding: $padding;
|
||||
}
|
||||
|
||||
switch {
|
||||
@include switch;
|
||||
}
|
||||
|
||||
spinbutton {
|
||||
@include unset;
|
||||
|
||||
entry {
|
||||
border-radius: $radius 0 0 $radius;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:last-child {
|
||||
border-radius: 0 $radius $radius 0;
|
||||
}
|
||||
}
|
||||
|
||||
.enum-setter {
|
||||
label {
|
||||
background-color: $widget-bg;
|
||||
border: $border;
|
||||
padding: 0 $padding;
|
||||
border-radius: $radius 0 0 $radius;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:last-child {
|
||||
border-radius: 0 $radius $radius 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.wallpaper {
|
||||
button {
|
||||
margin-top: $spacing * .5;
|
||||
}
|
||||
|
||||
.preview {
|
||||
border-radius: $radius;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user