Compare commits

...
14 Commits
Author SHA1 Message Date
Daniele Fucini e2fd3e4aa9 xmonad: restore floating windows after leaving fullscreen
Remember each window's float rect when it goes fullscreen and put it back on exit,
instead of always sinking it (Viewnior after F5). Windows with no record are still sunk.
2026-11-14 19:43:37 +01:00
Daniele Fucini 4a96319bb2 Merge the database dump scripts into db-dump
garmin-dump, weather-dump and library-dump differed only in project, database and rotation; one
db-dump PROJECT DATABASE week|day replaces them. The units keep their names and only change ExecStart.
2026-11-14 15:49:48 +01:00
Daniele Fucini faad434dad aurb: keep ThinLTO cache entries until the size cap
lld expires entries unused for a week by default, which empties the cache between most package rebuilds; prune_after=0s leaves only the 10 GB cap.
2026-11-13 21:27:56 +01:00
Daniele Fucini 9896d9bb21 xmobar: draw the weather icon larger
Add a 13 pt additional font and use it for the weather icon only, so the
rest of the bar and its height are unchanged.
2026-11-13 18:39:53 +01:00
Daniele Fucini 248a9b7d1f xmobar: treat CAVOK/NSC weather reports as clear
NOAA's decoded METAR has no sky conditions line for CAVOK or NSC, so the
weather showed a generic cloud icon and "Unknown"; read them from the raw
observation as clear.
2026-11-13 18:23:14 +01:00
Daniele Fucini 216e1bff0f xmobar: toggle mute on volume click
Left-clicking the volume widget toggles mute on the default sink, so it
works for speakers and headphones alike.
2026-11-12 21:26:39 +01:00
Daniele Fucini 472bf7246b xmobar: show current weather details on click
Clicking the weather now runs weather.sh notify, which shows the
description and temperature; the forecast keeps its ens-forecast week
notification.
2026-11-12 19:24:15 +01:00
Daniele Fucini 0d1cb6cf2a xmonad: fix Signal not moving to the chat workspace
Signal 8.x sets its WM_CLASS to lowercase "signal", so the case-sensitive "Signal" rule no longer matched. Merge it with the discord rule into a case-insensitive classIn.
2026-11-12 18:58:39 +01:00
Daniele Fucini 96903a4d22 Show a weather condition icon in xmobar
Replace the Weather plugin with weather.sh, which picks an icon from the METAR weather/sky and uses moon icons when the sun is down.
2026-11-12 16:54:08 +01:00
Daniele Fucini ce1cd479df Remove gsimplecal calendar popup from xmobar date
Drop the click action on the date and its xmonad float rule; delete the gsimplecal config.
2026-11-12 16:38:42 +01:00
Daniele Fucini c103510786 xmobar: show the next calcurse appointment
Time of today's next appointment, or an icon for a whole-day event today or an appointment tomorrow; click lists both days, right-click opens the calcurse scratchpad. Uptime dropped and the date shortened to make room.
2026-11-12 16:03:00 +01:00
Daniele Fucini 3053b35f26 calcurse: enable automatic note garbage collection
Removes note files no longer linked to any item on each save, instead of running calcurse -g by hand.
2026-11-12 15:40:55 +01:00
Daniele Fucini a260a42352 calcurse: tag appointment alerts with app name and icon
Matches the other calcurse notifications, so dunst rules can target them.
2026-11-12 15:38:16 +01:00
Daniele Fucini 566ac562bd calcurse: one-click Google login renewal
On expiry calcurse-sync starts calcurse-relogin, a notification that opens the consent page when clicked and catches the OAuth code on 127.0.0.1:8749. dunst's left-click now runs a notification's action before closing it.
2026-11-12 14:35:56 +01:00
19 changed files with 329 additions and 113 deletions

No files matched your search

+1 -1
View File
@@ -2,4 +2,4 @@
# Here rather than in LDFLAGS: old libtool (<2.4.7) drops -fuse-ld=lld, and these then reach GNU ld.
-fuse-ld=lld
-Wl,--thinlto-cache-dir=/home/fuxino/.cache/thinlto
-Wl,--thinlto-cache-policy=cache_size_bytes=10g
-Wl,--thinlto-cache-policy=cache_size_bytes=10g:prune_after=0s
+2 -2
View File
@@ -20,7 +20,7 @@ format.notifytime=%T
format.appointmenttime=%H:%M
format.outputdate=%F
format.dayheading=%B %e, %Y
general.autogc=no
general.autogc=yes
general.autosave=yes
general.confirmdelete=yes
general.confirmquit=yes
@@ -28,6 +28,6 @@ general.firstdayofweek=monday
general.multipledays=yes
general.periodicsave=0
general.systemevents=yes
notification.command=calcurse --next | xargs -0 notify-send "Appointment"
notification.command=calcurse --next | xargs -0 notify-send -a calcurse -i x-office-calendar "Appointment"
notification.notifyall=all
notification.warning=3600
+1 -1
View File
@@ -300,7 +300,7 @@
# * context_all: Open context menu for all notifications.
# These values can be strung together for each mouse event, and
# will be executed in sequence.
mouse_left_click = close_current
mouse_left_click = do_action, close_current
mouse_middle_click = do_action, close_current
mouse_right_click = close_all
-4
View File
@@ -1,4 +0,0 @@
mainwindow_position = mouse
close_on_unfocus = 1
mark_today = 1
show_week_numbers = 1
@@ -0,0 +1,7 @@
[Unit]
Description=Ask to renew the calcurse Google Calendar login
[Service]
# simple: the prompt waits for a click without a start timeout
Type=simple
ExecStart=%h/.local/bin/calcurse-relogin --prompt
+1 -1
View File
@@ -5,7 +5,7 @@ StartLimitBurst=4
[Service]
Type=oneshot
ExecStart=%h/.local/bin/garmin-dump
ExecStart=%h/.local/bin/db-dump negsplit Garmin week
# MariaDB not up yet (e.g. caught up right after boot)? retry a few times
Restart=on-failure
RestartSec=5min
+1 -1
View File
@@ -5,7 +5,7 @@ StartLimitBurst=4
[Service]
Type=oneshot
ExecStart=%h/.local/bin/library-dump
ExecStart=%h/.local/bin/db-dump library_manager Library day
# MariaDB not up yet (e.g. caught up right after boot)? retry a few times
Restart=on-failure
RestartSec=5min
+1 -1
View File
@@ -5,7 +5,7 @@ StartLimitBurst=4
[Service]
Type=oneshot
ExecStart=%h/.local/bin/weather-dump
ExecStart=%h/.local/bin/db-dump ens-forecast Weather week
# MariaDB not up yet (e.g. caught up right after boot)? retry a few times
Restart=on-failure
RestartSec=5min
+42
View File
@@ -0,0 +1,42 @@
#!/bin/bash
# Next calcurse item for xmobar: pink " 10:15 | " for an appointment later today, a cyan icon for a whole-day
# event today, a pink icon alone when the next appointment is tomorrow; empty otherwise.
# With "list": show today's and tomorrow's items in a notification (for a click on the bar)
set -uo pipefail
icon=$'\xf3\xb0\x83\xad' # nf-md-calendar, as UTF-8 bytes so the locale doesn't matter
today=$(date +%F)
query() {
calcurse -Q --filter-type cal --days 2 --output-datefmt %F "$@"
}
if [[ ${1:-} == list ]]; then
tomorrow=$(date -d tomorrow +%F)
body=$(query --format-apt '%S–%E %m\n' --format-recur-apt '%S–%E %m\n' \
--format-event 'all day %m\n' --format-recur-event 'all day %m\n' |
sed -e 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g' -e "s/^$today:$/Today:/; s/^$tomorrow:$/Tomorrow:/")
notify-send -a calcurse -i x-office-calendar "Calendar" "${body:-Nothing today or tomorrow}"
exit
fi
now=$(date +%s)
day="" next="" event=false
while read -r kind start; do
case $kind in
*:) day=${kind%:} ;;
A) [[ -z $next ]] && ((start > now)) && next=$start ;;
E) [[ $day == "$today" ]] && event=true ;;
esac
done < <(query --format-apt 'A %s\n' --format-recur-apt 'A %s\n' --format-event 'E\n' --format-recur-event 'E\n')
if [[ -n $next && $(date -d "@$next" +%F) == "$today" ]]; then
text="<fc=#ff79c6>$icon $(date -d "@$next" +%H:%M)</fc>"
elif $event; then
text="<fc=#8be9fd>$icon</fc>"
elif [[ -n $next ]]; then
text="<fc=#ff79c6>$icon</fc>"
else
exit 0
fi
echo "$text | "
+81
View File
@@ -0,0 +1,81 @@
#!/bin/bash
# Current weather for xmobar, e.g. "<icon> 7°C": NOAA's decoded METAR (the source of xmobar's Weather plugin)
# Icon from the weather, else the sky conditions; clear/partly/mostly cloudy get moon icons when the sun is down.
# Temperature colors match the plugin's -L 0 -H 30: low up to 0, high above 30.
# With "notify": show the description (e.g. "Light drizzle, mist") and temperature in a notification instead.
STATION=LZIB LAT=48.2 LON=17.2
declare -A ICON=(
[thermometer]=$'\xef\x8b\x89' [day_sunny]=$'\xee\x8c\x8d' [night_clear]=$'\xee\x8c\xab'
[day_cloudy]=$'\xee\x8c\x82' [night_partly_cloudy]=$'\xee\x8d\xb9' [day_sunny_overcast]=$'\xee\x8c\x8c'
[night_cloudy]=$'\xee\x8d\xbe' [cloudy]=$'\xee\x8c\x92' [cloud]=$'\xee\x8c\xbd'
[thunderstorm]=$'\xee\x8c\x9d' [rain_mix]=$'\xee\x8c\x96' [snow]=$'\xee\x8c\x9a' [showers]=$'\xee\x8c\x99'
[rain]=$'\xee\x8c\x98' [sprinkle]=$'\xee\x8c\x9b' [fog]=$'\xee\x8c\x93' [smoke]=$'\xee\x8d\x9c'
[dust]=$'\xee\x8d\x9d'
)
report=$(curl -sf --max-time 20 https://tgftp.nws.noaa.gov/data/observations/metar/decoded/"$STATION".TXT)
temp=$(sed -n 's/^Temperature: .*(\([-0-9.]*\) C).*/\1/p' <<< "$report")
if [[ -z $temp ]]; then
if [[ $1 == notify ]]; then notify-send "Weather" "No report from $STATION"; else echo "<fn=1>${ICON[thermometer]}</fn> N/A"; fi
exit 0
fi
# Phenomena "in the vicinity" aren't at the station; the rest are e.g. "light drizzle; mist"
wx=$(sed -n 's/^Weather: //p' <<< "$report" | tr ';' '\n' | sed 's/^ *//' | grep -v vicinity)
wx=${wx,,}
sky=$(sed -n 's/^Sky conditions: //p' <<< "$report")
# CAVOK/NSC (no cloud below 5000 ft) have no sky line in the decoded report
[[ -z $sky ]] && grep -qE '^ob: .* (CAVOK|NSC) ' <<< "$report" && sky=clear
sky=${sky,,}
# Sun above the horizon (elevation > -0.833°, as for sunrise/sunset) right now, from the low-precision solar position
sun_up() {
awk -v t="$(date +%s)" -v lat="$LAT" -v lon="$LON" 'BEGIN {
r = atan2(0, -1) / 180; d = t / 86400 - 10957.5
g = (357.529 + 0.98560028 * d) * r
l = (280.459 + 0.98564736 * d + 1.915 * sin(g) + 0.020 * sin(2 * g)) * r
e = (23.439 - 0.00000036 * d) * r
ra = atan2(cos(e) * sin(l), cos(l)); s = sin(e) * sin(l); dec = atan2(s, sqrt(1 - s * s))
h = (280.46061837 + 360.98564736629 * d + lon) * r - ra
exit !(sin(lat * r) * sin(dec) + cos(lat * r) * cos(dec) * cos(h) > sin(-0.833 * r))
}'
}
case $wx in
*thunder*) icon=thunderstorm ;;
*freezing*|*sleet*|*ice\ pellets*|*hail*) icon=rain_mix ;;
*snow*rain*|*rain*snow*|*snow*drizzle*|*drizzle*snow*) icon=rain_mix ;;
*snow*) icon=snow ;;
*showers*) icon=showers ;;
*rain*) icon=rain ;;
*drizzle*) icon=sprinkle ;;
*fog*|*mist*) icon=fog ;;
*haze*|*smoke*) icon=smoke ;;
*dust*|*sand*) icon=dust ;;
*)
case $sky in
clear*) if sun_up; then icon=day_sunny; else icon=night_clear; fi ;;
"mostly clear"*|"partly cloudy"*) if sun_up; then icon=day_cloudy; else icon=night_partly_cloudy; fi ;;
"mostly cloudy"*) if sun_up; then icon=day_sunny_overcast; else icon=night_cloudy; fi ;;
overcast*|obscured*) icon=cloudy ;;
*) icon=cloud ;;
esac
;;
esac
if awk -v t="$temp" 'BEGIN { exit !(t > 30) }'; then color="#ffb86c"
elif awk -v t="$temp" 'BEGIN { exit !(t > 0) }'; then color="#50fa7b"
else color="#8be9fd"
fi
if [[ $1 == notify ]]; then
if [[ -n $wx ]]; then desc=$(paste -sd, <<< "$wx" | sed 's/,/, /g')
elif [[ $icon == day_sunny ]]; then desc=sunny
else desc=${sky:-unknown}
fi
notify-send "${ICON[$icon]} Weather" "${desc^}, $temp°C"
exit 0
fi
echo "<fn=1>${ICON[$icon]}</fn> <fc=$color>$temp</fc>°C"
+5 -13
View File
@@ -1,6 +1,7 @@
Config { overrideRedirect = True
, lowerOnStart = True
, font = "Jetbrains Mono Nerd Font Bold 10"
, additionalFonts = ["Jetbrains Mono Nerd Font Bold 13"]
, bgColor = "#24283b"
, fgColor = "#737aa2"
, position = TopH 24
@@ -8,14 +9,7 @@ Config { overrideRedirect = True
, borderColor = "#24283b"
, allDesktops = True
, pickBroadest = True
, commands = [ Run Weather "LZIB"
[ "--template", "\xf2c9 <tempC>°C"
, "-L", "0"
, "-H", "30"
, "--low" , "#8be9fd"
, "--normal" , "#50fa7b"
, "--high" , "#ffb86c"
] 9000
, commands = [ Run Com "/home/fuxino/.config/xmobar/weather.sh" [] "weather" 9000
, Run MultiCpu
[ "--template" , "\xf4bc <total>%"
, "--Low" , "25"
@@ -45,17 +39,15 @@ Config { overrideRedirect = True
, ("us" , "<fc=#ffb86c>US</fc>")
, ("sk(qwerty)" , "<fc=#ff5555>SK</fc>")
]
, Run Date "\xf133 %a %Y-%m-%d <fc=#ff79c6>%H:%M</fc>" "date" 10
, Run Uptime
[ "--template" , "\xf017 <days>d <hours>h"
] 600
, Run Date "\xf133 %a %d/%m <fc=#ff79c6>%H:%M</fc>" "date" 10
, Run Com "/home/fuxino/.config/xmobar/pactl_volume.sh" [] "pactl_volume" 10
, Run Com "/home/fuxino/.config/xmobar/ens_brief.sh" [] "ens_brief" 18000
, Run Com "/home/fuxino/.config/xmobar/updates.sh" [] "updates" 600
, Run Com "/home/fuxino/.config/xmobar/backup.sh" [] "backup" 3600
, Run Com "/home/fuxino/.config/xmobar/calcurse.sh" [] "calcurse" 600
, Run UnsafeXMonadLog
]
, sepChar = "%"
, alignSep = "}{"
, template = "  | %UnsafeXMonadLog% } %uptime% | %multicpu% %coretemp% | %memory% | <action=`/home/fuxino/.config/xmobar/kbd_cycle.sh` button=1><action=`/home/fuxino/.config/xmobar/kbd_cycle.sh prev` button=3> %kbd%</action></action> { <action=`/home/fuxino/.config/xmobar/backup.sh list`>%backup%</action><action=`/home/fuxino/.config/xmobar/updates.sh list`>%updates%</action><action=`/home/fuxino/.config/xmobar/ens_brief.sh week`>%LZIB% <fc=#565f89>·</fc> %ens_brief%</action> | %pactl_volume% | <action=`env GDK_DPI_SCALE=1.25 gsimplecal`>%date%</action> "
, template = "  | %UnsafeXMonadLog% } %multicpu% %coretemp% | %memory% | <action=`/home/fuxino/.config/xmobar/kbd_cycle.sh` button=1><action=`/home/fuxino/.config/xmobar/kbd_cycle.sh prev` button=3> %kbd%</action></action> { <action=`/home/fuxino/.config/xmobar/calcurse.sh list` button=1><action=`xdotool key super+shift+a` button=3>%calcurse%</action></action><action=`/home/fuxino/.config/xmobar/backup.sh list`>%backup%</action><action=`/home/fuxino/.config/xmobar/updates.sh list`>%updates%</action><action=`/home/fuxino/.config/xmobar/weather.sh notify`>%weather%</action> <fc=#565f89>·</fc> <action=`/home/fuxino/.config/xmobar/ens_brief.sh week`>%ens_brief%</action> | <action=`pactl set-sink-mute @DEFAULT_SINK@ toggle` button=1>%pactl_volume%</action> | %date% "
}
+5 -13
View File
@@ -1,6 +1,7 @@
Config { overrideRedirect = True
, lowerOnStart = True
, font = "Jetbrains Mono Nerd Font Bold 10"
, additionalFonts = ["Jetbrains Mono Nerd Font Bold 13"]
, bgColor = "#24283b"
, fgColor = "#737aa2"
, position = TopH 24
@@ -8,14 +9,7 @@ Config { overrideRedirect = True
, borderColor = "#24283b"
, allDesktops = True
, pickBroadest = True
, commands = [ Run Weather "LZIB"
[ "--template", "\xf2c9 <tempC>°C"
, "-L", "0"
, "-H", "30"
, "--low" , "#8be9fd"
, "--normal" , "#50fa7b"
, "--high" , "#ffb86c"
] 9000
, commands = [ Run Com "/home/fuxino/.config/xmobar/weather.sh" [] "weather" 9000
, Run MultiCpu
[ "--template" , "\xf4bc <total>%"
, "--Low" , "25"
@@ -45,18 +39,16 @@ Config { overrideRedirect = True
, ("us" , "<fc=#ffb86c>US</fc>")
, ("sk(qwerty)" , "<fc=#ff5555>SK</fc>")
]
, Run Date "\xf133 %a %Y-%m-%d <fc=#ff79c6>%H:%M</fc>" "date" 10
, Run Uptime
[ "--template" , "\xf017 <days>d <hours>h"
] 600
, Run Date "\xf133 %a %d/%m <fc=#ff79c6>%H:%M</fc>" "date" 10
, Run XPropertyLog "_XMONAD_TRAYPAD"
, Run Com "/home/fuxino/.config/xmobar/pactl_volume.sh" [] "pactl_volume" 10
, Run Com "/home/fuxino/.config/xmobar/ens_brief.sh" [] "ens_brief" 18000
, Run Com "/home/fuxino/.config/xmobar/updates.sh" [] "updates" 600
, Run Com "/home/fuxino/.config/xmobar/backup.sh" [] "backup" 3600
, Run Com "/home/fuxino/.config/xmobar/calcurse.sh" [] "calcurse" 600
, Run UnsafeXMonadLog
]
, sepChar = "%"
, alignSep = "}{"
, template = "  | %UnsafeXMonadLog% } %uptime% | %multicpu% %coretemp% | %memory% | <action=`/home/fuxino/.config/xmobar/kbd_cycle.sh` button=1><action=`/home/fuxino/.config/xmobar/kbd_cycle.sh prev` button=3> %kbd%</action></action> { <action=`/home/fuxino/.config/xmobar/backup.sh list`>%backup%</action><action=`/home/fuxino/.config/xmobar/updates.sh list`>%updates%</action><action=`/home/fuxino/.config/xmobar/ens_brief.sh week`>%LZIB% <fc=#565f89>·</fc> %ens_brief%</action> | %pactl_volume% | <action=`env GDK_DPI_SCALE=1.25 gsimplecal`>%date%</action> %_XMONAD_TRAYPAD% "
, template = "  | %UnsafeXMonadLog% } %multicpu% %coretemp% | %memory% | <action=`/home/fuxino/.config/xmobar/kbd_cycle.sh` button=1><action=`/home/fuxino/.config/xmobar/kbd_cycle.sh prev` button=3> %kbd%</action></action> { <action=`/home/fuxino/.config/xmobar/calcurse.sh list` button=1><action=`xdotool key super+shift+a` button=3>%calcurse%</action></action><action=`/home/fuxino/.config/xmobar/backup.sh list`>%backup%</action><action=`/home/fuxino/.config/xmobar/updates.sh list`>%updates%</action><action=`/home/fuxino/.config/xmobar/weather.sh notify`>%weather%</action> <fc=#565f89>·</fc> <action=`/home/fuxino/.config/xmobar/ens_brief.sh week`>%ens_brief%</action> | <action=`pactl set-sink-mute @DEFAULT_SINK@ toggle` button=1>%pactl_volume%</action> | %date% %_XMONAD_TRAYPAD% "
}
+31 -4
View File
@@ -2,7 +2,8 @@
import Data.Monoid
import Data.Char (toLower)
import Control.Monad (forM_, liftM2, when)
import Control.Monad (forM_, join, liftM2, when)
import qualified Data.Map.Strict as M
import Data.Maybe (isJust)
import XMonad
import XMonad.Actions.OnScreen (greedyViewOnScreen)
@@ -25,6 +26,7 @@ import XMonad.Layout.ThreeColumns
import qualified XMonad.StackSet as W
import XMonad.Util.ClickableWorkspaces
import XMonad.Util.EZConfig
import qualified XMonad.Util.ExtensibleState as XS
import qualified XMonad.Util.Hacks as Hacks
import XMonad.Util.Loggers
import XMonad.Util.NamedActions
@@ -38,6 +40,7 @@ main :: IO ()
main =
xmonad
. ewmhFullscreen
. setEwmhFullscreenHooks myFullscreenHook myUnfullscreenHook
. setEwmhSwitchDesktopHook safeView
. ewmh
. docks
@@ -379,8 +382,8 @@ myManageHook =
className =? "Qalculate-gtk" --> doFloat,
title =? "Genshin Impact" --> doShift wsGames,
classIn ["gimp", "audacity"] --> doShift wsMedia,
classIn ["pavucontrol", "org.pulseaudio.pavucontrol", "zenity", "org.gnome.zenity", "lxappearance", "qt5ct", "qt6ct", "gsimplecal"] --> doFloat,
className =? "Signal" --> doShift wsChat,
classIn ["pavucontrol", "org.pulseaudio.pavucontrol", "zenity", "org.gnome.zenity", "lxappearance", "qt5ct", "qt6ct"] --> doFloat,
classIn ["signal", "discord"] --> doShift wsChat,
className =? "Spotify" --> doShift wsMedia,
className =? "Transmission-gtk" --> doShift wsWeb,
className =? "Viewnior" --> doFloat,
@@ -388,7 +391,6 @@ myManageHook =
className =? "Xmessage" --> doFloat,
className =? "Xreader" --> doShift wsDocs,
className =? "calibre" --> doShift wsDocs,
className =? "discord" --> doShift wsChat,
className =? "feh" --> doFloat,
title =? "glxgears" --> doFloat,
className =? "library_manager" --> doShift wsDocs,
@@ -416,6 +418,31 @@ myManageHook =
-- Case-insensitive, as GTK4 and firejailed apps capitalise their class differently
classIn names = (`elem` names) . map toLower <$> className
-- Fullscreen toggles: remember each window's float rect (Nothing if tiled) so leaving
-- fullscreen restores it instead of always sinking the window.
newtype PreFullscreen = PreFullscreen (M.Map Window (Maybe W.RationalRect))
instance ExtensionClass PreFullscreen where
initialValue = PreFullscreen M.empty
myFullscreenHook :: ManageHook
myFullscreenHook = do
w <- ask
liftX $ do
old <- gets (M.lookup w . W.floating . windowset)
XS.modify (\(PreFullscreen m) -> PreFullscreen (M.insert w old m))
doFullFloat
-- Windows with no record (e.g. opened fullscreen) are sunk, as before.
myUnfullscreenHook :: ManageHook
myUnfullscreenHook = do
w <- ask
old <- liftX $ do
PreFullscreen m <- XS.get
XS.put (PreFullscreen (M.delete w m))
pure (join (M.lookup w m))
maybe doSink doRectFloat old
-- Handle event hook
myHandleEventHook :: Event -> X All
myHandleEventHook =
+101
View File
@@ -0,0 +1,101 @@
#!/usr/bin/env python3
"""Renew calcurse-caldav's Google login: catch the OAuth redirect on 127.0.0.1 and hand the code to calcurse-sync.
--prompt shows a notification first and logs in when it is clicked.
"""
import argparse
import configparser
import os
import subprocess
import sys
import time
import urllib.parse
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
CONFIG = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "calcurse" / "caldav" / "config"
WAIT_SECONDS = 300
def notify(summary: str, body: str = "", *extra: str) -> str:
"""Send a calcurse notification; with actions in extra, wait and return the chosen action's name."""
cmd = ["notify-send", "-a", "calcurse", "-i", "x-office-calendar", *extra, summary, body]
return subprocess.run(cmd, capture_output=True, text=True).stdout.strip()
def redirect_port() -> int:
parser = configparser.RawConfigParser()
parser.read(CONFIG)
uri = urllib.parse.urlparse(parser.get("OAuth2", "RedirectURI", fallback=""))
if uri.hostname != "127.0.0.1" or uri.port is None:
sys.exit(f"RedirectURI in {CONFIG} must be http://127.0.0.1:<port>")
return uri.port
def ui_running() -> bool:
ttys = subprocess.run(["ps", "-C", "calcurse", "-o", "tty="], capture_output=True, text=True).stdout.split()
return any(tty != "?" for tty in ttys)
class RedirectHandler(BaseHTTPRequestHandler):
result: dict[str, str] = {}
def do_GET(self) -> None:
query = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
for key in ("code", "error"):
if key in query:
RedirectHandler.result[key] = query[key][0]
done = bool(RedirectHandler.result)
self.send_response(200 if done else 404)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.end_headers()
if done:
self.wfile.write(b"calcurse: login received, you can close this tab.\n")
def log_message(self, format: str, *args: object) -> None:
pass
def login() -> int:
if ui_running():
notify("Google Calendar login", "Close calcurse first, then log in again.", "-u", "critical")
return 1
server = HTTPServer(("127.0.0.1", redirect_port()), RedirectHandler)
# With an expired login calcurse-caldav opens the consent page and exits; with a valid one it just syncs.
if subprocess.run(["calcurse-caldav"], capture_output=True).returncode == 0:
notify("Google Calendar login", "Already logged in.")
return 0
deadline = time.monotonic() + WAIT_SECONDS
while not RedirectHandler.result and (left := deadline - time.monotonic()) > 0:
server.timeout = left
server.handle_request()
server.server_close()
code = RedirectHandler.result.get("code")
if not code:
reason = RedirectHandler.result.get("error", "no answer from the browser")
notify("Google Calendar login failed", reason, "-u", "critical")
return 1
# The user manager's PATH lacks ~/.local/bin: call calcurse-sync from this script's folder.
calcurse_sync = Path(__file__).resolve().with_name("calcurse-sync")
sync = subprocess.run([calcurse_sync, "--authcode", code], capture_output=True, text=True)
if sync.returncode != 0:
last_line = (sync.stdout.strip().splitlines() or [""])[-1]
notify("Google Calendar login failed", last_line, "-u", "critical")
return 1
notify("Google Calendar login renewed", "Synced again.")
return 0
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--prompt", action="store_true", help="ask with a notification before logging in")
if parser.parse_args().prompt:
body = "Click to log in again, then click Allow in the browser."
if notify("Google Calendar login expired", body, "-u", "critical", "-A", "default=Log in") != "default":
return 0
return login()
if __name__ == "__main__":
sys.exit(main())
+12 -7
View File
@@ -1,13 +1,18 @@
#!/bin/bash
# Sync calcurse with Google Calendar via calcurse-caldav; notify once when the weekly OAuth login expires.
# Sync calcurse with Google Calendar via calcurse-caldav; start the login prompt when the weekly OAuth login expires.
# --now: called from the UI's pre-load hook (skip the UI check, shorter timeout, no daemon restart).
# --authcode CODE: finish a login (from calcurse-relogin, which checks for the UI itself).
set -uo pipefail
data=${XDG_DATA_HOME:-$HOME/.local/share}/calcurse
state=${XDG_STATE_HOME:-$HOME/.local/state}/calcurse-sync
expired_flag=$state/login-expired
now=false
[[ ${1:-} == --now ]] && now=true
authcode=()
case ${1:-} in
--now) now=true ;;
--authcode) authcode=(--authcode "$2") ;;
esac
# The UI keeps the data in memory, so syncing under it would lose changes; its post-save hook syncs after it exits.
ui_running() {
@@ -15,22 +20,22 @@ ui_running() {
ps -C calcurse -o tty= | grep -qv '?'
}
if ! $now && ui_running; then
if ! $now && ((${#authcode[@]} == 0)) && ui_running; then
echo "calcurse UI is running, skipping"
exit 0
fi
mkdir -p "$state"
# BROWSER=true: an expired login must not open browser tabs on its own.
out=$(BROWSER=true timeout "$($now && echo 15 || echo 60)" calcurse-caldav 2>&1)
out=$(BROWSER=true timeout "$($now && echo 15 || echo 60)" calcurse-caldav "${authcode[@]}" 2>&1)
rc=$?
printf '%s\n' "$out"
if ((rc != 0)); then
if grep -q 'Access token is missing or refresh token is expired' <<<"$out"; then
if [[ ! -e $expired_flag ]]; then
notify-send -a calcurse -u critical -i x-office-calendar "Google Calendar login expired" \
"Run calcurse-caldav in a terminal, allow access in the browser, then calcurse-caldav --authcode '<code>'."
# Prompt via calcurse-relogin, again daily if the prompt was dismissed.
if [[ ! -e $expired_flag || -n $(find "$expired_flag" -mmin +1440) ]]; then
systemctl --user start --no-block calcurse-relogin.service
touch "$expired_flag"
fi
elif grep -q 'Another synchronization instance' <<<"$out"; then
+38
View File
@@ -0,0 +1,38 @@
#!/bin/bash
# Usage: db-dump PROJECT DATABASE week|day
# Dump a MariaDB database to ~/.local/share/PROJECT/dumps with login ~/.config/PROJECT/dump.cnf.
# week: per-ISO-week files, keeping the two newest; day: per-weekday files (seven self-rotating copies).
set -euo pipefail
if (($# != 3)) || [[ $3 != week && $3 != day ]]; then
echo "usage: ${0##*/} PROJECT DATABASE week|day" >&2
exit 2
fi
project=$1 db=$2 mode=$3
cnf=${XDG_CONFIG_HOME:-$HOME/.config}/$project/dump.cnf
dir=${XDG_DATA_HOME:-$HOME/.local/share}/$project/dumps
if [[ $mode == week ]]; then
out=$dir/$db-$(date +%G-W%V).sql.gz
else
out=$dir/$db-$(LC_ALL=C date +%a).sql.gz
fi
keep=2
umask 077
mkdir -p "$dir"
# Write to a temporary file so a failed dump never replaces a good one
tmp=$(mktemp "$dir/.dump.XXXXXX")
trap 'rm -f "$tmp"' EXIT
mariadb-dump --defaults-extra-file="$cnf" --single-transaction --databases "$db" | gzip > "$tmp"
mv "$tmp" "$out"
# Weekly names sort by week, so all but the last $keep are the old ones
if [[ $mode == week ]]; then
dumps=("$dir/$db"-[0-9][0-9][0-9][0-9]-W[0-9][0-9].sql.gz)
if ((${#dumps[@]} > keep)); then
rm -f -- "${dumps[@]:0:${#dumps[@]}-keep}"
fi
fi
-24
View File
@@ -1,24 +0,0 @@
#!/bin/bash
# Dump the Garmin database to a per-ISO-week file, keeping the two newest weeks.
set -euo pipefail
cnf=${XDG_CONFIG_HOME:-$HOME/.config}/negsplit/dump.cnf
dir=${XDG_DATA_HOME:-$HOME/.local/share}/negsplit/dumps
out=$dir/Garmin-$(date +%G-W%V).sql.gz
keep=2
umask 077
mkdir -p "$dir"
# Write to a temporary file so a failed dump never replaces a good one
tmp=$(mktemp "$dir/.dump.XXXXXX")
trap 'rm -f "$tmp"' EXIT
mariadb-dump --defaults-extra-file="$cnf" --single-transaction --databases Garmin | gzip > "$tmp"
mv "$tmp" "$out"
# Names sort by week, so all but the last $keep are the old ones
dumps=("$dir"/Garmin-*.sql.gz)
if (( ${#dumps[@]} > keep )); then
rm -f -- "${dumps[@]:0:${#dumps[@]}-keep}"
fi
-17
View File
@@ -1,17 +0,0 @@
#!/bin/bash
# Dump the Library database to a per-weekday file (seven self-rotating copies).
set -euo pipefail
cnf=${XDG_CONFIG_HOME:-$HOME/.config}/library_manager/dump.cnf
dir=${XDG_DATA_HOME:-$HOME/.local/share}/library_manager/dumps
out=$dir/Library-$(LC_ALL=C date +%a).sql.gz
umask 077
mkdir -p "$dir"
# Write to a temporary file so a failed dump never replaces a good one
tmp=$(mktemp "$dir/.dump.XXXXXX")
trap 'rm -f "$tmp"' EXIT
mariadb-dump --defaults-extra-file="$cnf" --single-transaction --databases Library | gzip > "$tmp"
mv "$tmp" "$out"
-24
View File
@@ -1,24 +0,0 @@
#!/bin/bash
# Dump the Weather database (ens-forecast) to a per-ISO-week file, keeping the two newest weeks.
set -euo pipefail
cnf=${XDG_CONFIG_HOME:-$HOME/.config}/ens-forecast/dump.cnf
dir=${XDG_DATA_HOME:-$HOME/.local/share}/ens-forecast/dumps
out=$dir/Weather-$(date +%G-W%V).sql.gz
keep=2
umask 077
mkdir -p "$dir"
# Write to a temporary file so a failed dump never replaces a good one
tmp=$(mktemp "$dir/.dump.XXXXXX")
trap 'rm -f "$tmp"' EXIT
mariadb-dump --defaults-extra-file="$cnf" --single-transaction --databases Weather | gzip > "$tmp"
mv "$tmp" "$out"
# Names sort by week, so all but the last $keep are the old ones
dumps=("$dir"/Weather-*.sql.gz)
if (( ${#dumps[@]} > keep )); then
rm -f -- "${dumps[@]:0:${#dumps[@]}-keep}"
fi