├── .gitignore
├── BrowserSearch.sln
├── BrowserSearch
├── BrowserSearch.csproj
├── Browsers
│ ├── Chromium.cs
│ ├── Firefox.cs
│ ├── IBrowser.cs
│ └── OperaGX.cs
├── Images
│ ├── BrowserSearch.dark.png
│ └── BrowserSearch.light.png
├── Main.cs
└── plugin.json
├── README.md
├── Screenshots
└── 1.png
└── build.bat
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Ww][Ii][Nn]32/
27 | [Aa][Rr][Mm]/
28 | [Aa][Rr][Mm]64/
29 | bld/
30 | [Bb]in/
31 | [Oo]bj/
32 | [Ll]og/
33 | [Ll]ogs/
34 |
35 | # Visual Studio 2015/2017 cache/options directory
36 | .vs/
37 | # Uncomment if you have tasks that create the project's static files in wwwroot
38 | #wwwroot/
39 |
40 | # Visual Studio 2017 auto generated files
41 | Generated\ Files/
42 |
43 | # MSTest test Results
44 | [Tt]est[Rr]esult*/
45 | [Bb]uild[Ll]og.*
46 |
47 | # NUnit
48 | *.VisualState.xml
49 | TestResult.xml
50 | nunit-*.xml
51 |
52 | # Build Results of an ATL Project
53 | [Dd]ebugPS/
54 | [Rr]eleasePS/
55 | dlldata.c
56 |
57 | # Benchmark Results
58 | BenchmarkDotNet.Artifacts/
59 |
60 | # .NET Core
61 | project.lock.json
62 | project.fragment.lock.json
63 | artifacts/
64 |
65 | # ASP.NET Scaffolding
66 | ScaffoldingReadMe.txt
67 |
68 | # StyleCop
69 | StyleCopReport.xml
70 |
71 | # Files built by Visual Studio
72 | *_i.c
73 | *_p.c
74 | *_h.h
75 | *.ilk
76 | *.meta
77 | *.obj
78 | *.iobj
79 | *.pch
80 | *.pdb
81 | *.ipdb
82 | *.pgc
83 | *.pgd
84 | *.rsp
85 | *.sbr
86 | *.tlb
87 | *.tli
88 | *.tlh
89 | *.tmp
90 | *.tmp_proj
91 | *_wpftmp.csproj
92 | *.log
93 | *.tlog
94 | *.vspscc
95 | *.vssscc
96 | .builds
97 | *.pidb
98 | *.svclog
99 | *.scc
100 |
101 | # Chutzpah Test files
102 | _Chutzpah*
103 |
104 | # Visual C++ cache files
105 | ipch/
106 | *.aps
107 | *.ncb
108 | *.opendb
109 | *.opensdf
110 | *.sdf
111 | *.cachefile
112 | *.VC.db
113 | *.VC.VC.opendb
114 |
115 | # Visual Studio profiler
116 | *.psess
117 | *.vsp
118 | *.vspx
119 | *.sap
120 |
121 | # Visual Studio Trace Files
122 | *.e2e
123 |
124 | # TFS 2012 Local Workspace
125 | $tf/
126 |
127 | # Guidance Automation Toolkit
128 | *.gpState
129 |
130 | # ReSharper is a .NET coding add-in
131 | _ReSharper*/
132 | *.[Rr]e[Ss]harper
133 | *.DotSettings.user
134 |
135 | # TeamCity is a build add-in
136 | _TeamCity*
137 |
138 | # DotCover is a Code Coverage Tool
139 | *.dotCover
140 |
141 | # AxoCover is a Code Coverage Tool
142 | .axoCover/*
143 | !.axoCover/settings.json
144 |
145 | # Coverlet is a free, cross platform Code Coverage Tool
146 | coverage*.json
147 | coverage*.xml
148 | coverage*.info
149 |
150 | # Visual Studio code coverage results
151 | *.coverage
152 | *.coveragexml
153 |
154 | # NCrunch
155 | _NCrunch_*
156 | .*crunch*.local.xml
157 | nCrunchTemp_*
158 |
159 | # MightyMoose
160 | *.mm.*
161 | AutoTest.Net/
162 |
163 | # Web workbench (sass)
164 | .sass-cache/
165 |
166 | # Installshield output folder
167 | [Ee]xpress/
168 |
169 | # DocProject is a documentation generator add-in
170 | DocProject/buildhelp/
171 | DocProject/Help/*.HxT
172 | DocProject/Help/*.HxC
173 | DocProject/Help/*.hhc
174 | DocProject/Help/*.hhk
175 | DocProject/Help/*.hhp
176 | DocProject/Help/Html2
177 | DocProject/Help/html
178 |
179 | # Click-Once directory
180 | publish/
181 |
182 | # Publish Web Output
183 | *.[Pp]ublish.xml
184 | *.azurePubxml
185 | # Note: Comment the next line if you want to checkin your web deploy settings,
186 | # but database connection strings (with potential passwords) will be unencrypted
187 | *.pubxml
188 | *.publishproj
189 |
190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
191 | # checkin your Azure Web App publish settings, but sensitive information contained
192 | # in these scripts will be unencrypted
193 | PublishScripts/
194 |
195 | # NuGet Packages
196 | *.nupkg
197 | # NuGet Symbol Packages
198 | *.snupkg
199 | # The packages folder can be ignored because of Package Restore
200 | **/[Pp]ackages/*
201 | # except build/, which is used as an MSBuild target.
202 | !**/[Pp]ackages/build/
203 | # Uncomment if necessary however generally it will be regenerated when needed
204 | #!**/[Pp]ackages/repositories.config
205 | # NuGet v3's project.json files produces more ignorable files
206 | *.nuget.props
207 | *.nuget.targets
208 |
209 | # Microsoft Azure Build Output
210 | csx/
211 | *.build.csdef
212 |
213 | # Microsoft Azure Emulator
214 | ecf/
215 | rcf/
216 |
217 | # Windows Store app package directories and files
218 | AppPackages/
219 | BundleArtifacts/
220 | Package.StoreAssociation.xml
221 | _pkginfo.txt
222 | *.appx
223 | *.appxbundle
224 | *.appxupload
225 |
226 | # Visual Studio cache files
227 | # files ending in .cache can be ignored
228 | *.[Cc]ache
229 | # but keep track of directories ending in .cache
230 | !?*.[Cc]ache/
231 |
232 | # Others
233 | ClientBin/
234 | ~$*
235 | *~
236 | *.dbmdl
237 | *.dbproj.schemaview
238 | *.jfm
239 | *.pfx
240 | *.publishsettings
241 | orleans.codegen.cs
242 |
243 | # Including strong name files can present a security risk
244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
245 | #*.snk
246 |
247 | # Since there are multiple workflows, uncomment next line to ignore bower_components
248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
249 | #bower_components/
250 |
251 | # RIA/Silverlight projects
252 | Generated_Code/
253 |
254 | # Backup & report files from converting an old project file
255 | # to a newer Visual Studio version. Backup files are not needed,
256 | # because we have git ;-)
257 | _UpgradeReport_Files/
258 | Backup*/
259 | UpgradeLog*.XML
260 | UpgradeLog*.htm
261 | ServiceFabricBackup/
262 | *.rptproj.bak
263 |
264 | # SQL Server files
265 | *.mdf
266 | *.ldf
267 | *.ndf
268 |
269 | # Business Intelligence projects
270 | *.rdl.data
271 | *.bim.layout
272 | *.bim_*.settings
273 | *.rptproj.rsuser
274 | *- [Bb]ackup.rdl
275 | *- [Bb]ackup ([0-9]).rdl
276 | *- [Bb]ackup ([0-9][0-9]).rdl
277 |
278 | # Microsoft Fakes
279 | FakesAssemblies/
280 |
281 | # GhostDoc plugin setting file
282 | *.GhostDoc.xml
283 |
284 | # Node.js Tools for Visual Studio
285 | .ntvs_analysis.dat
286 | node_modules/
287 |
288 | # Visual Studio 6 build log
289 | *.plg
290 |
291 | # Visual Studio 6 workspace options file
292 | *.opt
293 |
294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
295 | *.vbw
296 |
297 | # Visual Studio 6 auto-generated project file (contains which files were open etc.)
298 | *.vbp
299 |
300 | # Visual Studio 6 workspace and project file (working project files containing files to include in project)
301 | *.dsw
302 | *.dsp
303 |
304 | # Visual Studio 6 technical files
305 | *.ncb
306 | *.aps
307 |
308 | # Visual Studio LightSwitch build output
309 | **/*.HTMLClient/GeneratedArtifacts
310 | **/*.DesktopClient/GeneratedArtifacts
311 | **/*.DesktopClient/ModelManifest.xml
312 | **/*.Server/GeneratedArtifacts
313 | **/*.Server/ModelManifest.xml
314 | _Pvt_Extensions
315 |
316 | # Paket dependency manager
317 | .paket/paket.exe
318 | paket-files/
319 |
320 | # FAKE - F# Make
321 | .fake/
322 |
323 | # CodeRush personal settings
324 | .cr/personal
325 |
326 | # Python Tools for Visual Studio (PTVS)
327 | __pycache__/
328 | *.pyc
329 |
330 | # Cake - Uncomment if you are using it
331 | # tools/**
332 | # !tools/packages.config
333 |
334 | # Tabs Studio
335 | *.tss
336 |
337 | # Telerik's JustMock configuration file
338 | *.jmconfig
339 |
340 | # BizTalk build output
341 | *.btp.cs
342 | *.btm.cs
343 | *.odx.cs
344 | *.xsd.cs
345 |
346 | # OpenCover UI analysis results
347 | OpenCover/
348 |
349 | # Azure Stream Analytics local run output
350 | ASALocalRun/
351 |
352 | # MSBuild Binary and Structured Log
353 | *.binlog
354 |
355 | # NVidia Nsight GPU debugger configuration file
356 | *.nvuser
357 |
358 | # MFractors (Xamarin productivity tool) working folder
359 | .mfractor/
360 |
361 | # Local History for Visual Studio
362 | .localhistory/
363 |
364 | # Visual Studio History (VSHistory) files
365 | .vshistory/
366 |
367 | # BeatPulse healthcheck temp database
368 | healthchecksdb
369 |
370 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
371 | MigrationBackup/
372 |
373 | # Ionide (cross platform F# VS Code tools) working folder
374 | .ionide/
375 |
376 | # Fody - auto-generated XML schema
377 | FodyWeavers.xsd
378 |
379 | # VS Code files for those working on multiple tools
380 | .vscode/*
381 | !.vscode/settings.json
382 | !.vscode/tasks.json
383 | !.vscode/launch.json
384 | !.vscode/extensions.json
385 | *.code-workspace
386 |
387 | # Local History for Visual Studio Code
388 | .history/
389 |
390 | # Windows Installer files from build outputs
391 | *.cab
392 | *.msi
393 | *.msix
394 | *.msm
395 | *.msp
396 |
397 | # JetBrains Rider
398 | *.sln.iml
399 |
400 | # Libs should be added by the user
401 | *libs/
--------------------------------------------------------------------------------
/BrowserSearch.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.5.33312.197
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BrowserSearch", "BrowserSearch\BrowserSearch.csproj", "{EF807AEB-094D-449A-8FF3-B34E56FB05C6}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|ARM64 = Debug|ARM64
11 | Debug|x64 = Debug|x64
12 | Release|ARM64 = Release|ARM64
13 | Release|x64 = Release|x64
14 | EndGlobalSection
15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
16 | {EF807AEB-094D-449A-8FF3-B34E56FB05C6}.Debug|ARM64.ActiveCfg = Debug|ARM64
17 | {EF807AEB-094D-449A-8FF3-B34E56FB05C6}.Debug|ARM64.Build.0 = Debug|ARM64
18 | {EF807AEB-094D-449A-8FF3-B34E56FB05C6}.Debug|x64.ActiveCfg = Debug|x64
19 | {EF807AEB-094D-449A-8FF3-B34E56FB05C6}.Debug|x64.Build.0 = Debug|x64
20 | {EF807AEB-094D-449A-8FF3-B34E56FB05C6}.Release|ARM64.ActiveCfg = Release|ARM64
21 | {EF807AEB-094D-449A-8FF3-B34E56FB05C6}.Release|ARM64.Build.0 = Release|ARM64
22 | {EF807AEB-094D-449A-8FF3-B34E56FB05C6}.Release|x64.ActiveCfg = Release|x64
23 | {EF807AEB-094D-449A-8FF3-B34E56FB05C6}.Release|x64.Build.0 = Release|x64
24 | EndGlobalSection
25 | GlobalSection(SolutionProperties) = preSolution
26 | HideSolutionNode = FALSE
27 | EndGlobalSection
28 | GlobalSection(ExtensibilityGlobals) = postSolution
29 | SolutionGuid = {6BD530DB-D589-4E04-A00B-864C0233829A}
30 | EndGlobalSection
31 | EndGlobal
32 |
--------------------------------------------------------------------------------
/BrowserSearch/BrowserSearch.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0-windows
5 | enable
6 | true
7 | Community.PowerToys.Run.Plugin.$(MSBuildProjectName)
8 | AnyCPU;x64;ARM64
9 | $([System.IO.File]::ReadAllText('$(MSBuildThisFileDirectory)plugin.json').Split(',')[5].Split(':')[1].Trim().Trim('"'))
10 |
11 |
12 |
13 |
14 |
15 |
16 | libs\Wox.Plugin.dll
17 |
18 |
19 | libs\Wox.Infrastructure.dll
20 |
21 |
22 | libs\Microsoft.Data.Sqlite.dll
23 |
24 |
25 | libs\PowerToys.Settings.UI.Lib.dll
26 |
27 |
28 |
29 |
30 |
31 | PreserveNewest
32 |
33 |
34 | PreserveNewest
35 |
36 |
37 | PreserveNewest
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/BrowserSearch/Browsers/Chromium.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Data.Sqlite;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.IO;
5 | using System.Text.Json;
6 | using System.Windows;
7 | using Wox.Infrastructure;
8 | using Wox.Plugin;
9 | using Wox.Plugin.Logger;
10 | using BrowserInfo = Wox.Plugin.Common.DefaultBrowserInfo;
11 |
12 | namespace BrowserSearch.Browsers
13 | {
14 | internal class Chromium : IBrowser
15 | {
16 | protected string[] UserDataDirCandidates { get; }
17 | protected Dictionary Profiles { get; } = [];
18 | private readonly string? _selectedProfileName;
19 | private readonly List _history = [];
20 | // Key is query, Value is a list of predictions for that query
21 | private readonly Dictionary> _predictions = [];
22 |
23 | public Chromium(string[] userDataDirCandidates, string? profileName)
24 | {
25 | UserDataDirCandidates = userDataDirCandidates;
26 | _selectedProfileName = profileName;
27 | }
28 |
29 | void IBrowser.Init()
30 | {
31 | CreateProfiles();
32 |
33 | // Load history from all profiles
34 | if (_selectedProfileName is null)
35 | {
36 | foreach (ChromiumProfile profile in Profiles.Values)
37 | {
38 | profile.Init(_history, _predictions);
39 | }
40 |
41 | return;
42 | }
43 |
44 | // Load history from selected profile
45 | if (!Profiles.TryGetValue(_selectedProfileName.ToLower(), out ChromiumProfile? selectedProfile))
46 | {
47 | Log.Error($"Couldn't find profile '{_selectedProfileName}'", typeof(Chromium));
48 | MessageBox.Show($"No profile with the name '{_selectedProfileName}' was found.", "BrowserSearch");
49 |
50 | return;
51 | }
52 | selectedProfile.Init(_history, _predictions);
53 | }
54 |
55 | protected virtual void CreateProfiles()
56 | {
57 | string? userDataDir = null;
58 | foreach (string candidate in UserDataDirCandidates)
59 | {
60 | if (Directory.Exists(candidate))
61 | {
62 | userDataDir = candidate;
63 | break;
64 | }
65 | }
66 |
67 | if (userDataDir is null)
68 | throw new NullReferenceException("Couldn\'t find UserData directory");
69 |
70 | Log.Info($"Found UserData directory: {userDataDir}", typeof(Chromium));
71 | using StreamReader jsonFileReader = new(
72 | new FileStream(Path.Join(userDataDir, "Local State"), FileMode.Open, FileAccess.Read, FileShare.ReadWrite)
73 | );
74 |
75 | JsonDocument localState = JsonDocument.Parse(jsonFileReader.ReadToEnd());
76 | jsonFileReader.Close();
77 |
78 | string[] nameProperties = ["gaia_given_name", "gaia_name", "name", "shortcut_name"];
79 | JsonElement infoCache = localState.RootElement.GetProperty("profile").GetProperty("info_cache");
80 | foreach (JsonProperty profileInfo in infoCache.EnumerateObject())
81 | {
82 | ChromiumProfile profile = new(Path.Join(userDataDir, profileInfo.Name));
83 | Profiles[profileInfo.Name.ToLower()] = profile;
84 |
85 | foreach (string nameProp in nameProperties)
86 | {
87 | if (profileInfo.Value.TryGetProperty(nameProp, out JsonElement nameElem))
88 | {
89 | string? name = nameElem.GetString()?.ToLower();
90 | if (!string.IsNullOrEmpty(name))
91 | {
92 | Profiles[name] = profile;
93 | }
94 | }
95 | }
96 | }
97 | }
98 |
99 | List IBrowser.GetHistory()
100 | {
101 | return _history;
102 | }
103 |
104 | public int CalculateExtraScore(string query, string title, string url)
105 | {
106 | if (!_predictions.TryGetValue(query, out List? predictions))
107 | {
108 | return 0;
109 | }
110 |
111 | foreach (ChromiumPrediction prediction in predictions)
112 | {
113 | if (prediction.url == url)
114 | {
115 | return (int)prediction.hits;
116 | }
117 | }
118 |
119 | return 0;
120 | }
121 | }
122 |
123 | internal class ChromiumPrediction(string url, long hits)
124 | {
125 | public readonly string url = url;
126 | public readonly long hits = hits;
127 | }
128 |
129 | internal class ChromiumProfile
130 | {
131 | private readonly string _path;
132 | private bool _initialized;
133 | private SqliteConnection? _historyDbConnection, _predictorDbConnection;
134 |
135 | public ChromiumProfile(string path)
136 | {
137 | _path = path;
138 | }
139 |
140 | public void Init(List history, Dictionary> predictions)
141 | {
142 | if (_initialized)
143 | {
144 | return;
145 | }
146 | Log.Info($"Initializing Chromium profile: '{_path}'", typeof(ChromiumProfile));
147 |
148 | try
149 | {
150 | CopyDatabases();
151 | }
152 | catch (FileNotFoundException)
153 | {
154 | Log.Warn($"Couldn't find database files in '{_path}'", typeof(ChromiumProfile));
155 | return;
156 | }
157 | ArgumentNullException.ThrowIfNull(_historyDbConnection);
158 | ArgumentNullException.ThrowIfNull(_predictorDbConnection);
159 |
160 | PopulatePredictions(predictions);
161 | PopulateHistory(history);
162 |
163 | _historyDbConnection.Close();
164 | _predictorDbConnection.Close();
165 | _historyDbConnection.Dispose();
166 | _predictorDbConnection.Dispose();
167 | _initialized = true;
168 | }
169 |
170 | private void CopyDatabases()
171 | {
172 | string _dirName = _path[(_path.LastIndexOf('\\') + 1)..];
173 | string historyCopy = Path.GetTempPath() + @"\BrowserSearch_History_" + _dirName;
174 | string predictorCopy = Path.GetTempPath() + @"\BrowserSearch_ActionPredictor_" + _dirName;
175 |
176 | // We need to copy the databases, otherwise we can't open them while the browser is running
177 | File.Copy(
178 | Path.Join(_path, @"\History"), historyCopy, true
179 | );
180 | File.Copy(
181 | Path.Join(_path, @"\Network Action Predictor"), predictorCopy, true
182 | );
183 |
184 | _historyDbConnection = new($"Data Source={historyCopy}");
185 | _predictorDbConnection = new($"Data Source={predictorCopy}");
186 | }
187 |
188 | private static SqliteDataReader ExecuteCmd(SqliteConnection connection, SqliteCommand cmd)
189 | {
190 | cmd.Connection = connection;
191 | connection.Open();
192 |
193 | return cmd.ExecuteReader();
194 | }
195 |
196 | public void PopulatePredictions(Dictionary> predictions)
197 | {
198 | ArgumentNullException.ThrowIfNull(_predictorDbConnection);
199 |
200 | using SqliteCommand cmd = new("SELECT user_text, url, number_of_hits FROM network_action_predictor");
201 | using SqliteDataReader reader = ExecuteCmd(_predictorDbConnection, cmd);
202 | while (reader.Read())
203 | {
204 | string query = (string)reader[0];
205 | string url = (string)reader[1]; // Predicted URL for that query
206 | long hits = (long)reader[2]; // Amount of times the prediction was correct and the user selected it
207 |
208 | if (!predictions.TryGetValue(query, out List? value))
209 | {
210 | value = [];
211 | predictions[query] = value;
212 | }
213 |
214 | value.Add(new ChromiumPrediction(url, hits));
215 | }
216 | }
217 |
218 | public void PopulateHistory(List history)
219 | {
220 | ArgumentNullException.ThrowIfNull(_historyDbConnection);
221 |
222 | using SqliteCommand historyReadCmd = new("SELECT url, title FROM urls ORDER BY visit_count DESC");
223 | using SqliteDataReader reader = ExecuteCmd(_historyDbConnection, historyReadCmd);
224 | while (reader.Read())
225 | {
226 | string url = (string)reader[0];
227 | string title = (string)reader[1];
228 |
229 | Result result = new()
230 | {
231 | QueryTextDisplay = url,
232 | Title = title,
233 | SubTitle = url,
234 | IcoPath = BrowserInfo.IconPath,
235 | Action = action =>
236 | {
237 | // Open URL in default browser
238 | if (!Helper.OpenInShell(url))
239 | {
240 | Log.Error($"Couldn't open '{url}'", typeof(ChromiumProfile));
241 | return false;
242 | }
243 |
244 | return true;
245 | },
246 | };
247 |
248 | history.Add(result);
249 | }
250 | }
251 | }
252 | }
253 |
--------------------------------------------------------------------------------
/BrowserSearch/Browsers/Firefox.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Data.Sqlite;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.IO;
5 | using System.Windows;
6 | using Wox.Infrastructure;
7 | using Wox.Plugin;
8 | using Wox.Plugin.Logger;
9 | using BrowserInfo = Wox.Plugin.Common.DefaultBrowserInfo;
10 |
11 | namespace BrowserSearch.Browsers
12 | {
13 | internal class Firefox : IBrowser
14 | {
15 | private readonly string[] _profilesDirCandidates;
16 | private readonly Dictionary _profiles = [];
17 | private readonly string? _selectedProfileName;
18 | private readonly List _history = [];
19 | private readonly Dictionary<(string, string), long> _frecencyValues = new();
20 |
21 | public Firefox(string[] profilesDirCandidates, string? profileName)
22 | {
23 | _profilesDirCandidates = profilesDirCandidates;
24 | _selectedProfileName = profileName;
25 | }
26 |
27 | void IBrowser.Init()
28 | {
29 | CreateProfiles();
30 |
31 | Log.Info($"Loading firefox profile '{_selectedProfileName}'", typeof(Firefox));
32 |
33 | // Load history from all profiles
34 | if (_selectedProfileName is null)
35 | {
36 | foreach (FirefoxProfile profile in _profiles.Values)
37 | {
38 | profile.Init(_history, _frecencyValues);
39 | }
40 |
41 | return;
42 | }
43 |
44 | // Load history from selected profile
45 | if (!_profiles.TryGetValue(_selectedProfileName.ToLower(), out FirefoxProfile? selectedProfile))
46 | {
47 | Log.Error($"Couldn't find profile '{_selectedProfileName}'", typeof(Firefox));
48 | MessageBox.Show($"No profile with the name '{_selectedProfileName}' was found.", "BrowserSearch");
49 |
50 | return;
51 | }
52 | selectedProfile.Init(_history, _frecencyValues);
53 | }
54 |
55 | private void CreateProfiles()
56 | {
57 | // Inside the Profiles directory, there are directories for each profile.
58 | // They are named with the following format: .
59 |
60 | foreach (string profilesDir in _profilesDirCandidates)
61 | {
62 | if (!Directory.Exists(profilesDir))
63 | continue;
64 |
65 | Log.Info($"Found profiles dir: {profilesDir}", typeof(Firefox));
66 | foreach (string profileDir in Directory.GetDirectories(profilesDir))
67 | {
68 | string profileName = Path.GetFileName(profileDir);
69 | int dotIndex = profileName.IndexOf('.');
70 | if (dotIndex != -1)
71 | {
72 | profileName = profileName[(dotIndex + 1)..];
73 | }
74 | Log.Info($"Found profile: '{profileName}'", typeof(Firefox));
75 | _profiles[profileName.ToLower()] = new FirefoxProfile(profileDir);
76 | }
77 |
78 | return;
79 | }
80 |
81 | Log.Error("Failed to find profiles directory", typeof(Firefox));
82 | }
83 |
84 | List IBrowser.GetHistory()
85 | {
86 | return _history;
87 | }
88 |
89 | public int CalculateExtraScore(string query, string title, string url)
90 | {
91 | // The history entries are stored in the _history list
92 | // Those entries have a frecency value
93 | // The frecency value is a combination of the amount of times the user visited the page and the time since the last visit
94 |
95 | if (url.Contains(query, StringComparison.InvariantCultureIgnoreCase) || title.Contains(query, StringComparison.InvariantCultureIgnoreCase))
96 | {
97 | long frecency = _frecencyValues.GetValueOrDefault((url, title), 0);
98 | return (int)frecency / 1000;
99 | }
100 | else
101 | {
102 | return 0;
103 | }
104 | }
105 | }
106 |
107 | internal class FirefoxProfile
108 | {
109 | private readonly string _path;
110 | private bool _initialized;
111 | private SqliteConnection? _historyDbConnection;
112 |
113 | public FirefoxProfile(string path)
114 | {
115 | _path = path;
116 | }
117 |
118 | public void Init(List history, Dictionary<(string, string), long> frecencyValues)
119 | {
120 | if (_initialized)
121 | {
122 | return;
123 | }
124 | Log.Info($"Initializing Firefox profile: '{_path}'", typeof(FirefoxProfile));
125 |
126 | try
127 | {
128 | CopyDatabases();
129 | }
130 | catch (FileNotFoundException)
131 | {
132 | Log.Warn($"Couldn't find database file in '{_path}'", typeof(FirefoxProfile));
133 | return;
134 | }
135 | ArgumentNullException.ThrowIfNull(_historyDbConnection);
136 |
137 | PopulateHistory(history, frecencyValues);
138 |
139 |
140 | _historyDbConnection.Close();
141 | _historyDbConnection.Dispose();
142 | _initialized = true;
143 |
144 | Log.Info($"Finished initializing Firefox profile: '{_path}'", typeof(FirefoxProfile));
145 | }
146 |
147 | private void CopyDatabases()
148 | {
149 | string _dirName = _path[(_path.LastIndexOf('\\') + 1)..];
150 | string historyCopy = Path.GetTempPath() + @"\BrowserSearch_History_" + _dirName;
151 |
152 | File.Copy(
153 | Path.Join(_path, @"\places.sqlite"), historyCopy, true
154 | );
155 |
156 | _historyDbConnection = new($"Data Source={historyCopy}");
157 | }
158 |
159 | private static SqliteDataReader ExecuteCmd(SqliteConnection connection, SqliteCommand cmd)
160 | {
161 | cmd.Connection = connection;
162 | connection.Open();
163 |
164 | return cmd.ExecuteReader();
165 | }
166 |
167 | public void PopulateHistory(List history, Dictionary<(string, string), long> frecencyValues)
168 | {
169 | ArgumentNullException.ThrowIfNull(_historyDbConnection);
170 |
171 | // Read the history entries from the database
172 | using SqliteCommand historyReadCmd = new("SELECT url, title, frecency FROM moz_places GROUP BY url ORDER BY frecency DESC"); // Limiting here is possible
173 | using SqliteDataReader reader = ExecuteCmd(_historyDbConnection, historyReadCmd);
174 |
175 | // Iterate over the sql results
176 | while (reader.Read())
177 | {
178 | // Make sure there is no System.DBNull value
179 | if (reader.IsDBNull(0) || reader.IsDBNull(1) || reader.IsDBNull(2))
180 | {
181 | continue;
182 | }
183 |
184 |
185 | string url = (string)reader[0];
186 | string title = (string)reader[1];
187 | long frecency = (long)reader[2];
188 |
189 | // Add the frecency value to the frecencyValues dictionary
190 | frecencyValues[(url, title)] = frecency;
191 |
192 |
193 | // Create a new Wox Result object and add it to the history list
194 | Result result = new()
195 | {
196 | QueryTextDisplay = url, // The text that will be displayed in the search box
197 | Title = title, // The title of the result
198 | SubTitle = url, // The subtitle of the result
199 | IcoPath = BrowserInfo.IconPath, // The icon that will be displayed next to the result
200 | Action = action => // The action that will be executed when the result is selected
201 | {
202 | // Open URL in default browser
203 | if (!Helper.OpenInShell(url))
204 | {
205 | Log.Error($"Couldn't open '{url}'", typeof(FirefoxProfile));
206 | return false;
207 | }
208 |
209 | return true;
210 | },
211 | };
212 |
213 | history.Add(result);
214 | }
215 | history.Reverse(); // Reversing puts the highest frecency values to the end
216 | // This way, the highest frecency values will be the first to be displayed in the search results when the user input is vague
217 | }
218 | }
219 | }
220 |
--------------------------------------------------------------------------------
/BrowserSearch/Browsers/IBrowser.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using Wox.Plugin;
3 |
4 | namespace BrowserSearch.Browsers
5 | {
6 | internal interface IBrowser
7 | {
8 | void Init();
9 | List GetHistory();
10 | int CalculateExtraScore(string query, string title, string url);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/BrowserSearch/Browsers/OperaGX.cs:
--------------------------------------------------------------------------------
1 | using System.Windows;
2 |
3 | namespace BrowserSearch.Browsers
4 | {
5 | internal class OperaGX : Chromium
6 | {
7 | public OperaGX(string userDataDir, string? profileName) : base([userDataDir], null)
8 | {
9 | // I don't understand how profiles work on OperaGX nor I could get them to work
10 | // on the browser itself, so multiple profiles is currently unsupported and untested
11 | if (profileName is not null)
12 | {
13 | MessageBox.Show($"Browser profiles aren't supported on Opera GX", "BrowserSearch");
14 | }
15 | }
16 |
17 | protected override void CreateProfiles()
18 | {
19 | // Unlike most Chromium-based browsers, OperaGX doesn't have a "Local State" file
20 | Profiles["default"] = new ChromiumProfile(UserDataDirCandidates[0]);
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/BrowserSearch/Images/BrowserSearch.dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TBM13/BrowserSearch/1c31179a7849dba519d93bb2be612e05575dc271/BrowserSearch/Images/BrowserSearch.dark.png
--------------------------------------------------------------------------------
/BrowserSearch/Images/BrowserSearch.light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TBM13/BrowserSearch/1c31179a7849dba519d93bb2be612e05575dc271/BrowserSearch/Images/BrowserSearch.light.png
--------------------------------------------------------------------------------
/BrowserSearch/Main.cs:
--------------------------------------------------------------------------------
1 | using BrowserSearch.Browsers;
2 | using Microsoft.PowerToys.Settings.UI.Library;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.IO;
6 | using System.Linq;
7 | using System.Threading;
8 | using System.Windows;
9 | using System.Windows.Controls;
10 | using Wox.Plugin;
11 | using Wox.Plugin.Logger;
12 | using BrowserInfo = Wox.Plugin.Common.DefaultBrowserInfo;
13 |
14 | namespace Community.Powertoys.Run.Plugin.BrowserSearch
15 | {
16 | public class Main : IPlugin, ISettingProvider, IReloadable
17 | {
18 | public static string PluginID => "E5A9FC7A3F7F4320BE612DA95C57C32D";
19 | public string Name => "Browser Search";
20 | public string Description => "Searches in your browser's history.";
21 |
22 | public IEnumerable AdditionalOptions => new List()
23 | {
24 | new()
25 | {
26 | Key = MaxResults,
27 | DisplayLabel = "Maximum number of results",
28 | DisplayDescription = "Maximum number of results to show. Set to -1 to show all (may decrease performance)",
29 | PluginOptionType = PluginAdditionalOption.AdditionalOptionType.Numberbox,
30 | NumberValue = 15
31 | },
32 | new()
33 | {
34 | Key = SingleProfile,
35 | DisplayLabel = "Browser profile",
36 | DisplayDescription = "The name of the browser profile whose history will be loaded.\n" +
37 | "If empty, the history of ALL profiles will be loaded.",
38 | PluginOptionType = PluginAdditionalOption.AdditionalOptionType.Textbox
39 | }
40 | };
41 |
42 | private const string MaxResults = nameof(MaxResults);
43 | private const string SingleProfile = nameof(SingleProfile);
44 | private PluginInitContext? _context;
45 | private IBrowser? _defaultBrowser;
46 | private long _lastUpdateTickCount = -300L;
47 | private int _maxResults;
48 | private string? _selectedProfileName;
49 |
50 | public void Init(PluginInitContext context)
51 | {
52 | _context = context ?? throw new ArgumentNullException(nameof(context));
53 | InitDefaultBrowser();
54 | }
55 |
56 | public void ReloadData()
57 | {
58 | if (_context is null)
59 | {
60 | return;
61 | }
62 |
63 | // When the plugin is disabled and then re-enabled,
64 | // ReloadData() is called multiple times so this is needed
65 | if (Environment.TickCount64 - _lastUpdateTickCount >= 300)
66 | {
67 | InitDefaultBrowser();
68 | _lastUpdateTickCount = Environment.TickCount64;
69 | }
70 | }
71 |
72 | public Control CreateSettingPanel()
73 | {
74 | throw new NotImplementedException();
75 | }
76 |
77 | public void UpdateSettings(PowerLauncherPluginSettings settings)
78 | {
79 | _maxResults = (int)(settings?.AdditionalOptions?.FirstOrDefault(x => x.Key == MaxResults)?.NumberValue ?? 15);
80 |
81 | PluginAdditionalOption? profile = settings?.AdditionalOptions?.FirstOrDefault(x => x.Key == SingleProfile);
82 | if (profile is not null && profile.TextValue?.Length > 0)
83 | {
84 | _selectedProfileName = profile.TextValue;
85 | }
86 | else
87 | {
88 | _selectedProfileName = null;
89 | }
90 | }
91 |
92 | private void InitDefaultBrowser()
93 | {
94 | // Retrieve default browser info
95 | BrowserInfo.UpdateIfTimePassed();
96 |
97 | // It takes some time until BrowserInfo is updated, wait up to 500 ms
98 | int msSlept = 0;
99 | while (BrowserInfo.Name is null && msSlept < 500)
100 | {
101 | Thread.Sleep(50);
102 | msSlept += 50;
103 | }
104 |
105 | if (BrowserInfo.Name is null)
106 | {
107 | Log.Error("Couldn't retrieve default browser name: Timeout", typeof(Main));
108 | return;
109 | }
110 |
111 | _defaultBrowser = null;
112 | string localappdata = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
113 | string roamingappdata = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
114 |
115 | switch (BrowserInfo.Name)
116 | {
117 | case "Arc":
118 | _defaultBrowser = new Chromium(
119 | [Path.Join(localappdata, @"Packages\TheBrowserCompany.Arc_ttt1ap7aakyb4\LocalCache\Local\Arc\User Data")], _selectedProfileName);
120 | break;
121 | case "Brave":
122 | _defaultBrowser = new Chromium(
123 | [Path.Join(localappdata, @"BraveSoftware\Brave-Browser\User Data")], _selectedProfileName);
124 | break;
125 | case "Brave Beta":
126 | _defaultBrowser = new Chromium(
127 | [Path.Join(localappdata, @"BraveSoftware\Brave-Browser-Beta\User Data")], _selectedProfileName);
128 | break;
129 | case "Brave Nightly":
130 | _defaultBrowser = new Chromium(
131 | [Path.Join(localappdata, @"BraveSoftware\Brave-Browser-Nightly\User Data")], _selectedProfileName);
132 | break;
133 | case "Chromium":
134 | _defaultBrowser = new Chromium(
135 | [Path.Join(localappdata, @"Chromium\User Data")], _selectedProfileName);
136 | break;
137 | case "Firefox":
138 | _defaultBrowser = new Firefox([
139 | Path.Join(roamingappdata, @"Mozilla\Firefox\Profiles"), // Mozilla Firefox
140 | Path.Join(roamingappdata, @"zen\Profiles"), // Zen Browser
141 | ], _selectedProfileName);
142 | break;
143 | case "Google Chrome":
144 | _defaultBrowser = new Chromium(
145 | [Path.Join(localappdata, @"Google\Chrome\User Data")], _selectedProfileName);
146 | break;
147 | case "Google Chrome Beta":
148 | _defaultBrowser = new Chromium(
149 | [Path.Join(localappdata, @"Google\Chrome Beta\User Data")], _selectedProfileName);
150 | break;
151 | case "Google Chrome Dev":
152 | _defaultBrowser = new Chromium(
153 | [Path.Join(localappdata, @"Google\Chrome Dev\User Data")], _selectedProfileName);
154 | break;
155 | case "Google Chrome Canary":
156 | _defaultBrowser = new Chromium(
157 | [Path.Join(localappdata, @"Google\Chrome SxS\User Data")], _selectedProfileName);
158 | break;
159 | case "LibreWolf":
160 | _defaultBrowser = new Firefox([Path.Join(roamingappdata, @"librewolf\Profiles")], _selectedProfileName);
161 | break;
162 | case "Microsoft Edge":
163 | _defaultBrowser = new Chromium(
164 | [Path.Join(localappdata, @"Microsoft\Edge\User Data")], _selectedProfileName);
165 | break;
166 | case "Microsoft Edge Beta":
167 | _defaultBrowser = new Chromium(
168 | [Path.Join(localappdata, @"Microsoft\Edge Beta\User Data")], _selectedProfileName);
169 | break;
170 | case "Microsoft Edge Dev":
171 | _defaultBrowser = new Chromium(
172 | [Path.Join(localappdata, @"Microsoft\Edge Dev\User Data")], _selectedProfileName);
173 | break;
174 | case "Microsoft Edge Canary":
175 | _defaultBrowser = new Chromium(
176 | [Path.Join(localappdata, @"Microsoft\Edge SxS\User Data")], _selectedProfileName);
177 | break;
178 | case "NAVER Whale":
179 | _defaultBrowser = new Chromium(
180 | [Path.Join(localappdata, @"Naver\Naver Whale\User Data")], _selectedProfileName);
181 | break;
182 | case "Opera":
183 | _defaultBrowser = new Chromium([
184 | Path.Join(roamingappdata, @"Opera Software\Opera Stable"), // Opera
185 | Path.Join(roamingappdata, @"Opera Software\Opera Developer") // Opera Developer
186 | ], _selectedProfileName);
187 | break;
188 | case "Opera GX":
189 | _defaultBrowser = new OperaGX(
190 | Path.Join(roamingappdata, @"Opera Software\Opera GX Stable"), _selectedProfileName);
191 | break;
192 | case "Thorium":
193 | _defaultBrowser = new Chromium(
194 | [Path.Join(localappdata, @"Thorium\User Data")], _selectedProfileName);
195 | break;
196 | case "Vivaldi":
197 | _defaultBrowser = new Chromium(
198 | [Path.Join(localappdata, @"Vivaldi\User Data")], _selectedProfileName);
199 | break;
200 | case "Waterfox":
201 | _defaultBrowser = new Firefox([
202 | Path.Join(roamingappdata, @"Waterfox\Profiles")
203 | ], _selectedProfileName);
204 | break;
205 | case "Wavebox":
206 | _defaultBrowser = new Chromium(
207 | [Path.Join(localappdata, @"WaveboxApp\User Data")], _selectedProfileName);
208 | break;
209 | default:
210 | Log.Error($"Unsupported/unrecognized default browser '{BrowserInfo.Name}'", typeof(Main));
211 | MessageBox.Show($"Browser '{BrowserInfo.Name}' is not supported", "BrowserSearch");
212 | return;
213 | }
214 |
215 | Log.Info($"Initializing browser '{BrowserInfo.Name}'", typeof(Main));
216 | _defaultBrowser.Init();
217 | }
218 |
219 | public List Query(Query query)
220 | {
221 | ArgumentNullException.ThrowIfNull(query);
222 | if (_defaultBrowser is null)
223 | {
224 | return [];
225 | }
226 |
227 | List history = _defaultBrowser.GetHistory();
228 | // Happens when the user only types our ActionKeyword ("b?" by default)
229 | if (string.IsNullOrEmpty(query.Search))
230 | {
231 | // Returning the whole history here makes the search lag, so return only some entries
232 | int amount = _maxResults == -1 ? 15 : _maxResults;
233 | return history.TakeLast(amount).ToList();
234 | }
235 |
236 | List results = new(history.Count);
237 | for (int i = 0; i < history.Count; i++)
238 | {
239 | Result r = history[i];
240 |
241 | int score = CalculateScore(query.Search, r.Title, r.SubTitle);
242 | if (score <= 0)
243 | {
244 | continue;
245 | }
246 |
247 | r.Score = score;
248 | results.Add(r);
249 | }
250 |
251 | if (_maxResults != -1)
252 | {
253 | // Rendering the UI of every search entry is slow, so only show top results
254 | results.Sort((x, y) => y.Score.CompareTo(x.Score));
255 | results = results.Take(_maxResults).ToList();
256 | }
257 |
258 | return results;
259 | }
260 |
261 | private int CalculateScore(string query, string title, string url)
262 | {
263 | // Since PT Run's FuzzySearch is too slow, and the history usually has a lot of entries,
264 | // lets calculate the scores manually using a faster (but less accurate) method
265 | float titleScore = title.Contains(query, StringComparison.InvariantCultureIgnoreCase)
266 | ? (query.Length / (float)title.Length * 100f)
267 | : 0;
268 | float urlScore = url.Contains(query, StringComparison.InvariantCultureIgnoreCase)
269 | ? (query.Length / (float)url.Length * 100f)
270 | : 0;
271 |
272 | float score = new[] { titleScore, urlScore }.Max();
273 | score += _defaultBrowser!.CalculateExtraScore(query, title, url);
274 |
275 | return (int)score;
276 | }
277 | }
278 | }
279 |
--------------------------------------------------------------------------------
/BrowserSearch/plugin.json:
--------------------------------------------------------------------------------
1 | {
2 | "ID": "E5A9FC7A3F7F4320BE612DA95C57C32D",
3 | "ActionKeyword": "b?",
4 | "IsGlobal": true,
5 | "Name": "Browser Search",
6 | "Author": "TBM13",
7 | "Version": "1.10.0",
8 | "Language": "csharp",
9 | "Website": "https://github.com/TBM13/BrowserSearch",
10 | "ExecuteFileName": "Community.PowerToys.Run.Plugin.BrowserSearch.dll",
11 | "IcoPathDark": "Images\\BrowserSearch.dark.png",
12 | "IcoPathLight": "Images\\BrowserSearch.light.png"
13 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # BrowserSearch
2 | This is a plugin for PowerToys Run.
3 | It reads your default browser's history, allowing you to search its entries and open their URL.
4 |
5 |
6 |
7 |
8 |
9 | ## Supported browsers
10 | * Arc
11 | * Brave
12 | * Chromium
13 | * Firefox
14 | * Google Chrome
15 | * LibreWolf
16 | * Microsoft Edge (Chromium version)
17 | * Naver Whale
18 | * Opera
19 | * Opera GX (profile selection not supported)
20 | * Thorium
21 | * Ungoogled Chromium
22 | * Vivaldi Browser
23 | * Waterfox
24 | * Wavebox
25 | * Zen Browser
26 |
27 | Support for any other browser based on Chromium or Firefox can be added easily. If yours is not listed here, open an issue.
28 |
29 | **NOTE**: Some browsers share the same ID. For example, if you have both Opera and Opera Developer installed, only the history of Opera will be loaded no matter which one of them is set as the default browser since we can't differentiate them by their ID.
30 |
31 | ## Install instructions
32 | * Exit PowerToys
33 | * Download latest version from [releases](https://github.com/TBM13/BrowserSearch/releases)
34 | * Extract zip
35 | * Move extracted folder `BrowserSearch` to `%LOCALAPPDATA%\Microsoft\PowerToys\PowerToys Run\Plugins\`
36 | * Start PowerToys
37 |
38 | ## Build instructions
39 | * Clone this repo
40 | * Inside the `BrowserSearch` folder, create another one called `libs`
41 | * Copy the following files from `%ProgramFiles%\PowerToys\` to `libs`
42 | * Wox.Plugin.dll
43 | * Wox.Infrastructure.dll
44 | * Microsoft.Data.Sqlite.dll
45 | * PowerToys.Settings.UI.Lib.dll
46 | * Open the project in Visual Studio and build it in release mode
47 | * Copy the output folder `net9.0-windows` to `%LOCALAPPDATA%\Microsoft\PowerToys\PowerToys Run\Plugins\`
48 | * (Optional) Rename the copied folder to BrowserSearch
49 |
--------------------------------------------------------------------------------
/Screenshots/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TBM13/BrowserSearch/1c31179a7849dba519d93bb2be612e05575dc271/Screenshots/1.png
--------------------------------------------------------------------------------
/build.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 |
3 | dotnet build %cd%\BrowserSearch.sln /target:BrowserSearch /property:GenerateFullPaths=true /consoleloggerparameters:NoSummary /p:Configuration=Release /p:Platform="x64"
4 |
5 | set "source=%cd%\BrowserSearch\bin\x64\Release\net9.0-windows"
6 | set "destination=%LOCALAPPDATA%\Microsoft\PowerToys\PowerToys Run\Plugins\BrowserSearch"
7 |
8 | if exist "%destination%" (
9 | echo Removing existing directory...
10 | rmdir /s /q "%destination%"
11 | )
12 |
13 | echo Moving directory...
14 | move "%source%" "%destination%"
15 |
16 | echo Move completed.
17 | pause
--------------------------------------------------------------------------------