Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df72345a5c | ||
|
|
f1c3dca364 | ||
|
|
be14a1a730 | ||
|
|
04f074b71c | ||
|
|
0cf0bababc | ||
|
|
1fe518ff2b |
@@ -4,3 +4,4 @@ node_modules
|
|||||||
|
|
||||||
|
|
||||||
ags/@girs/
|
ags/@girs/
|
||||||
|
ags-dots/
|
||||||
@@ -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
|
||||||
|
████████████████████████████ ████████████████████ └────────────────────────────────────────────────────┘
|
||||||
+21
-1
@@ -1,10 +1,30 @@
|
|||||||
import app from "ags/gtk3/app"
|
import app from "ags/gtk3/app"
|
||||||
import style from "./style.css"
|
import style from "./style.css"
|
||||||
import Media from "./widget/Media"
|
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({
|
app.start({
|
||||||
css: style,
|
css: style,
|
||||||
main() {
|
main() {
|
||||||
app.get_monitors().map(Media)
|
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)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -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
@@ -79,3 +79,385 @@
|
|||||||
border: none;
|
border: none;
|
||||||
box-shadow: 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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export default function Media(gdkmonitor: Gdk.Monitor) {
|
|||||||
status: "Stopped",
|
status: "Stopped",
|
||||||
length: 0,
|
length: 0,
|
||||||
position: 0
|
position: 0
|
||||||
}, 1000, pollCmd, (stdout) => {
|
}, 1000, ["bash", "-c", pollCmd], (stdout) => {
|
||||||
const parts = stdout.split("|||");
|
const parts = stdout.split("|||");
|
||||||
const title = parts[0]?.trim() || "No Media";
|
const title = parts[0]?.trim() || "No Media";
|
||||||
const artist = parts[1]?.trim() || "";
|
const artist = parts[1]?.trim() || "";
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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"
|
bindd = SUPER SHIFT ALT, X, X Post, exec, omarchy-launch-webapp "https://x.com/compose/post"
|
||||||
|
|
||||||
# Add extra bindings
|
# Add extra bindings
|
||||||
|
bindd = SUPER ALT, W, Wallpaper picker, exec, ags toggle wall-picker
|
||||||
# bind = SUPER SHIFT, R, exec, alacritty -e ssh your-server
|
# bind = SUPER SHIFT, R, exec, alacritty -e ssh your-server
|
||||||
|
|
||||||
# Overwrite existing bindings, like putting Omarchy Menu on Super + Space
|
# Overwrite existing bindings, like putting Omarchy Menu on Super + Space
|
||||||
|
|||||||
+5
-5
@@ -1,28 +1,28 @@
|
|||||||
general {
|
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.
|
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.
|
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
|
inhibit_sleep = 3 # wait until screen is locked
|
||||||
}
|
}
|
||||||
|
|
||||||
listener {
|
listener {
|
||||||
timeout = 150 # 2.5min
|
timeout = 300 # 5min
|
||||||
on-timeout = pidof hyprlock || omarchy-launch-screensaver # start screensaver (if we haven't locked already)
|
on-timeout = pidof hyprlock || omarchy-launch-screensaver # start screensaver (if we haven't locked already)
|
||||||
}
|
}
|
||||||
|
|
||||||
listener {
|
listener {
|
||||||
timeout = 151 # 5min
|
timeout = 301 # 5min
|
||||||
on-timeout = loginctl lock-session # lock screen when timeout has passed
|
on-timeout = loginctl lock-session # lock screen when timeout has passed
|
||||||
}
|
}
|
||||||
|
|
||||||
listener {
|
listener {
|
||||||
timeout = 330 # 5.5min
|
timeout = 480 # 8min
|
||||||
on-timeout = brightnessctl -sd '*::kbd_backlight' set 0 # save state and turn off keyboard backlight
|
on-timeout = brightnessctl -sd '*::kbd_backlight' set 0 # save state and turn off keyboard backlight
|
||||||
on-resume = brightnessctl -rd '*::kbd_backlight' # restore keyboard backlight
|
on-resume = brightnessctl -rd '*::kbd_backlight' # restore keyboard backlight
|
||||||
}
|
}
|
||||||
|
|
||||||
listener {
|
listener {
|
||||||
timeout = 330 # 5.5min
|
timeout = 480 # 8min
|
||||||
on-timeout = hyprctl dispatch dpms off # screen off when timeout has passed
|
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
|
on-resume = hyprctl dispatch dpms on && brightnessctl -r # screen on when activity is detected
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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."
|
||||||
@@ -8,7 +8,11 @@ mkdir -p "$WALLPAPER_DIR"
|
|||||||
mkdir -p "$THEME_DIR/backgrounds"
|
mkdir -p "$THEME_DIR/backgrounds"
|
||||||
|
|
||||||
if [ -z "$1" ]; then
|
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…"
|
omarchy-launch-walker -m menus:blobBackgroundSelector --width 800 --minheight 400 -p "Select Wallpaper…"
|
||||||
exit 0
|
exit 0
|
||||||
else
|
else
|
||||||
|
|||||||
+14
-17
@@ -22,20 +22,14 @@
|
|||||||
"network",
|
"network",
|
||||||
"pulseaudio",
|
"pulseaudio",
|
||||||
"cpu",
|
"cpu",
|
||||||
"memory",
|
"battery",
|
||||||
"disk",
|
"custom/notification"
|
||||||
"battery"
|
|
||||||
],
|
],
|
||||||
"memory": {
|
"custom/notification": {
|
||||||
"interval": 10,
|
"format": "",
|
||||||
"format": "\udb81\ude1a",
|
"tooltip-format": "Notifications",
|
||||||
"tooltip-format": "Memory: {used:0.1f}G/{total:0.1f}G"
|
"on-click": "ags toggle notification-center",
|
||||||
},
|
"on-click-right": "omarchy-toggle-notification-silencing"
|
||||||
"disk": {
|
|
||||||
"interval": 30,
|
|
||||||
"format": "\udb80\udeca",
|
|
||||||
"path": "/",
|
|
||||||
"tooltip-format": "Disk: {used}/{total}"
|
|
||||||
},
|
},
|
||||||
"hyprland/workspaces": {
|
"hyprland/workspaces": {
|
||||||
"on-click": "activate",
|
"on-click": "activate",
|
||||||
@@ -84,12 +78,13 @@
|
|||||||
"interval": 5,
|
"interval": 5,
|
||||||
"format": "\udb80\udf5b",
|
"format": "\udb80\udf5b",
|
||||||
"on-click": "omarchy-launch-or-focus-tui btop",
|
"on-click": "omarchy-launch-or-focus-tui btop",
|
||||||
"on-click-right": "alacritty"
|
"on-click-middle": "alacritty",
|
||||||
|
"on-click-right": "ags toggle sys-monitor"
|
||||||
},
|
},
|
||||||
"clock": {
|
"clock": {
|
||||||
"format": "{:L%A %d %B %H:%M}",
|
"format": "{:L%A %d %B %H:%M}",
|
||||||
"format-alt": "{:L%d %B W%V %Y}",
|
|
||||||
"tooltip": false,
|
"tooltip": false,
|
||||||
|
"on-click": "ags toggle quick-settings",
|
||||||
"on-click-right": "omarchy-launch-floating-terminal-with-presentation omarchy-tz-select"
|
"on-click-right": "omarchy-launch-floating-terminal-with-presentation omarchy-tz-select"
|
||||||
},
|
},
|
||||||
"network": {
|
"network": {
|
||||||
@@ -109,7 +104,8 @@
|
|||||||
"tooltip-format-disconnected": "Disconnected",
|
"tooltip-format-disconnected": "Disconnected",
|
||||||
"interval": 3,
|
"interval": 3,
|
||||||
"spacing": 1,
|
"spacing": 1,
|
||||||
"on-click": "omarchy-launch-wifi"
|
"on-click": "omarchy-launch-wifi",
|
||||||
|
"on-click-right": "ags toggle quick-settings"
|
||||||
},
|
},
|
||||||
"battery": {
|
"battery": {
|
||||||
"format": "{capacity}% {icon}",
|
"format": "{capacity}% {icon}",
|
||||||
@@ -159,7 +155,8 @@
|
|||||||
"format-connected": "\udb80\udcb1",
|
"format-connected": "\udb80\udcb1",
|
||||||
"format-no-controller": "",
|
"format-no-controller": "",
|
||||||
"tooltip-format": "Devices connected: {num_connections}",
|
"tooltip-format": "Devices connected: {num_connections}",
|
||||||
"on-click": "omarchy-launch-bluetooth"
|
"on-click": "omarchy-launch-bluetooth",
|
||||||
|
"on-click-right": "ags toggle quick-settings"
|
||||||
},
|
},
|
||||||
"pulseaudio": {
|
"pulseaudio": {
|
||||||
"format": "{icon}",
|
"format": "{icon}",
|
||||||
|
|||||||
@@ -112,3 +112,9 @@ tooltip {
|
|||||||
#custom-notification-silencing-indicator.active {
|
#custom-notification-silencing-indicator.active {
|
||||||
color: #a55555;
|
color: #a55555;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#custom-notification {
|
||||||
|
min-width: 16px;
|
||||||
|
margin-left: 7.5px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user