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:
Chris Toph 2025-05-10 19:28:47 -04:00
parent 30ae94c523
commit 8746fa0a6e
11 changed files with 399 additions and 143 deletions

36
.vscode/settings.json vendored
View file

@ -1,18 +1,18 @@
{
"biome.enabled": true,
"editor.formatOnSave": true,
"explorer.fileNesting.patterns": {
"flake.nix": "*.nix, flake.lock, .envrc, .tool-versions",
"package.json": " pnpm-lock.yaml, tsconfig.json, .gitignore"
},
"files.exclude": {
".direnv": true
// "@girs": true,
// "node_modules": true
},
"terminal.integrated.defaultProfile.linux": "fish-fhs",
"terminal.integrated.profiles.linux": {
"fish-fhs": {
"biome.enabled": true,
"editor.formatOnSave": true,
"explorer.fileNesting.patterns": {
"flake.nix": "*.nix, flake.lock, .envrc, .tool-versions",
"package.json": " pnpm-lock.yaml, tsconfig.json, .gitignore, biome.jsonc"
},
"files.exclude": {
".direnv": true,
// "@girs": true,
"node_modules": true
},
"terminal.integrated.defaultProfile.linux": "fish-fhs",
"terminal.integrated.profiles.linux": {
"fish-fhs": {
"args": [
"--user",
"--pty",
@ -21,8 +21,8 @@
"--service-type=exec",
"fish"
],
"path": "systemd-run"
}
},
"typescript.tsdk": "./node_modules/typescript/lib"
"path": "systemd-run"
}
},
"typescript.tsdk": "./node_modules/typescript/lib"
}

16
app.ts
View file

@ -1,10 +1,10 @@
import { App } from "astal/gtk4"
import style from "./style/style.scss"
import Bar from "./windows/Bar"
import { App } from 'astal/gtk4'
import style from './style/style.scss'
import Bar from './windows/Bar'
App.start({
css: style,
main() {
App.get_monitors().map(Bar);
},
});
css: style,
main() {
App.get_monitors().map(Bar)
}
})

115
biome.jsonc Normal file
View 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"]
}
]
}

24
env.d.ts vendored
View file

@ -1,21 +1,21 @@
declare const SRC: string
declare module "inline:*" {
const content: string
export default content
declare module 'inline:*' {
const content: string
export default content
}
declare module "*.scss" {
const content: string
export default content
declare module '*.scss' {
const content: string
export default content
}
declare module "*.blp" {
const content: string
export default content
declare module '*.blp' {
const content: string
export default content
}
declare module "*.css" {
const content: string
export default content
declare module '*.css' {
const content: string
export default content
}

View 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>
)
}

View file

@ -1,10 +1,9 @@
import { GLib } from "astal";
import { GLib } from 'astal'
export default function Launcher() {
return (
<button cssName="barauncher">
<image iconName={GLib.get_os_info("LOGO") || "missing-symbolic"} />
</button>
);
return (
<button cssName="barauncher">
<image iconName={GLib.get_os_info('LOGO') || 'missing-symbolic'} />
</button>
)
}

View file

@ -1,20 +1,13 @@
import { bind } from "astal";
import Wp from "gi://AstalWp";
import { bind } from 'astal'
import Wp from 'gi://AstalWp'
export default function VolumeStatus() {
const speaker = Wp.get_default()?.audio.defaultSpeaker!;
const speaker = Wp.get_default()?.audio.defaultSpeaker!
return (
<box
onScroll={(_, __, dy) =>
dy < 0 ? (speaker.volume += 0.01) : (speaker.volume += -0.01)
}
spacing={2}
>
<image iconName={bind(speaker, "volumeIcon")} />
<label
label={bind(speaker, "volume").as((p) => `${Math.floor(p * 100)}%`)}
/>
</box>
);
return (
<box onScroll={(_, __, dy) => (dy < 0 ? (speaker.volume += 0.01) : (speaker.volume += -0.01))} spacing={2}>
<image iconName={bind(speaker, 'volumeIcon')} />
<label label={bind(speaker, 'volume').as((p) => `${Math.floor(p * 100)}%`)} />
</box>
)
}

View file

@ -1,26 +1,26 @@
import { bind } from "astal";
import Hyprland from "gi://AstalHyprland";
import { bind } from 'astal'
import Hyprland from 'gi://AstalHyprland'
export default function Workspaces() {
const hypr = Hyprland.get_default();
const hypr = Hyprland.get_default()
return (
<box cssName="Workspaces">
{bind(hypr, "workspaces").as((wss) =>
wss
.filter((ws) => !(ws.id >= -99 && ws.id <= -2)) // filter out special workspaces
.sort((a, b) => a.id - b.id)
.map((ws) => (
<button
cssName={bind(hypr, "focusedWorkspace")
.as((fw) => (ws === fw ? "focused" : ""))
.get()}
onClicked={() => ws.focus()}
>
{ws.id}
</button>
))
)}
</box>
);
return (
<box cssName="Workspaces">
{bind(hypr, 'workspaces').as((wss) =>
wss
.filter((ws) => !(ws.id >= -99 && ws.id <= -2)) // filter out special workspaces
.sort((a, b) => a.id - b.id)
.map((ws) => (
<button
cssName={bind(hypr, 'focusedWorkspace')
.as((fw) => (ws === fw ? 'focused' : ''))
.get()}
onClicked={() => ws.focus()}
>
{ws.id}
</button>
))
)}
</box>
)
}

View file

@ -1,3 +1,4 @@
export { default as Workspaces } from "./Workspaces";
export { default as Volume } from "./Volume";
export { default as Launcher } from "./Launcher";
export { default as Workspaces } from './Workspaces'
export { default as Volume } from './Volume'
export { default as Launcher } from './Launcher'
export { default as Brightness } from './Brightness'

View file

@ -1,66 +1,53 @@
import { bind, Variable } from "astal";
import { App, Astal, Gtk, Gdk } from "astal/gtk4";
import GLib from "gi://GLib";
import { Volume, Workspaces, Launcher } from "./components";
import { bind, Variable } from 'astal'
import { App, Astal, Gtk, Gdk } from 'astal/gtk4'
import GLib from 'gi://GLib'
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() {
return (
<box>
<Launcher />
<Gtk.Separator />
<Workspaces />
</box>
);
return (
<box halign={Gtk.Align.CENTER}>
<Launcher />
<Workspaces />
</box>
)
}
function Center() {
return (
<box>
{/* <Time /> */}
</box>
);
return (
<box halign={Gtk.Align.CENTER}>
<box>
<menubutton>
<label label={time()} />
<popover>
<Gtk.Calendar canTarget={false} canFocus={false} />
</popover>
</menubutton>
</box>
</box>
)
}
function Right() {
return <></>;
return (
<box halign={Gtk.Align.END}>
<Brightness />
<Volume />
</box>
)
}
export default function Bar(gdkmonitor: Gdk.Monitor) {
const { TOP, LEFT, RIGHT } = Astal.WindowAnchor;
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>
<menubutton>
<label label={time()} />
<popover>
<Gtk.Calendar canTarget={false} canFocus={false} />
</popover>
</menubutton>
</box>
</box>
<box spacing={6} halign={Gtk.Align.END}>
<Volume />
</box>
</centerbox>
</window>
);
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>
</window>
)
}

View 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
}