diff --git a/PRIVACY.md b/PRIVACY.md index 2cad850..f840657 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -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 diff --git a/README.md b/README.md index a045194..6a659b3 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@

- 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 + 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

Download LinkUnbound

@@ -87,6 +87,12 @@ There is no company behind this. I am one developer who got tired of the operati ## What It Does +

+ 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 +

+ +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 @@ -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. @@ -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. @@ -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. --- diff --git a/app/src/i18n.ts b/app/src/i18n.ts index ed8ea1a..b396a2f 100644 --- a/app/src/i18n.ts +++ b/app/src/i18n.ts @@ -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", @@ -403,7 +403,7 @@ const EN: Record = { 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", diff --git a/app/src/settings/About.tsx b/app/src/settings/About.tsx index 464dfaf..987e297 100644 --- a/app/src/settings/About.tsx +++ b/app/src/settings/About.tsx @@ -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 ( <>
@@ -357,14 +362,14 @@ export default function About({ /> )} - {starring && } + {starring && }

{t("aboutSupportWhy")}

- {!starring && ( + {offerStar && ( void openUrl(REPO).catch(noop)} @@ -375,7 +380,7 @@ export default function About({ /> )} - {build?.keptByTheStore && ( + {offerRate && ( { invoke.mockReset(); invoke.mockResolvedValue(null); settled.mockReset(); - render(); + render(); }); it("opens the repository and is never asked for again", async () => { @@ -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(); + }); + + 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"); + }); +}); diff --git a/app/src/settings/Star.tsx b/app/src/settings/Star.tsx index 0c0817b..f931735 100644 --- a/app/src/settings/Star.tsx +++ b/app/src/settings/Star.tsx @@ -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 ( @@ -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")}