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,
|
||||
"explorer.fileNesting.patterns": {
|
||||
"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": {
|
||||
".direnv": true
|
||||
".direnv": true,
|
||||
// "@girs": true,
|
||||
// "node_modules": true
|
||||
"node_modules": true
|
||||
},
|
||||
"terminal.integrated.defaultProfile.linux": "fish-fhs",
|
||||
"terminal.integrated.profiles.linux": {
|
||||
|
|
12
app.ts
12
app.ts
|
@ -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);
|
||||
},
|
||||
});
|
||||
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 module "inline:*" {
|
||||
declare module 'inline:*' {
|
||||
const content: string
|
||||
export default content
|
||||
}
|
||||
|
||||
declare module "*.scss" {
|
||||
declare module '*.scss' {
|
||||
const content: string
|
||||
export default content
|
||||
}
|
||||
|
||||
declare module "*.blp" {
|
||||
declare module '*.blp' {
|
||||
const content: string
|
||||
export default content
|
||||
}
|
||||
|
||||
declare module "*.css" {
|
||||
declare module '*.css' {
|
||||
const content: string
|
||||
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() {
|
||||
return (
|
||||
<button cssName="barauncher">
|
||||
<image iconName={GLib.get_os_info("LOGO") || "missing-symbolic"} />
|
||||
<image iconName={GLib.get_os_info('LOGO') || 'missing-symbolic'} />
|
||||
</button>
|
||||
);
|
||||
)
|
||||
}
|
|
@ -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 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>
|
||||
);
|
||||
)
|
||||
}
|
|
@ -1,19 +1,19 @@
|
|||
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) =>
|
||||
{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" : ""))
|
||||
cssName={bind(hypr, 'focusedWorkspace')
|
||||
.as((fw) => (ws === fw ? 'focused' : ''))
|
||||
.get()}
|
||||
onClicked={() => ws.focus()}
|
||||
>
|
||||
|
@ -22,5 +22,5 @@ export default function Workspaces() {
|
|||
))
|
||||
)}
|
||||
</box>
|
||||
);
|
||||
)
|
||||
}
|
|
@ -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'
|
||||
|
|
|
@ -1,52 +1,22 @@
|
|||
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>
|
||||
<box halign={Gtk.Align.CENTER}>
|
||||
<Launcher />
|
||||
<Gtk.Separator />
|
||||
<Workspaces />
|
||||
</box>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function Center() {
|
||||
return (
|
||||
<box>
|
||||
{/* <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 halign={Gtk.Align.CENTER}>
|
||||
<box>
|
||||
<menubutton>
|
||||
<label label={time()} />
|
||||
|
@ -56,11 +26,28 @@ export default function Bar(gdkmonitor: Gdk.Monitor) {
|
|||
</menubutton>
|
||||
</box>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
<box spacing={6} halign={Gtk.Align.END}>
|
||||
function Right() {
|
||||
return (
|
||||
<box halign={Gtk.Align.END}>
|
||||
<Brightness />
|
||||
<Volume />
|
||||
</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>
|
||||
</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