summaryrefslogtreecommitdiff
path: root/pim/rc
diff options
context:
space:
mode:
Diffstat (limited to 'pim/rc')
-rwxr-xr-xpim/rc/agenda88
-rw-r--r--pim/rc/calendars38
-rwxr-xr-xpim/rc/fetch56
-rwxr-xr-xpim/rc/find77
-rwxr-xr-xpim/rc/month4
-rwxr-xr-xpim/rc/next81
-rwxr-xr-xpim/rc/show115
-rwxr-xr-xpim/rc/showwin10
-rwxr-xr-xpim/rc/today4
-rwxr-xr-xpim/rc/week4
-rwxr-xr-xpim/rc/who87
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