Compare commits

...

13 Commits

Author SHA1 Message Date
salt 42d9758ecc Make scrape fails on the AI rate limit dash non-critical 2026-06-19 17:12:59 -05:00
salt b34d5b1d97 Trim script 2026-06-15 00:08:47 -05:00
salt ff86eeff4b Ignore pycache 2026-06-15 00:08:41 -05:00
salt ff30800337 Tweaks 2026-06-15 00:05:29 -05:00
salt 319db74bce More polishing up 2026-06-15 00:00:56 -05:00
salt f04e24b64d Polish pass 2026-06-14 23:51:23 -05:00
salt 9005edd58c First pass at an OpenAI limit script 2026-06-14 23:49:59 -05:00
salt 5abdee0d4b Fuck it, unfoot 2026-06-14 23:49:46 -05:00
salt ff670b78e6 Don't treat PHP as HTML 2026-06-13 19:56:30 -05:00
salt 573dbef686 Tweaks to workspace naming 2026-06-13 19:56:04 -05:00
salt 9fa21f1a9e Add new workspaces to laptop, too 2026-06-09 09:19:04 -05:00
salt 07840b0dd3 Ignore codex 2026-06-08 00:45:52 -05:00
salt 3ea2296eca Experimenting with hyprland 2026-06-08 00:45:40 -05:00
12 changed files with 320 additions and 31 deletions
+1
View File
@@ -1,3 +1,4 @@
*.swp
.netrwhist
*/.config/systemd/user/default.target.wants/
.codex
-3
View File
@@ -92,9 +92,6 @@ au BufNewFile,BufRead *.html,*.php
\ set shiftwidth=2 |
\ set autoindent |
\ set smartindent
" Treat PHP like HTML
au BufNewFile,BufRead *.php
\ set filetype=html
au FileType yaml
\ set tabstop=2 |
\ set softtabstop=2 |
+1 -1
View File
@@ -54,7 +54,7 @@ blink=yes
[touch]
# long-press-delay=400
[colors-dark]
[colors]
alpha=0.8
background=282828
foreground=ebdbb2
@@ -75,12 +75,31 @@ bind = $mainMod SHIFT, 9, movetoworkspace, 9
bind = $mainMod SHIFT, 0, movetoworkspace, 10
bind = $mainMod SHIFT, MINUS, movetoworkspace, 11
# Scroll through existing workspaces with mainMod + scroll
# Role workspaces
bind = $mainMod, E, workspace, name:mail
bind = $mainMod SHIFT, E, movetoworkspace, name:mail
bind = $mainMod, C, workspace, name:comms
bind = $mainMod SHIFT, C, movetoworkspace, name:comms
bind = $mainMod, M, workspace, name:media
bind = $mainMod SHIFT, M, movetoworkspace, name:media
bind = $mainMod, S, togglespecialworkspace, secrets
bind = $mainMod SHIFT, S, movetoworkspace, special:secrets
bind = $mainMod, N, togglespecialworkspace, scratch
bind = $mainMod SHIFT, N, movetoworkspace, special:scratch
# Monitor-local workspace movement
bind = $mainMod, bracketleft, workspace, r-1
bind = $mainMod, bracketright, workspace, r+1
bind = $mainMod SHIFT, bracketleft, movetoworkspace, r-1
bind = $mainMod SHIFT, bracketright, movetoworkspace, r+1
bind = $mainMod, BackSpace, workspace, previous_per_monitor
# Scroll through existing workspaces
bind = $mainMod, Period, workspace, e+1
bind = $mainMod, Comma, workspace, e-1
# Move windows around
#bindm = $mainMod, mouse:272, movewindow
bindm = $mainMod, mouse:272, movewindow
# Resize mouse binding and submap
bindm = $mainMod, mouse:273, resizewindow
@@ -14,10 +14,10 @@ windowrule = pin on, match:class ^(.*pavucontrol.*)$
# Assign specific windows to specific workspaces on launch
windowrule = workspace 2 silent, match:class ^(steam)$
windowrule = workspace 8 silent, match:class ^(org.keepassxc.KeePassXC)$
windowrule = workspace 8 silent, match:class ^(org.mozilla.Thunderbird)$
windowrule = workspace 9 silent, match:class ^(im.riot.Riot)$
windowrule = workspace 9 silent, match:class ^(.*vesktop.*)$
windowrule = workspace 10 silent, match:class ^(.*spotube.*)$
windowrule = workspace 10 silent, match:class ^(.*potify.*)$
windowrule = workspace 10 silent, match:class ^(feishin)$
windowrule = workspace special:secrets silent, match:class ^(org\.keepassxc\.KeePassXC)$
windowrule = workspace name:mail silent, match:class ^(org\.mozilla\.Thunderbird|org\.mozilla\.thunderbird_esr)$
windowrule = workspace name:comms silent, match:class ^(im\.riot\.Riot)$
windowrule = workspace name:comms silent, match:class ^(.*vesktop.*)$
windowrule = workspace name:media silent, match:class ^(.*spotube.*)$
windowrule = workspace name:media silent, match:class ^(.*potify.*)$
windowrule = workspace name:media silent, match:class ^(feishin)$
@@ -1,9 +1,9 @@
# vim: set ft=hyprlang:
# Workspace-specific applications
exec-once = [workspace 2 silent] flatpak run com.valvesoftware.Steam -silent
exec-once = [workspace 8 silent] flatpak run org.keepassxc.KeePassXC || keepassxc
exec-once = [workspace 8 silent] flatpak run org.mozilla.Thunderbird || thunderbird
exec-once = [workspace 9 silent] flatpak run im.riot.Riot || element-desktop
exec-once = [workspace 9 silent] flatpak run dev.vencord.Vesktop
exec-once = [workspace special:secrets silent] flatpak run org.keepassxc.KeePassXC || keepassxc
exec-once = [workspace name:mail silent] flatpak run org.mozilla.Thunderbird || thunderbird
exec-once = [workspace name:comms silent] flatpak run im.riot.Riot || element-desktop
exec-once = [workspace name:comms silent] flatpak run dev.vencord.Vesktop
#exec-once = [workspace 10 silent] flatpak run com.spotify.Client
exec-once = [workspace 10 silent] flatpak run org.jeffvli.feishin || ~/Programs/feishin
exec-once = [workspace name:media silent] flatpak run org.jeffvli.feishin || ~/Programs/feishin
@@ -16,3 +16,9 @@ workspace = 8, monitor:DP-2
workspace = 9, monitor:DP-2
workspace = 10, monitor:DP-2
workspace = 11, monitor:DP-2
workspace = name:mail, monitor:DP-2, persistent:true
workspace = name:comms, monitor:DP-2, persistent:true
workspace = name:media, monitor:DP-2, persistent:true
workspace = special:secrets, persistent:true
workspace = special:scratch, persistent:true
@@ -3,3 +3,20 @@
monitor=eDP-1,preferred,auto,1.00
# Should be the Nreal Air
monitor=DP-1,preferred,auto,1.5
workspace = 1, monitor:eDP-1
workspace = 2, monitor:eDP-1
workspace = 3, monitor:eDP-1
workspace = 4, monitor:eDP-1
workspace = 5, monitor:eDP-1
workspace = 6, monitor:eDP-1
workspace = 7, monitor:eDP-1
workspace = 8, monitor:eDP-1
workspace = 9, monitor:eDP-1
workspace = 10, monitor:eDP-1
workspace = 11, monitor:eDP-1
workspace = name:mail, monitor:eDP-1, persistent:true
workspace = name:comms, monitor:eDP-1, persistent:true
workspace = name:media, monitor:eDP-1, persistent:true
workspace = special:secrets, persistent:true
workspace = special:scratch, persistent:true
+52 -13
View File
@@ -8,18 +8,41 @@
"margin-right": 16,
"modules-left": ["hyprland/workspaces", "hyprland/window", "hyprland/submap", "sway/mode"],
"modules-right": ["tray"],
"hyprland/workspaces": {
"all-outputs": false,
"disable-scroll": true,
"persistent-workspaces": {
"eDP-1": [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 ],
"DP-1": [ 1, 2, 3, 4, 5, 6, 7 ],
"DP-2": [ 8, 9, 10, 11 ]
}
},
"hyprland/window": {
"separate-outputs": true,
"format": "{}"
"hyprland/workspaces": {
"all-outputs": false,
"disable-scroll": true,
"show-special": true,
"special-visible-only": false,
"format": "{icon}",
"format-icons": {
"1": "1",
"2": "2",
"3": "3",
"4": "4",
"5": "5",
"6": "6",
"7": "7",
"8": "8",
"9": "9",
"10": "10",
"11": "11",
"mail": "",
"comms": "",
"media": "",
"secrets": "S",
"scratch": "N",
"empty": "",
"default": ""
},
"persistent-workspaces": {
"eDP-1": [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 ],
"DP-1": [ 1, 2, 3, 4, 5, 6, 7 ],
"DP-2": [ 8, 9, 10, 11 ]
}
},
"hyprland/window": {
"separate-outputs": true,
"format": "{}"
},
"hyprland/submap": {
"on-click": "hyprctl dispatch submap reset"
@@ -38,7 +61,7 @@
"margin-bottom": 16,
"margin-left": 16,
"margin-right": 16,
"modules-left": ["gamemode", "battery", "temperature", "cpu", "memory", "network"],
"modules-left": ["gamemode", "battery", "temperature", "cpu", "memory", "custom/codex-primary", "custom/codex-secondary", "network"],
"modules-center": [],
"modules-right": ["mpris", "pulseaudio", "custom/output-device", "backlight", "idle_inhibitor", "clock"],
"clock": {
@@ -132,6 +155,22 @@
"exec": "flatpak remote-ls --updates --app | wc -l",
"exec-if": "test $(flatpak remote-ls --updates --app | wc -l) -gt 0"
},
"custom/codex-primary": {
"interval": 15,
"return-type": "json",
"exec": "$HOME/.config/waybar/scripts/openai-rate.py --window primary",
"exec-if": "test -r ~/.codex/auth.json",
"escape": false,
"format": " {}"
},
"custom/codex-secondary": {
"interval": 15,
"return-type": "json",
"exec": "$HOME/.config/waybar/scripts/openai-rate.py --window secondary",
"exec-if": "test -r ~/.codex/auth.json",
"escape": false,
"format": " {}"
},
"custom/backup": {
"interval": 60,
"format": "",
@@ -0,0 +1 @@
__pycache__
+178
View File
@@ -0,0 +1,178 @@
#!/usr/bin/env python3
import json
import math
import os
import sys
import urllib.error
import urllib.request
from argparse import ArgumentParser
from pathlib import Path
DEFAULT_BASE_URL = "https://chatgpt.com/backend-api"
def waybar(text, klass="good", tooltip=None):
print(json.dumps({"text": text, "class": klass, "tooltip": tooltip or text}))
sys.exit(0)
def unknown(tooltip):
waybar('unk <span font-size="7pt">--</span>', "good", tooltip)
def load_auth():
auth_path = Path(os.environ.get("OPENAI_AUTH_FILE", "~/.codex/auth.json")).expanduser()
auth = {}
if auth_path.is_file():
try:
auth = json.loads(auth_path.read_text())
except (OSError, json.JSONDecodeError):
waybar("AI auth unreadable", "critical", f"Could not parse {auth_path}")
tokens = auth.get("tokens") or {}
access_token = tokens.get("access_token")
account_id = tokens.get("account_id")
if access_token:
return access_token, account_id, "~/.codex/auth.json:tokens.access_token"
api_key = auth.get("OPENAI_API_KEY") or os.environ.get("OPENAI_API_KEY")
if api_key:
source = "~/.codex/auth.json:OPENAI_API_KEY" if auth.get("OPENAI_API_KEY") else "env:OPENAI_API_KEY"
return api_key, account_id, source
waybar(
"AI auth missing",
"warning",
"No Codex access token found in ~/.codex/auth.json",
)
def usage_url():
base = os.environ.get("CODEX_RATE_BASE_URL") or os.environ.get("OPENAI_BASE_URL") or DEFAULT_BASE_URL
base = base.rstrip("/")
if base.endswith("/backend-api/codex"):
base = base.removesuffix("/codex")
if base.startswith(("https://chatgpt.com", "https://chat.openai.com")) and "/backend-api" not in base:
base += "/backend-api"
return f"{base}/wham/usage" if "/backend-api" in base else f"{base}/api/codex/usage"
def fetch_usage(token, account_id):
headers = {
"Authorization": f"Bearer {token}",
"Accept": "application/json",
"User-Agent": "codex-waybar-rate",
}
if account_id:
headers["ChatGPT-Account-ID"] = account_id
req = urllib.request.Request(usage_url(), headers=headers)
try:
with urllib.request.urlopen(req, timeout=15) as res:
return json.load(res)
except urllib.error.HTTPError as exc:
detail = exc.read().decode("utf-8", "replace")[:240]
unknown(f"Usage endpoint returned HTTP {exc.code}\n{detail}")
except (OSError, TimeoutError) as exc:
unknown(f"Could not reach Codex usage endpoint\n{exc}")
except json.JSONDecodeError as exc:
unknown(f"Usage endpoint returned invalid JSON\n{exc}")
def window_label(seconds):
if not isinstance(seconds, int) or seconds <= 0:
return "usage"
minutes = math.ceil(seconds / 60)
if minutes < 90:
return f"{minutes}m"
hours = round(minutes / 60)
if hours < 36:
return f"{hours}h"
days = round(hours / 24)
if days < 365:
return f"{days}d"
return f"{round(days / 365)}y"
def reset_text(seconds):
if not isinstance(seconds, int) or seconds < 0:
return None
if seconds < 90:
return f"{seconds}s"
minutes = round(seconds / 60)
if minutes < 90:
return f"{minutes}m"
hours = round(minutes / 60)
if hours < 48:
return f"{hours}h"
return f"{round(hours / 24)}d"
def usage_class(used, limit_reached=False):
if limit_reached or used >= 95:
return "critical"
if used >= 90:
return "warning"
if used >= 75:
return "regular"
return "good"
def format_window(name, window):
if not isinstance(window, dict):
return None
used = window.get("used_percent")
if not isinstance(used, int):
return None
label = window_label(window.get("limit_window_seconds"))
left = max(0, 100 - used)
reset = reset_text(window.get("reset_after_seconds"))
tooltip = f"{name} {label}: {used}% used"
if reset:
tooltip += f", resets in {reset}"
return {
"label": label,
"left": left,
"used": used,
"tooltip": tooltip,
}
def output_window(data, source, selector):
rate_limit = data.get("rate_limit") or {}
window = format_window(selector, rate_limit.get(f"{selector}_window"))
if not window:
waybar("n/a", "warning", f"No {selector} usage window returned\nsource: {source}")
klass = usage_class(window["used"], rate_limit.get("limit_reached"))
tooltip = [window["tooltip"], f"{window['left']}% remaining", f"source: {source}"]
waybar(f"{window['left']}% <span font-size=\"7pt\">{window['label']}</span>", klass, "\n".join(tooltip))
def parse_args():
parser = ArgumentParser(description="Waybar Codex rate-limit widget")
parser.add_argument(
"--window",
choices=("primary", "secondary"),
required=True,
help="usage window to display",
)
return parser.parse_args()
def main():
args = parse_args()
token, account_id, source = load_auth()
data = fetch_usage(token, account_id)
output_window(data, source, args.window)
if __name__ == "__main__":
main()
+31
View File
@@ -66,6 +66,18 @@ window#waybar {
color: #fabd2f;
font-weight: bold;
}
#workspaces button.special {
color: rgba(235, 219, 178, 0.55);
}
#workspaces button.special.empty {
color: rgba(235, 219, 178, 0.2);
}
#workspaces button.special.visible,
#workspaces button.special.active {
color: rgba(40, 40, 40, 0.8);
background: #d3869b;
font-weight: bold;
}
/* Window and mode display */
#window {
color: rgba(235, 219, 178, 0.4);
@@ -226,3 +238,22 @@ window#waybar.fullscreen #window {
color: #ebdbb2;
padding: 0 1em;
}
#custom-codex-primary,
#custom-codex-secondary {
padding: 0 1em;
color: rgba(235, 219, 178, 0.2);
}
#custom-codex-primary.regular,
#custom-codex-secondary.regular {
color: #ebdbb2;
}
#custom-codex-primary.warning,
#custom-codex-secondary.warning {
color: #fabd2f;
}
#custom-codex-primary.critical,
#custom-codex-secondary.critical {
color: #282828;
background: #fb4934;
border-radius: 8px;
}