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/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
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";
@font-face {
font-family: "Martian Mono";
src: url("./assets/fonts/MartianMono-VariableFont_wdth,wght.ttf") format("truetype");
font-weight: normal;
font-style: normal;
}
:root {
--fa-style-family: "Font Awesome 6 Sharp";
}
@ -98,7 +91,7 @@ body a {
}
body code {
font-family: @terminal-font;
font-family: var(--termfontfamily);
background-color: transparent;
}
@ -312,7 +305,7 @@ a.a-block {
}
.mono {
font-family: @terminal-font;
font-family: var(--termfontfamily);
}
}

View File

@ -105,8 +105,15 @@ class App extends React.Component<{}, {}> {
if (dcWait) {
setTimeout(() => this.updateDcWait(false), 0);
}
// used to force a full reload of the application
let renderVersion = GlobalModel.renderVersion.get();
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">
<MainSideBar parentRef={this.mainContentRef} clientData={clientData} />
<ErrorBoundary>

View File

@ -26,6 +26,10 @@
padding: 0 18px 0 30px;
}
.wave-dropdown {
width: 200px;
}
// 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 {

View File

@ -15,9 +15,6 @@ import "./clientsettings.less";
@mobxReact.observer
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" });
@boundMethod
@ -30,7 +27,6 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
@boundMethod
handleChangeFontSize(fontSize: string): void {
const newFontSize = Number(fontSize);
this.fontSizeDropdownActive.set(false);
if (GlobalModel.termFontSize.get() == newFontSize) {
return;
}
@ -39,10 +35,12 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
}
@boundMethod
togglefontSizeDropdown(): void {
mobx.action(() => {
this.fontSizeDropdownActive.set(!this.fontSizeDropdownActive.get());
})();
handleChangeFontFamily(fontFamily: string): void {
if (GlobalModel.getTermFontFamily() == fontFamily) {
return;
}
const prtn = GlobalCommandRunner.setTermFontFamily(fontFamily, false);
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
@ -75,6 +73,13 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
return availableFontSizes;
}
getFontFamilies(): DropdownItem[] {
const availableFontFamilies: DropdownItem[] = [];
availableFontFamilies.push({ label: "JetBrains Mono", value: "JetBrains Mono" });
availableFontFamilies.push({ label: "Hack", value: "Hack" });
return availableFontFamilies;
}
@boundMethod
inlineUpdateOpenAIModel(newModel: string): void {
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
);
const curFontSize = GlobalModel.termFontSize.get();
const curFontFamily = GlobalModel.getTermFontFamily();
return (
<div className={cn("view clientsettings-view")}>
@ -161,6 +167,17 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
/>
</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-label">Client ID</div>
<div className="settings-input">{cdata.clientid}</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,6 +10,7 @@ contextBridge.exposeInMainWorld("api", {
ipcRenderer.send("get-last-logs", numberOfLines);
ipcRenderer.once("last-logs", (event, data) => callback(data));
},
getInitialTermFontFamily: () => ipcRenderer.sendSync("get-initial-termfontfamily"),
restartWaveSrv: () => ipcRenderer.sendSync("restart-server"),
reloadWindow: () => ipcRenderer.sendSync("reload-window"),
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 { loadFonts } from "./util/util";
import * as textmeasure from "./util/textmeasure";
import { getApi } from "@/models";
// @ts-ignore
let VERSION = __WAVETERM_VERSION__;
// @ts-ignore
let BUILD = __WAVETERM_BUILD__;
loadFonts();
let initialFontFamily = getApi().getInitialTermFontFamily();
if (initialFontFamily == null) {
initialFontFamily = "JetBrains Mono";
}
loadFonts(initialFontFamily);
document.addEventListener("DOMContentLoaded", () => {
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).sprintf = sprintf;
(window as any).DOMPurify = DOMPurify;

View File

@ -353,6 +353,14 @@ class CommandRunner {
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> {
let kwargs = {
nohist: "1",

View File

@ -12,6 +12,7 @@ import {
genMergeSimpleData,
isModKeyPress,
isBlank,
loadFonts,
} from "@/util/util";
import { WSControl } from "./ws";
import { cmdStatusIsRunning } from "@/app/line/lineutil";
@ -120,6 +121,10 @@ class Model {
});
packetSeqNum: number = 0;
renderVersion: OV<number> = mobx.observable.box(0, {
name: "renderVersion",
});
private constructor() {
this.clientId = getApi().getId();
this.isDev = getApi().getIsDev();
@ -185,6 +190,12 @@ class Model {
return (window as any).GlobalModel;
}
bumpRenderVersion() {
mobx.action(() => {
this.renderVersion.set(this.renderVersion.get() + 1);
})();
}
getNextPacketSeqNum(): number {
this.packetSeqNum++;
return this.packetSeqNum;
@ -314,6 +325,19 @@ class Model {
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) {
if (fontSize < appconst.MinFontSize) {
fontSize = appconst.MinFontSize;
@ -323,6 +347,7 @@ class Model {
}
mobx.action(() => {
this.termFontSize.set(fontSize);
this.bumpRenderVersion();
})();
}
@ -1114,6 +1139,15 @@ class Model {
shortcut = clientData?.clientopts?.globalshortcut;
}
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> {
@ -1452,4 +1486,4 @@ class Model {
}
}
export { Model };
export { Model, getApi };

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

@ -242,9 +242,19 @@ function incObs(inum: mobx.IObservableValue<number>) {
})();
}
// @check:font
function loadFonts() {
const jbmFontNormal = new FontFace("JetBrains Mono", "url('public/fonts/jetbrains-mono-v13-latin-regular.woff2')", {
function addToFontFaceSet(fontFaceSet: FontFaceSet, fontFace: FontFace) {
// any cast to work around typing issue
(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",
weight: "400",
});
@ -256,19 +266,78 @@ function loadFonts() {
style: "normal",
weight: "700",
});
const faFont = new FontFace("FontAwesome", "url(public/fonts/fontawesome-webfont-4.7.woff2)", {
style: "normal",
weight: "normal",
});
const docFonts: any = document.fonts; // work around ts typing issue
docFonts.add(jbmFontNormal);
docFonts.add(jbmFont200);
docFonts.add(jbmFont700);
docFonts.add(faFont);
addToFontFaceSet(document.fonts, jbmFontNormal);
addToFontFaceSet(document.fonts, jbmFont200);
addToFontFaceSet(document.fonts, jbmFont700);
jbmFontNormal.load();
jbmFont200.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();
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"];

View File

@ -5134,6 +5134,24 @@ func validateOpenAIModel(model string) error {
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) {
clientData, err := sstore.EnsureClientData(ctx)
if err != nil {
@ -5156,6 +5174,20 @@ func ClientSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sc
}
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 {
err = validateOpenAIAPIToken(apiToken)
if err != nil {
@ -5244,7 +5276,7 @@ func ClientSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sc
}
}
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)
if err != nil {

View File

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