From 2380d705083f5c223802c47873348a78cca12462 Mon Sep 17 00:00:00 2001 From: Howard Abrams Date: Wed, 29 Dec 2021 09:36:58 -0800 Subject: [PATCH] Commit tangled versions of my website code This makes this project self-contained for others to steal. --- elisp/beep.el | 107 +++++++++++++++++++ elisp/boxes-extras.el | 203 +++++++++++++++++++++++++++++++++++ elisp/boxes.el | 238 ++++++++++++++++++++++++++++++++++++++++++ elisp/ha-focus.el | 74 +++++++++++++ 4 files changed, 622 insertions(+) create mode 100644 elisp/beep.el create mode 100644 elisp/boxes-extras.el create mode 100644 elisp/boxes.el create mode 100644 elisp/ha-focus.el diff --git a/elisp/beep.el b/elisp/beep.el new file mode 100644 index 0000000..f70b1fa --- /dev/null +++ b/elisp/beep.el @@ -0,0 +1,107 @@ +;;; beep.el --- A literate programming for alerting after long projects. -*- lexical-binding: t; -*- +;; +;; Copyright (C) 2020 Howard X. Abrams +;; +;; Author: Howard X. Abrams +;; Maintainer: Howard X. Abrams +;; Created: December 23, 2020 +;; +;; This file is not part of GNU Emacs. +;; +;; *NB:* Do not edit this file. Instead, edit the original literate file at: +;; ~/website/Technical/Emacs/beep-for-emacs.org +;; And tangle the file to recreate this one. +;; +;;; Code: + +(defvar beep-alert-sound-file + (expand-file-name "~/other/hamacs/beep-notify.wav") + "A WAV or AU file used at the completion of a function.") + +;; My replacement in case we can't play internal sounds: +(defun beep--beep () + "Play a default notification sound file. +Customize the variable, `beep-alert-sound-file' to adjust the sound." + (if (fboundp 'play-sound-internal) + (play-sound-file beep-alert-sound-file) + (call-process-shell-command (format "afplay %s &" beep-alert-sound-file) nil 0))) + +(defvar beep-speech-executable "say %s" + "An OS-dependent shell string to speak. Replaces `%s' with a phrase.") + +(defun beep--speak (phrase) + "Call a program to speak the string, PHRASE. +Customize the variable, `beep-speech-executable'." + (let ((command (format beep-speech-executable phrase))) + (shell-command command))) + +(defun beep--when-finished (phrase) + "Notify us with string, PHRASE, to grab our attention. +Useful after a long process has completed, but use sparingly, +as this can be pretty distracting." + (message phrase) + (when (functionp 'alert) + (alert phrase :title "Completed")) + (beep--beep) + (beep--speak phrase)) + +(defun compile-and-notify () + "Call `counsel-compile' and notify us when finished. +See `beep--when-finished' for details." + (interactive) + (let ((root (projectile-project-root))) + (counsel-compile root) + (beep--when-finished "The compile command has finished."))) + +(defvar beep-func-too-long-time 5 + "The number of seconds a function runs before it is considered taking too much time, and needing to be alerted when it has finished.") + +(defun beep--after-function (func) + "Call the function, FUNC, interactively, and notify us when completed." + (let ((start-time (current-time)) + duration) + (call-interactively func) + (setq duration (thread-first + (current-time) + (time-subtract start-time) + decode-time + first)) + (when (> duration beep-func-too-long-time) + (beep--when-finished (format "The function, %s, has finished." func))))) + +(defun recompile-and-notify () + "Call `recompile' and notify us when finished. +See `beep--when-finished' for details." + (interactive) + (beep--after-function 'recompile)) + +(global-set-key (kbd "C-c c") 'recompile-and-notify) +(global-set-key (kbd "C-c C") 'compile-and-notify) + +(defun beep-when-runs-too-long (orig-function &rest args) + "Notifies us about the completion of ORIG-FUNCTION. + Useful as after advice to long-running functions, for instance: + + (advice-add 'org-publish :around #'beep-when-runs-too-long)" + (let ((start-time (current-time)) + duration) + (apply orig-function args) + (setq duration (thread-first + (current-time) + (time-subtract start-time) + decode-time + first)) + (when (> duration beep-func-too-long-time) + (beep--when-finished (format "The function, %s, has finished." + (beep--extract-function-name orig-function)))))) + +(defun beep--extract-function-name (expr) + "Extracts the original function from a lambda expression, EXPR." + (if (listp expr) + (if (equal (car expr) 'lambda) + (car (cadr expr)) + expr) + expr)) + +(provide 'beep) +;;; beep.el ends here diff --git a/elisp/boxes-extras.el b/elisp/boxes-extras.el new file mode 100644 index 0000000..2d46157 --- /dev/null +++ b/elisp/boxes-extras.el @@ -0,0 +1,203 @@ +;;; BOXES-EXTRAS --- Refiling Subtrees to Proper Org Files +;; +;; Author: Howard Abrams +;; Copyright © 2019, Howard Abrams, all rights reserved. +;; Created: 7 January 2019 +;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;;; Commentary: +;; +;; WARNING: This file is tangled from its original essay. +;; +;; For a thorough explanation of this code, see the online essay: +;; http://www.howardism.org/Technical/Emacs/getting-even-more-boxes-done.html +;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;; 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 3, 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; see the file COPYING. If not, write to +;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth +;; Floor, Boston, MA 02110-1301, USA. +;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;;; Code: + +(defun org-subtree-metadata () + "Return a list of key aspects of an org-subtree. Includes the +following: header text, body contents, list of tags, region list +of the start and end of the subtree." + (save-excursion + ;; Jump to the parent header if not already on a header + (when (not (org-at-heading-p)) + (org-previous-visible-heading 1)) + + (let* ((context (org-element-context)) + (attrs (second context)) + (props (org-entry-properties))) + + (list :region (list (plist-get attrs :begin) (plist-get attrs :end)) + :header (plist-get attrs :title) + :tags (org-get-subtree-tags props) + :properties (org-get-subtree-properties attrs) + :body (org-get-subtree-content attrs))))) + +(defun org-get-subtree-tags (&optional props) + "Given the properties, PROPS, from a call to +`org-entry-properties', return a list of tags." + (unless props + (setq props (org-entry-properties))) + (let ((tag-label (if org-get-subtree-tags-inherited "ALLTAGS" "TAGS"))) + (-some->> props + (assoc tag-label) + cdr + substring-no-properties + (s-split ":") + (--filter (not (equalp "" it)))))) + +(defvar org-get-subtree-tags-inherited t + "Returns a subtree's tags, and all tags inherited (from tags + specified in parents headlines or on the file itself). Defaults + to true.") + +(defun org-get-subtree-properties (attributes) + "Return a list of tuples of a subtrees properties where the keys are strings." + + (defun symbol-upcase? (sym) + (let ((case-fold-search nil)) + (string-match-p "^:[A-Z]+$" (symbol-name sym)))) + + (defun convert-tuple (tup) + (let ((key (first tup)) + (val (second tup))) + (list (substring (symbol-name key) 1) val))) + + (->> attributes + (-partition 2) ; Convert plist to list of tuples + (--filter (symbol-upcase? (first it))) ; Remove lowercase tuples + (-map 'convert-tuple))) + +(defun org-get-subtree-content (attributes) + "Return the contents of the current subtree as a string." + (let ((header-components '(clock diary-sexp drawer headline inlinetask + node-property planning property-drawer section))) + + (goto-char (plist-get attributes :contents-begin)) + + ;; Walk down past the properties, etc. + (while + (let* ((cntx (org-element-context)) + (elem (first cntx)) + (props (second cntx))) + (when (member elem header-components) + (goto-char (plist-get props :end))))) + + ;; At this point, we are at the beginning of what we consider + ;; the contents of the subtree, so we can return part of the buffer: + (buffer-substring-no-properties (point) (org-end-of-subtree)))) + +(defun org-refile-subtree-to-file (dir) + "Archive the org-mode subtree and create an entry in the +directory folder specified by DIR. It attempts to move as many of +the subtree's properties and other features to the new file." + (interactive "DDestination: ") + (let* ((props (org-subtree-metadata)) + (head (plist-get props :header)) + (body (plist-get props :body)) + (tags (plist-get props :tags)) + (properties (plist-get props :properties)) + (area (plist-get props :region)) + (filename (org-filename-from-title head)) + (filepath (format "%s/%s.org" dir filename))) + (apply #'delete-region area) + (org-create-org-file filepath head body tags properties))) + +(defun org-create-org-file (filepath header body tags properties) + "Create a new Org file by FILEPATH. The contents of the file is +pre-populated with the HEADER, BODY and any associated TAGS." + (find-file-other-window filepath) + (org-set-file-property "TITLE" header t) + (when tags + (org-set-file-property "FILETAGS" (s-join " " tags))) + + ;; Insert any drawer properties as #+PROPERTY entries: + (when properties + (goto-char (point-min)) + (or (re-search-forward "^\s*$" nil t) (point-max)) + (--map (insert (format "#+PROPERTY: %s %s" (first it) (second it))) properties)) + + ;; My auto-insert often adds an initial headline for a subtree, and in this + ;; case, I don't want that... Yeah, this isn't really globally applicable, + ;; but it shouldn't cause a problem for others. + (when (re-search-forward "^\\* [0-9]$" nil t) + (replace-match "")) + + (delete-blank-lines) + (goto-char (point-max)) + (insert "\n") + (insert body)) + +(defun org-filename-from-title (title) + "Creates a useful filename based on a header string, TITLE. +For instance, given the string: What's all this then? + This function will return: whats-all-this-then" + (let* ((no-letters (rx (one-or-more (not alphanumeric)))) + (init-try (->> title + downcase + (replace-regexp-in-string "'" "") + (replace-regexp-in-string no-letters "-")))) + (string-trim init-try "-+" "-+"))) + +(defun org-set-file-property (key value &optional spot) + "Make sure file contains a top-level, file-wide property. +KEY is something like `TITLE' or `FILETAGS'. This function makes +sure that the property contains the contents of VALUE, and if the +file doesn't have the property, it is inserted at either SPOT, or +if nil,the top of the file." + (save-excursion + (goto-char (point-min)) + (let ((case-fold-search t)) + (if (re-search-forward (format "^#\\+%s:\s*\\(.*\\)" key) nil t) + (replace-match value nil nil nil 1) + + (cond + ;; if SPOT is a number, go to it: + ((numberp spot) (goto-char spot)) + ;; If SPOT is not given, jump to first blank line: + ((null spot) (progn (goto-char (point-min)) + (re-search-forward "^\s*$" nil t))) + (t (goto-char (point-min)))) + + (insert (format "#+%s: %s\n" (upcase key) value)))))) + +(defun org-refile-to-projects-dir () + "Move the current subtree to a file in the `projects' directory." + (interactive) + (org-refile-subtree-to-file org-default-projects-dir)) + +(defun org-refile-to-technical-dir () + "Move the current subtree to a file in the `technical' directory." + (interactive) + (org-refile-subtree-to-file org-default-technical-dir)) + +(define-auto-insert "/personal/*\\.org" ["personal.org" ha/autoinsert-yas-expand]) + +(defun org-refile-to-personal-dir () + "Move the current subtree to a file in the `personal' directory." + (interactive) + (org-refile-subtree-to-file org-default-personal-dir)) + +(provide 'boxes-extras) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; boxes-extras.el ends here diff --git a/elisp/boxes.el b/elisp/boxes.el new file mode 100644 index 0000000..2fc17eb --- /dev/null +++ b/elisp/boxes.el @@ -0,0 +1,238 @@ +;;; BOXES --- An opinionated approach to a GTD workflow +;; +;; Author: Howard Abrams +;; Copyright © 2019, Howard Abrams, all rights reserved. +;; Created: 7 January 2019 +;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;;; Commentary: +;; +;; WARNING: This file is tangled from its original essay. +;; +;; For a thorough explanation of this code, see the online essay: +;; http://www.howardism.org/Technical/Emacs/getting-more-boxes-done.html +;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;; 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 3, 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; see the file COPYING. If not, write to +;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth +;; Floor, Boston, MA 02110-1301, USA. +;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;;; Code: + +(defvar org-default-projects-dir "~/projects" "Primary GTD directory") +(defvar org-default-technical-dir "~/technical" "Directory of shareable notes") +(defvar org-default-personal-dir "~/personal" "Directory of un-shareable, personal notes") +(defvar org-default-completed-dir "~/projects/trophies" "Directory of completed project files") +(defvar org-default-inbox-file "~/projects/breathe.org" "New stuff collects in this file") +(defvar org-default-tasks-file "~/projects/tasks.org" "Tasks, TODOs and little projects") +(defvar org-default-incubate-file "~/projects/incubate.org" "Ideas simmering on back burner") +(defvar org-default-completed-file nil "Ideas simmering on back burner") +(defvar org-default-notes-file "~/personal/general-notes.org" "Non-actionable, personal notes") +(defvar org-default-media-file "~/projects/media.org" "White papers and links to other things to check out") + +(defvar org-capture-templates (list)) + +(add-to-list 'org-capture-templates + `("t" "Task Entry" entry + (file ,org-default-inbox-file) + "* %?\n:PROPERTIES:\n:CREATED:%U\n:END:\n\n%i\n\nFrom: %a" + :empty-lines 1)) + +(defhydra hydra-org-refiler (org-mode-map "C-c s" :hint nil) + " + ^Navigate^ ^Refile^ ^Move^ ^Update^ ^Go To^ ^Dired^ + ^^^^^^^^^^--------------------------------------------------------------------------------------- + _k_: ↑ previous _t_: tasks _m X_: projects _T_: todo task _g t_: tasks _g X_: projects + _j_: ↓ next _i_/_I_: incubate _m P_: personal _S_: schedule _g i_: incubate _g P_: personal + _c_: archive _p_: personal _m T_: technical _D_: deadline _g x_: inbox _g T_: technical + _d_: delete _r_: refile _R_: rename _g n_: notes _g C_: completed + " + ("" org-previous-visible-heading) + ("" org-next-visible-heading) + ("k" org-previous-visible-heading) + ("j" org-next-visible-heading) + ("c" org-archive-subtree-as-completed) + ("d" org-cut-subtree) + ("t" org-refile-to-task) + ("i" org-refile-to-incubate) + ("I" org-refile-to-another-incubator) + ("p" org-refile-to-personal-notes) + ("r" org-refile) + ("m X" org-refile-to-projects-dir) + ("m P" org-refile-to-personal-dir) + ("m T" org-refile-to-technical-dir) + ("T" org-todo) + ("S" org-schedule) + ("D" org-deadline) + ("R" org-rename-header) + ("g t" (find-file-other-window org-default-tasks-file)) + ("g i" (find-file-other-window org-default-incubate-file)) + ("g x" (find-file-other-window org-default-inbox-file)) + ("g c" (find-file-other-window org-default-completed-file)) + ("g n" (find-file-other-window org-default-notes-file)) + ("g X" (dired org-default-projects-dir)) + ("g P" (dired org-default-personal-dir)) + ("g T" (dired org-default-technical-dir)) + ("g C" (dired org-default-completed-dir)) + ("[\t]" (org-cycle)) + ("s" (org-save-all-org-buffers) "save") + ("" (org-cycle) "toggle") + ("q" nil "quit")) + +(setq org-refile-use-outline-path 'file + org-refile-allow-creating-parent-nodes t + org-outline-path-complete-in-steps nil) + +(setq org-refile-targets + (append `((,(expand-file-name org-default-media-file) :level . 1) + (,(expand-file-name org-default-notes-file) :level . 0)) + (->> + (directory-files org-default-projects-dir nil ".org") + (-remove-item (file-name-base org-default-media-file)) + (--remove (s-starts-with? "." it)) + (--remove (s-ends-with? "_archive" it)) + (--map (format "%s/%s" (expand-file-name org-default-projects-dir) it)) + (--map `(,it :level . 0))))) + +(setq org-refile-target-table nil) + +(defun org-subtree-region () + "Return a list of the start and end of a subtree." + (save-excursion + (list (progn (org-back-to-heading) (point)) + (progn (org-end-of-subtree) (point))))) + +(defun org-refile-directly (file-dest) + "Move the current subtree to the end of FILE-DEST. +If SHOW-AFTER is non-nil, show the destination window, +otherwise, this destination buffer is not shown." + (interactive "fDestination: ") + + (defun dump-it (file contents) + (find-file-other-window file-dest) + (goto-char (point-max)) + (insert "\n" contents)) + + (save-excursion + (let* ((region (org-subtree-region)) + (contents (buffer-substring (first region) (second region)))) + (apply 'kill-region region) + (if org-refile-directly-show-after + (save-current-buffer (dump-it file-dest contents)) + (save-window-excursion (dump-it file-dest contents)))))) + +(defvar org-refile-directly-show-after nil + "When refiling directly (using the `org-refile-directly' +function), show the destination buffer afterwards if this is set +to `t', otherwise, just do everything in the background.") + +(defun org-refile-to-incubate () + "Refile (move) the current Org subtree to `org-default-incubate-fire'." + (interactive) + (org-refile-directly org-default-incubate-file)) + +(defun org-refile-to-task () + "Refile (move) the current Org subtree to `org-default-tasks-file'." + (interactive) + (org-refile-directly org-default-tasks-file)) + +(defun org-refile-to-personal-notes () + "Refile (move) the current Org subtree to `org-default-notes-file'." + (interactive) + (org-refile-directly org-default-notes-file)) + +(defun org-refile-to-completed () + "Refile (move) the current Org subtree to `org-default-completed-file', +unless it doesn't exist, in which case, refile to today's journal entry." + (interactive) + (if (and org-default-completed-file (file-exists-p org-default-completed-file)) + (org-refile-directly org-default-completed-file) + (org-refile-directly (get-journal-file-today)))) + +(defun org-rename-header (label) + "Rename the current section's header to LABEL, and moves the +point to the end of the line." + (interactive (list + (read-string "Header: " + (substring-no-properties (org-get-heading t t t t))))) + (org-back-to-heading) + (replace-string (org-get-heading t t t t) label)) + +(defun org-archive-subtree-as-completed () + "Archives the current subtree to today's current journal entry." + (interactive) + ;; According to the docs for `org-archive-subtree', the state should be + ;; automatically marked as DONE, but I don't notice it, so let's force: + (when (not (equal "DONE" (org-get-todo-state))) + (org-todo "DONE")) + + (let* ((org-archive-file (or org-default-completed-file + (todays-journal-entry))) + (org-archive-location (format "%s::" org-archive-file))) + (org-archive-subtree))) + +(defun todays-journal-entry () + "Return the full pathname to the day's journal entry file. +Granted, this assumes each journal's file entry to be formatted +with year/month/day, as in `20190104' for January 4th. + +Note: `org-journal-dir' variable must be set to the directory +where all good journal entries live, e.g. ~/journal." + (let* ((daily-name (format-time-string "%Y%m%d")) + (file-name (concat org-journal-dir daily-name))) + (expand-file-name file-name))) + +;; Attempt to load the extra library functions tangled from a different essay: +(condition-case nil + (load-library "boxes-extras") + (error + (defun org-refile-to-projects-dir () + (interactive) + (message "Need to load the 'boxes-extra project first.")) + (defun org-refile-to-personal-dir () + (interactive) + (message "Need to load the 'boxes-extra project first.")) + (defun org-refile-to-technical-dir () + (interactive) + (message "Need to load the 'boxes-extra project first.")))) + +(defun org-boxes-workflow () + "Load the default tasks file and start our hydra on the first task shown." + (interactive) + (let ((org-startup-folded nil)) + (find-file org-default-inbox-file) + (delete-other-windows) + (ignore-errors + (ha/org-agenda)) + (delete-other-windows) + (split-window-right-and-focus) + (pop-to-buffer (get-file-buffer org-default-inbox-file)) + (goto-char (point-min)) + (org-next-visible-heading 1) + (hydra-org-refiler/body))) + +(defun ha/org-agenda () + "Displays my favorite agenda perspective." + (interactive) + (org-agenda nil "a") + (get-buffer "*Org Agenda*") + (execute-kbd-macro (kbd "A t"))) + +(provide 'boxes) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; boxes.el ends here diff --git a/elisp/ha-focus.el b/elisp/ha-focus.el new file mode 100644 index 0000000..fb1906e --- /dev/null +++ b/elisp/ha-focus.el @@ -0,0 +1,74 @@ +;;; ha-focus.el --- Emacs lisp code to focus my thoughts. -*- lexical-binding: t; -*- +;; +;; Copyright (C) 2021 Howard X. Abrams +;; +;; Author: Howard X. Abrams +;; Maintainer: Howard X. Abrams +;; Created: May 28, 2021 +;; +;; This file is not part of GNU Emacs. +;; +;; *NB:* Do not edit this file. Instead, edit the original literate file at: +;; ~/website/Technical/Emacs/focused-work.org +;; And tangle the file to recreate this one. +;; +;;; Code: + +(defvar ha-focus-timer nil "A timer reference for the ha-focus functions") + +(defun ha-focus-countdown-timer (minutes fun) + (let ((the-future (* minutes 60))) + (run-at-time the-future nil fun))) + +(defun ha-focus-begin () + "Start a concerted, focused effort, ala Pomodoro Technique. +We first clock into the current org-mode header (or last one), +start some music to indicate we are working, and set a timer. + +Call `ha-focus-break' when finished." + (interactive) + (ha-focus-countdown-timer 25 'ha-focus-break) + (ha-focus--command "tell application \"Spotify\" to play") + (if (eq major-mode 'org-mode) + (org-clock-in) + (org-clock-in-last))) + +(defun ha-focus-break () + "Stop the focused time by stopping the music. +This also starts another break timer, that calls +`ha-focus-break-over' when finished." + (interactive) + (run-with-idle-timer 5 nil 'ha-focus-capture) + (ha-focus--command "tell application \"Spotify\" to pause") + (message "Time to take a break.")) + +(defun ha-focus-capture () + (ignore-errors + (org-capture nil "cc") + (sit-for 1) + (org-clock-out)) + (ha-focus-countdown-timer 5 'ha-focus-break-over) + (message "Taking a much needed break...")) + +(defun ha-focus-break-over () + "Message me to know that the break time is over. Notice that +this doesn't start anything automatically, as I may have simply +wandered off." + (ha-focus--command "set v to output volume of (get volume settings) +set volume output volume 1 +say \"Break time over. Back on your head.\" +set volume output volume v")) + +(defun ha-focus--command (osascript) + "Runs OSASCRIPT by passing to the `osascript' command asynchronously." + (async-start-process "focus-os" "osascript" 'ha-focus--command-callback "-e" osascript)) + +(defun ha-focus--command-callback (proc) + "Asynchronously called when the `osascript' process finishes." + (message "Finished calling osascript.")) + +(global-set-key (kbd "") 'ha-focus-begin) +(global-set-key (kbd "S-") 'ha-focus-break) + +(provide 'ha-focus) +;;; ha-focus.el ends here