summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--pim/cmd/caldavfs/main.go5
-rw-r--r--pim/cmd/caldavfs/rsvp.go44
-rw-r--r--pim/lib/cal/backend.go17
-rw-r--r--pim/lib/cal/compose.go112
-rw-r--r--pim/lib/cal/server.go86
-rwxr-xr-xpim/rc/agenda15
-rwxr-xr-xpim/rc/invite137
-rwxr-xr-xpim/rc/invites87
-rwxr-xr-xpim/rc/rsvp111
-rwxr-xr-xpim/rc/show16
10 files changed, 623 insertions, 7 deletions
diff --git a/pim/cmd/caldavfs/main.go b/pim/cmd/caldavfs/main.go
index 7a0a4d3..de37566 100644
--- a/pim/cmd/caldavfs/main.go
+++ b/pim/cmd/caldavfs/main.go
@@ -67,7 +67,7 @@ func (b *backend) Name() string { return b.name }
func (b *backend) Sched() bool { return b.sched }
func (b *backend) Caps() string {
- c := "read rsvp"
+ c := "read write rsvp"
if b.sched {
// the server sends the iMIP for us; one PUT does both halves
c += " schedule"
@@ -90,6 +90,9 @@ func (b *backend) Status() (time.Time, string) {
func (b *backend) Describe() string {
s := fmt.Sprintf("collection %s\n", b.path)
s += fmt.Sprintf("autoschedule %v\n", b.sched)
+ if len(b.me) > 0 {
+ s += "me " + strings.Join(b.me, ",") + "\n"
+ }
return s
}
diff --git a/pim/cmd/caldavfs/rsvp.go b/pim/cmd/caldavfs/rsvp.go
index 47d7425..25015a7 100644
--- a/pim/cmd/caldavfs/rsvp.go
+++ b/pim/cmd/caldavfs/rsvp.go
@@ -149,3 +149,47 @@ func quoteETag(s string) string {
}
return `"` + s + `"`
}
+
+// Create adds an event to the collection.
+//
+// The path is ours to choose, so it is derived from the UID: a server
+// that already has that object will replace it, which is what a second
+// PUT of the same event should do. With calendar-auto-schedule the
+// server mails every ATTENDEE for us, so creating an event with
+// attendees is the whole of sending an invitation.
+func (b *backend) Create(c *ical.Calendar) error {
+ var uid string
+ for _, e := range c.Events() {
+ if u, err := e.Props.Text(ical.PropUID); err == nil && u != "" {
+ uid = u
+ break
+ }
+ }
+ if uid == "" {
+ return fmt.Errorf("create: the event has no UID")
+ }
+
+ name := slugUID(uid) + ".ics"
+ path := strings.TrimSuffix(b.path, "/") + "/" + name
+ // no etag: this is a create, and If-Match on nothing is meaningless
+ return b.put(objref{path: path}, c)
+}
+
+// slugUID makes a UID safe as one path element.
+func slugUID(s string) string {
+ var out []rune
+ for _, r := range s {
+ switch {
+ case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9':
+ out = append(out, r)
+ case r == '-', r == '.', r == '_':
+ out = append(out, r)
+ default:
+ out = append(out, '-')
+ }
+ }
+ if len(out) > 100 {
+ out = out[:100]
+ }
+ return string(out)
+}
diff --git a/pim/lib/cal/backend.go b/pim/lib/cal/backend.go
index 6ed488c..91f793a 100644
--- a/pim/lib/cal/backend.go
+++ b/pim/lib/cal/backend.go
@@ -1,6 +1,10 @@
package cal
-import "time"
+import (
+ "time"
+
+ "github.com/emersion/go-ical"
+)
// A Backend supplies a calendar's events and, where the protocol allows
// it, accepts changes back.
@@ -49,6 +53,17 @@ type Identity interface {
Me() []string
}
+// Creator is implemented by a backend that can add an event.
+//
+// It takes a whole object rather than fields, because that is what the
+// protocols underneath take: CalDAV PUTs an entire icalendar object and
+// JMAP patches a whole JSCalendar one. Building an event field by field
+// and hoping the server knows when you have finished is a shape that
+// fits neither.
+type Creator interface {
+ Create(c *ical.Calendar) error
+}
+
// RSVPer is implemented by a backend that can answer an invitation.
//
// Accepting is two writes: update your own copy, and tell the organiser.
diff --git a/pim/lib/cal/compose.go b/pim/lib/cal/compose.go
new file mode 100644
index 0000000..1960696
--- /dev/null
+++ b/pim/lib/cal/compose.go
@@ -0,0 +1,112 @@
+package cal
+
+import (
+ "bufio"
+ "fmt"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/emersion/go-ical"
+)
+
+// Compose builds an event from the same key: value form the tree emits.
+//
+// summary: lunch, then a walk; maybe
+// start: 1787500800
+// end: 1787504400
+// location: the wire
+// attendee: bob@example.com
+// attendee: carol@example.com
+// organizer: you@example.com
+// description: anything
+//
+// Accepting this rather than raw icalendar keeps the escaping where the
+// parser already lives. A shell script writing SUMMARY: by hand has to
+// know that a comma means something, and will eventually forget; here
+// it writes what it means and go-ical spells it.
+//
+// Times are epoch seconds, because that is what date(1) hands a script.
+func Compose(r *bufio.Reader, uid string) (*ical.Calendar, error) {
+ f := map[string][]string{}
+ for {
+ line, err := r.ReadString('\n')
+ if line = strings.TrimRight(line, "\r\n"); line != "" {
+ k, v, ok := strings.Cut(line, ":")
+ if !ok {
+ return nil, fmt.Errorf("compose: %q is not key: value", line)
+ }
+ k = strings.ToLower(strings.TrimSpace(k))
+ f[k] = append(f[k], strings.TrimSpace(v))
+ }
+ if err != nil {
+ break
+ }
+ }
+
+ one := func(k string) string {
+ if v := f[k]; len(v) > 0 {
+ return v[0]
+ }
+ return ""
+ }
+ at := func(k string) (time.Time, error) {
+ v := one(k)
+ if v == "" {
+ return time.Time{}, fmt.Errorf("compose: no %s", k)
+ }
+ n, err := strconv.ParseInt(v, 10, 64)
+ if err != nil {
+ return time.Time{}, fmt.Errorf("compose: %s: want epoch seconds, got %q", k, v)
+ }
+ return time.Unix(n, 0).UTC(), nil
+ }
+
+ if one("summary") == "" {
+ return nil, fmt.Errorf("compose: no summary")
+ }
+ start, err := at("start")
+ if err != nil {
+ return nil, err
+ }
+ end, err := at("end")
+ if err != nil {
+ return nil, err
+ }
+
+ ev := ical.NewEvent()
+ ev.Props.SetText(ical.PropUID, uid)
+ ev.Props.SetDateTime(ical.PropDateTimeStamp, time.Now().UTC())
+ ev.Props.SetDateTime(ical.PropDateTimeStart, start)
+ ev.Props.SetDateTime(ical.PropDateTimeEnd, end)
+ ev.Props.SetText(ical.PropSummary, one("summary"))
+ if v := one("location"); v != "" {
+ ev.Props.SetText(ical.PropLocation, v)
+ }
+ if v := one("description"); v != "" {
+ ev.Props.SetText(ical.PropDescription, v)
+ }
+ if v := one("organizer"); v != "" {
+ p := ical.NewProp(ical.PropOrganizer)
+ p.Value = "mailto:" + v
+ ev.Props.Set(p)
+ // the organiser is attending their own meeting
+ a := ical.NewProp(ical.PropAttendee)
+ a.Value = "mailto:" + v
+ a.Params.Set(ical.ParamParticipationStatus, "ACCEPTED")
+ ev.Props.Add(a)
+ }
+ for _, who := range f["attendee"] {
+ a := ical.NewProp(ical.PropAttendee)
+ a.Value = "mailto:" + who
+ a.Params.Set(ical.ParamParticipationStatus, "NEEDS-ACTION")
+ a.Params.Set("RSVP", "TRUE")
+ ev.Props.Add(a)
+ }
+
+ c := ical.NewCalendar()
+ c.Props.SetText(ical.PropProductID, "-//9front//pim//EN")
+ c.Props.SetText(ical.PropVersion, "2.0")
+ c.Children = append(c.Children, ev.Component)
+ return c, nil
+}
diff --git a/pim/lib/cal/server.go b/pim/lib/cal/server.go
index f9da81e..4b588c5 100644
--- a/pim/lib/cal/server.go
+++ b/pim/lib/cal/server.go
@@ -14,6 +14,10 @@
package cal
import (
+ "bufio"
+ "bytes"
+ "crypto/rand"
+ "encoding/hex"
"fmt"
"os"
"sort"
@@ -22,6 +26,7 @@ import (
"sync"
"time"
+ "github.com/emersion/go-ical"
"github.com/knusbaum/go9p"
"github.com/knusbaum/go9p/fs"
)
@@ -81,6 +86,7 @@ func New(be Backend, c Config) *Server {
s.addAlarm()
s.addQuery()
s.addChanged()
+ s.addNew()
return s
}
@@ -323,3 +329,83 @@ func orNone(s string) string {
}
return s
}
+
+// addNew publishes the file you write an event to.
+//
+// cat invite.ics >/mnt/pim/calendars/work/new
+//
+// Writes are buffered and the object is created on close, because that
+// is when the thing you are writing is complete -- cp and cat both do
+// create, write, clunk, so the commit point falls out of 9p rather than
+// having to be invented.
+func (s *Server) addNew() {
+ create, ok := s.be.(Creator)
+ if !ok {
+ return // a backend that cannot add events does not offer the file
+ }
+ var mu sync.Mutex
+ buf := map[uint64][]byte{}
+
+ st := s.fsys.NewStat("new", s.user, s.user, 0222)
+ base := fs.NewStaticFile(st, []byte(""))
+ s.root.AddChild(&fs.WrappedFile{
+ File: base,
+ WriteF: func(fid uint64, off uint64, data []byte) (uint32, error) {
+ mu.Lock()
+ buf[fid] = append(buf[fid], data...)
+ mu.Unlock()
+ return uint32(len(data)), nil
+ },
+ CloseF: func(fid uint64) error {
+ mu.Lock()
+ b := buf[fid]
+ delete(buf, fid)
+ mu.Unlock()
+ if len(b) == 0 {
+ return nil
+ }
+ // Either a whole icalendar object, or the key: value form
+ // the tree itself emits. The second exists so a script
+ // never has to spell iCalendar, where a comma in a summary
+ // is a syntax error waiting to happen.
+ var c *ical.Calendar
+ var err error
+ if bytes.HasPrefix(bytes.TrimSpace(b), []byte("BEGIN:VCALENDAR")) {
+ c, err = ical.NewDecoder(bytes.NewReader(b)).Decode()
+ } else {
+ c, err = Compose(bufio.NewReader(bytes.NewReader(b)), newUID())
+ }
+ if err != nil {
+ Warnf("new: %v", err)
+ return err
+ }
+ if len(c.Events()) == 0 {
+ Warnf("new: no VEVENT")
+ return fmt.Errorf("new: no VEVENT")
+ }
+ if err := create.Create(c); err != nil {
+ Warnf("new: %v", err)
+ return err
+ }
+ if _, err := s.be.Sync(); err == nil {
+ s.Reload()
+ }
+ return nil
+ },
+ })
+}
+
+// newUID mints an identifier for an event we are creating. This is the
+// one place a UID is generated rather than echoed back: as organiser we
+// own the event, so we name it.
+func newUID() string {
+ var b [16]byte
+ if _, err := rand.Read(b[:]); err != nil {
+ return fmt.Sprintf("%d@pim", time.Now().UnixNano())
+ }
+ host, _ := os.Hostname()
+ if host == "" {
+ host = "pim"
+ }
+ return hex.EncodeToString(b[:]) + "@" + host
+}
diff --git a/pim/rc/agenda b/pim/rc/agenda
index ec38ad2..563758f 100755
--- a/pim/rc/agenda
+++ b/pim/rc/agenda
@@ -6,6 +6,7 @@ mtpt=/mnt/pim
days=7
off=0
only=()
+start=()
while(~ $1 -*){
switch($1){
@@ -15,10 +16,18 @@ while(~ $1 -*){
days=$2; shift
case -o
off=$2; shift
+ case -d
+ # seconds(1) parses a human date; date(1) only formats one
+ start=`{seconds $2}
+ if(~ $#start 0){
+ echo 'pim/agenda: cannot read the date '^$2 >[1=2]
+ exit baddate
+ }
+ shift
case -c
only=($only $2); shift
case *
- echo 'usage: agenda [-m mtpt] [-n days] [-o dayoffset] [-c cal]' >[1=2]
+ echo 'usage: agenda [-m mtpt] [-n days] [-d date] [-o dayoffset] [-c cal]' >[1=2]
exit usage
}
shift
@@ -39,7 +48,9 @@ if(! ~ $#cals 1)
tmp=/tmp/agenda.$pid
fn sigexit { rm -f $tmp }
-now=`{date -n}
+now=$start
+if(~ $#now 0)
+ now=`{date -n}
i=$off
last=`{echo $off + $days | bc}
n=0
diff --git a/pim/rc/invite b/pim/rc/invite
new file mode 100755
index 0000000..cf2e69c
--- /dev/null
+++ b/pim/rc/invite
@@ -0,0 +1,137 @@
+#!/bin/rc
+# pim/invite -- make an event, and invite people to it.
+#
+# pim/invite -c fastmail 'catch up' '2026-08-28 15:00' 30
+# pim/invite -c fastmail -a bob@example.com -l 'the wire' \
+# 'design review' 'tomorrow 14:00' 60
+#
+# What this really does is write an icalendar object. Where it goes is
+# somebody else's business:
+#
+# -p writes it to standard output, so you can mail it yourself
+# (upas/marshal -t 'text/calendar; method=REQUEST'), keep it,
+# or drop it in a directory some ical/fs is serving
+# otherwise it goes to the calendar's new file, and a backend with
+# calendar-auto-schedule mails every attendee for you
+#
+# So there is no separate verb for "invite": to a calendar there is no
+# separate thing, and to everything else it is just a file.
+rfork e
+
+mtpt=/mnt/pim
+print=()
+cal=()
+att=()
+loc=()
+desc=()
+
+while(~ $1 -*){
+ switch($1){
+ case -m
+ mtpt=$2; shift
+ case -c
+ cal=$2; shift
+ case -a
+ att=($att $2); shift
+ case -l
+ loc=$2; shift
+ case -D
+ desc=$2; shift
+ case -p
+ print=1
+ case *
+ echo 'usage: invite [-m mtpt] [-c cal] [-a attendee]... [-l where] [-D text] [-p] summary start [minutes]' >[1=2]
+ exit usage
+ }
+ shift
+}
+if(test $#* -lt 2){
+ echo 'usage: invite [-m mtpt] [-c cal] [-a attendee]... [-l where] [-D text] summary start [minutes]' >[1=2]
+ exit usage
+}
+summary=$1
+when=$2
+mins=30
+if(! ~ $#* 2)
+ mins=$3
+
+# with -p the object goes to standard output and no calendar is needed
+if(! ~ $#print 0){
+ if(~ $#cal 0)
+ cal=`{pim/calendars -m $mtpt | sed 1q}
+}
+if(~ $#print 0 && ~ $#cal 0){
+ for(c in `{pim/calendars -m $mtpt}){
+ caps=`{sed -n 's/^caps //p' $mtpt/calendars/$c/ctl >[2]/dev/null}
+ for(x in $caps)
+ if(~ $x write)
+ cal=($cal $c)
+ }
+ if(~ $#cal 0){
+ echo 'pim/invite: no calendar here can create events' >[1=2]
+ exit nowhere
+ }
+ if(! ~ $#cal 1){
+ echo 'pim/invite: which calendar? -c one of: '^$"cal >[1=2]
+ exit ambiguous
+ }
+}
+d=$mtpt/calendars/$"cal
+if(~ $#print 0)
+ if(! test -f $d/new){
+ echo 'pim/invite: '^$"cal^' cannot create events' >[1=2]
+ exit readonly
+ }
+
+start=`{seconds $"when}
+if(~ $#start 0){
+ echo 'pim/invite: cannot read the time '^$"when >[1=2]
+ exit baddate
+}
+# organiser is whoever this calendar says we are
+me=`{sed -n 's/^me //p' $d/ctl >[2]/dev/null | sed 's/,.*//'}
+if(~ $#att 0)
+ me=()
+if(! ~ $#att 0)
+ if(~ $#me 0){
+ echo 'pim/invite: '^$"cal^' has no me=; cannot say who is organising' >[1=2]
+ exit nome
+ }
+
+# The event is written as key: value, the same form the tree emits and
+# calfs composes from. A script spelling SUMMARY: by hand has to know
+# that a comma means something there, and will eventually forget; this
+# way go-ical does the spelling and the escaping.
+end=`{echo $"start + $"mins '*' 60 | bc}
+
+tmp=/tmp/invite.$pid
+fn sigexit { rm -f $tmp }
+{
+ echo 'summary: '^$"summary
+ echo 'start: '^$"start
+ echo 'end: '^$"end
+ if(! ~ $#loc 0)
+ echo 'location: '^$"loc
+ if(! ~ $#desc 0)
+ echo 'description: '^$"desc
+ if(! ~ $#att 0){
+ echo 'organizer: '^$"me
+ for(a in $att)
+ echo 'attendee: '^$a
+ }
+} >$tmp
+
+if(! ~ $#print 0){
+ cat $tmp
+ exit 0
+}
+cat $tmp >$d/new
+if(! ~ $status ''){
+ echo 'pim/invite: '^$"status >[1=2]
+ exit failed
+}
+w=`{date -f 'WWW DD MMM hh:mm' $"start}
+echo $"summary^': '^$"w^' on '^$"cal
+if(! ~ $#att 0)
+ echo 'invited: '^$"att
+exit 0
diff --git a/pim/rc/invites b/pim/rc/invites
new file mode 100755
index 0000000..de193ed
--- /dev/null
+++ b/pim/rc/invites
@@ -0,0 +1,87 @@
+#!/bin/rc
+# pim/invites -- what is waiting for your answer.
+#
+# pim/invites the next thirty days
+# pim/invites -n 7 just this week
+# pim/invites -a everything, answered or not
+#
+# Only upcoming events are looked at: an invitation you never answered
+# for a meeting last March is not a thing you need to see. Walking the
+# date tree rather than every event also keeps this to a few dozen reads
+# instead of a few thousand.
+rfork e
+
+mtpt=/mnt/pim
+days=30
+only=()
+all=()
+
+while(~ $1 -*){
+ switch($1){
+ case -m
+ mtpt=$2; shift
+ case -n
+ days=$2; shift
+ case -c
+ only=($only $2); shift
+ case -a
+ all=1
+ case *
+ echo 'usage: invites [-m mtpt] [-n days] [-c cal] [-a]' >[1=2]
+ exit usage
+ }
+ shift
+}
+
+cals=$only
+if(~ $#cals 0)
+ cals=`{pim/calendars -m $mtpt}
+
+seen=/tmp/invites.seen.$pid
+out=/tmp/invites.out.$pid
+fn sigexit { rm -f $seen $out }
+>$seen
+>$out
+
+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)
+ for(f in $d/*){
+ ev=`{sed -n 's/^event: //p' $f}
+ if(! ~ $#ev 0){
+ e=`{cleanname $d/$"ev}
+ # one line per event, not per occurrence
+ if(! grep -s '^'^$"e^'$' $seen){
+ echo $"e >>$seen
+ if(test -f $e/partstat){
+ st=`{cat $e/partstat}
+ show=()
+ if(~ $"st NEEDS-ACTION)
+ show=1
+ if(! ~ $#all 0)
+ show=1
+ if(! ~ $#show 0){
+ ep=`{sed -n 's/^epoch: //p' $f}
+ when=`{date -f 'WWW DD MMM hh:mm' $"ep}
+ sum=`{sed -n 's/^summary: //p' $f}
+ echo $"ep^' '^$"when^' '^$"st^' '^$"sum^' '^$c >>$out
+ }
+ }
+ }
+ }
+ }
+ }
+ i=`{echo $i + 1 | bc}
+}
+
+if(! test -s $out){
+ echo 'nothing waiting in the next '^$"days^' days'
+ exit 0
+}
+sort -n $out | awk -F' ' '{printf "%-22s %-13s %-9s %s\n", $2, $3, $5, $4}'
+exit 0
diff --git a/pim/rc/rsvp b/pim/rc/rsvp
new file mode 100755
index 0000000..0e21f04
--- /dev/null
+++ b/pim/rc/rsvp
@@ -0,0 +1,111 @@
+#!/bin/rc
+# pim/rsvp -- answer an invitation.
+#
+# pim/rsvp accepted 'meet with ben'
+# pim/rsvp declined /mnt/pim/calendars/work/events/date/2026/08/24/1000-API-WG
+#
+# Takes an occurrence path, as pim/agenda -p prints and cal9 plumbs, or a
+# pattern to search for. The answer goes to the event's partstat, and
+# what happens next is the backend's business: a caldav server with
+# calendar-auto-schedule updates your copy and mails the organiser, and
+# a published .ics refuses outright, which is the honest answer there.
+rfork e
+
+mtpt=/mnt/pim
+days=90
+only=()
+
+while(~ $1 -*){
+ switch($1){
+ case -m
+ mtpt=$2; shift
+ case -c
+ only=($only $2); shift
+ case -n
+ days=$2; shift
+ case *
+ echo 'usage: rsvp [-m mtpt] [-c cal] [-n days] accepted|declined|tentative path|pattern' >[1=2]
+ exit usage
+ }
+ shift
+}
+if(test $#* -lt 2){
+ echo 'usage: rsvp [-m mtpt] [-c cal] [-n days] accepted|declined|tentative path|pattern' >[1=2]
+ exit usage
+}
+
+want=`{echo $1 | tr a-z A-Z}
+shift
+switch($want){
+case ACCEPTED DECLINED TENTATIVE
+ ;
+case *
+ echo 'pim/rsvp: want accepted, declined or tentative' >[1=2]
+ exit usage
+}
+pat=$"*
+
+# an occurrence path names one outright; anything else is a search
+occ=()
+if(test -f $"pat)
+ occ=$"pat
+if(~ $#occ 0){
+ hits=`{pim/show -m $mtpt -n $days $only -a $"pat >[2]/dev/null | sed -n 's/^at *//p'}
+ if(~ $#hits 0){
+ echo 'pim/rsvp: nothing matching '^$"pat >[1=2]
+ exit notfound
+ }
+ # A recurring event matches once per occurrence, and the answer
+ # applies to the series, so count distinct events rather than hits.
+ evs=()
+ for(h in $hits){
+ hd=`{basename -d $h}
+ he=`{sed -n 's/^event: //p' $h}
+ if(! ~ $#he 0)
+ evs=($evs `{cleanname $hd/$"he})
+ }
+ evs=`{echo $evs | tr ' ' '\n' | sort -u}
+ if(! ~ $#evs 1){
+ echo 'pim/rsvp: '^$#evs^' events match; be more specific:' >[1=2]
+ for(x in $evs)
+ echo ' '^$x >[1=2]
+ exit ambiguous
+ }
+ occ=$hits(1)
+}
+
+# the occurrence points at its event; 9P has no symlinks, so it is a path
+d=`{basename -d $"occ}
+ev=`{sed -n 's/^event: //p' $"occ}
+if(~ $#ev 0){
+ echo 'pim/rsvp: '^$"occ^' has no event: line' >[1=2]
+ exit noevent
+}
+e=`{cleanname $d/$"ev}
+
+if(! test -f $e/partstat){
+ echo 'pim/rsvp: '^$"e^' has no partstat' >[1=2]
+ exit nopartstat
+}
+
+# Say why before the write fails. A published .ics has nowhere to put a
+# reply and no invitation to reply to, and "permission denied" does not
+# explain that.
+c=`{echo $"e | sed 's|(/.*/calendars/[^/]+)/.*|\1|'}
+caps=`{sed -n 's/^caps //p' $"c/ctl >[2]/dev/null}
+# ~ takes its first argument as the subject, so `~ $caps rsvp` asks
+# whether "read" is "rsvp". Walk the list instead.
+can=()
+for(x in $caps)
+ if(~ $x rsvp)
+ can=1
+if(! ~ $#caps 0)
+ if(~ $#can 0){
+ echo 'pim/rsvp: '^`{basename $"c}^' is '^$"caps^', it cannot answer invitations' >[1=2]
+ exit readonly
+ }
+
+echo $want >$e/partstat
+sum=`{sed -n 's/^summary: //p' $"occ}
+echo $"sum^': '^$want
+exit 0
diff --git a/pim/rc/show b/pim/rc/show
index da23973..b22ca82 100755
--- a/pim/rc/show
+++ b/pim/rc/show
@@ -9,6 +9,7 @@ mtpt=/mnt/pim
days=90
all=()
only=()
+start=()
while(~ $1 -*){
switch($1){
@@ -20,14 +21,21 @@ while(~ $1 -*){
all=1
case -c
only=($only $2); shift
+ case -d
+ start=`{seconds $2}
+ if(~ $#start 0){
+ echo 'pim/show: cannot read the date '^$2 >[1=2]
+ exit baddate
+ }
+ shift
case *
- echo 'usage: show [-m mtpt] [-n days] [-c cal] [-a] path|pattern' >[1=2]
+ echo 'usage: show [-m mtpt] [-n days] [-d date] [-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]
+ echo 'usage: show [-m mtpt] [-n days] [-d date] [-c cal] [-a] path|pattern' >[1=2]
exit usage
}
pat=$"*
@@ -76,7 +84,9 @@ fn search {
cals=$only
if(~ $#cals 0)
cals=`{pim/calendars -m $mtpt}
- now=`{date -n}
+ now=$start
+ if(~ $#now 0)
+ now=`{date -n}
i=0
while(test $i -lt $days){
sec=`{echo $now + $i '*' 86400 | bc}