Skip to content

Instantly share code, notes, and snippets.

@oz123
Last active August 20, 2026 19:54
Show Gist options
  • Select an option

  • Save oz123/da9bdb04ce47825be8206b4238e80e0c to your computer and use it in GitHub Desktop.

Select an option

Save oz123/da9bdb04ce47825be8206b4238e80e0c to your computer and use it in GitHub Desktop.
A script to ease testing and reviewing for https://github.com/mate-desktop/mate-panel/pull/1565
#!/usr/bin/env bash
#
# Build the clock applet with libical/vdir support (no EDS, no Wayland),
# spin up a local Radicale CalDAV server with a few events in the current
# week, sync it down with vdirsyncer, and drive the live MATE panel with
# xdotool to confirm the events show up in the clock applet's calendar
# popup.
#
# Run from the mate-panel source tree root:
# ./test-vdir-clock.sh
#
# Requires: an already-running MATE session (DISPLAY set), sudo access
# for `make install`, python3, xdotool, curl.
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WORKDIR="${WORKDIR:-/tmp/clock-vdir-test}"
VENV_DIR="$WORKDIR/venv"
RADICALE_DATA="$WORKDIR/radicale-data"
RADICALE_PORT="${RADICALE_PORT:-5232}"
RADICALE_USER="testuser"
RADICALE_COLLECTION="work"
VDIR_LOCAL_DIR="$WORKDIR/vdir-storage"
VDIRSYNCER_CONFIG="$WORKDIR/vdirsyncer-config"
VDIRSYNCER_STATUS="$WORKDIR/vdirsyncer-status"
log() { printf '\n==> %s\n' "$1"; }
# ---------------------------------------------------------------------------
# 1. Build & install the applet
# ---------------------------------------------------------------------------
build_applet() {
log "Configuring (libical enabled, EDS + Wayland disabled)"
cd "$REPO_ROOT"
autoreconf -fi
./configure --enable-libical --disable-eds --prefix=/usr --disable-wayland
log "Building"
make -j"$(nproc)"
log "Installing (sudo required)"
sudo make install
}
# ---------------------------------------------------------------------------
# 2. Python venv with radicale + vdirsyncer
# ---------------------------------------------------------------------------
setup_python_env() {
log "Creating venv at $VENV_DIR"
mkdir -p "$WORKDIR"
python3 -m venv "$VENV_DIR"
# shellcheck disable=SC1091
source "$VENV_DIR/bin/activate"
pip install --upgrade pip
pip install radicale vdirsyncer
}
# ---------------------------------------------------------------------------
# 3. Radicale server with a pre-seeded collection
# ---------------------------------------------------------------------------
seed_events() {
local coll_dir="$1"
mkdir -p "$coll_dir"
# Radicale's filesystem storage is vdir-compatible: one .ics per event
# plus a .Radicale.props JSON file for collection metadata.
cat > "$coll_dir/.Radicale.props" <<EOF
{"tag": "VCALENDAR", "D:displayname": "Work", "ICAL:calendar-color": "#3584e4"}
EOF
local today
today=$(date +%Y%m%d)
# All 3 events land on today so they show up in the calendar popup's
# default "Appointments" view (selected day = today) without needing
# to click around the month grid.
for hour in 09 13 17; do
local uid
uid="test-event-${hour}@example.com"
cat > "$coll_dir/$uid.ics" <<EOF
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//test//test//EN
BEGIN:VEVENT
UID:$uid
DTSTAMP:${today}T${hour}0000Z
DTSTART:${today}T${hour}0000Z
DTEND:${today}T$(( 10#$hour + 1 ))0000Z
SUMMARY:Test Event ${hour}:00 (radicale)
DESCRIPTION:Injected by test-vdir-clock.sh
END:VEVENT
END:VCALENDAR
EOF
done
}
start_radicale() {
log "Seeding Radicale collection with events in the current week"
seed_events "$RADICALE_DATA/collection-root/$RADICALE_USER/$RADICALE_COLLECTION"
cat > "$WORKDIR/radicale.conf" <<EOF
[server]
hosts = 127.0.0.1:$RADICALE_PORT
[auth]
type = none
[rights]
type = authenticated
[storage]
filesystem_folder = $RADICALE_DATA
EOF
log "Starting Radicale on 127.0.0.1:$RADICALE_PORT"
radicale --config "$WORKDIR/radicale.conf" &
RADICALE_PID=$!
sleep 2
curl -sf "http://127.0.0.1:$RADICALE_PORT/$RADICALE_USER/$RADICALE_COLLECTION/" \
-H "Depth: 1" -X PROPFIND >/dev/null \
|| { echo "Radicale did not come up"; exit 1; }
}
stop_radicale() {
if [[ -n "${RADICALE_PID:-}" ]]; then
log "Stopping Radicale (pid $RADICALE_PID)"
kill "$RADICALE_PID" 2>/dev/null || true
fi
}
trap stop_radicale EXIT
# ---------------------------------------------------------------------------
# 4. vdirsyncer: pull Radicale down into a local vdir
# ---------------------------------------------------------------------------
setup_vdirsyncer() {
log "Writing vdirsyncer config"
mkdir -p "$VDIR_LOCAL_DIR" "$VDIRSYNCER_STATUS"
cat > "$VDIRSYNCER_CONFIG" <<EOF
[general]
status_path = "$VDIRSYNCER_STATUS"
[pair test_pair]
a = "local"
b = "remote"
collections = ["from b"]
conflict_resolution = "b wins"
[storage local]
type = "filesystem"
path = "$VDIR_LOCAL_DIR"
fileext = ".ics"
[storage remote]
type = "caldav"
url = "http://127.0.0.1:$RADICALE_PORT/$RADICALE_USER/"
EOF
export VDIRSYNCER_CONFIG
log "vdirsyncer discover + sync"
vdirsyncer discover test_pair <<< "y"
vdirsyncer sync
log "Pointing clock applet at $VDIR_LOCAL_DIR via GSettings"
# org.mate.panel.applet.clock is relocatable; each panel applet instance
# gets its own prefs path under /org/mate/panel/objects/<id>/prefs/.
local clock_id
clock_id=$(dconf list /org/mate/panel/objects/ | grep -E '^(clock|object-[0-9]+)/$' | while read -r obj; do
iid=$(dconf read "/org/mate/panel/objects/${obj}applet-iid" 2>/dev/null)
[[ "$iid" == *"ClockApplet"* ]] && echo "${obj%/}" && break
done)
if [[ -z "$clock_id" ]]; then
echo "Could not find a clock applet instance in dconf; set vdir-calendar-paths manually:"
echo " gsettings set org.mate.panel.applet.clock:/org/mate/panel/objects/<id>/prefs/ vdir-calendar-paths \"['$VDIR_LOCAL_DIR/$RADICALE_COLLECTION']\""
else
gsettings set "org.mate.panel.applet.clock:/org/mate/panel/objects/$clock_id/prefs/" \
vdir-calendar-paths "['$VDIR_LOCAL_DIR/$RADICALE_COLLECTION']"
fi
}
# ---------------------------------------------------------------------------
# 5. Restart the panel and drive it with xdotool
# ---------------------------------------------------------------------------
exercise_clock_applet() {
log "Restarting mate-panel to pick up the freshly installed applet"
mate-panel --replace &
sleep 3
log "Locating the top panel window and clock applet"
local panel_win best_y=""
for win in $(xdotool search --class "mate-panel"); do
local geo geo_y geo_w
geo=$(xdotool getwindowgeometry --shell "$win")
geo_y=$(grep '^Y=' <<< "$geo" | cut -d= -f2)
geo_w=$(grep '^WIDTH=' <<< "$geo" | cut -d= -f2)
[[ -z "$geo_y" || -z "$geo_w" ]] && continue
# Skip tiny helper/proxy windows mate-panel also owns (e.g. 10x10).
(( geo_w < 200 )) && continue
if [[ -z "$best_y" || "$geo_y" -lt "$best_y" ]]; then
best_y="$geo_y"
panel_win="$win"
fi
done
if [[ -z "${panel_win:-}" ]]; then
echo "Could not find mate-panel window via xdotool"
exit 1
fi
eval "$(xdotool getwindowgeometry --shell "$panel_win")"
# Default assumption: top panel, clock lives near the right edge.
# Adjust CLOCK_X/CLOCK_Y below if your panel layout differs.
local click_x=$(( X + WIDTH - 60 ))
local click_y=$(( Y + HEIGHT / 2 ))
log "Screenshot before click: $WORKDIR/before-click.png"
import -window "$panel_win" "$WORKDIR/before-click.png" 2>/dev/null || \
xwd -root -out "$WORKDIR/before-click.xwd"
log "Clicking clock applet at ($click_x, $click_y)"
xdotool mousemove "$click_x" "$click_y" click 1
sleep 1
local cal_win
cal_win=$(xdotool search --name "^Calendar$" | head -1 || true)
if [[ -z "$cal_win" ]]; then
echo "Calendar popup did not appear at the guessed coordinates."
echo "Inspect $WORKDIR/before-click.png, adjust click_x/click_y, and retry."
exit 1
fi
#log "Calendar popup found (window $cal_win), taking screenshot"
#import -window "$cal_win" "$WORKDIR/calendar-popup.png" 2>/dev/null || true
#echo "Verify events visually in $WORKDIR/calendar-popup.png"
}
# ---------------------------------------------------------------------------
main() {
build_applet
setup_python_env
start_radicale
setup_vdirsyncer
exercise_clock_applet
log "Done. Radicale left running for further inspection; Ctrl-C to stop."
wait "$RADICALE_PID"
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment