Seek

Auto Hotkey

Seek Based on the v1 script by Phi

Navigating the Start Menu can be a hassle, especially if you have installed many programs over time. 'Seek' lets you specify a case-insensitive key word/phrase that it will use to filter only the matching programs and directories from the Start Menu, so that you can easily open your target program from a handful of matched entries. This eliminates the drudgery of searching and traversing the Start Menu.

Download This Script  |  Other Sample Scripts  |  Home

/*
Options:

-cache Use the cached directory-listing if available (this is the default mode
       when no option is specified)
-scan  Force a directory scan to retrieve the latest directory listing
-scex  Scan & exit (this is useful for scheduling the potentially
       time-consuming directory-scanning as a background job)
-help  Show this help

Important notes:

Check AutoHotkey's Tutorial how to run this script, or to compile it if
necessary.

The only 2 files 'Seek' creates are placed in your TMP directory:

  a. _Seek.key  (cache file for last query string)
  b. _Seek.list (cache file for directory listing)

When you run 'Seek' for the first time, it'll scan your Start Menu,
and save the directory listing into a cache file.

The following directories are included in the scanning:
- A_StartMenu
- A_StartMenuCommon

By default, subsequent runs will read from the cache file so as to reduce the
loading time. For more info on options, run 'Seek.exe -help'. If you think your
Start Menu doesn't contain too many programs, you can choose not to use the
cache and instruct 'Seek' to always do a directory scan (via option -scan).
That way, you will always get the latest listing.

When you run 'Seek', a window will appear, waiting for you to enter a
key word/phrase. After you have entered a query string, a list of
matching records will be displayed. Next, you need to highlight an entry and
press ENTER or click on the 'Open' button to run the selected program or open
the selected directory.
*/

/*
Specify which program to use when opening a directory. If the program cannot
be found or is not specified (i.e. variable is unassigned or assigned
a null value), the default Explorer will be used.
*/
config := {dirExplorer: "E:\utl\xplorer2_lite\xplorer2.exe"}

/*
User's customised list of additional directories to be included in the
scanning. The full path must not be enclosed by quotes or double-quotes.
If this file is missing, only the default directories will be scanned.
*/
config.SeekMyDir := "%A_ScriptDir%\Seek.dir"

/*
Specify the filename and directory location to save the cached directory/program
listing and the cached key word/phrase of the last search.
There is no need to change this unless you want to.
*/
config.saveFile := "%A_Temp%\_Seek.ini"

/*
Track search string (True/False)
If true, the last-used query string will be re-used as the default query
string the next time you run Seek. If false, the last-used query string will
not be tracked and there will not be a default query string value the
next time you run Seek.
*/
config.TrackKeyPhrase := True

/*
Specify what should be included in scan.
 F: Files are included.
 D: Directories are included.
*/
config.ScanMode := "FD"

; Init:
; #NoTrayIcon

; Define the script title:
config.ScriptTitle := "Seek - Search the Start Menu"

; Display the help instructions:
if A_Args[1] ~= "^(--help|-help|/h|-h|/\?|-\?)$"
{
  MsgBox("
  (
    Navigating the Start Menu can be a hassle, especially if you have installed
    many programs over time. 'Seek' lets you specify a case-insensitive key
    word/phrase that it will use to filter only the matching programs and
    directories from the Start Menu, so that you can easily open your target
    program from a handful of matched entries. This eliminates the drudgery of
    searching and traversing the Start Menu.

    Options:
      -cache  Use the cached directory-listing if available (this is the default mode
        when no option is specified)
      -scan  Force a directory scan to retrieve the latest directory listing
      -scex  Scan & exit (this is useful for scheduling the potentially
        time-consuming directory-scanning as a background job)
      -help  Show this help
  )", config.ScriptTitle)
  ExitApp()
}

; Check that the mandatory environment variables exist and are valid:
if !FileExist(A_Temp) ; Path does not exist.
{
  MsgBox("
  (
    This mandatory environment variable is either not defined or invalid:
    
        TMP = %A_Temp%
        
    Please fix it before running Seek.
  )", config.ScriptTitle)
  ExitApp()
}

; Scan the Start Menu without Gui:
if A_Args[1] = "-scex"
{
  SaveFileList(config)
  return
}

; Create the GUI window:
G := GuiCreate(, config.ScriptTitle)

; Add the text box for user to enter the query string:
E_Search := G.Add("Edit", "W600")
if config.TrackKeyPhrase
  E_Search.Value := IniRead(config.saveFile, "LastSession", "SearchText")

; Add my fav tagline:
G.Add("Text", "X625 Y10", "What do you seek, my friend?")

; Add the status bar for providing feedback to user:
T_Info := G.Add("Text", "X10 Y31 R1 W764")

; Add the selection listbox for displaying search results:
LB := G.Add("ListBox", "X10 Y53 R28 W764 HScroll Disabled")

; Add these buttons, but disable them for now:
B_1 := G.Add("Button", "Default X10 Y446 Disabled", "Open")
B_2 := G.Add("Button", "X59 Y446 Disabled", "Open Directory")
B_3 := G.Add("Button", "X340 Y446", "Scan Start-Menu")

; Add the Exit button:
B_4 := G.Add("Button", "X743 Y446", "Exit")

; Create function references for event callbacks:
FindMatches := Func("FindMatches").bind(config, LB, B_1, B_2)
OpenTarget := Func("OpenTarget").bind(config, LB)
OpenFolder := Func("OpenFolder").bind(config, LB)
ScanStartMenu := Func("ScanStartMenu").bind(config, E_Search, T_Info, LB, B_1, B_2, B_3)
Gui_Close := Func("Gui_Close").bind(config, E_Search, LB)

; Add control events:
E_Search.OnEvent("Change", FindMatches)
LB.OnEvent("DoubleClick", OpenTarget)
B_1.OnEvent("Click", OpenTarget)
B_2.OnEvent("Click", OpenFolder)
B_3.OnEvent("Click", ScanStartMenu)
B_4.OnEvent("Click", Gui_Close)

; Add window events:
G.OnEvent("Close", Gui_Close)
G.OnEvent("Escape", Gui_Close)

; Pop-up the query window:
G.Show("Center")

; Force re-scanning if -scan is enabled or listing cache file does not exist:
if (A_Args[1] = "-scan" or !FileExist(config.saveFile))
  ScanStartMenu(config, E_Search, T_Info, LB, B_1, B_2, B_3)

; Retrieve an set the matching list:
FindMatches(config, LB, B_1, B_2, E_Search)

; Retrieve the last selection from cache file and select the item:
if config.TrackKeyPhrase
  LB.Choose(IniRead(config.saveFile, "LastSession", "Selection"))

; Function definitions ---

; Scan the start-menu and store the directory/program listings in a cache file:
ScanStartMenu(config, E_Search, T_Info, LB, B_1, B_2, B_3)
{
  ; Inform user that scanning has started:
  T_Info.Value := "Scanning directory listing..."

  ; Disable listbox while scanning is in progress:
  LB.Enabled := false
  B_1.Enabled := false
  B_2.Enabled := false
  B_3.Enabled := false

  ; Retrieve and save the start menu files:
  SaveFileList(config)
  
  ; Inform user that scanning has completed:
  T_Info.Value := "Scan completed."
  
  ; Enable listbox:
  LB.Enabled := true
  B_1.Enabled := true
  B_2.Enabled := true
  B_3.Enabled := true
  
  ; Filter for search string with the new listing:
  FindMatches(config, LB, B_1, B_2, E_Search)
}

; Retrieve and save the start menu files:
SaveFileList(config)
{
  ; Define the directory paths to retrieve:
  LocationArray := [A_StartMenu, A_StartMenuCommon]

  ; Include additional user-defined paths for scanning:
  if FileExist(config.SeekMyDir)
    LoopRead(config.SeekMyDir)
    {
      if !FileExist(A_LoopReadLine)
        MsgBox("
        (
          Processing your customised directory list...
          
          '%A_LoopReadLine%' does not exist and will be excluded from the scanning.
          Please update [ %config.SeekMyDir% ].
        )", config.ScriptTitle, 8192)
      else
        LocationArray.Push(A_LoopReadLine)
    }

  ; Scan directory listing by recursing each directory to retrieve the contents.
  ; Hidden files are excluded:
  IniDelete(config.saveFile, "LocationList")
  For i, Location in LocationArray
  {
    ; Save space by using relative paths:
    IniWrite(Location, config.saveFile, "LocationList", "L" i)
    A_WorkingDir := Location
    LoopFiles("*", config.ScanMode "R")
      if !InStr(FileGetAttrib(A_LoopFilePath), "H") ; Exclude hidden file.
        FileList .= "`%L" i "`%\" A_LoopFilePath "`n"
  }
  IniDelete(config.saveFile, "FileList")
  IniWrite(FileList, config.saveFile, "FileList")
}

; Search and display all matching records in the listbox:
FindMatches(config, LB, B_1, B_2, E_Search)
{
  FileArray := []
  SearchText := E_Search.Value
  ; Filter matching records based on user query string:
  if SearchText
  {
    Loop
    {
      Location := IniRead(config.saveFile, "LocationList", "L" A_Index)
      if !Location
        break
      L%A_Index% := Location
    }
    LoopParse(IniRead(config.saveFile, "FileList"), "`n")
    {
      Line := Deref(A_LoopField) ; Replace %L_n% with location paths.
      if SearchText <gt; E_Search.Value
      {
        ; User has changed the search string.
        ; There is no point to continue searching using the old string, so abort.
        return
      }
      else
      {
        ; Append matching records into the list:
        SplitPath(Line, Name)
        MatchFound := true
        LoopParse(SearchText, " ")
        {
          if !InStr(Name, A_LoopField)
          {
            MatchFound := false
            break
          }
        }
        if MatchFound
          FileArray.Push(Line)
      }
    }
  }

  ; Refresh list with search results:
  LB.Delete(), LB.Add(FileArray)

  if !FileArray.Length()
  {
    ; No matching record is found. Disable listbox:
    LB.Enabled := false
    B_1.Enabled := false
    B_2.Enabled := false
  }
  else
  {
    ; Matching records are found. Enable listbox:
    LB.Enabled := true
    B_1.Enabled := true
    B_2.Enabled := true
    ; Select the first record if no other record has been selected:
    if LB.Text = ""
      LB.Choose(1, 1)
  }
}

; User clicked on 'Open' button or pressed ENTER:
OpenTarget(config, LB)
{
  ; Selected record does not exist (file or directory not found):
  if !FileExist(LB.Text)
  {
    MsgBox("
    (
      %LB.Text% does not exist.
      
      This means that the directory cache is outdated. You may click on
      the 'Scan Start-Menu' button below to update the directory cache with your
      latest directory listing now.
    )", config.ScriptTitle, 8192)
    return
  }

  ; Check whether the selected record is a file or directory:
  fileAttrib := FileGetAttrib(LB.Text)
  if InStr(fileAttrib, "D") ; is directory
    OpenFolder(config, LB)
  else if fileAttrib ; is file
    Run(LB.Text)
  else
  {
    MsgBox("
    (
      %LB.Text% is neither a DIRECTORY or a FILE.
      
      This shouldn't happen. Seek cannot proceed. Quitting...
    )")
  }
  WinClose()
}

; User clicked on 'Open Directory' button:
OpenFolder(config, LB)
{
  Path := LB.Text
  ; If user selected a file-record instead of a directory-record, extract the
  ; directory path (I'm using DriveGet instead of FileGetAttrib to allow the
  ; scenario whereby LB.Text is invalid but the directory path of LB.Text is valid):
  if DriveGet("Status", Path) <gt; "Ready" ; not a directory
  {
    SplitPath(Path,, Dir)
    Path := Dir
  }

  ; Check whether directory exists:
  if !FileExist(Path)
  {
    MsgBox("
    (
      %Path% does not exist.
      
      This means that the directory cache is outdated. You may click on
      the 'Scan Start-Menu' button below to update the directory cache with your
      latest directory listing now.
    )", config.ScriptTitle, 8192)
    return
  }

  ; Open the directory:
  if FileExist(config.dirExplorer)
    Run('"%config.dirExplorer%" "%Path%"') ; Open with custom file explorer.
  else
    Run(Path) ; Open with default windows file explorer.
}


Gui_Close(config, E_Search, LB)
{
  ; Save the key word/phrase for next run:
  if config.TrackKeyPhrase
  {
    IniWrite(E_Search.Value, config.saveFile, "LastSession", "SearchText")
    IniWrite(LB.Text, config.saveFile, "LastSession", "Selection")
  }
  ExitApp()
}