Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions BrowseRouter/Interop/Win32/BasicProcessInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace BrowseRouter.Interop.Win32
{
/// <summary>
/// A utility class to determine a process parent. Originally copied from https://stackoverflow.com/a/3346055
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct BasicProcessInfo
{
// These members must match PROCESS_BASIC_INFORMATION
internal IntPtr Reserved1;
internal IntPtr PebBaseAddress;
internal IntPtr Reserved2_0;
internal IntPtr Reserved2_1;
internal IntPtr UniqueProcessId;
internal IntPtr InheritedFromUniqueProcessId;

[DllImport("ntdll.dll")]
private static extern int NtQueryInformationProcess(IntPtr processHandle, int processInformationClass, ref BasicProcessInfo processInformation, int processInformationLength, out int returnLength);


/// <summary>
/// Gets the parent process of a specified process.
/// </summary>
/// <param name="handle">The process handle.</param>
/// <returns>An instance of the Process class.</returns>
public static Process? GetParentProcess(IntPtr handle)
{
BasicProcessInfo pbi = new();
int status = NtQueryInformationProcess(handle, 0, ref pbi, Marshal.SizeOf(pbi), out _);
if (status != 0)
throw new Win32Exception(status);

try
{
return Process.GetProcessById(pbi.InheritedFromUniqueProcessId.ToInt32());
}
catch (ArgumentException)
{
// not found
return null;
}
}
}
}
55 changes: 55 additions & 0 deletions BrowseRouter/ProcessService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using BrowseRouter.Interop.Win32;

namespace BrowseRouter
{

public interface IProcessService
{
/// <summary>
/// Try to get the name of the parent process of the current process.
/// </summary>
/// <param name="parentProcessTitle">The name of the parent process main window title (may be empty) and the specific process name.</param>
/// <returns>True if the name was succesfully found, False otherwise.</returns>
public bool TryGetParentProcessTitle(out string parentProcessTitle);
}

public class ProcessService : IProcessService
{
public bool TryGetParentProcessTitle(out string parentProcessTitle)
{
Process? parentProcess = GetParentProcess();
if (parentProcess is null || (parentProcess.MainWindowTitle == string.Empty && parentProcess.ProcessName == string.Empty))
{
parentProcessTitle = string.Empty;
return false;
}

parentProcessTitle = parentProcess.MainWindowTitle + " -> " + parentProcess.ProcessName;
return true;
}

/// <summary>
/// Gets the parent process of the current process.
/// </summary>
/// <returns>An instance of the Process class.</returns>
public static Process? GetParentProcess()
{
return BasicProcessInfo.GetParentProcess(Process.GetCurrentProcess().Handle);
}

/// <summary>
/// Gets the parent process of specified process.
/// </summary>
/// <param name="id">The process id.</param>
/// <returns>An instance of the Process class.</returns>
public static Process? GetParentProcess(int id)
{
Process process = Process.GetProcessById(id);
return BasicProcessInfo.GetParentProcess(process.Handle);
}

}
}
6 changes: 4 additions & 2 deletions BrowseRouter/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,11 @@ private static async Task<bool> RunOption(string arg)
private static async Task LaunchUrlAsyc(string url)
{
// Get the window title for whichever application is opening the URL.
string windowTitle = User32.GetActiveWindowTitle();
ProcessService processService = new();
if (!processService.TryGetParentProcessTitle(out string windowTitle))
windowTitle = User32.GetActiveWindowTitle(); //if it didn't work we get the current foreground window name instead

var configService = new ConfigService();
ConfigService configService = new();
Log.Preference = configService.GetLogPreference();

NotifyPreference notifyPref = configService.GetNotifyPreference();
Expand Down
51 changes: 29 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ Config is a poor man's INI file:
[notify]
# Show a desktop notification when opening a link. Defaults to true
enabled = true
# Should the windows notification sound be silenced when displaying it. Defaults to true
#silent = false

[log]
# Write log entries to a file. Defaults to false
Expand All @@ -98,13 +100,27 @@ enabled = true
#file = "C:\Users\<user>\Desktop\BrowseRouter.log"

# Default browser is first in list
# Use `{url}` to specify UWP app browser details
# Use `{url}` to specify UWP app browser details (not currently working, see following issue: https://github.com/nref/BrowseRouter/issues/10)
# Environment variables (like %ProgramFiles% for example) can be used
[browsers]
ff = C:\Program Files\Mozilla Firefox\firefox.exe
ff = %ProgramFiles%\Mozilla Firefox\firefox.exe
# Open in a new window
#chrome = "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --new-window
chrome = C:\Program Files (x86)\Google\Chrome\Application\chrome.exe
edge = C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe
edge = %ProgramFiles(x86)%\Microsoft\Edge\Application\msedge.exe
opera = %UserProfile%\AppData\Local\Programs\Opera\opera.exe

# Source preferences.
# - Only * is treated as a special character (wildcard).
# - Will take precedence over any URL preferences.
# - Matches on window title and specific process of the application used to open the link, like so "WindowTitle -> ProcessName".
[sources]
* - Notepad -> notepad = ff
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just want to check my understanding: Is this backward-compatible? So that it doesn't break for people with older config.ini files.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately it is not 100% compatible with previous config.ini files. But updating them is pretty easy in most cases. As I said in the PR message :

I hope adding "-> " to the main window title isn't too big of a problem for compatibility. In most cases, I believe adding "*" to the program name in the config file should be sufficient to update them to this new detection system.

For example * - Notepad in the config.ini will not recognise New File - Notepad -> notepad after the update, but writing * - Notepad* will.

Actually adding " -> *" would be even better to update old entries in the config file.
Following the previous example, * - Notepad -> * will recognise correctly New File - Notepad -> notepad and not recognise Weird window name that is not - Notepad, right? -> processName (whereas * - Notepad* would wrongly recognise it)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since there is no auto-update system with this tool nor any promises of future compatibility I believe, I thought the minimal change wasn't a big deal. But maybe I was wrong ?
If its an issue, I don't see how to make this compatible with older config.ini files without making a lot of checks when reading the file. And since those checks would be only for this compatibility with a (by comparison) incomplete version of the same data, that seems like a lot of weight compared to simply adding a few characters to existing entries in previous config files.
But maybe you have a solution for this if its an issue ?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the delay in making a decision. I'm unwilling to break backward compatibility. If you can't see a way to do that, I'll have to turn down this contribution.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've thought about it and I now see a few solutions for this :

  1. As proposed in my previous message : checks when reading the config file lines for the [sources] part and try to detect if it need to compare to the legacy version or the new one. Might be unreliable, adds processing for each read line but allow to have the legacy version and the new one side by side...
  2. Add another category called [Process Source] for example just to compare the process with no link with the source window. This is cleaner but there is no way to differentiate two processes with the same name but from different programs for example. I'm also not sure whether this should take precedence over [source] or not... Not the best solution.
  3. Add a config file version number to detect which version to use (and suppose no version # means 0.14.0 or lower) (my prefered solution for now):
    • a new Changelog file could be added to repository and contain simple directions for anyone wanting to update their config file.
    • we could also add an integrated config file updater in the program for the simpler changes (to easily add the mentionned characters in this case)
    • I have other thing I plan to implement that risk to break compatibility at one point or another so having this system might mitigate them too

However this pull request will never be fully backward compatible though, if that's what you want. Simply because the previous behaviour is wrong (see issue for the details) and this fixes it. So if any config file relied on this bug being present, they would not work the same afterward.
I must admit I'm having trouble grasping why you're unwilling to break backward compatibility exactly, I mean this program hasn't crossed the 1.0.0 mark yet, right ? And the previous releases are not wiped out from the repo either...

I guess if that's still incompatible with you, I could simply do the changes I want on my fork. But I'd be a little bummed out to no longer be able to share back my upgrades to this official repository (which I was really happy to discover when I needed it initially) once my version will have diverged too much from this one. And I fear this might arrive quickly if compatibility with incomplete previous versions is always expected...

Have a good day! <3

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You've won me over with your kind and thoughtful response, and with suggestion of the config file version. We can go ahead and merge this PR, and will wait to create a release until that's in place. This project inherited the ini file from BrowserSelector, and I have often wished I could migrate to something better, like JSON or YAML. Adding a version is a start.

Question. Do you find this syntax intuitive?

* - Notepad -> notepad = ff

Copy link
Contributor Author

@Elihpmap Elihpmap Jan 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you very much ! Sorry, I took a long time to come back to this!
I'm going to do a draft of the Changelog file to start then!

I have often wished I could migrate to something better, like JSON or YAML

I've looked into this too and found that a good upgrade could be TOML since its very close to common .ini syntax. But the smallest library to implement this easily I found was still a 2000 line .cs file so maybe not the best solution. I like the readability of the current file, a YAML parser would be less huge perhaps ?

Question. Do you find this syntax intuitive? * - Notepad -> notepad = ff

I was mainly searching for a delimitter that wouldn't appear often in normal window titles as this could make it harder for users to write string comarison that wouldn't trigger false positive otherwise. -> seemed to fit the role (I suppose most well distributed software would use the character instead in their window title if they really needed an arrow) and I feel like it translate to the idea of "process" pretty well too. But I'm definitely okay to change this if you have a better idea, its definitely possible we could find something more intuitive!

Slack | Test* = chrome
# Source with no window (background processes)
-> AutoHotkey64 = ff
# Default case. Added automatically
# * = whatever

# Url preferences.
# - Only * is treated as a special character (wildcard).
Expand All @@ -117,37 +133,32 @@ edge = C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe
*.youtube.com = chrome
*.visualstudio.com = edge
*.mozilla.org = ff

# Source preferences.
# Only * is treated as a special character (wildcard).
# Matches on window title of application used to open link.
# Applied regardless of any url preference match.
[sources]
* - Notepad = ff
Slack | Test = chrome
# Default case. Added automatically
# * = whatever
```

### Browsers

- Browsers must either be in your path or be fully-qualified paths to the executable e.g. `C:\Program Files (x86)\Google\Chrome\Application\chrome.exe`.
- Browsers must either be :
- fully-qualified paths to the executable e.g. `C:\Program Files (x86)\Google\Chrome\Application\chrome.exe`.
- full path to the executable with environment variable e.g. `%ProgramFiles(x86)%\Google\Chrome\Application\chrome.exe`.
- in your PATH environment variable (you will just have to set the name of the .exe then) e.g. `chrome.exe` with `%ProgramFiles(x86)%\Google\Chrome\Application` added to the PATH variable.
- Arguments are optional. However, if you provide arguments the path _must_ be enclosed in quotes. For example, `"chrome.exe" --new-window`
- If there are no arguments, then the paths do not need to be quoted. For example, `chrome.exe` will work.

### Sources

- You can optionally specify a "source preference" which matches the window title of the application used to open the link.
- You can specify a "source preference" which matches the window title and the process name of the application used to open the link.
- For example, with this in the previous example `config.ini`:

```ini
[sources]
*Microsoft Teams* = ff
* - Notepad -> notepad = ff
```

Then clicking a link in Microsoft Teams will open the link in Firefox, regardless of the URL.
Then clicking a link in Notepad (end of the windows title ending with " - Notepad" with the process named "notepad") will open the link in Firefox, regardless of the URL.

- Wildcards and full regular expressions may be used to match source window titles and process name the same way urls are. [See Urls section](#Urls).

- In the case of a conflict between a source preference and a URL preference, the source preference wins.
- Sources preferences takes precedence over all URLs preferences, so in the case of a conflict between a source preference and a URL preference, the source preference wins.

### Urls

Expand All @@ -171,10 +182,6 @@ There are two ways to specify an Url. You can use simple wildcards or full regul
- The domain _and_ path are used in the Url comparison.
- The regular expression syntax is based on the Microsoft .NET implementation.

### Sources

Wildcards and full regular expressions may also be used to match source window titles.

## Logs

Logs are stored by default in `%localappdata%/BrowseRouter/`. For example, if you user name is `joe`, then the logs will be in `C:\Users\joe\AppData\Local\BrowseRouter\`.
Expand Down
Loading