fix(settings): Reset button not showing properly + unify game settings across Cluster and Global launcher pages

This commit is contained in:
LynithDev 2024-08-07 10:52:07 +02:00
parent 7ef1774767
commit 3f30a41c5a
3 changed files with 315 additions and 439 deletions

View file

@ -1,5 +1,5 @@
import type { JSX } from 'solid-js';
import { createSignal, createUniqueId, splitProps } from 'solid-js';
import { createSignal, createUniqueId, onMount, splitProps } from 'solid-js';
import styles from './TextField.module.scss';
type TextFieldProps = {
@ -16,11 +16,13 @@ function TextField(props: TextFieldProps) {
const [isValid, setIsValid] = createSignal(true);
const id = createUniqueId();
function validate(e: Event & { currentTarget: HTMLInputElement }) {
let ref!: HTMLInputElement;
function validate() {
if (!split.inputFilter)
return;
const value = e.currentTarget.value;
const value = ref.value;
const valid = split.inputFilter(value);
setIsValid(valid);
@ -28,17 +30,22 @@ function TextField(props: TextFieldProps) {
split.onValidInput(value);
}
function onSubmit(e: Event & { currentTarget: HTMLInputElement }) {
function onSubmit() {
if (isValid() && split.onValidSubmit)
split.onValidSubmit(e.currentTarget.value);
split.onValidSubmit(ref.value);
}
onMount(() => {
validate();
});
return (
<label for={id} class={`${styles.textfield} ${isValid() ? '' : styles.invalid} ${split.labelClass || ''}`}>
{split.iconLeft && <span class={styles.icon}>{split.iconLeft}</span>}
<input
id={id}
type="text"
ref={ref}
onInput={validate}
onChange={onSubmit}
{...rest}
@ -48,11 +55,35 @@ function TextField(props: TextFieldProps) {
);
}
TextField.Number = (props: TextFieldProps) => (
<TextField
inputFilter={value => /^\d*$/.test(value)}
{...props}
/>
);
type NumberTextFieldProps = TextFieldProps & {
min?: number;
max?: number;
};
TextField.Number = (props: NumberTextFieldProps) => {
const [split, rest] = splitProps(props, ['min', 'max', 'inputFilter']);
return (
<TextField
{...rest}
inputFilter={(value) => {
const check_pattern = /^\d*$/.test(value);
if (!check_pattern)
return false;
const check_min = split.min === undefined || Number(value) >= split.min;
const check_max = split.max === undefined || Number(value) <= split.max;
if (!check_min || !check_max)
return false;
if (split.inputFilter)
return split.inputFilter(value);
return true;
}}
/>
);
};
export default TextField;

View file

@ -1,17 +1,11 @@
import { useBeforeLeave } from '@solidjs/router';
import { type Accessor, Show, splitProps } from 'solid-js';
import { CpuChip01Icon, Database01Icon, FilePlus02Icon, FileX02Icon, LayoutTopIcon, Maximize01Icon, ParagraphWrapIcon, VariableIcon, XIcon } from '@untitled-theme/icons-solid';
import { createSetting } from '../settings/game/SettingsMinecraft';
import { type Accessor, Show } from 'solid-js';
import { GameSettings, JvmSettings, LauncherSettings, ProcessSettings, createSetting } from '../settings/game/SettingsMinecraft';
import ScrollableContainer from '~ui/components/ScrollableContainer';
import type { SettingsRowProps } from '~ui/components/SettingsRow';
import Sidebar from '~ui/components/Sidebar';
import useClusterContext from '~ui/hooks/useCluster';
import BaseSettingsRow from '~ui/components/SettingsRow';
import type { Cluster } from '~bindings';
import useSettingsContext from '~ui/hooks/useSettings';
import Toggle from '~ui/components/base/Toggle';
import TextField from '~ui/components/base/TextField';
import { asEnvVariables } from '~utils';
import { tryResult } from '~ui/hooks/useCommand';
import { bridge } from '~imports';
@ -20,7 +14,7 @@ function ClusterSettings() {
return (
<Sidebar.Page>
<h1>Overview</h1>
<h1>Game Settings</h1>
<ScrollableContainer>
<Show when={cluster() !== undefined}>
{PageSettings(() => cluster()!)}
@ -30,30 +24,6 @@ function ClusterSettings() {
);
}
function SettingsRow(props: SettingsRowProps & {
reset: () => any;
isGlobal: Accessor<boolean>;
}) {
const [split, rest] = splitProps(props, ['children', 'reset', 'isGlobal']);
// TODO: Reset button doesn't show up immediately after changing a setting
return (
<BaseSettingsRow {...rest}>
<Show when={split.isGlobal() === false}>
<div
class="px-2 py-1 text-xs text-white bg-brand hover:bg-brand-hover active:bg-brand-pressed rounded-md font-italic font-medium"
onClick={() => split.reset()}
>
Reset
</div>
</Show>
{split.children}
</BaseSettingsRow>
);
}
function PageSettings(cluster: Accessor<Cluster>) {
const { settings } = useSettingsContext();
@ -97,207 +67,12 @@ function PageSettings(cluster: Accessor<Cluster>) {
});
});
const GameSettings = () => (
<>
<BaseSettingsRow.Header>Game</BaseSettingsRow.Header>
<SettingsRow
title="Force Fullscreen"
description="Force Minecraft to start in fullscreen mode."
icon={<Maximize01Icon />}
isGlobal={fullscreen.isGlobal}
reset={fullscreen.resetToFallback}
>
<Toggle
checked={fullscreen.get}
onChecked={fullscreen.set}
/>
</SettingsRow>
<SettingsRow
title="Resolution"
description="The game window resolution in pixels."
icon={<LayoutTopIcon />}
isGlobal={resolution.isGlobal}
reset={resolution.resetToFallback}
>
<div class="grid grid-justify-center grid-items-center gap-2 grid-cols-[70px_16px_70px]">
<TextField.Number
class="text-center"
value={resolution.get()[0]}
onValidSubmit={(value) => {
resolution.set([Number.parseInt(value), resolution.get()[1]]);
}}
/>
<XIcon class="w-4 h-4" />
<TextField.Number
class="text-center"
value={resolution.get()[1]}
onValidSubmit={(value) => {
resolution.set([resolution.get()[0], Number.parseInt(value)]);
}}
/>
</div>
</SettingsRow>
<SettingsRow
title="Memory"
description="The amount of memory in megabytes allocated for the game."
icon={<Database01Icon />}
isGlobal={memory.isGlobal}
reset={memory.resetToFallback}
>
<div class="flex flex-justify-center items-center gap-x-4">
<div class="flex flex-row items-center gap-x-2">
<span>Min:</span>
<TextField.Number
class="text-center"
labelClass="w-[70px]!"
value={memory.get().minimum}
onValidSubmit={(value) => {
memory.set({ minimum: Number.parseInt(value), maximum: memory.get().maximum });
}}
/>
</div>
<div class="flex flex-row items-center gap-x-2">
<span>Max:</span>
<TextField.Number
class="text-center"
labelClass="w-[70px]!"
value={memory.get().maximum}
onValidSubmit={(value) => {
memory.set({ minimum: memory.get().minimum, maximum: Number.parseInt(value) });
}}
/>
</div>
</div>
</SettingsRow>
</>
);
const LauncherSettings = () => (
<>
{/* <BaseSettingsRow.Header>Launcher</BaseSettingsRow.Header>
<SettingsRow
title="Hide On Launch"
description="Hide the launcher whenever you start a game."
icon={<EyeIcon />}
>
<Toggle
checked={hideOnLaunch.get}
onChecked={hideOnLaunch.set}
/>
</SettingsRow> */}
</>
);
const ProcessSettings = () => (
<>
<BaseSettingsRow.Header>Process</BaseSettingsRow.Header>
<SettingsRow
title="Pre-Launch Command"
description="Command to run before launching the game."
icon={<FilePlus02Icon />}
isGlobal={preCommand.isGlobal}
reset={preCommand.resetToFallback}
>
<TextField
labelClass="w-full! min-w-[260px]!"
placeholder="echo 'Game started'"
value={preCommand.get()}
onValidSubmit={preCommand.set}
/>
</SettingsRow>
<SettingsRow
title="Wrapper Command"
description="Command to run when launching the game."
icon={<ParagraphWrapIcon />}
isGlobal={wrapperCommand.isGlobal}
reset={wrapperCommand.resetToFallback}
>
<TextField
labelClass="w-full! min-w-[260px]!"
placeholder="gamescope"
value={wrapperCommand.get()}
onValidSubmit={wrapperCommand.set}
/>
</SettingsRow>
<SettingsRow
title="Post-Exit Command"
description="Command to run after exiting the game."
icon={<FileX02Icon />}
isGlobal={postCommand.isGlobal}
reset={postCommand.resetToFallback}
>
<TextField
labelClass="w-full! min-w-[260px]!"
placeholder="echo 'Game exited'"
value={postCommand.get()}
onValidSubmit={postCommand.set}
/>
</SettingsRow>
</>
);
const JvmSettings = () => (
<>
<BaseSettingsRow.Header>Java</BaseSettingsRow.Header>
<SettingsRow
title="Version"
description="Choose the JRE (Java Runtime Environment) used for the game."
icon={<CpuChip01Icon />}
isGlobal={() => true}
reset={() => {}}
/>
<SettingsRow
title="Arguments"
description="Additional arguments passed to the JVM (Java Virtual Machine)."
icon={<FilePlus02Icon />}
isGlobal={javaArgs.isGlobal}
reset={javaArgs.resetToFallback}
>
<TextField
labelClass="w-full! min-w-[260px]!"
placeholder='"-Xmx3G"'
value={javaArgs.get().join(' ')}
onValidSubmit={(value) => {
javaArgs.set(value.split(' '));
}}
/>
</SettingsRow>
<SettingsRow
title="Environment Variables"
description="Additional environment variables passed to the JVM (Java Virtual Machine)."
icon={<VariableIcon />}
isGlobal={envVars.isGlobal}
reset={envVars.resetToFallback}
>
<TextField
labelClass="w-full! min-w-[260px]!"
placeholder='"JAVA_HOME=/path/to/java"'
value={envVars.get().join(' ')}
onValidSubmit={(value) => {
envVars.set(asEnvVariables(value));
}}
/>
</SettingsRow>
</>
);
return (
<>
<GameSettings />
<LauncherSettings />
<ProcessSettings />
<JvmSettings />
<GameSettings {...{ fullscreen, memory, resolution }} />
<LauncherSettings {...{ hideOnLaunch: undefined }} />
<ProcessSettings {...{ preCommand, wrapperCommand, postCommand }} />
<JvmSettings {...{ javaArgs, envVars }} />
</>
);
}

View file

@ -1,19 +1,18 @@
import { CpuChip01Icon, Database01Icon, EyeIcon, FilePlus02Icon, FileX02Icon, LayoutTopIcon, Maximize01Icon, ParagraphWrapIcon, VariableIcon, XIcon } from '@untitled-theme/icons-solid';
import { type Accessor, type Setter, createSignal, onMount, untrack } from 'solid-js';
import { type Accessor, type Setter, Show, createSignal, onMount, splitProps, untrack } from 'solid-js';
import type { Memory, Resolution } from '~bindings';
import TextField from '~ui/components/base/TextField';
import Toggle from '~ui/components/base/Toggle';
import ScrollableContainer from '~ui/components/ScrollableContainer';
import SettingsRow from '~ui/components/SettingsRow';
import BaseSettingsRow, { type SettingsRowProps } from '~ui/components/SettingsRow';
import Sidebar from '~ui/components/Sidebar';
import useSettingsContext from '~ui/hooks/useSettings';
import { asEnvVariables } from '~utils';
// TODO: REFACTOR THIS ENTIRE FILE AND POSSIBLY `apps/frontend/src/ui/pages/cluster/ClusterSettings.tsx` TOO
function SettingsMinecraft() {
return (
<Sidebar.Page>
<h1>Global Minecraft Settings</h1>
<h1>Global Game Settings</h1>
<ScrollableContainer>
<PageSettings />
</ScrollableContainer>
@ -25,20 +24,24 @@ interface CreateSetting<T> {
get: Accessor<T>;
getRaw: () => T;
set: Setter<T>;
isGlobal: Accessor<boolean>;
isGlobal: Accessor<boolean | null>;
resetToFallback: (raw?: any) => void;
};
export function createSetting<T>(initial: T | undefined | null, fallback?: T): CreateSetting<T> {
const [value, setValue] = createSignal<T>(initial || fallback as T);
const [value, setValue] = createSignal<T>((initial === undefined || initial === null) ? fallback as T : initial);
const [raw, setRaw] = createSignal<T>(initial as T);
const [isGlobal, setIsGlobal] = createSignal(true);
const [isGlobal, setIsGlobal] = createSignal<boolean | null>(true);
const checkGlobal = () => {
const check_one = untrack(() => value()) === undefined || untrack(() => value()) === null;
const check_two = initial === undefined || initial === null;
// const check_three = untrack(() => raw()) === undefined || untrack(() => raw()) === null;
if (fallback === undefined || fallback === null) {
setIsGlobal(null);
return;
}
setIsGlobal(check_one || check_two);
const check_one = untrack(() => raw()) === undefined || untrack(() => raw()) === null;
setIsGlobal(check_one);
};
onMount(() => {
@ -67,17 +70,261 @@ export function createSetting<T>(initial: T | undefined | null, fallback?: T): C
checkGlobal();
};
const getRaw = () => raw();
return {
get: value,
getRaw,
getRaw: raw,
set,
isGlobal,
resetToFallback,
};
}
function SettingsRow(props: SettingsRowProps & {
reset: () => any;
isGlobal: Accessor<boolean | null>;
}) {
const [split, rest] = splitProps(props, ['children', 'reset', 'isGlobal']);
return (
<BaseSettingsRow {...rest}>
<Show when={split.isGlobal() === false}>
<div
class="px-2 py-1 text-xs text-white bg-brand hover:bg-brand-hover active:bg-brand-pressed rounded-md font-italic font-medium"
onClick={() => split.reset()}
>
Reset
</div>
</Show>
{split.children}
</BaseSettingsRow>
);
}
export function GameSettings(props: {
fullscreen: CreateSetting<boolean>;
resolution: CreateSetting<Resolution>;
memory: CreateSetting<Memory>;
}) {
return (
<>
<BaseSettingsRow.Header>Game</BaseSettingsRow.Header>
<SettingsRow
title="Force Fullscreen"
description="Force Minecraft to start in fullscreen mode."
icon={<Maximize01Icon />}
isGlobal={props.fullscreen.isGlobal}
reset={props.fullscreen.resetToFallback}
>
<Toggle
checked={props.fullscreen.get}
onChecked={props.fullscreen.set}
/>
</SettingsRow>
<SettingsRow
title="Resolution"
description="The game window resolution in pixels."
icon={<LayoutTopIcon />}
isGlobal={props.resolution.isGlobal}
reset={props.resolution.resetToFallback}
>
<div class="grid grid-justify-center grid-items-center gap-2 grid-cols-[70px_16px_70px]">
<TextField.Number
class="text-center"
value={props.resolution.get()[0]}
onValidSubmit={(value) => {
props.resolution.set([Number.parseInt(value), props.resolution.get()[1]]);
}}
/>
<XIcon class="w-4 h-4" />
<TextField.Number
class="text-center"
value={props.resolution.get()[1]}
onValidSubmit={(value) => {
props.resolution.set([props.resolution.get()[0], Number.parseInt(value)]);
}}
/>
</div>
</SettingsRow>
<SettingsRow
title="Memory"
description="The amount of memory in megabytes allocated for the game."
icon={<Database01Icon />}
isGlobal={props.memory.isGlobal}
reset={props.memory.resetToFallback}
>
<div class="flex flex-justify-center items-center gap-x-4">
<div class="flex flex-row items-center gap-x-2">
<span>Min:</span>
<TextField.Number
class="text-center"
labelClass="w-[70px]!"
value={props.memory.get().minimum}
min={1}
max={props.memory.get().maximum}
onValidSubmit={(value) => {
props.memory.set({ minimum: Number.parseInt(value), maximum: props.memory.get().maximum });
}}
/>
</div>
<div class="flex flex-row items-center gap-x-2">
<span>Max:</span>
<TextField.Number
class="text-center"
labelClass="w-[70px]!"
value={props.memory.get().maximum}
min={props.memory.get().minimum}
max={Number.MAX_SAFE_INTEGER}
onValidSubmit={(value) => {
props.memory.set({ minimum: props.memory.get().minimum, maximum: Number.parseInt(value) });
}}
/>
</div>
</div>
</SettingsRow>
</>
);
}
export function LauncherSettings(props: {
hideOnLaunch: CreateSetting<boolean> | undefined;
}) {
return (
<>
<Show when={props.hideOnLaunch !== undefined}>
<BaseSettingsRow.Header>Launcher</BaseSettingsRow.Header>
<SettingsRow
title="Hide On Launch"
description="Hide the launcher whenever you start a game."
icon={<EyeIcon />}
isGlobal={props.hideOnLaunch!.isGlobal}
reset={props.hideOnLaunch!.resetToFallback}
>
<Toggle
checked={props.hideOnLaunch!.get}
onChecked={props.hideOnLaunch!.set}
/>
</SettingsRow>
</Show>
</>
);
}
export function ProcessSettings(props: {
preCommand: CreateSetting<string>;
wrapperCommand: CreateSetting<string>;
postCommand: CreateSetting<string>;
}) {
return (
<>
<BaseSettingsRow.Header>Process</BaseSettingsRow.Header>
<SettingsRow
title="Pre-Launch Command"
description="Command to run before launching the game."
icon={<FilePlus02Icon />}
isGlobal={props.preCommand.isGlobal}
reset={props.preCommand.resetToFallback}
>
<TextField
labelClass="w-full! min-w-[260px]!"
placeholder="echo 'Game started'"
value={props.preCommand.get()}
onValidSubmit={props.preCommand.set}
/>
</SettingsRow>
<SettingsRow
title="Wrapper Command"
description="Command to run when launching the game."
icon={<ParagraphWrapIcon />}
isGlobal={props.wrapperCommand.isGlobal}
reset={props.wrapperCommand.resetToFallback}
>
<TextField
labelClass="w-full! min-w-[260px]!"
placeholder="gamescope"
value={props.wrapperCommand.get()}
onValidSubmit={props.wrapperCommand.set}
/>
</SettingsRow>
<SettingsRow
title="Post-Exit Command"
description="Command to run after exiting the game."
icon={<FileX02Icon />}
isGlobal={props.postCommand.isGlobal}
reset={props.postCommand.resetToFallback}
>
<TextField
labelClass="w-full! min-w-[260px]!"
placeholder="echo 'Game exited'"
value={props.postCommand.get()}
onValidSubmit={props.postCommand.set}
/>
</SettingsRow>
</>
);
}
export function JvmSettings(props: {
javaArgs: CreateSetting<string[]>;
envVars: CreateSetting<[string, string][]>;
}) {
return (
<>
<BaseSettingsRow.Header>Java</BaseSettingsRow.Header>
<SettingsRow
title="Version"
description="Choose the JRE (Java Runtime Environment) used for the game."
icon={<CpuChip01Icon />}
isGlobal={() => true}
reset={() => {}}
/>
<SettingsRow
title="Arguments"
description="Additional arguments passed to the JVM (Java Virtual Machine)."
icon={<FilePlus02Icon />}
isGlobal={props.javaArgs.isGlobal}
reset={props.javaArgs.resetToFallback}
>
<TextField
labelClass="w-full! min-w-[260px]!"
placeholder='"-Xmx3G"'
value={props.javaArgs.get().join(' ')}
onValidSubmit={(value) => {
props.javaArgs.set(value.split(' '));
}}
/>
</SettingsRow>
<SettingsRow
title="Environment Variables"
description="Additional environment variables passed to the JVM (Java Virtual Machine)."
icon={<VariableIcon />}
isGlobal={props.envVars.isGlobal}
reset={props.envVars.resetToFallback}
>
<TextField
labelClass="w-full! min-w-[260px]!"
placeholder='"JAVA_HOME=/path/to/java"'
value={props.envVars.get().join(' ')}
onValidSubmit={(value) => {
props.envVars.set(asEnvVariables(value));
}}
/>
</SettingsRow>
</>
);
}
function PageSettings() {
const { settings, saveOnLeave } = useSettingsContext();
@ -119,189 +366,12 @@ function PageSettings() {
custom_env_args: envVars.get(),
}));
const GameSettings = () => (
<>
<SettingsRow.Header>Game</SettingsRow.Header>
<SettingsRow
title="Force Fullscreen"
description="Force Minecraft to start in fullscreen mode."
icon={<Maximize01Icon />}
>
<Toggle
checked={fullscreen.get}
onChecked={fullscreen.set}
/>
</SettingsRow>
<SettingsRow
title="Resolution"
description="The game window resolution in pixels."
icon={<LayoutTopIcon />}
>
<div class="grid grid-justify-center grid-items-center gap-2 grid-cols-[70px_16px_70px]">
<TextField.Number
class="text-center"
value={resolution.get()[0]}
onValidSubmit={(value) => {
resolution.set([Number.parseInt(value), resolution.get()[1]]);
}}
/>
<XIcon class="w-4 h-4" />
<TextField.Number
class="text-center"
value={resolution.get()[1]}
onValidSubmit={(value) => {
resolution.set([resolution.get()[0], Number.parseInt(value)]);
}}
/>
</div>
</SettingsRow>
<SettingsRow
title="Memory"
description="The amount of memory in megabytes allocated for the game."
icon={<Database01Icon />}
>
<div class="flex flex-justify-center items-center gap-x-4">
<div class="flex flex-row items-center gap-x-2">
<span>Min:</span>
<TextField.Number
class="text-center"
labelClass="w-[70px]!"
value={memory.get().minimum}
onValidSubmit={(value) => {
memory.set({ minimum: Number.parseInt(value), maximum: memory.get().maximum });
}}
/>
</div>
<div class="flex flex-row items-center gap-x-2">
<span>Max:</span>
<TextField.Number
class="text-center"
labelClass="w-[70px]!"
value={memory.get().maximum}
onValidSubmit={(value) => {
memory.set({ minimum: memory.get().minimum, maximum: Number.parseInt(value) });
}}
/>
</div>
</div>
</SettingsRow>
</>
);
const LauncherSettings = () => (
<>
<SettingsRow.Header>Launcher</SettingsRow.Header>
<SettingsRow
title="Hide On Launch"
description="Hide the launcher whenever you start a game."
icon={<EyeIcon />}
>
<Toggle
checked={hideOnLaunch.get}
onChecked={hideOnLaunch.set}
/>
</SettingsRow>
</>
);
const ProcessSettings = () => (
<>
<SettingsRow.Header>Process</SettingsRow.Header>
<SettingsRow
title="Pre-Launch Command"
description="Command to run before launching the game."
icon={<FilePlus02Icon />}
>
<TextField
labelClass="w-full! min-w-[260px]!"
placeholder="echo 'Game started'"
value={preCommand.get()}
onValidSubmit={preCommand.set}
/>
</SettingsRow>
<SettingsRow
title="Wrapper Command"
description="Command to run when launching the game."
icon={<ParagraphWrapIcon />}
>
<TextField
labelClass="w-full! min-w-[260px]!"
placeholder="gamescope"
value={wrapperCommand.get()}
onValidSubmit={wrapperCommand.set}
/>
</SettingsRow>
<SettingsRow
title="Post-Exit Command"
description="Command to run after exiting the game."
icon={<FileX02Icon />}
>
<TextField
labelClass="w-full! min-w-[260px]!"
placeholder="echo 'Game exited'"
value={postCommand.get()}
onValidSubmit={postCommand.set}
/>
</SettingsRow>
</>
);
const JvmSettings = () => (
<>
<SettingsRow.Header>Java</SettingsRow.Header>
<SettingsRow
title="Version"
description="Choose the JRE (Java Runtime Environment) used for the game."
icon={<CpuChip01Icon />}
/>
<SettingsRow
title="Arguments"
description="Additional arguments passed to the JVM (Java Virtual Machine)."
icon={<FilePlus02Icon />}
>
<TextField
labelClass="w-full! min-w-[260px]!"
placeholder='"-Xmx3G"'
value={javaArgs.get().join(' ')}
onValidSubmit={(value) => {
javaArgs.set(value.split(' '));
}}
/>
</SettingsRow>
<SettingsRow
title="Environment Variables"
description="Additional environment variables passed to the JVM (Java Virtual Machine)."
icon={<VariableIcon />}
>
<TextField
labelClass="w-full! min-w-[260px]!"
placeholder='"JAVA_HOME=/path/to/java"'
value={envVars.get().join(' ')}
onValidSubmit={(value) => {
envVars.set(asEnvVariables(value));
}}
/>
</SettingsRow>
</>
);
return (
<>
<GameSettings />
<LauncherSettings />
<ProcessSettings />
<JvmSettings />
<GameSettings {...{ fullscreen, memory, resolution }} />
<LauncherSettings {...{ hideOnLaunch }} />
<ProcessSettings {...{ preCommand, wrapperCommand, postCommand }} />
<JvmSettings {...{ javaArgs, envVars }} />
</>
);
}