Compare commits

...
Author SHA1 Message Date
陈大猫 545bba1783 chore: retrigger CI after lint-and-test runner shutdown (#4205) 2026-11-10 18:52:15 +08:00
陈大猫 2d83553e3c fix(win): register Default Apps capabilities for ssh:// links 2026-11-10 15:50:05 +08:00
陈大猫 ad05867026 fix(linux): use Secret Service keyring on non-GNOME/KDE desktops (#4190)
Chromium only enables gnome-libsecret or kwallet when it recognizes a
GNOME/KDE desktop name, so niri/sway/Hyprland fall through to BASIC_TEXT
even when org.freedesktop.secrets is available. Set --password-store
before app ready when Secret Service is present, without overriding an
explicit switch or KDE's kwallet default.
2026-11-09 09:29:59 +08:00
github-actions[bot]andgithub-actions[bot] e72d6fc7c6 chore: update star history charts [skip ci] (#4193)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-09 00:07:23 +00:00
陈大猫 907f60616b fix(nix): work around bwrap ssh config owner check for home-manager (#4192)
* fix(nix): work around bwrap ssh config owner check for home-manager

Unprivileged wrapType2/bwrap maps only the caller uid, so a home-manager
~/.ssh/config symlink into the nix store appears as overflowuid 65534.
OpenSSH then rejects the default user config and the Mosh handshake
never sees MOSH CONNECT. Inject an ssh wrapper via extraPkgs that
snapshots a badly owned config and execs nixpkgs OpenSSH with -F.

* fix(nix): always refresh ssh config snapshot for home-manager

Nix store files have normalized old mtimes, so a -nt check never
notices a newer home-manager generation after the first snapshot.

* fix(nix): publish ssh config snapshot atomically

cp truncates the destination before copying, so a concurrent ssh -F
reader can see an empty or partial config and ignore HostName,
identity, or proxy settings. Copy to a unique staging file and
rename it over the snapshot.
2026-11-09 01:46:36 +08:00
陈大猫 d536b22dee fix(host-details): show inherited group proxy and jump hosts (#4188)
* fix(host-details): show inherited group proxy and jump hosts

Host editor summaries for HTTP/SOCKS5/command proxy and ProxyJump
chains now display the same values applyGroupDefaults uses at
connect time, without writing inherited settings onto the host
until the user edits them.

Fixes #4187

* fix(host-details): keep custom proxy and empty jump-host overrides

Selecting Custom while a group saved proxy is inherited now keeps a host-owned draft instead of snapping back to that profile. Removing the last hop of a multi-hop inherited chain keeps an empty override so applyGroupDefaults cannot restore the group hops.

* fix(host-details): keep empty override when clearing inherited chain

Clear Chain on an inherited-only jump-host list now stores { hostIds: [] }
instead of deleting the field so group hops cannot snap back.
2026-11-08 21:30:36 +08:00
陈大猫 74d5e126d4 fix(system-manager): show all CPU cores in the overview list (#4184)
* fix(system-manager): show all CPU cores in the overview list

The overview CPU card hardcoded cpuPerCore.slice(0, 12), so a 16-thread
host showed "16 核" at the top but only C1–C12 below. Render every
reported core and allow the list to scroll when it overflows.

Fixes #4183

* ci: retrigger lint-and-test after plugin-contract runner shutdown
2026-11-08 12:59:27 +08:00
14 changed files with 765 additions and 82 deletions

No files matched your search

+30
View File
@@ -22,9 +22,39 @@
WriteRegStr SHCTX "Software\Classes\Directory\Background\shell\Netcatty" "MUIVerb" "Open in Netcatty"
WriteRegStr SHCTX "Software\Classes\Directory\Background\shell\Netcatty" "Icon" "$INSTDIR\${APP_EXECUTABLE_FILENAME},0"
WriteRegStr SHCTX "Software\Classes\Directory\Background\shell\Netcatty\command" "" '"$INSTDIR\${APP_EXECUTABLE_FILENAME}" -- --open-terminal-path="%V."'
; #4204: Windows Default Apps requires Capabilities/RegisteredApplications
; to list URL handlers; Electron's bare scheme keys are not sufficient.
WriteRegStr SHCTX "Software\Classes\Netcatty.SSH" "" "URL:ssh Protocol"
WriteRegStr SHCTX "Software\Classes\Netcatty.SSH" "URL Protocol" ""
WriteRegStr SHCTX "Software\Classes\Netcatty.SSH\DefaultIcon" "" '"$INSTDIR\${APP_EXECUTABLE_FILENAME}",0'
WriteRegStr SHCTX "Software\Classes\Netcatty.SSH\shell\open\command" "" '"$INSTDIR\${APP_EXECUTABLE_FILENAME}" "%1"'
WriteRegStr SHCTX "Software\Classes\Netcatty.Telnet" "" "URL:telnet Protocol"
WriteRegStr SHCTX "Software\Classes\Netcatty.Telnet" "URL Protocol" ""
WriteRegStr SHCTX "Software\Classes\Netcatty.Telnet\DefaultIcon" "" '"$INSTDIR\${APP_EXECUTABLE_FILENAME}",0'
WriteRegStr SHCTX "Software\Classes\Netcatty.Telnet\shell\open\command" "" '"$INSTDIR\${APP_EXECUTABLE_FILENAME}" "%1"'
WriteRegStr SHCTX "Software\Classes\Netcatty.JMS" "" "URL:jms Protocol"
WriteRegStr SHCTX "Software\Classes\Netcatty.JMS" "URL Protocol" ""
WriteRegStr SHCTX "Software\Classes\Netcatty.JMS\DefaultIcon" "" '"$INSTDIR\${APP_EXECUTABLE_FILENAME}",0'
WriteRegStr SHCTX "Software\Classes\Netcatty.JMS\shell\open\command" "" '"$INSTDIR\${APP_EXECUTABLE_FILENAME}" "%1"'
WriteRegStr SHCTX "Software\Netcatty\Capabilities" "ApplicationName" "Netcatty"
WriteRegStr SHCTX "Software\Netcatty\Capabilities" "ApplicationDescription" "Open SSH, Telnet and JMS links in Netcatty."
WriteRegStr SHCTX "Software\Netcatty\Capabilities" "ApplicationIcon" '"$INSTDIR\${APP_EXECUTABLE_FILENAME}",0'
WriteRegStr SHCTX "Software\Netcatty\Capabilities\URLAssociations" "ssh" "Netcatty.SSH"
WriteRegStr SHCTX "Software\Netcatty\Capabilities\URLAssociations" "telnet" "Netcatty.Telnet"
WriteRegStr SHCTX "Software\Netcatty\Capabilities\URLAssociations" "jms" "Netcatty.JMS"
WriteRegStr SHCTX "Software\RegisteredApplications" "Netcatty" "Software\Netcatty\Capabilities"
!macroend
!macro customUnInstall
DeleteRegKey SHCTX "Software\Classes\Directory\shell\Netcatty"
DeleteRegKey SHCTX "Software\Classes\Directory\Background\shell\Netcatty"
DeleteRegKey SHCTX "Software\Classes\Netcatty.SSH"
DeleteRegKey SHCTX "Software\Classes\Netcatty.Telnet"
DeleteRegKey SHCTX "Software\Classes\Netcatty.JMS"
DeleteRegKey SHCTX "Software\Netcatty\Capabilities"
DeleteRegValue SHCTX "Software\RegisteredApplications" "Netcatty"
!macroend
+19 -4
View File
@@ -49,11 +49,13 @@ export const HostDetailsAdvancedSections: React.FC<HostDetailsAdvancedSectionsPr
showAlgorithmOverrides,
setShowAlgorithmOverrides,
chainedHosts,
hostChainInherited = false,
setActiveSubPanel,
clearHostChain,
proxySummaryType,
proxySummaryLabel,
proxySummaryTooltip,
proxyInherited = false,
clearProxyConfig,
}) => {
const inheritedDeviceType = effectiveGroupDefaults?.deviceType;
@@ -641,9 +643,16 @@ export const HostDetailsAdvancedSections: React.FC<HostDetailsAdvancedSectionsPr
title={t("hostDetails.jumpHosts")}
action={
chainedHosts.length > 0 ? (
<Badge variant="secondary" className="text-xs">
{t("hostDetails.jumpHosts.hops", { count: chainedHosts.length })}
</Badge>
<div className="flex items-center gap-1">
{hostChainInherited && (
<Badge variant="outline" className="text-xs text-muted-foreground">
{t("vault.groups.details.inherited")}
</Badge>
)}
<Badge variant="secondary" className="text-xs">
{t("hostDetails.jumpHosts.hops", { count: chainedHosts.length })}
</Badge>
</div>
) : (
<Badge variant="outline" className="text-xs text-muted-foreground">
{t("hostDetails.jumpHosts.direct")}
@@ -664,8 +673,10 @@ export const HostDetailsAdvancedSections: React.FC<HostDetailsAdvancedSectionsPr
/>
<span className="text-xs text-muted-foreground">
{t("hostDetails.jumpHosts.hops", { count: chainedHosts.length })}
{hostChainInherited ? ` · ${t("vault.groups.details.inherited")}` : ""}
</span>
</div>
{!hostChainInherited && (
<X
size={14}
className="text-muted-foreground hover:text-destructive flex-shrink-0"
@@ -674,6 +685,7 @@ export const HostDetailsAdvancedSections: React.FC<HostDetailsAdvancedSectionsPr
clearHostChain();
}}
/>
)}
</div>
<div className="w-full space-y-1 pl-5">
{chainedHosts.slice(0, 5).map((h, idx) => (
@@ -735,7 +747,7 @@ export const HostDetailsAdvancedSections: React.FC<HostDetailsAdvancedSectionsPr
<X size={14} />
</Button>
</div>
) : form.proxyConfig?.host || form.proxyConfig?.command || form.proxyProfileId ? (
) : form.proxyConfig?.host || form.proxyConfig?.command || form.proxyProfileId || proxyInherited ? (
<div className="w-full min-w-0 grid grid-cols-[minmax(0,1fr)_auto] items-center gap-1">
<button
type="button"
@@ -750,6 +762,7 @@ export const HostDetailsAdvancedSections: React.FC<HostDetailsAdvancedSectionsPr
<TooltipTrigger asChild>
<span className="block min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-sm">
{proxySummaryLabel}
{proxyInherited ? ` · ${t("vault.groups.details.inherited")}` : ""}
</span>
</TooltipTrigger>
<TooltipContent side="bottom" align="start" className="max-w-xs break-all">
@@ -758,6 +771,7 @@ export const HostDetailsAdvancedSections: React.FC<HostDetailsAdvancedSectionsPr
</Tooltip>
</TooltipProvider>
</button>
{!proxyInherited && (
<Button
type="button"
variant="ghost"
@@ -768,6 +782,7 @@ export const HostDetailsAdvancedSections: React.FC<HostDetailsAdvancedSectionsPr
>
<X size={14} />
</Button>
)}
</div>
) : (
<Button
+67
View File
@@ -1,4 +1,5 @@
import type { GroupConfig } from "../domain/models";
import { applyGroupDefaults } from "../domain/groupConfig";
import {
findProxyProfile,
hasIncompleteProxyIdentity,
@@ -157,3 +158,69 @@ export const prepareProxyConfigForSave = (params: {
normalizedProxyConfig: normalizeManualProxyConfig(params.proxyConfig),
};
};
export const EMPTY_CUSTOM_PROXY_DRAFT: ProxyConfig = { type: "http", host: "", port: 8080 };
export const resolveDisplayProxyProfileId = (
form: Pick<Host, "directProxy" | "proxyProfileId" | "proxyConfig">,
inheritedProfileId?: string,
): string | undefined => {
if (form.directProxy || form.proxyConfig !== undefined) return form.proxyProfileId;
return form.proxyProfileId ?? inheritedProfileId;
};
export const nextFormAfterSelectProxyProfile = (
prev: Host,
profileId: string | undefined,
groupDefaults?: Partial<GroupConfig>,
): Host => {
const { proxyConfig: _proxyConfig, proxyProfileId: _proxyProfileId, directProxy: _directProxy, ...rest } = prev;
if (!profileId) {
const inheritedConfig = !prev.directProxy
&& prev.proxyProfileId === undefined
&& prev.proxyConfig === undefined
&& groupDefaults
? applyGroupDefaults(prev, groupDefaults).proxyConfig
: undefined;
return {
...rest,
proxyConfig: prev.proxyConfig ?? inheritedConfig ?? EMPTY_CUSTOM_PROXY_DRAFT,
} as Host;
}
return { ...rest, proxyProfileId: profileId } as Host;
};
export const resolveFormHostChainIds = (
host: Host,
groupDefaults?: Partial<GroupConfig>,
): string[] => {
if (host.hostChain !== undefined) return host.hostChain.hostIds || [];
if (!groupDefaults) return [];
return applyGroupDefaults(host, groupDefaults).hostChain?.hostIds || [];
};
export const nextFormAfterClearHostChain = (
prev: Host,
groupDefaults?: Partial<GroupConfig>,
): Host => {
const groupWouldRestore = Boolean(
groupDefaults
&& applyGroupDefaults({ ...prev, hostChain: undefined }, groupDefaults).hostChain?.hostIds?.length,
);
if (prev.hostChain === undefined || groupWouldRestore) {
return { ...prev, hostChain: { hostIds: [] } };
}
const { hostChain: _hostChain, ...rest } = prev;
return rest as Host;
};
export const nextFormAfterRemoveHostFromChain = (
prev: Host,
index: number,
groupDefaults?: Partial<GroupConfig>,
): Host => {
const ids = resolveFormHostChainIds(prev, groupDefaults).filter((_, i) => i !== index);
if (ids.length > 0) return { ...prev, hostChain: { hostIds: ids } };
return nextFormAfterClearHostChain(prev, groupDefaults);
};
@@ -6,12 +6,17 @@ import { renderToStaticMarkup } from "react-dom/server";
import { I18nProvider } from "../application/i18n/I18nProvider.tsx";
import type { Host, Identity } from "../types.ts";
import HostDetailsPanel, { parseOptionalPortInput } from "./HostDetailsPanel.tsx";
import { applyGroupDefaults } from "../domain/groupConfig.ts";
import {
prepareProxyConfigForSave,
prepareTelnetCredentialsForSave,
resolvePrimaryProtocolSavePort,
resolvePrimaryProtocolSwitchPort,
validateProxyConfigForSave,
nextFormAfterSelectProxyProfile,
nextFormAfterClearHostChain,
nextFormAfterRemoveHostFromChain,
resolveDisplayProxyProfileId,
} from "./HostDetailsPanel.helpers.ts";
import { TooltipProvider } from "./ui/tooltip.tsx";
@@ -549,3 +554,198 @@ test("HostDetailsPanel expands notes when the host already has notes", () => {
"expected notes to render after connection settings",
);
});
const networkSshHost: Host = {
...hostWithMissingProxyProfile,
proxyProfileId: undefined,
group: "network",
};
const jumpHost: Host = {
id: "jump-1",
label: "Bastion",
hostname: "bastion.example.com",
username: "root",
tags: [],
os: "linux",
port: 22,
protocol: "ssh",
authMethod: "password",
createdAt: 1,
};
const hostOwnedJump: Host = {
...jumpHost,
id: "jump-host",
label: "Edge",
hostname: "edge.example.com",
};
const renderHostDetailsWithGroup = (
initialData: Host,
groupConfig: Record<string, unknown>,
extraHosts: Host[] = [],
) =>
renderToStaticMarkup(
React.createElement(
I18nProvider,
{ locale: "en" },
React.createElement(
TooltipProvider,
null,
React.createElement(HostDetailsPanel, {
initialData,
availableKeys: [],
identities: [],
proxyProfiles: [],
groups: ["network"],
managedSources: [],
allTags: [],
allHosts: extraHosts,
terminalThemeId: "default",
terminalFontSize: 14,
groupConfigs: [{ path: "network", ...groupConfig }],
onSave: () => {},
onCancel: () => {},
}),
),
),
);
test("HostDetailsPanel displays inherited group HTTP proxy", () => {
const markup = renderHostDetailsWithGroup(networkSshHost, {
proxyConfig: { type: "http", host: "group-proxy.example.com", port: 8080 },
});
assert.match(markup, /group-proxy\.example\.com/);
assert.match(markup, /Inherited from group/);
});
test("HostDetailsPanel host proxy override wins over the group proxy", () => {
const markup = renderHostDetailsWithGroup({
...networkSshHost,
proxyConfig: { type: "http", host: "host-proxy.example.com", port: 9090 },
}, {
proxyConfig: { type: "http", host: "group-proxy.example.com", port: 8080 },
});
assert.match(markup, /host-proxy\.example\.com/);
assert.doesNotMatch(markup, /group-proxy\.example\.com/);
assert.doesNotMatch(markup, /Inherited from group/);
});
test("HostDetailsPanel direct proxy hides inherited group proxy", () => {
const markup = renderHostDetailsWithGroup({
...networkSshHost,
directProxy: true,
}, {
proxyConfig: { type: "http", host: "group-proxy.example.com", port: 8080 },
});
assert.match(markup, /Direct \(no proxy\)/);
assert.doesNotMatch(markup, /group-proxy\.example\.com/);
});
test("HostDetailsPanel displays inherited group jump hosts", () => {
const markup = renderHostDetailsWithGroup(networkSshHost, {
hostChain: { hostIds: ["jump-1"] },
}, [jumpHost]);
assert.match(markup, /bastion\.example\.com/);
assert.match(markup, /Inherited from group/);
});
test("HostDetailsPanel host jump-host override wins over the group chain", () => {
const markup = renderHostDetailsWithGroup({
...networkSshHost,
hostChain: { hostIds: ["jump-host"] },
}, {
hostChain: { hostIds: ["jump-1"] },
}, [jumpHost, hostOwnedJump]);
assert.match(markup, /edge\.example\.com/);
assert.doesNotMatch(markup, /bastion\.example\.com/);
assert.doesNotMatch(markup, /Inherited from group/);
});
test("selecting Custom while inheriting a saved proxy keeps a custom draft instead of the profile", () => {
const group = { proxyProfileId: "office-proxy" };
const next = nextFormAfterSelectProxyProfile(networkSshHost, undefined, group);
assert.equal(next.proxyProfileId, undefined);
assert.equal(next.directProxy, undefined);
assert.deepEqual(next.proxyConfig, { type: "http", host: "", port: 8080 });
assert.equal(resolveDisplayProxyProfileId(next, "office-proxy"), undefined);
assert.equal(applyGroupDefaults(next, group).proxyProfileId, undefined);
});
test("removing the last hop of a multi-hop inherited chain keeps an empty override", () => {
const group = { hostChain: { hostIds: ["jump-1", "jump-host"] } };
const afterFirst = nextFormAfterRemoveHostFromChain(networkSshHost, 0, group);
assert.deepEqual(afterFirst.hostChain, { hostIds: ["jump-host"] });
const afterLast = nextFormAfterRemoveHostFromChain(afterFirst, 0, group);
assert.deepEqual(afterLast.hostChain, { hostIds: [] });
assert.deepEqual(applyGroupDefaults(afterLast, group).hostChain, { hostIds: [] });
});
test("clearing an inherited-only chain keeps an empty override instead of restoring group hops", () => {
const group = { hostChain: { hostIds: ["jump-1", "jump-host"] } };
const cleared = nextFormAfterClearHostChain(networkSshHost, group);
assert.deepEqual(cleared.hostChain, { hostIds: [] });
assert.deepEqual(applyGroupDefaults(cleared, group).hostChain, { hostIds: [] });
});
test("HostDetailsPanel custom proxy draft hides the inherited saved proxy", () => {
const markup = renderToStaticMarkup(
React.createElement(
I18nProvider,
{ locale: "en" },
React.createElement(
TooltipProvider,
null,
React.createElement(HostDetailsPanel, {
initialData: {
...networkSshHost,
proxyConfig: { type: "http", host: "", port: 8080 },
},
availableKeys: [],
identities: [],
proxyProfiles: [{
id: "office-proxy",
label: "Office Proxy",
config: { type: "http", host: "office-proxy.example.com", port: 8080 },
createdAt: 1,
}],
groups: ["network"],
managedSources: [],
allTags: [],
allHosts: [],
terminalThemeId: "default",
terminalFontSize: 14,
groupConfigs: [{ path: "network", proxyProfileId: "office-proxy" }],
onSave: () => {},
onCancel: () => {},
}),
),
),
);
assert.doesNotMatch(markup, /Office Proxy/);
assert.doesNotMatch(markup, /office-proxy\.example\.com/);
assert.doesNotMatch(markup, /Inherited from group/);
});
test("HostDetailsPanel empty jump-host override hides the inherited group chain", () => {
const markup = renderHostDetailsWithGroup({
...networkSshHost,
hostChain: { hostIds: [] },
}, {
hostChain: { hostIds: ["jump-1", "jump-host"] },
}, [jumpHost, hostOwnedJump]);
assert.doesNotMatch(markup, /bastion\.example\.com/);
assert.doesNotMatch(markup, /edge\.example\.com/);
assert.doesNotMatch(markup, /Inherited from group/);
});
+52 -31
View File
@@ -55,6 +55,11 @@ import {
resolvePrimaryProtocolSwitchPort,
prepareTelnetCredentialsForSave,
prepareProxyConfigForSave,
nextFormAfterSelectProxyProfile,
nextFormAfterClearHostChain,
nextFormAfterRemoveHostFromChain,
resolveFormHostChainIds,
resolveDisplayProxyProfileId,
} from "./HostDetailsPanel.helpers";
export { parseOptionalPortInput } from "./HostDetailsPanel.helpers";
import { TerminalEncodingSelect } from "./TerminalEncodingSelect";
@@ -424,24 +429,34 @@ const HostDetailsPanel: React.FC<HostDetailsPanelPropsWithResize> = ({
);
const effectiveFormDistro = getEffectiveHostDistro(form);
const hasHostProxyOverride = form.directProxy === true
|| form.proxyProfileId !== undefined
|| form.proxyConfig !== undefined;
const displayProxyProfileId = resolveDisplayProxyProfileId(form, effectiveAuthHost.proxyProfileId);
const displayProxyConfig = form.directProxy
? undefined
: (form.proxyConfig ?? effectiveAuthHost.proxyConfig);
const selectedProxyProfile = useMemo(
() => proxyProfiles.find((profile) => profile.id === form.proxyProfileId),
[form.proxyProfileId, proxyProfiles],
() => proxyProfiles.find((profile) => profile.id === displayProxyProfileId),
[displayProxyProfileId, proxyProfiles],
);
const hasMissingProxyProfile = Boolean(form.proxyProfileId && !selectedProxyProfile);
const hasMissingProxyProfile = Boolean(displayProxyProfileId && !selectedProxyProfile);
const proxyInherited = !hasHostProxyOverride && Boolean(
displayProxyProfileId || displayProxyConfig?.host || displayProxyConfig?.command,
) && Boolean(effectiveGroupDefaults?.proxyProfileId || effectiveGroupDefaults?.proxyConfig);
const proxySummaryType = hasMissingProxyProfile
? t("hostDetails.proxyPanel.missing")
: formatProxyConfigType(selectedProxyProfile?.config || form.proxyConfig) || "HTTP";
: formatProxyConfigType(selectedProxyProfile?.config || displayProxyConfig) || "HTTP";
const proxySummaryLabel = hasMissingProxyProfile
? t("hostDetails.proxyPanel.missingSaved")
: selectedProxyProfile
? selectedProxyProfile.label
: formatProxyConfigEndpoint(form.proxyConfig);
: formatProxyConfigEndpoint(displayProxyConfig);
const proxySummaryTooltip = hasMissingProxyProfile
? t("hostDetails.proxyPanel.missingSaved")
: selectedProxyProfile
? `${selectedProxyProfile.label} - ${formatProxyConfigEndpoint(selectedProxyProfile.config)}`
: `${formatProxyConfigType(form.proxyConfig)} ${formatProxyConfigEndpoint(form.proxyConfig)}`;
: `${formatProxyConfigType(displayProxyConfig)} ${formatProxyConfigEndpoint(displayProxyConfig)}`;
const handleDistroModeChange = useCallback((mode: "auto" | "manual") => {
setForm((prev) => ({
@@ -458,13 +473,19 @@ const HostDetailsPanel: React.FC<HostDetailsPanelPropsWithResize> = ({
(field: keyof ProxyConfig, value: ProxyConfig[keyof ProxyConfig]) => {
setForm((prev) => {
const { proxyProfileId: _proxyProfileId, directProxy: _directProxy, ...rest } = prev;
const inheritedConfig = !prev.directProxy
&& prev.proxyProfileId === undefined
&& prev.proxyConfig === undefined
&& effectiveGroupDefaults
? applyGroupDefaults(prev, effectiveGroupDefaults).proxyConfig
: undefined;
return {
...rest,
proxyConfig: updateProxyConfigField(prev.proxyConfig, field, value),
proxyConfig: updateProxyConfigField(prev.proxyConfig ?? inheritedConfig, field, value),
} as Host;
});
},
[],
[effectiveGroupDefaults],
);
const clearProxyConfig = useCallback(() => {
@@ -475,12 +496,8 @@ const HostDetailsPanel: React.FC<HostDetailsPanelPropsWithResize> = ({
}, []);
const selectProxyProfile = useCallback((profileId: string | undefined) => {
setForm((prev) => {
const { proxyConfig: _proxyConfig, proxyProfileId: _proxyProfileId, directProxy: _directProxy, ...rest } = prev;
if (!profileId) return rest as Host;
return { ...rest, proxyProfileId: profileId } as Host;
});
}, []);
setForm((prev) => nextFormAfterSelectProxyProfile(prev, profileId, effectiveGroupDefaults));
}, [effectiveGroupDefaults]);
const selectDirectProxy = useCallback(() => {
setForm((prev) => {
@@ -493,24 +510,18 @@ const HostDetailsPanel: React.FC<HostDetailsPanelPropsWithResize> = ({
setForm((prev) => ({
...prev,
hostChain: {
hostIds: [...(prev.hostChain?.hostIds || []), hostId],
hostIds: [...resolveFormHostChainIds(prev, effectiveGroupDefaults), hostId],
},
}));
};
const removeHostFromChain = (index: number) => {
setForm((prev) => {
const ids = (prev.hostChain?.hostIds || []).filter((_, i) => i !== index);
return { ...prev, hostChain: ids.length > 0 ? { hostIds: ids } : undefined };
});
setForm((prev) => nextFormAfterRemoveHostFromChain(prev, index, effectiveGroupDefaults));
};
const clearHostChain = useCallback(() => {
setForm((prev) => {
const { hostChain: _hostChain, ...rest } = prev;
return rest as Host;
});
}, []);
setForm((prev) => nextFormAfterClearHostChain(prev, effectiveGroupDefaults));
}, [effectiveGroupDefaults]);
const [newEnvName, setNewEnvName] = useState("");
const [newEnvValue, setNewEnvValue] = useState("");
@@ -773,8 +784,16 @@ const HostDetailsPanel: React.FC<HostDetailsPanelPropsWithResize> = ({
setActiveSubPanel("none");
};
const displayHostChainIds = useMemo(
() => form.hostChain !== undefined
? (form.hostChain.hostIds || [])
: (effectiveAuthHost.hostChain?.hostIds || []),
[effectiveAuthHost.hostChain?.hostIds, form.hostChain],
);
const hostChainInherited = form.hostChain === undefined && displayHostChainIds.length > 0;
const availableHostsForChain = useMemo(() => {
const chainedIds = new Set(form.hostChain?.hostIds || []);
const chainedIds = new Set(displayHostChainIds);
// Chain hops are SSH ProxyJump hops; never offer local/serial/telnet/plugin
// entries (e.g. saved local shells) as jump hosts.
return allHosts.filter((h) =>
@@ -782,14 +801,13 @@ const HostDetailsPanel: React.FC<HostDetailsPanelPropsWithResize> = ({
&& !chainedIds.has(h.id)
&& canServeAsSshJumpHost(h),
);
}, [allHosts, form.id, form.hostChain?.hostIds]);
}, [allHosts, displayHostChainIds, form.id]);
const chainedHosts = useMemo(() => {
const ids = form.hostChain?.hostIds || [];
return ids
return displayHostChainIds
.map((id) => allHosts.find((h) => h.id === id))
.filter(Boolean) as Host[];
}, [allHosts, form.hostChain?.hostIds]);
}, [allHosts, displayHostChainIds]);
const groupOptions: ComboboxOption[] = useMemo(() => {
return groups.map((g) => ({
@@ -905,11 +923,12 @@ const HostDetailsPanel: React.FC<HostDetailsPanelPropsWithResize> = ({
if (activeSubPanel === "proxy") {
return (
<ProxyPanel
proxyConfig={form.proxyConfig}
proxyConfig={displayProxyConfig}
proxyProfiles={proxyProfiles}
identities={identities}
selectedProxyProfileId={form.proxyProfileId}
selectedProxyProfileId={displayProxyProfileId}
directProxy={form.directProxy}
allowClear={hasHostProxyOverride}
onUpdateProxy={updateProxyConfig}
onSelectProxyProfile={selectProxyProfile}
onSelectDirectProxy={selectDirectProxy}
@@ -1237,11 +1256,13 @@ const HostDetailsPanel: React.FC<HostDetailsPanelPropsWithResize> = ({
showAlgorithmOverrides={showAlgorithmOverrides}
setShowAlgorithmOverrides={setShowAlgorithmOverrides}
chainedHosts={chainedHosts}
hostChainInherited={hostChainInherited}
setActiveSubPanel={setActiveSubPanel}
clearHostChain={clearHostChain}
proxySummaryType={proxySummaryType}
proxySummaryLabel={proxySummaryLabel}
proxySummaryTooltip={proxySummaryTooltip}
proxyInherited={proxyInherited}
clearProxyConfig={clearProxyConfig}
/>
+3 -1
View File
@@ -34,6 +34,7 @@ export interface ProxyPanelProps {
onClearProxy: () => void;
onBack: () => void;
onCancel: () => void;
allowClear?: boolean;
layout?: AsidePanelLayout;
}
@@ -51,6 +52,7 @@ export const ProxyPanel: React.FC<ProxyPanelPropsWithResize> = ({
onClearProxy,
onBack,
onCancel,
allowClear = true,
layout = 'overlay',
resizable,
persistWidthStorageKey,
@@ -346,7 +348,7 @@ export const ProxyPanel: React.FC<ProxyPanelPropsWithResize> = ({
</>
)}
{(proxyConfig?.host || proxyConfig?.command || selectedProxyProfileId || directProxy) && (
{allowClear && (proxyConfig?.host || proxyConfig?.command || selectedProxyProfileId || directProxy) && (
<Button variant="ghost" className="w-full h-10 text-destructive" onClick={onClearProxy}>
<Trash2 size={14} className="mr-2" /> {t('hostDetails.proxyPanel.remove')}
</Button>
@@ -0,0 +1,13 @@
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
test("overview CPU cores list renders every reported core and can scroll", () => {
const source = readFileSync(new URL("./SystemOverviewTab.tsx", import.meta.url), "utf8");
assert.doesNotMatch(source, /cpuPerCore\.slice\(/);
assert.match(
source,
/className="grid grid-cols-2 gap-x-3 gap-y-1\.5 max-h-\[\d+px\] overflow-y-auto pr-1 overscroll-contain"/,
);
});
@@ -358,8 +358,8 @@ export const SystemOverviewTab = memo(function SystemOverviewTab({
</span>
</div>
{stats.cpuPerCore.length > 0 ? (
<div className="grid grid-cols-2 gap-x-3 gap-y-1.5">
{stats.cpuPerCore.slice(0, 12).map((core, index) => (
<div className="grid grid-cols-2 gap-x-3 gap-y-1.5 max-h-[260px] overflow-y-auto pr-1 overscroll-contain">
{stats.cpuPerCore.map((core, index) => (
<ResourceBar key={`core-${index}`} label={`C${index + 1}`} value={core} />
))}
</div>
+24 -22
View File
@@ -15,33 +15,35 @@
<text x="50%" y="30" fill="#e6edf3" font-size="20" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Star History</text>
<rect width="178" height="32" x="105" y="90" fill="#161b22" fill-opacity="0.9" stroke="#30363d" stroke-width="1.5" rx="4" ry="4" filter="url(#xkcdify)"/><rect width="8" height="8" x="115" y="102" rx="2" ry="2" fill="#dd4528" filter="url(#xkcdify)"/><text x="131" y="110" fill="#e6edf3" font-size="15" font-family="Handlee, cursive" font-weight="bold">binaricat/Netcatty</text>
<text x="85" y="468.0" fill="#e6edf3" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="end">0</text>
<line x1="90" y1="408.3" x2="765" y2="408.3" stroke="#21262d" stroke-width="1" filter="url(#xkcdify)"/>
<text x="85" y="413.2857142857143" fill="#e6edf3" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="end">1K</text>
<line x1="90" y1="353.6" x2="765" y2="353.6" stroke="#21262d" stroke-width="1" filter="url(#xkcdify)"/>
<text x="85" y="358.57142857142856" fill="#e6edf3" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="end">2K</text>
<line x1="90" y1="298.9" x2="765" y2="298.9" stroke="#21262d" stroke-width="1" filter="url(#xkcdify)"/>
<text x="85" y="303.8571428571429" fill="#e6edf3" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="end">3K</text>
<line x1="90" y1="244.1" x2="765" y2="244.1" stroke="#21262d" stroke-width="1" filter="url(#xkcdify)"/>
<text x="85" y="249.14285714285717" fill="#e6edf3" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="end">4K</text>
<line x1="90" y1="189.4" x2="765" y2="189.4" stroke="#21262d" stroke-width="1" filter="url(#xkcdify)"/>
<text x="85" y="194.42857142857144" fill="#e6edf3" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="end">5K</text>
<line x1="90" y1="134.7" x2="765" y2="134.7" stroke="#21262d" stroke-width="1" filter="url(#xkcdify)"/>
<text x="85" y="139.71428571428572" fill="#e6edf3" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="end">6K</text>
<line x1="90" y1="415.1" x2="765" y2="415.1" stroke="#21262d" stroke-width="1" filter="url(#xkcdify)"/>
<text x="85" y="420.125" fill="#e6edf3" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="end">1K</text>
<line x1="90" y1="367.2" x2="765" y2="367.2" stroke="#21262d" stroke-width="1" filter="url(#xkcdify)"/>
<text x="85" y="372.25" fill="#e6edf3" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="end">2K</text>
<line x1="90" y1="319.4" x2="765" y2="319.4" stroke="#21262d" stroke-width="1" filter="url(#xkcdify)"/>
<text x="85" y="324.375" fill="#e6edf3" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="end">3K</text>
<line x1="90" y1="271.5" x2="765" y2="271.5" stroke="#21262d" stroke-width="1" filter="url(#xkcdify)"/>
<text x="85" y="276.5" fill="#e6edf3" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="end">4K</text>
<line x1="90" y1="223.6" x2="765" y2="223.6" stroke="#21262d" stroke-width="1" filter="url(#xkcdify)"/>
<text x="85" y="228.625" fill="#e6edf3" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="end">5K</text>
<line x1="90" y1="175.8" x2="765" y2="175.8" stroke="#21262d" stroke-width="1" filter="url(#xkcdify)"/>
<text x="85" y="180.75" fill="#e6edf3" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="end">6K</text>
<line x1="90" y1="127.9" x2="765" y2="127.9" stroke="#21262d" stroke-width="1" filter="url(#xkcdify)"/>
<text x="85" y="132.875" fill="#e6edf3" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="end">7K</text>
<line x1="90" y1="80.0" x2="765" y2="80.0" stroke="#21262d" stroke-width="1" filter="url(#xkcdify)"/>
<text x="85" y="85.0" fill="#e6edf3" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="end">7K</text>
<text x="85" y="85.0" fill="#e6edf3" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="end">8K</text>
<text x="43" y="271.5" fill="#e6edf3" font-size="17" font-family="Handlee, cursive" font-weight="bold" transform="rotate(-90, 43, 271.5)">GitHub Stars</text>
<text x="90.0" y="488" fill="#e6edf3" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Jan 2026</text>
<text x="217.4" y="488" fill="#e6edf3" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Mar 2026</text>
<text x="353.7" y="488" fill="#e6edf3" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">May 2026</text>
<text x="490.1" y="488" fill="#e6edf3" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Jul 2026</text>
<text x="628.7" y="488" fill="#e6edf3" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Sep 2026</text>
<text x="765.0" y="488" fill="#e6edf3" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Nov 2026</text>
<text x="214.5" y="488" fill="#e6edf3" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Mar 2026</text>
<text x="347.8" y="488" fill="#e6edf3" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">May 2026</text>
<text x="481.0" y="488" fill="#e6edf3" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Jul 2026</text>
<text x="616.5" y="488" fill="#e6edf3" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Sep 2026</text>
<text x="749.7" y="488" fill="#e6edf3" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Nov 2026</text>
<text x="50%" y="525" fill="#e6edf3" font-size="17" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Date</text>
<line x1="90" y1="80" x2="90" y2="463" stroke="#30363d" stroke-width="2.5" filter="url(#xkcdify)"/>
<line x1="90" y1="463" x2="765" y2="463" stroke="#30363d" stroke-width="2.5" filter="url(#xkcdify)"/>
<path d="M 90.0,462.2 C 90.4,462.2 91.9,461.9 92.2,461.8 C 93.0,461.6 93.7,461.5 94.5,461.3 C 95.2,461.1 96.0,460.9 96.7,460.8 C 97.5,460.7 98.2,460.8 98.9,460.8 C 99.7,460.7 100.4,460.7 101.2,460.6 C 101.9,460.6 102.7,460.5 103.4,460.5 C 104.2,460.4 104.5,460.4 105.6,460.3 C 106.8,460.3 109.0,460.2 110.1,460.2 C 111.2,460.1 111.6,460.1 112.4,460.0 C 113.1,460.0 113.8,460.0 114.6,460.0 C 115.3,460.0 116.1,460.0 116.8,459.9 C 117.6,459.9 118.3,459.9 119.1,459.9 C 119.8,459.8 120.5,459.8 121.3,459.7 C 122.0,459.7 122.8,459.7 123.5,459.5 C 124.3,459.3 125.0,459.1 125.8,458.7 C 126.5,458.4 127.3,457.6 128.0,457.4 C 128.7,457.1 129.5,457.2 130.2,457.2 C 131.0,457.2 131.7,457.1 132.5,457.1 C 133.2,457.1 133.6,457.0 134.7,457.0 C 135.8,457.0 137.7,457.0 139.2,456.9 C 140.7,456.9 142.5,456.9 143.6,456.8 C 144.8,456.8 145.1,456.7 145.9,456.7 C 146.6,456.7 147.4,456.6 148.1,456.6 C 148.9,456.6 149.6,456.5 150.3,456.5 C 151.1,456.5 151.8,456.5 152.6,456.4 C 153.3,456.4 153.7,456.4 154.8,456.4 C 155.9,456.4 157.4,456.3 159.3,456.3 C 161.2,456.2 164.1,456.2 166.0,456.2 C 167.9,456.2 168.2,456.2 170.5,456.2 C 172.7,456.1 177.5,456.0 179.4,455.9 C 181.3,455.9 180.5,455.9 181.6,455.9 C 182.8,455.9 184.2,455.9 186.1,455.8 C 188.0,455.8 189.1,455.8 192.8,455.7 C 196.5,455.7 205.5,455.6 208.5,455.6 C 211.4,455.5 210.0,455.5 210.7,455.5 C 211.4,455.5 211.8,456.1 212.9,455.4 C 214.0,454.8 216.3,454.2 217.4,451.4 C 218.5,448.6 218.9,440.8 219.6,438.4 C 220.4,436.0 221.1,437.4 221.9,437.0 C 222.6,436.6 223.4,436.4 224.1,436.1 C 224.9,435.9 225.6,435.7 226.3,435.5 C 227.1,435.4 227.8,435.4 228.6,435.3 C 229.3,435.2 230.1,435.0 230.8,434.9 C 231.6,434.9 232.3,434.9 233.0,434.9 C 233.8,434.8 234.5,434.6 235.3,434.5 C 236.0,434.4 236.8,434.5 237.5,434.3 C 238.3,434.2 239.0,433.9 239.8,433.7 C 240.5,433.6 241.2,433.7 242.0,433.6 C 242.7,433.5 243.1,433.3 244.2,433.2 C 245.3,433.1 247.6,433.1 248.7,433.0 C 249.8,433.0 250.2,432.9 250.9,432.9 C 251.7,432.8 252.4,432.8 253.2,432.7 C 253.9,432.6 254.7,432.3 255.4,432.2 C 256.1,432.0 256.9,432.7 257.6,431.8 C 258.4,430.9 259.1,427.7 259.9,426.7 C 260.6,425.6 261.4,425.8 262.1,425.6 C 262.8,425.4 263.6,425.7 264.3,425.5 C 265.1,425.4 265.8,425.0 266.6,424.8 C 267.3,424.5 268.1,424.3 268.8,423.8 C 269.6,423.3 270.3,422.3 271.0,421.7 C 271.8,421.1 272.5,420.6 273.3,420.2 C 274.0,419.7 274.8,419.4 275.5,419.1 C 276.3,418.9 277.0,418.7 277.7,418.6 C 278.5,418.5 279.2,418.5 280.0,418.4 C 280.7,418.3 281.5,418.1 282.2,418.0 C 283.0,417.9 283.7,417.9 284.5,417.9 C 285.2,417.8 285.9,417.8 286.7,417.6 C 287.4,417.5 288.2,417.6 288.9,417.1 C 289.7,416.7 290.4,415.7 291.2,415.2 C 291.9,414.7 292.6,414.5 293.4,414.1 C 294.1,413.8 294.9,414.5 295.6,412.9 C 296.4,411.4 297.1,406.6 297.9,404.7 C 298.6,402.9 299.4,402.6 300.1,401.8 C 300.8,401.0 301.6,400.5 302.3,400.0 C 303.1,399.4 303.8,398.9 304.6,398.5 C 305.3,398.1 306.1,397.7 306.8,397.5 C 307.5,397.2 308.3,397.1 309.0,396.9 C 309.8,396.7 310.5,396.4 311.3,396.1 C 312.0,395.8 312.8,395.4 313.5,395.1 C 314.3,394.8 315.0,394.9 315.7,394.6 C 316.5,394.4 317.2,393.9 318.0,393.6 C 318.7,393.3 319.5,393.0 320.2,392.7 C 321.0,392.5 321.7,392.3 322.5,392.1 C 323.2,392.0 323.9,392.0 324.7,391.8 C 325.4,391.7 326.2,391.5 326.9,391.3 C 327.7,391.2 328.4,391.0 329.2,390.8 C 329.9,390.6 330.6,390.4 331.4,390.1 C 332.1,389.7 332.9,389.2 333.6,388.9 C 334.4,388.6 335.1,388.5 335.9,388.3 C 336.6,388.2 337.4,388.1 338.1,387.9 C 338.8,387.8 339.6,387.7 340.3,387.6 C 341.1,387.5 341.8,387.4 342.6,387.3 C 343.3,387.2 344.1,387.2 344.8,387.1 C 345.5,387.0 346.3,386.8 347.0,386.5 C 347.8,386.2 348.5,385.8 349.3,385.4 C 350.0,385.1 350.8,384.7 351.5,384.5 C 352.3,384.3 353.0,384.2 353.7,384.1 C 354.5,384.0 355.2,383.9 356.0,383.7 C 356.7,383.4 357.5,382.9 358.2,382.7 C 359.0,382.5 359.7,382.5 360.4,382.4 C 361.2,382.3 361.9,382.0 362.7,381.9 C 363.4,381.7 364.2,381.5 364.9,381.3 C 365.7,381.1 366.4,380.9 367.2,380.7 C 367.9,380.5 368.6,380.4 369.4,380.2 C 370.1,380.0 370.9,379.6 371.6,379.4 C 372.4,379.2 373.1,379.0 373.9,378.8 C 374.6,378.6 375.3,378.2 376.1,378.0 C 376.8,377.7 377.6,377.4 378.3,377.2 C 379.1,376.9 379.8,376.8 380.6,376.4 C 381.3,376.0 382.1,375.1 382.8,374.7 C 383.5,374.4 384.3,374.3 385.0,374.2 C 385.8,374.1 386.5,374.1 387.3,374.0 C 388.0,373.9 388.8,373.7 389.5,373.6 C 390.2,373.5 391.0,373.7 391.7,373.3 C 392.5,373.0 393.2,371.9 394.0,371.5 C 394.7,371.1 395.5,371.1 396.2,370.9 C 397.0,370.6 397.7,370.1 398.4,369.9 C 399.2,369.7 399.9,369.9 400.7,369.7 C 401.4,369.5 402.2,368.9 402.9,368.7 C 403.7,368.5 404.4,368.6 405.1,368.5 C 405.9,368.4 406.6,368.3 407.4,368.1 C 408.1,367.9 408.9,367.7 409.6,367.1 C 410.4,366.6 411.1,365.6 411.9,365.1 C 412.6,364.5 413.3,364.3 414.1,363.9 C 414.8,363.5 415.6,363.4 416.3,362.7 C 417.1,362.0 417.8,360.3 418.6,359.5 C 419.3,358.7 420.0,358.3 420.8,357.8 C 421.5,357.4 422.3,357.3 423.0,356.6 C 423.8,355.9 424.5,354.2 425.3,353.5 C 426.0,352.8 426.8,352.7 427.5,352.3 C 428.2,351.9 429.0,351.6 429.7,351.1 C 430.5,35Line truncated
<path d="M 90.0,462.2 C 90.4,462.2 91.9,461.9 92.2,461.8 C 93.0,461.6 93.7,461.5 94.5,461.3 C 95.2,461.1 96.0,460.9 96.7,460.8 C 97.5,460.7 98.2,460.8 98.9,460.8 C 99.7,460.7 100.4,460.7 101.2,460.6 C 101.9,460.6 102.7,460.5 103.4,460.5 C 104.2,460.4 104.5,460.4 105.6,460.3 C 106.8,460.3 109.0,460.2 110.1,460.2 C 111.2,460.1 111.6,460.1 112.4,460.0 C 113.1,460.0 113.8,460.0 114.6,460.0 C 115.3,460.0 116.1,460.0 116.8,459.9 C 117.6,459.9 118.3,459.9 119.1,459.9 C 119.8,459.8 120.5,459.8 121.3,459.7 C 122.0,459.7 122.8,459.7 123.5,459.5 C 124.3,459.3 125.0,459.1 125.8,458.7 C 126.5,458.4 127.3,457.6 128.0,457.4 C 128.7,457.1 129.5,457.2 130.2,457.2 C 131.0,457.2 131.7,457.1 132.5,457.1 C 133.2,457.1 133.6,457.0 134.7,457.0 C 135.8,457.0 137.7,457.0 139.2,456.9 C 140.7,456.9 142.5,456.9 143.6,456.8 C 144.8,456.8 145.1,456.7 145.9,456.7 C 146.6,456.7 147.4,456.6 148.1,456.6 C 148.9,456.6 149.6,456.5 150.3,456.5 C 151.1,456.5 151.8,456.5 152.6,456.4 C 153.3,456.4 153.7,456.4 154.8,456.4 C 155.9,456.4 157.4,456.3 159.3,456.3 C 161.2,456.2 164.1,456.2 166.0,456.2 C 167.9,456.2 168.2,456.2 170.5,456.2 C 172.7,456.1 177.5,456.0 179.4,455.9 C 181.3,455.9 180.5,455.9 181.6,455.9 C 182.8,455.9 184.2,455.9 186.1,455.8 C 188.0,455.8 189.1,455.8 192.8,455.7 C 196.5,455.7 205.5,455.6 208.5,455.6 C 211.4,455.5 210.0,455.5 210.7,455.5 C 211.4,455.5 211.8,456.1 212.9,455.4 C 214.0,454.8 216.3,454.2 217.4,451.4 C 218.5,448.6 218.9,440.8 219.6,438.4 C 220.4,436.0 221.1,437.4 221.9,437.0 C 222.6,436.6 223.4,436.4 224.1,436.1 C 224.9,435.9 225.6,435.7 226.3,435.5 C 227.1,435.4 227.8,435.4 228.6,435.3 C 229.3,435.2 230.1,435.0 230.8,434.9 C 231.6,434.9 232.3,434.9 233.0,434.9 C 233.8,434.8 234.5,434.6 235.3,434.5 C 236.0,434.4 236.8,434.5 237.5,434.3 C 238.3,434.2 239.0,433.9 239.8,433.7 C 240.5,433.6 241.2,433.7 242.0,433.6 C 242.7,433.5 243.1,433.3 244.2,433.2 C 245.3,433.1 247.6,433.1 248.7,433.0 C 249.8,433.0 250.2,432.9 250.9,432.9 C 251.7,432.8 252.4,432.8 253.2,432.7 C 253.9,432.6 254.7,432.3 255.4,432.2 C 256.1,432.0 256.9,432.7 257.6,431.8 C 258.4,430.9 259.1,427.7 259.9,426.7 C 260.6,425.6 261.4,425.8 262.1,425.6 C 262.8,425.4 263.6,425.7 264.3,425.5 C 265.1,425.4 265.8,425.0 266.6,424.8 C 267.3,424.5 268.1,424.3 268.8,423.8 C 269.6,423.3 270.3,422.3 271.0,421.7 C 271.8,421.1 272.5,420.6 273.3,420.2 C 274.0,419.7 274.8,419.4 275.5,419.1 C 276.3,418.9 277.0,418.7 277.7,418.6 C 278.5,418.5 279.2,418.5 280.0,418.4 C 280.7,418.3 281.5,418.1 282.2,418.0 C 283.0,417.9 283.7,417.9 284.5,417.9 C 285.2,417.8 285.9,417.8 286.7,417.6 C 287.4,417.5 288.2,417.6 288.9,417.1 C 289.7,416.7 290.4,415.7 291.2,415.2 C 291.9,414.7 292.6,414.5 293.4,414.1 C 294.1,413.8 294.9,414.5 295.6,412.9 C 296.4,411.4 297.1,406.6 297.9,404.7 C 298.6,402.9 299.4,402.6 300.1,401.8 C 300.8,401.0 301.6,400.5 302.3,400.0 C 303.1,399.4 303.8,398.9 304.6,398.5 C 305.3,398.1 306.1,397.7 306.8,397.5 C 307.5,397.2 308.3,397.1 309.0,396.9 C 309.8,396.7 310.5,396.4 311.3,396.1 C 312.0,395.8 312.8,395.4 313.5,395.1 C 314.3,394.8 315.0,394.9 315.7,394.6 C 316.5,394.4 317.2,393.9 318.0,393.6 C 318.7,393.3 319.5,393.0 320.2,392.7 C 321.0,392.5 321.7,392.3 322.5,392.1 C 323.2,392.0 323.9,392.0 324.7,391.8 C 325.4,391.7 326.2,391.5 326.9,391.3 C 327.7,391.2 328.4,391.0 329.2,390.8 C 329.9,390.6 330.6,390.4 331.4,390.1 C 332.1,389.7 332.9,389.2 333.6,388.9 C 334.4,388.6 335.1,388.5 335.9,388.3 C 336.6,388.2 337.4,388.1 338.1,387.9 C 338.8,387.8 339.6,387.7 340.3,387.6 C 341.1,387.5 341.8,387.4 342.6,387.3 C 343.3,387.2 344.1,387.2 344.8,387.1 C 345.5,387.0 346.3,386.8 347.0,386.5 C 347.8,386.2 348.5,385.8 349.3,385.4 C 350.0,385.1 350.8,384.7 351.5,384.5 C 352.3,384.3 353.0,384.2 353.7,384.1 C 354.5,384.0 355.2,383.9 356.0,383.7 C 356.7,383.4 357.5,382.9 358.2,382.7 C 359.0,382.5 359.7,382.5 360.4,382.4 C 361.2,382.3 361.9,382.0 362.7,381.9 C 363.4,381.7 364.2,381.5 364.9,381.3 C 365.7,381.1 366.4,380.9 367.2,380.7 C 367.9,380.5 368.6,380.4 369.4,380.2 C 370.1,380.0 370.9,379.6 371.6,379.4 C 372.4,379.2 373.1,379.0 373.9,378.8 C 374.6,378.6 375.3,378.2 376.1,378.0 C 376.8,377.7 377.6,377.4 378.3,377.2 C 379.1,376.9 379.8,376.8 380.6,376.4 C 381.3,376.0 382.1,375.1 382.8,374.7 C 383.5,374.4 384.3,374.3 385.0,374.2 C 385.8,374.1 386.5,374.1 387.3,374.0 C 388.0,373.9 388.8,373.7 389.5,373.6 C 390.2,373.5 391.0,373.7 391.7,373.3 C 392.5,373.0 393.2,371.9 394.0,371.5 C 394.7,371.1 395.5,371.1 396.2,370.9 C 397.0,370.6 397.7,370.1 398.4,369.9 C 399.2,369.7 399.9,369.9 400.7,369.7 C 401.4,369.5 402.2,368.9 402.9,368.7 C 403.7,368.5 404.4,368.6 405.1,368.5 C 405.9,368.4 406.6,368.3 407.4,368.1 C 408.1,367.9 408.9,367.7 409.6,367.1 C 410.4,366.6 411.1,365.6 411.9,365.1 C 412.6,364.5 413.3,364.3 414.1,363.9 C 414.8,363.5 415.6,363.4 416.3,362.7 C 417.1,362.0 417.8,360.3 418.6,359.5 C 419.3,358.7 420.0,358.3 420.8,357.8 C 421.5,357.4 422.3,357.3 423.0,356.6 C 423.8,355.9 424.5,354.2 425.3,353.5 C 426.0,352.8 426.8,352.7 427.5,352.3 C 428.2,351.9 429.0,351.6 429.7,351.1 C 430.5,35Line truncated
<circle cx="765.0" cy="80.1" r="5" fill="#dd4528" stroke="#0d1117" stroke-width="2" filter="url(#xkcdify)"/>
<text x="753.0" y="77.0" fill="#dd4528" font-size="18" font-family="Handlee, cursive" font-weight="bold" text-anchor="end">6999</text>
<path d="M 90.0,462.3 C 90.4,462.3 91.8,462.0 92.2,461.9 C 92.9,461.8 93.6,461.7 94.4,461.5 C 95.1,461.4 95.8,461.2 96.6,461.1 C 97.3,461.0 98.0,461.1 98.7,461.0 C 99.5,461.0 100.2,461.0 100.9,460.9 C 101.7,460.9 102.4,460.8 103.1,460.8 C 103.8,460.7 104.2,460.7 105.3,460.7 C 106.4,460.6 108.6,460.6 109.7,460.5 C 110.8,460.5 111.1,460.4 111.8,460.4 C 112.6,460.4 113.3,460.4 114.0,460.4 C 114.8,460.4 115.5,460.3 116.2,460.3 C 116.9,460.3 117.7,460.3 118.4,460.3 C 119.1,460.2 119.9,460.2 120.6,460.1 C 121.3,460.1 122.0,460.1 122.8,459.9 C 123.5,459.8 124.2,459.6 125.0,459.3 C 125.7,459.0 126.4,458.3 127.1,458.1 C 127.9,457.8 128.6,458.0 129.3,457.9 C 130.0,457.9 130.8,457.9 131.5,457.8 C 132.2,457.8 132.6,457.8 133.7,457.7 C 134.8,457.7 136.6,457.7 138.1,457.7 C 139.5,457.7 141.3,457.6 142.4,457.6 C 143.5,457.6 143.9,457.5 144.6,457.5 C 145.3,457.5 146.1,457.4 146.8,457.4 C 147.5,457.4 148.3,457.3 149.0,457.3 C 149.7,457.3 150.4,457.3 151.2,457.3 C 151.9,457.2 152.3,457.2 153.3,457.2 C 154.4,457.2 155.9,457.1 157.7,457.1 C 159.5,457.1 162.5,457.1 164.3,457.1 C 166.1,457.0 166.5,457.1 168.6,457.0 C 170.8,457.0 175.6,456.9 177.4,456.8 C 179.2,456.8 178.5,456.8 179.6,456.8 C 180.7,456.8 182.1,456.8 183.9,456.7 C 185.8,456.7 186.8,456.7 190.5,456.6 C 194.1,456.6 202.9,456.5 205.8,456.5 C 208.7,456.5 207.2,456.5 208.0,456.4 C 208.7,456.4 209.1,457.0 210.1,456.4 C 211.2,455.8 213.4,455.3 214.5,452.9 C 215.6,450.4 216.0,443.6 216.7,441.5 C 217.4,439.4 218.2,440.6 218.9,440.3 C 219.6,439.9 220.3,439.7 221.1,439.5 C 221.8,439.3 222.5,439.1 223.3,439.0 C 224.0,438.8 224.7,438.8 225.4,438.7 C 226.2,438.6 226.9,438.5 227.6,438.4 C 228.3,438.4 229.1,438.5 229.8,438.4 C 230.5,438.3 231.3,438.1 232.0,438.1 C 232.7,438.0 233.4,438.0 234.2,437.9 C 234.9,437.8 235.6,437.5 236.4,437.4 C 237.1,437.3 237.8,437.3 238.5,437.2 C 239.3,437.2 239.6,437.0 240.7,436.9 C 241.8,436.8 244.0,436.8 245.1,436.8 C 246.2,436.7 246.6,436.7 247.3,436.6 C 248.0,436.6 248.7,436.6 249.5,436.5 C 250.2,436.4 250.9,436.2 251.7,436.0 C 252.4,435.9 253.1,436.5 253.8,435.7 C 254.6,434.9 255.3,432.1 256.0,431.2 C 256.7,430.3 257.5,430.4 258.2,430.3 C 258.9,430.1 259.7,430.3 260.4,430.2 C 261.1,430.1 261.8,429.8 262.6,429.5 C 263.3,429.3 264.0,429.1 264.8,428.7 C 265.5,428.2 266.2,427.4 266.9,426.9 C 267.7,426.4 268.4,425.9 269.1,425.5 C 269.9,425.1 270.6,424.8 271.3,424.6 C 272.0,424.4 272.8,424.3 273.5,424.2 C 274.2,424.1 275.0,424.1 275.7,424.0 C 276.4,423.9 277.1,423.7 277.9,423.6 C 278.6,423.5 279.3,423.6 280.0,423.5 C 280.8,423.5 281.5,423.4 282.2,423.3 C 283.0,423.2 283.7,423.2 284.4,422.9 C 285.1,422.5 285.9,421.6 286.6,421.2 C 287.3,420.7 288.1,420.6 288.8,420.2 C 289.5,419.9 290.2,420.6 291.0,419.2 C 291.7,417.8 292.4,413.6 293.2,412.0 C 293.9,410.4 294.6,410.1 295.3,409.4 C 296.1,408.7 296.8,408.3 297.5,407.8 C 298.3,407.4 299.0,406.9 299.7,406.6 C 300.4,406.2 301.2,405.9 301.9,405.6 C 302.6,405.4 303.3,405.4 304.1,405.2 C 304.8,405.0 305.5,404.8 306.3,404.5 C 307.0,404.2 307.7,403.8 308.4,403.6 C 309.2,403.4 309.9,403.4 310.6,403.2 C 311.4,402.9 312.1,402.5 312.8,402.2 C 313.5,402.0 314.3,401.7 315.0,401.5 C 315.7,401.3 316.5,401.1 317.2,401.0 C 317.9,400.9 318.6,400.8 319.4,400.7 C 320.1,400.6 320.8,400.4 321.6,400.3 C 322.3,400.1 323.0,400.0 323.7,399.8 C 324.5,399.6 325.2,399.5 325.9,399.2 C 326.7,398.9 327.4,398.4 328.1,398.1 C 328.8,397.9 329.6,397.8 330.3,397.7 C 331.0,397.5 331.7,397.4 332.5,397.3 C 333.2,397.2 333.9,397.1 334.7,397.0 C 335.4,396.9 336.1,396.8 336.8,396.7 C 337.6,396.7 338.3,396.7 339.0,396.6 C 339.8,396.5 340.5,396.3 341.2,396.1 C 341.9,395.8 342.7,395.4 343.4,395.1 C 344.1,394.8 344.9,394.5 345.6,394.3 C 346.3,394.1 347.0,394.1 347.8,394.0 C 348.5,393.8 349.2,393.8 350.0,393.6 C 350.7,393.4 351.4,392.9 352.1,392.7 C 352.9,392.5 353.6,392.6 354.3,392.5 C 355.0,392.4 355.8,392.2 356.5,392.0 C 357.2,391.8 358.0,391.7 358.7,391.5 C 359.4,391.4 360.1,391.2 360.9,391.0 C 361.6,390.8 362.3,390.8 363.1,390.6 C 363.8,390.4 364.5,390.1 365.2,389.8 C 366.0,389.6 366.7,389.5 367.4,389.3 C 368.2,389.1 368.9,388.8 369.6,388.6 C 370.3,388.4 371.1,388.1 371.8,387.9 C 372.5,387.7 373.3,387.6 374.0,387.3 C 374.7,386.9 375.4,386.1 376.2,385.8 C 376.9,385.5 377.6,385.4 378.3,385.3 C 379.1,385.2 379.8,385.2 380.5,385.1 C 381.3,385.0 382.0,384.9 382.7,384.8 C 383.4,384.7 384.2,384.8 384.9,384.5 C 385.6,384.2 386.4,383.3 387.1,382.9 C 387.8,382.5 388.5,382.6 389.3,382.4 C 390.0,382.2 390.7,381.7 391.5,381.6 C 392.2,381.4 392.9,381.5 393.6,381.3 C 394.4,381.1 395.1,380.7 395.8,380.5 C 396.6,380.3 397.3,380.4 398.0,380.3 C 398.7,380.2 399.5,380.2 400.2,380.0 C 400.9,379.8 401.7,379.6 402.4,379.1 C 403.1,378.7 403.8,377.8 404.6,377.3 C 405.3,376.8 406.0,376.6 406.7,376.3 C 407.5,376.0 408.2,375.9 408.9,375.2 C 409.7,374.6 410.4,373.2 411.1,372.5 C 411.8,371.8 412.6,371.5 413.3,371.0 C 414.0,370.6 414.8,370.6 415.5,369.9 C 416.2,369.3 416.9,367.9 417.7,367.2 C 418.4,366.6 419.1,366.5 419.9,366.2 C 420.6,365.8 421.3,365.6 422.0,365.1 C 422.8,36Line truncated
<path d="M 90.0,462.3 C 90.4,462.3 91.8,462.0 92.2,461.9 C 92.9,461.8 93.6,461.7 94.4,461.5 C 95.1,461.4 95.8,461.2 96.6,461.1 C 97.3,461.0 98.0,461.1 98.7,461.0 C 99.5,461.0 100.2,461.0 100.9,460.9 C 101.7,460.9 102.4,460.8 103.1,460.8 C 103.8,460.7 104.2,460.7 105.3,460.7 C 106.4,460.6 108.6,460.6 109.7,460.5 C 110.8,460.5 111.1,460.4 111.8,460.4 C 112.6,460.4 113.3,460.4 114.0,460.4 C 114.8,460.4 115.5,460.3 116.2,460.3 C 116.9,460.3 117.7,460.3 118.4,460.3 C 119.1,460.2 119.9,460.2 120.6,460.1 C 121.3,460.1 122.0,460.1 122.8,459.9 C 123.5,459.8 124.2,459.6 125.0,459.3 C 125.7,459.0 126.4,458.3 127.1,458.1 C 127.9,457.8 128.6,458.0 129.3,457.9 C 130.0,457.9 130.8,457.9 131.5,457.8 C 132.2,457.8 132.6,457.8 133.7,457.7 C 134.8,457.7 136.6,457.7 138.1,457.7 C 139.5,457.7 141.3,457.6 142.4,457.6 C 143.5,457.6 143.9,457.5 144.6,457.5 C 145.3,457.5 146.1,457.4 146.8,457.4 C 147.5,457.4 148.3,457.3 149.0,457.3 C 149.7,457.3 150.4,457.3 151.2,457.3 C 151.9,457.2 152.3,457.2 153.3,457.2 C 154.4,457.2 155.9,457.1 157.7,457.1 C 159.5,457.1 162.5,457.1 164.3,457.1 C 166.1,457.0 166.5,457.1 168.6,457.0 C 170.8,457.0 175.6,456.9 177.4,456.8 C 179.2,456.8 178.5,456.8 179.6,456.8 C 180.7,456.8 182.1,456.8 183.9,456.7 C 185.8,456.7 186.8,456.7 190.5,456.6 C 194.1,456.6 202.9,456.5 205.8,456.5 C 208.7,456.5 207.2,456.5 208.0,456.4 C 208.7,456.4 209.1,457.0 210.1,456.4 C 211.2,455.8 213.4,455.3 214.5,452.9 C 215.6,450.4 216.0,443.6 216.7,441.5 C 217.4,439.4 218.2,440.6 218.9,440.3 C 219.6,439.9 220.3,439.7 221.1,439.5 C 221.8,439.3 222.5,439.1 223.3,439.0 C 224.0,438.8 224.7,438.8 225.4,438.7 C 226.2,438.6 226.9,438.5 227.6,438.4 C 228.3,438.4 229.1,438.5 229.8,438.4 C 230.5,438.3 231.3,438.1 232.0,438.1 C 232.7,438.0 233.4,438.0 234.2,437.9 C 234.9,437.8 235.6,437.5 236.4,437.4 C 237.1,437.3 237.8,437.3 238.5,437.2 C 239.3,437.2 239.6,437.0 240.7,436.9 C 241.8,436.8 244.0,436.8 245.1,436.8 C 246.2,436.7 246.6,436.7 247.3,436.6 C 248.0,436.6 248.7,436.6 249.5,436.5 C 250.2,436.4 250.9,436.2 251.7,436.0 C 252.4,435.9 253.1,436.5 253.8,435.7 C 254.6,434.9 255.3,432.1 256.0,431.2 C 256.7,430.3 257.5,430.4 258.2,430.3 C 258.9,430.1 259.7,430.3 260.4,430.2 C 261.1,430.1 261.8,429.8 262.6,429.5 C 263.3,429.3 264.0,429.1 264.8,428.7 C 265.5,428.2 266.2,427.4 266.9,426.9 C 267.7,426.4 268.4,425.9 269.1,425.5 C 269.9,425.1 270.6,424.8 271.3,424.6 C 272.0,424.4 272.8,424.3 273.5,424.2 C 274.2,424.1 275.0,424.1 275.7,424.0 C 276.4,423.9 277.1,423.7 277.9,423.6 C 278.6,423.5 279.3,423.6 280.0,423.5 C 280.8,423.5 281.5,423.4 282.2,423.3 C 283.0,423.2 283.7,423.2 284.4,422.9 C 285.1,422.5 285.9,421.6 286.6,421.2 C 287.3,420.7 288.1,420.6 288.8,420.2 C 289.5,419.9 290.2,420.6 291.0,419.2 C 291.7,417.8 292.4,413.6 293.2,412.0 C 293.9,410.4 294.6,410.1 295.3,409.4 C 296.1,408.7 296.8,408.3 297.5,407.8 C 298.3,407.4 299.0,406.9 299.7,406.6 C 300.4,406.2 301.2,405.9 301.9,405.6 C 302.6,405.4 303.3,405.4 304.1,405.2 C 304.8,405.0 305.5,404.8 306.3,404.5 C 307.0,404.2 307.7,403.8 308.4,403.6 C 309.2,403.4 309.9,403.4 310.6,403.2 C 311.4,402.9 312.1,402.5 312.8,402.2 C 313.5,402.0 314.3,401.7 315.0,401.5 C 315.7,401.3 316.5,401.1 317.2,401.0 C 317.9,400.9 318.6,400.8 319.4,400.7 C 320.1,400.6 320.8,400.4 321.6,400.3 C 322.3,400.1 323.0,400.0 323.7,399.8 C 324.5,399.6 325.2,399.5 325.9,399.2 C 326.7,398.9 327.4,398.4 328.1,398.1 C 328.8,397.9 329.6,397.8 330.3,397.7 C 331.0,397.5 331.7,397.4 332.5,397.3 C 333.2,397.2 333.9,397.1 334.7,397.0 C 335.4,396.9 336.1,396.8 336.8,396.7 C 337.6,396.7 338.3,396.7 339.0,396.6 C 339.8,396.5 340.5,396.3 341.2,396.1 C 341.9,395.8 342.7,395.4 343.4,395.1 C 344.1,394.8 344.9,394.5 345.6,394.3 C 346.3,394.1 347.0,394.1 347.8,394.0 C 348.5,393.8 349.2,393.8 350.0,393.6 C 350.7,393.4 351.4,392.9 352.1,392.7 C 352.9,392.5 353.6,392.6 354.3,392.5 C 355.0,392.4 355.8,392.2 356.5,392.0 C 357.2,391.8 358.0,391.7 358.7,391.5 C 359.4,391.4 360.1,391.2 360.9,391.0 C 361.6,390.8 362.3,390.8 363.1,390.6 C 363.8,390.4 364.5,390.1 365.2,389.8 C 366.0,389.6 366.7,389.5 367.4,389.3 C 368.2,389.1 368.9,388.8 369.6,388.6 C 370.3,388.4 371.1,388.1 371.8,387.9 C 372.5,387.7 373.3,387.6 374.0,387.3 C 374.7,386.9 375.4,386.1 376.2,385.8 C 376.9,385.5 377.6,385.4 378.3,385.3 C 379.1,385.2 379.8,385.2 380.5,385.1 C 381.3,385.0 382.0,384.9 382.7,384.8 C 383.4,384.7 384.2,384.8 384.9,384.5 C 385.6,384.2 386.4,383.3 387.1,382.9 C 387.8,382.5 388.5,382.6 389.3,382.4 C 390.0,382.2 390.7,381.7 391.5,381.6 C 392.2,381.4 392.9,381.5 393.6,381.3 C 394.4,381.1 395.1,380.7 395.8,380.5 C 396.6,380.3 397.3,380.4 398.0,380.3 C 398.7,380.2 399.5,380.2 400.2,380.0 C 400.9,379.8 401.7,379.6 402.4,379.1 C 403.1,378.7 403.8,377.8 404.6,377.3 C 405.3,376.8 406.0,376.6 406.7,376.3 C 407.5,376.0 408.2,375.9 408.9,375.2 C 409.7,374.6 410.4,373.2 411.1,372.5 C 411.8,371.8 412.6,371.5 413.3,371.0 C 414.0,370.6 414.8,370.6 415.5,369.9 C 416.2,369.3 416.9,367.9 417.7,367.2 C 418.4,366.6 419.1,366.5 419.9,366.2 C 420.6,365.8 421.3,365.6 422.0,365.1 C 422.8,36Line truncated
<circle cx="765.0" cy="119.4" r="5" fill="#dd4528" stroke="#0d1117" stroke-width="2" filter="url(#xkcdify)"/>
<text x="753.0" y="77.0" fill="#dd4528" font-size="18" font-family="Handlee, cursive" font-weight="bold" text-anchor="end">7176</text>
<a href="https://github.com/carsteneu/mystarhistory" target="_blank" rel="noopener"><text x="763" y="527" font-size="13" font-family="Handlee, cursive" fill="#fff" text-anchor="end">Made with carsteneu/mystarhistory</text></a>
</svg>

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 37 KiB

+24 -22
View File
@@ -15,33 +15,35 @@
<text x="50%" y="30" fill="#000" font-size="20" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Star History</text>
<rect width="178" height="32" x="105" y="90" fill="#fff" fill-opacity="0.9" stroke="#000" stroke-width="1.5" rx="4" ry="4" filter="url(#xkcdify)"/><rect width="8" height="8" x="115" y="102" rx="2" ry="2" fill="#dd4528" filter="url(#xkcdify)"/><text x="131" y="110" fill="#000" font-size="15" font-family="Handlee, cursive" font-weight="bold">binaricat/Netcatty</text>
<text x="85" y="468.0" fill="#000" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="end">0</text>
<line x1="90" y1="408.3" x2="765" y2="408.3" stroke="#eee" stroke-width="1" filter="url(#xkcdify)"/>
<text x="85" y="413.2857142857143" fill="#000" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="end">1K</text>
<line x1="90" y1="353.6" x2="765" y2="353.6" stroke="#eee" stroke-width="1" filter="url(#xkcdify)"/>
<text x="85" y="358.57142857142856" fill="#000" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="end">2K</text>
<line x1="90" y1="298.9" x2="765" y2="298.9" stroke="#eee" stroke-width="1" filter="url(#xkcdify)"/>
<text x="85" y="303.8571428571429" fill="#000" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="end">3K</text>
<line x1="90" y1="244.1" x2="765" y2="244.1" stroke="#eee" stroke-width="1" filter="url(#xkcdify)"/>
<text x="85" y="249.14285714285717" fill="#000" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="end">4K</text>
<line x1="90" y1="189.4" x2="765" y2="189.4" stroke="#eee" stroke-width="1" filter="url(#xkcdify)"/>
<text x="85" y="194.42857142857144" fill="#000" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="end">5K</text>
<line x1="90" y1="134.7" x2="765" y2="134.7" stroke="#eee" stroke-width="1" filter="url(#xkcdify)"/>
<text x="85" y="139.71428571428572" fill="#000" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="end">6K</text>
<line x1="90" y1="415.1" x2="765" y2="415.1" stroke="#eee" stroke-width="1" filter="url(#xkcdify)"/>
<text x="85" y="420.125" fill="#000" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="end">1K</text>
<line x1="90" y1="367.2" x2="765" y2="367.2" stroke="#eee" stroke-width="1" filter="url(#xkcdify)"/>
<text x="85" y="372.25" fill="#000" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="end">2K</text>
<line x1="90" y1="319.4" x2="765" y2="319.4" stroke="#eee" stroke-width="1" filter="url(#xkcdify)"/>
<text x="85" y="324.375" fill="#000" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="end">3K</text>
<line x1="90" y1="271.5" x2="765" y2="271.5" stroke="#eee" stroke-width="1" filter="url(#xkcdify)"/>
<text x="85" y="276.5" fill="#000" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="end">4K</text>
<line x1="90" y1="223.6" x2="765" y2="223.6" stroke="#eee" stroke-width="1" filter="url(#xkcdify)"/>
<text x="85" y="228.625" fill="#000" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="end">5K</text>
<line x1="90" y1="175.8" x2="765" y2="175.8" stroke="#eee" stroke-width="1" filter="url(#xkcdify)"/>
<text x="85" y="180.75" fill="#000" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="end">6K</text>
<line x1="90" y1="127.9" x2="765" y2="127.9" stroke="#eee" stroke-width="1" filter="url(#xkcdify)"/>
<text x="85" y="132.875" fill="#000" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="end">7K</text>
<line x1="90" y1="80.0" x2="765" y2="80.0" stroke="#eee" stroke-width="1" filter="url(#xkcdify)"/>
<text x="85" y="85.0" fill="#000" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="end">7K</text>
<text x="85" y="85.0" fill="#000" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="end">8K</text>
<text x="43" y="271.5" fill="#000" font-size="17" font-family="Handlee, cursive" font-weight="bold" transform="rotate(-90, 43, 271.5)">GitHub Stars</text>
<text x="90.0" y="488" fill="#000" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Jan 2026</text>
<text x="217.4" y="488" fill="#000" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Mar 2026</text>
<text x="353.7" y="488" fill="#000" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">May 2026</text>
<text x="490.1" y="488" fill="#000" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Jul 2026</text>
<text x="628.7" y="488" fill="#000" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Sep 2026</text>
<text x="765.0" y="488" fill="#000" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Nov 2026</text>
<text x="214.5" y="488" fill="#000" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Mar 2026</text>
<text x="347.8" y="488" fill="#000" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">May 2026</text>
<text x="481.0" y="488" fill="#000" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Jul 2026</text>
<text x="616.5" y="488" fill="#000" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Sep 2026</text>
<text x="749.7" y="488" fill="#000" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Nov 2026</text>
<text x="50%" y="525" fill="#000" font-size="17" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Date</text>
<line x1="90" y1="80" x2="90" y2="463" stroke="#222" stroke-width="2.5" filter="url(#xkcdify)"/>
<line x1="90" y1="463" x2="765" y2="463" stroke="#222" stroke-width="2.5" filter="url(#xkcdify)"/>
<path d="M 90.0,462.2 C 90.4,462.2 91.9,461.9 92.2,461.8 C 93.0,461.6 93.7,461.5 94.5,461.3 C 95.2,461.1 96.0,460.9 96.7,460.8 C 97.5,460.7 98.2,460.8 98.9,460.8 C 99.7,460.7 100.4,460.7 101.2,460.6 C 101.9,460.6 102.7,460.5 103.4,460.5 C 104.2,460.4 104.5,460.4 105.6,460.3 C 106.8,460.3 109.0,460.2 110.1,460.2 C 111.2,460.1 111.6,460.1 112.4,460.0 C 113.1,460.0 113.8,460.0 114.6,460.0 C 115.3,460.0 116.1,460.0 116.8,459.9 C 117.6,459.9 118.3,459.9 119.1,459.9 C 119.8,459.8 120.5,459.8 121.3,459.7 C 122.0,459.7 122.8,459.7 123.5,459.5 C 124.3,459.3 125.0,459.1 125.8,458.7 C 126.5,458.4 127.3,457.6 128.0,457.4 C 128.7,457.1 129.5,457.2 130.2,457.2 C 131.0,457.2 131.7,457.1 132.5,457.1 C 133.2,457.1 133.6,457.0 134.7,457.0 C 135.8,457.0 137.7,457.0 139.2,456.9 C 140.7,456.9 142.5,456.9 143.6,456.8 C 144.8,456.8 145.1,456.7 145.9,456.7 C 146.6,456.7 147.4,456.6 148.1,456.6 C 148.9,456.6 149.6,456.5 150.3,456.5 C 151.1,456.5 151.8,456.5 152.6,456.4 C 153.3,456.4 153.7,456.4 154.8,456.4 C 155.9,456.4 157.4,456.3 159.3,456.3 C 161.2,456.2 164.1,456.2 166.0,456.2 C 167.9,456.2 168.2,456.2 170.5,456.2 C 172.7,456.1 177.5,456.0 179.4,455.9 C 181.3,455.9 180.5,455.9 181.6,455.9 C 182.8,455.9 184.2,455.9 186.1,455.8 C 188.0,455.8 189.1,455.8 192.8,455.7 C 196.5,455.7 205.5,455.6 208.5,455.6 C 211.4,455.5 210.0,455.5 210.7,455.5 C 211.4,455.5 211.8,456.1 212.9,455.4 C 214.0,454.8 216.3,454.2 217.4,451.4 C 218.5,448.6 218.9,440.8 219.6,438.4 C 220.4,436.0 221.1,437.4 221.9,437.0 C 222.6,436.6 223.4,436.4 224.1,436.1 C 224.9,435.9 225.6,435.7 226.3,435.5 C 227.1,435.4 227.8,435.4 228.6,435.3 C 229.3,435.2 230.1,435.0 230.8,434.9 C 231.6,434.9 232.3,434.9 233.0,434.9 C 233.8,434.8 234.5,434.6 235.3,434.5 C 236.0,434.4 236.8,434.5 237.5,434.3 C 238.3,434.2 239.0,433.9 239.8,433.7 C 240.5,433.6 241.2,433.7 242.0,433.6 C 242.7,433.5 243.1,433.3 244.2,433.2 C 245.3,433.1 247.6,433.1 248.7,433.0 C 249.8,433.0 250.2,432.9 250.9,432.9 C 251.7,432.8 252.4,432.8 253.2,432.7 C 253.9,432.6 254.7,432.3 255.4,432.2 C 256.1,432.0 256.9,432.7 257.6,431.8 C 258.4,430.9 259.1,427.7 259.9,426.7 C 260.6,425.6 261.4,425.8 262.1,425.6 C 262.8,425.4 263.6,425.7 264.3,425.5 C 265.1,425.4 265.8,425.0 266.6,424.8 C 267.3,424.5 268.1,424.3 268.8,423.8 C 269.6,423.3 270.3,422.3 271.0,421.7 C 271.8,421.1 272.5,420.6 273.3,420.2 C 274.0,419.7 274.8,419.4 275.5,419.1 C 276.3,418.9 277.0,418.7 277.7,418.6 C 278.5,418.5 279.2,418.5 280.0,418.4 C 280.7,418.3 281.5,418.1 282.2,418.0 C 283.0,417.9 283.7,417.9 284.5,417.9 C 285.2,417.8 285.9,417.8 286.7,417.6 C 287.4,417.5 288.2,417.6 288.9,417.1 C 289.7,416.7 290.4,415.7 291.2,415.2 C 291.9,414.7 292.6,414.5 293.4,414.1 C 294.1,413.8 294.9,414.5 295.6,412.9 C 296.4,411.4 297.1,406.6 297.9,404.7 C 298.6,402.9 299.4,402.6 300.1,401.8 C 300.8,401.0 301.6,400.5 302.3,400.0 C 303.1,399.4 303.8,398.9 304.6,398.5 C 305.3,398.1 306.1,397.7 306.8,397.5 C 307.5,397.2 308.3,397.1 309.0,396.9 C 309.8,396.7 310.5,396.4 311.3,396.1 C 312.0,395.8 312.8,395.4 313.5,395.1 C 314.3,394.8 315.0,394.9 315.7,394.6 C 316.5,394.4 317.2,393.9 318.0,393.6 C 318.7,393.3 319.5,393.0 320.2,392.7 C 321.0,392.5 321.7,392.3 322.5,392.1 C 323.2,392.0 323.9,392.0 324.7,391.8 C 325.4,391.7 326.2,391.5 326.9,391.3 C 327.7,391.2 328.4,391.0 329.2,390.8 C 329.9,390.6 330.6,390.4 331.4,390.1 C 332.1,389.7 332.9,389.2 333.6,388.9 C 334.4,388.6 335.1,388.5 335.9,388.3 C 336.6,388.2 337.4,388.1 338.1,387.9 C 338.8,387.8 339.6,387.7 340.3,387.6 C 341.1,387.5 341.8,387.4 342.6,387.3 C 343.3,387.2 344.1,387.2 344.8,387.1 C 345.5,387.0 346.3,386.8 347.0,386.5 C 347.8,386.2 348.5,385.8 349.3,385.4 C 350.0,385.1 350.8,384.7 351.5,384.5 C 352.3,384.3 353.0,384.2 353.7,384.1 C 354.5,384.0 355.2,383.9 356.0,383.7 C 356.7,383.4 357.5,382.9 358.2,382.7 C 359.0,382.5 359.7,382.5 360.4,382.4 C 361.2,382.3 361.9,382.0 362.7,381.9 C 363.4,381.7 364.2,381.5 364.9,381.3 C 365.7,381.1 366.4,380.9 367.2,380.7 C 367.9,380.5 368.6,380.4 369.4,380.2 C 370.1,380.0 370.9,379.6 371.6,379.4 C 372.4,379.2 373.1,379.0 373.9,378.8 C 374.6,378.6 375.3,378.2 376.1,378.0 C 376.8,377.7 377.6,377.4 378.3,377.2 C 379.1,376.9 379.8,376.8 380.6,376.4 C 381.3,376.0 382.1,375.1 382.8,374.7 C 383.5,374.4 384.3,374.3 385.0,374.2 C 385.8,374.1 386.5,374.1 387.3,374.0 C 388.0,373.9 388.8,373.7 389.5,373.6 C 390.2,373.5 391.0,373.7 391.7,373.3 C 392.5,373.0 393.2,371.9 394.0,371.5 C 394.7,371.1 395.5,371.1 396.2,370.9 C 397.0,370.6 397.7,370.1 398.4,369.9 C 399.2,369.7 399.9,369.9 400.7,369.7 C 401.4,369.5 402.2,368.9 402.9,368.7 C 403.7,368.5 404.4,368.6 405.1,368.5 C 405.9,368.4 406.6,368.3 407.4,368.1 C 408.1,367.9 408.9,367.7 409.6,367.1 C 410.4,366.6 411.1,365.6 411.9,365.1 C 412.6,364.5 413.3,364.3 414.1,363.9 C 414.8,363.5 415.6,363.4 416.3,362.7 C 417.1,362.0 417.8,360.3 418.6,359.5 C 419.3,358.7 420.0,358.3 420.8,357.8 C 421.5,357.4 422.3,357.3 423.0,356.6 C 423.8,355.9 424.5,354.2 425.3,353.5 C 426.0,352.8 426.8,352.7 427.5,352.3 C 428.2,351.9 429.0,351.6 429.7,351.1 C 430.5,35Line truncated
<path d="M 90.0,462.2 C 90.4,462.2 91.9,461.9 92.2,461.8 C 93.0,461.6 93.7,461.5 94.5,461.3 C 95.2,461.1 96.0,460.9 96.7,460.8 C 97.5,460.7 98.2,460.8 98.9,460.8 C 99.7,460.7 100.4,460.7 101.2,460.6 C 101.9,460.6 102.7,460.5 103.4,460.5 C 104.2,460.4 104.5,460.4 105.6,460.3 C 106.8,460.3 109.0,460.2 110.1,460.2 C 111.2,460.1 111.6,460.1 112.4,460.0 C 113.1,460.0 113.8,460.0 114.6,460.0 C 115.3,460.0 116.1,460.0 116.8,459.9 C 117.6,459.9 118.3,459.9 119.1,459.9 C 119.8,459.8 120.5,459.8 121.3,459.7 C 122.0,459.7 122.8,459.7 123.5,459.5 C 124.3,459.3 125.0,459.1 125.8,458.7 C 126.5,458.4 127.3,457.6 128.0,457.4 C 128.7,457.1 129.5,457.2 130.2,457.2 C 131.0,457.2 131.7,457.1 132.5,457.1 C 133.2,457.1 133.6,457.0 134.7,457.0 C 135.8,457.0 137.7,457.0 139.2,456.9 C 140.7,456.9 142.5,456.9 143.6,456.8 C 144.8,456.8 145.1,456.7 145.9,456.7 C 146.6,456.7 147.4,456.6 148.1,456.6 C 148.9,456.6 149.6,456.5 150.3,456.5 C 151.1,456.5 151.8,456.5 152.6,456.4 C 153.3,456.4 153.7,456.4 154.8,456.4 C 155.9,456.4 157.4,456.3 159.3,456.3 C 161.2,456.2 164.1,456.2 166.0,456.2 C 167.9,456.2 168.2,456.2 170.5,456.2 C 172.7,456.1 177.5,456.0 179.4,455.9 C 181.3,455.9 180.5,455.9 181.6,455.9 C 182.8,455.9 184.2,455.9 186.1,455.8 C 188.0,455.8 189.1,455.8 192.8,455.7 C 196.5,455.7 205.5,455.6 208.5,455.6 C 211.4,455.5 210.0,455.5 210.7,455.5 C 211.4,455.5 211.8,456.1 212.9,455.4 C 214.0,454.8 216.3,454.2 217.4,451.4 C 218.5,448.6 218.9,440.8 219.6,438.4 C 220.4,436.0 221.1,437.4 221.9,437.0 C 222.6,436.6 223.4,436.4 224.1,436.1 C 224.9,435.9 225.6,435.7 226.3,435.5 C 227.1,435.4 227.8,435.4 228.6,435.3 C 229.3,435.2 230.1,435.0 230.8,434.9 C 231.6,434.9 232.3,434.9 233.0,434.9 C 233.8,434.8 234.5,434.6 235.3,434.5 C 236.0,434.4 236.8,434.5 237.5,434.3 C 238.3,434.2 239.0,433.9 239.8,433.7 C 240.5,433.6 241.2,433.7 242.0,433.6 C 242.7,433.5 243.1,433.3 244.2,433.2 C 245.3,433.1 247.6,433.1 248.7,433.0 C 249.8,433.0 250.2,432.9 250.9,432.9 C 251.7,432.8 252.4,432.8 253.2,432.7 C 253.9,432.6 254.7,432.3 255.4,432.2 C 256.1,432.0 256.9,432.7 257.6,431.8 C 258.4,430.9 259.1,427.7 259.9,426.7 C 260.6,425.6 261.4,425.8 262.1,425.6 C 262.8,425.4 263.6,425.7 264.3,425.5 C 265.1,425.4 265.8,425.0 266.6,424.8 C 267.3,424.5 268.1,424.3 268.8,423.8 C 269.6,423.3 270.3,422.3 271.0,421.7 C 271.8,421.1 272.5,420.6 273.3,420.2 C 274.0,419.7 274.8,419.4 275.5,419.1 C 276.3,418.9 277.0,418.7 277.7,418.6 C 278.5,418.5 279.2,418.5 280.0,418.4 C 280.7,418.3 281.5,418.1 282.2,418.0 C 283.0,417.9 283.7,417.9 284.5,417.9 C 285.2,417.8 285.9,417.8 286.7,417.6 C 287.4,417.5 288.2,417.6 288.9,417.1 C 289.7,416.7 290.4,415.7 291.2,415.2 C 291.9,414.7 292.6,414.5 293.4,414.1 C 294.1,413.8 294.9,414.5 295.6,412.9 C 296.4,411.4 297.1,406.6 297.9,404.7 C 298.6,402.9 299.4,402.6 300.1,401.8 C 300.8,401.0 301.6,400.5 302.3,400.0 C 303.1,399.4 303.8,398.9 304.6,398.5 C 305.3,398.1 306.1,397.7 306.8,397.5 C 307.5,397.2 308.3,397.1 309.0,396.9 C 309.8,396.7 310.5,396.4 311.3,396.1 C 312.0,395.8 312.8,395.4 313.5,395.1 C 314.3,394.8 315.0,394.9 315.7,394.6 C 316.5,394.4 317.2,393.9 318.0,393.6 C 318.7,393.3 319.5,393.0 320.2,392.7 C 321.0,392.5 321.7,392.3 322.5,392.1 C 323.2,392.0 323.9,392.0 324.7,391.8 C 325.4,391.7 326.2,391.5 326.9,391.3 C 327.7,391.2 328.4,391.0 329.2,390.8 C 329.9,390.6 330.6,390.4 331.4,390.1 C 332.1,389.7 332.9,389.2 333.6,388.9 C 334.4,388.6 335.1,388.5 335.9,388.3 C 336.6,388.2 337.4,388.1 338.1,387.9 C 338.8,387.8 339.6,387.7 340.3,387.6 C 341.1,387.5 341.8,387.4 342.6,387.3 C 343.3,387.2 344.1,387.2 344.8,387.1 C 345.5,387.0 346.3,386.8 347.0,386.5 C 347.8,386.2 348.5,385.8 349.3,385.4 C 350.0,385.1 350.8,384.7 351.5,384.5 C 352.3,384.3 353.0,384.2 353.7,384.1 C 354.5,384.0 355.2,383.9 356.0,383.7 C 356.7,383.4 357.5,382.9 358.2,382.7 C 359.0,382.5 359.7,382.5 360.4,382.4 C 361.2,382.3 361.9,382.0 362.7,381.9 C 363.4,381.7 364.2,381.5 364.9,381.3 C 365.7,381.1 366.4,380.9 367.2,380.7 C 367.9,380.5 368.6,380.4 369.4,380.2 C 370.1,380.0 370.9,379.6 371.6,379.4 C 372.4,379.2 373.1,379.0 373.9,378.8 C 374.6,378.6 375.3,378.2 376.1,378.0 C 376.8,377.7 377.6,377.4 378.3,377.2 C 379.1,376.9 379.8,376.8 380.6,376.4 C 381.3,376.0 382.1,375.1 382.8,374.7 C 383.5,374.4 384.3,374.3 385.0,374.2 C 385.8,374.1 386.5,374.1 387.3,374.0 C 388.0,373.9 388.8,373.7 389.5,373.6 C 390.2,373.5 391.0,373.7 391.7,373.3 C 392.5,373.0 393.2,371.9 394.0,371.5 C 394.7,371.1 395.5,371.1 396.2,370.9 C 397.0,370.6 397.7,370.1 398.4,369.9 C 399.2,369.7 399.9,369.9 400.7,369.7 C 401.4,369.5 402.2,368.9 402.9,368.7 C 403.7,368.5 404.4,368.6 405.1,368.5 C 405.9,368.4 406.6,368.3 407.4,368.1 C 408.1,367.9 408.9,367.7 409.6,367.1 C 410.4,366.6 411.1,365.6 411.9,365.1 C 412.6,364.5 413.3,364.3 414.1,363.9 C 414.8,363.5 415.6,363.4 416.3,362.7 C 417.1,362.0 417.8,360.3 418.6,359.5 C 419.3,358.7 420.0,358.3 420.8,357.8 C 421.5,357.4 422.3,357.3 423.0,356.6 C 423.8,355.9 424.5,354.2 425.3,353.5 C 426.0,352.8 426.8,352.7 427.5,352.3 C 428.2,351.9 429.0,351.6 429.7,351.1 C 430.5,35Line truncated
<circle cx="765.0" cy="80.1" r="5" fill="#dd4528" stroke="#fff" stroke-width="2" filter="url(#xkcdify)"/>
<text x="753.0" y="77.0" fill="#dd4528" font-size="18" font-family="Handlee, cursive" font-weight="bold" text-anchor="end">6999</text>
<path d="M 90.0,462.3 C 90.4,462.3 91.8,462.0 92.2,461.9 C 92.9,461.8 93.6,461.7 94.4,461.5 C 95.1,461.4 95.8,461.2 96.6,461.1 C 97.3,461.0 98.0,461.1 98.7,461.0 C 99.5,461.0 100.2,461.0 100.9,460.9 C 101.7,460.9 102.4,460.8 103.1,460.8 C 103.8,460.7 104.2,460.7 105.3,460.7 C 106.4,460.6 108.6,460.6 109.7,460.5 C 110.8,460.5 111.1,460.4 111.8,460.4 C 112.6,460.4 113.3,460.4 114.0,460.4 C 114.8,460.4 115.5,460.3 116.2,460.3 C 116.9,460.3 117.7,460.3 118.4,460.3 C 119.1,460.2 119.9,460.2 120.6,460.1 C 121.3,460.1 122.0,460.1 122.8,459.9 C 123.5,459.8 124.2,459.6 125.0,459.3 C 125.7,459.0 126.4,458.3 127.1,458.1 C 127.9,457.8 128.6,458.0 129.3,457.9 C 130.0,457.9 130.8,457.9 131.5,457.8 C 132.2,457.8 132.6,457.8 133.7,457.7 C 134.8,457.7 136.6,457.7 138.1,457.7 C 139.5,457.7 141.3,457.6 142.4,457.6 C 143.5,457.6 143.9,457.5 144.6,457.5 C 145.3,457.5 146.1,457.4 146.8,457.4 C 147.5,457.4 148.3,457.3 149.0,457.3 C 149.7,457.3 150.4,457.3 151.2,457.3 C 151.9,457.2 152.3,457.2 153.3,457.2 C 154.4,457.2 155.9,457.1 157.7,457.1 C 159.5,457.1 162.5,457.1 164.3,457.1 C 166.1,457.0 166.5,457.1 168.6,457.0 C 170.8,457.0 175.6,456.9 177.4,456.8 C 179.2,456.8 178.5,456.8 179.6,456.8 C 180.7,456.8 182.1,456.8 183.9,456.7 C 185.8,456.7 186.8,456.7 190.5,456.6 C 194.1,456.6 202.9,456.5 205.8,456.5 C 208.7,456.5 207.2,456.5 208.0,456.4 C 208.7,456.4 209.1,457.0 210.1,456.4 C 211.2,455.8 213.4,455.3 214.5,452.9 C 215.6,450.4 216.0,443.6 216.7,441.5 C 217.4,439.4 218.2,440.6 218.9,440.3 C 219.6,439.9 220.3,439.7 221.1,439.5 C 221.8,439.3 222.5,439.1 223.3,439.0 C 224.0,438.8 224.7,438.8 225.4,438.7 C 226.2,438.6 226.9,438.5 227.6,438.4 C 228.3,438.4 229.1,438.5 229.8,438.4 C 230.5,438.3 231.3,438.1 232.0,438.1 C 232.7,438.0 233.4,438.0 234.2,437.9 C 234.9,437.8 235.6,437.5 236.4,437.4 C 237.1,437.3 237.8,437.3 238.5,437.2 C 239.3,437.2 239.6,437.0 240.7,436.9 C 241.8,436.8 244.0,436.8 245.1,436.8 C 246.2,436.7 246.6,436.7 247.3,436.6 C 248.0,436.6 248.7,436.6 249.5,436.5 C 250.2,436.4 250.9,436.2 251.7,436.0 C 252.4,435.9 253.1,436.5 253.8,435.7 C 254.6,434.9 255.3,432.1 256.0,431.2 C 256.7,430.3 257.5,430.4 258.2,430.3 C 258.9,430.1 259.7,430.3 260.4,430.2 C 261.1,430.1 261.8,429.8 262.6,429.5 C 263.3,429.3 264.0,429.1 264.8,428.7 C 265.5,428.2 266.2,427.4 266.9,426.9 C 267.7,426.4 268.4,425.9 269.1,425.5 C 269.9,425.1 270.6,424.8 271.3,424.6 C 272.0,424.4 272.8,424.3 273.5,424.2 C 274.2,424.1 275.0,424.1 275.7,424.0 C 276.4,423.9 277.1,423.7 277.9,423.6 C 278.6,423.5 279.3,423.6 280.0,423.5 C 280.8,423.5 281.5,423.4 282.2,423.3 C 283.0,423.2 283.7,423.2 284.4,422.9 C 285.1,422.5 285.9,421.6 286.6,421.2 C 287.3,420.7 288.1,420.6 288.8,420.2 C 289.5,419.9 290.2,420.6 291.0,419.2 C 291.7,417.8 292.4,413.6 293.2,412.0 C 293.9,410.4 294.6,410.1 295.3,409.4 C 296.1,408.7 296.8,408.3 297.5,407.8 C 298.3,407.4 299.0,406.9 299.7,406.6 C 300.4,406.2 301.2,405.9 301.9,405.6 C 302.6,405.4 303.3,405.4 304.1,405.2 C 304.8,405.0 305.5,404.8 306.3,404.5 C 307.0,404.2 307.7,403.8 308.4,403.6 C 309.2,403.4 309.9,403.4 310.6,403.2 C 311.4,402.9 312.1,402.5 312.8,402.2 C 313.5,402.0 314.3,401.7 315.0,401.5 C 315.7,401.3 316.5,401.1 317.2,401.0 C 317.9,400.9 318.6,400.8 319.4,400.7 C 320.1,400.6 320.8,400.4 321.6,400.3 C 322.3,400.1 323.0,400.0 323.7,399.8 C 324.5,399.6 325.2,399.5 325.9,399.2 C 326.7,398.9 327.4,398.4 328.1,398.1 C 328.8,397.9 329.6,397.8 330.3,397.7 C 331.0,397.5 331.7,397.4 332.5,397.3 C 333.2,397.2 333.9,397.1 334.7,397.0 C 335.4,396.9 336.1,396.8 336.8,396.7 C 337.6,396.7 338.3,396.7 339.0,396.6 C 339.8,396.5 340.5,396.3 341.2,396.1 C 341.9,395.8 342.7,395.4 343.4,395.1 C 344.1,394.8 344.9,394.5 345.6,394.3 C 346.3,394.1 347.0,394.1 347.8,394.0 C 348.5,393.8 349.2,393.8 350.0,393.6 C 350.7,393.4 351.4,392.9 352.1,392.7 C 352.9,392.5 353.6,392.6 354.3,392.5 C 355.0,392.4 355.8,392.2 356.5,392.0 C 357.2,391.8 358.0,391.7 358.7,391.5 C 359.4,391.4 360.1,391.2 360.9,391.0 C 361.6,390.8 362.3,390.8 363.1,390.6 C 363.8,390.4 364.5,390.1 365.2,389.8 C 366.0,389.6 366.7,389.5 367.4,389.3 C 368.2,389.1 368.9,388.8 369.6,388.6 C 370.3,388.4 371.1,388.1 371.8,387.9 C 372.5,387.7 373.3,387.6 374.0,387.3 C 374.7,386.9 375.4,386.1 376.2,385.8 C 376.9,385.5 377.6,385.4 378.3,385.3 C 379.1,385.2 379.8,385.2 380.5,385.1 C 381.3,385.0 382.0,384.9 382.7,384.8 C 383.4,384.7 384.2,384.8 384.9,384.5 C 385.6,384.2 386.4,383.3 387.1,382.9 C 387.8,382.5 388.5,382.6 389.3,382.4 C 390.0,382.2 390.7,381.7 391.5,381.6 C 392.2,381.4 392.9,381.5 393.6,381.3 C 394.4,381.1 395.1,380.7 395.8,380.5 C 396.6,380.3 397.3,380.4 398.0,380.3 C 398.7,380.2 399.5,380.2 400.2,380.0 C 400.9,379.8 401.7,379.6 402.4,379.1 C 403.1,378.7 403.8,377.8 404.6,377.3 C 405.3,376.8 406.0,376.6 406.7,376.3 C 407.5,376.0 408.2,375.9 408.9,375.2 C 409.7,374.6 410.4,373.2 411.1,372.5 C 411.8,371.8 412.6,371.5 413.3,371.0 C 414.0,370.6 414.8,370.6 415.5,369.9 C 416.2,369.3 416.9,367.9 417.7,367.2 C 418.4,366.6 419.1,366.5 419.9,366.2 C 420.6,365.8 421.3,365.6 422.0,365.1 C 422.8,36Line truncated
<path d="M 90.0,462.3 C 90.4,462.3 91.8,462.0 92.2,461.9 C 92.9,461.8 93.6,461.7 94.4,461.5 C 95.1,461.4 95.8,461.2 96.6,461.1 C 97.3,461.0 98.0,461.1 98.7,461.0 C 99.5,461.0 100.2,461.0 100.9,460.9 C 101.7,460.9 102.4,460.8 103.1,460.8 C 103.8,460.7 104.2,460.7 105.3,460.7 C 106.4,460.6 108.6,460.6 109.7,460.5 C 110.8,460.5 111.1,460.4 111.8,460.4 C 112.6,460.4 113.3,460.4 114.0,460.4 C 114.8,460.4 115.5,460.3 116.2,460.3 C 116.9,460.3 117.7,460.3 118.4,460.3 C 119.1,460.2 119.9,460.2 120.6,460.1 C 121.3,460.1 122.0,460.1 122.8,459.9 C 123.5,459.8 124.2,459.6 125.0,459.3 C 125.7,459.0 126.4,458.3 127.1,458.1 C 127.9,457.8 128.6,458.0 129.3,457.9 C 130.0,457.9 130.8,457.9 131.5,457.8 C 132.2,457.8 132.6,457.8 133.7,457.7 C 134.8,457.7 136.6,457.7 138.1,457.7 C 139.5,457.7 141.3,457.6 142.4,457.6 C 143.5,457.6 143.9,457.5 144.6,457.5 C 145.3,457.5 146.1,457.4 146.8,457.4 C 147.5,457.4 148.3,457.3 149.0,457.3 C 149.7,457.3 150.4,457.3 151.2,457.3 C 151.9,457.2 152.3,457.2 153.3,457.2 C 154.4,457.2 155.9,457.1 157.7,457.1 C 159.5,457.1 162.5,457.1 164.3,457.1 C 166.1,457.0 166.5,457.1 168.6,457.0 C 170.8,457.0 175.6,456.9 177.4,456.8 C 179.2,456.8 178.5,456.8 179.6,456.8 C 180.7,456.8 182.1,456.8 183.9,456.7 C 185.8,456.7 186.8,456.7 190.5,456.6 C 194.1,456.6 202.9,456.5 205.8,456.5 C 208.7,456.5 207.2,456.5 208.0,456.4 C 208.7,456.4 209.1,457.0 210.1,456.4 C 211.2,455.8 213.4,455.3 214.5,452.9 C 215.6,450.4 216.0,443.6 216.7,441.5 C 217.4,439.4 218.2,440.6 218.9,440.3 C 219.6,439.9 220.3,439.7 221.1,439.5 C 221.8,439.3 222.5,439.1 223.3,439.0 C 224.0,438.8 224.7,438.8 225.4,438.7 C 226.2,438.6 226.9,438.5 227.6,438.4 C 228.3,438.4 229.1,438.5 229.8,438.4 C 230.5,438.3 231.3,438.1 232.0,438.1 C 232.7,438.0 233.4,438.0 234.2,437.9 C 234.9,437.8 235.6,437.5 236.4,437.4 C 237.1,437.3 237.8,437.3 238.5,437.2 C 239.3,437.2 239.6,437.0 240.7,436.9 C 241.8,436.8 244.0,436.8 245.1,436.8 C 246.2,436.7 246.6,436.7 247.3,436.6 C 248.0,436.6 248.7,436.6 249.5,436.5 C 250.2,436.4 250.9,436.2 251.7,436.0 C 252.4,435.9 253.1,436.5 253.8,435.7 C 254.6,434.9 255.3,432.1 256.0,431.2 C 256.7,430.3 257.5,430.4 258.2,430.3 C 258.9,430.1 259.7,430.3 260.4,430.2 C 261.1,430.1 261.8,429.8 262.6,429.5 C 263.3,429.3 264.0,429.1 264.8,428.7 C 265.5,428.2 266.2,427.4 266.9,426.9 C 267.7,426.4 268.4,425.9 269.1,425.5 C 269.9,425.1 270.6,424.8 271.3,424.6 C 272.0,424.4 272.8,424.3 273.5,424.2 C 274.2,424.1 275.0,424.1 275.7,424.0 C 276.4,423.9 277.1,423.7 277.9,423.6 C 278.6,423.5 279.3,423.6 280.0,423.5 C 280.8,423.5 281.5,423.4 282.2,423.3 C 283.0,423.2 283.7,423.2 284.4,422.9 C 285.1,422.5 285.9,421.6 286.6,421.2 C 287.3,420.7 288.1,420.6 288.8,420.2 C 289.5,419.9 290.2,420.6 291.0,419.2 C 291.7,417.8 292.4,413.6 293.2,412.0 C 293.9,410.4 294.6,410.1 295.3,409.4 C 296.1,408.7 296.8,408.3 297.5,407.8 C 298.3,407.4 299.0,406.9 299.7,406.6 C 300.4,406.2 301.2,405.9 301.9,405.6 C 302.6,405.4 303.3,405.4 304.1,405.2 C 304.8,405.0 305.5,404.8 306.3,404.5 C 307.0,404.2 307.7,403.8 308.4,403.6 C 309.2,403.4 309.9,403.4 310.6,403.2 C 311.4,402.9 312.1,402.5 312.8,402.2 C 313.5,402.0 314.3,401.7 315.0,401.5 C 315.7,401.3 316.5,401.1 317.2,401.0 C 317.9,400.9 318.6,400.8 319.4,400.7 C 320.1,400.6 320.8,400.4 321.6,400.3 C 322.3,400.1 323.0,400.0 323.7,399.8 C 324.5,399.6 325.2,399.5 325.9,399.2 C 326.7,398.9 327.4,398.4 328.1,398.1 C 328.8,397.9 329.6,397.8 330.3,397.7 C 331.0,397.5 331.7,397.4 332.5,397.3 C 333.2,397.2 333.9,397.1 334.7,397.0 C 335.4,396.9 336.1,396.8 336.8,396.7 C 337.6,396.7 338.3,396.7 339.0,396.6 C 339.8,396.5 340.5,396.3 341.2,396.1 C 341.9,395.8 342.7,395.4 343.4,395.1 C 344.1,394.8 344.9,394.5 345.6,394.3 C 346.3,394.1 347.0,394.1 347.8,394.0 C 348.5,393.8 349.2,393.8 350.0,393.6 C 350.7,393.4 351.4,392.9 352.1,392.7 C 352.9,392.5 353.6,392.6 354.3,392.5 C 355.0,392.4 355.8,392.2 356.5,392.0 C 357.2,391.8 358.0,391.7 358.7,391.5 C 359.4,391.4 360.1,391.2 360.9,391.0 C 361.6,390.8 362.3,390.8 363.1,390.6 C 363.8,390.4 364.5,390.1 365.2,389.8 C 366.0,389.6 366.7,389.5 367.4,389.3 C 368.2,389.1 368.9,388.8 369.6,388.6 C 370.3,388.4 371.1,388.1 371.8,387.9 C 372.5,387.7 373.3,387.6 374.0,387.3 C 374.7,386.9 375.4,386.1 376.2,385.8 C 376.9,385.5 377.6,385.4 378.3,385.3 C 379.1,385.2 379.8,385.2 380.5,385.1 C 381.3,385.0 382.0,384.9 382.7,384.8 C 383.4,384.7 384.2,384.8 384.9,384.5 C 385.6,384.2 386.4,383.3 387.1,382.9 C 387.8,382.5 388.5,382.6 389.3,382.4 C 390.0,382.2 390.7,381.7 391.5,381.6 C 392.2,381.4 392.9,381.5 393.6,381.3 C 394.4,381.1 395.1,380.7 395.8,380.5 C 396.6,380.3 397.3,380.4 398.0,380.3 C 398.7,380.2 399.5,380.2 400.2,380.0 C 400.9,379.8 401.7,379.6 402.4,379.1 C 403.1,378.7 403.8,377.8 404.6,377.3 C 405.3,376.8 406.0,376.6 406.7,376.3 C 407.5,376.0 408.2,375.9 408.9,375.2 C 409.7,374.6 410.4,373.2 411.1,372.5 C 411.8,371.8 412.6,371.5 413.3,371.0 C 414.0,370.6 414.8,370.6 415.5,369.9 C 416.2,369.3 416.9,367.9 417.7,367.2 C 418.4,366.6 419.1,366.5 419.9,366.2 C 420.6,365.8 421.3,365.6 422.0,365.1 C 422.8,36Line truncated
<circle cx="765.0" cy="119.4" r="5" fill="#dd4528" stroke="#fff" stroke-width="2" filter="url(#xkcdify)"/>
<text x="753.0" y="77.0" fill="#dd4528" font-size="18" font-family="Handlee, cursive" font-weight="bold" text-anchor="end">7176</text>
<a href="https://github.com/carsteneu/mystarhistory" target="_blank" rel="noopener"><text x="763" y="527" font-size="13" font-family="Handlee, cursive" fill="#000" text-anchor="end">Made with carsteneu/mystarhistory</text></a>
</svg>

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 37 KiB

+139
View File
@@ -0,0 +1,139 @@
/**
* Pick Chromium's Linux password store before app.whenReady().
*
* Chromium sniffs XDG_CURRENT_DESKTOP and only enables gnome-libsecret /
* kwallet for GNOME-family and KDE/Plasma names. Other compositors
* (niri, sway, Hyprland, …) fall through to BASIC_TEXT even when
* Secret Service is running, so safeStorage.isEncryptionAvailable()
* is false (#4189).
*
* Must run before app.whenReady(); Electron ignores --password-store
* afterwards.
*/
const { execFileSync } = require("node:child_process");
const SECRET_SERVICE_NAME = "org.freedesktop.secrets";
const PASSWORD_STORE_SWITCH = "password-store";
const GNOME_LIBSECRET = "gnome-libsecret";
const DEFAULT_PROBE_TIMEOUT_MS = 1000;
const SECRET_SERVICE_PROBE_COMMANDS = [
{
file: "busctl",
args: [
"--user",
"call",
"org.freedesktop.DBus",
"/org/freedesktop/DBus",
"org.freedesktop.DBus",
"NameHasOwner",
"s",
SECRET_SERVICE_NAME,
],
},
{
file: "gdbus",
args: [
"call",
"--session",
"--dest",
"org.freedesktop.DBus",
"--object-path",
"/org/freedesktop/DBus",
"--method",
"org.freedesktop.DBus.NameHasOwner",
SECRET_SERVICE_NAME,
],
},
{
file: "dbus-send",
args: [
"--session",
"--print-reply=literal",
"--dest=org.freedesktop.DBus",
"/org/freedesktop/DBus",
"org.freedesktop.DBus.NameHasOwner",
`string:${SECRET_SERVICE_NAME}`,
],
},
];
function desktopTokens(env = process.env) {
const raw = [env?.XDG_CURRENT_DESKTOP, env?.XDG_SESSION_DESKTOP]
.filter((value) => typeof value === "string" && value.length > 0)
.join(":");
return raw
.split(":")
.map((token) => token.trim().toLowerCase())
.filter(Boolean);
}
function isKdePlasmaDesktop(env = process.env) {
return desktopTokens(env).some((token) => token === "kde" || token === "plasma");
}
function parseNameHasOwnerOutput(stdout) {
const text = String(stdout ?? "");
if (/\bfalse\b/i.test(text)) return false;
if (/\btrue\b/i.test(text)) return true;
return false;
}
function defaultProbeSecretService(options = {}) {
const run = options.execFileSync ?? execFileSync;
const timeout = options.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS;
for (const command of SECRET_SERVICE_PROBE_COMMANDS) {
try {
const stdout = run(command.file, command.args, {
encoding: "utf8",
timeout,
stdio: ["ignore", "pipe", "ignore"],
});
return parseNameHasOwnerOutput(stdout);
} catch {
// Missing binary, timeout, or D-Bus error: try the next helper.
}
}
return false;
}
function applyLinuxPasswordStore(app, options = {}) {
const platform = options.platform ?? process.platform;
if (platform !== "linux") return false;
if (!app?.commandLine) return false;
const env = options.env ?? process.env;
const hasSwitch = options.hasSwitch
?? ((name) => Boolean(app.commandLine.hasSwitch?.(name)));
const appendSwitch = options.appendSwitch
?? ((name, value) => app.commandLine.appendSwitch(name, value));
if (hasSwitch(PASSWORD_STORE_SWITCH)) return false;
if (isKdePlasmaDesktop(env)) return false;
let available = false;
try {
const probe = options.probeSecretService ?? defaultProbeSecretService;
available = Boolean(probe({
execFileSync: options.execFileSync,
timeoutMs: options.timeoutMs,
}));
} catch {
return false;
}
if (!available) return false;
appendSwitch(PASSWORD_STORE_SWITCH, GNOME_LIBSECRET);
return true;
}
module.exports = {
SECRET_SERVICE_NAME,
PASSWORD_STORE_SWITCH,
GNOME_LIBSECRET,
applyLinuxPasswordStore,
isKdePlasmaDesktop,
parseNameHasOwnerOutput,
defaultProbeSecretService,
};
+152
View File
@@ -0,0 +1,152 @@
const test = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const {
GNOME_LIBSECRET,
PASSWORD_STORE_SWITCH,
applyLinuxPasswordStore,
defaultProbeSecretService,
isKdePlasmaDesktop,
parseNameHasOwnerOutput,
} = require("./linuxPasswordStore.cjs");
function createApp({ existingSwitch = false } = {}) {
const appended = [];
return {
appended,
app: {
commandLine: {
hasSwitch(name) {
return existingSwitch && name === PASSWORD_STORE_SWITCH;
},
appendSwitch(name, value) {
appended.push([name, value]);
},
},
},
};
}
function apply(overrides = {}, appOptions = {}) {
const { app, appended } = createApp(appOptions);
const applied = applyLinuxPasswordStore(app, {
platform: "linux",
env: { XDG_CURRENT_DESKTOP: "niri" },
probeSecretService: () => true,
...overrides,
});
return { applied, appended };
}
test("niri with Secret Service appends gnome-libsecret", () => {
const { applied, appended } = apply({
env: { XDG_CURRENT_DESKTOP: "niri" },
});
assert.equal(applied, true);
assert.deepEqual(appended, [[PASSWORD_STORE_SWITCH, GNOME_LIBSECRET]]);
});
test("sway and Hyprland with Secret Service append gnome-libsecret", () => {
for (const desktop of ["sway", "Hyprland"]) {
const { applied, appended } = apply({
env: { XDG_CURRENT_DESKTOP: desktop },
});
assert.equal(applied, true, desktop);
assert.deepEqual(appended, [[PASSWORD_STORE_SWITCH, GNOME_LIBSECRET]], desktop);
}
});
test("KDE and Plasma desktops do not append even when Secret Service is present", () => {
for (const env of [
{ XDG_CURRENT_DESKTOP: "KDE" },
{ XDG_CURRENT_DESKTOP: "plasma" },
{ XDG_CURRENT_DESKTOP: "ubuntu:KDE" },
{ XDG_SESSION_DESKTOP: "plasma" },
]) {
const { applied, appended } = apply({ env });
assert.equal(applied, false, JSON.stringify(env));
assert.deepEqual(appended, [], JSON.stringify(env));
}
});
test("existing --password-store switch is left alone", () => {
const { applied, appended } = apply({}, { existingSwitch: true });
assert.equal(applied, false);
assert.deepEqual(appended, []);
});
test("absent, throwing, or empty Secret Service probe does not append", () => {
for (const probeSecretService of [
() => false,
() => {
throw new Error("probe failed");
},
]) {
const { applied, appended } = apply({ probeSecretService });
assert.equal(applied, false);
assert.deepEqual(appended, []);
}
});
test("non-Linux platforms do not append", () => {
for (const platform of ["darwin", "win32"]) {
const { applied, appended } = apply({ platform });
assert.equal(applied, false, platform);
assert.deepEqual(appended, [], platform);
}
});
test("isKdePlasmaDesktop splits colon-separated desktop names", () => {
assert.equal(isKdePlasmaDesktop({ XDG_CURRENT_DESKTOP: "niri" }), false);
assert.equal(isKdePlasmaDesktop({ XDG_CURRENT_DESKTOP: "ubuntu:GNOME" }), false);
assert.equal(isKdePlasmaDesktop({ XDG_CURRENT_DESKTOP: "ubuntu:KDE" }), true);
assert.equal(isKdePlasmaDesktop({ XDG_SESSION_DESKTOP: "Plasma" }), true);
});
test("parseNameHasOwnerOutput understands busctl, gdbus, and dbus-send", () => {
assert.equal(parseNameHasOwnerOutput("b true\n"), true);
assert.equal(parseNameHasOwnerOutput("b false\n"), false);
assert.equal(parseNameHasOwnerOutput("(true,)"), true);
assert.equal(parseNameHasOwnerOutput("(false,)"), false);
assert.equal(parseNameHasOwnerOutput("boolean true"), true);
assert.equal(parseNameHasOwnerOutput(" boolean false"), false);
assert.equal(parseNameHasOwnerOutput(""), false);
});
test("defaultProbeSecretService falls through missing binaries and reads the first reply", () => {
const calls = [];
const execFileSync = (file, args) => {
calls.push(file);
if (file === "busctl") {
const err = new Error("not found");
err.code = "ENOENT";
throw err;
}
if (file === "gdbus") return "(true,)\n";
throw new Error(`unexpected ${file}`);
};
assert.equal(defaultProbeSecretService({ execFileSync }), true);
assert.deepEqual(calls, ["busctl", "gdbus"]);
});
test("defaultProbeSecretService is false when every helper fails", () => {
assert.equal(defaultProbeSecretService({
execFileSync() {
throw new Error("missing");
},
}), false);
});
test("main.cjs applies the helper before app.whenReady()", () => {
const source = fs.readFileSync(path.join(__dirname, "main.cjs"), "utf8");
const helperIdx = source.indexOf('require("./linuxPasswordStore.cjs")');
const callIdx = source.indexOf("applyLinuxPasswordStore(app)");
const readyIdx = source.indexOf("app.whenReady()");
assert.ok(helperIdx >= 0, "requires linuxPasswordStore.cjs");
assert.ok(callIdx >= 0, "calls applyLinuxPasswordStore(app)");
assert.ok(readyIdx >= 0, "has app.whenReady()");
assert.ok(helperIdx < readyIdx, "require is before whenReady");
assert.ok(callIdx < readyIdx, "call is before whenReady");
});
+6
View File
@@ -255,6 +255,12 @@ if (process.env.NETCATTY_COMPAT_GPU !== "1") {
app.commandLine.appendSwitch("enable-zero-copy");
}
// Linux: Chromium only auto-selects gnome-libsecret / kwallet from
// GNOME/KDE desktop names. Other compositors (niri/sway/Hyprland) fall
// through to BASIC_TEXT even when Secret Service is running (#4189).
const { applyLinuxPasswordStore } = require("./linuxPasswordStore.cjs");
applyLinuxPasswordStore(app);
// Silence noisy DevTools Autofill CDP errors (Electron's backend doesn't expose this domain)
app.on("web-contents-created", (_event, contents) => {
if (contents.getType() !== "devtools") return;
+34
View File
@@ -2,8 +2,10 @@
appimageTools,
fetchurl,
lib,
openssh,
release ? import ./release.nix,
stdenv,
writeShellScriptBin,
}:
let
@@ -22,10 +24,42 @@ let
appimageContents = appimageTools.extractType2 {
inherit pname version src;
};
# home-manager's ~/.ssh/config is a root-owned symlink into the nix store.
# Unprivileged bwrap (wrapType2 / buildFHSEnv) maps only the caller uid, so
# that file appears as overflowuid 65534 and OpenSSH refuses the default
# user config with "Bad owner or permissions". A -F path skips that check.
# The wrapper is injected via extraPkgs so it lands on the FHS /usr/bin,
# which buildFHSEnv prepends ahead of any bind-mounted host profile PATH.
sshWrapped = writeShellScriptBin "ssh" ''
real_ssh="${openssh}/bin/ssh"
config="''${HOME}/.ssh/config"
if [ -e "$config" ]; then
owner="$(stat -c %u -L "$config" 2>/dev/null || true)"
me="$(id -u)"
if [ -n "$owner" ] && [ "$owner" != "$me" ] && [ "$owner" != 0 ]; then
cache="''${XDG_CACHE_HOME:-$HOME/.cache}/netcatty"
snap="$cache/ssh_config"
mkdir -p "$cache"
# Always refresh: nix store files have frozen old mtimes, so -nt
# never sees a newer home-manager generation after the first copy.
# Publish via a unique staging file + mv so a concurrent ssh -F
# reader never sees a truncated snapshot (cp truncates first).
snap_tmp="$snap.tmp.$$"
cp -L "$config" "$snap_tmp"
chmod 600 "$snap_tmp"
mv -f "$snap_tmp" "$snap"
exec "$real_ssh" -F "$snap" "$@"
fi
fi
exec "$real_ssh" "$@"
'';
in
appimageTools.wrapType2 {
inherit pname version src;
extraPkgs = pkgs: [ sshWrapped ];
extraInstallCommands = ''
desktopSource="$(find ${appimageContents} -maxdepth 3 -name '*.desktop' -print -quit)"
if [ -n "$desktopSource" ]; then