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