The Sharat's

The Magic of AutoHotkey — Part 2

In the previous part of The Magic of AutoHotkey, we looked at automating small pieces of routine tasks with various applications, as well as identifying things that could be done better with a quick hotkey. This is the next chapter of the story. In this article, I’ll show you how I tamed the stock file explorer as well as connecting to office applications with OLE to provide additional rich functionality.

This article is part of a series:

  1. Part 1
  2. Part 2 (this article).

File Explorer Magic

The file explorer is probably my most used application during work. Yet, it doesn’t feel like it’s tuned for a power user. Maybe that’s also why there’s so many alternatives to file explorers. I’ve tried a few of them in the past, but the best has been to add exactly the few things I needed in the native file explorer, using AutoHotkey. I’ll run through those here.

As is the case in the previous part, I have a module called file-explorer-tweaks.ahk which is #Include-ed in my master script.

To start, we define a window group, which includes all file explorer windows. We later use this group to define hotkeys that we want to work only on the file explorer windows.

GroupAdd, FileListers, ahk_class CabinetWClass
GroupAdd, FileListers, ahk_class WorkerW
GroupAdd, FileListers, ahk_class #32770, ShellView

This group now matches the file explorer windows, desktop and the file open dialog windows.

Focus Location Editor

Almost all the web browsers today have the default hotkey ^l which focuses the location bar, and selects everything in it. But in the file explorer, this is !d. Habits rule and I constantly hit ^l in the file explorer window when I wanted to change something in the location bar. Obviously, it didn’t work, and it would drive me crazy. Until I added the following to save me from insanity:

#IfWinActive ahk_group FileListers
^l::SendInput !d

While this works fine on the face of it, if I hit Escape after focusing the location bar like this, the focus is not returned to the file list. I haven’t figured out a solution to that yet, so that one’s open.

Open Command Window

The file explorer has a nice less-known trick. If I right-click without any files selected and with the Shift key held down, I get an extra option in the context menu, called “Open command window here”. Clicking on that menu item will open a new command prompt window in the current directory. This is extremely convenient if you need the command window often (which you might, especially if you’re a software developer).

But this needed the mouse. I wanted to do this with the keyboard. Turns out it’s easier than one might think:

#IfWinActive ahk_group FileListers
^!t::SendInput !dcmd{Enter}

Here, we define the ^!t hotkey which will focus the location bar and type in cmd and hit the Enter key. This will actually open up a command window in the current directory.

Folder Shortcuts

Folder shortcuts is where I define a hotkey that will navigate to a specific directory, always. For example, while in a file explorer, hitting ^h should navigate to the home folder, hitting ^j should navigate to the Downloads folder (this key opens the downloads view in web browsers, see what I did there?).

#IfWinActive ahk_group FileListers
^h::Send !d%homedir%{Enter}
^j::Send !d%homedir%\Downloads{Enter}
^y::Send !dLibraries\Documents{enter}
^k::Send !dC:\work{Enter}
^t::Send !dC:\tools{Enter}
^b::Send !dC:\labs{Enter}

This snippet uses the homedir variable defined in the previous article.

On the face of it, these are very simple hotkeys. We pass !d to focus the location input and type in the location where we want to go to. Simple & effective. They serve sort of like quick access bookmarks and are probably my most used hotkeys defined with AutoHotkey overall, by a margin.

Better Hotkeys for Directional Navigation

In the previous section, we dealt with navigating to absolution locations. But how about directional navigation, where we want to go back or forward or even up the directory chain?

The default hotkeys for this leverage the arrow keys, which require taking my hands off the keyboard’s home row. So, I’m using the following keys for these three operations, which are inspired by similar behavior in Vim (again!).

; Navigate with the keyboard better!
#IfWinActive ahk_group FileListers
^o::SendInput, !{Left}
^i::SendInput, !{Right}
^u::SendInput, !{Up}

To top it, I have also defined mouse “hotkeys” for these three actions. I rarely use these nowadays, but they’re still there for when I already have a hand on the mouse.

; Navigate with the mouse!
#IfWinActive ahk_group FileListers
!WheelUp::SendInput, !{Up}
^WheelUp::SendInput, !{Left}
^WheelDown::SendInput, !{Right}

Pretty self-explanatory really.

Select Files by Pattern

I particularly love this one. When I trigger this hotkey, a little prompt shows up where I enter a regular expression and then every file in the current folder that matches this pattern will be selected. The first time I used this on a folder with ~300 files, I practically had tears in my eyes at how easy it was to make the file selection by a pattern.

So, here’s the code for this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
; Get selected files in explorer and more:
; http://www.autohotkey.com/board/topic/60985-get-paths-of-selected-items-in-an-explorer-window/
#IfWinActive ahk_group FileListers
^s::
SelectByRegEx() {
    static selectionPattern := ""
    WinGetPos, wx, wy
    ControlGetPos, cx, cy, cw, , DirectUIHWND3
    x := wx + cx + cw/2 - 200
    y := wy + cy
    InputBox, selectionPattern, Select by regex
        , Enter regex pattern to select files that CONTAIN it (Empty to select all)
        , , 400, 150, %x%, %y%, , , %selectionPattern%
    if ErrorLevel
        Return
    for window in ComObjCreate("Shell.Application").Windows
        if WinActive("ahk_id " . window.hwnd) {
            pattern := "S)" . selectionPattern
            items := window.document.Folder.Items
            total := items.Count()
            i := 0
            showProgress := total > 160
            if (showProgress)
                Progress, b w200, , Matching...
            for item in items {
                match := RegExMatch(item.Name, pattern) ? 17 : 0
                window.document.SelectItem(item, match)
                if (showProgress) {
                    i := i + 100
                    Progress, % i / total
                }
            }
            Break
        }
    Progress, Off
}

The code is not very pretty, but oh well. It works well, and I’d rather not touch it.

Here’s a little mute video recording of this at work:

Batch Rename

This is actually built to be invoked as a separate AutoHotkey process, not to be #Include-ed into a master script. That’s because the GUI is slightly more complex than what we’ve seen in previous sections and I didn’t bother to make it work well as a module.

batch-rename.ahk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
#NoEnv
#NoTrayIcon

active_hwnd := WinActive("ahk_class CabinetWClass")
If (active_hwnd) {
    for window in ComObjCreate("Shell.Application").Windows
        If (active_hwnd == window.hwnd) {
            parent := uriDecode(StrReplace(window.LocationURL, "file:///", "", , 1))
            ShowGui()
        }
}

ShowGui() {
    global active_hwnd, parent, SourcePattern, TargetPattern, WindowListView
    Gui, Font, s10 q5, Segoe UI
    Gui, Margin, 6, 6
    Gui, +Owner%active_hwnd%
    Gui, Add, Text, , Search pattern:
    Gui, Add, Edit, r1 w300 vSourcePattern gInputChanged -WantReturn X+6 Section
    Gui, Add, Text, X+6, Full regex is supported
    Gui, Add, Text, XM, Replacement:
    Gui, Add, Edit, r1 w300 vTargetPattern gInputChanged -WantReturn XS YP
    Gui, Add, Text, X+6, Use $1, $2, ${10}, ${named}, $U1, $U{10}, $L2, $T0 etc.
    Gui, Add, Button, Default gDoRename XM w80, Apply
    Gui, Add, Button, gShowHelp X+6 w80, Help
    Gui, Add, ListView, Grid r12 w800 vWindowListView XM, Replacements|Current name|Renamed to

    imList := IL_Create(2)
    LV_SetImageList(imList)
    IL_Add(imList, "check.png", 0xFFFFFF, 1)
    IL_Add(imList, "error.png", 0xFFFFFF, 1)
    ; IL_Add(imList, "shell32.dll", 145)
    ; IL_Add(imList, "shell32.dll", 234)

    Gui, Show, , Rename with Regex: %parent%
}

InputChanged() {
    global parent, SourcePattern, TargetPattern
    GuiControlGet, SourcePattern
    GuiControlGet, TargetPattern
    LV_Delete()
    Loop, Files, %parent%\*, FD
    {
        toName := RegExReplace(A_LoopFileName, SourcePattern, TargetPattern, count)
        icon := 1
        If (A_LoopFileName == toName)
            icon := 3
        Else if (FileExist(parent . "/" . toName))
            icon := 2
        LV_Add("Icon" . icon, count, A_LoopFileName, toName)
    }
    LV_ModifyCol()
}

DoRename() {
    global parent, SourcePattern, TargetPattern
    Gui, Submit

    If (SourcePattern != "")
        Loop %parent%\* {
            toName := RegExReplace(A_LoopFileName, SourcePattern, TargetPattern)
            FileMove, %parent%\%A_LoopFileName%, %parent%\%toName%
        }

    GuiClose()
}

GuiEscape() {
    GuiClose()
}

GuiClose() {
    ExitApp
}

uriDecode(str) {
    Loop
        If RegExMatch(str, "i)(?<=%)[\da-f]{1,2}", hex)
            StringReplace, str, str, `%%hex%, % Chr("0x" . hex), All
        Else Break
    Return, str
}

ShowHelp() {
    help=
    (
## Pattern:

The pattern to search for, which is a Perl-compatible regular expression (PCRE). The pattern's options (if any) must be included at the beginning of the string followed by a close-parenthesis. For example, the pattern "i)abc.*123" would turn on the case-insensitive option and search for "abc", followed by zero or more occurrences of any character, followed by "123". If there are no options, the ")" is optional; for example, ")abc" is equivalent to "abc".

## Replacement:

The string to be substituted for each match, which is plain text (not a regular expression). It may include backreferences like $1, which brings in the substring from Haystack that matched the first subpattern. The simplest backreferences are $0 through $9, where $0 is the substring that matched the entire pattern, $1 is the substring that matched the first subpattern, $2 is the second, and so on. For backreferences above 9 (and optionally those below 9), enclose the number in braces; e.g. ${10}, ${11}, and so on. For named subpatterns, enclose the name in braces; e.g. ${SubpatternName}. To specify a literal $, use $$ (this is the only character that needs such special treatment; backslashes are never needed to escape anything).

To convert the case of a subpattern, follow the $ with one of the following characters: U or u (uppercase), L or l (lowercase), T or t (title case, in which the first letter of each word is capitalized but all others are made lowercase). For example, both $U1 and $U{1} transcribe an uppercase version of the first subpattern.

Nonexistent backreferences and those that did not match anything in Haystack -- such as one of the subpatterns in "(abc)|(xyz)" -- are transcribed as empty strings.
)
    MsgBox, %help%
}

Put this script at a convenient location, probably right next to your master script, and add the following hotkey to your master script:

#IfWinActive ahk_group FileListers
^+b::Run batch-rename.ahk

Here’s a little mute video recording of some usage examples of this tool:

If you’re using this, please keep caution. Please inspect the previous table before clicking on the “Apply” button. If it ends up messing your files up, don’t hold me responsible. I’m sharing this without warranty. As any source code block on this website, this is shared here with MIT License.

Copy Paths of Selected Files

This, again, is actually partly fulfilled by default Windows functionality. When we Shift+Right Click on a file, we get the option to “Copy as path”, which works fine for simple cases. But I wanted the following additional things for this feature:

  1. A keyboard hotkey, like ^+c.
  2. No surrounding double quotes.
  3. Work with multiple files being selected. Copy each file’s path as one line.

For this, I defined the following ^+c hotkey on the file explorer windows.

#IfWinActive ahk_group FileListers
^+c::
    Clipboard := JoinArrayContents(Explorer_GetSelected())
    Return

This will get a list of all selected files in the current explorer window and join them into a single string. The Explorer_GetSelected function comes from this AutoHotkey forum post and the JoinArrayContents is given below:

JoinArrayContents(arr, delimiter="`n") {
    content := ""
    for index, item in arr {
        if index > 1
            content := content . delimiter
        content := content . item
    }
    return content
}

Now I can select one or more files, hit ^+c and the full paths of all the selected files will end up in my clipboard.

Copy Contents of Selected Files

This one, although sounds similar to the previous section, is quite different and useful in a very different way. Where the previous section’s hotkey copies the selected files’ paths, this hotkey is intended to copy the selected files’ contents as a whole.

I have a few (several?) small text files with snippets, template messages, etc. With this, I just select one or multiple files and hit Ctrl+Shift+x and I’m ready to paste their contents.

#IfWinActive ahk_group FileListers
^+x::
    CopySelectedFileContents() {
        files := Explorer_GetSelected()
        content := ""
        for i, file in files {
            FileRead, text, %file%
            if i > 1
                content := content . "`n`n"
            content := content . text
        }
        Clipboard := content
    }

This is the same Explorer_GetSelected I referred to in the previous section. However, in the above hotkey definition, instead of setting the paths to Clipboard, we set the contents of the files.

Just like the previous hotkey, I can select multiple text files and hit ^+x and the contents of all selected files will end up in my clipboard, separated by two blank lines.

This doesn’t work with images yet though. Still have to figure that one out.

Create File with Clipboard Contents

This is the opposite of the previous hotkey. Here, I want whatever is in the Clipboard to be saved to a text file in the current folder.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#IfWinActive ahk_group FileListers
^+v::
    CreateFileWithClipboardContents() {
        loc := Explorer_GetPath()
        WinGetPos, wx, wy
        ControlGetPos, cx, cy, cw, , DirectUIHWND3
        x := wx + cx + cw/2 - 200
        y := wy + cy
        InputBox, filename, Clipboard File
            , Enter file name to paste clipboard contents in:, , 400, 120, %x%, %y%, ,
            , clip.txt
        if ErrorLevel
            Return
        filepath := loc . "\" . filename
        if (FileExist(filepath)) {
            MsgBox, 1, Overwrite, Overwriting existing '%filename%'!
            IfMsgBox Cancel
                Return
            FileDelete, %filepath%
        }
        Fileappend, %Clipboard%, %filepath%
    }

The Explorer_GetPath function used in the above snippet is also from the same source I mentioned in the previous sections. The way this works is when the hotkey is triggered, we are prompted to enter the name of the file to which the clipboard’s contents will be saved. Once we provide a file name and submit, the file is created.

With this, I can copy some text out of a webpage or an email in Outlook and saving it to a text file is a quick ^+v. Once I created this hotkey, it became my primary way of creating new text files. I no longer open Notepad, write (or paste) and then save the file to the desired directory. Instead, I open the folder, use this hotkey to create the file, and then open the file in Notepad. Somehow, it feels more natural.

This doesn’t work with images either. Have to figure this one out too.

Create Folder Hierarchy and Enter it

The file explorer has a default hotkey for creating new folders (Ctrl+Shift+n), but it doesn’t let us create a tree or folders at one go. To do that, we have to create a directory, enter it, create again etc. This quickly gets tedious if it has to be done often.

As always I tried to address it with AutoHotkey.

#IfWinActive ahk_group FileListers
^n::
CreateFolderHierarchy() {
    loc := Explorer_GetPath()
    WinGetPos, wx, wy
    ControlGetPos, cx, cy, cw, , DirectUIHWND3
    x := wx + cx + cw/2 - 200
    y := wy + cy
    InputBox, folder, Create Folder, Enter folder name/path create:, , 400, 120
        , %x%, %y%
    if ErrorLevel
        Return
    folder := StrReplace(folder, "/", "\")
    pos := RegExMatch(folder, "O)\{([^\{]+)\}", match)
    folders := []
    if (pos > 0) {
        parts := StrSplit(match.value(1), ",")
        prefix := SubStr(folder, 1, match.Pos(0) - 1)
        suffix := SubStr(folder, match.Pos(0) + match.Len(0))
        for i, part in parts {
            folders.Push(prefix . part . suffix)
        }
    } else {
        folders.Push(folder)
    }
    for i, folder in folders {
        FileCreateDir, %loc%\%folder%
    }
    Explorer_GetWindow().Navigate2(loc . "\" . folders[folders.Length()])
}

This uses the same explorer library I mentioned in the previous sections. When this hotkey is triggered, we get a prompt where we can enter a folder tree (i.e., folders separated by / or \) and they will all be created. As a bonus, we are also switched to that newly created folder, so we can start working with it right away.

Now I can hit ^n and type in src/main/java or 2020-01/pics, and all nesting structure is created and navigated, which is usually followed by pasting some files.

Email Selected File(s) with Outlook

Outlook is necessary tool for email at most corporate workplaces. So it’s important to look at how we use it, and what parts of it we can automate / improve.

It’s also quite common to have to send files over email as attachments. Yet, considering how often we tend to do that, it’s still a tedious process. Go to outlook, start new mail, drag-drop the file in this window, fill up the mail, send. It gets a bit better if you copy the file to clipboard and then instead of starting a new mail with Ctrl+n, you could just hit Ctrl+v in the Outlook Mails view and new mail will open up with file in clipboard as attachment. But I’d say it’s still not good enough.

The solution I currently use is the Ctrl+m hotkey for file explorers. The workflow is that I select some files in my file explorer, hit Ctrl+m and a new mail window opens up with the selected files as attachments, the message body containing the list of files for me to edit and subject containing the list of files.

#IfWinActive ahk_group FileListers
^m::OutlookNewMail(Explorer_GetSelected())

The Explorer_GetSelected function is from the same library I mentioned in an earlier section. The following is the definition of the OutlookNewMail function:

OutlookNewMail(attachments=0) {
    outlook := ComObjActive("Outlook.Application")
    mail := outlook.CreateItem(0)

    if (attachments != 0) {
        msg := ""
        sub := "Files: "
        for index, file in attachments {
            mail.Attachments.Add(file)
            SplitPath, file, basename
            msg := msg . "<p class=MsoNormal>&nbsp;&nbsp;&nbsp; "
                    . basename . "<o:p></o:p></p>"
            if (attachments.Length() == 1)
                sub := "File: " . basename
            else if (index == attachments._MaxIndex())
                sub := sub . " & " . basename
            else if (index == attachments._MinIndex())
                sub := sub . basename
            else
                sub := sub . ", " . basename
        }

        FileRead, emailTpl, email.tpl.txt
        mail.HTMLBody := StrReplace(emailTpl, "$$MESSAGE$$", msg . "</ul>")
        mail.Subject := sub
    }

    mail.Display
}

AutoHotkey supports connecting to OLE objects, which means we can create hotkey that create rich interactions with Office applications like Outlook. We leverage this in the above function.

All I have to do now, is fill up the “To:” field and hit Ctrl+Enter. I’ve been loving this ever since.

Note, of course, that since this connects to the Outlook OLE object, Outlook needs to be running for this work.

Global Hotkey for New Mail

If you’ve noticed, the above function’s attachments argument has a default value. If this argument is not provided, we just get a blank email window open up. This is convenient on its own. So I have it as a global hotkey:

#c::OutlookNewMail()

This works really well since the new mail window opens up with my signature already filled up and the focus is set to the “To:” field perfectly to quickly start working on my email.

Conclusion

AutoHotkey is a powerful tool for automating all sorts of workflows on Windows. If you can get past the quirks in the language itself, the underlying engine is very powerful. I know that over the few years I’ve used it, I’ve only made use of a small portion of its potential. In addition, the help file that is shipped with AutoHotkey (right-click on the tray icon and click on “Help”) is very good. It’s exhaustive, very detailed and contains lots of examples. I encourage going over it occasionally to find interesting things to add to your workflow. Good luck!

Discuss on: Reddit.