add support for "Hack" font family (#309)

* testing hack font

* updates to allow font to be variable

* allow setting of font-family.  get initial font family from electron (needed for loading)

* add termfontfamily

* update wave logos for README

* hook up fontfamily setting, remove old dropdown active var

* get app to reload on font change.  reload fonts

* on termfontsize change, bump render version as well
This commit is contained in:
Mike Sawka 2024-02-22 11:45:43 -08:00 committed by GitHub
parent 743d6d8622
commit 3f83b30b06
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 249 additions and 49 deletions

View File

@ -1,3 +1,4 @@
github.com/aws/aws-sdk-go-v2/service/s3 v1.27.11 h1:3/gm/JTX9bX8CpzTgIlrtYpB3EVBDxyg/GY/QdcIEZw=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,12 +1,5 @@
@import "./common/themes/themes.less"; @import "./common/themes/themes.less";
@font-face {
font-family: "Martian Mono";
src: url("./assets/fonts/MartianMono-VariableFont_wdth,wght.ttf") format("truetype");
font-weight: normal;
font-style: normal;
}
:root { :root {
--fa-style-family: "Font Awesome 6 Sharp"; --fa-style-family: "Font Awesome 6 Sharp";
} }
@ -98,7 +91,7 @@ body a {
} }
body code { body code {
font-family: @terminal-font; font-family: var(--termfontfamily);
background-color: transparent; background-color: transparent;
} }
@ -312,7 +305,7 @@ a.a-block {
} }
.mono { .mono {
font-family: @terminal-font; font-family: var(--termfontfamily);
} }
} }

View File

@ -105,8 +105,15 @@ class App extends React.Component<{}, {}> {
if (dcWait) { if (dcWait) {
setTimeout(() => this.updateDcWait(false), 0); setTimeout(() => this.updateDcWait(false), 0);
} }
// used to force a full reload of the application
let renderVersion = GlobalModel.renderVersion.get();
return ( return (
<div id="main" className={"platform-" + platform} onContextMenu={this.handleContextMenu}> <div
key={"version-" + renderVersion}
id="main"
className={"platform-" + platform}
onContextMenu={this.handleContextMenu}
>
<div ref={this.mainContentRef} className="main-content"> <div ref={this.mainContentRef} className="main-content">
<MainSideBar parentRef={this.mainContentRef} clientData={clientData} /> <MainSideBar parentRef={this.mainContentRef} clientData={clientData} />
<ErrorBoundary> <ErrorBoundary>

View File

@ -26,6 +26,10 @@
padding: 0 18px 0 30px; padding: 0 18px 0 30px;
} }
.wave-dropdown {
width: 200px;
}
// just marked these as important since we're keeping the // just marked these as important since we're keeping the
// settings-field styles below this intact until we figure out what do with them // settings-field styles below this intact until we figure out what do with them
.settings-field { .settings-field {

View File

@ -15,9 +15,6 @@ import "./clientsettings.less";
@mobxReact.observer @mobxReact.observer
class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hoveredItemId: string }> { class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hoveredItemId: string }> {
fontSizeDropdownActive: OV<boolean> = mobx.observable.box(false, {
name: "clientSettings-fontSizeDropdownActive",
});
errorMessage: OV<string> = mobx.observable.box(null, { name: "ClientSettings-errorMessage" }); errorMessage: OV<string> = mobx.observable.box(null, { name: "ClientSettings-errorMessage" });
@boundMethod @boundMethod
@ -30,7 +27,6 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
@boundMethod @boundMethod
handleChangeFontSize(fontSize: string): void { handleChangeFontSize(fontSize: string): void {
const newFontSize = Number(fontSize); const newFontSize = Number(fontSize);
this.fontSizeDropdownActive.set(false);
if (GlobalModel.termFontSize.get() == newFontSize) { if (GlobalModel.termFontSize.get() == newFontSize) {
return; return;
} }
@ -39,10 +35,12 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
} }
@boundMethod @boundMethod
togglefontSizeDropdown(): void { handleChangeFontFamily(fontFamily: string): void {
mobx.action(() => { if (GlobalModel.getTermFontFamily() == fontFamily) {
this.fontSizeDropdownActive.set(!this.fontSizeDropdownActive.get()); return;
})(); }
const prtn = GlobalCommandRunner.setTermFontFamily(fontFamily, false);
commandRtnHandler(prtn, this.errorMessage);
} }
@boundMethod @boundMethod
@ -75,6 +73,13 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
return availableFontSizes; return availableFontSizes;
} }
getFontFamilies(): DropdownItem[] {
const availableFontFamilies: DropdownItem[] = [];
availableFontFamilies.push({ label: "JetBrains Mono", value: "JetBrains Mono" });
availableFontFamilies.push({ label: "Hack", value: "Hack" });
return availableFontFamilies;
}
@boundMethod @boundMethod
inlineUpdateOpenAIModel(newModel: string): void { inlineUpdateOpenAIModel(newModel: string): void {
const prtn = GlobalCommandRunner.setClientOpenAISettings({ model: newModel }); const prtn = GlobalCommandRunner.setClientOpenAISettings({ model: newModel });
@ -140,6 +145,7 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
openAIOpts.maxtokens == null || openAIOpts.maxtokens == 0 ? 1000 : openAIOpts.maxtokens openAIOpts.maxtokens == null || openAIOpts.maxtokens == 0 ? 1000 : openAIOpts.maxtokens
); );
const curFontSize = GlobalModel.termFontSize.get(); const curFontSize = GlobalModel.termFontSize.get();
const curFontFamily = GlobalModel.getTermFontFamily();
return ( return (
<div className={cn("view clientsettings-view")}> <div className={cn("view clientsettings-view")}>
@ -161,6 +167,17 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
/> />
</div> </div>
</div> </div>
<div className="settings-field">
<div className="settings-label">Term Font Family</div>
<div className="settings-input">
<Dropdown
className="font-size-dropdown"
options={this.getFontFamilies()}
defaultValue={curFontFamily}
onChange={this.handleChangeFontFamily}
/>
</div>
</div>
<div className="settings-field"> <div className="settings-field">
<div className="settings-label">Client ID</div> <div className="settings-label">Client ID</div>
<div className="settings-input">{cdata.clientid}</div> <div className="settings-input">{cdata.clientid}</div>

View File

@ -10,14 +10,14 @@
code { code {
background-color: @markdown-highlight; background-color: @markdown-highlight;
color: @term-white; color: @term-white;
font-family: @terminal-font; font-family: var(--termfontfamily);
border-radius: 4px; border-radius: 4px;
} }
code.inline { code.inline {
padding-top: 0; padding-top: 0;
padding-bottom: 0; padding-bottom: 0;
font-family: @terminal-font; font-family: var(--termfontfamily);
} }
.title { .title {

View File

@ -1,7 +1,9 @@
@import "@/common/themes/themes.less"; @import "@/common/themes/themes.less";
.term-prompt { .term-prompt {
font-weight: 300; font-weight: normal;
font-family: var(--termfontfamily);
.icon { .icon {
margin: 0 4px 0 2px; margin: 0 4px 0 2px;
vertical-align: middle; vertical-align: middle;

View File

@ -69,7 +69,6 @@
// @font-face declaration lives in app.less // @font-face declaration lives in app.less
@fixed-font: "Martian Mono", sans-serif; @fixed-font: "Martian Mono", sans-serif;
@terminal-font: "JetBrains Mono", sans-serif;
@text-s1-font: "Martian Mono", sans-serif; @text-s1-font: "Martian Mono", sans-serif;

View File

@ -492,7 +492,8 @@ class LineCmd extends React.Component<
maxSize: screen.getMaxContentSize(), maxSize: screen.getMaxContentSize(),
idealSize: screen.getIdealContentSize(), idealSize: screen.getIdealContentSize(),
termOpts: cmd.getTermOpts(), termOpts: cmd.getTermOpts(),
termFontSize: GlobalModel.termFontSize.get(), termFontSize: GlobalModel.getTermFontSize(),
termFontFamily: GlobalModel.getTermFontFamily(),
}; };
} }

View File

@ -132,7 +132,7 @@
position: relative; position: relative;
.cmd-rtnstate-label { .cmd-rtnstate-label {
font-family: @terminal-font; font-family: var(--termfontfamily);
position: relative; position: relative;
font-size: 10px; font-size: 10px;
z-index: 2; z-index: 2;
@ -145,7 +145,7 @@
} }
.cmd-rtnstate-diff { .cmd-rtnstate-diff {
font-family: @terminal-font; font-family: var(--termfontfamily);
color: @term-white; color: @term-white;
white-space: pre; white-space: pre;
margin-left: 10px; margin-left: 10px;

View File

@ -137,7 +137,8 @@
overflow-wrap: anywhere; overflow-wrap: anywhere;
border-color: transparent; border-color: transparent;
border: none; border: none;
font-family: @terminal-font; font-family: var(--termfontfamily);
font-weight: normal;
&.display-disabled { &.display-disabled {
background-color: #444; background-color: #444;
} }
@ -229,7 +230,8 @@
overflow-wrap: anywhere; overflow-wrap: anywhere;
border-color: transparent; border-color: transparent;
border: none; border: none;
font-family: @terminal-font; font-family: var(--termfontfamily);
font-weight: normal;
flex-shrink: 0; flex-shrink: 0;
flex-grow: 1; flex-grow: 1;
border-radius: 4px; border-radius: 4px;

View File

@ -31,6 +31,7 @@ let oldConsoleLog = console.log;
let wasActive = true; let wasActive = true;
let wasInFg = true; let wasInFg = true;
let currentGlobalShortcut: string | null = null; let currentGlobalShortcut: string | null = null;
let initialClientData: ClientDataType = null;
checkPromptMigrate(); checkPromptMigrate();
ensureDir(waveHome); ensureDir(waveHome);
@ -519,6 +520,11 @@ electron.ipcMain.on("wavesrv-status", (event) => {
return; return;
}); });
electron.ipcMain.on("get-initial-termfontfamily", (event) => {
event.returnValue = initialClientData?.feopts?.termfontfamily;
return;
});
electron.ipcMain.on("restart-server", (event) => { electron.ipcMain.on("restart-server", (event) => {
if (waveSrvProc != null) { if (waveSrvProc != null) {
waveSrvProc.kill(); waveSrvProc.kill();
@ -709,6 +715,7 @@ async function createMainWindowWrap() {
let clientData: ClientDataType | null = null; let clientData: ClientDataType | null = null;
try { try {
clientData = await getClientDataPoll(1); clientData = await getClientDataPoll(1);
initialClientData = clientData;
} catch (e) { } catch (e) {
console.log("error getting wavesrv clientdata", e.toString()); console.log("error getting wavesrv clientdata", e.toString());
} }

View File

@ -10,6 +10,7 @@ contextBridge.exposeInMainWorld("api", {
ipcRenderer.send("get-last-logs", numberOfLines); ipcRenderer.send("get-last-logs", numberOfLines);
ipcRenderer.once("last-logs", (event, data) => callback(data)); ipcRenderer.once("last-logs", (event, data) => callback(data));
}, },
getInitialTermFontFamily: () => ipcRenderer.sendSync("get-initial-termfontfamily"),
restartWaveSrv: () => ipcRenderer.sendSync("restart-server"), restartWaveSrv: () => ipcRenderer.sendSync("restart-server"),
reloadWindow: () => ipcRenderer.sendSync("reload-window"), reloadWindow: () => ipcRenderer.sendSync("reload-window"),
reregisterGlobalShortcut: (shortcut) => ipcRenderer.sendSync("reregister-global-shortcut", shortcut), reregisterGlobalShortcut: (shortcut) => ipcRenderer.sendSync("reregister-global-shortcut", shortcut),

View File

@ -9,13 +9,18 @@ import { App } from "@/app/app";
import * as DOMPurify from "dompurify"; import * as DOMPurify from "dompurify";
import { loadFonts } from "./util/util"; import { loadFonts } from "./util/util";
import * as textmeasure from "./util/textmeasure"; import * as textmeasure from "./util/textmeasure";
import { getApi } from "@/models";
// @ts-ignore // @ts-ignore
let VERSION = __WAVETERM_VERSION__; let VERSION = __WAVETERM_VERSION__;
// @ts-ignore // @ts-ignore
let BUILD = __WAVETERM_BUILD__; let BUILD = __WAVETERM_BUILD__;
loadFonts(); let initialFontFamily = getApi().getInitialTermFontFamily();
if (initialFontFamily == null) {
initialFontFamily = "JetBrains Mono";
}
loadFonts(initialFontFamily);
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
let reactElem = React.createElement(App, null, null); let reactElem = React.createElement(App, null, null);
@ -26,6 +31,7 @@ document.addEventListener("DOMContentLoaded", () => {
}); });
}); });
// put some items on the window for debugging
(window as any).mobx = mobx; (window as any).mobx = mobx;
(window as any).sprintf = sprintf; (window as any).sprintf = sprintf;
(window as any).DOMPurify = DOMPurify; (window as any).DOMPurify = DOMPurify;

View File

@ -353,6 +353,14 @@ class CommandRunner {
return GlobalModel.submitCommand("client", "set", null, kwargs, interactive); return GlobalModel.submitCommand("client", "set", null, kwargs, interactive);
} }
setTermFontFamily(fontFamily: string, interactive: boolean): Promise<CommandRtnType> {
let kwargs = {
nohist: "1",
termfontfamily: fontFamily,
};
return GlobalModel.submitCommand("client", "set", null, kwargs, interactive);
}
setClientOpenAISettings(opts: { model?: string; apitoken?: string; maxtokens?: string }): Promise<CommandRtnType> { setClientOpenAISettings(opts: { model?: string; apitoken?: string; maxtokens?: string }): Promise<CommandRtnType> {
let kwargs = { let kwargs = {
nohist: "1", nohist: "1",

View File

@ -12,6 +12,7 @@ import {
genMergeSimpleData, genMergeSimpleData,
isModKeyPress, isModKeyPress,
isBlank, isBlank,
loadFonts,
} from "@/util/util"; } from "@/util/util";
import { WSControl } from "./ws"; import { WSControl } from "./ws";
import { cmdStatusIsRunning } from "@/app/line/lineutil"; import { cmdStatusIsRunning } from "@/app/line/lineutil";
@ -120,6 +121,10 @@ class Model {
}); });
packetSeqNum: number = 0; packetSeqNum: number = 0;
renderVersion: OV<number> = mobx.observable.box(0, {
name: "renderVersion",
});
private constructor() { private constructor() {
this.clientId = getApi().getId(); this.clientId = getApi().getId();
this.isDev = getApi().getIsDev(); this.isDev = getApi().getIsDev();
@ -185,6 +190,12 @@ class Model {
return (window as any).GlobalModel; return (window as any).GlobalModel;
} }
bumpRenderVersion() {
mobx.action(() => {
this.renderVersion.set(this.renderVersion.get() + 1);
})();
}
getNextPacketSeqNum(): number { getNextPacketSeqNum(): number {
this.packetSeqNum++; this.packetSeqNum++;
return this.packetSeqNum; return this.packetSeqNum;
@ -314,6 +325,19 @@ class Model {
return appconst.ProdServerEndpoint; return appconst.ProdServerEndpoint;
} }
getTermFontFamily(): string {
let cdata = this.clientData.get();
let ff = cdata?.feopts?.termfontfamily;
if (ff == null) {
ff = "JetBrains Mono, monospace";
}
return ff;
}
getTermFontSize(): number {
return this.termFontSize.get();
}
setTermFontSize(fontSize: number) { setTermFontSize(fontSize: number) {
if (fontSize < appconst.MinFontSize) { if (fontSize < appconst.MinFontSize) {
fontSize = appconst.MinFontSize; fontSize = appconst.MinFontSize;
@ -323,6 +347,7 @@ class Model {
} }
mobx.action(() => { mobx.action(() => {
this.termFontSize.set(fontSize); this.termFontSize.set(fontSize);
this.bumpRenderVersion();
})(); })();
} }
@ -1114,6 +1139,15 @@ class Model {
shortcut = clientData?.clientopts?.globalshortcut; shortcut = clientData?.clientopts?.globalshortcut;
} }
getApi().reregisterGlobalShortcut(shortcut); getApi().reregisterGlobalShortcut(shortcut);
let fontFamily = clientData?.feopts?.termfontfamily;
if (fontFamily == null) {
fontFamily = "JetBrains Mono";
}
loadFonts(fontFamily);
document.fonts.ready.then(() => {
clearMonoFontCache();
this.bumpRenderVersion();
});
} }
submitCommandPacket(cmdPk: FeCmdPacketType, interactive: boolean): Promise<CommandRtnType> { submitCommandPacket(cmdPk: FeCmdPacketType, interactive: boolean): Promise<CommandRtnType> {
@ -1452,4 +1486,4 @@ class Model {
} }
} }
export { Model }; export { Model, getApi };

View File

@ -190,7 +190,8 @@ class RemotesModel {
}, },
focusHandler: this.setRemoteTermWrapFocus.bind(this), focusHandler: this.setRemoteTermWrapFocus.bind(this),
isRunning: true, isRunning: true,
fontSize: this.globalModel.termFontSize.get(), fontSize: this.globalModel.getTermFontSize(),
fontFamily: this.globalModel.getTermFontFamily(),
ptyDataSource: getTermPtyData, ptyDataSource: getTermPtyData,
onUpdateContentHeight: null, onUpdateContentHeight: null,
}); });

View File

@ -588,7 +588,8 @@ class Screen {
focusHandler: (focus: boolean) => this.setLineFocus(line.linenum, focus), focusHandler: (focus: boolean) => this.setLineFocus(line.linenum, focus),
isRunning: cmd.isRunning(), isRunning: cmd.isRunning(),
customKeyHandler: this.termCustomKeyHandler.bind(this), customKeyHandler: this.termCustomKeyHandler.bind(this),
fontSize: this.globalModel.termFontSize.get(), fontSize: this.globalModel.getTermFontSize(),
fontFamily: this.globalModel.getTermFontFamily(),
ptyDataSource: getTermPtyData, ptyDataSource: getTermPtyData,
onUpdateContentHeight: (termContext: RendererContext, height: number) => { onUpdateContentHeight: (termContext: RendererContext, height: number) => {
this.globalModel.setContentHeight(termContext, height); this.globalModel.setContentHeight(termContext, height);

View File

@ -90,7 +90,8 @@ class SpecialLineContainer {
focusHandler: null, focusHandler: null,
isRunning: cmd.isRunning(), isRunning: cmd.isRunning(),
customKeyHandler: null, customKeyHandler: null,
fontSize: this.globalModel.termFontSize.get(), fontSize: this.globalModel.getTermFontSize(),
fontFamily: this.globalModel.getTermFontFamily(),
ptyDataSource: getTermPtyData, ptyDataSource: getTermPtyData,
onUpdateContentHeight: null, onUpdateContentHeight: null,
}); });

View File

@ -337,8 +337,8 @@ class SourceCodeRenderer extends React.Component<
onMount={this.handleEditorDidMount} onMount={this.handleEditorDidMount}
options={{ options={{
scrollBeyondLastLine: false, scrollBeyondLastLine: false,
fontSize: GlobalModel.termFontSize.get() * 0.9, fontSize: GlobalModel.getTermFontSize(),
/* fontFamily: "Martian Mono", */ fontFamily: GlobalModel.getTermFontFamily(),
readOnly: !this.getAllowEditing(), readOnly: !this.getAllowEditing(),
}} }}
onChange={this.handleEditorChange} onChange={this.handleEditorChange}

View File

@ -31,6 +31,7 @@ type TermWrapOpts = {
isRunning: boolean; isRunning: boolean;
customKeyHandler?: (event: any, termWrap: TermWrap) => boolean; customKeyHandler?: (event: any, termWrap: TermWrap) => boolean;
fontSize: number; fontSize: number;
fontFamily: string;
ptyDataSource: (termContext: TermContextUnion) => Promise<PtyDataType>; ptyDataSource: (termContext: TermContextUnion) => Promise<PtyDataType>;
onUpdateContentHeight: (termContext: RendererContext, height: number) => void; onUpdateContentHeight: (termContext: RendererContext, height: number) => void;
}; };
@ -53,6 +54,7 @@ class TermWrap {
focusHandler: (focus: boolean) => void; focusHandler: (focus: boolean) => void;
isRunning: boolean; isRunning: boolean;
fontSize: number; fontSize: number;
fontFamily: string;
onUpdateContentHeight: (termContext: RendererContext, height: number) => void; onUpdateContentHeight: (termContext: RendererContext, height: number) => void;
ptyDataSource: (termContext: TermContextUnion) => Promise<PtyDataType>; ptyDataSource: (termContext: TermContextUnion) => Promise<PtyDataType>;
initializing: boolean; initializing: boolean;
@ -67,6 +69,7 @@ class TermWrap {
this.focusHandler = opts.focusHandler; this.focusHandler = opts.focusHandler;
this.isRunning = opts.isRunning; this.isRunning = opts.isRunning;
this.fontSize = opts.fontSize; this.fontSize = opts.fontSize;
this.fontFamily = opts.fontFamily;
this.ptyDataSource = opts.ptyDataSource; this.ptyDataSource = opts.ptyDataSource;
this.onUpdateContentHeight = opts.onUpdateContentHeight; this.onUpdateContentHeight = opts.onUpdateContentHeight;
this.initializing = true; this.initializing = true;
@ -88,7 +91,7 @@ class TermWrap {
rows: this.termSize.rows, rows: this.termSize.rows,
cols: this.termSize.cols, cols: this.termSize.cols,
fontSize: opts.fontSize, fontSize: opts.fontSize,
fontFamily: "JetBrains Mono", fontFamily: opts.fontFamily,
theme: { foreground: terminal.foreground, background: terminal.background }, theme: { foreground: terminal.foreground, background: terminal.background },
}); });
this.terminal.loadAddon( this.terminal.loadAddon(

10
src/types/custom.d.ts vendored
View File

@ -471,6 +471,7 @@ declare global {
idealSize: WindowSize; idealSize: WindowSize;
termOpts: TermOptsType; termOpts: TermOptsType;
termFontSize: number; termFontSize: number;
termFontFamily: string;
}; };
type RendererOptsUpdate = { type RendererOptsUpdate = {
@ -558,6 +559,7 @@ declare global {
type FeOptsType = { type FeOptsType = {
termfontsize: number; termfontsize: number;
termfontfamily: string;
}; };
type ConfirmFlagsType = { type ConfirmFlagsType = {
@ -589,6 +591,13 @@ declare global {
fullscreen: boolean; fullscreen: boolean;
}; };
type KeyModsType = {
meta?: boolean;
ctrl?: boolean;
alt?: boolean;
shift?: boolean;
};
type ClientDataType = { type ClientDataType = {
clientid: string; clientid: string;
userid: string; userid: string;
@ -847,6 +856,7 @@ declare global {
getPlatform: () => string; getPlatform: () => string;
getAuthKey: () => string; getAuthKey: () => string;
getWaveSrvStatus: () => boolean; getWaveSrvStatus: () => boolean;
getInitialTermFontFamily: () => string;
restartWaveSrv: () => boolean; restartWaveSrv: () => boolean;
reloadWindow: () => void; reloadWindow: () => void;
openExternalLink: (url: string) => void; openExternalLink: (url: string) => void;

View File

@ -242,9 +242,19 @@ function incObs(inum: mobx.IObservableValue<number>) {
})(); })();
} }
// @check:font function addToFontFaceSet(fontFaceSet: FontFaceSet, fontFace: FontFace) {
function loadFonts() { // any cast to work around typing issue
const jbmFontNormal = new FontFace("JetBrains Mono", "url('public/fonts/jetbrains-mono-v13-latin-regular.woff2')", { (fontFaceSet as any).add(fontFace);
}
let isJetBrainsMonoLoaded = false;
function loadJetBrainsMonoFont() {
if (isJetBrainsMonoLoaded) {
return;
}
isJetBrainsMonoLoaded = true;
let jbmFontNormal = new FontFace("JetBrains Mono", "url('public/fonts/jetbrains-mono-v13-latin-regular.woff2')", {
style: "normal", style: "normal",
weight: "400", weight: "400",
}); });
@ -256,19 +266,78 @@ function loadFonts() {
style: "normal", style: "normal",
weight: "700", weight: "700",
}); });
const faFont = new FontFace("FontAwesome", "url(public/fonts/fontawesome-webfont-4.7.woff2)", { addToFontFaceSet(document.fonts, jbmFontNormal);
style: "normal", addToFontFaceSet(document.fonts, jbmFont200);
weight: "normal", addToFontFaceSet(document.fonts, jbmFont700);
});
const docFonts: any = document.fonts; // work around ts typing issue
docFonts.add(jbmFontNormal);
docFonts.add(jbmFont200);
docFonts.add(jbmFont700);
docFonts.add(faFont);
jbmFontNormal.load(); jbmFontNormal.load();
jbmFont200.load(); jbmFont200.load();
jbmFont700.load(); jbmFont700.load();
}
let isHackFontLoaded = false;
function loadHackFont() {
if (isHackFontLoaded) {
return;
}
isHackFontLoaded = true;
let hackRegular = new FontFace("Hack", "url('public/fonts/hack-regular.woff2')", {
style: "normal",
weight: "400",
});
let hackBold = new FontFace("Hack", "url('public/fonts/hack-bold.woff2')", {
style: "normal",
weight: "700",
});
let hackItalic = new FontFace("Hack", "url('public/fonts/hack-italic.woff2')", {
style: "italic",
weight: "400",
});
let hackBoldItalic = new FontFace("Hack", "url('public/fonts/hack-bolditalic.woff2')", {
style: "italic",
weight: "700",
});
addToFontFaceSet(document.fonts, hackRegular);
addToFontFaceSet(document.fonts, hackBold);
addToFontFaceSet(document.fonts, hackItalic);
addToFontFaceSet(document.fonts, hackBoldItalic);
hackRegular.load();
hackBold.load();
hackItalic.load();
hackBoldItalic.load();
}
let isBaseFontsLoaded = false;
function loadBaseFonts() {
if (isBaseFontsLoaded) {
return;
}
isBaseFontsLoaded = true;
let faFont = new FontFace("FontAwesome", "url(public/fonts/fontawesome-webfont-4.7.woff2)", {
style: "normal",
weight: "normal",
});
let mmFont = new FontFace("Martian Mono", "url(public/fonts/MartianMono-VariableFont_wdth,wght.ttf)", {
style: "normal",
weight: "normal",
});
addToFontFaceSet(document.fonts, faFont);
addToFontFaceSet(document.fonts, mmFont);
faFont.load(); faFont.load();
mmFont.load();
}
function loadFonts(termFont: string) {
loadBaseFonts();
loadJetBrainsMonoFont();
if (termFont == "Hack") {
loadHackFont();
}
document.documentElement.style.setProperty(
"--termfontfamily",
'"' + termFont + '"' + ', "JetBrains Mono", monospace'
);
} }
const DOW_STRS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; const DOW_STRS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];

View File

@ -5134,6 +5134,24 @@ func validateOpenAIModel(model string) error {
return nil return nil
} }
const MaxFontFamilyLen = 50
var fontfamilyRe = regexp.MustCompile(`^[a-zA-Z0-9_ -]+$`)
func validateFontFamily(fontFamily string) error {
if len(fontFamily) == 0 {
return nil
}
if len(fontFamily) > MaxFontFamilyLen {
return fmt.Errorf("invalid font family, too long")
}
m := fontfamilyRe.MatchString(fontFamily)
if !m {
return fmt.Errorf("invalid font family, must match %q", fontfamilyRe.String())
}
return nil
}
func ClientSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.UpdatePacket, error) { func ClientSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.UpdatePacket, error) {
clientData, err := sstore.EnsureClientData(ctx) clientData, err := sstore.EnsureClientData(ctx)
if err != nil { if err != nil {
@ -5156,6 +5174,20 @@ func ClientSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sc
} }
varsUpdated = append(varsUpdated, "termfontsize") varsUpdated = append(varsUpdated, "termfontsize")
} }
if fontFamilyStr, found := pk.Kwargs["termfontfamily"]; found {
newFontFamily := fontFamilyStr
err = validateFontFamily(newFontFamily)
if err != nil {
return nil, err
}
feOpts := clientData.FeOpts
feOpts.TermFontFamily = newFontFamily
err = sstore.UpdateClientFeOpts(ctx, feOpts)
if err != nil {
return nil, fmt.Errorf("error updating client feopts: %v", err)
}
varsUpdated = append(varsUpdated, "termfontfamily")
}
if apiToken, found := pk.Kwargs["openaiapitoken"]; found { if apiToken, found := pk.Kwargs["openaiapitoken"]; found {
err = validateOpenAIAPIToken(apiToken) err = validateOpenAIAPIToken(apiToken)
if err != nil { if err != nil {
@ -5244,7 +5276,7 @@ func ClientSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sc
} }
} }
if len(varsUpdated) == 0 { if len(varsUpdated) == 0 {
return nil, fmt.Errorf("/client:set requires a value to set: %s", formatStrs([]string{"termfontsize", "openaiapitoken", "openaimodel", "openaibaseurl", "openaimaxtokens", "openaimaxchoices"}, "or", false)) return nil, fmt.Errorf("/client:set requires a value to set: %s", formatStrs([]string{"termfontsize", "termfontfamily", "openaiapitoken", "openaimodel", "openaibaseurl", "openaimaxtokens", "openaimaxchoices"}, "or", false))
} }
clientData, err = sstore.EnsureClientData(ctx) clientData, err = sstore.EnsureClientData(ctx)
if err != nil { if err != nil {

View File

@ -296,6 +296,7 @@ type ClientOptsType struct {
type FeOptsType struct { type FeOptsType struct {
TermFontSize int `json:"termfontsize,omitempty"` TermFontSize int `json:"termfontsize,omitempty"`
TermFontFamily string `json:"termfontfamily,omitempty"`
} }
type ReleaseInfoType struct { type ReleaseInfoType struct {