[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
168
components/settings/TerminalCjkFontSelect.tsx
Normal file
168
components/settings/TerminalCjkFontSelect.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { RefreshCw } from 'lucide-react';
|
||||
import { useI18n } from '../../application/i18n/I18nProvider';
|
||||
import {
|
||||
refreshFonts,
|
||||
useFontsLoading,
|
||||
useInstalledFontFamilies,
|
||||
} from '../../application/state/fontStore';
|
||||
import { isFontInstalled } from '../../lib/fontAvailability';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Button } from '../ui/button';
|
||||
import { Combobox, type ComboboxOption } from '../ui/combobox';
|
||||
import {
|
||||
buildTerminalCjkFontOptions,
|
||||
getTerminalCjkFontSelectionStatus,
|
||||
RECOMMENDED_CJK_FONT_FAMILIES,
|
||||
type TerminalCjkFontOptionKind,
|
||||
} from '../../domain/terminalCjkFonts';
|
||||
|
||||
const previewFontFamily = (family: string): string | undefined => {
|
||||
const trimmed = family.trim();
|
||||
if (!trimmed) return undefined;
|
||||
const escaped = trimmed.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||
return `"${escaped}", monospace`;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
value: string;
|
||||
onChange: (next: string) => void;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
label?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export const TerminalCjkFontSelect: React.FC<Props> = ({
|
||||
value,
|
||||
onChange,
|
||||
className,
|
||||
disabled,
|
||||
label,
|
||||
description,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const installedFamilies = useInstalledFontFamilies();
|
||||
const isLoading = useFontsLoading();
|
||||
const [previewValue, setPreviewValue] = useState(value);
|
||||
|
||||
useEffect(() => {
|
||||
setPreviewValue(value);
|
||||
}, [value]);
|
||||
|
||||
const availableRecommendedFamilies = RECOMMENDED_CJK_FONT_FAMILIES.filter(
|
||||
(family) => isFontInstalled(family),
|
||||
);
|
||||
|
||||
const options = useMemo<ComboboxOption[]>(() => {
|
||||
const built = buildTerminalCjkFontOptions({
|
||||
installedFamilies,
|
||||
selectedValue: value,
|
||||
availableRecommendedFamilies,
|
||||
});
|
||||
const kindLabels: Record<TerminalCjkFontOptionKind, string> = {
|
||||
auto: '',
|
||||
recommended: t('settings.terminal.font.cjk.option.recommended'),
|
||||
installed: t('settings.terminal.font.cjk.option.installed'),
|
||||
unverified: t('settings.terminal.font.cjk.option.unverified'),
|
||||
unavailable: t('settings.terminal.font.cjk.option.unavailable'),
|
||||
};
|
||||
|
||||
return built.map((option) => {
|
||||
const label = option.kind === 'auto'
|
||||
? t('settings.terminal.font.cjk.option.auto')
|
||||
: option.value.trim();
|
||||
return {
|
||||
value: option.value,
|
||||
label,
|
||||
sublabel: kindLabels[option.kind] || undefined,
|
||||
labelStyle: option.value
|
||||
? { fontFamily: previewFontFamily(option.value) }
|
||||
: undefined,
|
||||
};
|
||||
});
|
||||
}, [availableRecommendedFamilies, installedFamilies, t, value]);
|
||||
|
||||
const previewSelection = previewValue.trim();
|
||||
const status = getTerminalCjkFontSelectionStatus(
|
||||
previewSelection,
|
||||
installedFamilies,
|
||||
availableRecommendedFamilies,
|
||||
Boolean(previewSelection && isFontInstalled(previewSelection)),
|
||||
);
|
||||
const selectedFontFamily = previewFontFamily(value);
|
||||
const previewFamily = previewFontFamily(previewValue);
|
||||
const controls = (
|
||||
<div className="flex items-center gap-2">
|
||||
<Combobox
|
||||
options={options}
|
||||
value={value}
|
||||
onValueChange={onChange}
|
||||
placeholder={t('settings.terminal.font.cjk.searchPlaceholder')}
|
||||
emptyText={t('settings.terminal.font.cjk.empty')}
|
||||
allowCreate
|
||||
createText={t('settings.terminal.font.cjk.useCustom')}
|
||||
triggerClassName="h-9"
|
||||
inputStyle={{ fontFamily: selectedFontFamily }}
|
||||
onInputValueChange={setPreviewValue}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-9 w-9 shrink-0"
|
||||
aria-label={t('settings.terminal.font.cjk.refresh')}
|
||||
title={t('settings.terminal.font.cjk.refresh')}
|
||||
disabled={disabled || isLoading}
|
||||
onClick={() => void refreshFonts()}
|
||||
>
|
||||
<RefreshCw size={14} className={cn(isLoading && 'animate-spin')} />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
const preview = previewValue.trim() && (
|
||||
<>
|
||||
<pre
|
||||
className="m-0 py-2 text-center text-base leading-7 text-foreground"
|
||||
style={{ fontFamily: previewFamily }}
|
||||
>
|
||||
{'你好 │ ABC │ 123\n123 │ 测试 │ ABC'}
|
||||
</pre>
|
||||
|
||||
{status === 'alignment-risk' && (
|
||||
<p className="m-0 text-center text-xs text-amber-600 dark:text-amber-400">
|
||||
{t('settings.terminal.font.cjk.alignmentWarning')}
|
||||
</p>
|
||||
)}
|
||||
{status === 'unavailable' && (
|
||||
<p className="m-0 text-center text-xs text-muted-foreground">
|
||||
{t('settings.terminal.font.cjk.unavailableWarning')}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
if (label) {
|
||||
return (
|
||||
<div className={cn('grid grid-cols-[minmax(0,1fr)_auto] items-start gap-x-4 gap-y-2 py-3', className)}>
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-medium">{label}</div>
|
||||
{description && <div className="mt-0.5 text-xs text-muted-foreground">{description}</div>}
|
||||
</div>
|
||||
<div className="w-72 shrink-0">{controls}</div>
|
||||
{preview && <div className="col-span-2 justify-self-center">{preview}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('space-y-2', className)}>
|
||||
{controls}
|
||||
{preview}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TerminalCjkFontSelect;
|
||||
Reference in New Issue
Block a user