;;; planner-notes.el --- Topic/subtopic support for notes in planner pages ;; Some code to deal with notes sections in the planner pages ;; Time-stamp: "2003-08-02 00:05:05 Thomas Gehrlein" ;; Version: $Version$ ;;; Commentary: ;; This is probably similar to records-mode. FIXME: Write more about this? ;; Definitions ;; A headline: ".#1 Random thoughts on a random subject" ;; A 2nd headline: ".#1 Some topic --- with or without a subtopic" ;; The title: "Some topic --- with or without subtopic" ;; The topic: "Some topic" ;; The subtopic: "with or without subtopic" ;; ;; The Notes section is the part of the buffer that starts with ;; `planner-notes-section-regexp' (default is "^* Notes$") and ends with the ;; next section in the planner page, usually starting with "^* " ;; The notes section consists of notes. Notes start with ;; `planner-notes-tag' (defaulting to "\.#") plus a number and a space. ;; Notes have a headline that typically describes their subject. If you feel ;; bored on a rainy afternoon, you might want to write a note on "Bad Weather ;; Today" describing the rain and how you feel about it. "Bad Weather Today" ;; is the title/subject/topic/whatever of the note. The exact terminology does ;; not matter because it's not important. But I will call this thing a ;; "headline". ;; But if you develop a more profound interest in meteorological phenomena, you ;; may decide to keep track of your local weather. Many of you day planning ;; files will contain notes like "Weather --- Sunny Day with Small Clouds", or ;; "Weather --- Rainy Day with Large Clouds". "Weather" is the topic of these ;; notes and everything after the " --- " is the subtopic. ;; You may have more topics you like to write about frequently, like "My Cat", ;; or projects you work on. planner-notes helps you to deal with these ;; recurring topics. You can jump to the next or previous note with the same ;; topic, get a list of all subtopics for a given topic and probably many more ;; things. ;; Topic and subtopic are separated by `planner-notes-topic-subtopic-separator' ;; " --- ". (require 'planner) ;;; Code: (defvar planner-notes-section-regexp "^* Notes$" "*A regexp for the headline of the notes section.") (defvar planner-notes-tag "\\.#" "*A regexp that starts lines with notes headlines.") (defvar planner-notes-topic-subtopic-separator " --- " "*A regexp that separates notes topics from notes subtopics.") (defvar planner-notes-topics '("foo-topic" "bar-topic") "List of topics used in notes. Includes only topics with subtopics. FIXME: Is this obsolete, now that there is an index for notes?") (defvar planner-notes-index-file (concat planner-directory "/.notes-index") "*Index file for planner notes. Every notes has a line like this: 2003.07.12#3 Here comes the topic --- and the subtopic.") (defvar planner-notes-index-tag "* Index of Planner Notes" "*Tag that starts the index section in `planner-notes-index-file'.") ;;; Utility functions (defun planner-notes-beg-of-notes () "Return the buffer position of the start of the notes section. Return nil if there is no notes section." (save-excursion ;; search backward from the end of the buffer, this leaves point at the ;; beginning of the regexp (goto-char (point-max)) (re-search-backward planner-notes-section-regexp (point-min) 't))) (defun planner-notes-end-of-notes () "Return the buffer position where the notes section ends. Return nil if there is no notes section." (save-excursion (goto-char (point-min)) (when (planner-notes-beg-of-notes) ; no beg, no end (re-search-forward planner-notes-section-regexp) ; skip "^* Notes$" (if (re-search-forward "^* .+" (point-max) t) (match-beginning 0) (point-max))))) (defun planner-notes-narrow-to-notes () "Narrow the current buffer to the notes section." (interactive) (widen) (let ((beg (planner-notes-beg-of-notes)) (end (planner-notes-end-of-notes))) ;; check for beg to make sure there is a notes section (and beg (narrow-to-region beg end)))) (defun planner-notes-narrow-to-index-lines () "Narrow to the index section of `planner-notes-index-file'. Used before sorting functions are called. Don't call this function in other buffers." (interactive) ;; safety check (if (string= (buffer-file-name) planner-notes-index-file) (error "Only use this function in planner-notes-index-file")) (goto-char (point-min)) (re-search-forward planner-notes-index-tag nil t) ;; hack alert: assumption on number of blank lines (forward-line 2) (beginning-of-line) (narrow-to-region (point) (point-max))) ;; Getting the topic lines and extracting information from them (defun planner-notes-get-headlines () "Get the headlines of all notes in the notes section of the current buffer. If there is not notes section in the current buffer, return nil." (when (planner-notes-beg-of-notes) ; Check if notes section exists ;; (save-excursion ;; (save-restriction (planner-notes-narrow-to-notes) (goto-char (point-min)) (let ((list ())) (while (re-search-forward (concat planner-notes-tag ".+$") (point-max) t) (add-to-list 'list (match-string-no-properties 0) 'append)) list))) (defun planner-notes-get-note-number (headline) "Return the number of a note from its HEADLINE." (with-temp-buffer (insert headline) (goto-char (point-min)) ;; all topics must have a number!!! (if (re-search-forward "[0-9]+" (point-max) t) (string-to-number (match-string 0)) nil))) (defun planner-notes-get-highest-note-number () "Get the highest note number in the current buffer. This is not always the same as the total number of notes in the buffer, since some notes may have been deleted." (interactive) (apply 'max (mapcar 'planner-notes-get-note-number (or (planner-notes-get-headlines) ; returns nil if no topics ;; dummy topic (list (concat planner-notes-tag "0 Bla")))))) (defun planner-notes-get-note-title (headline) "Extract the note title from HEADLINE. HEADLINE is a string." (with-temp-buffer (insert headline) (goto-char (point-min)) (re-search-forward "[0-9]+ " (point-max) t) ; skip that part (buffer-substring (point) (point-max)))) (defun planner-notes-get-note-topic (headline) "Extract the topic from HEADLINE. Return nil if there is no topic, i.e. there is no `planner-notes-topic-subtopic-separator'. \".#1 This headline has no topic and not subtopic\" \".#2 This headline has a topic --- and a subtopic\" No topic without subtopic and vice versa." (with-temp-buffer (insert (planner-notes-get-note-title headline)) (goto-char (point-max)) (if (re-search-backward planner-notes-topic-subtopic-separator nil t) (buffer-substring (point-min) (point)) nil))) (defun planner-notes-get-note-subtopic (headline) "Extract the subtopic from HEADLINE. Return nil if there is no subtopic, i.e. there is no `planner-notes-topic-subtopic-separator'. \".#1 This headline has no topic and not subtopic\" \".#2 This headline has a topic --- and a subtopic\" No topic without subtopic and vice versa." (with-temp-buffer (insert (planner-notes-get-note-title headline)) (goto-char (point-min)) (if (re-search-forward planner-notes-topic-subtopic-separator nil t) (buffer-substring (point) (point-max)) nil))) ;;; index (defun planner-notes-make-index (file) "Return a string with index lines for all notes in FILE." (let ((file-name (file-name-nondirectory file)) (list (with-temp-buffer (insert-file-contents file) (planner-notes-get-headlines)))) (with-temp-buffer (while list (let ((headline (car list))) (insert (planner-notes-get-note-title headline) " (" file-name ; 2003.07.23 "#" (int-to-string (planner-notes-get-note-number headline)) ; #3 ")\n")) (setq list (cdr list))) (buffer-string)))) (defun planner-notes-update-index (file) "Update the index for FILE in `planner-notes-index-file'." ;; go to beginning of index section ;; insert stuff from `planner-notes-make-index' ;; back to point-min ;; delete all old lines from file ;; maybe sort index entries ? ;;FIXME: write this defun) ) (defun planner-notes-index-all-files () "Update the index for all planner daily files." (interactive) ;; find index file (find-file planner-notes-index-file) ;; find index section or insert index headline (goto-char (point-min)) (if (re-search-forward planner-notes-index-tag nil t) (forward-line 2) (goto-char (point-max)) (insert planner-notes-index-tag "\n\n")) ;; delete index section (delete-region (point) (point-max)) ;; insert the index for all files (apply 'insert ;;get list of all daily files (let* ((files (planner-list-daily-files)) ;; get full files names (files (mapcar '(lambda (x) (concat planner-directory "/" x)) files))) ;; index files (mapcar 'planner-notes-make-index files))) ;; sort (planner-notes-sort-index-alphabetically) ) ;; sorting the index file (defun planner-notes-sort-index-alphabetically () "Sort index in `planner-notes-index-file' alphabetically by headline." (interactive) (message "Sorting index ...") (save-excursion (planner-notes-narrow-to-index-lines) (let ((sort-fold-case t)) (sort-lines nil (point-min) (point-max))) (widen)) (message "Sorting index ... done")) (defun planner-notes-sort-index-by-date (&optional reverse) "Sort the index in `planner-notes-index-file' by file name. This is more or less the chronological order. Optional arg REVERSE means, sort in reverse order." (interactive "P") (message "Sorting index ...") (save-excursion (planner-notes-narrow-to-index-lines) (let ((sort-fold-case t)) (sort-fields -1 (point-min) (point-max))) ; sort by last field ;; FIXME: is this a terrible hack?? (when reverse (reverse-region (point-min) (point-max))) (widen)) (message "Sorting index ... done")) (defun planner-notes-sort-index-by-date-reverse () "Sort the index in `planner-notes-index-file' by file name in reverse order. This is more or less the reverse chronological order." (interactive) (planner-notes-sort-index-by-date t)) (defun planner-notes-sort-index-by-topic () "Sort the index section in `planner-notes-index-file' by topic. If there is more than one entry for a topic, sort by subtopic. Subtopics without topics come after the topics." ) (defun planner-notes-initialize-index-file () "Initialize the index file. Find it, create an index, if the file doesn't already exist. Set a few key-bindings." (unless (file-exists-p planner-notes-index-file) (save-excursion (find-file planner-notes-index-file) (erase-buffer) ; delete stuff from planner-mode (planner-notes-index-all-files)))) ;; FIXME: does this work? ;; (add-to-list 'auto-mode-alist ;; (cons (concat "^" ;; (file-name-nondirectory planner-notes-index-file) ;; "$") ;; 'planner-notes-index-mode)) ;;; index mode ;; ideas stolen from and thanks to: ;; bhl.el --- From (B)rute text to (H)tml and (L)aTeX. ;; Copyright (C) 2002 2003 Bastien Guerry ;; Author: Bastien Guerry ;; Maintainer: Bastien Guerry ;; Version: 1.7 ;; Revised: 23/07/2003 ;; Created: 11/16/2002 ;; FIXME: show note in other window (defun planner-notes-index-show-note () "Show the note under point in planner index buffer." (interactive) (save-excursion (beginning-of-line) (emacs-wiki-next-reference) (emacs-wiki-follow-name-at-point))) ;; FIXME: is this the right approach? ;; define it as derived mode from planner mode. Set it read-only. ;; local-set-keys (or somesuch) (define-derived-mode planner-notes-index-mode planner-mode "Planner Notes Index" "A major mode for browsing the planner notes index. \\{planner-notes-index-mode-map}" (setq buffer-read-only t)) (defvar planner-notes-index-mode-map (let ((map (make-keymap))) ;; sorting (define-key map "sa" 'planner-notes-sort-index-alphabetically) (define-key map "sd" 'planner-notes-sort-index-by-date) (define-key map "sr" 'planner-notes-sort-index-by-date-reverse) (define-key map "st" 'planner-notes-sort-index-by-topic) ;; updating the index (define-key map "i" 'planner-notes-index-all-files) ; index (define-key map "u" 'planner-notes-index-all-files) ; update ;; viewing ;;(define-key map "u" 'planner-notes-index-show-note) ; update map) "Keymap used by Planner Notes Index mode.") ;; (define-key bhl-toc-mode-map [(mouse-2)] 'bhl-toc-mouse-visit) ;; (define-key bhl-toc-mode-map [?\r] 'bhl-quit-temp-buffer) ;; (define-key bhl-toc-mode-map [(left)] 'bhl-visit-location-previous) ;; (define-key bhl-toc-mode-map [(right)] 'bhl-visit-location-next) ;; (define-key bhl-toc-mode-map [(up)] 'bhl-visit-location-previous) ;; (define-key bhl-toc-mode-map [(down)] 'bhl-visit-location-next) ;; (define-key bhl-toc-mode-map "q" 'bhl-quit-temp-buffer) ;; (define-key bhl-toc-mode-map "f" 'bhl-quit-temp-buffer) ;; (define-key bhl-toc-mode-map "v" 'bhl-quit-temp-buffer) ;; (define-key bhl-toc-mode-map "n" 'bhl-visit-location-next) ;; (define-key bhl-toc-mode-map "p" 'bhl-visit-location-previous) ;; (define-key bhl-toc-mode-map "<" 'bhl-visit-location-top) ;; (define-key bhl-toc-mode-map ">" 'bhl-visit-location-bottom) ;; (define-key bhl-toc-mode-map "?" 'bhl-toc-show-help) ;; (define-key bhl-toc-mode-map "1" 'bhl-show-toc-1) ;; (define-key bhl-toc-mode-map "2" 'bhl-show-toc-2) ;; (define-key bhl-toc-mode-map "3" 'bhl-show-toc-3) ;; (define-key bhl-toc-mode-map "i" 'bhl-toc-insert-toc) ;; ;; BHL TOC menu ;; (easy-menu-define bhl-toc-menu bhl-toc-mode-map ;; "Menu of the BHL minor mode" ;; '("Toc" ;; ["Next item" bhl-visit-location-next t] ;; ["Previous item" bhl-visit-location-previous t] ;; ["Top" bhl-visit-location-top t] ;; ["Bottom" bhl-visit-location-bottom t] ;; ["Follow" bhl-quit-temp-buffer t] ;; "---" ;; ("Toc depth" ;; ["Sections" bhl-show-toc-1 t] ;; ["Subsections" bhl-show-toc-2 t] ;; ["Subsubsections" bhl-show-toc-3 t]) ;; "---" ;; ["Quick help" bhl-toc-show-help t])) ;; ;; This is probably obsolete ;; (defun planner-notes-make-topics-list () ;; "Make a list of all planner notes topics with subtopics." ;; ;; get a list of all day plan files ;; (planner-list-daily-files) ;; ;; walk through the file list and extract notes headlines ;; ;; open file, extract headlines, delete buffer (what if buffer was open ;; ;; before?) ;; ;; check the headlines for topics and add-to-list ;; ) (provide 'planner-notes) ;;; planner-notes.el ends here