diff --git a/.vscode/settings.json b/.vscode/settings.json
index 91bc4f5..d2b39dc 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -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"
-}
\ No newline at end of file
+ "path": "systemd-run"
+ }
+ },
+ "typescript.tsdk": "./node_modules/typescript/lib"
+}
diff --git a/app.ts b/app.ts
index fa92351..16c0eca 100644
--- a/app.ts
+++ b/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);
- },
-});
\ No newline at end of file
+ css: style,
+ main() {
+ App.get_monitors().map(Bar)
+ }
+})
diff --git a/biome.jsonc b/biome.jsonc
new file mode 100644
index 0000000..2dd09dd
--- /dev/null
+++ b/biome.jsonc
@@ -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"]
+ }
+ ]
+}
diff --git a/env.d.ts b/env.d.ts
index 467c0a4..fd9ba09 100644
--- a/env.d.ts
+++ b/env.d.ts
@@ -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
}
diff --git a/windows/Bar/components/Brightness.tsx b/windows/Bar/components/Brightness.tsx
new file mode 100644
index 0000000..a27909c
--- /dev/null
+++ b/windows/Bar/components/Brightness.tsx
@@ -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 (
+ handleScroll(dy)}
+ onButtonReleased={(_, button) => handleButtonReleased(button)}
+ cssClasses={['brightness']}
+ spacing={2}>
+
+
+ )
+}
diff --git a/windows/Bar/components/Launcher.tsx b/windows/Bar/components/Launcher.tsx
index df4205e..766ec96 100644
--- a/windows/Bar/components/Launcher.tsx
+++ b/windows/Bar/components/Launcher.tsx
@@ -1,10 +1,9 @@
-
-import { GLib } from "astal";
+import { GLib } from 'astal'
export default function Launcher() {
- return (
-
- );
-}
\ No newline at end of file
+ return (
+
+ )
+}
diff --git a/windows/Bar/components/Volume.tsx b/windows/Bar/components/Volume.tsx
index 0509d71..b3a3f35 100644
--- a/windows/Bar/components/Volume.tsx
+++ b/windows/Bar/components/Volume.tsx
@@ -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 (
-
- dy < 0 ? (speaker.volume += 0.01) : (speaker.volume += -0.01)
- }
- spacing={2}
- >
-
-
- );
-}
\ No newline at end of file
+ return (
+ (dy < 0 ? (speaker.volume += 0.01) : (speaker.volume += -0.01))} spacing={2}>
+
+
+ )
+}
diff --git a/windows/Bar/components/Workspaces.tsx b/windows/Bar/components/Workspaces.tsx
index b4f7936..122b82e 100644
--- a/windows/Bar/components/Workspaces.tsx
+++ b/windows/Bar/components/Workspaces.tsx
@@ -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 (
-
- {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) => (
-
- ))
- )}
-
- );
-}
\ No newline at end of file
+ return (
+
+ {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) => (
+
+ ))
+ )}
+
+ )
+}
diff --git a/windows/Bar/components/index.tsx b/windows/Bar/components/index.tsx
index 06970ae..b7e01b1 100644
--- a/windows/Bar/components/index.tsx
+++ b/windows/Bar/components/index.tsx
@@ -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'
diff --git a/windows/Bar/index.tsx b/windows/Bar/index.tsx
index b21181c..1394454 100644
--- a/windows/Bar/index.tsx
+++ b/windows/Bar/index.tsx
@@ -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("").poll(1000, () => GLib.DateTime.new_now_local().format("%H:%M - %A %e.")!);
+const time = Variable('').poll(1000, () => GLib.DateTime.new_now_local().format('%H:%M - %A %e.')!)
function Left() {
- return (
-
-
-
-
-
- );
+ return (
+
+
+
+
+ )
}
function Center() {
- return (
-
- {/* */}
-
- );
+ return (
+
+
+
+
+
+
+
+
+
+
+ )
}
function Right() {
- return <>>;
+ return (
+
+
+
+
+ )
}
-
export default function Bar(gdkmonitor: Gdk.Monitor) {
- const { TOP, LEFT, RIGHT } = Astal.WindowAnchor;
+ const { TOP, LEFT, RIGHT } = Astal.WindowAnchor
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
\ No newline at end of file
+ return (
+
+
+
+
+
+
+
+ )
+}
diff --git a/windows/Bar/services/brightness.ts b/windows/Bar/services/brightness.ts
new file mode 100644
index 0000000..563ee95
--- /dev/null
+++ b/windows/Bar/services/brightness.ts
@@ -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
+}