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

View file

@ -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
View file

@ -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
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"]
}
]
}

8
env.d.ts vendored
View file

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

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() { 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>
); )
} }

View file

@ -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>
); )
} }

View file

@ -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>
); )
} }

View file

@ -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'

View file

@ -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>
); )
} }

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
}