From b0bdea5ba9b4bccbf0cf2426fc05e42693734189 Mon Sep 17 00:00:00 2001 From: svenhofman <70135014+svenhofman@users.noreply.github.com> Date: Fri, 3 Jan 2025 09:50:02 +0100 Subject: [PATCH] Update mullvad extension (#15981) * Update mullvad extension - Support Reconnecting via the Mullvad CLI, Fixed HUD message display - Initial commit * Fixed code style issues * Update mullvad extension - Cleanup - Added frequency sorting location/server list - Add option to select specific server - Removed test code - Initial commit * updated changelog * Update CHANGELOG.md and optimise images --------- Co-authored-by: raycastbot --- extensions/mullvad/CHANGELOG.md | 2 + extensions/mullvad/src/selectLocation.tsx | 103 ++++++++++++++++------ 2 files changed, 79 insertions(+), 26 deletions(-) diff --git a/extensions/mullvad/CHANGELOG.md b/extensions/mullvad/CHANGELOG.md index b53f343d1a8..c77014bfa1f 100644 --- a/extensions/mullvad/CHANGELOG.md +++ b/extensions/mullvad/CHANGELOG.md @@ -1,5 +1,7 @@ # Mullvad Changelog +## [Added ability to select specific server, Added freceny sorting] - 2025-01-03 + ## [Support Reconnecting via the Mullvad CLI, Fixed HUD message display] - 2024-10-25 ## [Support Connecting, Disconnecting and Selecting Target Locations via the Mullvad CLI] - 2024-01-20 diff --git a/extensions/mullvad/src/selectLocation.tsx b/extensions/mullvad/src/selectLocation.tsx index 1854cd8a6d1..8acb0662da0 100644 --- a/extensions/mullvad/src/selectLocation.tsx +++ b/extensions/mullvad/src/selectLocation.tsx @@ -1,8 +1,6 @@ import { Action, ActionPanel, Detail, List, PopToRootType, showHUD } from "@raycast/api"; -import { showFailureToast, useExec } from "@raycast/utils"; -import { exec, execSync } from "child_process"; -import { useState } from "react"; -import { promisify } from "node:util"; +import { showFailureToast, useExec, useFrecencySorting } from "@raycast/utils"; +import { execSync } from "child_process"; import { mullvadNotInstalledHint } from "./utils"; type Location = { @@ -11,23 +9,37 @@ type Location = { city: string; cityCode: string; id: string; + servers: { id: string }[]; }; const countryRegex = /^(?.+)\s\((?.+)\)/; const cityRegex = /^(?.+)\s\((?.+)\)/; +const serverRegex = /^(?.+?)\s/; function parseRelayList(rawRelayList: string): Location[] { /* eslint-disable @typescript-eslint/no-non-null-assertion */ const locations: Location[] = []; let currentCountry; let currentCountryCode; - if (rawRelayList) - for (const line of rawRelayList.split("\n")) { - if (line.startsWith("\t\t")) continue; + let currentServerList: { id: string }[] = []; + if (rawRelayList) { + const lines = rawRelayList.split("\n"); + let i = 0; + while (i < lines.length) { + const line = lines[i]; if (line.startsWith("\t")) { const cityMatch = line.trim().match(cityRegex); if (cityMatch) { + while (i + 1 < lines.length && lines[i + 1].startsWith("\t\t")) { + const serverMatch = lines[i + 1].trim().match(serverRegex); + if (serverMatch) { + const { server } = serverMatch.groups!; + currentServerList.push({ id: server }); + } + i++; + } + const { city, cityCode } = cityMatch.groups!; locations.push({ country: currentCountry!, @@ -35,38 +47,77 @@ function parseRelayList(rawRelayList: string): Location[] { city, cityCode, id: `${currentCountryCode!}/${cityCode}`, + servers: currentServerList, }); + currentServerList = []; + } + } else { + const countryMatch = line.match(countryRegex); + if (countryMatch) { + const { country, countryCode } = countryMatch.groups!; + currentCountry = country; + currentCountryCode = countryCode; } - continue; - } - - const countryMatch = line.match(countryRegex); - if (countryMatch) { - const { country, countryCode } = countryMatch.groups!; - currentCountry = country; - currentCountryCode = countryCode; } + i++; } + } /* eslint-enable @typescript-eslint/no-non-null-assertion */ - return locations; } +function ServerList({ + location, + visitLocation, +}: { + location: Location; + visitLocation: (item: Location) => Promise; +}) { + const { data: sortedServers, visitItem: visitServer } = useFrecencySorting(location.servers); + + async function setServer(server: { id: string }) { + visitLocation(location); + // If we call visitServer directly afterwards, it won't update both frequencies + setTimeout(() => visitServer(server), 10); + + execSync(`mullvad relay set location ${server.id}`); + + await showHUD("Location changed", { clearRootSearch: true, popToRootType: PopToRootType.Immediate }); + } + + return ( + + {sortedServers.map((server) => ( + + setServer(server).catch(showFailureToast)} /> + + } + /> + ))} + + ); +} + export default function Command() { const isMullvadInstalled = useExec("mullvad", ["version"]); const rawRelayList = useExec("mullvad", ["relay", "list"], { execute: !!isMullvadInstalled.data }); - const [selectedLocation, setSelectedLocation] = useState(null); + + const locations = rawRelayList.data ? parseRelayList(rawRelayList.data) : []; + const { data: sortedLocations, visitItem: visitLocation } = useFrecencySorting(locations); if (rawRelayList.isLoading || isMullvadInstalled.isLoading) return ; if (!isMullvadInstalled.data || isMullvadInstalled.error) return ; if (rawRelayList.error) return ; if (!rawRelayList.data) throw new Error("Couldn't fetch list of relays"); - const locations = parseRelayList(rawRelayList.data); - - async function setLocation() { - if (!selectedLocation) return; - const [countryCode, cityCode] = selectedLocation.split("/"); + async function setLocation(location: Location) { + const [countryCode, cityCode] = location.id.split("/"); + visitLocation(location); execSync(`mullvad relay set location ${countryCode} ${cityCode}`); @@ -74,8 +125,8 @@ export default function Command() { } return ( - - {locations.map((l) => ( + + {sortedLocations.map((l) => ( @@ -94,7 +144,8 @@ export default function Command() { } actions={ - setLocation().catch(showFailureToast)} /> + setLocation(l).catch(showFailureToast)} /> + } /> } />