Files
yaak/src-web/components/Settings/SettingsInterface.tsx
2025-12-10 13:54:22 -08:00

246 lines
8.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { type } from '@tauri-apps/plugin-os';
import { useFonts } from '@yaakapp-internal/fonts';
import { useLicense } from '@yaakapp-internal/license';
import type { EditorKeymap, Settings } from '@yaakapp-internal/models';
import { patchModel, settingsAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import { useState } from 'react';
import { activeWorkspaceAtom } from '../../hooks/useActiveWorkspace';
import { clamp } from '../../lib/clamp';
import { showConfirm } from '../../lib/confirm';
import { invokeCmd } from '../../lib/tauri';
import { CargoFeature } from '../CargoFeature';
import { Button } from '../core/Button';
import { Checkbox } from '../core/Checkbox';
import { Heading } from '../core/Heading';
import { Icon } from '../core/Icon';
import { Link } from '../core/Link';
import { Select } from '../core/Select';
import { HStack, VStack } from '../core/Stacks';
const NULL_FONT_VALUE = '__NULL_FONT__';
const fontSizeOptions = [
8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
].map((n) => ({ label: `${n}`, value: `${n}` }));
const keymaps: { value: EditorKeymap; label: string }[] = [
{ value: 'default', label: 'Default' },
{ value: 'vim', label: 'Vim' },
{ value: 'vscode', label: 'VSCode' },
{ value: 'emacs', label: 'Emacs' },
];
export function SettingsInterface() {
const workspace = useAtomValue(activeWorkspaceAtom);
const settings = useAtomValue(settingsAtom);
const fonts = useFonts();
if (settings == null || workspace == null) {
return null;
}
return (
<VStack space={3} className="mb-4">
<div className="mb-3">
<Heading>Interface</Heading>
<p className="text-text-subtle">Tweak settings related to the user interface.</p>
</div>
<Select
name="switchWorkspaceBehavior"
label="Open workspace behavior"
size="sm"
help="When opening a workspace, should it open in the current window or a new window?"
value={
settings.openWorkspaceNewWindow === true
? 'new'
: settings.openWorkspaceNewWindow === false
? 'current'
: 'ask'
}
onChange={async (v) => {
if (v === 'current') await patchModel(settings, { openWorkspaceNewWindow: false });
else if (v === 'new') await patchModel(settings, { openWorkspaceNewWindow: true });
else await patchModel(settings, { openWorkspaceNewWindow: null });
}}
options={[
{ label: 'Always ask', value: 'ask' },
{ label: 'Open in current window', value: 'current' },
{ label: 'Open in new window', value: 'new' },
]}
/>
<HStack space={2} alignItems="end">
{fonts.data && (
<Select
size="sm"
name="uiFont"
label="Interface font"
value={settings.interfaceFont ?? NULL_FONT_VALUE}
options={[
{ label: 'System default', value: NULL_FONT_VALUE },
...(fonts.data.uiFonts.map((f) => ({
label: f,
value: f,
})) ?? []),
// Some people like monospace fonts for the UI
...(fonts.data.editorFonts.map((f) => ({
label: f,
value: f,
})) ?? []),
]}
onChange={async (v) => {
const interfaceFont = v === NULL_FONT_VALUE ? null : v;
await patchModel(settings, { interfaceFont });
}}
/>
)}
<Select
hideLabel
size="sm"
name="interfaceFontSize"
label="Interface Font Size"
defaultValue="14"
value={`${settings.interfaceFontSize}`}
options={fontSizeOptions}
onChange={(v) => patchModel(settings, { interfaceFontSize: Number.parseInt(v, 10) })}
/>
</HStack>
<HStack space={2} alignItems="end">
{fonts.data && (
<Select
size="sm"
name="editorFont"
label="Editor font"
value={settings.editorFont ?? NULL_FONT_VALUE}
options={[
{ label: 'System default', value: NULL_FONT_VALUE },
...(fonts.data.editorFonts.map((f) => ({
label: f,
value: f,
})) ?? []),
]}
onChange={async (v) => {
const editorFont = v === NULL_FONT_VALUE ? null : v;
await patchModel(settings, { editorFont });
}}
/>
)}
<Select
hideLabel
size="sm"
name="editorFontSize"
label="Editor Font Size"
defaultValue="12"
value={`${settings.editorFontSize}`}
options={fontSizeOptions}
onChange={(v) =>
patchModel(settings, { editorFontSize: clamp(Number.parseInt(v, 10) || 14, 8, 30) })
}
/>
</HStack>
<Select
leftSlot={<Icon icon="keyboard" color="secondary" />}
size="sm"
name="editorKeymap"
label="Editor keymap"
value={`${settings.editorKeymap}`}
options={keymaps}
onChange={(v) => patchModel(settings, { editorKeymap: v })}
/>
<Checkbox
checked={settings.editorSoftWrap}
title="Wrap editor lines"
onChange={(editorSoftWrap) => patchModel(settings, { editorSoftWrap })}
/>
<Checkbox
checked={settings.coloredMethods}
title="Colorize request methods"
onChange={(coloredMethods) => patchModel(settings, { coloredMethods })}
/>
<CargoFeature feature="license">
<LicenseSettings settings={settings} />
</CargoFeature>
<NativeTitlebarSetting settings={settings} />
{type() !== 'macos' && (
<Checkbox
checked={settings.hideWindowControls}
title="Hide window controls"
help="Hide the close/maximize/minimize controls on Windows or Linux"
onChange={(hideWindowControls) => patchModel(settings, { hideWindowControls })}
/>
)}
</VStack>
);
}
function NativeTitlebarSetting({ settings }: { settings: Settings }) {
const [nativeTitlebar, setNativeTitlebar] = useState(settings.useNativeTitlebar);
return (
<div className="flex gap-1 overflow-hidden h-2xs">
<Checkbox
checked={nativeTitlebar}
title="Native title bar"
help="Use the operating system's standard title bar and window controls"
onChange={setNativeTitlebar}
/>
{settings.useNativeTitlebar !== nativeTitlebar && (
<Button
color="primary"
size="2xs"
onClick={async () => {
await patchModel(settings, { useNativeTitlebar: nativeTitlebar });
await invokeCmd('cmd_restart');
}}
>
Apply and Restart
</Button>
)}
</div>
);
}
function LicenseSettings({ settings }: { settings: Settings }) {
const license = useLicense();
if (license.check.data?.status !== 'personal_use') {
return null;
}
return (
<Checkbox
checked={settings.hideLicenseBadge}
title="Hide personal use badge"
onChange={async (hideLicenseBadge) => {
if (hideLicenseBadge) {
const confirmed = await showConfirm({
id: 'hide-license-badge',
title: 'Confirm Personal Use',
confirmText: 'Confirm',
description: (
<VStack space={3}>
<p>Hey there 👋🏼</p>
<p>
Yaak is free for personal projects and learning.{' '}
<strong>If youre using Yaak at work, a license is required.</strong>
</p>
<p>
Licenses help keep Yaak independent and sustainable.{' '}
<Link href="https://yaak.app/pricing?s=badge">Purchase a License </Link>
</p>
</VStack>
),
requireTyping: 'Personal Use',
color: 'info',
});
if (!confirmed) {
return; // Cancel
}
}
await patchModel(settings, { hideLicenseBadge });
}}
/>
);
}