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
38 changes: 20 additions & 18 deletions PRIVACY.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,29 +224,31 @@ After these steps, no LinkUnbound data remains on your system.

## Diagnostics Export

LinkUnbound includes an optional **Export diagnostics** feature (Settings → Maintenance) that generates a ZIP file for troubleshooting. This file is created locally and **never sent automatically** — you choose whether and where to share it.
LinkUnbound can write a diagnostic report for troubleshooting: Settings → **About** → *Save report*. The file is written locally and **never sent anywhere** — you decide whether to share it and with whom.

### What the ZIP Contains
It is a single Markdown file named `linkunbound-diagnostico.md`. On Windows it is written to your Desktop; where that folder does not exist, it goes to the system's temporary directory. The message on screen names the exact path it landed in.

| File | Content |
| :-------------------- | :------------------------------------------------------------------------ |
| `system_info.txt` | OS version, locale, app version, executable path, data files |
| `registry.txt` | Windows only — LinkUnbound's own registry entries, those three keys alone |
| `launch_services.txt` | macOS only — the system's URL-handler associations (see the note below) |
| `navigate.log` | Last 200 lines of the navigation log (URLs already redacted) |
### What the report contains

### What the ZIP Does NOT Contain
| Section | Content |
| :-- | :-- |
| Heading | The version of LinkUnbound you are running |
| `Sistema` | Operating system, whether LinkUnbound is registered and default, the health check, how many associations it holds, whether it starts with the system, whether Edge is installed, and the names of the browsers it detected |
| `Preferencias` | Theme, language, the global shortcut, and whether a rule announces itself when it decides |
| `Reglas` | Every rule you have: what it matches, the application it is tied to when it has one, the browser it opens in, and whether it opens privately |

- **Browser list** (`browsers.json`) — not included
- **Domain rules** (`rules.json`) — not included, so no originating app names leave your machine through the export
- **Icons** — not included
- **Actual URLs** — URLs are redacted at the source (log writing), not at export time
### What the report does not contain

**Worth knowing before you share one:** on macOS the export includes
`launch_services.txt`, which lists which application your system uses to open
each URL scheme and file type. That is information about your machine rather
than about your browsing, but it does name other software you have installed.
Open the ZIP and look before attaching it to a public issue.
- **The addresses you open.** It carries your rules, not your browsing.
- **Passwords, tokens or anything from a URL's authority** — those never reach a file at all.
- **Icons, executables or the contents of any browser profile.**

**Worth knowing before you share one:** the rules section names the sites you
wrote rules for and the applications they answer to — `desde ms-teams`, for
example — and the system section lists the browsers installed on the machine.
That is the point of the file, because a rule is usually what decided where a
link went, but it is your information: open it and read it before attaching it
to a public issue.

### URL Redaction

Expand Down
26 changes: 18 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
</p>

<p>
<img src="resources/assets/linkUnbound_2_0_hero_en.png" alt="LinkUnbound: the settings window on the Rules page, and the browser picker that appears next to a link with the browsers, their profiles and the keys 1 to 3" width="92%"/>
<img src="resources/assets/github-en/a-dark.png" alt="LinkUnbound: choose which browser opens every link. The picker sits next to the link, listing Google Chrome, Mozilla Firefox and Vivaldi with the profiles each one carries" width="92%"/>
</p>

<h4>Download LinkUnbound</h4>
Expand Down Expand Up @@ -87,6 +87,12 @@ There is no company behind this. I am one developer who got tired of the operati

## What It Does

<p align="center">
<img src="resources/assets/linkUnbound_2_picker.gif" alt="A link is clicked inside a desktop application; the picker opens beside the cursor naming the address and the application it came from, and the link opens in the browser chosen" width="700"/>
</p>

The picker names the address and the application the link came from, and each browser carries the profile it will open in. The settings window covers the rest: the rules and how they are matched, the browsers it found and the ones you add by hand, the diagnostic report, and what is kept on disk.

- **Registers as default browser** — intercepts every link click system-wide on Windows and macOS
- **Shows a floating picker** near your cursor, in two looks: a classic list or a mosaic of tiles. Pick with the mouse or the keys `1`–`9`; hold **Shift** for a private window; `Ctrl+C` copies the address; `Esc` puts it away
- **Remembers the choice at the reach you pick** — this URL, this subdomain, the whole site, or everything a given app sends
Expand Down Expand Up @@ -188,10 +194,10 @@ Since LinkUnbound is an independent open source project, the installer is signed

- **Links** — whether the system sends links here, what is wrong when it does not and the button that fixes it; on Windows, the associations held
- **Rules** — every rule, the most precise one deciding; change where each opens, add one, remove one
- **Browsers** — what was detected, with profiles; hide, duplicate, or add a custom one with its arguments
- **Browsers** — what was detected, with profiles; hide, duplicate, or add a custom one with its arguments, and look for browsers again after installing one
- **Application** — theme, language, picker look, start at sign-in, the global shortcut, background update checks
- **Maintenance** — export a diagnostic report, rescan browsers, reset the configuration, remove LinkUnbound from the system's list
- **About** — version, licence, the update button, the candidate-versions switch, support links
- **Maintenance** — the two that destroy something: reset the configuration, and remove LinkUnbound from the system's list
- **About** — version, licence, the update button, the candidate-versions switch, the diagnostic report when something goes wrong, and support links

**Updates.** Every six hours the resident asks the release feed, without a window. A newer version shows up as a strip atop the picker and in About; **Update** downloads the signed installer and runs it, and the resident comes back on its own. A copy from the Microsoft Store updates through the Store; a Homebrew copy updates itself the same way as a downloaded one (the cask says `auto_updates`), and `brew upgrade` works as well. Turn the background check off under Application if you prefer to ask by hand.

Expand Down Expand Up @@ -234,7 +240,9 @@ Yes. LinkUnbound detects all browsers registered with the operating system. You
Yes. LinkUnbound unwraps SafeLinks before matching rules, so your rules work on the actual destination — and the picker names it, not the wrapper.

**Why do Teams and Outlook still open Edge?**
Some Microsoft applications hand links straight to Edge down a channel of their own, skipping the default browser, and Windows 11 no longer lets another application stand in the middle of it. When one of those links arrives wrapped in a SafeLink it is unwrapped; the rest is Microsoft's to change.
Both carry a setting of their own that sends links to whatever your default browser is, and that is the first thing to change: in Teams it lives under *Settings → Files and links*, and in classic Outlook under *File → Options → Advanced → «Open hyperlinks from Outlook in»*. Microsoft moves these menus about, so look for «link open preference» or «hyperlinks» if they are not where this says. Set that way, their links reach LinkUnbound like anybody else's.

What is left after that is a channel of Microsoft's own that hands links straight to Edge, skipping the default browser altogether, and Windows 11 no longer lets another application stand in the middle of it. When one of those arrives wrapped in a SafeLink it is unwrapped; the rest is Microsoft's to change.

**I do not see the tray icon on Windows.**
It is there, in the hidden icons behind the `^` at the right of the taskbar: Windows keeps every new application's icon in that overflow until you drag it onto the bar, or promote it under Settings → Personalization → Taskbar → Other system tray icons. Closing the settings window never stops the resident; only *Exit* in the tray menu does.
Expand Down Expand Up @@ -301,14 +309,16 @@ Other browser pickers, so you can pick the one that fits. Platform and licence c
| Project | Platform | Licence |
| :-- | :-- | :-- |
| [Finicky](https://github.com/johnste/finicky) | macOS | MIT |
| [Browserosaurus](https://github.com/will-stone/browserosaurus) | macOS | GPL-3.0, archived by its author |
| [Browserino](https://github.com/AlexStrNik/Browserino) | macOS | GPL-3.0 |
| [Browserosaurus](https://github.com/will-stone/browserosaurus) | macOS | GPL-3.0, no longer maintained |
| [BrowserPicker](https://github.com/mortenn/BrowserPicker) | Windows | MIT |
| [Browser Tamer](https://github.com/aloneguid/bt) | Windows, Linux | Apache-2.0 |
| [Junction](https://github.com/sonnyp/Junction) | Linux | GPL-3.0 |
| [Choosy](https://www.choosyosx.com/) | macOS, Windows | Paid |
| [Switchbar](https://switchbar.app/) | Windows, macOS | Freemium, closed source |
| [Choosy](https://choosy.app/) | macOS | Paid |
| [Velja](https://sindresorhus.com/velja) | macOS | Free, closed source |

What LinkUnbound does that most of these do not: **one build for Windows and macOS**, rules that match **the app a link came from** and not only its address, **browser profiles** detected on their own, and signed automatic updates. Finicky is configured in a file and is macOS only; Browserosaurus is no longer maintained; Junction is Linux only.
What LinkUnbound does that most of these do not: **one build for Windows and macOS**, rules that match **the app a link came from** and not only its address, **browser profiles** detected on their own, and signed automatic updates. Finicky is configured in a file and is macOS only; Junction is Linux only; Browserosaurus now points its own readers at Browserino, which is macOS only too.

---

Expand Down
4 changes: 2 additions & 2 deletions app/src/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ const ES = {
reportWhat:
"El informe reúne en un archivo la versión de LinkUnbound, tus reglas y qué sistema usas, sin las direcciones que visitas.",
reportStays: "No se envía a ninguna parte.",
reportPick: "Se guarda donde tú elijas y solo lo adjuntas si quieres.",
reportPick: "Queda en tu equipo y solo lo adjuntas si quieres.",
reportGo: "Guardar informe…",
reportSaved: "Guardado en {}",
rescanTitle: "Volver a buscar navegadores",
Expand Down Expand Up @@ -403,7 +403,7 @@ const EN: Record<Key, string> = {
reportWhat:
"The report gathers into one file LinkUnbound's version, your rules and which system you are on, without the addresses you visit.",
reportStays: "It is never sent anywhere.",
reportPick: "It is saved where you choose, and you attach it only if you want to.",
reportPick: "It stays on your machine, and you attach it only if you want to.",
reportGo: "Save report…",
reportSaved: "Saved to {}",
rescanTitle: "Look for browsers again",
Expand Down
13 changes: 9 additions & 4 deletions app/src/settings/About.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,11 @@ export default function About({

useEffect(look, [look]);

const store = build?.keptByTheStore ?? false;
const offerStar = !starring || store;
const offerRate = store && !starring;
const lopsided = (offerStar ? 1 : 0) + (offerRate ? 1 : 0) === 1;

return (
<>
<div className="brow">
Expand Down Expand Up @@ -357,14 +362,14 @@ export default function About({
/>
)}

{starring && <Star onSettled={onStarSettled} />}
{starring && <Star store={store} onSettled={onStarSettled} />}

<Rule said={t("aboutSponsorSection")} />
<p className="quiet">{t("aboutSupportWhy")}</p>
<div className="gives">
{!starring && (
{offerStar && (
<Gives
wide={!build?.keptByTheStore}
wide={lopsided}
said={t("aboutStar")}
where="github.com/rgdevment/LinkUnbound"
onPick={() => void openUrl(REPO).catch(noop)}
Expand All @@ -375,7 +380,7 @@ export default function About({
/>
</Gives>
)}
{build?.keptByTheStore && (
{offerRate && (
<Gives
said={t("aboutRate")}
where="Microsoft Store"
Expand Down
30 changes: 29 additions & 1 deletion app/src/settings/Star.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ describe("the card that asks for a star", () => {
invoke.mockReset();
invoke.mockResolvedValue(null);
settled.mockReset();
render(<Star onSettled={settled} />);
render(<Star store={false} onSettled={settled} />);
});

it("opens the repository and is never asked for again", async () => {
Expand All @@ -53,3 +53,31 @@ describe("the card that asks for a star", () => {
expect(settled).toHaveBeenCalled();
});
});

/// The star does nothing for a copy the Store sold: what decides whether anybody searching the
/// Store ever sees this is the rating, and that is what such a copy has to be asked for.
describe("the same card in a copy the Store sold", () => {
beforeEach(() => {
opened.length = 0;
invoke.mockReset();
invoke.mockResolvedValue(null);
settled.mockReset();
render(<Star store onSettled={settled} />);
});

it("asks for the rating, and opens the Store rather than the repository", async () => {
expect(screen.queryByRole("button", { name: "Dar una estrella" })).toBeNull();
await userEvent.click(said("Valorar en la Store"));

expect(opened).toEqual(["ms-windows-store://review/?ProductId=9N9F7C8Q43KC"]);
expect(invoke).toHaveBeenCalledWith("star_done");
expect(settled).toHaveBeenCalled();
});

it("is retired the same way when the answer is no", async () => {
await userEvent.click(said("No mostrar más"));

expect(opened).toEqual([]);
expect(invoke).toHaveBeenCalledWith("star_done");
});
});
9 changes: 6 additions & 3 deletions app/src/settings/Star.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@ import { openUrl } from "@tauri-apps/plugin-opener";
import { useWords } from "../i18n";

const REPO = "https://github.com/rgdevment/LinkUnbound";
const RATING = "ms-windows-store://review/?ProductId=9N9F7C8Q43KC";

export default function Star({ onSettled }: { onSettled: () => void }) {
/// A copy the Store sold is asked for the rating instead of the star: the rating is what decides
/// whether anybody searching the Store ever sees this, and the star does nothing there.
export default function Star({ store, onSettled }: { store: boolean; onSettled: () => void }) {
const t = useWords();

const settle = (open: boolean) => {
onSettled();
void invoke("star_done").catch(noop);
if (open) void openUrl(REPO).catch(noop);
if (open) void openUrl(store ? RATING : REPO).catch(noop);
};

return (
Expand Down Expand Up @@ -39,7 +42,7 @@ export default function Star({ onSettled }: { onSettled: () => void }) {
onClick={() => settle(true)}
className="rounded-md bg-[#2F62D8] px-3 py-1.5 text-[11.5px] font-medium text-white dark:bg-[#6E9BFF] dark:text-[#12141B]"
>
{t("starGo")}
{t(store ? "aboutRate" : "starGo")}
</button>
<button
type="button"
Expand Down
Binary file added resources/assets/github-en/a-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added resources/assets/github-en/b-blue.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added resources/assets/github-es/a-oscuro.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added resources/assets/github-es/b-azul.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added resources/assets/linkUnbound_2_picker.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added resources/assets/store-en/01-picker.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added resources/assets/store-en/02-rules.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added resources/assets/store-en/03-privacy.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added resources/assets/store-es/01-selector.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added resources/assets/store-es/02-reglas.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added resources/assets/store-es/03-privacidad.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading