Compare commits

..

2 Commits

Author SHA1 Message Date
Sylvia Crowe 3135a3cd76 fix: split the apple locale on the region for LANG
We use AppleLocale to get the desired language when running on macos.
Unfortunately, if a region is set, the locale is not equivalent to the
LANG environment variable. It appends an extra region field that we did
not previously filter out. This change ensures that it will be filtered
out when present.
2024-03-13 17:59:43 -07:00
Mike Sawka fc0b82836c quick UI updates (center imageview, fix terminal cursor colors, and add firacode font) (#445)
* center imageview images

* fix terminal selection colors #442

* add firacode font
2024-03-13 16:24:47 -07:00
17 changed files with 318 additions and 115 deletions
Binary file not shown.
Binary file not shown.
+3 -2
View File
@@ -3,7 +3,7 @@
:root {
/*
* term colors (16 + 5) form the base terminal theme
* term colors (16 + 6) form the base terminal theme
* for consistency these colors should be used by plugins/applications
*/
--term-black: #000000;
@@ -27,5 +27,6 @@
--term-cmdtext: #ffffff;
--term-foreground: #d3d7cf;
--term-background: #000000;
--term-selection-background: #ffffff40;
--term-selection-background: #ffffff90;
--term-cursor-accent: #000000;
}
+2 -1
View File
@@ -6,5 +6,6 @@
--term-foreground: #000000;
--term-background: #fefefe;
--term-cmdtext: #000000;
--term-selection-background: #00000018;
--term-selection-background: #00000040;
--term-cursor-accent: #000000;
}
@@ -87,6 +87,7 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
const availableFontFamilies: DropdownItem[] = [];
availableFontFamilies.push({ label: "JetBrains Mono", value: "JetBrains Mono" });
availableFontFamilies.push({ label: "Hack", value: "Hack" });
availableFontFamilies.push({ label: "Fira Code", value: "Fira Code" });
return availableFontFamilies;
}
+276
View File
@@ -0,0 +1,276 @@
// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { debounce } from "throttle-debounce";
import * as util from "@/util/util";
import { GlobalModel } from "@/models";
class SimpleBlobRendererModel {
context: RendererContext;
opts: RendererOpts;
isDone: OV<boolean>;
api: RendererModelContainerApi;
savedHeight: number;
loading: OV<boolean>;
loadError: OV<string> = mobx.observable.box(null, {
name: "renderer-loadError",
});
lineState: LineStateType;
ptyData: PtyDataType;
ptyDataSource: (termContext: TermContextUnion) => Promise<PtyDataType>;
dataBlob: Blob;
readOnly: boolean;
notFound: boolean;
initialize(params: RendererModelInitializeParams): void {
this.loading = mobx.observable.box(true, { name: "renderer-loading" });
this.isDone = mobx.observable.box(params.isDone, {
name: "renderer-isDone",
});
this.context = params.context;
this.opts = params.opts;
this.api = params.api;
this.lineState = params.lineState;
this.savedHeight = params.savedHeight;
this.ptyDataSource = params.ptyDataSource;
if (this.isDone.get()) {
setTimeout(() => this.reload(0), 10);
}
}
dispose(): void {
return;
}
giveFocus(): void {
return;
}
updateOpts(update: RendererOptsUpdate): void {
Object.assign(this.opts, update);
}
updateHeight(newHeight: number): void {
if (this.savedHeight != newHeight) {
this.savedHeight = newHeight;
this.api.saveHeight(newHeight);
}
}
setIsDone(): void {
if (this.isDone.get()) {
return;
}
mobx.action(() => {
this.isDone.set(true);
})();
this.reload(0);
}
reload(delayMs: number): void {
mobx.action(() => {
this.loading.set(true);
})();
if (delayMs == 0) {
this.reload_noDelay();
} else {
setTimeout(() => {
this.reload_noDelay();
}, delayMs);
}
}
reload_noDelay(): void {
let source = this.lineState["prompt:source"] || "pty";
if (source == "pty") {
this.reloadPtyData();
} else if (source == "file") {
this.reloadFileData();
} else {
mobx.action(() => {
this.loadError.set("error: invalid load source: " + source);
})();
}
}
reloadFileData(): void {
// todo add file methods to API, so we don't have a GlobalModel dependency here!
let path = this.lineState["prompt:file"];
if (util.isBlank(path)) {
mobx.action(() => {
this.loadError.set("renderer has file source, but no prompt:file specified");
})();
return;
}
let rtnp = GlobalModel.readRemoteFile(this.context.screenId, this.context.lineId, path);
rtnp.then((file) => {
this.notFound = (file as any).notFound;
this.readOnly = (file as any).readOnly;
this.dataBlob = file;
mobx.action(() => {
this.loading.set(false);
this.loadError.set(null);
})();
}).catch((e) => {
mobx.action(() => {
this.loadError.set("error loading file data: " + e);
})();
});
}
reloadPtyData(): void {
this.readOnly = true;
let rtnp = this.ptyDataSource(this.context);
if (rtnp == null) {
console.log("no promise returned from ptyDataSource (simplerenderer)", this.context);
return;
}
rtnp.then((ptydata) => {
this.ptyData = ptydata;
this.dataBlob = new Blob([this.ptyData.data]);
mobx.action(() => {
this.loading.set(false);
this.loadError.set(null);
})();
}).catch((e) => {
mobx.action(() => {
this.loadError.set("error loading data: " + e);
})();
});
}
receiveData(pos: number, data: Uint8Array, reason?: string): void {
// this.dataBuf.receiveData(pos, data, reason);
}
}
@mobxReact.observer
class SimpleBlobRenderer extends React.Component<
{
rendererContainer: RendererContainerType;
lineId: string;
plugin: RendererPluginType;
onHeightChange: () => void;
initParams: RendererModelInitializeParams;
scrollToBringIntoViewport: () => void;
isSelected: boolean;
shouldFocus: boolean;
},
{}
> {
model: SimpleBlobRendererModel;
wrapperDivRef: React.RefObject<any> = React.createRef();
rszObs: ResizeObserver;
updateHeight_debounced: (newHeight: number) => void;
constructor(props: any) {
super(props);
let { rendererContainer, lineId, plugin, initParams } = this.props;
this.model = new SimpleBlobRendererModel();
this.model.initialize(initParams);
rendererContainer.registerRenderer(lineId, this.model);
this.updateHeight_debounced = debounce(1000, this.updateHeight.bind(this));
}
updateHeight(newHeight: number): void {
this.model.updateHeight(newHeight);
}
handleResize(entries: ResizeObserverEntry[]): void {
if (this.model.loading.get()) {
return;
}
if (this.props.onHeightChange) {
this.props.onHeightChange();
}
if (!this.model.loading.get() && this.wrapperDivRef.current != null) {
let height = this.wrapperDivRef.current.offsetHeight;
this.updateHeight_debounced(height);
}
}
checkRszObs() {
if (this.rszObs != null) {
return;
}
if (this.wrapperDivRef.current == null) {
return;
}
this.rszObs = new ResizeObserver(this.handleResize.bind(this));
this.rszObs.observe(this.wrapperDivRef.current);
}
componentDidMount() {
this.checkRszObs();
}
componentWillUnmount() {
let { rendererContainer, lineId } = this.props;
rendererContainer.unloadRenderer(lineId);
if (this.rszObs != null) {
this.rszObs.disconnect();
this.rszObs = null;
}
}
componentDidUpdate() {
this.checkRszObs();
}
render() {
let { plugin } = this.props;
let model = this.model;
if (model.loadError.get() != null) {
let errorText = model.loadError.get();
let height = this.model.savedHeight;
return (
<div ref={this.wrapperDivRef} style={{ minHeight: height, fontSize: model.opts.termFontSize }}>
<div className="load-error-text">ERROR: {errorText}</div>
</div>
);
}
if (model.loading.get()) {
let height = this.model.savedHeight;
return (
<div
ref={this.wrapperDivRef}
className="renderer-loading"
style={{ minHeight: height, fontSize: model.opts.termFontSize }}
>
loading content <i className="fa fa-ellipsis fa-fade" />
</div>
);
}
let Comp = plugin.simpleComponent;
if (Comp == null) {
<div ref={this.wrapperDivRef}>(no component found in plugin)</div>;
}
let { festate, cmdstr, exitcode } = this.props.initParams.rawCmd;
return (
<div ref={this.wrapperDivRef} className="sr-wrapper">
<Comp
cwd={festate.cwd}
cmdstr={cmdstr}
exitcode={exitcode}
data={model.dataBlob as ExtBlob}
readOnly={model.readOnly}
notFound={model.notFound}
lineState={model.lineState}
context={model.context}
opts={model.opts}
savedHeight={model.savedHeight}
scrollToBringIntoViewport={this.props.scrollToBringIntoViewport}
isSelected={this.props.isSelected}
shouldFocus={this.props.shouldFocus}
rendererApi={model.api}
/>
</div>
);
}
}
export { SimpleBlobRendererModel, SimpleBlobRenderer };
+1 -6
View File
@@ -303,21 +303,16 @@ function shFrameNavHandler(event: Electron.Event<Electron.WebContentsWillFrameNa
// only use this handler to process iframe events (non-iframe events go to shNavHandler)
return;
}
event.preventDefault();
const url = event.url;
console.log(`frame-navigation url=${url} frame=${event.frame.name}`);
if (event.frame.name == "webview") {
// "webview" links always open in new window
// this will *not* effect the initial load because srcdoc does not count as an electron navigation
console.log("open external, frameNav", url);
event.preventDefault();
electron.shell.openExternal(url);
return;
}
if (event.frame.name == "pdfview" && url.startsWith("blob:file:///")) {
// allowed
return;
}
event.preventDefault();
console.log("frame navigation canceled");
}
+2 -5
View File
@@ -1583,15 +1583,12 @@ class Model {
return remote.remotecanonicalname;
}
readRemoteFile(screenId: string, lineId: string, path: string, mimetype?: string): Promise<ExtFile> {
const urlParams: Record<string, string> = {
readRemoteFile(screenId: string, lineId: string, path: string): Promise<ExtFile> {
const urlParams = {
screenid: screenId,
lineid: lineId,
path: path,
};
if (mimetype != null) {
urlParams["mimetype"] = mimetype;
}
const usp = new URLSearchParams(urlParams);
const url = new URL(this.getBaseHostPort() + "/api/read-file?" + usp.toString());
const fetchHeaders = this.getFetchHeaders();
+6
View File
@@ -1,4 +1,10 @@
.image-renderer {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding-top: var(--termpad);
img {
display: block;
}
-7
View File
@@ -1,7 +0,0 @@
.pdf-renderer {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding-top: var(--termpad);
}
-49
View File
@@ -1,49 +0,0 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as React from "react";
import * as mobx from "mobx";
import * as mobxReact from "mobx-react";
import "./pdf.less";
@mobxReact.observer
class SimplePdfRenderer extends React.Component<
{ data: ExtBlob; context: RendererContext; opts: RendererOpts; savedHeight: number },
{}
> {
objUrl: string = null;
componentWillUnmount() {
if (this.objUrl != null) {
URL.revokeObjectURL(this.objUrl);
}
}
render() {
let dataBlob = this.props.data;
if (dataBlob == null || dataBlob.notFound) {
return (
<div className="pdf-renderer" style={{ fontSize: this.props.opts.termFontSize }}>
<div className="load-error-text">
ERROR: file {dataBlob && dataBlob.name ? JSON.stringify(dataBlob.name) : ""} not found
</div>
</div>
);
}
if (this.objUrl == null) {
const pdfBlob = new File([dataBlob], "test.pdf", { type: "application/pdf" });
this.objUrl = URL.createObjectURL(pdfBlob);
}
const opts = this.props.opts;
const maxHeight = opts.maxSize.height - 10;
const maxWidth = opts.maxSize.width - 10;
return (
<div className="pdf-renderer">
<iframe src={this.objUrl} width={maxWidth} height={maxHeight} name="pdfview" />
</div>
);
}
}
export { SimplePdfRenderer };
-11
View File
@@ -7,7 +7,6 @@ import { SourceCodeRenderer } from "./code/code";
import { SimpleMustacheRenderer } from "./mustache/mustache";
import { CSVRenderer } from "./csv/csv";
import { OpenAIRenderer, OpenAIRendererModel } from "./openai/openai";
import { SimplePdfRenderer } from "./pdf/pdf";
import { isBlank } from "@/util/util";
import { sprintf } from "sprintf-js";
@@ -79,16 +78,6 @@ const PluginConfigs: RendererPluginType[] = [
mimeTypes: ["image/*"],
simpleComponent: SimpleImageRenderer,
},
{
name: "pdf",
rendererType: "simple",
heightType: "pixels",
dataType: "blob",
collapseType: "hide",
globalCss: null,
mimeTypes: ["application/pdf"],
simpleComponent: SimplePdfRenderer,
},
];
class PluginModelClass {
+3
View File
@@ -58,6 +58,9 @@ function getThemeFromCSSVars(): ITheme {
theme.brightCyan = rootStyle.getPropertyValue("--term-bright-cyan");
theme.brightWhite = rootStyle.getPropertyValue("--term-bright-white");
theme.selectionBackground = rootStyle.getPropertyValue("--term-selection-background");
theme.selectionInactiveBackground = rootStyle.getPropertyValue("--term-selection-background");
theme.cursor = rootStyle.getPropertyValue("--term-selection-background");
theme.cursorAccent = rootStyle.getPropertyValue("--term-cursor-accent");
return theme;
}
+21
View File
@@ -5,6 +5,7 @@ let isJetBrainsMonoLoaded = false;
let isLatoFontLoaded = false;
let isHackFontLoaded = false;
let isBaseFontsLoaded = false;
let isFiraCodeLoaded = false;
function addToFontFaceSet(fontFaceSet: FontFaceSet, fontFace: FontFace) {
// any cast to work around typing issue
@@ -55,6 +56,25 @@ function loadLatoFont() {
latoFontBold.load();
}
function loadFiraCodeFont() {
if (isFiraCodeLoaded) {
return;
}
isFiraCodeLoaded = true;
let firaCodeRegular = new FontFace("Fira Code", "url('public/fonts/firacode-regular.woff2')", {
style: "normal",
weight: "400",
});
let firaCodeBold = new FontFace("Fira Code", "url('public/fonts/firacode-bold.woff2')", {
style: "normal",
weight: "700",
});
addToFontFaceSet(document.fonts, firaCodeRegular);
addToFontFaceSet(document.fonts, firaCodeBold);
firaCodeRegular.load();
firaCodeBold.load();
}
function loadHackFont() {
if (isHackFontLoaded) {
return;
@@ -104,6 +124,7 @@ function loadFonts() {
loadLatoFont();
loadJetBrainsMonoFont();
loadHackFont();
loadFiraCodeFont();
}
export { loadFonts };
-32
View File
@@ -276,7 +276,6 @@ func init() {
registerCmdFn("imageview", ImageViewCommand)
registerCmdFn("mdview", MarkdownViewCommand)
registerCmdFn("markdownview", MarkdownViewCommand)
registerCmdFn("pdfview", PdfViewCommand)
registerCmdFn("csvview", CSVViewCommand)
}
@@ -4839,37 +4838,6 @@ func ImageViewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sc
return update, nil
}
func PdfViewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.UpdatePacket, error) {
if len(pk.Args) == 0 {
return nil, fmt.Errorf("%s requires an argument (file name)", GetCmdStr(pk))
}
// TODO more error checking on filename format?
if pk.Args[0] == "" {
return nil, fmt.Errorf("%s argument cannot be empty", GetCmdStr(pk))
}
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_RemoteConnected)
if err != nil {
return nil, err
}
outputStr := fmt.Sprintf("%s %q", GetCmdStr(pk), pk.Args[0])
cmd, err := makeStaticCmd(ctx, GetCmdStr(pk), ids, pk.GetRawStr(), []byte(outputStr))
if err != nil {
// TODO tricky error since the command was a success, but we can't show the output
return nil, err
}
// set the line state
lineState := make(map[string]any)
lineState[sstore.LineState_Source] = "file"
lineState[sstore.LineState_File] = pk.Args[0]
update, err := addLineForCmd(ctx, "/"+GetCmdStr(pk), false, ids, cmd, "pdf", lineState)
if err != nil {
// TODO tricky error since the command was a success, but we can't show the output
return nil, err
}
update.AddUpdate(sstore.InteractiveUpdate(pk.Interactive))
return update, nil
}
func MarkdownViewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.UpdatePacket, error) {
if len(pk.Args) == 0 {
return nil, fmt.Errorf("%s requires an argument (file name)", GetCmdStr(pk))
-1
View File
@@ -35,7 +35,6 @@ var BareMetaCmds = []BareMetaCmdDecl{
{"markdownview", "markdownview"},
{"mdview", "markdownview"},
{"csvview", "csvview"},
{"pdfview", "pdfview"},
}
const (
+3 -1
View File
@@ -393,7 +393,9 @@ func determineLang() string {
log.Printf("error executing 'defaults read -g AppleLocale': %v\n", err)
return ""
}
return strings.TrimSpace(string(out)) + ".UTF-8"
strOut := string(out)
truncOut := strings.Split(strOut, "@")[0]
return strings.TrimSpace(truncOut) + ".UTF-8"
} else {
// this is specifically to get the wavesrv LANG so waveshell
// on a remote uses the same LANG