diff --git a/lib/types/options.d.ts b/lib/types/options.d.ts index 640b96f..29673bf 100644 --- a/lib/types/options.d.ts +++ b/lib/types/options.d.ts @@ -3,6 +3,7 @@ import { Opt } from "lib/option"; export type Unit = "imperial" | "metric"; export type PowerOptions = "sleep" | "reboot" | "logout" | "shutdown"; export type NotificationAnchor = "top" | "top right" | "top left" | "bottom" | "bottom right" | "bottom left"; +export type OSDAnchor = "top left" | "top" | "top right" | "right" | "bottom right" | "bottom" | "bottom left" | "left"; export type RowProps = { opt: Opt title: string @@ -22,3 +23,5 @@ export type RowProps = { min?: number subtitle?: string } + +export type OSDOrientation = "horizontal" | "vertical"; diff --git a/main.ts b/main.ts index 25411cb..636b0e1 100644 --- a/main.ts +++ b/main.ts @@ -6,6 +6,7 @@ import MenuWindows from "./modules/menus/main.js"; import SettingsDialog from "widget/settings/SettingsDialog" import Notifications from "./modules/notifications/index.js"; import { forMonitors } from "lib/utils" +import OSD from "modules/osd/index" App.config({ onConfigParsed: () => Utils.execAsync(`python3 ${App.configDir}/services/bluetooth.py`), @@ -14,6 +15,7 @@ App.config({ Notifications(), SettingsDialog(), ...forMonitors(Bar), + OSD(), ], closeWindowDelay: { sideright: 350, diff --git a/modules/menus/energy/brightness/index.ts b/modules/menus/energy/brightness/index.ts index c375375..b1f8b12 100644 --- a/modules/menus/energy/brightness/index.ts +++ b/modules/menus/energy/brightness/index.ts @@ -33,20 +33,20 @@ const Brightness = () => { Widget.Slider({ vpack: "center", vexpand: true, - value: brightness.bind("screen_value"), + value: brightness.bind("screen"), class_name: "menu-active-slider menu-slider brightness", draw_value: false, hexpand: true, min: 0, max: 1, - onChange: ({ value }) => (brightness.screen_value = value), + onChange: ({ value }) => (brightness.screen = value), }), Widget.Label({ vpack: "center", vexpand: true, class_name: "brightness-slider-label", label: brightness - .bind("screen_value") + .bind("screen") .as((b) => `${Math.floor(b * 100)}%`), }), ], diff --git a/modules/osd/bar/index.ts b/modules/osd/bar/index.ts new file mode 100644 index 0000000..a4e9cbc --- /dev/null +++ b/modules/osd/bar/index.ts @@ -0,0 +1,29 @@ +import { OSDOrientation } from "lib/types/options"; +import brightness from "services/Brightness" +const audio = await Service.import("audio") + +export const OSDBar = (ort: OSDOrientation) => { + return Widget.Box({ + class_name: "osd-bar-container", + children: [ + Widget.LevelBar({ + class_name: "osd-bar", + vertical: ort === "vertical", + inverted: ort === "vertical", + bar_mode: "continuous", + setup: self => { + self.hook(brightness, () => { + self.value = brightness.screen; + }, "notify::screen") + self.hook(brightness, () => { + self.value = brightness.kbd; + }, "notify::kbd") + self.hook(audio, () => { + self.toggleClassName("overflow", audio.speaker.volume > 1) + self.value = audio.speaker.volume <= 1 ? audio.speaker.volume : audio.speaker.volume - 1; + }) + } + }) + ] + }); +} diff --git a/modules/osd/icon/index.ts b/modules/osd/icon/index.ts new file mode 100644 index 0000000..f1e5088 --- /dev/null +++ b/modules/osd/icon/index.ts @@ -0,0 +1,28 @@ +import { OSDOrientation } from "lib/types/options"; +import brightness from "services/Brightness" +const audio = await Service.import("audio") + +export const OSDIcon = (ort: OSDOrientation) => { + return Widget.Box({ + class_name: "osd-icon-container", + hexpand: true, + child: Widget.Label({ + class_name: "osd-icon", + hexpand: true, + vexpand: true, + hpack: "center", + vpack: "center", + setup: self => { + self.hook(brightness, () => { + self.label = "󱍖"; + }, "notify::screen") + self.hook(brightness, () => { + self.label = "󰥻"; + }, "notify::kbd") + self.hook(audio, () => { + self.label = audio.speaker.is_muted ? "󰝟" : "󰕾"; + }) + } + }) + }); +} diff --git a/modules/osd/index.ts b/modules/osd/index.ts new file mode 100644 index 0000000..6940a5a --- /dev/null +++ b/modules/osd/index.ts @@ -0,0 +1,113 @@ +import { OSDAnchor } from "lib/types/options"; +import options from "options"; +import brightness from "services/Brightness" +import { OSDLabel } from "./label/index"; +import { OSDBar } from "./bar/index"; +import { OSDIcon } from "./icon/index"; +const hyprland = await Service.import("hyprland"); +const audio = await Service.import("audio") + +const { + enable, + orientation, + location, + active_monitor, + monitor +} = options.theme.osd; + +const curMonitor = Variable(monitor.value); + +hyprland.active.connect("changed", () => { + curMonitor.value = hyprland.active.monitor.id; +}) + +const DELAY = 2500; + +const getPosition = (pos: OSDAnchor): ("top" | "bottom" | "left" | "right")[] => { + const positionMap: { [key: string]: ("top" | "bottom" | "left" | "right")[] } = { + "top": ["top"], + "top right": ["top", "right"], + "top left": ["top", "left"], + "bottom": ["bottom"], + "bottom right": ["bottom", "right"], + "bottom left": ["bottom", "left"], + "right": ["right"], + "left": ["left"], + }; + + return positionMap[pos]; +} +const renderOSD = () => { + let count = 0 + + const handleReveal = (self: any) => { + self.reveal_child = true + count++ + Utils.timeout(DELAY, () => { + count-- + + if (count === 0) + self.reveal_child = false + }) + } + + return Widget.Revealer({ + transition: "crossfade", + reveal_child: false, + setup: self => { + self.hook(brightness, () => { + handleReveal(self); + }, "notify::screen") + self.hook(brightness, () => { + handleReveal(self); + }, "notify::kbd") + self.hook(audio.speaker, () => { + handleReveal(self); + }) + + }, + child: Widget.Box({ + class_name: "osd-container", + vertical: orientation.bind("value").as(ort => ort === "vertical"), + children: orientation.bind("value").as(ort => { + if (ort === "vertical") { + return [ + OSDLabel(ort), + OSDBar(ort), + OSDIcon(ort) + ] + } + + return [ + OSDIcon(ort), + OSDBar(ort), + OSDLabel(ort), + ] + }) + }) + }) +} + +export default () => Widget.Window({ + monitor: Utils.merge([ + curMonitor.bind("value"), + monitor.bind("value"), + active_monitor.bind("value")], (curMon, mon, activeMonitor) => { + if (activeMonitor === true) { + return curMon; + } + + return mon; + }), + name: `indicator`, + visible: enable.bind("value"), + class_name: "indicator", + layer: "overlay", + anchor: location.bind("value").as(v => getPosition(v)), + click_through: true, + child: Widget.Box({ + css: "padding: 1px;", + expand: true, + child: renderOSD(), + }), +}) diff --git a/modules/osd/label/index.ts b/modules/osd/label/index.ts new file mode 100644 index 0000000..47cb771 --- /dev/null +++ b/modules/osd/label/index.ts @@ -0,0 +1,30 @@ +import { OSDOrientation } from "lib/types/options"; +import brightness from "services/Brightness" +const audio = await Service.import("audio") + +export const OSDLabel = (ort: OSDOrientation) => { + return Widget.Box({ + class_name: "osd-label-container", + hexpand: true, + vexpand: true, + child: Widget.Label({ + class_name: "osd-label", + hexpand: true, + vexpand: true, + hpack: "center", + vpack: "center", + setup: self => { + self.hook(brightness, () => { + self.label = `${Math.floor(brightness.screen * 100)}`; + }, "notify::screen") + self.hook(brightness, () => { + self.label = `${Math.floor(brightness.kbd * 100)}`; + }, "notify::kbd") + self.hook(audio, () => { + self.toggleClassName("overflow", audio.speaker.volume > 1) + self.label = `${Math.floor(audio.speaker.volume * 100)}`; + }) + } + }) + }); +} diff --git a/options.ts b/options.ts index e76105a..c9cbff5 100644 --- a/options.ts +++ b/options.ts @@ -53,6 +53,22 @@ const options = mkOptions(OPTIONS, { label: opt(colors.crust) } }, + osd: { + enable: opt(true), + orientation: opt<"horizontal" | "vertical">("vertical"), + bar_container: opt(colors.crust), + icon_container: opt(colors.lavender), + bar_color: opt(colors.lavender), + bar_empty_color: opt(colors.surface0), + bar_overflow_color: opt(colors.red), + icon: opt(colors.crust), + label: opt(colors.lavender), + monitor: opt(0), + active_monitor: opt(true), + radius: opt("0.4em"), + margins: opt("0px 5px 0px 0px"), + location: opt<"top left" | "top" | "top right" | "right" | "bottom right" | "bottom" | "bottom left" | "left">("right"), + }, bar: { floating: opt(false), margin_top: opt("0.5em"), @@ -725,22 +741,6 @@ const options = mkOptions(OPTIONS, { terminal: opt("kitty"), - osd: { - progress: { - vertical: opt(true), - pack: { - h: opt<"start" | "center" | "end">("end"), - v: opt<"start" | "center" | "end">("center"), - }, - }, - microphone: { - pack: { - h: opt<"start" | "center" | "end">("center"), - v: opt<"start" | "center" | "end">("end"), - }, - }, - }, - notifications: { position: opt<"top" | "top right" | "top left" | "bottom" | "bottom right" | "bottom left">("top right"), timeout: opt(7000), diff --git a/scss/entry.scss b/scss/entry.scss index ca895e1..cca67aa 100644 --- a/scss/entry.scss +++ b/scss/entry.scss @@ -42,5 +42,8 @@ //notifications @import "style/notifications/popups"; +//osd +@import "style/osd/index"; + //settings dialog @import "style/settings/dialog" diff --git a/scss/main.scss b/scss/main.scss index bd5aef5..1620be2 100644 --- a/scss/main.scss +++ b/scss/main.scss @@ -41,5 +41,8 @@ //notifications @import "style/notifications/popups"; +//osd +@import "style/osd/index"; + //settings dialog @import "style/settings/dialog" diff --git a/scss/style/menus/media.scss b/scss/style/menus/media.scss index 177a255..1de64fc 100644 --- a/scss/style/menus/media.scss +++ b/scss/style/menus/media.scss @@ -2,9 +2,10 @@ @import '../../variables'; .menu-items.media { - background: if($bar-menus-monochrome, $bar-menus-background, $bar-menus-menu-media-background-color); - border-color: if($bar-menus-monochrome, $bar-menus-border-color, $bar-menus-menu-media-border-color); + background: if($bar-menus-monochrome, $bar-menus-background, $bar-menus-menu-media-background-color); + border-color: if($bar-menus-monochrome, $bar-menus-border-color, $bar-menus-menu-media-border-color); } + .menu-items-container.media { min-width: 23em; min-height: 10em; @@ -108,6 +109,7 @@ background: if($bar-menus-monochrome, $bar-menus-slider-primary, $bar-menus-menu-media-slider-primary); } } + slider { background: if($bar-menus-monochrome, $bar-menus-slider-puck, $bar-menus-menu-media-slider-puck); } diff --git a/scss/style/osd/index.scss b/scss/style/osd/index.scss new file mode 100644 index 0000000..0f8155c --- /dev/null +++ b/scss/style/osd/index.scss @@ -0,0 +1,79 @@ +@import "../colors"; +@import '../../variables'; + +.indicator { + .osd-container { + margin: $osd-margins; + } + + .osd-label-container { + background: $osd-bar_container; + + border-radius: if($osd-orientation =="vertical", $osd-radius $osd-radius 0em 0em, 0em $osd-radius $osd-radius 0em); + + .osd-label { + font-size: 1em; + padding-top: if($osd-orientation =="vertical", 1em, 0em); + padding-right: if($osd-orientation =="horizontal", 1em, 0em); + color: $osd-label; + + &.overflow { + color: $osd-bar_overflow_color; + } + } + } + + .osd-icon-container { + background: $osd-icon_container; + + border-radius: if($osd-orientation =="vertical", 0em 0em $osd-radius $osd-radius, $osd-radius 0em 0em $osd-radius ); + + .osd-icon { + font-size: 2em; + padding: if($osd-orientation =="vertical", 0.2em 0em, 0em 0.2em); + color: $osd-icon; + } + } + + .osd-bar-container { + padding: 1.25em; + background: $osd-bar_container; + + .osd-bar { + levelbar * { + transition: 200ms; + } + + trough { + min-height: if($osd-orientation =="vertical", 10em, 0); + min-width: if($osd-orientation =="horizontal", 10em, 0); + } + + block { + border-radius: $osd-radius/2; + + &.empty { + background: $osd-bar_empty_color; + + } + + &.filled { + background: $osd-bar_color; + padding: 0.45em; + } + } + + &.overflow { + block { + &.empty { + background: $osd-bar_color; + } + + &.filled { + background: $osd-bar_overflow_color; + } + } + } + } + } +} diff --git a/services/Brightness.ts b/services/Brightness.ts index 895f6f9..0c9bcf2 100644 --- a/services/Brightness.ts +++ b/services/Brightness.ts @@ -1,84 +1,70 @@ -class BrightnessService extends Service { - // every subclass of GObject.Object has to register itself +// <3 Aylur for this brightness service +import { bash, dependencies, sh } from "lib/utils" + +if (!dependencies("brightnessctl")) + App.quit() + +const get = (args: string) => Number(Utils.exec(`brightnessctl ${args}`)) +const screen = await bash`ls -w1 /sys/class/backlight | head -1` +const kbd = await bash`ls -w1 /sys/class/leds | head -1` + +class Brightness extends Service { static { - // takes three arguments - // the class itself - // an object defining the signals - // an object defining its properties - Service.register( - this, - { - // 'name-of-signal': [type as a string from GObject.TYPE_], - 'screen-changed': ['float'], - }, - { - // 'kebab-cased-name': [type as a string from GObject.TYPE_, 'r' | 'w' | 'rw'] - // 'r' means readable - // 'w' means writable - // guess what 'rw' means - 'screen-value': ['float', 'rw'], - }, - ); + Service.register(this, {}, { + "screen": ["float", "rw"], + "kbd": ["int", "rw"], + }) } - // this Service assumes only one device with backlight - #interface = Utils.exec("sh -c 'ls -w1 /sys/class/backlight | head -1'"); + #kbdMax = get(`--device ${kbd} max`) + #kbd = get(`--device ${kbd} get`) + #screenMax = get("max") + #screen = get("get") / (get("max") || 1) - // # prefix means private in JS - #screenValue = 0; - #max = Number(Utils.exec('brightnessctl max')); + get kbd() { return this.#kbd } + get screen() { return this.#screen } - // the getter has to be in snake_case - get screen_value() { - return this.#screenValue; + set kbd(value) { + if (value < 0 || value > this.#kbdMax) + return + + sh(`brightnessctl -d ${kbd} s ${value} -q`).then(() => { + this.#kbd = value + this.changed("kbd") + }) } - // the setter has to be in snake_case too - set screen_value(percent) { + set screen(percent) { if (percent < 0) - percent = 0; + percent = 0 if (percent > 1) - percent = 1; + percent = 1 - Utils.execAsync(`brightnessctl set ${percent * 100}% -q`); - // the file monitor will handle the rest + sh(`brightnessctl set ${Math.floor(percent * 100)}% -q`).then(() => { + this.#screen = percent + this.changed("screen") + }) } constructor() { - super(); + super() - // setup monitor - const brightness = `/sys/class/backlight/${this.#interface}/brightness`; - Utils.monitorFile(brightness, () => this.#onChange()); + const screenPath = `/sys/class/backlight/${screen}/brightness` + const kbdPath = `/sys/class/leds/${kbd}/brightness` - // initialize - this.#onChange(); - } + Utils.monitorFile(screenPath, async f => { + const v = await Utils.readFileAsync(f) + this.#screen = Number(v) / this.#screenMax + this.changed("screen") + }) - #onChange() { - this.#screenValue = Number(Utils.exec('brightnessctl get')) / this.#max; - - // signals have to be explicitly emitted - this.emit('changed'); // emits "changed" - this.notify('screen-value'); // emits "notify::screen-value" - - // or use Service.changed(propName: string) which does the above two - // this.changed('screen-value'); - - // emit screen-changed with the percent as a parameter - this.emit('screen-changed', this.#screenValue); - } - - // overwriting the connect method, let's you - // change the default event that widgets connect to - connect(event: string = 'screen-changed', callback: any) { - return super.connect(event, callback); + Utils.monitorFile(kbdPath, async f => { + const v = await Utils.readFileAsync(f) + this.#kbd = Number(v) / this.#kbdMax + this.changed("kbd") + }) } } -// the singleton instance -const service = new BrightnessService; - -// export to use in other modules -export default service; +export default new Brightness diff --git a/widget/settings/pages/config/general/index.ts b/widget/settings/pages/config/general/index.ts index 10bcecc..f43d472 100644 --- a/widget/settings/pages/config/general/index.ts +++ b/widget/settings/pages/config/general/index.ts @@ -13,6 +13,15 @@ export const BarGeneral = () => { Option({ opt: options.theme.font.size, title: 'Font Size', type: 'string' }), Option({ opt: options.theme.font.weight, title: 'Font Weight', subtitle: "100, 200, 300, etc.", type: 'number' }), Option({ opt: options.terminal, title: 'Terminal', subtitle: "Tools such as 'btop' will open in this terminal", type: 'string' }), + + Header('On Screen Display'), + Option({ opt: options.theme.osd.enable, title: 'Enabled', type: 'boolean' }), + Option({ opt: options.theme.osd.orientation, title: 'Orientation', type: 'enum', enums: ["horizontal", "vertical"] }), + Option({ opt: options.theme.osd.location, title: 'Position', subtitle: 'Position of the OSD on the screen', type: 'enum', enums: ["top left", "top", "top right", "right", "bottom right", "bottom", "bottom left", "left"] }), + Option({ opt: options.theme.osd.monitor, title: 'Monitor', subtitle: 'The ID of the monitor on which to display the OSD', type: 'number' }), + Option({ opt: options.theme.osd.active_monitor, title: 'Follow Cursor', subtitle: 'The OSD will follow the monitor of your cursor', type: 'boolean' }), + Option({ opt: options.theme.osd.radius, title: 'Radius', subtitle: 'Radius of the on-screen-display that indicates volume/brightness change', type: 'string' }), + Option({ opt: options.theme.osd.margins, title: 'Margins', subtitle: 'Margins in the following format: top right bottom left', type: 'string' }), ] }) } diff --git a/widget/settings/pages/theme/index.ts b/widget/settings/pages/theme/index.ts index 9612a9f..253a919 100644 --- a/widget/settings/pages/theme/index.ts +++ b/widget/settings/pages/theme/index.ts @@ -10,10 +10,12 @@ import { NetworkMenuTheme } from "./menus/network"; import { NotificationsMenuTheme } from "./menus/notifications"; import { SystrayMenuTheme } from "./menus/systray"; import { VolumeMenuTheme } from "./menus/volume"; +import { OsdTheme } from "./osd/index"; type Page = "General Settings" | "Bar" | "Notifications" + | "OSD" | "Battery Menu" | "Bluetooth Menu" | "Clock Menu" @@ -30,6 +32,7 @@ const pagerMap: Page[] = [ "General Settings", "Bar", "Notifications", + "OSD", "Battery Menu", "Bluetooth Menu", "Clock Menu", @@ -74,6 +77,7 @@ export const ThemesMenu = () => { "General Settings": MenuTheme(), "Bar": BarTheme(), "Notifications": NotificationsTheme(), + "OSD": OsdTheme(), "Battery Menu": BatteryMenuTheme(), "Bluetooth Menu": BluetoothMenuTheme(), "Clock Menu": ClockMenuTheme(), diff --git a/widget/settings/pages/theme/osd/index.ts b/widget/settings/pages/theme/osd/index.ts new file mode 100644 index 0000000..ca9562e --- /dev/null +++ b/widget/settings/pages/theme/osd/index.ts @@ -0,0 +1,26 @@ +import { Option } from "widget/settings/shared/Option"; +import { Header } from "widget/settings/shared/Header"; + +import options from "options"; + +export const OsdTheme = () => { + return Widget.Scrollable({ + vscroll: "automatic", + hscroll: "never", + class_name: "osd-theme-page paged-container", + vexpand: true, + child: Widget.Box({ + vertical: true, + children: [ + Header('On Screen Display Settings'), + Option({ opt: options.theme.osd.bar_color, title: 'Bar', type: 'color' }), + Option({ opt: options.theme.osd.bar_overflow_color, title: 'Overflow', subtitle: 'Overflow color is for when the volume goes over a 100', type: 'color' }), + Option({ opt: options.theme.osd.bar_empty_color, title: 'Bar Background', type: 'color' }), + Option({ opt: options.theme.osd.bar_container, title: 'Bar Container Background', type: 'color' }), + Option({ opt: options.theme.osd.icon, title: 'Icon Background', type: 'color' }), + Option({ opt: options.theme.osd.icon_container, title: 'Icon Container', type: 'color' }), + Option({ opt: options.theme.osd.label, title: 'Value', type: 'color' }), + ] + }) + }) +}