P02: Application Launcher with Fuzzy Search
P02: Application Launcher with Fuzzy Search
Project Overview
What youâll build: A Spotlight/Alfred-like launcher for Windowsâpress a hotkey, type a few characters, and launch any program, file, or folder using fuzzy matching.
| Attribute | Value |
|---|---|
| Difficulty | Level 2: Intermediate |
| Time Estimate | 1-2 weeks |
| Programming Language | AutoHotkey v2 |
| Knowledge Area | Windows Automation, Search Algorithms, GUI Design |
| Prerequisites | Basic AutoHotkey syntax, understanding of filesystem concepts |
1. Learning Objectives
After completing this project, you will be able to:
- Traverse the Windows filesystem - Recursively scan directories using both iterative and recursive approaches, understanding the tradeoffs
- Implement string matching algorithms - Build fuzzy search using Levenshtein distance, subsequence matching, and scoring heuristics
- Design for performance - Create indexes that enable sub-millisecond search on thousands of items
- Build responsive GUIs - Keep interfaces snappy by separating indexing from searching
- Work with Windows special folders - Navigate Program Files, Start Menu, AppData, and understand Windows application deployment
- Implement result ranking - Combine match quality with usage frequency for intelligent ordering
- Track usage patterns - Persist user behavior to improve future predictions
- Handle Windows shortcuts - Resolve .lnk files using COM automation
- Apply debouncing patterns - Prevent excessive computation during rapid user input
2. Deep Theoretical Foundation
2.1 Filesystem Traversal and Indexing Strategies
The Windows filesystem is a hierarchical tree structure where directories (folders) contain files and other directories. When building an application launcher, you need to visit every node in this tree to find launchable items.
C:\
|
+-----------+-----------+
| | |
Program Files Users Windows
| | |
+---+---+ Douglas System32
| | | |
Chrome Python AppData (skip)
| | |
chrome.exe python.exe Local
|
Programs
|
+---+---+
| |
Discord VSCode
| |
discord.exe code.exe
Two Fundamental Traversal Approaches:
1. Recursive Traversal (Depth-First)
Algorithm: RecursiveTraverse(folder)
for each item in folder:
if item is file AND matches criteria:
add to index
else if item is directory:
RecursiveTraverse(item) <-- Stack frame created
Visual of stack frames during recursion:
Call Stack:
|-------------------------|
| RecursiveTraverse(Chrome) | <- Current call
|-------------------------|
| RecursiveTraverse(Program Files) |
|-------------------------|
| RecursiveTraverse(C:\) |
|-------------------------|
As we go deeper, stack grows. Deep nesting = stack overflow risk!
Pros: Elegant, easy to understand, natural for tree structures Cons: Stack overflow risk for deep paths (Windows allows 32,767 characters in paths)
2. Iterative Traversal (Breadth-First using Queue)
Algorithm: IterativeTraverse(rootFolder)
queue = [rootFolder]
while queue is not empty:
folder = queue.dequeue()
for each item in folder:
if item is file AND matches criteria:
add to index
else if item is directory:
queue.enqueue(item) <-- Uses heap memory
Visual of queue during traversal:
Step 1: queue = [C:\]
Processing C:\, found: Program Files, Users, Windows
queue = [Program Files, Users, Windows]
Step 2: queue = [Users, Windows]
Processing Program Files, found: Chrome, Python
queue = [Users, Windows, Chrome, Python]
Step 3: queue = [Windows, Chrome, Python]
Processing Users, found: Douglas
queue = [Windows, Chrome, Python, Douglas]
... continues until queue is empty
Pros: No stack overflow, uses heap memory, can handle any depth Cons: Slightly more code, less intuitive
AutoHotkeyâs Built-in Solution:
; The 'R' flag enables recursive traversal
Loop Files, "C:\Program Files\*.exe", "R"
{
; A_LoopFileFullPath contains the complete path
; A_LoopFileName contains just the filename
}
AutoHotkey handles the recursion internally, but understanding the underlying concepts helps you debug performance issues.
Indexing Strategy: Scan Once, Search Many Times
Script Startup
|
v
+------------------------+
| Scan All Folders | <-- Takes 2-5 seconds
| (One-time cost) |
+------------------------+
|
v
+------------------------+
| Build In-Memory |
| Index Array |
+------------------------+
|
v
+----------------+----------------+
| | |
Search #1 Search #2 Search #N
(< 5ms) (< 5ms) (< 5ms)
User types "chr" User types "pyt" etc.
What to Index (Program Locations on Windows):
| Location | Contains | Priority |
|---|---|---|
%APPDATA%\Microsoft\Windows\Start Menu\Programs |
User shortcuts (.lnk) | High |
C:\ProgramData\Microsoft\Windows\Start Menu\Programs |
System-wide shortcuts | High |
C:\Program Files |
64-bit applications | Medium |
C:\Program Files (x86) |
32-bit applications | Medium |
%LOCALAPPDATA%\Programs |
User-installed apps (Discord, VS Code) | Medium |
%USERPROFILE%\Desktop |
Desktop shortcuts | Medium |
| Registry App Paths | Registered executables | Low |
2.2 String Matching and Fuzzy Search Algorithms
String matching is the core intellectual challenge of this project. Users expect âintelligentâ behaviorâthe launcher should understand intent, not just literal text.
The Spectrum of String Matching:
Strictness Flexibility
| |
v v
+----------+ +--------+ +----------+ +-------+
| Exact | --> | Prefix | --> | Substring| --> | Fuzzy |
| Match | | Match | | Match | | Match |
+----------+ +--------+ +----------+ +-------+
"chrome" "chr..." "...rom..." "chrme"
= "chrome" matches matches matches
"chrome" "chrome" "chrome"
1. Exact Matching
Query: "chrome"
Target: "chrome"
Result: MATCH (query == target)
Query: "Chrome"
Target: "chrome"
Result: NO MATCH (case-sensitive) or MATCH (case-insensitive)
2. Prefix Matching
Query: "chr"
Target: "chrome"
Check: Does target start with query?
"chrome".startsWith("chr") = true
c h r o m e
| | |
c h r <- Query matches beginning
Result: MATCH
3. Substring/Contains Matching
Query: "rom"
Target: "chrome"
Check: Does target contain query anywhere?
"chrome".contains("rom") = true
c h r o m e
| | |
r o m <- Query found inside
Result: MATCH (but weaker than prefix)
4. Fuzzy Matching - Subsequence Algorithm
The key insight: users type characters in order, but may skip some.
Query: "vscd"
Target: "Visual Studio Code"
Attempt to find all query characters in order:
V i s u a l S t u d i o C o d e
^ ^ ^ ^
v s c d
Query characters found in sequence: v-s-c-d
All 4 characters found in order!
Result: MATCH
Subsequence Matching Algorithm (Pseudocode):
function subsequenceMatch(query, target):
queryIndex = 0
targetIndex = 0
while queryIndex < query.length AND targetIndex < target.length:
if query[queryIndex] == target[targetIndex]:
queryIndex++ // Found this character, move to next query char
targetIndex++ // Always move through target
// If we matched all query characters, it's a match
return queryIndex == query.length
Visual trace of algorithm:
Query: "pyt"
Target: "PyTest"
Step 1: query[0]='p' vs target[0]='P' -> MATCH (case-insensitive)
queryIndex=1, targetIndex=1
Step 2: query[1]='y' vs target[1]='y' -> MATCH
queryIndex=2, targetIndex=2
Step 3: query[2]='t' vs target[2]='T' -> MATCH
queryIndex=3, targetIndex=3
Result: queryIndex(3) == query.length(3) -> TRUE, it's a match!
5. Levenshtein Distance (Edit Distance)
For true typo tolerance, we calculate the minimum edits needed to transform one string into another.
Operations allowed:
- Insert a character
- Delete a character
- Replace a character
Query: "chrme" (user typo)
Target: "chrome"
Alignment:
c h r m e
c h r o m e
Edits needed:
1. Insert 'o' after 'r'
Distance = 1 (very close match!)
Levenshtein Distance Algorithm (Dynamic Programming):
function levenshteinDistance(s1, s2):
// Create matrix of size (len(s1)+1) x (len(s2)+1)
For "cat" vs "car":
"" c a t
"" 0 1 2 3
c 1 0 1 2
a 2 1 0 1
r 3 2 1 1 <- Final answer: 1 edit
Each cell [i,j] = minimum of:
- cell[i-1,j] + 1 (delete)
- cell[i,j-1] + 1 (insert)
- cell[i-1,j-1] + (0 if match else 1) (replace or no-op)
Practical Consideration: Levenshtein is O(n*m) complexity. For a launcher with 5000 items, this can be slow. Use it only as a fallback after faster algorithms fail.
6. Scoring System Design
Not all matches are equal. We need a scoring system:
Score Components:
+------------------------------------------+
| Match Type | Base Score |
+------------------------------------------+
| Exact match | 1000 |
| Prefix match | 500 + ratio bonus |
| Substring match | 200 + ratio bonus |
| Subsequence match | 100 + bonuses |
| Levenshtein <= 2 | 50 |
| No match | 0 |
+------------------------------------------+
Ratio bonus = (query.length / target.length) * 100
Consecutive bonus = +10 per consecutive matched character
Start-of-word bonus = +30 if match starts at word boundary
Example Scoring:
Query: "vs"
Target 1: "VS Code"
- Prefix match: 500
- Ratio: 2/7 * 100 = 28
- Starts at word: +30
- Score: 558
Target 2: "Visual Studio"
- Subsequence match: 100
- V and S are at word starts: +30 each
- Score: 160
Result: "VS Code" ranks higher (correct!)
2.3 Caching and Performance Optimization
Performance is critical for a launcherâusers expect instant results.
The Caching Hierarchy:
Speed +-------------------+
^ | CPU Cache | < 1 nanosecond
| +-------------------+
| | RAM | ~100 nanoseconds
| +-------------------+
| | SSD/NVMe | ~100 microseconds
| +-------------------+
| | HDD | ~10 milliseconds
v +-------------------+
Slow | Network | ~100 milliseconds
Strategy: Keep the index in RAM
Startup:
1. Read from disk (slow, once)
2. Build array in RAM
3. All searches use RAM (fast, many times)
+-----------------+
Disk (HDD/SSD) | index.json |
+-----------------+
|
| Load once at startup
v
+-----------------+
RAM | AppIndex[] | <- All searches here
| 5000 items |
| ~2MB memory |
+-----------------+
Optimization Techniques:
1. Prefix Hashtable for O(1) Lookup
Instead of searching all 5000 items for "chr":
Standard array: Check all 5000 items = O(n)
Prefix hashtable:
prefixMap = {
"a": [items starting with a...],
"b": [items starting with b...],
...
"chr": [Chrome, ChromeDriver], <- Direct lookup!
...
}
Search "chr": prefixMap["chr"] = [Chrome, ChromeDriver]
Only 2 items to check!
2. Early Termination
function search(query, index, maxResults=10):
results = []
for item in index:
if results.length >= maxResults * 3: // Got enough candidates
break // Stop searching
score = calculateScore(query, item)
if score > 0:
results.add({item, score})
return results.sortByScore().take(maxResults)
3. Lazy Scoring
// Bad: Calculate full Levenshtein for every item
for item in index:
score = levenshteinDistance(query, item.name) // Expensive!
// Good: Quick filter first, expensive scoring only for survivors
for item in index:
if quickMatch(query, item): // Fast check
score = fullScore(query, item) // Only for candidates
2.4 GUI Responsiveness and Event Loops
Understanding event loops is essential for building responsive applications.
The Windows Message Loop:
+------------------+
| Windows OS |
+------------------+
|
| WM_KEYDOWN, WM_PAINT, WM_TIMER, etc.
v
+------------------+
| Message Queue | <- Messages wait here
| [msg1, msg2, ...]|
+------------------+
|
v
+------------------+
| GetMessage() | <- AutoHotkey's event loop
| DispatchMessage()|
+------------------+
|
v
+------------------+
| Your Handler |
| OnTextChange() |
+------------------+
The Problem: Blocking the Main Thread
Timeline:
0ms User types 'p'
|
+-> OnTextChange fires
|
+-> Search 5000 items (500ms!)
|
+-> GUI frozen!
|
User types 'y' 't' 'h'
|
+-> These events queue up
(UI unresponsive)
|
500ms Search completes
|
+-> Process queued 'y' 't' 'h'
(feels laggy)
The Solution: Fast Operations + Debouncing
Timeline with debouncing:
0ms User types 'p'
+-> OnTextChange: Start 100ms timer
50ms User types 'y'
+-> OnTextChange: Cancel old timer, start new 100ms timer
100ms User types 't'
+-> OnTextChange: Cancel old timer, start new 100ms timer
200ms Timer fires! User stopped typing.
+-> Search for "pyt" (fast, <10ms because index is in RAM)
+-> Update GUI
Total: Smooth experience, only 1 search performed
Debouncing Implementation Pattern:
+--------+ +--------+ +--------+
| Type p | --> | Type y | --> | Type t | --> [100ms pause] --> Search!
+--------+ +--------+ +--------+
| | |
Start Reset Reset
Timer Timer Timer
; Pseudocode for debouncing
global searchTimer := 0
OnTextChange(ctrl) {
global searchTimer
; Cancel any pending search
if (searchTimer)
SetTimer(searchTimer, 0)
; Schedule new search in 100ms
searchTimer := SetTimer(DoSearch, -100)
}
DoSearch() {
; This only runs after 100ms of no typing
results := FuzzySearch(currentQuery, appIndex)
UpdateResultsList(results)
}
2.5 Windows Special Folders and Program Locations
Windows has a complex system for where programs are installed.
The Windows Program Landscape:
+-----------------------------------------------------------+
| Windows Filesystem |
+-----------------------------------------------------------+
| |
| C:\Program Files\ |
| +-- Microsoft Office\ |
| | +-- Office16\ |
| | +-- WINWORD.EXE <- 64-bit Microsoft Word |
| +-- Google\ |
| | +-- Chrome\ |
| | +-- Application\ |
| | +-- chrome.exe <- 64-bit Chrome |
| +-- Python311\ |
| +-- python.exe <- 64-bit Python |
| |
| C:\Program Files (x86)\ |
| +-- Steam\ |
| +-- steam.exe <- 32-bit Steam |
| |
| C:\Users\Douglas\ |
| +-- AppData\ |
| +-- Local\ |
| | +-- Programs\ |
| | +-- Microsoft VS Code\ |
| | +-- Code.exe <- Per-user install |
| +-- Roaming\ |
| +-- Microsoft\ |
| +-- Windows\ |
| +-- Start Menu\ |
| +-- Programs\ |
| +-- Discord.lnk <- Shortcut! |
| +-- VS Code.lnk |
| |
| C:\ProgramData\Microsoft\Windows\Start Menu\Programs\ |
| +-- Word.lnk <- System-wide shortcut |
| +-- Excel.lnk |
| |
+-----------------------------------------------------------+
Understanding .lnk Shortcuts:
A shortcut file doesnât contain the programâit points to it.
File: C:\Users\Douglas\Desktop\Chrome.lnk
|
+-- Target: C:\Program Files\Google\Chrome\Application\chrome.exe
+-- Arguments: --profile-directory="Default"
+-- Working Dir: C:\Program Files\Google\Chrome\Application
+-- Icon: C:\Program Files\Google\Chrome\Application\chrome.exe,0
Resolving Shortcuts with COM:
ResolveShortcut(lnkPath) {
; Create Windows Script Host Shell object
shell := ComObject("WScript.Shell")
; Create shortcut object
shortcut := shell.CreateShortcut(lnkPath)
; Extract properties
return {
path: shortcut.TargetPath, ; The actual .exe
args: shortcut.Arguments, ; Command-line arguments
workingDir: shortcut.WorkingDirectory,
icon: shortcut.IconLocation
}
}
Environment Variables for Special Folders:
| Variable | Typical Path | Use |
|---|---|---|
%APPDATA% |
C:\Users\Douglas\AppData\Roaming |
User settings |
%LOCALAPPDATA% |
C:\Users\Douglas\AppData\Local |
User-installed apps |
%PROGRAMFILES% |
C:\Program Files |
64-bit apps |
%PROGRAMFILES(X86)% |
C:\Program Files (x86) |
32-bit apps |
%PROGRAMDATA% |
C:\ProgramData |
System-wide app data |
%USERPROFILE% |
C:\Users\Douglas |
User home |
In AutoHotkey:
; Use A_AppData, A_ProgramFiles, etc.
startMenuPath := A_AppData . "\Microsoft\Windows\Start Menu\Programs"
3. Complete Project Specification
Functional Requirements
| ID | Requirement | Priority | Notes |
|---|---|---|---|
| F1 | Scan standard application directories for executables | Must Have | Program Files, Start Menu |
| F2 | Build an in-memory index on startup | Must Have | With loading indicator |
| F3 | Display search popup on configurable hotkey | Must Have | Default: Alt+Space |
| F4 | Filter index as user types with <50ms response | Must Have | Real-time filtering |
| F5 | Implement fuzzy matching for typo tolerance | Must Have | Subsequence + scoring |
| F6 | Launch selected application | Must Have | Handle .exe and .lnk |
| F7 | Support .exe executable files | Must Have | Direct launch |
| F8 | Support .lnk shortcut files | Must Have | Resolve target first |
| F9 | Rank results by match quality | Must Have | Exact > Prefix > Fuzzy |
| F10 | Display application icons | Should Have | Extract from .exe |
| F11 | Track frequently used applications | Should Have | Persist launch counts |
| F12 | Rank results by usage frequency | Should Have | Combine with match score |
| F13 | Re-scan index on demand | Should Have | Hotkey or menu option |
| F14 | Support launching with arguments | Nice to Have | For shortcuts with args |
| F15 | Custom search paths in config | Nice to Have | User-configurable |
| F16 | Keyboard navigation (Up/Down arrows) | Must Have | Navigate in search box |
| F17 | Close on Escape or focus loss | Must Have | Standard UX pattern |
Non-Functional Requirements
| ID | Requirement | Threshold |
|---|---|---|
| NF1 | Startup indexing time | < 5 seconds |
| NF2 | Search response time | < 50ms after typing stops |
| NF3 | GUI appearance time | < 100ms from hotkey press |
| NF4 | Memory usage | < 100MB RAM |
| NF5 | Compatibility | Windows 10/11 without admin rights |
| NF6 | Index size | Handle 10,000+ items |
| NF7 | Debounce delay | 80-150ms configurable |
Input/Output Specification
Inputs:
- Hotkey press (Alt+Space by default)
- Search text from user
- Arrow key navigation
- Enter key to launch
- Escape key to close
Outputs:
- GUI popup with search results
- Launched application
- Persisted usage statistics
4. Real World Outcome
When complete, youâll have a launcher you use dozens of times daily:
Example GUI Mockup (ASCII)
Initial State (No Query):
+--------------------------------------------------+
| [Search box with focus] |
| ________________________________________________ |
| | | |
| |________________________________________________| |
| |
| RECENT |
| +----------------------------------------------+ |
| | > [icon] Visual Studio Code (1m) | |
| | [icon] Google Chrome (5m) | |
| | [icon] Windows Terminal (12m) | |
| | [icon] Slack (1h) | |
| | [icon] Discord (2h) | |
| +----------------------------------------------+ |
| |
| Press Enter to launch | Esc to close |
+--------------------------------------------------+
Active Search:
+--------------------------------------------------+
| [pyt] |
| ________________________________________________ |
| | pyt | |
| |________________________________________________| |
| |
| RESULTS 3 matches |
| +----------------------------------------------+ |
| | > [py] Python 3.11 ***** | |
| | C:\Python311\python.exe | |
| | [py] PyCharm Professional **** | |
| | C:\Program Files\JetBrains\... | |
| | [py] pytest *** | |
| | C:\Users\...\venv\Scripts\pytest.exe | |
| +----------------------------------------------+ |
| |
| Arrow keys to navigate | Enter to launch |
+--------------------------------------------------+
Fuzzy Match Example:
+--------------------------------------------------+
| [vscd] |
| ________________________________________________ |
| | vscd | |
| |________________________________________________| |
| |
| RESULTS 1 match |
| +----------------------------------------------+ |
| | > [vs] Visual Studio Code **** | |
| | Matched: V-isual S-tudio C-o-D-e | |
| | C:\Users\...\Programs\VS Code\Code.exe | |
| +----------------------------------------------+ |
| |
| Fuzzy match: vscd in "Visual Studio Code" |
+--------------------------------------------------+
Example Search Interactions
Scenario 1: Quick Launch of Frequent App
User Action Launcher Response
----------- -----------------
Press Alt+Space GUI appears instantly (<100ms)
Type "c" Shows: Chrome, Calculator, CMD, Code...
(Chrome first because launched 47 times)
Press Enter Chrome launches, GUI closes
Total time: ~500ms
Scenario 2: Finding Unfamiliar App
User Action Launcher Response
----------- -----------------
Press Alt+Space GUI appears with recent apps
Type "disk" Shows: Disk Cleanup, Disk Management
Arrow Down Selects "Disk Management"
Press Enter Disk Management launches
Total time: ~2 seconds
Scenario 3: Fuzzy Match with Typo
User Action Launcher Response
----------- -----------------
Press Alt+Space GUI appears
Type "chrmoe" (typo!) Shows: Chrome (fuzzy matched)
Score breakdown: subsequence + low edit distance
Press Enter Chrome launches
Total time: ~800ms
Performance Expectations
| Operation | Target | Measurement Method |
|---|---|---|
| Index build (1000 items) | < 2 seconds | StartTime to LoadComplete |
| Index build (5000 items) | < 5 seconds | StartTime to LoadComplete |
| Search (empty to results) | < 30ms | KeyUp to ListUpdate |
| GUI show | < 100ms | HotkeyPress to WindowVisible |
| Launch application | < 200ms | EnterPress to ProcessStart |
Frequency Tracking Behavior
Launch History Tracking:
+--------------------------------------------------+
| App Name | Launch Count | Last Used |
+--------------------------------------------------+
| Chrome | 127 | 2 min ago |
| VS Code | 89 | 15 min ago |
| Terminal | 64 | 1 hour ago |
| Discord | 23 | 3 hours ago |
+--------------------------------------------------+
Ranking Formula:
FinalScore = MatchScore + (UsageCount * UsageWeight)
where UsageWeight = 5 (configurable)
Example for query "c":
Chrome: PrefixMatch(100) + (127 * 5) = 735
Code: PrefixMatch(100) + (89 * 5) = 545
CMD: PrefixMatch(100) + (12 * 5) = 160
Result order: Chrome, VS Code, CMD
5. Solution Architecture
Component Diagram (ASCII)
+========================================================================+
|| APPLICATION LAUNCHER ||
+========================================================================+
| |
| +------------------+ +----------------------+ |
| | CONFIG FILE | | USAGE DATA FILE | |
| | config.ini |<-------------------->| usage.json | |
| +------------------+ Load/Save +----------------------+ |
| | ^ |
| v | |
| +------------------+ | |
| | Settings | | |
| | - hotkey | | |
| | - maxResults | | |
| | - debounceMs | | |
| | - searchPaths[] | | |
| +------------------+ | |
| | | |
| v | |
| +------------------------------------------------------------------+ |
| | INDEXER | |
| | +------------------+ +------------------+ | |
| | | FilesystemScanner| | ShortcutResolver | | |
| | | - ScanDirectory()| | - ResolveLink() | | |
| | +------------------+ +------------------+ | |
| | | | | |
| | v v | |
| | +------------------------------------------------+ | |
| | | APP INDEX (Array) | | |
| | | [AppItem, AppItem, AppItem, ...] | | |
| | +------------------------------------------------+ | |
| +------------------------------------------------------------------+ |
| | |
| v |
| +------------------------------------------------------------------+ |
| | SEARCH ENGINE | |
| | +------------------+ +------------------+ | |
| | | FuzzyMatcher | | ResultRanker | | |
| | | - ExactMatch() | | - CombineScores()| | |
| | | - PrefixMatch() | | - SortResults() | | |
| | | - Subsequence() | | - ApplyUsage() | | |
| | | - EditDistance() | +------------------+ | |
| | +------------------+ | |
| +------------------------------------------------------------------+ |
| | |
| +---------------------------+---------------------------+ |
| v v |
| +------------------+ +------------------+
| | HOTKEY HANDLER | | GUI MANAGER |
| | - RegisterHotkey | | - CreateWindow |
| | - ToggleGUI() | | - ShowResults |
| +------------------+ | - HandleInput |
| | +------------------+
| v | |
| +------------------------------------------------------------------+ |
| | LAUNCHER | |
| | - Run(path, args) | |
| | - UpdateUsage(item) | |
| | - SaveUsageData() | |
| +------------------------------------------------------------------+ |
| |
+=========================================================================+
Index Data Structure Design
; Individual application item
AppItem := {
; Core identification
name: "Google Chrome", ; Display name
path: "C:\Program Files\Google\...\chrome.exe", ; Launch path
; Launch configuration
args: "--new-window", ; Optional arguments
workingDir: "C:\Program Files\...", ; Working directory
; Search optimization
lowerName: "google chrome", ; Pre-computed lowercase
keywords: ["chrome", "browser", "web", "google"], ; Additional search terms
; Metadata
isShortcut: false, ; Was this from a .lnk file?
originalPath: "", ; If shortcut, the .lnk path
; Usage tracking
usageCount: 47, ; Times launched
lastUsed: "20251227120000", ; A_Now format timestamp
; Display
iconPath: "C:\Program Files\...\chrome.exe", ; For icon extraction
iconIndex: 0 ; Icon index in the file
}
; Full index structure
AppIndex := [AppItem1, AppItem2, AppItem3, ...]
; Usage data (persisted separately)
UsageData := Map(
"C:\...\chrome.exe" => {count: 47, lastUsed: "20251227120000"},
"C:\...\code.exe" => {count: 89, lastUsed: "20251227110000"},
...
)
Memory Layout Visualization:
AppIndex Array in RAM:
+------------------------------------------------------------------+
| Index 0 | Index 1 | Index 2 | ... | Index 4999 |
+------------------------------------------------------------------+
| AppItem | AppItem | AppItem | ... | AppItem |
| (Chrome) | (Firefox) | (VS Code) | ... | (Zoom) |
+------------------------------------------------------------------+
|
v
+---------------------------+
| name: "Google Chrome" |
| path: "C:\...\chrome.exe" |
| lowerName: "google chrome"|
| keywords: [...] |
| usageCount: 47 |
| ... |
+---------------------------+
Estimated memory per item: ~500 bytes
5000 items = ~2.5 MB RAM
Search Algorithm Design
+--------------------------------------------------------------------+
| SEARCH ALGORITHM PIPELINE |
+--------------------------------------------------------------------+
| |
| INPUT: query = "chro" |
| |
| Step 1: Normalize Query |
| +-------------------+ |
| | query.lower() | "chro" -> "chro" |
| | query.trim() | |
| +-------------------+ |
| | |
| v |
| Step 2: Quick Filter (Prefix Check) |
| +-----------------------------------------------------------+ |
| | For each item in AppIndex: | |
| | if item.lowerName.startsWith(query) OR | |
| | any(keyword.startsWith(query) for keyword in item): | |
| | add to candidates with HIGH priority | |
| +-----------------------------------------------------------+ |
| | |
| v |
| Step 3: Substring Check |
| +-----------------------------------------------------------+ |
| | For remaining items: | |
| | if item.lowerName.contains(query): | |
| | add to candidates with MEDIUM priority | |
| +-----------------------------------------------------------+ |
| | |
| v |
| Step 4: Fuzzy Match (if few results) |
| +-----------------------------------------------------------+ |
| | For remaining items: | |
| | if subsequenceMatch(query, item.lowerName): | |
| | calculate score with bonuses | |
| | add to candidates with score | |
| +-----------------------------------------------------------+ |
| | |
| v |
| Step 5: Calculate Final Scores |
| +-----------------------------------------------------------+ |
| | For each candidate: | |
| | matchScore = calculateMatchScore(query, item) | |
| | usageScore = item.usageCount * USAGE_WEIGHT | |
| | recencyBonus = calculateRecency(item.lastUsed) | |
| | finalScore = matchScore + usageScore + recencyBonus | |
| +-----------------------------------------------------------+ |
| | |
| v |
| Step 6: Sort and Limit |
| +-----------------------------------------------------------+ |
| | candidates.sortByScore(descending) | |
| | return candidates.take(MAX_RESULTS) | |
| +-----------------------------------------------------------+ |
| | |
| v |
| OUTPUT: [Chrome, ChromeDriver, ...] |
| |
+--------------------------------------------------------------------+
6. Phased Implementation Guide
Phase 1: Static Index with Hardcoded Programs (2-3 hours)
Goal: Prove you can search a list and launch programs without filesystem complexity.
What Youâll Build:
- Hardcoded array of 10-15 common programs
- Basic search box GUI
- Simple string matching
- Launch on Enter
Steps:
- Create the script skeleton
#Requires AutoHotkey v2.0 ; Global variables AppIndex := [] ; Entry point InitializeStaticIndex() Hotkey("!Space", ShowLauncher) Persistent - Build hardcoded index
InitializeStaticIndex() { global AppIndex ; Add common programs manually AppIndex.Push({ name: "Notepad", path: "notepad.exe", lowerName: "notepad" }) AppIndex.Push({ name: "Calculator", path: "calc.exe", lowerName: "calculator" }) ; Add 8-10 more programs } - Create basic GUI with search and results
- Implement simple contains-match search
- Handle Enter key to launch selected item
Verification Checklist:
- Alt+Space opens the launcher
- Typing ânoteâ shows Notepad
- Pressing Enter launches the application
- Pressing Escape closes the launcher
What Youâre Learning: AHK v2 syntax, GUI creation, event handling, basic data structures.
Phase 2: Directory Scanning (3-4 hours)
Goal: Replace hardcoded list with dynamically scanned programs.
What Youâll Build:
- Filesystem scanner function
- Support for .exe files
- Loading indicator during scan
- Error handling for inaccessible folders
Steps:
- Create the indexer module
BuildIndex() { global AppIndex AppIndex := [] ; Show loading indicator ShowLoadingIndicator() ; Scan key directories ScanDirectory(A_ProgramFiles) ScanDirectory(A_ProgramFiles . " (x86)") HideLoadingIndicator() } - Implement recursive scanning
ScanDirectory(path) { global AppIndex if (!DirExist(path)) return Loop Files, path "\*.exe", "R" { AppIndex.Push({ name: StrReplace(A_LoopFileName, ".exe", ""), path: A_LoopFileFullPath, lowerName: StrLower(StrReplace(A_LoopFileName, ".exe", "")) }) } } - Add exclusion patterns (skip updaters, uninstallers)
- Call BuildIndex() at startup
- Display item count after indexing
Verification Checklist:
- Indexing completes in < 5 seconds
- Index contains 100+ items
- âIndexingâŚâ message appears during scan
- Searching still works with dynamic index
What Youâre Learning: Filesystem operations, Loop Files syntax, error handling, performance awareness.
Phase 3: Fuzzy Search Implementation (3-4 hours)
Goal: Implement intelligent string matching that handles typos and abbreviations.
What Youâll Build:
- Scoring system for match quality
- Subsequence matching algorithm
- Multi-tier matching (exact > prefix > contains > fuzzy)
Steps:
- Create scoring function
CalculateScore(query, target) { ; Exact match if (query == target) return 1000 ; Prefix match if (SubStr(target, 1, StrLen(query)) == query) return 500 + (StrLen(query) / StrLen(target) * 100) ; Contains match if (InStr(target, query)) return 200 + (StrLen(query) / StrLen(target) * 100) ; Fuzzy match return FuzzyScore(query, target) } - Implement subsequence matching
FuzzyScore(query, target) { queryPos := 1 consecutiveBonus := 0 lastMatchPos := 0 Loop Parse, target { if (queryPos > StrLen(query)) break if (A_LoopField == SubStr(query, queryPos, 1)) { if (A_Index == lastMatchPos + 1) consecutiveBonus += 10 lastMatchPos := A_Index queryPos++ } } if (queryPos > StrLen(query)) return 50 + consecutiveBonus return 0 } - Update search to use scoring
- Sort results by score
- Test with various query patterns
Verification Checklist:
- âchromeâ exactly matches âChromeâ (score ~1000)
- âchrâ matches âChromeâ as prefix (score ~550)
- âromâ matches âChromeâ as substring (score ~250)
- âvscdâ matches âVisual Studio Codeâ (fuzzy)
- Results are sorted by score
What Youâre Learning: String algorithms, scoring systems, algorithm design.
Phase 4: GUI Integration (3-4 hours)
Goal: Create a polished, responsive user interface.
What Youâll Build:
- Professional-looking search window
- Real-time filtering with debouncing
- Keyboard navigation
- Visual feedback for selection
Steps:
- Design the GUI layout
CreateLauncherGUI() { global MainGui, SearchBox, ResultsList MainGui := Gui("+AlwaysOnTop -Caption +Border", "Launcher") MainGui.BackColor := "1a1a2e" MainGui.SetFont("s12 cWhite", "Segoe UI") SearchBox := MainGui.Add("Edit", "w400 h35 Background0f0f23") SearchBox.OnEvent("Change", OnSearchChange) ResultsList := MainGui.Add("ListView", "w400 h350 Background0f0f23 -Hdr", ["Icon", "Name", "Path"]) } - Implement debouncing
global DebounceTimer := 0 OnSearchChange(ctrl, *) { global DebounceTimer if (DebounceTimer) SetTimer(DebounceTimer, 0) DebounceTimer := SetTimer(DoSearch, -100) } - Add keyboard navigation in search box
- Handle focus loss and escape key
- Add visual polish (colors, fonts)
Verification Checklist:
- GUI appears centered on screen
- Typing updates results in real-time
- Up/Down arrows navigate results from search box
- Enter launches selected item
- Escape closes the window
- Clicking elsewhere closes the window
What Youâre Learning: GUI design, UX patterns, event coordination, visual design.
Phase 5: Shortcut Support and Usage Tracking (4-5 hours)
Goal: Index Start Menu shortcuts and learn user preferences.
What Youâll Build:
- .lnk file resolution using COM
- Usage count persistence
- Ranking that combines match quality with frequency
- Periodic auto-save of usage data
Steps:
- Add shortcut scanning
ScanShortcuts(path) { global AppIndex Loop Files, path "\*.lnk", "R" { try { info := ResolveShortcut(A_LoopFileFullPath) if (info.path != "" && FileExist(info.path)) { AppIndex.Push({ name: StrReplace(A_LoopFileName, ".lnk", ""), path: info.path, args: info.args, isShortcut: true, lowerName: StrLower(StrReplace(A_LoopFileName, ".lnk", "")) }) } } } } - Implement shortcut resolution
ResolveShortcut(lnkPath) { shell := ComObject("WScript.Shell") shortcut := shell.CreateShortcut(lnkPath) return { path: shortcut.TargetPath, args: shortcut.Arguments } } - Add usage tracking
TrackUsage(item) { item.usageCount++ item.lastUsed := A_Now ScheduleUsageSave() } - Persist usage data to JSON or INI
- Load usage data on startup
- Incorporate usage into scoring
Verification Checklist:
- Start Menu shortcuts appear in search
- Launching an app increments its usage count
- Usage data survives script restart
- Frequently used apps rank higher for same query
What Youâre Learning: COM automation, data persistence, machine learning basics (frequency learning).
Phase 6: Polish and Production Readiness (3-4 hours)
Goal: Create a tool youâll use every day.
What Youâll Build:
- Application icons in results
- Re-index functionality
- Configuration file support
- Error handling and edge cases
- System tray integration
Steps:
- Add application icons
; Icons require ImageList and ListView integration ImageListID := IL_Create(100) LV_SetImageList(ImageListID) ; Extract icon from exe iconIndex := IL_Add(ImageListID, item.path, 1) - Create configuration system
LoadConfig() { global Settings configPath := A_AppData "\Launcher\config.ini" Settings := { hotkey: IniRead(configPath, "General", "Hotkey", "!Space"), maxResults: IniRead(configPath, "General", "MaxResults", 10), debounceMs: IniRead(configPath, "General", "DebounceMs", 100) } } - Add re-index hotkey (Ctrl+Shift+R while open)
- Handle edge cases:
- Missing/moved executables
- Very long paths
- Permission errors
- Network paths (skip or timeout)
- Add system tray icon with menu
- Create startup shortcut option
Verification Checklist:
- Icons appear next to application names
- Re-indexing works without restart
- Configuration file is created and respected
- System tray icon with âExitâ and âRe-indexâ options
- Graceful handling of missing files
What Youâre Learning: Production software considerations, configuration management, user experience polish.
7. Testing Strategy
Unit Tests
| Test ID | Function | Input | Expected Output |
|---|---|---|---|
| U1 | CalculateScore | (âchromeâ, âchromeâ) | 1000 (exact) |
| U2 | CalculateScore | (âchrâ, âchromeâ) | ~550 (prefix) |
| U3 | CalculateScore | (âromâ, âchromeâ) | ~250 (contains) |
| U4 | CalculateScore | (âcrmâ, âchromeâ) | ~70 (fuzzy) |
| U5 | CalculateScore | (âxyzâ, âchromeâ) | 0 (no match) |
| U6 | FuzzyScore | (âvscdâ, âvisual studio codeâ) | > 0 |
| U7 | FuzzyScore | (âabcâ, âxyzâ) | 0 |
| U8 | ResolveShortcut | (valid .lnk) | {path: ââŚâ, args: ââŚâ} |
| U9 | ResolveShortcut | (invalid .lnk) | Exception/empty |
Test Implementation Pattern:
RunTests() {
passed := 0
failed := 0
; Test U1: Exact match
result := CalculateScore("chrome", "chrome")
if (result == 1000) {
passed++
} else {
failed++
OutputDebug("U1 FAILED: Expected 1000, got " . result)
}
; Continue for other tests...
MsgBox("Tests: " . passed . " passed, " . failed . " failed")
}
Integration Tests
| Test ID | Scenario | Steps | Expected |
|---|---|---|---|
| I1 | Index builds | Start script | Index > 100 items in < 5s |
| I2 | Search works | Type ânotepadâ | Notepad in results |
| I3 | Fuzzy works | Type ântpdâ | Notepad in results |
| I4 | Launch works | Select, Enter | App launches |
| I5 | Usage tracks | Launch 3x | usageCount = 3 |
| I6 | Persistence | Restart script | Usage data preserved |
| I7 | Shortcuts | Type âWordâ | MS Word (from Start Menu) |
| I8 | Icons | View results | Icons display correctly |
Performance Tests
| Test ID | Metric | Threshold | How to Measure |
|---|---|---|---|
| P1 | Index build (1000 items) | < 2s | A_TickCount before/after |
| P2 | Index build (5000 items) | < 5s | A_TickCount before/after |
| P3 | Search response | < 50ms | Timestamp in OnSearchChange vs results displayed |
| P4 | GUI show | < 100ms | Timestamp in hotkey handler |
| P5 | Memory usage | < 100MB | Task Manager |
Edge Case Tests
| Test ID | Scenario | Steps | Expected |
|---|---|---|---|
| E1 | Empty query | Clear search box | Show recent/all |
| E2 | Very long query | Type 100+ chars | No crash, truncate |
| E3 | Special characters | Type âc:\â | Handled gracefully |
| E4 | Missing file | Indexed file deleted | Skip or show error |
| E5 | No matches | Type âxyzxyzâ | Empty results, no crash |
| E6 | Network path | Scan network folder | Timeout, donât hang |
| E7 | Unicode | Search for emoji app | Works correctly |
8. Common Pitfalls and Debugging Tips
Pitfall 1: Index Takes Too Long
Symptoms:
- Script hangs on startup
- âNot Respondingâ in title bar
- User gives up waiting
Causes:
- Scanning too many directories (entire C: drive)
- Including network paths
- Antivirus scanning each file access
- Very deep folder structures
Solutions:
; 1. Limit scan depth
Loop Files, path "\*.exe", "R" {
; Skip if too deep
if (StrLen(A_LoopFileDir) - StrLen(path) > 100)
continue
; ...
}
; 2. Skip known slow paths
if (InStr(A_LoopFileDir, "node_modules") ||
InStr(A_LoopFileDir, ".git") ||
InStr(A_LoopFileDir, "temp"))
continue
; 3. Add timeout for network paths
if (SubStr(path, 1, 2) == "\\") ; UNC path
return ; Skip entirely
; 4. Show progress
SplashGui.Add("Text", , "Scanning: " . path)
Pitfall 2: Search Feels Laggy
Symptoms:
- Noticeable delay after typing
- UI stutters during search
- Characters appear delayed
Causes:
- Searching on every keystroke without debouncing
- Expensive operations in search loop
- Too many items being processed
- Updating GUI too frequently
Solutions:
; 1. Implement debouncing (essential!)
global DebounceTimer := 0
OnSearchChange(ctrl, *) {
global DebounceTimer
if (DebounceTimer)
SetTimer(DebounceTimer, 0)
DebounceTimer := SetTimer(DoSearch, -100) ; Wait 100ms
}
; 2. Early termination
Search(query) {
results := []
for item in AppIndex {
if (results.Length >= 50) ; Got enough candidates
break
; ...
}
}
; 3. Pre-compute lowercase names at index time, not search time
; BAD:
if (InStr(StrLower(item.name), query)) ; Called every search!
; GOOD:
if (InStr(item.lowerName, query)) ; Pre-computed once
Pitfall 3: Apps Donât Launch
Symptoms:
- Enter is pressed but nothing happens
- Error message about file not found
- Wrong application opens
Causes:
- Path has spaces but not quoted
- Shortcut target no longer exists
- Requires administrator privileges
- Path stored incorrectly
Solutions:
; 1. Always handle paths with spaces
LaunchApp(item) {
path := item.path
; Quote if contains spaces
if (InStr(path, " "))
path := '"' . path . '"'
; Add arguments if present
if (item.HasProp("args") && item.args != "")
path .= " " . item.args
try {
Run(path)
} catch Error as e {
MsgBox("Failed to launch: " . e.Message . "`nPath: " . item.path)
}
}
; 2. Verify file exists before adding to index
if (!FileExist(info.path)) {
; Skip this shortcut - target missing
continue
}
; 3. Handle admin requirements
try {
Run(path)
} catch Error as e {
if (InStr(e.Message, "requires elevation")) {
; Offer to run as admin
result := MsgBox("Run as administrator?", , "YesNo")
if (result == "Yes")
Run("*RunAs " . path)
}
}
Pitfall 4: Shortcuts Not Resolving
Symptoms:
- Start Menu shortcuts missing from index
- Error when processing .lnk files
- Empty path returned
Causes:
- COM object not created properly
- Shortcut file is corrupted
- Target is a UWP/Store app (different format)
- Insufficient permissions
Solutions:
ResolveShortcut(lnkPath) {
try {
shell := ComObject("WScript.Shell")
shortcut := shell.CreateShortcut(lnkPath)
target := shortcut.TargetPath
; Handle empty target (might be UWP app)
if (target == "") {
; UWP apps have special handling
return {path: "", args: "", isUWP: true}
}
; Verify target exists
if (!FileExist(target)) {
return {path: "", args: ""}
}
return {
path: target,
args: shortcut.Arguments
}
} catch Error as e {
; Log but don't crash
OutputDebug("Failed to resolve: " . lnkPath . " - " . e.Message)
return {path: "", args: ""}
}
}
Pitfall 5: Usage Data Not Persisting
Symptoms:
- Frequently used apps donât rise in ranking
- Usage resets on script restart
- Data file is empty or corrupted
Causes:
- File path doesnât exist
- Writing before directory is created
- File locked by another process
- JSON/INI format errors
Solutions:
SaveUsageData() {
global AppIndex
; 1. Ensure directory exists
dataDir := A_AppData . "\Launcher"
if (!DirExist(dataDir))
DirCreate(dataDir)
dataFile := dataDir . "\usage.json"
; 2. Build data structure
data := Map()
for item in AppIndex {
if (item.usageCount > 0) {
data[item.path] := {
count: item.usageCount,
lastUsed: item.lastUsed
}
}
}
; 3. Write with error handling
try {
; Delete old file first
if (FileExist(dataFile))
FileDelete(dataFile)
; Write new data
jsonStr := JSON.Stringify(data) ; Assuming JSON library
FileAppend(jsonStr, dataFile, "UTF-8")
} catch Error as e {
OutputDebug("Failed to save usage data: " . e.Message)
}
}
Debugging Techniques
1. Use OutputDebug for tracing:
OutputDebug("Index size: " . AppIndex.Length)
OutputDebug("Search query: [" . query . "]")
OutputDebug("Match score for " . item.name . ": " . score)
View in DebugView or VS Code with AHK extension.
2. Add timing measurements:
startTime := A_TickCount
; ... operation ...
elapsed := A_TickCount - startTime
OutputDebug("Operation took " . elapsed . "ms")
3. Create a debug mode:
global DebugMode := true
DebugLog(msg) {
global DebugMode
if (DebugMode)
OutputDebug("[Launcher] " . msg)
}
4. Add a debug panel:
; Show debug info in GUI when Ctrl is held
if (GetKeyState("Control")) {
ResultsList.Add(["DEBUG", "Index: " . AppIndex.Length . " items"])
ResultsList.Add(["DEBUG", "Query: " . query])
ResultsList.Add(["DEBUG", "Results: " . results.Length])
}
9. Extensions and Challenges
Easy Extensions (1-2 hours each)
- Configurable Hotkey
- Read hotkey from config file
- Allow runtime changing via tray menu
- Theme Support
- Dark mode (default)
- Light mode option
- Custom color schemes
- Clear Recent
- Button or hotkey to reset usage data
- Useful when preferences change
- Search Highlighting
- Highlight matched characters in results
- Show which part of name matched
Medium Extensions (3-5 hours each)
- Application Icons
- Extract icons from .exe files
- Display in ListView with ImageList
- Cache icons for performance
- File/Folder Search
- Search recent documents
- Index common folders (Desktop, Documents)
- Open folder in Explorer option
- Web Search Fallback
- If no local matches, offer Google/Bing search
- Detect URLs and open directly
- Calculator Mode
- Detect math expressions (starts with â=â)
- Evaluate and show result
- Copy result to clipboard
- Multiple Selection
- Shift+Enter to launch multiple apps
- Ctrl+Click to toggle selection
Advanced Extensions (1-2 days each)
- Plugin System
- Define plugin interface
- Load external .ahk files as plugins
- Plugins provide custom result types
- Clipboard Integration
- Search clipboard history
- Paste to active window option
- Connect with P01 (Clipboard Manager)
- Window Switcher
- Search open windows by title
- Switch to or close windows
- Preview window thumbnails
- Custom Commands
- Define shortcuts like âemail -> Open Gmailâ
- Support aliases and macros
- Parameterized commands
- Learning Algorithm
- Track search-to-launch patterns
- Predict most likely app for partial queries
- Time-of-day weighting
- Sync Across Machines
- Export/import configuration
- Cloud sync for usage data
- Shared custom commands
10. Books That Will Help
| Topic | Book | Chapter/Section | Why It Helps |
|---|---|---|---|
| String Searching Algorithms | Algorithms, Fourth Edition by Robert Sedgewick | Chapter 5.3: Substring Search | Understand KMP, Boyer-Moore, and Rabin-Karp algorithms for efficient string matching |
| Edit Distance | Introduction to Algorithms (CLRS) | Chapter 15: Dynamic Programming (specifically 15.4) | Deep dive into Levenshtein distance and optimal substructure |
| Data Structures for Search | Algorithms, Fourth Edition by Robert Sedgewick | Chapter 5.2: Tries | Learn trie/prefix tree implementation for O(m) prefix lookup |
| Caching Strategies | Designing Data-Intensive Applications by Martin Kleppmann | Chapter 11: Stream Processing (caching section) | Understand write-through, write-back, and cache invalidation |
| Hash Tables | Designing Data-Intensive Applications by Martin Kleppmann | Chapter 3: Storage and Retrieval | Hash indexes, LSM trees, and efficient lookup structures |
| Event Loops | Game Programming Patterns by Robert Nystrom | Game Loop chapter | Understand main loop architecture, fixed vs variable timestep |
| Observer Pattern | Game Programming Patterns by Robert Nystrom | Observer chapter | Decouple event sources from handlers (clipboard changes, hotkeys) |
| GUI Responsiveness | The Art of UNIX Programming by Eric S. Raymond | Chapter 11: Interfaces | Philosophy of interface design and user responsiveness |
| Usability | Donât Make Me Think by Steve Krug | Chapters 1-4 | Launcher UX design principles, obvious vs clever |
| Search UX | Designing the Search Experience by Tony Russell-Rose | Chapters 3-5 | How users search, result ranking, and display patterns |
| Windows Internals | Windows Internals, Part 1 by Yosifovich et al. | Chapter 3: System Mechanisms | Understand Windows message loop, window procedures |
| Windows Internals | Windows Security Internals by James Forshaw | Chapter 2: User Mode | Filesystem, registry, and special folders explained |
Reading Priority:
- Algorithms, Fourth Edition - Core algorithms knowledge (Chapters 5.2-5.3)
- Game Programming Patterns - Event-driven architecture concepts
- Donât Make Me Think - UX principles for launcher design
- Designing Data-Intensive Applications - Caching and performance
11. Self-Assessment Checklist
Before considering this project complete, verify each item:
Core Functionality
- Index builds automatically on startup
- Scans Program Files, Program Files (x86), Start Menu
- Completes in < 5 seconds
- Shows loading indicator during scan
- At least 200 applications are indexed
- Includes .exe files from Program Files
- Includes .lnk files from Start Menu
- No duplicates (same path)
- Search results appear in < 100ms after typing stops
- Debouncing prevents excessive searches
- Pre-computed lowercase names
- Efficient scoring algorithm
- Fuzzy matching catches common patterns
- âvscdâ finds âVisual Studio Codeâ
- âffâ finds âFirefoxâ
- âchrmeâ finds âChromeâ (typo tolerance)
User Experience
- GUI is polished and professional
- Clean visual design
- Readable fonts and colors
- Proper spacing and alignment
- Keyboard navigation works completely
- Up/Down arrows in search box move selection
- Enter launches selected item
- Escape closes window
- Tab navigates between controls
- Focus behavior is correct
- Search box focused when window opens
- Clicking elsewhere closes window
- Focus returns to previous window after close
Data Management
- Selected application launches correctly
- Handles paths with spaces
- Respects arguments from shortcuts
- Shows error for missing files
- Shortcut (.lnk) files resolve properly
- Target path extracted via COM
- Arguments preserved
- Invalid shortcuts skipped gracefully
- Usage tracking affects result ranking
- Launch count increments on each launch
- More frequent apps rank higher
- Ranking combines match score and usage
- Usage data persists across restarts
- Data saved to file
- Loaded on startup
- No corruption on crash
Reliability
- Handles missing/moved applications gracefully
- No crash if indexed file is deleted
- Optionally shows âNot Foundâ message
- Can re-index to refresh
- Works without administrator privileges
- Uses user-writable paths for data
- Scans only accessible folders
- Gracefully skips restricted areas
- No memory leaks during extended use
- Memory stays stable after hours of use
- GUI properly destroyed on close
- Event handlers cleaned up
Performance
- Index build time < 5 seconds
- Search response < 50ms
- GUI show time < 100ms
- Memory usage < 100MB
Code Quality
- Code is organized into logical functions
- Variables have meaningful names
- Complex logic is commented
- Error handling is comprehensive
- No hardcoded paths (uses environment variables)
Summary
This project teaches you to build a productivity tool that combines:
- Systems Programming - Filesystem traversal, Windows APIs, COM automation
- Algorithm Design - Fuzzy matching, scoring systems, optimization
- User Interface Design - Responsive GUIs, keyboard navigation, visual feedback
- Data Persistence - Saving/loading state, learning from user behavior
- Performance Engineering - Caching, debouncing, profiling
When complete, youâll have a launcher that rivals commercial tools like Alfred or Raycast, demonstrating real software engineering skills applicable to any platform or language.
Total estimated time: 20-30 hours across 2-3 weeks
Difficulty progression:
- Phases 1-2: Fundamentals (approachable)
- Phases 3-4: Core features (moderate challenge)
- Phases 5-6: Polish and production (advanced topics)
Good luck, and enjoy building something youâll use every day!