diff --git a/src/lib/settings.ts b/src/lib/settings.ts index 9bdfed2..c5febe5 100644 --- a/src/lib/settings.ts +++ b/src/lib/settings.ts @@ -1,10 +1,13 @@ import { persisted } from "svelte-persisted-store"; +import { type ThemeName, defaultTheme } from "./ui/themes"; export type SettingsStore = { name: string; + theme: ThemeName; }; /** A persisted store for settings of the current user. */ export const settings = persisted<SettingsStore>("sshx-settings-store", { name: "", + theme: defaultTheme, }); diff --git a/src/lib/ui/ChooseName.svelte b/src/lib/ui/ChooseName.svelte index 0ad1102..c6e0c23 100644 --- a/src/lib/ui/ChooseName.svelte +++ b/src/lib/ui/ChooseName.svelte @@ -7,7 +7,10 @@ let value = ""; function handleSubmit() { - settings.set({ ...settings, name: value }); + settings.update((curSettings) => ({ + ...curSettings, + name: value, + })); } </script> diff --git a/src/lib/ui/Settings.svelte b/src/lib/ui/Settings.svelte index 0a33965..e12fa6a 100644 --- a/src/lib/ui/Settings.svelte +++ b/src/lib/ui/Settings.svelte @@ -1,6 +1,8 @@ <script lang="ts"> import { settings } from "$lib/settings"; + import { ChevronDownIcon } from "svelte-feather-icons"; import OverlayMenu from "./OverlayMenu.svelte"; + import themes, { defaultTheme, type ThemeName } from "./themes"; export let open: boolean; @@ -12,6 +14,20 @@ initialized = true; nameValue = $settings.name; } + + let selectedTheme: ThemeName; // Bound to the settings input. + if (Object.hasOwn(themes, $settings.theme)) { + selectedTheme = $settings.theme; + } else { + selectedTheme = defaultTheme; + } + + function handleThemeChange() { + settings.update((curSettings) => ({ + ...curSettings, + theme: selectedTheme, + })); + } </script> <OverlayMenu @@ -21,23 +37,26 @@ {open} on:close > - <div class="flex flex-col gap-2"> + <div class="flex flex-col gap-4"> <div class="item"> <div class="flex-1"> <p class="font-medium mb-2">Name</p> <p class="text-sm text-zinc-400"> - How you appear to other users online. + Choose how you appear to other users. </p> </div> <div> <input - class="w-52 px-3 py-1.5 rounded-md bg-zinc-700 outline-none focus:ring-2 focus:ring-indigo-500" + class="input-common" placeholder="Your name" bind:value={nameValue} maxlength="50" on:input={() => { if (nameValue.length >= 2) { - settings.set({ ...$settings, name: nameValue }); + settings.update((curSettings) => ({ + ...curSettings, + name: nameValue, + })); } }} /> @@ -46,19 +65,30 @@ <div class="item"> <div class="flex-1"> <p class="font-medium mb-2">Color palette</p> - <p class="text-sm text-zinc-400">Color scheme for text in terminals.</p> + <p class="text-sm text-zinc-400">Color theme for text in terminals.</p> + </div> + <div class="relative"> + <ChevronDownIcon + class="absolute top-[11px] right-2.5 w-4 h-4 text-zinc-400" + /> + <select + class="input-common !pr-5" + bind:value={selectedTheme} + on:change={handleThemeChange} + > + {#each Object.keys(themes) as themeName (themeName)} + <option value={themeName}>{themeName}</option> + {/each} + </select> </div> - <div class="text-red-500">Coming soon</div> </div> - <div class="item"> + <!-- <div class="item"> <div class="flex-1"> <p class="font-medium mb-2">Cursor style</p> - <p class="text-sm text-zinc-400"> - How live cursors should be displayed. - </p> + <p class="text-sm text-zinc-400">Style of live cursors.</p> </div> <div class="text-red-500">Coming soon</div> - </div> + </div> --> </div> <!-- svelte-ignore missing-declaration --> @@ -71,6 +101,12 @@ <style lang="postcss"> .item { - @apply bg-zinc-800/25 rounded-lg p-4 flex gap-4 flex-col sm:flex-row; + @apply bg-zinc-800/25 rounded-lg p-4 flex gap-4 flex-col sm:flex-row items-start; + } + + .input-common { + @apply w-52 px-3 py-2 text-sm rounded-md bg-transparent hover:bg-white/5; + @apply border border-zinc-700 outline-none focus:ring-2 focus:ring-indigo-500/50; + @apply appearance-none transition-colors; } </style> diff --git a/src/lib/ui/XTerm.svelte b/src/lib/ui/XTerm.svelte index 8fb763e..6eed2bf 100644 --- a/src/lib/ui/XTerm.svelte +++ b/src/lib/ui/XTerm.svelte @@ -39,13 +39,12 @@ import type { Terminal } from "sshx-xterm"; import { Buffer } from "buffer"; - import themes from "./themes"; + import themes, { defaultTheme } from "./themes"; import CircleButton from "./CircleButton.svelte"; import CircleButtons from "./CircleButtons.svelte"; + import { settings } from "$lib/settings"; import { TypeAheadAddon } from "$lib/typeahead"; - const theme = themes.defaultDark; - /** Used to determine Cmd versus Ctrl keyboard shortcuts. */ const isMac = browser && navigator.platform.startsWith("Mac"); @@ -68,6 +67,15 @@ export let termEl: HTMLDivElement = null as any; // suppress "missing prop" warning let term: Terminal | null = null; + $: theme = Object.hasOwn(themes, $settings.theme) + ? themes[$settings.theme] + : themes[defaultTheme]; + + $: if (term) { + // If the theme changes, update existing terminals' appearance. + term.options.theme = theme; + } + let loaded = false; let focused = false; let currentTitle = "Remote Terminal"; diff --git a/src/lib/ui/themes.ts b/src/lib/ui/themes.ts index 18dc7bf..d326a74 100644 --- a/src/lib/ui/themes.ts +++ b/src/lib/ui/themes.ts @@ -1,7 +1,7 @@ import type { ITheme } from "sshx-xterm"; /** VSCode default dark theme, from https://glitchbone.github.io/vscode-base16-term/. */ -export const defaultDark: ITheme = { +const defaultDark: ITheme = { foreground: "#d8d8d8", background: "#181818", @@ -27,7 +27,7 @@ export const defaultDark: ITheme = { }; /** Hybrid theme from https://terminal.sexy/, using Alacritty export format. */ -export const hybrid: ITheme = { +const hybrid: ITheme = { foreground: "#c5c8c6", background: "#1d1f21", @@ -50,4 +50,172 @@ export const hybrid: ITheme = { brightWhite: "#c5c8c6", }; -export default { defaultDark, hybrid }; +/** Below themes are converted from https://github.com/alacritty/alacritty-theme/. */ +const rosePine: ITheme = { + foreground: "#e0def4", + background: "#191724", + + cursor: "#524f67", + + black: "#26233a", + red: "#eb6f92", + green: "#31748f", + yellow: "#f6c177", + blue: "#9ccfd8", + magenta: "#c4a7e7", + cyan: "#ebbcba", + white: "#e0def4", + + brightBlack: "#6e6a86", + brightRed: "#eb6f92", + brightGreen: "#31748f", + brightYellow: "#f6c177", + brightBlue: "#9ccfd8", + brightMagenta: "#c4a7e7", + brightCyan: "#ebbcba", + brightWhite: "#e0def4", +}; + +const ubuntu: ITheme = { + foreground: "#eeeeec", + background: "#300a24", + black: "#2e3436", + red: "#cc0000", + green: "#4e9a06", + yellow: "#c4a000", + blue: "#3465a4", + magenta: "#75507b", + cyan: "#06989a", + white: "#d3d7cf", + brightBlack: "#555753", + brightRed: "#ef2929", + brightGreen: "#8ae234", + brightYellow: "#fce94f", + brightBlue: "#729fcf", + brightMagenta: "#ad7fa8", + brightCyan: "#34e2e2", + brightWhite: "#eeeeec", +}; + +const dracula: ITheme = { + foreground: "#f8f8f2", + background: "#282a36", + black: "#000000", + red: "#ff5555", + green: "#50fa7b", + yellow: "#f1fa8c", + blue: "#bd93f9", + magenta: "#ff79c6", + cyan: "#8be9fd", + white: "#bbbbbb", + brightBlack: "#555555", + brightRed: "#ff5555", + brightGreen: "#50fa7b", + brightYellow: "#f1fa8c", + brightBlue: "#caa9fa", + brightMagenta: "#ff79c6", + brightCyan: "#8be9fd", + brightWhite: "#ffffff", +}; + +const githubDark: ITheme = { + foreground: "#d1d5da", + background: "#24292e", + black: "#586069", + red: "#ea4a5a", + green: "#34d058", + yellow: "#ffea7f", + blue: "#2188ff", + magenta: "#b392f0", + cyan: "#39c5cf", + white: "#d1d5da", + brightBlack: "#959da5", + brightRed: "#f97583", + brightGreen: "#85e89d", + brightYellow: "#ffea7f", + brightBlue: "#79b8ff", + brightMagenta: "#b392f0", + brightCyan: "#56d4dd", + brightWhite: "#fafbfc", +}; + +const gruvboxDark: ITheme = { + foreground: "#ebdbb2", + background: "#282828", + black: "#282828", + red: "#cc241d", + green: "#98971a", + yellow: "#d79921", + blue: "#458588", + magenta: "#b16286", + cyan: "#689d6a", + white: "#a89984", + brightBlack: "#928374", + brightRed: "#fb4934", + brightGreen: "#b8bb26", + brightYellow: "#fabd2f", + brightBlue: "#83a598", + brightMagenta: "#d3869b", + brightCyan: "#8ec07c", + brightWhite: "#ebdbb2", +}; + +const solarizedDark: ITheme = { + foreground: "#839496", + background: "#002b36", + black: "#073642", + red: "#dc322f", + green: "#859900", + yellow: "#b58900", + blue: "#268bd2", + magenta: "#d33682", + cyan: "#2aa198", + white: "#eee8d5", + brightBlack: "#002b36", + brightRed: "#cb4b16", + brightGreen: "#586e75", + brightYellow: "#657b83", + brightBlue: "#839496", + brightMagenta: "#6c71c4", + brightCyan: "#93a1a1", + brightWhite: "#fdf6e3", +}; + +const tokyoNight: ITheme = { + foreground: "#a9b1d6", + background: "#1a1b26", + black: "#32344a", + red: "#f7768e", + green: "#9ece6a", + yellow: "#e0af68", + blue: "#7aa2f7", + magenta: "#ad8ee6", + cyan: "#449dab", + white: "#787c99", + brightBlack: "#444b6a", + brightRed: "#ff7a93", + brightGreen: "#b9f27c", + brightYellow: "#ff9e64", + brightBlue: "#7da6ff", + brightMagenta: "#bb9af7", + brightCyan: "#0db9d7", + brightWhite: "#acb0d0", +}; + +const themes = { + "VS Code Dark": defaultDark, + Hybrid: hybrid, + "Rosé Pine": rosePine, + Ubuntu: ubuntu, + Dracula: dracula, + "GitHub Dark": githubDark, + "Gruvbox Dark": gruvboxDark, + "Solarized Dark": solarizedDark, + "Tokyo Night": tokyoNight, +}; + +export type ThemeName = keyof typeof themes; + +export const defaultTheme: ThemeName = "VS Code Dark"; + +export default themes;