Skip to content

feat: support multiple terminal fonts #17257

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
Apr 7, 2025
Prev Previous commit
Next Next commit
form
  • Loading branch information
mtojek committed Apr 4, 2025
commit 6a6272b24d362a7ca68ec6ae18b70f55ca6b4b51
2 changes: 1 addition & 1 deletion coderd/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -1035,7 +1035,7 @@
})
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error updating user theme prefence.",

Check warning on line 1038 in coderd/users.go

View workflow job for this annotation

GitHub Actions / lint

"prefence" should be "preference" or "presence" or "pretence".
Detail: err.Error(),
})
return
Expand All @@ -1043,7 +1043,7 @@

updatedTerminalFont, err := api.Database.UpdateUserTerminalFont(ctx, database.UpdateUserTerminalFontParams{
UserID: user.ID,
TerminalFont: string(params.ThemePreference),
TerminalFont: string(params.TerminalFont),
})
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Expand Down
11 changes: 11 additions & 0 deletions site/site.go
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,7 @@ func (h *Handler) renderHTMLWithState(r *http.Request, filePath string, state ht
var eg errgroup.Group
var user database.User
var themePreference string
var terminalFont string
orgIDs := []uuid.UUID{}
eg.Go(func() error {
var err error
Expand All @@ -443,6 +444,15 @@ func (h *Handler) renderHTMLWithState(r *http.Request, filePath string, state ht
}
return err
})
eg.Go(func() error {
var err error
terminalFont, err = h.opts.Database.GetUserTerminalFont(ctx, apiKey.UserID)
if errors.Is(err, sql.ErrNoRows) {
terminalFont = ""
return nil
}
return err
})
eg.Go(func() error {
memberIDs, err := h.opts.Database.GetOrganizationIDsByMemberIDs(ctx, []uuid.UUID{apiKey.UserID})
if errors.Is(err, sql.ErrNoRows) || len(memberIDs) == 0 {
Expand Down Expand Up @@ -471,6 +481,7 @@ func (h *Handler) renderHTMLWithState(r *http.Request, filePath string, state ht
defer wg.Done()
userAppearance, err := json.Marshal(codersdk.UserAppearanceSettings{
ThemePreference: themePreference,
TerminalFont: codersdk.TerminalFontName(terminalFont),
})
if err == nil {
state.UserAppearance = html.EscapeString(string(userAppearance))
Expand Down
1 change: 1 addition & 0 deletions site/src/api/queries/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ export const updateAppearanceSettings = (
// more responsive.
queryClient.setQueryData(myAppearanceKey, {
theme_preference: patch.theme_preference,
terminal_font: patch.terminal_font,
});
},
onSuccess: async () =>
Expand Down
125 changes: 101 additions & 24 deletions site/src/pages/UserSettingsPage/AppearancePage/AppearanceForm.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
import type { Interpolation } from "@emotion/react";
import CircularProgress from "@mui/material/CircularProgress";
import FormControl from "@mui/material/FormControl";
import FormControlLabel from "@mui/material/FormControlLabel";
import Radio from "@mui/material/Radio";
import RadioGroup from "@mui/material/RadioGroup";
import { visuallyHidden } from "@mui/utils";
import type { UpdateUserAppearanceSettingsRequest } from "api/typesGenerated";
import {
type TerminalFontName,
TerminalFontNames,
type UpdateUserAppearanceSettingsRequest,
} from "api/typesGenerated";
import { ErrorAlert } from "components/Alert/ErrorAlert";
import { PreviewBadge } from "components/Badges/Badges";
import { Stack } from "components/Stack/Stack";
import { ThemeOverride } from "contexts/ThemeProvider";
import type { FC } from "react";
import themes, { DEFAULT_TERMINAL_FONT, DEFAULT_THEME, type Theme } from "theme";
import themes, {
DEFAULT_TERMINAL_FONT,
DEFAULT_THEME,
type Theme,
} from "theme";
import { Section } from "../Section";

export interface AppearanceFormProps {
isUpdating?: boolean;
Expand All @@ -22,44 +36,107 @@ export const AppearanceForm: FC<AppearanceFormProps> = ({
initialValues,
}) => {
const currentTheme = initialValues.theme_preference || DEFAULT_THEME;
const currentTerminalFont = initialValues.terminal_font || DEFAULT_TERMINAL_FONT;
const currentTerminalFont =
initialValues.terminal_font || DEFAULT_TERMINAL_FONT;

const onChangeTheme = async (theme: string) => {
if (isUpdating) {
return;
}
await onSubmit({
theme_preference: theme,
terminal_font: currentTerminalFont,
});
};

await onSubmit({ theme_preference: theme, terminal_font: currentTerminalFont });
const onChangeTerminalFont = async (terminalFont: TerminalFontName) => {
if (isUpdating) {
return;
}
await onSubmit({
theme_preference: currentTheme,
terminal_font: terminalFont,
});
};

return (
<form>
{Boolean(error) && <ErrorAlert error={error} />}

<Stack direction="row" wrap="wrap">
<AutoThemePreviewButton
displayName="Auto"
active={currentTheme === "auto"}
themes={[themes.dark, themes.light]}
onSelect={() => onChangeTheme("auto")}
/>
<ThemePreviewButton
displayName="Dark"
active={currentTheme === "dark"}
theme={themes.dark}
onSelect={() => onChangeTheme("dark")}
/>
<ThemePreviewButton
displayName="Light"
active={currentTheme === "light"}
theme={themes.light}
onSelect={() => onChangeTheme("light")}
/>
</Stack>
<Section
title={
<Stack direction="row" alignItems="center">
<span>Theme</span>
{isUpdating && <CircularProgress size={16} />}
</Stack>
}
layout="fluid"
>
<Stack direction="row" wrap="wrap">
<AutoThemePreviewButton
displayName="Auto"
active={currentTheme === "auto"}
themes={[themes.dark, themes.light]}
onSelect={() => onChangeTheme("auto")}
/>
<ThemePreviewButton
displayName="Dark"
active={currentTheme === "dark"}
theme={themes.dark}
onSelect={() => onChangeTheme("dark")}
/>
<ThemePreviewButton
displayName="Light"
active={currentTheme === "light"}
theme={themes.light}
onSelect={() => onChangeTheme("light")}
/>
</Stack>
</Section>
<div css={{ marginBottom: 48 }}></div>
<Section
title={
<Stack direction="row" alignItems="center">
<span>Terminal Font</span>
{isUpdating && <CircularProgress size={16} />}
</Stack>
}
layout="fluid"
>
<FormControl>
<RadioGroup
aria-labelledby="demo-radio-buttons-group-label"
defaultValue={currentTerminalFont}
name="radio-buttons-group"
onChange={(_, value) =>
onChangeTerminalFont(toTerminalFontName(value))
}
>
<FormControlLabel
value="ibm-plex-mono"
control={<Radio />}
label={
<div css={{ fontFamily: "IBM Plex Mono" }}>IBM Plex Mono</div>
}
/>
<FormControlLabel
value="fira-code"
control={<Radio />}
label={<div css={{ fontFamily: "Fira Code" }}>Fira Code</div>}
/>
</RadioGroup>
</FormControl>
</Section>
</form>
);
};

export function toTerminalFontName(value: string): TerminalFontName {
return TerminalFontNames.includes(value as TerminalFontName)
? (value as TerminalFontName)
: "";
}

interface AutoThemePreviewButtonProps extends Omit<ThemePreviewProps, "theme"> {
themes: [Theme, Theme];
onSelect?: () => void;
Expand Down
33 changes: 9 additions & 24 deletions site/src/pages/UserSettingsPage/AppearancePage/AppearancePage.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import CircularProgress from "@mui/material/CircularProgress";
import { updateAppearanceSettings } from "api/queries/users";
import { appearanceSettings } from "api/queries/users";
import { ErrorAlert } from "components/Alert/ErrorAlert";
import { Loader } from "components/Loader/Loader";
import { Stack } from "components/Stack/Stack";
import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata";
import type { FC } from "react";
import { useMutation, useQuery, useQueryClient } from "react-query";
import { Section } from "../Section";
import { AppearanceForm } from "./AppearanceForm";

export const AppearancePage: FC = () => {
Expand All @@ -31,27 +28,15 @@ export const AppearancePage: FC = () => {

return (
<>
<Section
title={
<Stack direction="row" alignItems="center">
<span>Theme</span>
{updateAppearanceSettingsMutation.isLoading && (
<CircularProgress size={16} />
)}
</Stack>
}
layout="fluid"
>
<AppearanceForm
isUpdating={updateAppearanceSettingsMutation.isLoading}
error={updateAppearanceSettingsMutation.error}
initialValues={{
theme_preference: appearanceSettingsQuery.data.theme_preference,
terminal_font: appearanceSettingsQuery.data.terminal_font,
}}
onSubmit={updateAppearanceSettingsMutation.mutateAsync}
/>
</Section>
<AppearanceForm
isUpdating={updateAppearanceSettingsMutation.isLoading}
error={updateAppearanceSettingsMutation.error}
initialValues={{
theme_preference: appearanceSettingsQuery.data.theme_preference,
terminal_font: appearanceSettingsQuery.data.terminal_font,
}}
onSubmit={updateAppearanceSettingsMutation.mutateAsync}
/>
</>
);
};
Expand Down
Loading