Compare commits
No commits in common. "8746fa0a6ecc5d5ce59b25d2167020bc5aee125e" and "8116060e57596a0f840aa1875ed3a70a2a7152d5" have entirely different histories.
8746fa0a6e
...
8116060e57
12 changed files with 144 additions and 400 deletions
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
|
@ -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, biome.jsonc"
|
"package.json": " pnpm-lock.yaml, tsconfig.json, .gitignore"
|
||||||
},
|
},
|
||||||
"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
12
app.ts
|
@ -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
115
biome.jsonc
|
@ -1,115 +0,0 @@
|
||||||
{
|
|
||||||
"$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 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
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,7 @@
|
||||||
ags-watch = pkgs.writeScriptBin "ags-watch" ''
|
ags-watch = pkgs.writeScriptBin "ags-watch" ''
|
||||||
#!${pkgs.fish}/bin/fish
|
#!${pkgs.fish}/bin/fish
|
||||||
|
|
||||||
ls **.{tsx,ts,scss,css} | ${pkgs.lib.getExe pkgs.entr} -r ags run -d ./ --gtk4
|
ls **.tsx | ${pkgs.lib.getExe pkgs.entr} -r ags run -d ./ --gtk4
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,48 +0,0 @@
|
||||||
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,9 +1,10 @@
|
||||||
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>
|
||||||
)
|
);
|
||||||
}
|
}
|
|
@ -1,13 +1,20 @@
|
||||||
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 onScroll={(_, __, dy) => (dy < 0 ? (speaker.volume += 0.01) : (speaker.volume += -0.01))} spacing={2}>
|
<box
|
||||||
<image iconName={bind(speaker, 'volumeIcon')} />
|
onScroll={(_, __, dy) =>
|
||||||
<label label={bind(speaker, 'volume').as((p) => `${Math.floor(p * 100)}%`)} />
|
dy < 0 ? (speaker.volume += 0.01) : (speaker.volume += -0.01)
|
||||||
</box>
|
}
|
||||||
)
|
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 { 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>
|
||||||
)
|
);
|
||||||
}
|
}
|
|
@ -1,4 +1,3 @@
|
||||||
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'
|
|
||||||
|
|
|
@ -1,22 +1,52 @@
|
||||||
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, Brightness } from './components'
|
import { Volume, Workspaces, Launcher } 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 halign={Gtk.Align.CENTER}>
|
<box>
|
||||||
<Launcher />
|
<Launcher />
|
||||||
|
<Gtk.Separator />
|
||||||
<Workspaces />
|
<Workspaces />
|
||||||
</box>
|
</box>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Center() {
|
function Center() {
|
||||||
return (
|
return (
|
||||||
<box halign={Gtk.Align.CENTER}>
|
<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>
|
<box>
|
||||||
<menubutton>
|
<menubutton>
|
||||||
<label label={time()} />
|
<label label={time()} />
|
||||||
|
@ -26,28 +56,11 @@ function Center() {
|
||||||
</menubutton>
|
</menubutton>
|
||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Right() {
|
<box spacing={6} halign={Gtk.Align.END}>
|
||||||
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>
|
||||||
)
|
);
|
||||||
}
|
}
|
|
@ -1,113 +0,0 @@
|
||||||
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