Compare commits

..
6 Commits
16 changed files with 1293 additions and 27 deletions
+1
View File
@@ -4,3 +4,4 @@ node_modules
ags/@girs/
ags-dots/
+56
View File
@@ -0,0 +1,56 @@
# Dotfiles
These are my omarchy dotfiles.
## Objective
I want you to make a couple additions and fixes to my config files.
## Developement and Coding Rules
You must follows these rule at all times
1. Do not add code comments
2. Do not run commands, I will test everything myself
3. Do not use emojies in documentation or the code
4. Keep code understandable and easy to read (name variables with easy to read names)
5. Do not give paragraph explanations unless asked.
## What I Want Done
1. I want you to add more widgets. I recently saw a dotfiles config with widgets for notification hub.
Look at ./ags-dots for ideas on how I was inspired. I want to keep my current omarchy theme and style.
2. I want a wallpaper selector like the in ./ags-dots. I want to be able to select a wallpaper from a widget, and it should change the wallpaper. This will replace the blob_wallpaper command.
3. I want to make a vercel website that I can upload a wallpaper, and it will generate a color theme for the wallpaper. It should work like my custom dynamic wallpaper generater. I want to be able to tweak and download the config files for the wallpaper. I want it to be responsive. Make it look modern and polished. Use Svelte 5 syntax and tailwindcss.
## My Laptop Specs
fastfetch
┌──────────────────────Hardware──────────────────────┐
█████████████████████████████████████████████████████  PC: 82Y0 (IdeaPad Flex 5 14IRU8)
█████████████████████████████████████████████████████ │ ├: 13th Gen Intel(R) Core(TM) i5-1335U (4+8) @ 4.60 GHz
████ ████ ████ │ ├: Intel Iris Xe Graphics @ 1.25 GHz [Integrated]
████ ████ ████ │ ├󱄄: 1920x1200 @ 1.5x in 14", 60 Hz [Built-in]
████ █████████████████████ ████████ ████ │ ├󰋊: 127.89 GiB / 213.19 GiB (60%) - ext4
████ █████████████████████ ████████ ████ │ ├: 3.62 GiB / 15.16 GiB (24%)
████ ████ ████ ████ └ └󰓡 : 0 B / 4.00 GiB (0%)
████ ████ ████ ████ └────────────────────────────────────────────────────┘
████ ████ ████ ████
████ ████ ████ ████ ┌──────────────────────Software──────────────────────┐
████ ████ ████ ████  OS: Omarchy 3.8.2
████ ████ ████ ████ │ ├󰘬: master
████████████ ████ ████ │ ├󰔫: stable
████████████ ████ ████ │ ├: Linux 7.0.9-arch2-1
████ ████ ████ ████ │ ├: Hyprland 0.55.2 (Wayland)
████ ████ ████ ████ │ ├: alacritty 0.17.0
████ ████ ████ ████ │ ├󰏖: 1250 (pacman)
████ ████ ████ ████ │ ├󰸌: Blob Dynamic ●●●●●●●●
████ ████ ████ ████ └ └: JetBrainsMono Nerd Font (9pt, Regular)
████ ████ ████ ████ └────────────────────────────────────────────────────┘
████ ██████████████████████████████████████ ████
████ ██████████████████████████████████████ ████ ┌────────────────Age / Uptime / Update───────────────┐
████ ████ ████ 󱦟 OS Age: 69 days
████ ████ ████ 󱫐 Uptime: 4 mins
████████████████████████████ ████████████████████  Update: Thursday, June 11 2026 at 13:13
████████████████████████████ ████████████████████ └────────────────────────────────────────────────────┘
+20
View File
@@ -1,10 +1,30 @@
import app from "ags/gtk3/app"
import style from "./style.css"
import Media from "./widget/Media"
import NotificationCenter from "./widget/Notifications"
import QuickSettings from "./widget/QuickSettings"
import SysMonitor from "./widget/SysMonitor"
import WallPicker from "./widget/WallPicker"
const SHOW_DESKTOP_MEDIA = false
function start(name: string, build: () => unknown) {
try {
build()
} catch (error) {
console.error(`Failed to start widget "${name}":`, error)
}
}
app.start({
css: style,
main() {
if (SHOW_DESKTOP_MEDIA) {
app.get_monitors().map(Media)
}
start("notification-center", NotificationCenter)
start("quick-settings", QuickSettings)
start("sys-monitor", SysMonitor)
start("wall-picker", WallPicker)
},
})
+17
View File
@@ -0,0 +1,17 @@
import app from "ags/gtk3/app"
import { execAsync } from "ags/process"
export function sh(command: string) {
return execAsync(["bash", "-c", command]).catch((error) => {
console.error(error)
return ""
})
}
export function shell(command: string): string[] {
return ["bash", "-c", command]
}
export function toggleWindow(name: string) {
app.toggle_window(name)
}
+382
View File
@@ -79,3 +79,385 @@
border: none;
box-shadow: none;
}
.NotificationCenter,
.QuickSettings,
.SysMonitor,
.Calendar,
.WallPicker {
background-color: transparent;
}
.panel {
background-color: alpha(@background, 0.92);
border-radius: 12px;
border: 2px solid alpha(@color4, 0.5);
padding: 14px;
font-family: "JetBrainsMono Nerd Font", sans-serif;
color: @foreground;
}
.panel-title {
color: @foreground;
font-size: 15px;
font-weight: bold;
}
.panel-header {
margin-bottom: 2px;
}
.panel-icon-btn {
color: @color5;
font-size: 13px;
padding: 3px 6px;
border-radius: 8px;
transition: all 0.2s;
}
.panel-icon-btn:hover {
background-color: alpha(@color4, 0.25);
color: @color4;
}
.panel-icon-btn.active {
background-color: @accent;
color: @background;
}
.notification-center {
min-width: 360px;
min-height: 420px;
}
.notification-scroll {
min-height: 360px;
}
.notification-item {
background-color: alpha(@color0, 0.6);
border-radius: 10px;
border-left: 3px solid @accent;
padding: 10px 12px;
}
.notification-item-app {
color: @accent;
font-size: 12px;
font-weight: bold;
}
.notification-item-close {
color: @color5;
font-size: 12px;
padding: 0 4px;
transition: all 0.2s;
}
.notification-item-close:hover {
color: @color1;
}
.notification-item-summary {
color: @foreground;
font-size: 13px;
font-weight: bold;
margin-top: 2px;
}
.notification-item-body {
color: @foreground;
font-size: 12px;
opacity: 0.8;
margin-top: 2px;
}
.notification-empty {
opacity: 0.6;
}
.notification-empty-icon {
font-size: 42px;
color: @color4;
margin-bottom: 8px;
}
.notification-empty-text {
font-size: 13px;
color: @foreground;
}
.quick-settings {
min-width: 320px;
}
.qs-tile {
background-color: alpha(@color0, 0.6);
border-radius: 10px;
padding: 8px 4px;
transition: all 0.2s;
}
.qs-tile:hover {
background-color: alpha(@color4, 0.25);
}
.qs-tile.active {
background-color: @accent;
color: @background;
}
.qs-tile-icon {
font-size: 15px;
}
.qs-tile-label {
font-size: 10px;
}
.qs-slider-row {
padding: 2px 4px;
}
.qs-slider-icon {
color: @color5;
font-size: 14px;
min-width: 20px;
}
.qs-slider trough {
min-height: 8px;
border-radius: 8px;
background-color: alpha(@foreground, 0.2);
}
.qs-slider highlight {
border-radius: 8px;
background-color: @accent;
}
.qs-slider slider {
background-color: @foreground;
border-radius: 50%;
min-width: 14px;
min-height: 14px;
margin: -4px;
}
.sys-monitor {
min-width: 320px;
}
.sys-metric-icon {
font-size: 16px;
color: @color5;
min-width: 22px;
}
.sys-metric-name {
font-size: 13px;
font-weight: bold;
}
.sys-metric-detail {
font-size: 12px;
opacity: 0.8;
}
.sys-metric-bar {
min-height: 8px;
}
.sys-metric-bar trough {
min-height: 8px;
border-radius: 8px;
background-color: alpha(@foreground, 0.2);
}
.sys-metric-bar block.filled {
border-radius: 8px;
background-color: @accent;
}
.calendar {
min-width: 280px;
}
.calendar-time {
font-size: 40px;
font-weight: bold;
color: @foreground;
}
.calendar-date {
font-size: 14px;
color: @accent;
}
.calendar-month {
font-size: 14px;
font-weight: bold;
color: @accent;
}
.calendar-days {
font-family: "JetBrainsMono Nerd Font", monospace;
font-size: 14px;
color: @foreground;
}
.wall-picker {
min-width: 680px;
min-height: 460px;
}
.wall-scroll {
min-height: 400px;
}
.wall-thumb {
padding: 0;
border-radius: 10px;
border: 2px solid transparent;
transition: all 0.2s;
}
.wall-thumb:hover {
border: 2px solid @accent;
}
.wall-thumb-image {
min-width: 200px;
min-height: 112px;
border-radius: 8px;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
.wall-empty {
opacity: 0.6;
}
.wall-empty-icon {
font-size: 42px;
color: @color4;
margin-bottom: 8px;
}
.wall-empty-text {
font-size: 13px;
}
.control-center {
min-width: 360px;
}
.cc-header {
margin-bottom: 2px;
}
.cc-uptime {
font-size: 13px;
color: @foreground;
}
.cc-uptime-icon {
color: @accent;
}
.cc-clock {
font-size: 22px;
font-weight: bold;
color: @foreground;
}
.cc-date {
font-size: 12px;
color: @accent;
}
.cc-media {
background-color: alpha(@color0, 0.6);
border-radius: 10px;
padding: 10px;
}
.cc-media-cover {
min-width: 84px;
min-height: 84px;
border-radius: 8px;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
.cc-media-info {
margin-left: 10px;
}
.cc-media-title {
font-size: 13px;
font-weight: bold;
color: @foreground;
}
.cc-media-artist {
font-size: 12px;
opacity: 0.8;
}
.cc-media-seek {
margin: 6px 0;
}
.cc-media-seek trough {
min-height: 4px;
border-radius: 4px;
background-color: alpha(@foreground, 0.2);
}
.cc-media-seek highlight {
border-radius: 4px;
background-color: @accent;
}
.cc-media-seek slider {
background-color: transparent;
min-width: 0;
min-height: 0;
border: none;
box-shadow: none;
}
.cc-media-controls button {
color: @color5;
font-size: 14px;
padding: 0 4px;
transition: all 0.2s;
}
.cc-media-controls button:hover {
color: @accent;
}
.cc-calendar {
margin-top: 2px;
}
.cc-calendar-month {
font-size: 13px;
font-weight: bold;
color: @accent;
}
.cc-calendar-time {
font-size: 13px;
color: @foreground;
}
.cc-calendar-grid {
font-family: "JetBrainsMono Nerd Font", monospace;
font-size: 13px;
color: @foreground;
}
+1 -1
View File
@@ -15,7 +15,7 @@ export default function Media(gdkmonitor: Gdk.Monitor) {
status: "Stopped",
length: 0,
position: 0
}, 1000, pollCmd, (stdout) => {
}, 1000, ["bash", "-c", pollCmd], (stdout) => {
const parts = stdout.split("|||");
const title = parts[0]?.trim() || "No Media";
const artist = parts[1]?.trim() || "";
+142
View File
@@ -0,0 +1,142 @@
import app from "ags/gtk3/app"
import { Astal, Gtk } from "ags/gtk3"
import { createPoll } from "ags/time"
import { For } from "ags"
import { sh, shell } from "../lib/utils"
export const NOTIFICATION_WINDOW = "notification-center"
type Notification = {
id: number
appName: string
summary: string
body: string
}
function parseNotifications(stdout: string): Notification[] {
try {
const parsed = JSON.parse(stdout)
const group = parsed?.data?.[0] ?? []
return group.map((item: any) => ({
id: item.id?.value ?? 0,
appName: item["app-name"]?.value ?? "",
summary: item.summary?.value ?? "",
body: item.body?.value ?? "",
}))
} catch {
return []
}
}
const notifications = createPoll<Notification[]>(
[],
1000,
shell("makoctl list -j 2>/dev/null || echo '{}'"),
(stdout) => parseNotifications(stdout),
)
const doNotDisturb = createPoll(
false,
1000,
shell("makoctl mode 2>/dev/null"),
(stdout) => stdout.split("\n").includes("do-not-disturb"),
)
const dismiss = (id: number) => sh(`makoctl dismiss -n ${id}`)
const clearAll = () => sh("makoctl dismiss -a")
const toggleDoNotDisturb = () => sh("makoctl mode -t do-not-disturb")
function NotificationItem({ item }: { item: Notification }) {
return (
<box class="notification-item" vertical>
<box class="notification-item-header">
<label
class="notification-item-app"
label={item.appName || "Notification"}
xalign={0}
halign={Gtk.Align.START}
hexpand
/>
<button
class="notification-item-close"
halign={Gtk.Align.END}
onClicked={() => dismiss(item.id)}
>
<label label={""} />
</button>
</box>
<label
class="notification-item-summary"
label={item.summary}
xalign={0}
halign={Gtk.Align.START}
wrap
maxWidthChars={34}
/>
<label
class="notification-item-body"
visible={item.body.length > 0}
label={item.body}
xalign={0}
halign={Gtk.Align.START}
wrap
maxWidthChars={34}
/>
</box>
)
}
export default function NotificationCenter() {
const { TOP, RIGHT } = Astal.WindowAnchor
const hasNotifications = notifications.as((list) => list.length > 0)
const isEmpty = notifications.as((list) => list.length === 0)
return (
<window
name={NOTIFICATION_WINDOW}
namespace={NOTIFICATION_WINDOW}
class="NotificationCenter"
anchor={TOP | RIGHT}
margin={10}
layer={Astal.Layer.OVERLAY}
exclusivity={Astal.Exclusivity.NORMAL}
visible={false}
application={app}
>
<box class="panel notification-center" vertical spacing={10}>
<box class="panel-header" spacing={8}>
<label class="panel-title" label="Notifications" xalign={0} hexpand halign={Gtk.Align.START} />
<button
class={doNotDisturb.as((on) => (on ? "panel-icon-btn active" : "panel-icon-btn"))}
tooltipText="Do not disturb"
onClicked={toggleDoNotDisturb}
>
<label label={doNotDisturb.as((on) => (on ? "" : ""))} />
</button>
<button class="panel-icon-btn" tooltipText="Clear all" onClicked={clearAll}>
<label label={""} />
</button>
</box>
<scrollable
class="notification-scroll"
visible={hasNotifications}
vexpand
hscroll={Gtk.PolicyType.NEVER}
vscroll={Gtk.PolicyType.AUTOMATIC}
>
<box vertical spacing={8}>
<For each={notifications} id={(item: Notification) => item.id}>
{(item: Notification) => <NotificationItem item={item} />}
</For>
</box>
</scrollable>
<box class="notification-empty" visible={isEmpty} vertical valign={Gtk.Align.CENTER} vexpand>
<label class="notification-empty-icon" label={""} halign={Gtk.Align.CENTER} />
<label class="notification-empty-text" label="You're all caught up" halign={Gtk.Align.CENTER} />
</box>
</box>
</window>
)
}
+334
View File
@@ -0,0 +1,334 @@
import app from "ags/gtk3/app"
import { Astal, Gtk } from "ags/gtk3"
import { createState } from "ags"
import { createPoll, interval } from "ags/time"
import { sh, shell } from "../lib/utils"
export const QUICKSETTINGS_WINDOW = "quick-settings"
const closePanel = () => app.toggle_window(QUICKSETTINGS_WINDOW)
function pollBool(command: string, intervalMs: number) {
const [value, setValue] = createState(false)
const refresh = () => sh(command).then((out) => setValue(out.trim() === "on"))
refresh()
interval(intervalMs, refresh)
return [value, setValue] as const
}
const volume = createPoll(
0,
1000,
shell("pamixer --get-volume 2>/dev/null || echo 0"),
(stdout) => Number(stdout.trim()) || 0,
)
const [muted, setMuted] = pollBool(
"pamixer --get-mute 2>/dev/null | grep -qx true && echo on || echo off",
1000,
)
const brightness = createPoll(
0,
2000,
shell("brightnessctl -m 2>/dev/null | cut -d, -f4 | tr -d '%' || echo 0"),
(stdout) => Number(stdout.trim()) || 0,
)
const [bluetoothOn] = pollBool(
"bluetoothctl show 2>/dev/null | grep -q 'Powered: yes' && echo on || echo off",
2000,
)
const [doNotDisturb, setDoNotDisturb] = pollBool(
"makoctl mode 2>/dev/null | grep -qx do-not-disturb && echo on || echo off",
2000,
)
const [nightLight, setNightLight] = pollBool(
"pgrep -x hyprsunset >/dev/null && echo on || echo off",
2000,
)
const clock = createPoll("", 1000, shell("date '+%H:%M'"), (stdout) => stdout.trim())
const today = createPoll("", 10000, shell("date '+%A, %B %-d'"), (stdout) => stdout.trim())
type Media = {
title: string
artist: string
artUrl: string
status: string
length: number
position: number
}
const mediaCommand =
"playerctl metadata -f '{{title}}|||{{artist}}|||{{mpris:artUrl}}|||{{status}}|||{{mpris:length}}|||{{position}}' " +
"2>/dev/null || echo 'No Media||||||Stopped|||0|||0'"
const media = createPoll<Media>(
{ title: "No Media", artist: "", artUrl: "", status: "Stopped", length: 0, position: 0 },
1000,
shell(mediaCommand),
(stdout) => {
const parts = stdout.split("|||")
return {
title: parts[0]?.trim() || "No Media",
artist: parts[1]?.trim() || "",
artUrl: (parts[2]?.trim() || "").replace(/^file:\/\//, ""),
status: parts[3]?.trim() || "Stopped",
length: Number(parts[4]) || 0,
position: Number(parts[5]) || 0,
}
},
)
function Header() {
return (
<box class="cc-header" spacing={8}>
<box class="cc-datetime" vertical hexpand halign={Gtk.Align.START}>
<label class="cc-clock" label={clock} xalign={0} halign={Gtk.Align.START} />
<label class="cc-date" label={today} xalign={0} halign={Gtk.Align.START} />
</box>
<button class="panel-icon-btn" tooltipText="Lock" onClicked={() => { closePanel(); sh("omarchy-system-lock") }}>
<label label={""} />
</button>
<button class="panel-icon-btn" tooltipText="Log out" onClicked={() => { closePanel(); sh("omarchy-system-logout") }}>
<label label={""} />
</button>
<button class="panel-icon-btn" tooltipText="Close" onClicked={closePanel}>
<label label={"\uf00d"} />
</button>
</box>
)
}
function Tile({
icon,
label,
active,
onClicked,
}: {
icon: string
label: string
active?: any
onClicked: () => void
}) {
return (
<button
class={active ? active.as((on: boolean) => (on ? "qs-tile active" : "qs-tile")) : "qs-tile"}
onClicked={onClicked}
hexpand
>
<box vertical spacing={4} halign={Gtk.Align.CENTER}>
<label class="qs-tile-icon" label={icon} />
<label class="qs-tile-label" label={label} />
</box>
</button>
)
}
function Toggles() {
return (
<box vertical spacing={8}>
<box class="qs-tiles" spacing={8} homogeneous>
<Tile icon={""} label="Wi-Fi" onClicked={() => { closePanel(); sh("omarchy-launch-wifi") }} />
<Tile
icon={""}
label="Bluetooth"
active={bluetoothOn}
onClicked={() => {
closePanel()
sh("omarchy-launch-bluetooth")
}}
/>
<Tile
icon={""}
label="Silence"
active={doNotDisturb}
onClicked={() => {
setDoNotDisturb(!doNotDisturb.get())
sh("makoctl mode -t do-not-disturb")
}}
/>
</box>
<box class="qs-tiles" spacing={8} homogeneous>
<Tile
icon={""}
label="Night Light"
active={nightLight}
onClicked={() => {
setNightLight(!nightLight.get())
sh("omarchy-toggle-nightlight")
}}
/>
<Tile icon={""} label="Record" onClicked={() => { closePanel(); sh("omarchy-capture-screenrecording") }} />
<Tile icon={""} label="Pick Color" onClicked={() => { closePanel(); sh("hyprpicker -a") }} />
</box>
</box>
)
}
function VolumeSlider() {
return (
<box class="qs-slider-row" spacing={10}>
<button
class="qs-slider-icon"
onClicked={() => {
setMuted(!muted.get())
sh("pamixer -t")
}}
tooltipText="Toggle mute"
>
<label label={muted.as((m) => (m ? "" : ""))} />
</button>
<slider
class="qs-slider"
hexpand
min={0}
max={100}
step={1}
value={volume}
$={(self) => {
self.connect("value-changed", () => {
if (self.dragging) {
sh(`pamixer --set-volume ${Math.round(self.value)}`)
}
})
}}
/>
</box>
)
}
function BrightnessSlider() {
return (
<box class="qs-slider-row" spacing={10}>
<label class="qs-slider-icon" label={""} />
<slider
class="qs-slider"
hexpand
min={1}
max={100}
step={1}
value={brightness}
$={(self) => {
self.connect("value-changed", () => {
if (self.dragging) {
sh(`brightnessctl set ${Math.round(self.value)}%`)
}
})
}}
/>
</box>
)
}
function MediaPlayer() {
return (
<box class="cc-media" spacing={12} visible={media.as((m) => m.title !== "No Media")}>
<box
class="cc-media-cover"
css={media.as((m) =>
m.artUrl
? `background-image: url('${m.artUrl}');`
: "background-color: alpha(@color0, 0.5);",
)}
/>
<box class="cc-media-info" vertical valign={Gtk.Align.CENTER} hexpand>
<label
class="cc-media-title"
label={media.as((m) => m.title)}
xalign={0}
halign={Gtk.Align.START}
truncate
maxWidthChars={22}
/>
<label
class="cc-media-artist"
label={media.as((m) => m.artist)}
xalign={0}
halign={Gtk.Align.START}
truncate
maxWidthChars={26}
/>
<slider
class="cc-media-seek"
hexpand
min={0}
max={1}
step={0.01}
value={media.as((m) => {
const seconds = m.length / 1000000
return seconds > 0 ? Math.min(m.position / seconds, 1) : 0
})}
$={(self) => {
self.connect("value-changed", () => {
if (self.dragging) {
const seconds = media.get().length / 1000000
sh(`playerctl position ${Math.round(self.value * seconds)}`)
}
})
}}
/>
<box class="cc-media-controls" spacing={14} halign={Gtk.Align.CENTER}>
<button onClicked={() => sh("playerctl previous")}>
<label label={""} />
</button>
<button onClicked={() => sh("playerctl play-pause")}>
<label label={media.as((m) => (m.status === "Playing" ? "" : ""))} />
</button>
<button onClicked={() => sh("playerctl next")}>
<label label={""} />
</button>
</box>
</box>
</box>
)
}
const monthName = createPoll("", 60000, shell("date '+%B %Y'"), (stdout) => stdout.trim())
const monthGrid = createPoll(
"",
60000,
shell("cal | sed '1d'"),
(stdout) => stdout.replace(/\s+$/, ""),
)
function CalendarSection() {
return (
<box class="cc-calendar" vertical spacing={6}>
<box class="cc-calendar-head" spacing={8}>
<label class="cc-calendar-month" label={monthName} xalign={0} hexpand halign={Gtk.Align.START} />
</box>
<label class="cc-calendar-grid" label={monthGrid} halign={Gtk.Align.CENTER} />
</box>
)
}
export default function QuickSettings() {
const { TOP } = Astal.WindowAnchor
return (
<window
name={QUICKSETTINGS_WINDOW}
namespace={QUICKSETTINGS_WINDOW}
class="QuickSettings"
anchor={TOP}
margin={10}
layer={Astal.Layer.OVERLAY}
exclusivity={Astal.Exclusivity.NORMAL}
visible={false}
application={app}
>
<box class="panel quick-settings control-center" vertical spacing={12}>
<Header />
<CalendarSection />
<Toggles />
<VolumeSlider />
<BrightnessSlider />
<MediaPlayer />
</box>
</window>
)
}
+137
View File
@@ -0,0 +1,137 @@
import app from "ags/gtk3/app"
import { Astal, Gtk } from "ags/gtk3"
import { createPoll } from "ags/time"
import { shell } from "../lib/utils"
export const SYSMONITOR_WINDOW = "sys-monitor"
let previousIdle = 0
let previousTotal = 0
const cpuUsage = createPoll(
0,
2000,
shell("grep '^cpu ' /proc/stat"),
(stdout) => {
const values = stdout.trim().split(/\s+/).slice(1).map(Number)
const idle = (values[3] || 0) + (values[4] || 0)
const total = values.reduce((sum, value) => sum + value, 0)
const idleDelta = idle - previousIdle
const totalDelta = total - previousTotal
previousIdle = idle
previousTotal = total
if (totalDelta <= 0) return 0
return Math.round((1 - idleDelta / totalDelta) * 100)
},
)
type Usage = { percent: number; used: string; total: string }
const memory = createPoll<Usage>(
{ percent: 0, used: "0", total: "0" },
2000,
shell("free -m | awk '/^Mem:/ {print $3\" \"$2}'"),
(stdout) => {
const [used, total] = stdout.trim().split(/\s+/).map(Number)
if (!total) return { percent: 0, used: "0", total: "0" }
return {
percent: Math.round((used / total) * 100),
used: `${(used / 1024).toFixed(1)}G`,
total: `${(total / 1024).toFixed(1)}G`,
}
},
)
const disk = createPoll<Usage>(
{ percent: 0, used: "0", total: "0" },
30000,
shell("df -h --output=pcent,used,size / | tail -1"),
(stdout) => {
const [percent, used, total] = stdout.trim().split(/\s+/)
return {
percent: Number(percent.replace("%", "")) || 0,
used: used || "0",
total: total || "0",
}
},
)
const temperature = createPoll(
0,
2000,
shell("cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null || echo 0"),
(stdout) => Math.round(Number(stdout.trim()) / 1000) || 0,
)
function Metric({
icon,
name,
percent,
detail,
}: {
icon: string
name: string
percent: any
detail: any
}) {
return (
<box class="sys-metric" vertical spacing={4}>
<box spacing={8}>
<label class="sys-metric-icon" label={icon} />
<label class="sys-metric-name" label={name} xalign={0} halign={Gtk.Align.START} hexpand />
<label class="sys-metric-detail" label={detail} halign={Gtk.Align.END} />
</box>
<levelbar
class="sys-metric-bar"
value={percent.as((value: number) => value / 100)}
/>
</box>
)
}
export default function SysMonitor() {
const { TOP, RIGHT } = Astal.WindowAnchor
return (
<window
name={SYSMONITOR_WINDOW}
namespace={SYSMONITOR_WINDOW}
class="SysMonitor"
anchor={TOP | RIGHT}
margin={10}
layer={Astal.Layer.OVERLAY}
exclusivity={Astal.Exclusivity.NORMAL}
visible={false}
application={app}
>
<box class="panel sys-monitor" vertical spacing={12}>
<label class="panel-title" label="System Monitor" xalign={0} halign={Gtk.Align.START} />
<Metric
icon={"\udb80\udf5b"}
name="CPU"
percent={cpuUsage}
detail={cpuUsage.as((value) => `${value}%`)}
/>
<Metric
icon={"\udb81\ude1a"}
name="Memory"
percent={memory.as((value) => value.percent)}
detail={memory.as((value) => `${value.used} / ${value.total}`)}
/>
<Metric
icon={"\udb80\udeca"}
name="Disk"
percent={disk.as((value) => value.percent)}
detail={disk.as((value) => `${value.used} / ${value.total}`)}
/>
<Metric
icon={"\uf2c9"}
name="Temperature"
percent={temperature}
detail={temperature.as((value) => `${value}°C`)}
/>
</box>
</window>
)
}
+106
View File
@@ -0,0 +1,106 @@
import app from "ags/gtk3/app"
import { Astal, Gtk } from "ags/gtk3"
import { createPoll } from "ags/time"
import { For } from "ags"
import { sh, shell } from "../lib/utils"
export const WALLPICKER_WINDOW = "wall-picker"
const COLUMNS = 3
type Wallpaper = { name: string; path: string }
const findCommand =
"find -L \"$HOME/wallpapers\" -maxdepth 1 -type f " +
"\\( -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.webp' -o -iname '*.gif' \\) " +
"2>/dev/null | sort"
const wallpapers = createPoll<Wallpaper[]>(
[],
5000,
shell(findCommand),
(stdout) =>
stdout
.split("\n")
.filter((line) => line.length > 0)
.map((path) => ({ path, name: path.split("/").pop() ?? path })),
)
const rows = wallpapers.as((list) => {
const chunks: Wallpaper[][] = []
for (let index = 0; index < list.length; index += COLUMNS) {
chunks.push(list.slice(index, index + COLUMNS))
}
return chunks
})
const setWallpaper = (path: string) => {
app.toggle_window(WALLPICKER_WINDOW)
sh(`"$HOME/scripts/blob_wallpaper.sh" "${path}"`)
}
function Thumbnail({ wall }: { wall: Wallpaper }) {
return (
<button class="wall-thumb" onClicked={() => setWallpaper(wall.path)} tooltipText={wall.name}>
<box
class="wall-thumb-image"
css={`background-image: url('${wall.path}');`}
/>
</button>
)
}
export default function WallPicker() {
const hasWalls = wallpapers.as((list) => list.length > 0)
const isEmpty = wallpapers.as((list) => list.length === 0)
return (
<window
name={WALLPICKER_WINDOW}
namespace={WALLPICKER_WINDOW}
class="WallPicker"
layer={Astal.Layer.OVERLAY}
keymode={Astal.Keymode.ON_DEMAND}
exclusivity={Astal.Exclusivity.NORMAL}
visible={false}
application={app}
>
<box class="panel wall-picker" vertical spacing={12}>
<box class="panel-header" spacing={8}>
<label class="panel-title" label="Wallpapers" xalign={0} hexpand halign={Gtk.Align.START} />
<button
class="panel-icon-btn"
tooltipText="Close"
onClicked={() => app.toggle_window(WALLPICKER_WINDOW)}
>
<label label={""} />
</button>
</box>
<scrollable
class="wall-scroll"
visible={hasWalls}
hscroll={Gtk.PolicyType.NEVER}
vscroll={Gtk.PolicyType.AUTOMATIC}
>
<box vertical spacing={8}>
<For each={rows} id={(row: Wallpaper[]) => row.map((wall) => wall.name).join("|")}>
{(row: Wallpaper[]) => (
<box spacing={8} halign={Gtk.Align.CENTER}>
{row.map((wall) => (
<Thumbnail wall={wall} />
))}
</box>
)}
</For>
</box>
</scrollable>
<box class="wall-empty" visible={isEmpty} vertical valign={Gtk.Align.CENTER}>
<label class="wall-empty-icon" label={""} halign={Gtk.Align.CENTER} />
<label class="wall-empty-text" label="No wallpapers in ~/wallpapers" halign={Gtk.Align.CENTER} />
</box>
</box>
</window>
)
}
+1
View File
@@ -27,6 +27,7 @@ bindd = SUPER SHIFT, X, X, exec, omarchy-launch-webapp "https://x.com/"
bindd = SUPER SHIFT ALT, X, X Post, exec, omarchy-launch-webapp "https://x.com/compose/post"
# Add extra bindings
bindd = SUPER ALT, W, Wallpaper picker, exec, ags toggle wall-picker
# bind = SUPER SHIFT, R, exec, alacritty -e ssh your-server
# Overwrite existing bindings, like putting Omarchy Menu on Super + Space
+5 -5
View File
@@ -1,28 +1,28 @@
general {
lock_cmd = omarchy-lock-screen # lock screen and 1password
lock_cmd = omarchy-system-lock # lock screen and 1password
before_sleep_cmd = loginctl lock-session # lock before suspend.
after_sleep_cmd = sleep 1 && hyprctl dispatch dpms on # delay for PAM readiness, then turn on display.
inhibit_sleep = 3 # wait until screen is locked
}
listener {
timeout = 150 # 2.5min
timeout = 300 # 5min
on-timeout = pidof hyprlock || omarchy-launch-screensaver # start screensaver (if we haven't locked already)
}
listener {
timeout = 151 # 5min
timeout = 301 # 5min
on-timeout = loginctl lock-session # lock screen when timeout has passed
}
listener {
timeout = 330 # 5.5min
timeout = 480 # 8min
on-timeout = brightnessctl -sd '*::kbd_backlight' set 0 # save state and turn off keyboard backlight
on-resume = brightnessctl -rd '*::kbd_backlight' # restore keyboard backlight
}
listener {
timeout = 330 # 5.5min
timeout = 480 # 8min
on-timeout = hyprctl dispatch dpms off # screen off when timeout has passed
on-resume = hyprctl dispatch dpms on && brightnessctl -r # screen on when activity is detected
}
+63
View File
@@ -0,0 +1,63 @@
#!/bin/bash
# Pull a color theme shared from the wall-styles website and apply it.
THEME_DIR="$HOME/.config/omarchy/themes/blob-dynamic"
# Used only when a bare id is passed instead of a full share link.
# Override with: export BLOB_THEME_URL="https://your-deployment.vercel.app"
BASE_URL="${BLOB_THEME_URL:-https://wall-styles.vercel.app}"
if [ -z "$1" ]; then
echo "Usage: blob_theme <share-link-or-id>"
echo "Example: blob_theme \"https://wall-styles.vercel.app/?id=ab12cd34ef\""
exit 1
fi
INPUT="$1"
if [[ "$INPUT" == http*://* ]]; then
HOST=$(echo "$INPUT" | sed -E 's#^(https?://[^/]+).*#\1#')
ID=$(echo "$INPUT" | grep -oE 'id=[A-Za-z0-9]+' | head -1 | cut -d= -f2)
if [ -z "$ID" ]; then
echo "Error: no theme id found in link '$INPUT'."
exit 1
fi
API="$HOST/api/theme?id=$ID&format=toml"
else
API="$BASE_URL/api/theme?id=$INPUT&format=toml"
fi
mkdir -p "$THEME_DIR/backgrounds"
echo "Fetching theme..."
TMP=$(mktemp)
if ! curl -fsSL "$API" -o "$TMP"; then
echo "Error: could not fetch theme. The link may be expired (themes last one hour)."
rm -f "$TMP"
exit 1
fi
if ! grep -q '^background = ' "$TMP"; then
echo "Error: the response did not look like a valid colors.toml."
rm -f "$TMP"
exit 1
fi
mv "$TMP" "$THEME_DIR/colors.toml"
cat <<EOF > "$THEME_DIR/neovim.lua"
return {
{
"LazyVim/LazyVim",
opts = {
colorscheme = "tokyonight",
},
},
}
EOF
# Apply the Blob-Dynamic theme (reloads waybar, ags, etc.)
omarchy-theme-set "blob-dynamic"
echo "Applied shared theme to blob-dynamic."
+5 -1
View File
@@ -8,7 +8,11 @@ mkdir -p "$WALLPAPER_DIR"
mkdir -p "$THEME_DIR/backgrounds"
if [ -z "$1" ]; then
# Use walker dmenu for GUI selection
# Open the AGS wallpaper selector widget
ags toggle wall-picker
exit 0
elif [ "$1" = "--menu" ]; then
# Fallback: walker dmenu selection
omarchy-launch-walker -m menus:blobBackgroundSelector --width 800 --minheight 400 -p "Select Wallpaper…"
exit 0
else
+14 -17
View File
@@ -22,20 +22,14 @@
"network",
"pulseaudio",
"cpu",
"memory",
"disk",
"battery"
"battery",
"custom/notification"
],
"memory": {
"interval": 10,
"format": "\udb81\ude1a",
"tooltip-format": "Memory: {used:0.1f}G/{total:0.1f}G"
},
"disk": {
"interval": 30,
"format": "\udb80\udeca",
"path": "/",
"tooltip-format": "Disk: {used}/{total}"
"custom/notification": {
"format": "",
"tooltip-format": "Notifications",
"on-click": "ags toggle notification-center",
"on-click-right": "omarchy-toggle-notification-silencing"
},
"hyprland/workspaces": {
"on-click": "activate",
@@ -84,12 +78,13 @@
"interval": 5,
"format": "\udb80\udf5b",
"on-click": "omarchy-launch-or-focus-tui btop",
"on-click-right": "alacritty"
"on-click-middle": "alacritty",
"on-click-right": "ags toggle sys-monitor"
},
"clock": {
"format": "{:L%A %d %B %H:%M}",
"format-alt": "{:L%d %B W%V %Y}",
"tooltip": false,
"on-click": "ags toggle quick-settings",
"on-click-right": "omarchy-launch-floating-terminal-with-presentation omarchy-tz-select"
},
"network": {
@@ -109,7 +104,8 @@
"tooltip-format-disconnected": "Disconnected",
"interval": 3,
"spacing": 1,
"on-click": "omarchy-launch-wifi"
"on-click": "omarchy-launch-wifi",
"on-click-right": "ags toggle quick-settings"
},
"battery": {
"format": "{capacity}% {icon}",
@@ -159,7 +155,8 @@
"format-connected": "\udb80\udcb1",
"format-no-controller": "",
"tooltip-format": "Devices connected: {num_connections}",
"on-click": "omarchy-launch-bluetooth"
"on-click": "omarchy-launch-bluetooth",
"on-click-right": "ags toggle quick-settings"
},
"pulseaudio": {
"format": "{icon}",
+6
View File
@@ -112,3 +112,9 @@ tooltip {
#custom-notification-silencing-indicator.active {
color: #a55555;
}
#custom-notification {
min-width: 16px;
margin-left: 7.5px;
margin-right: 10px;
}