Adds brightness control & biome config
• Updates IDE settings for improved file nesting and exclusions • Introduces biome configuration file for consistent project formatting • Implements brightness service and UI component for monitor brightness control • Refactors layout and code formatting for consistency across components
This commit is contained in:
parent
30ae94c523
commit
8746fa0a6e
11 changed files with 399 additions and 143 deletions
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
|
@ -3,12 +3,12 @@
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"explorer.fileNesting.patterns": {
|
"explorer.fileNesting.patterns": {
|
||||||
"flake.nix": "*.nix, flake.lock, .envrc, .tool-versions",
|
"flake.nix": "*.nix, flake.lock, .envrc, .tool-versions",
|
||||||
"package.json": " pnpm-lock.yaml, tsconfig.json, .gitignore"
|
"package.json": " pnpm-lock.yaml, tsconfig.json, .gitignore, biome.jsonc"
|
||||||
},
|
},
|
||||||
"files.exclude": {
|
"files.exclude": {
|
||||||
".direnv": true
|
".direnv": true,
|
||||||
// "@girs": true,
|
// "@girs": true,
|
||||||
// "node_modules": true
|
"node_modules": true
|
||||||
},
|
},
|
||||||
"terminal.integrated.defaultProfile.linux": "fish-fhs",
|
"terminal.integrated.defaultProfile.linux": "fish-fhs",
|
||||||
"terminal.integrated.profiles.linux": {
|
"terminal.integrated.profiles.linux": {
|
||||||
|
|
12
app.ts
12
app.ts
|
@ -1,10 +1,10 @@
|
||||||
import { App } from "astal/gtk4"
|
import { App } from 'astal/gtk4'
|
||||||
import style from "./style/style.scss"
|
import style from './style/style.scss'
|
||||||
import Bar from "./windows/Bar"
|
import Bar from './windows/Bar'
|
||||||
|
|
||||||
App.start({
|
App.start({
|
||||||
css: style,
|
css: style,
|
||||||
main() {
|
main() {
|
||||||
App.get_monitors().map(Bar);
|
App.get_monitors().map(Bar)
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
|
|
115
biome.jsonc
Normal file
115
biome.jsonc
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||||
|
"vcs": {
|
||||||
|
"enabled": false,
|
||||||
|
"clientKind": "git",
|
||||||
|
"useIgnoreFile": false
|
||||||
|
},
|
||||||
|
"files": {
|
||||||
|
"ignoreUnknown": false,
|
||||||
|
"ignore": []
|
||||||
|
},
|
||||||
|
"formatter": {
|
||||||
|
"enabled": true,
|
||||||
|
"useEditorconfig": true,
|
||||||
|
"formatWithErrors": false,
|
||||||
|
"indentStyle": "space",
|
||||||
|
"indentWidth": 2,
|
||||||
|
"lineEnding": "lf",
|
||||||
|
"lineWidth": 160,
|
||||||
|
"attributePosition": "auto",
|
||||||
|
"bracketSpacing": true
|
||||||
|
},
|
||||||
|
"organizeImports": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"linter": {
|
||||||
|
"enabled": true,
|
||||||
|
"rules": {
|
||||||
|
"recommended": false,
|
||||||
|
"a11y": {
|
||||||
|
"noAccessKey": "error",
|
||||||
|
"noAriaUnsupportedElements": "error",
|
||||||
|
"noAutofocus": "error",
|
||||||
|
"noBlankTarget": "error",
|
||||||
|
"noDistractingElements": "error",
|
||||||
|
"noHeaderScope": "error",
|
||||||
|
"noInteractiveElementToNoninteractiveRole": "error",
|
||||||
|
"noLabelWithoutControl": "error",
|
||||||
|
"noNoninteractiveElementToInteractiveRole": "error",
|
||||||
|
"noNoninteractiveTabindex": "error",
|
||||||
|
"noPositiveTabindex": "error",
|
||||||
|
"noRedundantAlt": "error",
|
||||||
|
"noRedundantRoles": "error",
|
||||||
|
"useAltText": "error",
|
||||||
|
"useAnchorContent": "error",
|
||||||
|
"useAriaActivedescendantWithTabindex": "error",
|
||||||
|
"useAriaPropsForRole": "error",
|
||||||
|
"useFocusableInteractive": "warn",
|
||||||
|
"useHeadingContent": "error",
|
||||||
|
"useHtmlLang": "error",
|
||||||
|
"useIframeTitle": "error",
|
||||||
|
"useKeyWithClickEvents": "warn",
|
||||||
|
"useKeyWithMouseEvents": "error",
|
||||||
|
"useMediaCaption": "error",
|
||||||
|
"useValidAnchor": "error",
|
||||||
|
"useValidAriaProps": "error",
|
||||||
|
"useValidAriaRole": "error",
|
||||||
|
"useValidAriaValues": "error"
|
||||||
|
},
|
||||||
|
"correctness": {
|
||||||
|
"noChildrenProp": "error",
|
||||||
|
"noUnusedImports": "warn",
|
||||||
|
"noUnusedVariables": "warn",
|
||||||
|
"useExhaustiveDependencies": "off",
|
||||||
|
"useHookAtTopLevel": "error",
|
||||||
|
"useJsxKeyInIterable": "error"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"noDangerouslySetInnerHtmlWithChildren": "error"
|
||||||
|
},
|
||||||
|
"style": {
|
||||||
|
"useBlockStatements": "off"
|
||||||
|
},
|
||||||
|
"suspicious": {
|
||||||
|
"noCommentText": "error",
|
||||||
|
"noConsole": "warn",
|
||||||
|
"noDuplicateJsxProps": "error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ignore": [
|
||||||
|
".now/*",
|
||||||
|
"**/*.css",
|
||||||
|
"**/.changeset",
|
||||||
|
"**/dist",
|
||||||
|
"esm/*",
|
||||||
|
"public/*",
|
||||||
|
"tests/*",
|
||||||
|
"scripts/*",
|
||||||
|
"**/*.config.js",
|
||||||
|
"**/.DS_Store",
|
||||||
|
"**/node_modules",
|
||||||
|
"**/coverage",
|
||||||
|
"**/.next",
|
||||||
|
"**/build"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"javascript": {
|
||||||
|
"formatter": {
|
||||||
|
"jsxQuoteStyle": "double",
|
||||||
|
"quoteProperties": "asNeeded",
|
||||||
|
"trailingCommas": "none",
|
||||||
|
"semicolons": "asNeeded",
|
||||||
|
"arrowParentheses": "always",
|
||||||
|
"bracketSameLine": false,
|
||||||
|
"quoteStyle": "single",
|
||||||
|
"attributePosition": "auto",
|
||||||
|
"bracketSpacing": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"include": ["*.svelte"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
8
env.d.ts
vendored
8
env.d.ts
vendored
|
@ -1,21 +1,21 @@
|
||||||
declare const SRC: string
|
declare const SRC: string
|
||||||
|
|
||||||
declare module "inline:*" {
|
declare module 'inline:*' {
|
||||||
const content: string
|
const content: string
|
||||||
export default content
|
export default content
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module "*.scss" {
|
declare module '*.scss' {
|
||||||
const content: string
|
const content: string
|
||||||
export default content
|
export default content
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module "*.blp" {
|
declare module '*.blp' {
|
||||||
const content: string
|
const content: string
|
||||||
export default content
|
export default content
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module "*.css" {
|
declare module '*.css' {
|
||||||
const content: string
|
const content: string
|
||||||
export default content
|
export default content
|
||||||
}
|
}
|
||||||
|
|
48
windows/Bar/components/Brightness.tsx
Normal file
48
windows/Bar/components/Brightness.tsx
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import { bind } from 'astal'
|
||||||
|
import { Gdk } from 'astal/gtk4'
|
||||||
|
import { Box } from 'astal/gtk4/widget'
|
||||||
|
import BrightnessService from '../services/brightness'
|
||||||
|
|
||||||
|
const brightness = new BrightnessService()
|
||||||
|
|
||||||
|
// Throttling settings
|
||||||
|
const THROTTLE_DELAY = 250 // milliseconds
|
||||||
|
let lastUpdateTime = 0
|
||||||
|
|
||||||
|
function handleScroll(dy: number) {
|
||||||
|
const now = Date.now()
|
||||||
|
|
||||||
|
// If enough time passed
|
||||||
|
if (now - lastUpdateTime >= THROTTLE_DELAY) {
|
||||||
|
// Handle brightness
|
||||||
|
if (dy > 0) {
|
||||||
|
brightness.value -= 10
|
||||||
|
} else if (dy < 0) {
|
||||||
|
brightness.value += 10
|
||||||
|
}
|
||||||
|
|
||||||
|
lastUpdateTime = now // Update timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not ignore this scroll event
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleButtonReleased(button: Gdk.ButtonEvent) {
|
||||||
|
// If middle click refresh brightness with ddcutil
|
||||||
|
if (button.get_button() === 2) {
|
||||||
|
brightness.fetchBrightness()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Brightness() {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
onScroll={(_, __, dy) => handleScroll(dy)}
|
||||||
|
onButtonReleased={(_, button) => handleButtonReleased(button)}
|
||||||
|
cssClasses={['brightness']}
|
||||||
|
spacing={2}>
|
||||||
|
<image iconName={bind(brightness, 'iconName')} />
|
||||||
|
<label label={bind(brightness, 'value').as((p) => `${p}%`)} />
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,10 +1,9 @@
|
||||||
|
import { GLib } from 'astal'
|
||||||
import { GLib } from "astal";
|
|
||||||
|
|
||||||
export default function Launcher() {
|
export default function Launcher() {
|
||||||
return (
|
return (
|
||||||
<button cssName="barauncher">
|
<button cssName="barauncher">
|
||||||
<image iconName={GLib.get_os_info("LOGO") || "missing-symbolic"} />
|
<image iconName={GLib.get_os_info('LOGO') || 'missing-symbolic'} />
|
||||||
</button>
|
</button>
|
||||||
);
|
)
|
||||||
}
|
}
|
|
@ -1,20 +1,13 @@
|
||||||
import { bind } from "astal";
|
import { bind } from 'astal'
|
||||||
import Wp from "gi://AstalWp";
|
import Wp from 'gi://AstalWp'
|
||||||
|
|
||||||
export default function VolumeStatus() {
|
export default function VolumeStatus() {
|
||||||
const speaker = Wp.get_default()?.audio.defaultSpeaker!;
|
const speaker = Wp.get_default()?.audio.defaultSpeaker!
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<box
|
<box onScroll={(_, __, dy) => (dy < 0 ? (speaker.volume += 0.01) : (speaker.volume += -0.01))} spacing={2}>
|
||||||
onScroll={(_, __, dy) =>
|
<image iconName={bind(speaker, 'volumeIcon')} />
|
||||||
dy < 0 ? (speaker.volume += 0.01) : (speaker.volume += -0.01)
|
<label label={bind(speaker, 'volume').as((p) => `${Math.floor(p * 100)}%`)} />
|
||||||
}
|
|
||||||
spacing={2}
|
|
||||||
>
|
|
||||||
<image iconName={bind(speaker, "volumeIcon")} />
|
|
||||||
<label
|
|
||||||
label={bind(speaker, "volume").as((p) => `${Math.floor(p * 100)}%`)}
|
|
||||||
/>
|
|
||||||
</box>
|
</box>
|
||||||
);
|
)
|
||||||
}
|
}
|
|
@ -1,19 +1,19 @@
|
||||||
import { bind } from "astal";
|
import { bind } from 'astal'
|
||||||
import Hyprland from "gi://AstalHyprland";
|
import Hyprland from 'gi://AstalHyprland'
|
||||||
|
|
||||||
export default function Workspaces() {
|
export default function Workspaces() {
|
||||||
const hypr = Hyprland.get_default();
|
const hypr = Hyprland.get_default()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<box cssName="Workspaces">
|
<box cssName="Workspaces">
|
||||||
{bind(hypr, "workspaces").as((wss) =>
|
{bind(hypr, 'workspaces').as((wss) =>
|
||||||
wss
|
wss
|
||||||
.filter((ws) => !(ws.id >= -99 && ws.id <= -2)) // filter out special workspaces
|
.filter((ws) => !(ws.id >= -99 && ws.id <= -2)) // filter out special workspaces
|
||||||
.sort((a, b) => a.id - b.id)
|
.sort((a, b) => a.id - b.id)
|
||||||
.map((ws) => (
|
.map((ws) => (
|
||||||
<button
|
<button
|
||||||
cssName={bind(hypr, "focusedWorkspace")
|
cssName={bind(hypr, 'focusedWorkspace')
|
||||||
.as((fw) => (ws === fw ? "focused" : ""))
|
.as((fw) => (ws === fw ? 'focused' : ''))
|
||||||
.get()}
|
.get()}
|
||||||
onClicked={() => ws.focus()}
|
onClicked={() => ws.focus()}
|
||||||
>
|
>
|
||||||
|
@ -22,5 +22,5 @@ export default function Workspaces() {
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</box>
|
</box>
|
||||||
);
|
)
|
||||||
}
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
export { default as Workspaces } from "./Workspaces";
|
export { default as Workspaces } from './Workspaces'
|
||||||
export { default as Volume } from "./Volume";
|
export { default as Volume } from './Volume'
|
||||||
export { default as Launcher } from "./Launcher";
|
export { default as Launcher } from './Launcher'
|
||||||
|
export { default as Brightness } from './Brightness'
|
||||||
|
|
|
@ -1,52 +1,22 @@
|
||||||
import { bind, Variable } from "astal";
|
import { bind, Variable } from 'astal'
|
||||||
import { App, Astal, Gtk, Gdk } from "astal/gtk4";
|
import { App, Astal, Gtk, Gdk } from 'astal/gtk4'
|
||||||
import GLib from "gi://GLib";
|
import GLib from 'gi://GLib'
|
||||||
import { Volume, Workspaces, Launcher } from "./components";
|
import { Volume, Workspaces, Launcher, Brightness } from './components'
|
||||||
|
|
||||||
const time = Variable<string>("").poll(1000, () => GLib.DateTime.new_now_local().format("%H:%M - %A %e.")!);
|
const time = Variable<string>('').poll(1000, () => GLib.DateTime.new_now_local().format('%H:%M - %A %e.')!)
|
||||||
|
|
||||||
function Left() {
|
function Left() {
|
||||||
return (
|
return (
|
||||||
<box>
|
<box halign={Gtk.Align.CENTER}>
|
||||||
<Launcher />
|
<Launcher />
|
||||||
<Gtk.Separator />
|
|
||||||
<Workspaces />
|
<Workspaces />
|
||||||
</box>
|
</box>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Center() {
|
function Center() {
|
||||||
return (
|
return (
|
||||||
<box>
|
<box halign={Gtk.Align.CENTER}>
|
||||||
{/* <Time /> */}
|
|
||||||
</box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Right() {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export default function Bar(gdkmonitor: Gdk.Monitor) {
|
|
||||||
const { TOP, LEFT, RIGHT } = Astal.WindowAnchor;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<window
|
|
||||||
visible
|
|
||||||
cssName="window"
|
|
||||||
gdkmonitor={gdkmonitor}
|
|
||||||
exclusivity={Astal.Exclusivity.EXCLUSIVE}
|
|
||||||
anchor={TOP | LEFT | RIGHT}
|
|
||||||
application={App}
|
|
||||||
>
|
|
||||||
<centerbox cssName="centerbox" cssClasses={["container"]}>
|
|
||||||
<box spacing={6} halign={Gtk.Align.CENTER}>
|
|
||||||
<Launcher />
|
|
||||||
<Workspaces />
|
|
||||||
</box>
|
|
||||||
|
|
||||||
<box spacing={6} halign={Gtk.Align.CENTER}>
|
|
||||||
<box>
|
<box>
|
||||||
<menubutton>
|
<menubutton>
|
||||||
<label label={time()} />
|
<label label={time()} />
|
||||||
|
@ -56,11 +26,28 @@ export default function Bar(gdkmonitor: Gdk.Monitor) {
|
||||||
</menubutton>
|
</menubutton>
|
||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
<box spacing={6} halign={Gtk.Align.END}>
|
function Right() {
|
||||||
|
return (
|
||||||
|
<box halign={Gtk.Align.END}>
|
||||||
|
<Brightness />
|
||||||
<Volume />
|
<Volume />
|
||||||
</box>
|
</box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Bar(gdkmonitor: Gdk.Monitor) {
|
||||||
|
const { TOP, LEFT, RIGHT } = Astal.WindowAnchor
|
||||||
|
|
||||||
|
return (
|
||||||
|
<window visible cssName="window" gdkmonitor={gdkmonitor} exclusivity={Astal.Exclusivity.EXCLUSIVE} anchor={TOP | LEFT | RIGHT} application={App}>
|
||||||
|
<centerbox cssName="centerbox" cssClasses={['container']}>
|
||||||
|
<Left />
|
||||||
|
<Center />
|
||||||
|
<Right />
|
||||||
</centerbox>
|
</centerbox>
|
||||||
</window>
|
</window>
|
||||||
);
|
)
|
||||||
}
|
}
|
113
windows/Bar/services/brightness.ts
Normal file
113
windows/Bar/services/brightness.ts
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
import { exec } from 'astal'
|
||||||
|
import GObject, { register, property, signal } from 'astal/gobject'
|
||||||
|
|
||||||
|
@register({ GTypeName: 'BrightnessService' })
|
||||||
|
export default class BrightnessService extends GObject.Object {
|
||||||
|
private declare _value: number
|
||||||
|
private declare _buses: string[]
|
||||||
|
private declare _max: number
|
||||||
|
|
||||||
|
@property(Number)
|
||||||
|
get value() {
|
||||||
|
return this._value
|
||||||
|
}
|
||||||
|
|
||||||
|
set value(value: number) {
|
||||||
|
// Ensure value is between 0-100
|
||||||
|
this._value = Math.min(100, Math.max(0, Math.round(value)))
|
||||||
|
|
||||||
|
console.log(`Setting brightness to ${this._value}%`)
|
||||||
|
this._buses.forEach((bus) => {
|
||||||
|
exec(`ddcutil setvcp 10 ${this._value} --bus ${bus}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.emit('value_changed', this._value)
|
||||||
|
this.refreshUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
@property(String)
|
||||||
|
get iconName() {
|
||||||
|
if (this._value < 25) {
|
||||||
|
return 'display-brightness-low-symbolic'
|
||||||
|
} else if (this._value < 75) {
|
||||||
|
return 'display-brightness-medium-symbolic'
|
||||||
|
} else {
|
||||||
|
return 'display-brightness-high-symbolic'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
this._value = 0
|
||||||
|
this._max = 100 // DDC brightness is typically 0-100
|
||||||
|
this._buses = this.detectBuses()
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
detectBuses() {
|
||||||
|
try {
|
||||||
|
const detectOutput = exec('ddcutil detect')
|
||||||
|
const busRegex = /I2C bus:\s+\/dev\/i2c-(\d+)/g
|
||||||
|
|
||||||
|
const buses = []
|
||||||
|
let match
|
||||||
|
while ((match = busRegex.exec(detectOutput)) !== null) {
|
||||||
|
buses.push(match[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
return buses
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error detecting monitors:', error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial setup - validate monitors and get initial brightness
|
||||||
|
init() {
|
||||||
|
if (this._buses.length === 0) {
|
||||||
|
console.warn('No valid DDC monitors detected')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get initial brightness values from monitors
|
||||||
|
this.fetchBrightness()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch actual brightness values from monitors
|
||||||
|
fetchBrightness() {
|
||||||
|
let totalBrightness = 0
|
||||||
|
let validMonitors = 0
|
||||||
|
|
||||||
|
console.log('Fetching brightness from monitors...')
|
||||||
|
|
||||||
|
// Get brightness from each monitor
|
||||||
|
this._buses.forEach((bus) => {
|
||||||
|
try {
|
||||||
|
const brightnessOutput = exec(`ddcutil getvcp 10 --bus ${bus}`)
|
||||||
|
const match = brightnessOutput.match(/current value\s*=\s*(\d+)/)
|
||||||
|
if (match) {
|
||||||
|
totalBrightness += parseInt(match[1], 10)
|
||||||
|
validMonitors++
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error getting brightness for bus ${bus}:`, error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (validMonitors > 0) {
|
||||||
|
this._value = Math.round(totalBrightness / validMonitors)
|
||||||
|
this.refreshUI()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshUI() {
|
||||||
|
this.notify('icon-name')
|
||||||
|
this.notify('value')
|
||||||
|
this.emit('value-changed', this._value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@signal(Number)
|
||||||
|
declare valueChanged: (value: Number) => void
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue