;;;; journal.el -- stuff for keeping a diary ;;; Time-stamp: <2007-11-26 15:33:24 jcgs> ;; This program is free software; you can redistribute it and/or modify it ;; under the terms of the GNU General Public License as published by the ;; Free Software Foundation; either version 2 of the License, or (at your ;; option) any later version. ;; This program is distributed in the hope that it will be useful, but ;; WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ;; General Public License for more details. ;; You should have received a copy of the GNU General Public License along ;; with this program; if not, write to the Free Software Foundation, Inc., ;; 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA (provide 'journal) (require 'handsfree-read-number) (require 'cl) (require 'time-stamp) (defvar journal-monthname-alist '(("January" . 1) ("February" . 2) ("March" . 3) ("April" . 4) ("May" . 5) ("June" . 6) ("July" . 7) ("August" . 8) ("September" . 9) ("October" . 10) ("November" . 11) ("December" . 12) ("Jan" . 1) ("Feb" . 2) ("Mar" . 3) ("Apr" . 4) ("May" . 5) ("Jun" . 6) ("Jul" . 7) ("Aug" . 8) ("Sep" . 9) ("Oct" . 10) ("Nov" . 11) ("Dec" . 12)) "Mapping between month names and numbers.") ;;;###autoload (defvar journal-dates-directories '(("research" . (substitute-in-file-name "$COMMON/research/log/")) ("asr33 restoration" . (substitute-in-file-name "$COMMON/www/computing/asr33/")) ("talks" . (substitute-in-file-name "$COMMON/www/talks"))) "*The directories for diary files for each journal. This is an alist from the journal name, to a lisp form to eval to get the directory. Thus, finding the directory can be delayed until that journal is chosen, which avoids looking for removable media that are not present.") (defvar journal-default-journal (car (car journal-dates-directories)) "*Which journal to use by default.") (defvar journal-month-lengths [0 31 28 31 30 31 30 31 31 30 31 30 31] "The number of days in each month.") (defvar journal-month-full-names [ "" "January" "February" "March" "April" "May" "June" "July" "August" "September" "October" "November" "December" ] "The names of the months.") (defvar journal-weekday-full-names ["Sunday" "Monday" "Tuesday" "Wednesday" "Thursday" "Friday" "Saturday"] "The names of the days.") (defvar journal-new-day-previous-day-read nil "The previous day read.") (defvar journal-new-day-previous-previous-day-read nil "The previous previous day read.") (defun date-add-day (base-date days) "To BASE-DATE add DAYS. Not a full implementation!" ;; todo: handle moving into adjacent months (let ((day-part (nthcdr 3 base-date))) (rplaca day-part (+ (car day-part) days)) base-date )) (defvar journal-history-var nil "History hack variable for selecting journal.") (defun journal-current-journal (&optional specified-dir) "Return the journal that we are currently in, if identifiable." ;; todo: write the body of this (unless specified-dir (setq specified-dir default-directory)) (catch 'found (let ((dirs journal-dates-directories)) (while dirs (let ((directory (if (stringp (cdar dirs)) (cdar dirs) (eval (cdar dirs))))) (when (and (stringp directory) (>= (length specified-dir) (length directory)) (string= (substring specified-dir 0 (length directory)) directory )) (throw 'found (caar dirs)))) (setq dirs (cdr dirs)))) nil)) (defun journal-choose-journal (prompt) "Choose a journal, prompting with PROMPT." (setq journal-history-var (mapcar 'car journal-dates-directories)) (completing-read prompt journal-dates-directories nil t journal-default-journal (cons 'journal-history-var (position journal-default-journal journal-history-var :test 'string=)))) (defun journal-calculate-date-at-point () "Calculate the date at point." (let* ((date-at (save-excursion (re-search-backward "\\[\\([0-9]+\\)[- ]\\([a-z]+\\)[- ]\\([0-9]+\\)\\]" (point-min) t))) (found-date (if date-at (list nil ; 0 nil ; 1 nil ; 2 (string-to-int (match-string-no-properties 3)) ; 3 = day (cdr (assoc (match-string-no-properties 2) journal-monthname-alist ; journal-monthname-alist )) ; 4 = month (string-to-int (match-string-no-properties 1)) ; 5 = year ) nil))) found-date)) (defun journal-new-day-interactive-reader (&optional two) "Interactive command reader for journal-new-day. Made into a separate routine for legibility. With optional argument, read two days to give a range within a month." ;; we try to extract a date from somewhere before point; ;; if you find one we can pass its position as a hint where to enter the new day (let* ((journal (journal-choose-journal "Journal: ")) (date-at (save-excursion (re-search-backward "\\[\\([0-9]+\\)[- ]\\([a-z]+\\)[- ]\\([0-9]+\\)\\]" (point-min) t))) (found-date (if date-at (list nil ; 0 nil ; 1 nil ; 2 (string-to-int (match-string-no-properties 3)) ; 3 = day (cdr (assoc (match-string-no-properties 2) journal-monthname-alist ; journal-monthname-alist )) ; 4 = month (string-to-int (match-string-no-properties 1)) ; 5 = year ) nil)) ;; (thing (message "found-date=%S month=%S" found-date (match-string-no-properties 2))) (direction (if (and (integerp journal-new-day-previous-day-read) (integerp journal-new-day-previous-previous-day-read)) (if (> journal-new-day-previous-day-read journal-new-day-previous-previous-day-read) 1 -1) nil)) (now (if found-date (if direction (date-add-day found-date direction) found-date) (decode-time (current-time)))) (now-year (nth 5 now)) (now-month-number (nth 4 now)) (now-day (nth 3 now)) (year (handsfree-read-number "Year: " 1900 2100 now-year)) (month (handsfree-read-number "Month: " 1 12 now-month-number)) (day (handsfree-read-number (if two "Start day: " "Day: ") 1 (aref journal-month-lengths month) (if (= month now-month-number) now-day (if (> month now-month-number) 1 (aref journal-month-lengths month))))) (end-day (if two (handsfree-read-number "End day: " day (aref journal-month-lengths month) day) nil)) (month-name (substring (aref journal-month-full-names month) 0 3))) ;; If the day number has gone over the end of the month, go into the next month. ;; Keep doing this as long as necessary. (while (> day (aref journal-month-lengths month)) (decf day (aref journal-month-lengths month)) (incf month) (if (> month 12) (setq month 1 year (1+ year)))) (setq month-name (substring (aref journal-month-full-names month) 0 3) journal-new-day-previous-previous-day-read journal-new-day-previous-day-read journal-new-day-previous-day-read day journal-default-journal journal) (if two (list journal year month month-name day end-day) (list journal year month month-name day ;;;;;;;;; not quite right... (if t;; (and (= year now-year) (= month now-month) (= day now-day)) date-at nil))))) (defun journal-new-year (journal year) "In JOURNAL, make the directory for YEAR (unless it already exists). Return the directory concerned." (let* ((journal-dates-directory (eval (cdr (assoc journal journal-dates-directories)))) (year-directory (expand-file-name (format "%04d" year) journal-dates-directory))) (unless (file-directory-p year-directory) (make-directory year-directory) (save-window-excursion (find-file (expand-file-name "index.html" year-directory)) (goto-char (point-min)) (if (re-search-forward "
" "Lead-in for journal entry.") (defvar journal-entry-body-postamble "
\n\n" "Lead-out for journal entry.") (make-variable-buffer-local ' journal-entry-body-preamble) (make-variable-buffer-local 'journal-entry-body-postamble) ;;;###autoload (defun journal-new-day (journal year month monthname day &optional prev-date-at) "Start a new day's entry. The arguments are JOURNAL YEAR MONTH MONTHNAME DAY. An optional extra argument gives where in the buffer the previous day was found." (interactive (journal-new-day-interactive-reader)) (setq journal-made-entry t) ; mark that some journal activity has happened (if (null monthname) ; you can give just the number (but must always give the number) (setq monthname (substring (aref journal-month-full-names month) 0 3)) (setq monthname (substring monthname 0 3))) (find-file (journal-new-month (journal-new-year journal year) year month)) (message "That appears to be a %s" (aref journal-weekday-full-names (nth 6 (decode-time (encode-time 0 0 0 day month year))))) ;; see whether we already have this day (if (journal-find-end-of-day journal year month day) (progn (message "Already started that day") (insert "\n" journal-entry-body-preamble) (save-excursion (insert journal-entry-body-postamble)) ) ;; we don't already have that day, so must find a place for it ;; if given a hint, use that (if (and nil prev-date-at) (progn (goto-char prev-date-at) (if (re-search-forward "\n[Last month]\n[This year]\n[Dates index]\n
\n"))) (goto-char (point-min)) (unless (search-forward ".html\">Next month]" (point-max) t) (let ((next-month (journal-month-next (journal-month-this-page)))) (goto-char (point-max)) (search-backward "\n[Next month]\n[This year]\n[Dates index]\n
\n"))))) (defun journal-find-start-of-day (journal year month day) "Move to the start of the specified day, if it can be found." (let* ((year-string (if (stringp year) year (format "%04d" year))) (short-year-number (mod (if (numberp year) year (string-to-int year)) 100)) (month-number (if (numberp month) month (cdr (assoc month journal-monthname-alist)))) (month-file-string (format "%02d-%02d.html" short-year-number month-number))) (find-file (expand-file-name month-file-string (expand-file-name year-string (eval (cdr (assoc journal journal-dates-directories))))))) (goto-char (point-max)) (let ((marker (format "