diff --git a/src/app/appconst.ts b/src/app/appconst.ts
index 742331801..d8b05e742 100644
--- a/src/app/appconst.ts
+++ b/src/app/appconst.ts
@@ -18,7 +18,8 @@ export const ConfirmKey_HideShellPrompt = "hideshellprompt";
export const NoStrPos = -1;
-export const RemotePtyRows = 8; // also in main.tsx
+export const RemotePtyRows = 8;
+export const RemotePtyTotalRows = 25;
export const RemotePtyCols = 80;
export const ProdServerEndpoint = "http://127.0.0.1:1619";
export const ProdServerWsEndpoint = "ws://127.0.0.1:1623";
diff --git a/src/app/common/modals/viewremoteconndetail.tsx b/src/app/common/modals/viewremoteconndetail.tsx
index 48536c603..cc93e122c 100644
--- a/src/app/common/modals/viewremoteconndetail.tsx
+++ b/src/app/common/modals/viewremoteconndetail.tsx
@@ -370,7 +370,11 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
ref={this.termRef}
data-remoteid={remote.remoteid}
style={{
- height: textmeasure.termHeightFromRows(appconst.RemotePtyRows, termFontSize),
+ height: textmeasure.termHeightFromRows(
+ appconst.RemotePtyRows,
+ termFontSize,
+ appconst.RemotePtyTotalRows
+ ),
width: termWidth,
}}
>
diff --git a/src/app/line/linecomps.tsx b/src/app/line/linecomps.tsx
index 306b03588..1b75962b6 100644
--- a/src/app/line/linecomps.tsx
+++ b/src/app/line/linecomps.tsx
@@ -388,7 +388,7 @@ class LineCmd extends React.Component<
let height = 45 + 24; // height of zero height terminal
const usedRows = screen.getUsedRows(lineutil.getRendererContext(line), line, cmd, width);
if (usedRows > 0) {
- height = 48 + 24 + termHeightFromRows(usedRows, GlobalModel.termFontSize.get());
+ height = 48 + 24 + termHeightFromRows(usedRows, GlobalModel.termFontSize.get(), cmd.getTermMaxRows());
}
return height;
}
diff --git a/src/app/line/lines.less b/src/app/line/lines.less
index 19c5bc62a..6291d49e7 100644
--- a/src/app/line/lines.less
+++ b/src/app/line/lines.less
@@ -464,6 +464,7 @@
padding: 0 0 10px 0;
flex-grow: 1;
position: relative;
+ overflow-x: hidden;
&::-webkit-scrollbar-thumb {
background-color: transparent !important;
diff --git a/src/app/magiclayout.ts b/src/app/magiclayout.ts
index 4b2bb70f4..76d73dc92 100644
--- a/src/app/magiclayout.ts
+++ b/src/app/magiclayout.ts
@@ -18,8 +18,6 @@ let MagicLayout = {
ScreenMinContentSize: 100,
ScreenMaxContentSize: 5000,
- // the 3 is for descenders, which get cut off in the terminal without this
- TermDescendersHeight: 3,
TermWidthBuffer: 15,
TabWidth: 154,
diff --git a/src/app/workspace/screen/screenview.tsx b/src/app/workspace/screen/screenview.tsx
index 9e88fb2f2..f37dce701 100644
--- a/src/app/workspace/screen/screenview.tsx
+++ b/src/app/workspace/screen/screenview.tsx
@@ -109,6 +109,7 @@ class ScreenView extends React.Component<{ session: Session; screen: Screen }, {
return
;
}
let fontSize = GlobalModel.termFontSize.get();
+ let dprStr = sprintf("%0.3f", GlobalModel.devicePixelRatio.get());
let viewOpts = screen.viewOpts.get();
let hasSidebar = viewOpts?.sidebar?.open;
let winWidth = "100%";
@@ -145,7 +146,7 @@ class ScreenView extends React.Component<{ session: Session; screen: Screen }, {
return (
{
+ if (MainWindow == null) {
+ return;
+ }
+ MainWindow.webContents.setZoomFactor(1);
+ MainWindow.webContents.send("zoom-changed");
+ },
+ },
+ {
+ label: "Zoom In",
+ accelerator: cmdOrAlt + "+Plus",
+ click: () => {
+ if (MainWindow == null) {
+ return;
+ }
+ const zoomFactor = MainWindow.webContents.getZoomFactor();
+ MainWindow.webContents.setZoomFactor(zoomFactor * 1.1);
+ MainWindow.webContents.send("zoom-changed");
+ },
+ },
+ {
+ label: "Zoom Out",
+ accelerator: cmdOrAlt + "+-",
+ click: () => {
+ if (MainWindow == null) {
+ return;
+ }
+ const zoomFactor = MainWindow.webContents.getZoomFactor();
+ MainWindow.webContents.setZoomFactor(zoomFactor / 1.1);
+ MainWindow.webContents.send("zoom-changed");
+ },
+ },
{ type: "separator" },
{ role: "togglefullscreen" },
],
@@ -375,6 +408,9 @@ function createMainWindow(clientData: ClientDataType | null) {
win.on("close", () => {
MainWindow = null;
});
+ win.webContents.on("zoom-changed", (e) => {
+ win.webContents.send("zoom-changed");
+ });
win.webContents.setWindowOpenHandler(({ url, frameName }) => {
if (url.startsWith("https://docs.waveterm.dev/")) {
console.log("openExternal docs", url);
diff --git a/src/electron/preload.js b/src/electron/preload.js
index e7be2ec57..cd807c652 100644
--- a/src/electron/preload.js
+++ b/src/electron/preload.js
@@ -21,6 +21,7 @@ contextBridge.exposeInMainWorld("api", {
onWCmd: (callback) => ipcRenderer.on("w-cmd", callback),
onPCmd: (callback) => ipcRenderer.on("p-cmd", callback),
onRCmd: (callback) => ipcRenderer.on("r-cmd", callback),
+ onZoomChanged: (callback) => ipcRenderer.on("zoom-changed", callback),
onMetaArrowUp: (callback) => ipcRenderer.on("meta-arrowup", callback),
onMetaArrowDown: (callback) => ipcRenderer.on("meta-arrowdown", callback),
onMetaPageUp: (callback) => ipcRenderer.on("meta-pageup", callback),
diff --git a/src/index.ts b/src/index.ts
index 87ebfcf2c..2a2c699b5 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -8,6 +8,7 @@ import { sprintf } from "sprintf-js";
import { App } from "@/app/app";
import * as DOMPurify from "dompurify";
import { loadFonts } from "./util/util";
+import * as textmeasure from "./util/textmeasure";
// @ts-ignore
let VERSION = __WAVETERM_VERSION__;
@@ -28,5 +29,6 @@ document.addEventListener("DOMContentLoaded", () => {
(window as any).mobx = mobx;
(window as any).sprintf = sprintf;
(window as any).DOMPurify = DOMPurify;
+(window as any).textmeasure = textmeasure;
console.log("WaveTerm", VERSION, BUILD);
diff --git a/src/models/cmd.ts b/src/models/cmd.ts
index a55eb0749..d2ab3aa85 100644
--- a/src/models/cmd.ts
+++ b/src/models/cmd.ts
@@ -88,6 +88,11 @@ class Cmd {
return this.data.get().termopts;
}
+ getTermMaxRows(): number {
+ let termOpts = this.getTermOpts();
+ return termOpts?.rows;
+ }
+
getCmdStr(): string {
return this.data.get().cmdstr;
}
diff --git a/src/models/historyview.ts b/src/models/historyview.ts
index 44c0649a0..b309ac9ed 100644
--- a/src/models/historyview.ts
+++ b/src/models/historyview.ts
@@ -115,7 +115,7 @@ class HistoryViewModel {
} else {
this.activeItem.set(hitem.historyid);
let width = termWidthFromCols(80, this.globalModel.termFontSize.get());
- let height = termHeightFromRows(25, this.globalModel.termFontSize.get());
+ let height = termHeightFromRows(25, this.globalModel.termFontSize.get(), 25);
this.specialLineContainer = new SpecialLineContainer(
this,
{ width, height },
@@ -149,7 +149,7 @@ class HistoryViewModel {
}
_deleteSelected(): void {
- let lineIds = Array.from(this.selectedItems.keys());
+ let lineIds: string[] = Array.from(this.selectedItems.keys());
let prtn = GlobalCommandRunner.historyPurgeLines(lineIds);
prtn.then((result: CommandRtnType) => {
if (!result.success) {
diff --git a/src/models/model.ts b/src/models/model.ts
index d3f5841d9..f2f9dc980 100644
--- a/src/models/model.ts
+++ b/src/models/model.ts
@@ -32,13 +32,8 @@ import { MainSidebarModel } from "./mainsidebar";
import { Screen } from "./screen";
import { Cmd } from "./cmd";
import { GlobalCommandRunner } from "./global";
-
-type KeyModsType = {
- meta?: boolean;
- ctrl?: boolean;
- alt?: boolean;
- shift?: boolean;
-};
+import { clearMonoFontCache } from "@/util/textmeasure";
+import type { TermWrap } from "@/plugins/terminal/term";
type SWLinePtr = {
line: LineType;
@@ -46,36 +41,6 @@ type SWLinePtr = {
screen: Screen;
};
-type ElectronApi = {
- getId: () => string;
- getIsDev: () => boolean;
- getPlatform: () => string;
- getAuthKey: () => string;
- getWaveSrvStatus: () => boolean;
- restartWaveSrv: () => boolean;
- reloadWindow: () => void;
- openExternalLink: (url: string) => void;
- reregisterGlobalShortcut: (shortcut: string) => void;
- onTCmd: (callback: (mods: KeyModsType) => void) => void;
- onICmd: (callback: (mods: KeyModsType) => void) => void;
- onLCmd: (callback: (mods: KeyModsType) => void) => void;
- onHCmd: (callback: (mods: KeyModsType) => void) => void;
- onPCmd: (callback: (mods: KeyModsType) => void) => void;
- onRCmd: (callback: (mods: KeyModsType) => void) => void;
- onWCmd: (callback: (mods: KeyModsType) => void) => void;
- onMenuItemAbout: (callback: () => void) => void;
- onMetaArrowUp: (callback: () => void) => void;
- onMetaArrowDown: (callback: () => void) => void;
- onMetaPageUp: (callback: () => void) => void;
- onMetaPageDown: (callback: () => void) => void;
- onBracketCmd: (callback: (event: any, arg: { relative: number }, mods: KeyModsType) => void) => void;
- onDigitCmd: (callback: (event: any, arg: { digit: number }, mods: KeyModsType) => void) => void;
- contextScreen: (screenOpts: { screenId: string }, position: { x: number; y: number }) => void;
- contextEditMenu: (position: { x: number; y: number }, opts: ContextMenuOpts) => void;
- onWaveSrvStatusChange: (callback: (status: boolean, pid: number) => void) => void;
- getLastLogs: (numOfLines: number, callback: (logs: any) => void) => void;
-};
-
function getApi(): ElectronApi {
return (window as any).api;
}
@@ -133,7 +98,10 @@ class Model {
});
lineSettingsModal: OV = mobx.observable.box(null, {
name: "lineSettingsModal",
- }); // linenum
+ });
+ devicePixelRatio: OV = mobx.observable.box(window.devicePixelRatio, {
+ name: "devicePixelRatio",
+ });
remotesModel: RemotesModel;
inputModel: InputModel;
@@ -196,6 +164,7 @@ class Model {
getApi().onPCmd(this.onPCmd.bind(this));
getApi().onWCmd(this.onWCmd.bind(this));
getApi().onRCmd(this.onRCmd.bind(this));
+ getApi().onZoomChanged(this.onZoomChanged.bind(this));
getApi().onMenuItemAbout(this.onMenuItemAbout.bind(this));
getApi().onMetaArrowUp(this.onMetaArrowUp.bind(this));
getApi().onMetaArrowDown(this.onMetaArrowDown.bind(this));
@@ -514,6 +483,30 @@ class Model {
}
}
+ onZoomChanged(): void {
+ mobx.action(() => {
+ this.devicePixelRatio.set(window.devicePixelRatio);
+ clearMonoFontCache();
+ })();
+ }
+
+ // for debuggin
+ getSelectedTermWrap(): TermWrap {
+ let screen = this.getActiveScreen();
+ if (screen == null) {
+ return null;
+ }
+ let lineNum = screen.selectedLine.get();
+ if (lineNum == null) {
+ return null;
+ }
+ let line = screen.getLineByNum(lineNum);
+ if (line == null) {
+ return null;
+ }
+ return screen.getTermWrap(line.lineid);
+ }
+
clearModals(): boolean {
let didSomething = false;
mobx.action(() => {
diff --git a/src/models/model_old.ts-deprecated b/src/models/model_old.ts-deprecated
index 5368a8937..d0cc731bb 100644
--- a/src/models/model_old.ts-deprecated
+++ b/src/models/model_old.ts-deprecated
@@ -205,6 +205,7 @@ type ElectronApi = {
onPCmd: (callback: (mods: KeyModsType) => void) => void;
onRCmd: (callback: (mods: KeyModsType) => void) => void;
onWCmd: (callback: (mods: KeyModsType) => void) => void;
+ onZoomChanged: (callback: () => void) => void;
onMenuItemAbout: (callback: () => void) => void;
onMetaArrowUp: (callback: () => void) => void;
onMetaArrowDown: (callback: () => void) => void;
@@ -308,6 +309,11 @@ class Cmd {
return this.data.get().termopts;
}
+ getTermMaxRows(): number {
+ let termOpts = this.getTermOpts();
+ return termOpts?.rows;
+ }
+
getCmdStr(): string {
return this.data.get().cmdstr;
}
@@ -743,7 +749,7 @@ class Screen {
getMaxContentSize(): WindowSize {
if (this.lastScreenSize == null) {
let width = termWidthFromCols(80, GlobalModel.termFontSize.get());
- let height = termHeightFromRows(25, GlobalModel.termFontSize.get());
+ let height = termHeightFromRows(25, GlobalModel.termFontSize.get(), 25);
return { width, height };
}
let winSize = this.lastScreenSize;
@@ -757,7 +763,7 @@ class Screen {
getIdealContentSize(): WindowSize {
if (this.lastScreenSize == null) {
let width = termWidthFromCols(80, GlobalModel.termFontSize.get());
- let height = termHeightFromRows(25, GlobalModel.termFontSize.get());
+ let height = termHeightFromRows(25, GlobalModel.termFontSize.get(), 25);
return { width, height };
}
let winSize = this.lastScreenSize;
@@ -2396,7 +2402,7 @@ class HistoryViewModel {
} else {
this.activeItem.set(hitem.historyid);
let width = termWidthFromCols(80, GlobalModel.termFontSize.get());
- let height = termHeightFromRows(25, GlobalModel.termFontSize.get());
+ let height = termHeightFromRows(25, GlobalModel.termFontSize.get(), 25);
this.specialLineContainer = new SpecialLineContainer(
this,
{ width, height },
@@ -3451,6 +3457,9 @@ class Model {
lineSettingsModal: OV = mobx.observable.box(null, {
name: "lineSettingsModal",
}); // linenum
+ devicePixelRatio: OV = mobx.observable.box(window.devicePixelRatio, {
+ name: "devicePixelRatio",
+ });
remotesModalModel: RemotesModalModel;
remotesModel: RemotesModel;
@@ -3515,6 +3524,7 @@ class Model {
getApi().onPCmd(this.onPCmd.bind(this));
getApi().onWCmd(this.onWCmd.bind(this));
getApi().onRCmd(this.onRCmd.bind(this));
+ getApi().onZoomChanged(this.onZoomChanged.bind(this));
getApi().onMenuItemAbout(this.onMenuItemAbout.bind(this));
getApi().onMetaArrowUp(this.onMetaArrowUp.bind(this));
getApi().onMetaArrowDown(this.onMetaArrowDown.bind(this));
@@ -3826,6 +3836,29 @@ class Model {
}
}
+ onZoomChanged(): void {
+ mobx.action(() => {
+ this.devicePixelRatio.set(window.devicePixelRatio);
+ clearMonoFontCache();
+ })();
+ }
+
+ getSelectedTermWrap(): TermWrap {
+ let screen = this.getActiveScreen();
+ if (screen == null) {
+ return null;
+ }
+ let lineNum = screen.selectedLine.get();
+ if (lineNum == null) {
+ return null;
+ }
+ let line = screen.getLineByNum(lineNum);
+ if (line == null) {
+ return null;
+ }
+ return screen.getTermWrap(line.lineid);
+ }
+
clearModals(): boolean {
let didSomething = false;
mobx.action(() => {
diff --git a/src/models/screen.ts b/src/models/screen.ts
index 9b0366711..624554711 100644
--- a/src/models/screen.ts
+++ b/src/models/screen.ts
@@ -410,7 +410,7 @@ class Screen {
getMaxContentSize(): WindowSize {
if (this.lastScreenSize == null) {
let width = termWidthFromCols(80, this.globalModel.termFontSize.get());
- let height = termHeightFromRows(25, this.globalModel.termFontSize.get());
+ let height = termHeightFromRows(25, this.globalModel.termFontSize.get(), 25);
return { width, height };
}
let winSize = this.lastScreenSize;
@@ -424,7 +424,7 @@ class Screen {
getIdealContentSize(): WindowSize {
if (this.lastScreenSize == null) {
let width = termWidthFromCols(80, this.globalModel.termFontSize.get());
- let height = termHeightFromRows(25, this.globalModel.termFontSize.get());
+ let height = termHeightFromRows(25, this.globalModel.termFontSize.get(), 25);
return { width, height };
}
let winSize = this.lastScreenSize;
diff --git a/src/plugins/terminal/terminal.tsx b/src/plugins/terminal/terminal.tsx
index 261056265..31bd33f1e 100644
--- a/src/plugins/terminal/terminal.tsx
+++ b/src/plugins/terminal/terminal.tsx
@@ -148,7 +148,7 @@ class TerminalRenderer extends React.Component<
.get();
let cmd = screen.getCmd(line); // will not be null
let usedRows = screen.getUsedRows(lineutil.getRendererContext(line), line, cmd, width);
- let termHeight = termHeightFromRows(usedRows, GlobalModel.termFontSize.get());
+ let termHeight = termHeightFromRows(usedRows, GlobalModel.termFontSize.get(), cmd.getTermMaxRows());
if (usedRows === 0) {
termHeight = 0;
}
@@ -164,6 +164,7 @@ class TerminalRenderer extends React.Component<
{ "zero-height": termHeight == 0 },
{ collapsed: collapsed }
)}
+ data-usedrows={usedRows}
>
diff --git a/src/types/custom.d.ts b/src/types/custom.d.ts
index 500746c2a..f93f76423 100644
--- a/src/types/custom.d.ts
+++ b/src/types/custom.d.ts
@@ -833,6 +833,44 @@ declare global {
isLineIdInSidebar(lineId: string): boolean;
getContainerType(): LineContainerStrs;
};
+
+ type KeyModsType = {
+ meta?: boolean;
+ ctrl?: boolean;
+ alt?: boolean;
+ shift?: boolean;
+ };
+
+ type ElectronApi = {
+ getId: () => string;
+ getIsDev: () => boolean;
+ getPlatform: () => string;
+ getAuthKey: () => string;
+ getWaveSrvStatus: () => boolean;
+ restartWaveSrv: () => boolean;
+ reloadWindow: () => void;
+ openExternalLink: (url: string) => void;
+ reregisterGlobalShortcut: (shortcut: string) => void;
+ onTCmd: (callback: (mods: KeyModsType) => void) => void;
+ onICmd: (callback: (mods: KeyModsType) => void) => void;
+ onLCmd: (callback: (mods: KeyModsType) => void) => void;
+ onHCmd: (callback: (mods: KeyModsType) => void) => void;
+ onPCmd: (callback: (mods: KeyModsType) => void) => void;
+ onRCmd: (callback: (mods: KeyModsType) => void) => void;
+ onWCmd: (callback: (mods: KeyModsType) => void) => void;
+ onZoomChanged: (callback: () => void) => void;
+ onMenuItemAbout: (callback: () => void) => void;
+ onMetaArrowUp: (callback: () => void) => void;
+ onMetaArrowDown: (callback: () => void) => void;
+ onMetaPageUp: (callback: () => void) => void;
+ onMetaPageDown: (callback: () => void) => void;
+ onBracketCmd: (callback: (event: any, arg: { relative: number }, mods: KeyModsType) => void) => void;
+ onDigitCmd: (callback: (event: any, arg: { digit: number }, mods: KeyModsType) => void) => void;
+ contextScreen: (screenOpts: { screenId: string }, position: { x: number; y: number }) => void;
+ contextEditMenu: (position: { x: number; y: number }, opts: ContextMenuOpts) => void;
+ onWaveSrvStatusChange: (callback: (status: boolean, pid: number) => void) => void;
+ getLastLogs: (numOfLines: number, callback: (logs: any) => void) => void;
+ };
}
export {};
diff --git a/src/util/textmeasure.ts b/src/util/textmeasure.ts
index 9551e00bd..5f3f5ca91 100644
--- a/src/util/textmeasure.ts
+++ b/src/util/textmeasure.ts
@@ -30,6 +30,10 @@ function getMonoFontSize(fontSize: number): { height: number; width: number } {
return size;
}
+function clearMonoFontCache(): void {
+ MonoFontSizes = [];
+}
+
function measureText(
text: string,
textOpts?: { pre?: boolean; mono?: boolean; fontSize?: number | string }
@@ -57,8 +61,9 @@ function measureText(
throw new Error("cannot measure text, no #measure div");
}
measureDiv.replaceChildren(textElem);
- let rect = textElem.getBoundingClientRect();
- return { width: rect.width, height: Math.ceil(rect.height) };
+ let height = textElem.offsetHeight;
+ let width = textElem.offsetWidth;
+ return { width: width, height: Math.ceil(height) };
}
function windowWidthToCols(width: number, fontSize: number): number {
@@ -82,10 +87,27 @@ function termWidthFromCols(cols: number, fontSize: number): number {
return Math.ceil(dr.width * cols) + MagicLayout.TermWidthBuffer;
}
-function termHeightFromRows(rows: number, fontSize: number): number {
+// we need to match the xtermjs calculation in CharSizeService.ts and DomRenderer.ts
+// it does some crazy rounding depending on the value of window.devicePixelRatio
+// works out to `realHeight = round(ceil(height * dpr) * rows / dpr) / rows`
+// their calculation is based off the "totalRows" (so that argument has been added)
+function termHeightFromRows(rows: number, fontSize: number, totalRows: number): number {
let dr = getMonoFontSize(fontSize);
- // TODO: replace the TermDescendersHeight with some calculation based on termFontSize.
- return Math.ceil(dr.height * rows) + MagicLayout.TermDescendersHeight;
+ const dpr = window.devicePixelRatio;
+ if (totalRows == null || totalRows == 0) {
+ totalRows = rows > 25 ? rows : 25;
+ }
+ let realHeight = Math.round((Math.ceil(dr.height * dpr) * totalRows) / dpr) / totalRows;
+ return Math.ceil(realHeight * rows);
}
-export { measureText, getMonoFontSize, windowWidthToCols, windowHeightToRows, termWidthFromCols, termHeightFromRows };
+export {
+ measureText,
+ getMonoFontSize,
+ windowWidthToCols,
+ windowHeightToRows,
+ termWidthFromCols,
+ termHeightFromRows,
+ clearMonoFontCache,
+ MonoFontSizes,
+};