;;; notdeft.el --- Note manager and search engine -*- lexical-binding: t; -*- ;; Copyright (C) 2011 Jason R. Blevins ;; Copyright (C) 2011-2020 Tero Hasu ;; All rights reserved. ;; Author: Tero Hasu ;; Jason R. Blevins ;; Maintainer: Tero Hasu ;; Homepage: https://tero.hasu.is/notdeft/ ;; Keywords: files text notes search ;; Package-Requires: ((emacs "24.3")) ;; This file is not part of GNU Emacs. ;; Redistribution and use in source and binary forms, with or without ;; modification, are permitted provided that the following conditions ;; are met: ;; 1. Redistributions of source code must retain the above copyright ;; notice, this list of conditions and the following disclaimer. ;; 2. Redistributions in binary form must reproduce the above ;; copyright notice, this list of conditions and the following ;; disclaimer in the documentation and/or other materials provided ;; with the distribution. ;; 3. Neither the names of the copyright holders nor the names of any ;; contributors may be used to endorse or promote products derived ;; from this software without specific prior written permission. ;; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ;; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ;; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS ;; FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE ;; COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, ;; INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ;; (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR ;; SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ;; HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, ;; STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ;; ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED ;; OF THE POSSIBILITY OF SUCH DAMAGE. ;;; Commentary: ;; NotDeft is an Emacs mode for quickly browsing, filtering, and ;; editing directories of plain text notes. It was designed for ;; increased productivity when writing and taking notes by making it ;; fast to find the right file at the right time. ;; NotDeft is open source software and may be freely distributed and ;; modified under the BSD license. This version is a fork of ;; Deft version 0.3, which was released on September 11, 2011. ;; File Browser ;; The NotDeft buffer is simply a local search engine result browser ;; which lists the titles of all text files matching a search query ;; (entered by first pressing TAB or `C-c C-o`), followed by short ;; summaries and last modified times. The title is taken to be the ;; first line of the file (or as specified by an Org "TITLE" file ;; property) and the summary is extracted from the text that follows. ;; By default, files are sorted in terms of the last modified date, ;; from newest to oldest. ;; All NotDeft files or notes are simple plain text files (e.g., Org ;; markup files). As an example, the following directory structure ;; generated the screenshot above. ;; ;; % ls ~/.deft ;; about.org browser.org directory.org operations.org ;; ack.org completion.org extensions.org text-mode.org ;; binding.org creation.org filtering.org ;; ;; % cat ~/.deft/about.org ;; About ;; ;; An Emacs mode for slicing and dicing plain text files. ;; Searching and Filtering ;; NotDeft's primary operations are searching and filtering. The list ;; of files matching a search query can be further narrowed down using ;; a filter string, which will match both the title and the body text. ;; To initiate a filter, simply start typing. Filtering happens on the ;; fly. As you type, the file browser is updated to include only files ;; that match the current string. ;; To open the first matching file, simply press `RET`. If no files ;; match your filter string, pressing `RET` will create a new file ;; using the string as the title. This is a very fast way to start ;; writing new notes. The filename will be generated automatically. ;; To open files other than the first match, navigate up and down ;; using `C-p` and `C-n` and press `RET` on the file you want to open. ;; Press `C-c C-c` to clear the filter string and display all files ;; and `C-c C-x g` to refresh the file browser using the current ;; filter string. ;; Static filtering is also possible by pressing `C-c C-l`. This is ;; sometimes useful on its own, and it may be preferable in some ;; situations, such as over slow connections or on older systems, ;; where interactive filtering performance is poor. ;; Common file operations can also be carried out from within a ;; NotDeft buffer. Files can be renamed using `C-c C-r` or deleted ;; using `C-c C-d`. New files can also be created using `C-c C-n` for ;; quick creation or `C-c C-m` for a filename prompt. You can leave a ;; `notdeft-mode' buffer at any time with `C-c C-q`, which buries the ;; buffer, or kills it with a prefix argument `C-u`. ;; Archiving unused files can be carried out by pressing `C-c C-a`. ;; Files will be moved to `notdeft-archive-directory' under the note ;; file's NotDeft data directory. The archive directory is by default ;; named so that it gets excluded from searches. ;; Instead of the above mode of operation, it is also possible to use ;; NotDeft's search functionality without a NotDeft buffer, by ;; invoking NotDeft's variants of the `find-file' command from any ;; major mode. The `notdeft-lucky-find-file' opens the "best" search ;; query match directly, whereas `notdeft-query-select-find-file' ;; presents the matches for selection in the minibuffer. ;; Getting Started ;; To start using NotDeft, place it somewhere in your Emacs ;; `load-path' and add the line ;; ;; (require 'notdeft-autoloads) ;; ;; in your `.emacs` file. Then run `M-x notdeft` to start. ;; Alternatively, you may find it convenient to execute `M-x ;; notdeft-open-query` to enter a search query from anywhere, which ;; then also opens a `notdeft-mode' buffer for displaying the results. ;; To actually use NotDeft's search engine to get search results, you ;; must first compile the `notdeft-xapian` program, which is ;; responsible for accessing the search index(es). The variable ;; `notdeft-xapian-program' must specify the location of the compiled ;; executable in order for NotDeft to use it. ;; You should preferably also have the `notdeft-note-mode' minor mode ;; enabled for all of your note file buffers, in order to get NotDeft ;; to automatically update the search index according to changes made, ;; no matter how the buffers were opened. The minor mode is best ;; enabled for the relevant file formats and directories only, which ;; can be arranged by enabling it only when a certain directory-local ;; variable has been set to indicate note-containing directories. For ;; example, the `add-dir-local-variable' command can be used to set ;; such variables for the relevant modes and directories, and the ;; minor mode can then be enabled based on their values: ;; ;; (defvar-local notdeft-note-mode-auto-enable nil) ;; ;; (add-hook ;; 'hack-local-variables-hook ;; (lambda () ;; (when notdeft-note-mode-auto-enable ;; (notdeft-note-mode 1)))) ;; One useful way to use NotDeft is to keep a directory of notes in a ;; synchronized folder. This can be used with other applications and ;; mobile devices, for example, Notational Velocity or Simplenote ;; on OS X, Elements on iOS, or Epistle on Android. ;; Customization ;; Customize the `notdeft` group to change the functionality. ;; ;; (customize-group "notdeft") ;; By default, NotDeft looks for notes by searching for files with the ;; extension `.org` in the `~/.deft` directory. You can customize ;; both the file extension and the NotDeft note search path by running ;; `M-x customize-group` and typing `notdeft`. Alternatively, you can ;; configure them in your `.emacs` file: ;; ;; (setq notdeft-directories '("~/.deft/" "~/Dropbox/notes/")) ;; (setq notdeft-extension "txt") ;; (setq notdeft-secondary-extensions '("md" "scrbl")) ;; ;; The variable `notdeft-extension' specifies the default extension ;; for new notes. There can be `notdeft-secondary-extensions' for ;; files that are also considered to be NotDeft notes. ;; While you can choose a `notdeft-extension' that is not ".org", ;; NotDeft is somewhat optimized to working with files in Org format. ;; Refer to the `notdeft-org` feature for NotDeft's Org-specific ;; commands. ;; To enable the `notdeft-xapian` program to be compiled from within ;; Emacs, you may specify a suitable shell command by setting the ;; variable `notdeft-xapian-program-compile-command-format'. After ;; that you can use the command `notdeft-xapian-compile-program' to ;; build the program. It even possible to instruct the compilation to ;; happen transparently, by having your configuration include ;; ;; (add-hook 'notdeft-load-hook ;; 'notdeft-xapian-make-program-when-uncurrent) ;; It can be useful to create a global keybinding for the `notdeft' ;; function (e.g., a function key) to start it quickly. You can easily ;; set up such a binding. For example, to bind `notdeft' to F8, add ;; the following code to your `.emacs` file: ;; ;; (global-set-key [f8] 'notdeft) ;; NotDeft also comes with a predefined `notdeft-global-map' keymap of ;; commands, and that keymap can also be given a global keybinding to ;; make its commands accessible quickly. Both `notdeft' and ;; `notdeft-open-query' are included in the keymap, among other ;; commands that may be useful outside a NotDeft buffer. ;; The faces used for highlighting various parts of the screen can ;; also be customized. By default, these faces inherit their ;; properties from the standard font-lock faces defined by your current ;; color theme. ;;; History: ;; NotDeft: ;; * Most notably, add a Xapian-based query engine. ;; * Add support for multiple notes directories. ;; Deft version 0.3 (2011-09-11): ;; * Internationalization: support filtering with multibyte characters. ;; Deft version 0.2 (2011-08-22): ;; * Match filenames when filtering. ;; * Automatically save opened files (optional). ;; * Address some byte-compilation warnings. ;; Deft was originally written by Jason Blevins. ;; The initial version, 0.1, was released on August 6, 2011. ;;; Code: (require 'cl-lib) (require 'widget) (require 'wid-edit) ;; Customization ;;;###autoload (defgroup notdeft nil "Emacs NotDeft mode." :group 'local) (defcustom notdeft-directories '("~/.deft/") "NotDeft directories. Each element must be a directory path string. Each named directory may or may not exist." :type '(repeat string) :safe (lambda (lst) (cl-every 'stringp lst)) :group 'notdeft) (defcustom notdeft-directory nil "Default or previously selected NotDeft data directory. One of the `notdeft-directories', or nil if none. The value may be modified locally for each NotDeft mode buffer. The global default is customizable." :type '(choice (string :tag "Default directory") (const :tag "None" nil)) :safe #'string-or-null-p :group 'notdeft) (defcustom notdeft-extension "org" "Default NotDeft file extension." :type 'string :safe #'stringp :group 'notdeft) (defcustom notdeft-secondary-extensions nil "Additional NotDeft file extensions." :type '(repeat string) :safe (lambda (lst) (cl-every 'stringp lst)) :group 'notdeft) (defcustom notdeft-sparse-directories nil "Directories indexed only for specified files. Complements `notdeft-directories', with the difference that sparse directory contents are not managed, other than being searchable and tracked. The elements of the directory list are of the form (DIR . (FILE ...)) where each FILE is a path string relative to DIR." :type '(repeat (cons file (repeat string))) :group 'notdeft) (defcustom notdeft-notename-function 'notdeft-default-title-to-notename "Function for deriving a note name from a title. Returns nil if no name can be derived from the argument." :type 'function :group 'notdeft) (defcustom notdeft-select-note-file-by-search nil "Whether to do a search when selecting a note file. Ignored if `notdeft-select-note-file-function' is non-nil, in which case that function defines how selection is done." :type 'boolean :safe #'booleanp :group 'notdeft) (defcustom notdeft-archive-directory "_archive" "Sub-directory name for archived notes. Should begin with '.', '_', or '#' to be excluded from indexing for Xapian searches." :type 'string :safe #'stringp :group 'notdeft) (defcustom notdeft-time-format " %Y-%m-%d %H:%M" "Format string for modification times in the NotDeft browser. Set to nil to hide." :type '(choice (string :tag "Time format") (const :tag "Hide" nil)) :safe #'string-or-null-p :group 'notdeft) (defcustom notdeft-file-display-function nil "Formatter for file names in the NotDeft browser. If a function, it must accept the filename and a maximum width (as for `string-width') as its two arguments. Set to nil to have no file information displayed." :type '(choice (function :tag "Formatting function") (const :tag "Hide" nil)) :safe #'null :group 'notdeft) (defcustom notdeft-allow-org-property-drawers t "Whether to recognize Org property drawers. If non-nil, then buffer-level Org \"PROPERTIES\" drawers are treated as being part of the header of the note, which in practice means that they are treated the same as comments." :type 'boolean :safe #'booleanp :group 'notdeft) (defcustom notdeft-open-query-in-new-buffer nil "Whether to open query results in a new buffer. More specifically, when this variable is non-nil, the `notdeft-open-query' command shows its matches in a freshly created NotDeft buffer." :type 'boolean :safe #'booleanp :group 'notdeft) (defcustom notdeft-cache-compaction-factor 20 "Indicates file cache compaction frequency. If nil, then no compaction takes place. If it is 0, then compaction happens after every query. Otherwise the value should be an integer specifying a limit for the cache size as a factor of the maximum result set size. This value is ignored if the Xapian backend is not in use, as in that case filtering requires information about all files at all times." :type '(choice (integer :tag "Times maximum") (const :tag "Unlimited" nil)) :safe (lambda (v) (or (not v) (numberp v))) :group 'notdeft) ;; Faces (defgroup notdeft-faces nil "Faces used in NotDeft mode" :group 'notdeft :group 'faces) (defface notdeft-header-face '((t :inherit font-lock-keyword-face :bold t)) "Face for NotDeft header." :group 'notdeft-faces) (defface notdeft-filter-string-face '((t :inherit font-lock-string-face)) "Face for NotDeft filter string." :group 'notdeft-faces) (defface notdeft-title-face '((t :inherit font-lock-function-name-face :bold t)) "Face for NotDeft file titles." :group 'notdeft-faces) (defface notdeft-separator-face '((t :inherit font-lock-comment-delimiter-face)) "Face for NotDeft separator string." :group 'notdeft-faces) (defface notdeft-summary-face '((t :inherit font-lock-comment-face)) "Face for NotDeft file summary strings." :group 'notdeft-faces) (defface notdeft-time-face '((t :inherit font-lock-variable-name-face)) "Face for NotDeft last modified times." :group 'notdeft-faces) ;; Internal requires (require 'notdeft-global) (require 'notdeft-xapian) ;; Constants (defconst notdeft-buffer "*NotDeft*" "NotDeft buffer name.") (defconst notdeft-separator " --- " "Text used to separate file titles and summaries.") ;; Global variables (defvar notdeft-new-file-data-function 'notdeft-new-file-data "Function for computing a new note's name and content. Will be called for all new notes, but not ones that are renamed or moved. Must return the note data as a (file-name . content-data) pair, where the data part can be nil for an empty note. The function must accept the parameters (DIR NOTENAME EXT DATA TITLE). DIR is a non-nil directory path for the name. NOTENAME may be nil if one has not been given for the new note. EXT is a non-nil file name extension for the note. Note content text DATA may be given for the new note, possibly for further manipulation, but will be nil for an empty note. TITLE may be nil if one has not been provided. Uniqueness of the constructed file name should be ensured if desired, as otherwise note creation will fail due to a naming conflict. See `notdeft-new-file-data' for an example implementation.") (defvar notdeft-completing-read-history nil "History of selected NotDeft note files. May be used by `notdeft-completing-read-function' as the history variable.") (defvar notdeft-completing-read-function 'notdeft-ido-completing-read "Function to use for note file selection. The function is used in the sense of `completing-read' to pick a file from a list. The function must take a list of file paths, and an optional prompt. See `notdeft-ido-completing-read' for an example implementation.") (defvar notdeft-select-note-file-query nil "A note file selection option. Some implementations of `notdeft-select-note-file-function' may check the value of this variable. If it is non-nil it should be a search query string.") (defvar notdeft-select-note-file-all nil "Whether to select a note from all matches. Setting this variable to a non-nil indicates that `notdeft-select-note-file-function' should preferably offer all the relevant notes for selection rather than just the most highly ranked ones.") (defvar notdeft-select-note-file-function nil "Function for selecting a note file. Used generally when another operation needs a note file to be selected, probably interactively. The function is called without arguments. For example implementations, see `notdeft-ido-select-note-file' and `notdeft-search-select-note-file'.") (defvar notdeft-load-hook nil "Hook run immediately after `notdeft' feature load.") (defvar notdeft-mode-hook nil "Hook run when entering NotDeft mode.") (defvar notdeft-pre-refresh-hook nil "Hook run before each `notdeft-refresh'.") (defvar notdeft-post-refresh-hook nil "Hook run after each `notdeft-refresh'.") (defvar notdeft-xapian-query nil "Current Xapian query string. Where `notdeft-xapian-program' is available, it determines the contents of `notdeft-all-files' for a NotDeft buffer. Local to NotDeft mode buffers.") (defvar notdeft-filter-string nil "Current filter string used by NotDeft. A string that is treated as a list of whitespace-separated strings (not regular expressions) that are required to match. Local to a NotDeft mode buffer.") (defvar notdeft-dirlist-cache (make-hash-table :test 'equal) "A cache of lists of notes in NotDeft directories. NotDeft directory names as keys, in their `notdeft-canonicalize-root' form. Lists of full note file names as values. Only used with the dirlist backend, in which case this data structure gets built instead of a search index.") (defvar notdeft-all-files nil "List of all files to list or filter. Local to a NotDeft mode buffer.") (defvar notdeft-current-files nil "List of files matching current filter. Local to a NotDeft mode buffer.") (defvar notdeft-hash-entries (make-hash-table :test 'equal) "Hash containing file information, keyed by filename. Each value is of the form (MTIME CONTENT TITLE SUMMARY).") (defvar notdeft-buffer-width nil "Width of NotDeft buffer, as currently drawn, or nil. Local to a NotDeft mode buffer.") (defvar notdeft-pending-reindex t "Whether to do initial, one-off search indexing. This is a global flag referenced by `notdeft-global-do-pending'. For the search index to stay current for subsequent queries, use only NotDeft mode, NotDeft note mode, and NotDeft commands for making changes to a note collection.") (defvar notdeft-pending-updates 'requery "Whether there are pending updates for a NotDeft buffer. Either nil for no pending updates, the symbol `redraw' for a pending redrawing of the buffer, the symbol `refilter' for a pending recomputation of `notdeft-current-files', or the symbol `requery' for a pending querying of `notdeft-all-files'. Local to a NotDeft mode buffer.") ;;; NotDeft directory information cache (defvar notdeft-dcache--cache nil "A cache of directory information. When set, contains a vector of form [MDIRS SDIRS ADIRS SDIRS-FILES DIR-MAP], where all pathnames are canonicalized and absolute, and where directory names are such also syntactically. SDIRS-FILES is of the form ((SDIR . FILES) ...).") (defun notdeft-canonicalize-root (path) "Canonicalize NotDeft directory PATH. Converts the NotDeft directore PATH into the internal representation used in `notdeft-dcache--cache'." (file-name-as-directory (expand-file-name path))) (defun notdeft-dcache (&optional refresh) "Get the value of the variable `notdeft-dcache--cache'. Compute it if not yet done, or if REFRESH is true." (when (or (not notdeft-dcache--cache) refresh) (let* ((mdirs (mapcar #'notdeft-canonicalize-root notdeft-directories)) (sfiles (mapcar (lambda (x) (let* ((sdir (notdeft-canonicalize-root (car x))) (files (mapcar (lambda (file) (expand-file-name file sdir)) (cdr x)))) (cons sdir files))) notdeft-sparse-directories)) (sdirs (mapcar #'car sfiles)) (adirs (append mdirs sdirs)) (dirmap (append (mapcar (lambda (dir) (cons (notdeft-canonicalize-root dir) dir)) notdeft-directories) (mapcar (lambda (dir) (let ((dir (car dir))) (cons (notdeft-canonicalize-root dir) dir))) notdeft-sparse-directories)))) (setq notdeft-dcache--cache (vector mdirs sdirs adirs sfiles dirmap)))) notdeft-dcache--cache) (defun notdeft-dcache--root-to-original (root cache) "Translate NotDeft ROOT to configured form. Use information in CACHE to do that. That is, given a NotDeft directory path in any form, return the form that it has in either `notdeft-directories' or `notdeft-sparse-directories', or nil if it does not." (cdr (assoc (notdeft-canonicalize-root root) (aref cache 4)))) (defun notdeft-dcache--roots (cache) "Return all NotDeft roots in the CACHE. The result includes both managed and sparse directory paths in their canonical form." (aref cache 2)) (defun notdeft-dcache--filter-roots (dirs cache) "Filter NotDeft roots in DIRS. Use information in CACHE. That is, functionally drop all DIRS that are not NotDeft root directories. Return a filtered list of directory paths in canonical form." (let ((roots (aref cache 2))) (delete nil (mapcar (lambda (dir) (let ((dir (notdeft-canonicalize-root dir))) (when (member dir roots) dir))) dirs)))) (defun notdeft-dcache--expand-sparse-root (dir cache) "Expand NotDeft root path DIR. Use information in CACHE. Expand the DIR path into a specification for the sparse directory. Return nil if it is not a sparse root." (assoc (notdeft-canonicalize-root dir) (aref cache 3))) (defun notdeft-dcache--sparse-file-root (file cache) "Resolve sparse FILE root directory. More specifically, if FILE is a sparse NotDeft directory note file, return its NotDeft directory in an absolute and canonical form. Otherwise return nil. Assume FILE to be in an absolute, canonical form. Use CACHE information for resolution." (let ((sdirs-files (aref cache 3))) (cl-some (lambda (sdir-files) (when (member file (cdr sdir-files)) (car sdir-files))) sdirs-files))) (defun notdeft-dcache--managed-file-root (file cache) "Resolve managed FILE root directory. Do this syntactically, using information in CACHE. More specifically, if FILE names a managed NotDeft directory note file, return its NotDeft directory in an absolute and canonical form. Otherwise return nil. FILE must be in an absolute, canonical form. The FILE name extension is not checked against `notdeft-extension' and `notdeft-secondary-extensions', which may be done separately on the argument if required. Also, it is not checked that FILE is strictly under the returned root, rather than the root itself, and that may also be done separately." (let ((mdirs (aref cache 0))) (cl-some (lambda (dir) (when (string-prefix-p dir file) dir)) mdirs))) (defun notdeft-dcache--strict-managed-file-root (file cache) "Resolve managed FILE root, strictly, syntactically. Return nil if FILE has no root, or if it itself names the root. Otherwise return the root. Assume FILE to be in an absolute, canonical form. Use CACHE information for resolution." (let ((root (notdeft-dcache--managed-file-root file cache))) (when (and root (not (string= root (file-name-as-directory file)))) root))) (defun notdeft-dcache--managed-file-subdir (file cache) "Resolve managed FILE subdirectory. That is, if FILE is syntactically in a subdirectory of a managed NotDeft root, return the absolute and canonical directory path of that subdirectory. Otherwise return nil. The result need not be an immediate subdirectory of a NotDeft root. Assume FILE to be in an absolute, canonical form. Use CACHE information for resolution." (let ((root (notdeft-dcache--strict-managed-file-root file cache))) (when root (let ((dir (file-name-as-directory (file-name-directory file)))) (unless (string= root dir) dir))))) (defun notdeft-dcache--file-root (file cache) "Resolve note FILE root, syntactically. Return the NotDeft root directory, or nil if FILE is neither under a managed or sparse NotDeft directory. Assume FILE to be in an absolute, canonical form. Use CACHE information for resolution." (or (notdeft-dcache--strict-managed-file-root file cache) (notdeft-dcache--sparse-file-root file cache))) (defun notdeft-dcache--sparse-file-by-basename (name cache) "Resolve sparse note file by NAME. Return the file's absolute, canonical pathname. If multiple such files exist, return one of them. If none exist, return nil. NAME is assumed to be without leading directory components, but with any extension. Use CACHE information for resolution." (let ((sdirs-files (aref cache 3))) (cl-some (lambda (sdir-files) (let ((files (cdr sdir-files))) (cl-some (lambda (file) (when (string= name (file-name-nondirectory file)) file)) files))) sdirs-files))) ;; File processing (defun notdeft-title-to-notename (str) "Call `notdeft-notename-function' on STR." (funcall notdeft-notename-function str)) (defun notdeft-default-title-to-notename (str) "Turn a title string STR to a note name string. Return that string, or nil if no usable name can be derived." (save-match-data (when (string-match "^[^a-zA-Z0-9-]+" str) (setq str (replace-match "" t t str))) (when (string-match "[^a-zA-Z0-9-]+$" str) (setq str (replace-match "" t t str))) (while (string-match "[`'“”\"]" str) (setq str (replace-match "" t t str))) (while (string-match "[^a-zA-Z0-9-]+" str) (setq str (replace-match "-" t t str))) (setq str (downcase str)) (and (not (string= "" str)) str))) (defun notdeft-format-time-for-filename (tm) "Format time TM suitably for filenames." (format-time-string "%Y-%m-%d-%H-%M-%S" tm t)) ; UTC (defun notdeft-generate-notename () "Generate a notename, and return it. The generated name is not guaranteed to be unique. Format with the format string \"Deft--%s\", whose placeholder is filled in with a current time string, as formatted with `notdeft-format-time-for-filename'. This is the NotDeft detault naming for notes that are created without a title." (let* ((ctime (current-time)) (ctime-s (notdeft-format-time-for-filename ctime)) (base-filename (format "Deft--%s" ctime-s))) base-filename)) (defun notdeft-make-filename (notename &optional ext dir in-subdir) "Derive a filename from NotDeft note name NOTENAME. The filename shall have the extension EXT, defaulting to `notdeft-extension'. The file shall reside in the directory DIR (or a default directory computed by `notdeft-get-directory'), except that IN-SUBDIR indicates that the file should be given its own subdirectory." (let ((root (or dir (notdeft-get-directory)))) (concat (file-name-as-directory root) (if in-subdir (file-name-as-directory notename) "") notename "." (or ext notdeft-extension)))) (defun notdeft-generate-filename (&optional ext dir) "Generate a new unique filename. Do so without being given any information about note title or content. Have the file have the extension EXT, and be in directory DIR \(their defaults are as for `notdeft-make-filename')." (let (filename) (while (or (not filename) (file-exists-p filename)) (let ((base-filename (notdeft-generate-notename))) (setq filename (notdeft-make-filename base-filename ext dir)))) filename)) (defun notdeft-new-file-data (dir notename ext data title) "Generate a file name and data for a new note. Use the directory path DIR, a note basename NOTENAME, and file name extension EXT to construct a complete file name. Use DATA as the note content, or just the TITLE if there is no other content. Use NOTENAME as specified, or derive it from any TITLE with `notdeft-title-to-notename'. Without either NOTENAME or TITLE, use the current date and time to derive a name for a note, attempting to construct a unique name." (let* ((notename (or notename (when title (notdeft-title-to-notename title)))) (file (if notename (notdeft-make-filename notename ext dir) (notdeft-generate-filename ext dir)))) (cons file (or data title)))) (defun notdeft-make-file-re () "Return a regexp matching strings with a NotDeft extension." (let ((exts (cons notdeft-extension notdeft-secondary-extensions))) (concat "\\.\\(?:" (mapconcat #'regexp-quote exts "\\|") "\\)$"))) (defun notdeft-strip-extension (file) "Strip any NotDeft filename extension from FILE." (replace-regexp-in-string (notdeft-make-file-re) "" file)) (defun notdeft-base-filename (file) "Strip the leading path and NotDeft extension from filename FILE. Use `file-name-directory' to get the directory component. Strip any extension with `notdeft-strip-extension'." (let* ((file (file-name-nondirectory file)) (file (notdeft-strip-extension file))) file)) (defun notdeft-file-equal-p (x y) "Whether X and Y are the same file. Compare based on path names only, without consulting the filesystem, unlike `file-equal-p'. Disregard directory syntax, so that \"x\" is equal to \"x/\"." (string= (file-name-as-directory (expand-file-name x)) (file-name-as-directory (expand-file-name y)))) (defun notdeft-file-in-directory-p (file dir) "Whether FILE is in DIR, syntactically. A directory is considered to be in itself. Compare based on path names only, without consulting the filesystem, unlike `file-in-directory-p'." (let ((dir (file-name-as-directory (expand-file-name dir))) (file (file-name-as-directory (expand-file-name file)))) (string-prefix-p dir file))) (defun notdeft-file-strictly-in-directory-p (file dir) "Whether FILE is strictly in DIR, syntactically. Like `notdeft-file-in-directory-p', but a directory is not considered to be in itself." (let ((dir (file-name-as-directory (expand-file-name dir))) (file (file-name-as-directory (expand-file-name file)))) (and (string-prefix-p dir file) (not (string= dir file))))) (defun notdeft-file-member (file list) "Whether FILE is a member of LIST. Comparisons are syntactic only. Return the matching member of the list, or nil." (cl-some (lambda (elem) (when (notdeft-file-equal-p file elem) elem)) list)) (defun notdeft-dir-of-file (file) "Return the NotDeft directory for FILE, or nil. FILE may not itself be one of the NotDeft roots. Compare syntactically, without consulting the file system." (notdeft-dcache--file-root (expand-file-name file) (notdeft-dcache))) (defun notdeft-file-sparse-p (file) "Whether FILE is in a sparse NotDeft directory." (notdeft-dcache--sparse-file-root (expand-file-name file) (notdeft-dcache))) (defun notdeft-file-in-subdir-p (file) "Whether FILE is in a NotDeft sub-directory. More accurately, whether FILE syntactically names a file or directory that is not an immediate child of one of the `notdeft-directories'. FILE need not actually exist for this predicate to hold, nor does the containing NotDeft directory." (notdeft-dcache--managed-file-subdir (expand-file-name file) (notdeft-dcache))) (defun notdeft-file-readable-p (file) "Whether FILE is a readable non-directory." (and (file-readable-p file) (not (file-directory-p file)))) (defun notdeft-read-file (file) "Return the contents of FILE as a string." (with-temp-buffer (insert-file-contents file) (buffer-string))) (defun notdeft-title-from-file-content (file) "Extract a title from FILE content. Return nil on failure." (when (notdeft-file-readable-p file) (let* ((contents (notdeft-read-file file)) (title (notdeft-parse-title contents))) title))) (defun notdeft-chomp (str) "Trim leading and trailing whitespace from STR." (replace-regexp-in-string "\\(\\`[[:space:]\n\r]+\\|[[:space:]\n\r]+\\'\\)" "" str)) ;;;###autoload (defun notdeft-chomp-nullify (str &optional trim) "Return string STR if non-empty, otherwise return nil. Optionally, use function TRIM to trim any result string." (when str (let ((str (notdeft-chomp str))) (unless (string= "" str) (if trim (funcall trim str) str))))) (defvar notdeft-directory-files-regexp "^[^._#/][^/]*$" "A match regexp for `directory-files'. The regular expression to use as the third argument when calling `directory-files' to look for notes and note subdirectories from the file system. This should be specified to that it is consistent with the Xapian program's filtering of readdir results.") (defun notdeft-root-find-file (file-p root) "Find a file matching predicate FILE-P under ROOT. FILE-P is called with the file path name \(including the ROOT component) as its sole argument. ROOT is assumed to be a NotDeft root, which need not exist. Return nil if no matching file is found." (and (file-readable-p root) (file-directory-p root) (let ((root (file-name-as-directory root)) (files (directory-files root nil notdeft-directory-files-regexp t)) result) (while (and files (not result)) (let* ((abs-file (concat root (car files)))) (setq files (cdr files)) (cond ((file-directory-p abs-file) (setq result (notdeft-root-find-file file-p abs-file))) ((funcall file-p abs-file) (setq result abs-file))))) result))) (defun notdeft-file-by-basename (name) "Resolve a NotDeft note NAME to a full pathname. NAME is a non-directory filename, with extension. Resolve it to the path of a file under `notdeft-directories' or `notdeft-sparse-directories', if such a note file does exist. If multiple such files exist, return one of them. If none exist, return nil." (or (notdeft-managed-file-by-basename name) (notdeft-dcache--sparse-file-by-basename name (notdeft-dcache)))) (defun notdeft-managed-file-by-basename (name) "Resolve managed note file by basename NAME." (let* ((file-p (lambda (pn) (string= name (file-name-nondirectory pn)))) (cand-roots notdeft-directories) result) (while (and cand-roots (not result)) (let ((abs-root (expand-file-name (car cand-roots)))) (setq cand-roots (cdr cand-roots)) (setq result (notdeft-root-find-file file-p abs-root)))) result)) (defun notdeft-glob (root &optional dir result file-re) "Return a list of all NotDeft files in a directory tree. List the NotDeft files under the specified NotDeft ROOT and its directory DIR, with DIR given as a path relative to the directory ROOT. If DIR is nil, then list NotDeft files under ROOT. Add to the RESULT list in undefined order, and return the resulting value. Only include files whose non-directory names match the regexp FILE-RE, defaulting to the result of `notdeft-make-file-re'. If ROOT does not exist, return nil." (let* ((root (file-name-as-directory (expand-file-name root))) (dir (file-name-as-directory (or dir "."))) (abs-dir (expand-file-name dir root))) (and (file-readable-p abs-dir) (file-directory-p abs-dir) (let* ((files (directory-files abs-dir nil notdeft-directory-files-regexp t)) (file-re (or file-re (notdeft-make-file-re)))) (dolist (file files result) (let* ((rel-file (file-relative-name (expand-file-name file abs-dir) root)) (abs-file (concat root rel-file))) (cond ((file-directory-p abs-file) (setq result (notdeft-glob root rel-file result file-re))) ((string-match-p file-re file) (setq result (cons rel-file result)))))))))) (defun notdeft-glob--absolute (root &optional dir result file-re) "Like `notdeft-glob', but return the results as absolute paths. The arguments ROOT, DIR, RESULT, and FILE-RE are the same." (mapcar (lambda (rel) (expand-file-name rel root)) (notdeft-glob root dir result file-re))) (defun notdeft-find-all-files-in-dir (dir full) "Return a list of all NotDeft files under DIR. The specified directory must be a NotDeft root. Return an empty list if there is no readable directory. Return the files' absolute paths if FULL is true." (if full (notdeft-glob--absolute dir) (notdeft-glob dir))) (defun notdeft-make-basename-list () "Return the names of all NotDeft notes. Search all existing `notdeft-directories', and include all existing `notdeft-sparse-directories' files. The result list is sorted by the `string-lessp' relation, and it may contain duplicates." (let ((fn-lst '())) (dolist (dir notdeft-directories) (setq fn-lst (append fn-lst (notdeft-find-all-files-in-dir dir t)))) (dolist (sdir-files notdeft-sparse-directories) (let ((dir (car sdir-files)) (files (cdr sdir-files))) (dolist (file files) (let ((file (expand-file-name file dir))) (when (file-exists-p file) (setq fn-lst (cons file fn-lst))))))) ;; `sort` may modify `name-lst` (let ((name-lst (mapcar #'file-name-nondirectory fn-lst))) (sort name-lst 'string-lessp)))) (defun notdeft-parse-title (contents) "Parse the given file CONTENTS and determine the title. The title is taken to be the first non-empty line of a file. Org comments are skipped, and \"#+TITLE\" syntax is recognized, and may also be used to define the title. Returns nil if there is no non-empty, not-just-whitespace title in CONTENTS." (let* ((res (with-temp-buffer (insert contents) (notdeft-parse-buffer))) (title (car res))) title)) (defun notdeft-condense-whitespace (str) "Condense whitespace in STR into a single space." (replace-regexp-in-string "[[:space:]\n\r]+" " " str)) (defun notdeft-parse-buffer () "Parse the file contents in the current buffer. Extract a title and summary. The summary is a string extracted from the contents following the title. The result is a list (TITLE SUMMARY KEYWORDS) where any component may be nil. The result list may include additional, undefined components." (let (title summary keywords dbg (end (point-max))) (save-match-data (save-excursion (goto-char (point-min)) (while (and (< (point) end) (not (and title summary))) ;;(message "%S" (list (point) title summary)) (cond ((looking-at "^\\(?:%\\|@;\\|