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:
- Part 1
- 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.
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:
- A keyboard hotkey, like ^+c.
- No surrounding double quotes.
- 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> "
. 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!