diff options
Diffstat (limited to 'pim/rc')
| -rwxr-xr-x | pim/rc/agenda | 88 | ||||
| -rw-r--r-- | pim/rc/calendars | 38 | ||||
| -rwxr-xr-x | pim/rc/fetch | 56 | ||||
| -rwxr-xr-x | pim/rc/find | 77 | ||||
| -rwxr-xr-x | pim/rc/month | 4 | ||||
| -rwxr-xr-x | pim/rc/next | 81 | ||||
| -rwxr-xr-x | pim/rc/show | 115 | ||||
| -rwxr-xr-x | pim/rc/showwin | 10 | ||||
| -rwxr-xr-x | pim/rc/today | 4 | ||||
| -rwxr-xr-x | pim/rc/week | 4 | ||||
| -rwxr-xr-x | pim/rc/who | 87 |
11 files changed, 564 insertions, 0 deletions
diff --git a/pim/rc/agenda b/pim/rc/agenda new file mode 100755 index 0000000..ec38ad2 --- /dev/null +++ b/pim/rc/agenda @@ -0,0 +1,88 @@ +#!/bin/rc +# pim/agenda -- what is happening, across every mounted calendar. +rfork e + +mtpt=/mnt/pim +days=7 +off=0 +only=() + +while(~ $1 -*){ + switch($1){ + case -m + mtpt=$2; shift + case -n + days=$2; shift + case -o + off=$2; shift + case -c + only=($only $2); shift + case * + echo 'usage: agenda [-m mtpt] [-n days] [-o dayoffset] [-c cal]' >[1=2] + exit usage + } + shift +} + +cals=$only +if(~ $#cals 0) + cals=`{pim/calendars -m $mtpt} +if(~ $#cals 0){ + echo 'pim/agenda: no calendars under '^$mtpt^'/calendars' >[1=2] + exit nocal +} +# only worth naming the calendar when more than one is in play +tag=0 +if(! ~ $#cals 1) + tag=1 + +tmp=/tmp/agenda.$pid +fn sigexit { rm -f $tmp } + +now=`{date -n} +i=$off +last=`{echo $off + $days | bc} +n=0 + +while(test $i -lt $last){ + sec=`{echo $now + $i '*' 86400 | bc} + day=`{date -f YYYY/MM/DD $sec} + + # name, calendar, summary, location -- one line per occurrence, + # keyed by the file name so a sort interleaves the calendars by time + { + for(c in $cals){ + d=$mtpt/calendars/$c/events/date/$day + if(test -d $d) + for(f in $d/*){ + b=`{basename $f} + sum=`{sed -n 's/^summary: //p' $f} + loc=`{sed -n 's/^location: //p' $f} + echo $"b^' '^$"c^' '^$"sum^' '^$"loc + } + } + } | sort >$tmp + + if(test -s $tmp){ + if(test $n -gt 0) + echo + n=1 + date -f 'WWW DD MMM YYYY' $sec + awk -F' ' -v 'tag='^$tag '{ + if ($1 ~ /^0000-allday-/) + t = "all day" + else + t = substr($1,1,2) ":" substr($1,3,2) + s = "\t" t "\t" $3 + if ($4 != "") + s = s " (" $4 ")" + if (tag) + s = s "\t" $2 + print s + }' $tmp + } + i=`{echo $i + 1 | bc} +} +if(~ $n 0) + echo 'nothing in the next '^$days^' days' +exit 0 diff --git a/pim/rc/calendars b/pim/rc/calendars new file mode 100644 index 0000000..e1f191e --- /dev/null +++ b/pim/rc/calendars @@ -0,0 +1,38 @@ +#!/bin/rc +# pim/calendars -- name every mounted calendar, one per line. +# +# rc globs only metacharacters written literally in the source, so a +# pattern held in a variable will not expand. Every tool that works +# across calendars enumerates them through here instead. +rfork e + +mtpt=/mnt/pim +long=() +while(~ $1 -*){ + switch($1){ + case -m + mtpt=$2; shift + case -l + long=1 + case * + echo 'usage: calendars [-m mtpt] [-l]' >[1=2] + exit usage + } + shift +} +# mntgen leaves the directory behind when a server goes away, so an +# empty name is not a calendar. A live one answers for ctl. +for(c in `{ls -p $mtpt/calendars >[2]/dev/null}) + if(test -f $mtpt/calendars/$c/ctl){ + if(~ $#long 0) + echo $c + if(! ~ $#long 0){ + # caps says what the backend can do: a published .ics is + # read-only, with no iMIP path and nothing to write to + caps=`{sed -n 's/^caps //p' $mtpt/calendars/$c/ctl} + if(~ $#caps 0) + caps=unknown + echo $c^' '^$"caps + } + } +exit 0 diff --git a/pim/rc/fetch b/pim/rc/fetch new file mode 100755 index 0000000..2208cc7 --- /dev/null +++ b/pim/rc/fetch @@ -0,0 +1,56 @@ +#!/bin/rc +# pim/fetch -- refresh a subscribed calendar from its url. +# +# pim/fetch $home/lib/cal/work.url $home/lib/cal/work.ics \ +# /mnt/pim/calendars/work/ctl +# +# ical/fs does not fetch: a subscribed calendar is a file somebody else +# wrote. This is that somebody. It writes the file only when the content +# actually changed, then pokes ctl so the server reloads at once instead +# of waiting to notice. +# +# The url of a private calendar is a secret, so it is read from a file +# rather than passed as an argument where ps(1) would show it. +rfork e + +if(! ~ $#* 2 && ! ~ $#* 3){ + echo 'usage: fetch urlfile dest.ics [ctl]' >[1=2] + exit usage +} +urlfile=$1 +dest=$2 +ctl=$3 + +if(! test -r $urlfile){ + echo 'pim/fetch: cannot read '^$urlfile >[1=2] + exit nourl +} +url=`{sed -n '/^#/d; /^$/d; s/["'']//g; p; q' $urlfile} +if(~ $#url 0){ + echo 'pim/fetch: no url in '^$urlfile >[1=2] + exit nourl +} + +tmp=$dest.new +if(! hget $"url > $tmp){ + rm -f $tmp + echo 'pim/fetch: fetch failed' >[1=2] + exit fetch +} +if(! test -s $tmp){ + rm -f $tmp + echo 'pim/fetch: empty response' >[1=2] + exit empty +} + +# a fetch that changes nothing must not rebuild the tree or wake watchers +if(test -f $dest) + if(cmp -s $tmp $dest){ + rm -f $tmp + exit 0 + } +mv $tmp $dest +if(! ~ $#ctl 0) + if(test -f $ctl) + echo refresh >$ctl +exit 0 diff --git a/pim/rc/find b/pim/rc/find new file mode 100755 index 0000000..8ee162a --- /dev/null +++ b/pim/rc/find @@ -0,0 +1,77 @@ +#!/bin/rc +# pim/find -- ask every calendar a question, by attribute. +# +# pim/find attendee michael +# pim/find summary standup from 2026-08-19 to 2026-08-26 +# pim/find -c work attendee michael +# +# Attributes are separate words, as ndb/query takes them: rc lexes a bare +# attr=value as an assignment, so it cannot be an argument unquoted. +rfork e + +mtpt=/mnt/pim +long=() +only=() + +while(~ $1 -*){ + switch($1){ + case -m + mtpt=$2; shift + case -c + only=($only $2); shift + case -l + long=1 + case * + echo 'usage: find [-m mtpt] [-c cal] [-l] attr value ...' >[1=2] + exit usage + } + shift +} +if(~ $#* 0){ + echo 'usage: find [-m mtpt] [-c cal] [-l] attr value ...' >[1=2] + exit usage +} + +q=() +while(! ~ $#* 0){ + switch($1){ + case '*=*' + q=($q $1) + shift + case * + if(~ $#* 1){ + echo 'pim/find: '^$1^' has no value' >[1=2] + exit usage + } + q=($q $1^'='^$2) + shift; shift + } +} + +cals=$only +if(~ $#cals 0) + cals=`{pim/calendars -m $mtpt} + +# the server answers with paths relative to its own root, since it +# cannot know where it was mounted +hits=() +for(c in $cals){ + echo $q >$mtpt/calendars/$c/query + for(r in `{cat $mtpt/calendars/$c/query}) + hits=($hits $mtpt/calendars/$c/$r) +} + +if(~ $#hits 0) + exit 0 +if(~ $#long 0){ + for(h in $hits) + echo $h + exit 0 +} +for(h in $hits){ + ep=`{sed -n 's/^epoch: //p' $h} + sum=`{sed -n 's/^summary: //p' $h} + when=`{date -f 'WWW DD MMM YYYY hh:mm' $"ep} + echo $"when^' '^$"sum +} +exit 0 diff --git a/pim/rc/month b/pim/rc/month new file mode 100755 index 0000000..1fdbe6f --- /dev/null +++ b/pim/rc/month @@ -0,0 +1,4 @@ +#!/bin/rc +# pim/month -- the next thirty-one days. +rfork e +exec pim/agenda -n 31 $* diff --git a/pim/rc/next b/pim/rc/next new file mode 100755 index 0000000..0436cb3 --- /dev/null +++ b/pim/rc/next @@ -0,0 +1,81 @@ +#!/bin/rc +# pim/next -- the next thing, one line, for a window label. +rfork e + +# test(1) reads a leading zero as octal: 0700 compares as 448 and 0900 +# is a syntax error. Strip the padding before any arithmetic. +fn num { + echo $1 | sed 's/^0*//; s/^$/0/' +} + +mtpt=/mnt/pim +only=() +while(~ $1 -*){ + switch($1){ + case -m + mtpt=$2; shift + case -c + only=($only $2); shift + case * + echo 'usage: next [-m mtpt] [-c cal]' >[1=2] + exit usage + } + shift +} +cals=$only +if(~ $#cals 0) + cals=`{pim/calendars -m $mtpt} + +now=`{date -n} +hhmm=`{num `{date -f hhmm $now}} +tmp=/tmp/next.$pid +fn sigexit { rm -f $tmp } + +i=0 +while(test $i -lt 14){ + sec=`{echo $now + $i '*' 86400 | bc} + day=`{date -f YYYY/MM/DD $sec} + { + for(c in $cals){ + d=$mtpt/calendars/$c/events/date/$day + if(test -d $d) + for(f in $d/*){ + b=`{basename $f} + sum=`{sed -n 's/^summary: //p' $f} + echo $"b^' '^$"c^' '^$"sum + } + } + } | sort >$tmp + if(test -s $tmp){ + day='' + if(test $i -gt 0){ + d=`{date -f 'WWW DD MMM' $sec} + day=$"d^' ' + } + # awk prints the finished line: a value carried back through + # an rc variable would be split on its tabs and rejoined + # with spaces + out=/tmp/next.out.$pid + awk -F' ' -v 'first='^$i -v 'now='^$"hhmm -v 'day='^$"day ' + { + t = $1 + 0 + if (first > 0 || t >= now) { + split($1, a, "-") + when = substr(a[1],1,2) ":" substr(a[1],3,2) + if ($1 ~ /^0000-allday-/) + when = "all day" + printf "%s%s %s\n", day, when, $3 + exit + } + }' $tmp >$out + if(test -s $out){ + cat $out + rm -f $out $tmp + exit 0 + } + rm -f $out + } + i=`{echo $i + 1 | bc} +} +echo 'nothing scheduled' +exit 0 diff --git a/pim/rc/show b/pim/rc/show new file mode 100755 index 0000000..da23973 --- /dev/null +++ b/pim/rc/show @@ -0,0 +1,115 @@ +#!/bin/rc +# pim/show -- everything known about one event. +# +# Takes a path to an occurrence under events/date/, as printed by +# "pim/agenda -p" or plumbed from a window, or a pattern to search for. +rfork e + +mtpt=/mnt/pim +days=90 +all=() +only=() + +while(~ $1 -*){ + switch($1){ + case -m + mtpt=$2; shift + case -n + days=$2; shift + case -a + all=1 + case -c + only=($only $2); shift + case * + echo 'usage: show [-m mtpt] [-n days] [-c cal] [-a] path|pattern' >[1=2] + exit usage + } + shift +} +if(~ $#* 0){ + echo 'usage: show [-m mtpt] [-n days] [-c cal] [-a] path|pattern' >[1=2] + exit usage +} +pat=$"* + +fn field { + sed -n 's/^'^$2^': //p' $1 +} + +fn one { + f=$1 + d=`{basename -d $f} + sum=`{field $f summary} + echo $"sum + echo $"sum | sed 's/./-/g' + ep=`{field $f epoch} + ee=`{field $f epochend} + if(~ $#ep 0) + sed -n 's/^start: /when /p' $f + if(! ~ $#ep 0){ + # backquotes give a list and ^ distributes over it; flatten first + w=`{date -f 'WWW DD MMM YYYY hh:mm' $"ep} + x=`{date -f hh:mm $"ee} + echo 'when '^$"w^'-'^$"x + } + sed -n 's/^location: /where /p' $f + sed -n 's/^rrule: /repeats /p' $f + ev=`{field $f event} + if(! ~ $#ev 0){ + e=$d/$"ev + if(test -f $e/organizer){ + o=`{cat $e/organizer} + echo 'from '^$"o + } + if(test -f $e/attendees){ + echo who + sed 's/^/ /' $e/attendees + } + echo 'event '^`{cleanname $"e} + } + echo 'at '^$"f + # a blank line ends the header; the rest is the description + sed -n '/^$/,$p' $f | sed 1d +} + +fn search { + cals=$only + if(~ $#cals 0) + cals=`{pim/calendars -m $mtpt} + now=`{date -n} + i=0 + while(test $i -lt $days){ + sec=`{echo $now + $i '*' 86400 | bc} + day=`{date -f YYYY/MM/DD $sec} + for(c in $cals){ + d=$mtpt/calendars/$c/events/date/$day + if(test -d $d) + grep -li '^summary: .*'^$"pat $d/* >[2]/dev/null + } + i=`{echo $i + 1 | bc} + } +} + +if(test -f $"pat){ + one $"pat + exit 0 +} + +hits=`{search} +if(~ $#hits 0){ + echo 'pim/show: nothing matching '^$"pat^' in the next '^$"days^' days' >[1=2] + exit notfound +} +if(! ~ $#all 0){ + for(h in $hits){ + one $h + echo + } + exit 0 +} +one $hits(1) +if(test $#hits -gt 1){ + echo + echo '('^`{echo $#hits - 1 | bc}^' more; -a for all)' +} +exit 0 diff --git a/pim/rc/showwin b/pim/rc/showwin new file mode 100755 index 0000000..d71546b --- /dev/null +++ b/pim/rc/showwin @@ -0,0 +1,10 @@ +#!/bin/rc +# pim/showwin -- show an event in a window and stay there. +# +# rio closes a window when its command exits, so this leaves an +# interactive shell behind: the event stays up while you carry on +# elsewhere, and the window is still a window. +rfork e +pim/show $* +echo +exec rc -i diff --git a/pim/rc/today b/pim/rc/today new file mode 100755 index 0000000..6f458c7 --- /dev/null +++ b/pim/rc/today @@ -0,0 +1,4 @@ +#!/bin/rc +# pim/today -- just today. +rfork e +exec pim/agenda -n 1 $* diff --git a/pim/rc/week b/pim/rc/week new file mode 100755 index 0000000..1d85529 --- /dev/null +++ b/pim/rc/week @@ -0,0 +1,4 @@ +#!/bin/rc +# pim/week -- the next seven days. +rfork e +exec pim/agenda -n 7 $* diff --git a/pim/rc/who b/pim/rc/who new file mode 100755 index 0000000..353ca4f --- /dev/null +++ b/pim/rc/who @@ -0,0 +1,87 @@ +#!/bin/rc +# pim/who -- who you met with, over a range of days. +# +# pim/who the last seven days +# pim/who -n 30 the last thirty +# pim/who -a only those who accepted +# pim/who -x calvinm leave yourself out +rfork e + +mtpt=/mnt/pim +days=7 +only=() +me=() +acc=() + +while(~ $1 -*){ + switch($1){ + case -m + mtpt=$2; shift + case -n + days=$2; shift + case -x + me=$2; shift + case -a + acc=1 + case -c + only=($only $2); shift + case * + echo 'usage: who [-m mtpt] [-n days] [-x me] [-a]' >[1=2] + exit usage + } + shift +} + +now=`{date -n} +from=`{date -i `{echo $now - $days '*' 86400 | bc}} +to=`{date -i `{echo $now + 86400 | bc}} + +cals=$only +if(~ $#cals 0) + cals=`{pim/calendars -m $mtpt} + +# Who counts as you comes from the calendar's me= in the config, which +# the server reports in ctl. -x adds to it. +ex=/tmp/who.ex.$pid +fn sigexit { rm -f $ex $tmp } +>$ex +if(! ~ $#me 0) + echo $me >>$ex +for(c in $cals) + sed -n 's/^cal .* me //p' $mtpt/calendars/$c/ctl >[2]/dev/null | + tr , ' ' | tr ' ' ' +' >>$ex +grep -v '^$' $ex >$ex^.t; mv $ex^.t $ex + +hits=() +for(c in $cals){ + echo 'from='^$"from^' to='^$"to >$mtpt/calendars/$c/query + for(r in `{cat $mtpt/calendars/$c/query}) + hits=($hits $mtpt/calendars/$c/$r) +} +if(~ $#hits 0){ + echo 'nobody, in the last '^$"days^' days' + exit 0 +} + +fn people { + for(h in $hits){ + d=`{basename -d $h} + ev=`{sed -n 's/^event: //p' $h} + if(! ~ $#ev 0){ + f=$d/$"ev/attendees + if(test -f $f){ + if(~ $#acc 0) + awk -F'\t' 'NF>2{print $3}' $f + if(! ~ $#acc 0) + awk -F'\t' 'NF>2 && $1 ~ /ACCEPTED/{print $3}' $f + } + } + } +} + +if(test -s $ex) + people | grep -v -f $ex | sort | uniq -c | sort -nr +if(! test -s $ex) + people | sort | uniq -c | sort -nr +exit 0 |
