Skip to content

Commit f9beada

Browse files
committed
docs(blog): add building winget search post
✨ Add a new blog post describing the Winget Search web interface and how it was built to simplify finding winget package IDs 📁 Added: _posts/2025-09-15-building-winget-search-a-fast-web-interface-for-winget-packages.md 💡 Covers Python manifest extraction, client-side JS search, and GitHub Actions automation for daily updates 🎯 Motivation: speed up package discovery and provide copy-ready winget install commands for faster machine setup
1 parent 73ce095 commit f9beada

File tree

1 file changed

+236
-0
lines changed

1 file changed

+236
-0
lines changed
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
---
2+
published: true
3+
layout: post
4+
title: Building Winget Search - A fast web interface for Windows Package Manager
5+
description: >-
6+
How I built a GitHub Pages-hosted search interface for winget packages to solve my machine setup workflow
7+
tags:
8+
- python
9+
- javascript
10+
- winget
11+
- github-actions
12+
- github-pages
13+
---
14+
15+
## Background
16+
17+
When setting up a new Windows machine, I used to rely on [Scoop](https://scoop.sh/) and [Chocolatey](https://chocolatey.org/) for package management. Both are excellent tools, but when Microsoft introduced [Windows Package Manager (winget)](https://learn.microsoft.com/en-us/windows/package-manager/), I decided to give it a try on my latest machine setup.
18+
19+
The problem? Finding winget package IDs was tedious. While `winget search` works, I wanted something faster - a web interface where I could quickly search, find packages, and copy installation commands. That's how [winget-search](https://github.com/solrevdev/winget-search) was born.
20+
21+
![Winget Search Interface](https://i.imgur.com/YLysmmV.png)
22+
23+
## The Challenge
24+
25+
Winget packages are stored in Microsoft's [winget-pkgs repository](https://github.com/microsoft/winget-pkgs) with over 30,000 YAML manifest files. The repository structure follows a pattern: `manifests/publisher/package_name/version/` with separate files for different locales and installers.
26+
27+
I needed to:
28+
- Extract package metadata from thousands of YAML files
29+
- Handle multiple versions and keep only the latest
30+
- Filter for English descriptions only
31+
- Build a fast, searchable web interface
32+
- Keep data fresh with automated updates
33+
34+
## The Solution
35+
36+
### Architecture Overview
37+
38+
I built a three-part system:
39+
40+
1. **Python extraction script** - Parses YAML manifests and generates JSON
41+
2. **Static HTML search interface** - Client-side search with instant results
42+
3. **GitHub Actions automation** - Daily updates and deployment
43+
44+
### Data Extraction
45+
46+
The Python script (`extract_packages.py`) does the heavy lifting:
47+
48+
```python
49+
def extract_package_info(manifest_dir):
50+
"""Extract comprehensive package info from a manifest directory"""
51+
package_info = {
52+
"id": None,
53+
"name": None,
54+
"description": None,
55+
"publisher": None,
56+
"version": None,
57+
"tags": [],
58+
"homepage": None,
59+
"license": None
60+
}
61+
62+
# Process version manifest first
63+
version_file = next((f for f in yaml_files if not '.locale.' in f), None)
64+
if version_file:
65+
with open(os.path.join(manifest_dir, version_file), encoding="utf-8") as f:
66+
doc = yaml.safe_load(f)
67+
package_info["id"] = doc.get("PackageIdentifier")
68+
package_info["version"] = doc.get("PackageVersion")
69+
70+
# Look for English locale file
71+
locale_file = next((f for f in yaml_files if '.locale.en-US.' in f), None)
72+
if locale_file:
73+
with open(os.path.join(manifest_dir, locale_file), encoding="utf-8") as f:
74+
doc = yaml.safe_load(f)
75+
package_info["name"] = doc.get("PackageName")
76+
package_info["publisher"] = doc.get("Publisher")
77+
package_info["description"] = doc.get("Description")
78+
```
79+
80+
The script handles version comparison using Python's `packaging` library to ensure we only keep the latest version of each package:
81+
82+
```python
83+
def parse_version(ver_str):
84+
"""Parse version string for proper comparison"""
85+
try:
86+
return version.parse(ver_str)
87+
except:
88+
return version.parse("0.0.0") # Fallback for non-standard versions
89+
```
90+
91+
### Web Interface
92+
93+
The search interface is pure vanilla JavaScript - no frameworks needed. It loads a ~5MB JSON file containing all package data and performs client-side search for instant results:
94+
95+
```javascript
96+
function showResults(query) {
97+
const q = query.trim().toLowerCase();
98+
99+
let results = packages.filter(pkg => {
100+
const idMatch = pkg.id?.toLowerCase().includes(q);
101+
const nameMatch = pkg.name?.toLowerCase().includes(q);
102+
const descMatch = pkg.description?.toLowerCase().includes(q);
103+
const publisherMatch = pkg.publisher?.toLowerCase().includes(q);
104+
const tagMatch = pkg.tags?.some(tag =>
105+
tag && typeof tag === 'string' && tag.toLowerCase().includes(q)
106+
);
107+
108+
return idMatch || nameMatch || descMatch || publisherMatch || tagMatch;
109+
}).slice(0, 100);
110+
111+
// Render results...
112+
}
113+
```
114+
115+
Each search result includes a one-click copy button for the winget install command:
116+
117+
```javascript
118+
function copyCommand(button, cmd) {
119+
navigator.clipboard.writeText(cmd).then(() => {
120+
button.innerHTML = 'Copied!';
121+
setTimeout(() => button.innerHTML = 'Copy', 2000);
122+
});
123+
}
124+
```
125+
126+
### Automation with GitHub Actions
127+
128+
The magic happens in the GitHub Actions workflow that runs daily at 2 AM UTC:
129+
130+
```yaml
131+
name: Build and Deploy
132+
on:
133+
schedule:
134+
- cron: '0 2 * * *' # Daily at 2 AM UTC
135+
workflow_dispatch:
136+
137+
jobs:
138+
build-and-deploy:
139+
runs-on: ubuntu-latest
140+
steps:
141+
- name: Checkout
142+
uses: actions/checkout@v4
143+
144+
- name: Setup Python
145+
uses: actions/setup-python@v5
146+
with:
147+
python-version: '3.11'
148+
149+
- name: Cache winget-pkgs
150+
uses: actions/cache@v4
151+
with:
152+
path: winget-pkgs
153+
key: winget-pkgs-${{ github.run_id }}
154+
restore-keys: winget-pkgs-
155+
156+
- name: Clone winget-pkgs repository
157+
run: |
158+
if [ ! -d "winget-pkgs" ]; then
159+
git clone --depth 1 https://github.com/microsoft/winget-pkgs.git
160+
else
161+
cd winget-pkgs && git pull origin main
162+
fi
163+
164+
- name: Extract packages
165+
run: python extract_packages.py winget-pkgs/manifests/ packages.json
166+
167+
- name: Deploy to GitHub Pages
168+
uses: peaceiris/actions-gh-pages@v4
169+
with:
170+
github_token: ${{ secrets.GITHUB_TOKEN }}
171+
publish_dir: .
172+
```
173+
174+
## Technical Highlights
175+
176+
### Performance Optimizations
177+
178+
- **Client-side search**: No server required, instant results
179+
- **Debounced input**: 300ms delay prevents excessive filtering
180+
- **Result limiting**: Shows max 100 results for smooth scrolling
181+
- **Caching**: GitHub Actions caches the large winget-pkgs repository
182+
183+
### User Experience Features
184+
185+
- **Keyboard shortcuts**: Press `/` to focus search, `Esc` to clear
186+
- **Dark mode**: Automatic theme based on system preferences
187+
- **Mobile-friendly**: Responsive design works on all devices
188+
- **Copy feedback**: Visual confirmation when commands are copied
189+
190+
### Data Quality
191+
192+
- **English-only**: Filters for `.locale.en-US.yaml` files
193+
- **Latest versions**: Semantic version comparison ensures freshness
194+
- **Type safety**: Handles edge cases like non-string tags
195+
- **Error resilience**: Continues processing even if individual manifests fail
196+
197+
## Deployment
198+
199+
The entire deployment is automated through GitHub Pages. The workflow:
200+
201+
1. Clones the microsoft/winget-pkgs repository (1GB+, hence the caching)
202+
2. Extracts package data using Python script
203+
3. Generates a `packages.json` file with ~30,000 packages
204+
4. Deploys everything to GitHub Pages using `peaceiris/actions-gh-pages`
205+
206+
No manual intervention needed - just push code and GitHub Actions handles the rest!
207+
208+
## Results
209+
210+
The end result is a fast, searchable interface hosted at GitHub Pages that:
211+
212+
- Loads 30,000+ packages in under 3 seconds
213+
- Provides instant search results
214+
- Generates copy-ready `winget install` commands
215+
- Updates automatically every day
216+
- Costs nothing to host
217+
218+
Perfect for when you need to quickly find that package ID for your setup scripts!
219+
220+
## Future Improvements
221+
222+
Some ideas I'm considering:
223+
224+
- **Package categories** - Group by software type
225+
- **Fuzzy search** - Better matching for typos
226+
- **PowerShell commands** - Alternative to cmd syntax
227+
- **Package details modal** - Show more metadata
228+
- **Search history** - Remember recent searches
229+
230+
## Live Demo
231+
232+
Check out the live site: [https://solrevdev.github.io/winget-search/](https://solrevdev.github.io/winget-search/)
233+
234+
The source code is available on [GitHub](https://github.com/solrevdev/winget-search) under the MIT license.
235+
236+
Success 🎉

0 commit comments

Comments
 (0)