mac

Case-Insensitive Sorting in Dired on OS X

I like my dired directory listings to be sorted by name regardless of case. This was a bit fiddly to get working in OS X, but I found a solution using the built-in ls-lisp with a few extra options, rather than the system ls to generate the dired listing.

Here are the required settings:

;; using ls-lisp with these settings gives case-insensitve
;; sorting on OS X
(require 'ls-lisp)
(setq dired-listing-switches "-alhG")
(setq ls-lisp-use-insert-directory-program nil)
(setq ls-lisp-ignore-case t)
(setq ls-lisp-use-string-collate nil)
;; customise the appearance of the listing
(setq ls-lisp-verbosity '(links uid))
(setq ls-lisp-format-time-list '("%b %e %H:%M" "%b %e  %Y"))
(setq ls-lisp-use-localized-time-format t)

One downside of this is that it breaks dired-quick-sort, but I can live with that.

Advertisement

An unobtrusive email monitor for mu4e on the Mac

This is not exactly an Emacs post, but some users of the mighty mu4e might find it useful. I don’t like big alerts to new emails that distract me from what I am doing, but I also don’t want to keep switching to my mu4e buffer to see if new emails have arrived. My solution is to have a small notification item in my Mac status bar to tell me how many emails I have.

It looks like this (the bit underlined in red):

email-counter2.png

The text @ 1/3/0/0 tells me that I have, respectively,

  • 1 unread email in my inbox.
  • 3 total emails in my inbox (i.e. 3 too many!).
  • 0 emails in my drafts folder. This is useful to keep an eye on in case I save a draft and then forget about it.
  • 0 emails in my outbox. This is my postfix email outbox and is useful to keep an eye on in case emails sent when I’m offline don’t get automatically sent when I reconnect.

To set this up I use shellwrangler which is a small app that embeds the output of a script in the status bar. Update 2017-11-10 shellwrangler appears dead and bitbar looks like a superior alternative. This then calls the following perl script which generates the text. Shellwrangler Bitbar updates at a specified interval so that is all there is to it.

#!/usr/bin/perl -w

###########################################################################
#
# Count number of emails in inbox, drafts and outbox for mail indexed
# with mu and sent with sendmail or equivalent
#
# by Ben Maughan
# http://www.pragmaticemacs.com
#
# Feel free to distribute, modify, whatever
#
###########################################################################

## total mails in inbox
chomp(my @tot=`/usr/local/bin/mu find maildir:/work/INBOX 2>/dev/null`);
## unread mails in inbox
chomp(my @unread=`/usr/local/bin/mu find maildir:/work/INBOX AND flag:unread 2>/dev/null`);
## drafts
chomp(my @drafts=`/usr/local/bin/mu find maildir:/work/[Gmail].Drafts 2>/dev/null`);

## number in outbox
chomp(my @mailq=`/usr/bin/mailq`);
my @outbox = grep /^[0-9A-F]{12}/, @mailq;

printf "@ %d/%d/%d/%d",$#unread+1,$#tot+1,$#drafts+1,$#outbox+1;

exit;

Automatically copy text selected with the mouse

I’m not much of a mouse user, but a colleague using emacs for OS X wanted to replicate the normal X11 behaviour that linux users are familiar with – text selected with the mouse is automatically copied to the system clipboard. It turns out to be as easy as adding one line to your emacs config file:

(setq mouse-drag-copy-region t)

From the help for the variable (C-h v mouse-drag-copy-region)

If non-nil, copy to kill-ring upon mouse adjustments of the region.

In other words, highlight a block of text and it is copied to the kill-ring (Emacs’ internal clipboard). This also copies to the system clipboard on the Mac so you can then paste to other apps.

Search with OS X Spotlight from emacs

I wrote previously about an idea to use the spotlight search tool in Mac OS X from within emacs. At the time, I put together some pretty crude code to do this, but I have now improved it into a package that I am quite pleased with.

The package is spotlight and is available now to install through MELPA.

The package gives you a powerful way to call spotlight from emacs. You can do a live search for a string in the text of a file, filter the file list by file name and then open the selected file with a swiper search for your query text.

Here is an example of my using it. In the animation below, I use M-x spotlight to run a spotlight search for “tomatoes”. Notice how the number of matches updates as I type or delete the last couple of characters. I then use M-RET to narrow the list of matching files using the string “docs org$” which narrows the list to files with “doc” in their full name, and with names ending in “org”. Finally once I select the file I want, swiper takes me to the matches of my original “tomatoes” query.

spotlight.gif

See the README on github for more details.

The package relies on the excellent ivy and swiper libraries to do the hard work, and benefited greatly from useful comments by redditors at /r/emacs.

Super spotlight search with counsel

Inspired by abo-abo‘s post on using his excellent tools counsel, ivy and swiper to search the contents of files indexed by the recoll search tool, I tried to make something similar for spotlight on the Mac. My attempt is below.

Using counsel gives us incremental updates of the spotlight search results (accessed using its command line interface mdfind). When a match is selected, it is opened in emacs, and (unless it is a pdf) a swiper search is launched on the search string.

This works really nicely. The only problem I’ve had is that I wanted to sort the results to prioritise .org and .tex files, but my sort function is not being used correctly in ivy, but I can’t tell why. It gets passed the counsel prompt for more characters, but not the set of mdfind matches for sorting. My lisp skills have been exhausted at this point, but maybe someone else can see what I’ve done wrong! UPDATE: this problem was fixed with an update to ivy and counsel, and the sorting command below now works.

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; counsel-spotlight                                                      ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Incrementally search the Mac spotlight database and open matching
;; files with a swiper search on the query text.
;; Based on http://oremacs.com/2015/07/27/counsel-recoll/

(require 'counsel)

;; Function to be called by counsel-spotlight
;; The onlyin option limits results to my home directory
;; and directories below that
;; mdfind is the command-line interface to spotlight
(defun counsel-mdfind-function (string &rest _unused)
  "Issue mdfind for STRING."
  (if (< (length string) 4)
      (counsel-more-chars 4)
    (counsel--async-command
     (format "mdfind -onlyin ~/ '%s'" string))
    nil))

;; Main function
(defun counsel-spotlight (&optional initial-input)
  "Search for a string in the mdfind database.
You'll be given a list of files that match.
Selecting a file will launch `swiper' for that file.
INITIAL-INPUT can be given as the initial minibuffer input."
  (interactive)
  (ivy-read "spotlight: " 'counsel-mdfind-function
            :initial-input initial-input
            :dynamic-collection t
            :sort t
            :action (lambda (x)
                      (when (string-match "\\(\/.*\\)\\'" x)
                        (let ((file-name (match-string 1 x)))
                          (find-file file-name)
                          (unless (string-match "pdf$" x)
                            (swiper ivy-text)))))))

;; Define my sort function
(defun bjm-counsel-mdfind-sort-function (x y)
  "Compare two files X and Y. Prioritise org then tex."
  (if (string-match "org$" x)
      t
    (if (string-match "tex$" x)
        (if (string-match "org$" y)
            nil
          t)
      nil)))

;; Add to list of ivy sorting functions
(add-to-list 'ivy-sort-functions-alist
             '(counsel-mdfind-function . bjm-counsel-mdfind-sort-function))

In an ideal world, I’d like to be able to interactively narrow the matches from mdfind with a second counsel on the filenames. The selected file would then open with a swiper search for the first search term. For example, I would like to

  1. Call M-x counsel-spotlight and enter caustic (or enough characters to give me useful results) to get a list of names of files which contain the text caustic somewhere inside them.
  2. Hit some keybinding and get a new counsel prompt and enter chandra to incrementally filter the list of filenames to those containing the string chandra in the filename.
  3. Select the file I want and hit RET to have emacs open that with a swiper search of my original query caustic.

Given the state of my lisp skills, this might take me a while, but it is nice to dream!

Update: A cheat to sort/filter results

Given that my perl skills are much better than my lisp skills, I cheated and wrote a perl wrapper for mdfind that filters and sorts the results for me. The code is below – feel free to use and modify and share. To use this, save the code to a file called bjm-mdfind in your $PATH, make sure it has executable permissions set, and modify the lisp code above to use it:

;; Function to be called by counsel-spotlight
;;
;; mdfind is the command-line interface to spotlight
;;
;; The onlyin option limits results to my home directory
;; and directories below that
;;
;; N.B. below this is replaced with a custom perl wrapper to sort
;; and filter the mdfind results
(defun counsel-mdfind-function (string &rest _unused)
  "Issue mdfind for STRING."
  (if (< (length string) 4)
      (counsel-more-chars 4)
    (counsel--async-command
     (format "bjm-mdfind '%s'" string))
     ;;(format "mdfind -onlyin ~/ '%s'" string))
    nil))

Here is the perl code:

#!/usr/bin/perl -w

###############################################################################
# bjm-mdfind
#
# wrapper for mdfind to filter and sort results
# written by Ben Maughan http://pragmaticemacs.com/
#
# $Id: bjm-mdfind,v 1.2 2015/08/06 11:21:37 bjm Exp $
###############################################################################

use strict;
use File::Basename;
use Getopt::Long;

my $scriptname = basename($0); # Strip away the leading directory names
my $runtime = localtime;

##########################################################################
# customise these options                                                #
##########################################################################
# limit search to this dir (recursively)
my $dir="~/";
# preferred order of file extensions
# others will appear later
my @order=(".org",".tex",".el",".txt",".dat",".pdf");
# exclude files matching these strings
my @exclude=("Library/Caches","Application Support");

###############################################################################
# Handle command line arguments

my $help;
my $man;
my $v=0; #set default verbosity
my $version = defined ((split / /, q/$Revision: 1.2 $/)[1]) ? (split / /, q/$Revision: 1.2 $/)[1] : 0;
my @args=@ARGV;
my $nargs=1; #number of required command line args

&Getopt::Long::Configure( 'bundling' );
GetOptions(
           'help|h' => \$help,
           'man|m'  => \$man,
           'verbose|v=i' => \$v,
) or die "ERROR: Invalid command line option $!";

if ($help||$man||$#ARGV<$nargs-1) { #print help
  # Load Pod::Usage only if needed.
  require "Pod/Usage.pm";
  import Pod::Usage;
  pod2usage(VERBOSE => 1) if $help;
  pod2usage(VERBOSE => 2) if $man;
  pod2usage(VERBOSE => 0, -message => "ERROR: not enough arguments use --help or --man for more help") if $#ARGV<0;
  pod2usage(VERBOSE => 0, -message => "ERROR: not enough arguments - your input was:\n    $scriptname @args") if $#ARGV<$nargs-1;
}

#check input
my $string=$ARGV[0];

###############################################################################
# Main part of program

if ($v > 0) {
  print <<EOF

------------------------$scriptname version $version------------------

Invocation was:
$scriptname @args

Runtime $runtime

EOF
}

chomp(my @out=`mdfind -onlyin $dir $string`);

## sort and filter
##print "$order[2]\n\n";

##filter
my $exc=join "|", @exclude;
@out = grep !/$exc/, @out;

##sort
my @out2;
foreach my $ext (@order) {
    my @tmp = grep /$ext$/, @out;
    print "###$ext\n###@tmp\n\n" if $v>1;
    push @out2, @tmp;
}
##everything else
my $inv=join "\$|", @order;
print "###$inv\n" if $v>1;
my @rest = grep !/$inv$/, @out;
print "###@rest\n" if $v>1;
push @out2, @rest;

##join
my $out=join "\n", @out2;

##print
print "$out\n";

#report successful completion
print "$scriptname completed successfully\n\n" if $v>0;

###############################################################################
# POD documentation

=head1 NAME

bjm-mdfind

=head1 SYNOPSIS

B<bjm-mdfind> [options] string

=head1 DESCRIPTION

wrapper for mdfind to filter and sort results

  string        - search query

=head1 OPTIONS

=over 4

=item B<-h, --help>

Prints out a brief help message.

=item B<-m, --man>

Prints out detailed help.

=item B<-v, --verbosity>

Control the amount of output B<(Default = 1)>

=back

=head1 VERSION

This is $RCSfile: bjm-mdfind,v $ $Revision: 1.2 $

=head1 AUTHOR

Ben Maughan <benmaughan@gmail.com>

=cut

###############################################################################
# $Log: bjm-mdfind,v $
# Revision 1.2  2015/08/06 11:21:37  bjm
# Summary: added URL
#
# Revision 1.1  2015/08/06 11:17:37  bjm
# Initial revision
#

Using server and clients for instantaneous startup

If you use an advanced emacs configuration like my recommended set up prelude, then the startup time for emacs can be several seconds or longer. To avoid this, and to get other benefits I strongly suggest running emacs with a server and clients.

The way this works is that the first time you start emacs, you start a server, and then any other emacs sessions you start just connect to that server. This means they start instantly, and also have the same buffer list as every other emacs window (frame in emacs terminology) that you have open.

To do this we need to make a simple shell script

#! /bin/tcsh -f

# start emacs server - replace with path to your emacs
/Applications/Emacs.app/Contents/MacOS/Emacs --daemon

# start emacs client - replace with path to your emacsclient
# this is usually in the same place as your emacs executable
/Applications/Emacs.app/Contents/MacOS/bin-x86_64-10_9/emacsclient -c $argv

and save it somewhere sensible like ~/scripts/emacs_daemon.csh

Then set up an alias for your emacs command in your ~/.cshrc or similar:

# alias to start emacs in client mode
# starts server if one is not already started
# replace with path to your emacsclient
alias em '/Applications/Emacs.app/Contents/MacOS/bin-x86_64-10_9/emacsclient --alternate-editor "~/scripts/emacs_daemon.csh" -c'

Now you can use the command em to start emacs every time. The first time it will take as long as usual, but after that it will be instant. Running em file1 file2 or similar will open a new emacs window with the named files as you would expect.

One last point to note is that since your emacs windows are now clients of an emacs server that is running in the background, closing the window will not stop the server. If you want to close the entire emacs session and stop the server (e.g. so you can start a fresh emacs session after changing your config file), you can use M-x save-buffers-kill-emacs, which I have bound to C-x c for convenience. To do this, add the following to your emacs config file

;; set shortcut to kill whole emacs session
(global-set-key (kbd "C-x c") 'save-buffers-kill-emacs)

Show file in Finder

This is a tip for Mac users. Use M-x reveal-in-finder to show the current file in a new finder window. If you use this in dired, it will show the file at the cursor in a new finder window.

The reveal-in-finder package may be installed by default with your emacs, but if not, you can install it through the package manager using package-list-packages.

I find this useful for example to quickly attach files to emails in thunderbird. I just reveal the file in finder and then drag it onto my compose window in thunderbird.