Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ updates:
update-types: ["version-update:semver-minor"]
- dependency-name: "@better-auth/*"
update-types: ["version-update:semver-minor"]
# Pinned in the root overrides so better-auth and better-call share one copy;
# a bump of the direct dependency alone does nothing.
- dependency-name: "@better-auth/utils"
- dependency-name: "stripe"
- dependency-name: "@stepperize/react"
- dependency-name: "@dokploy/trpc-openapi"
Expand Down
4 changes: 2 additions & 2 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
"dependencies": {
"@dokploy/server": "workspace:*",
"@hono/node-server": "^1.19.17",
"@hono/zod-validator": "0.7.6",
"@hono/zod-validator": "0.9.1",
"dotenv": "^18.0.5",
"hono": "^4.13.12",
"hono": "^4.13.13",
"inngest": "4.21.1",
"pino": "10.4.0",
"pino-pretty": "13.1.3",
Expand Down
88 changes: 88 additions & 0 deletions apps/dokploy/__test__/lib/terminal-keyboard.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import type { Terminal } from "@xterm/xterm";
import { afterEach, describe, expect, it, vi } from "vitest";
import { attachTerminalKeyHandlers } from "@/lib/terminal-keyboard";

const setup = (platform: string) => {
vi.stubGlobal("navigator", { platform });
let handler: ((event: KeyboardEvent) => boolean) | undefined;
const input = vi.fn();
const term = {
attachCustomKeyEventHandler: (fn: (event: KeyboardEvent) => boolean) => {
handler = fn;
},
input,
} as unknown as Terminal;
attachTerminalKeyHandlers(term);
const press = (
key: string,
modifiers: Partial<
Pick<KeyboardEvent, "altKey" | "ctrlKey" | "metaKey" | "shiftKey">
> = {},
type = "keydown",
) => {
const preventDefault = vi.fn();
const result = handler?.({
type,
key,
altKey: false,
ctrlKey: false,
metaKey: false,
shiftKey: false,
...modifiers,
preventDefault,
} as unknown as KeyboardEvent);
return { result, preventDefault };
};
return { press, input };
};

afterEach(() => {
vi.unstubAllGlobals();
});

describe("attachTerminalKeyHandlers", () => {
it.each([
["ArrowUp", "\x1b[1;5A"],
["ArrowDown", "\x1b[1;5B"],
["ArrowRight", "\x1b[1;5C"],
["ArrowLeft", "\x1b[1;5D"],
])("sends Ctrl+%s for Alt+%s on every platform", (key, sequence) => {
for (const platform of ["MacIntel", "Win32", "Linux x86_64"]) {
const { press, input } = setup(platform);
const { result, preventDefault } = press(key, { altKey: true });
expect(result).toBe(false);
expect(preventDefault).toHaveBeenCalled();
expect(input).toHaveBeenCalledWith(sequence);
}
});

it.each([{ shiftKey: true }, { ctrlKey: true }, { metaKey: true }])(
"leaves Alt+Arrow with %o to xterm",
(extra) => {
const { press, input } = setup("Linux x86_64");
expect(press("ArrowLeft", { altKey: true, ...extra }).result).toBe(true);
expect(input).not.toHaveBeenCalled();
},
);

it("leaves plain arrows and keyup events to xterm", () => {
const { press, input } = setup("MacIntel");
expect(press("ArrowLeft").result).toBe(true);
expect(press("ArrowLeft", { altKey: true }, "keyup").result).toBe(true);
expect(input).not.toHaveBeenCalled();
});

it("types Option-composed characters on macOS", () => {
const { press, input } = setup("MacIntel");
const { result, preventDefault } = press("@", { altKey: true });
expect(result).toBe(false);
expect(preventDefault).toHaveBeenCalled();
expect(input).toHaveBeenCalledWith("@");
});

it("keeps Alt+letter for xterm outside macOS", () => {
const { press, input } = setup("Win32");
expect(press("l", { altKey: true }).result).toBe(true);
expect(input).not.toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { AttachAddon } from "@xterm/addon-attach";
import { ClipboardAddon } from "@xterm/addon-clipboard";
import { useTheme } from "next-themes";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { fixMacOsAltKeys } from "@/lib/terminal-keyboard";
import { attachTerminalKeyHandlers } from "@/lib/terminal-keyboard";

interface Props {
id: string;
Expand Down Expand Up @@ -63,7 +63,7 @@ export const DockerTerminal: React.FC<Props> = ({
const addonFit = new FitAddon();
const clipboardAddon = new ClipboardAddon();
term.loadAddon(clipboardAddon);
fixMacOsAltKeys(term);
attachTerminalKeyHandlers(term);
// @ts-expect-error
term.open(termRef.current);
term.loadAddon(addonFit);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import "@xterm/xterm/css/xterm.css";
import { AttachAddon } from "@xterm/addon-attach";
import { ClipboardAddon } from "@xterm/addon-clipboard";
import { useTheme } from "next-themes";
import { fixMacOsAltKeys } from "@/lib/terminal-keyboard";
import { attachTerminalKeyHandlers } from "@/lib/terminal-keyboard";
import { getLocalServerData } from "./local-server-config";

interface Props {
Expand Down Expand Up @@ -43,7 +43,7 @@ export const Terminal: React.FC<Props> = ({ id, serverId }) => {
const addonFit = new FitAddon();
const clipboardAddon = new ClipboardAddon();
term.loadAddon(clipboardAddon);
fixMacOsAltKeys(term);
attachTerminalKeyHandlers(term);

// @ts-expect-error
term.open(termRef.current);
Expand Down
38 changes: 29 additions & 9 deletions apps/dokploy/lib/terminal-keyboard.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,41 @@
import type { Terminal } from "@xterm/xterm";

const ALT_ARROW_AS_CTRL_ARROW: Record<string, string> = {
ArrowUp: "\x1b[1;5A",
ArrowDown: "\x1b[1;5B",
ArrowRight: "\x1b[1;5C",
ArrowLeft: "\x1b[1;5D",
};

// xterm's platform detection mistakes the bundled Next.js `process` polyfill
// for Node.js, so it never treats Option/Alt as third-level shift on macOS and
// swallows composed characters like Option+L (@ on German layouts).
// https://github.com/Dokploy/dokploy/issues/4297
export const fixMacOsAltKeys = (term: Terminal) => {
if (!/Mac/.test(navigator.platform)) {
return;
}
//
// xterm 6 also dropped its Alt+Arrow -> Ctrl+Arrow rewrite (xtermjs/xterm.js#5346),
// which shells bind to word jumps. Because of the same detection bug, xterm 5
// always took its non-macOS branch here, so these are the sequences it sent.
// xterm keeps a single custom key handler, so both fixes share it.
export const attachTerminalKeyHandlers = (term: Terminal) => {
const isMac = /Mac/.test(navigator.platform);
term.attachCustomKeyEventHandler((event) => {
if (
event.type === "keydown" &&
event.altKey &&
!event.ctrlKey &&
!event.metaKey &&
event.key.length === 1
event.type !== "keydown" ||
!event.altKey ||
event.ctrlKey ||
event.metaKey
) {
return true;
}
const ctrlArrow = event.shiftKey
? undefined
: ALT_ARROW_AS_CTRL_ARROW[event.key];
if (ctrlArrow) {
event.preventDefault();
term.input(ctrlArrow);
return false;
}
if (isMac && event.key.length === 1) {
event.preventDefault();
term.input(event.key);
return false;
Expand Down
14 changes: 7 additions & 7 deletions apps/dokploy/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,10 @@
"@trpc/server": "^11.19.0",
"@uiw/codemirror-theme-github": "^4.25.12",
"@uiw/react-codemirror": "^4.25.12",
"@xterm/addon-attach": "0.10.0",
"@xterm/addon-clipboard": "0.1.0",
"@xterm/addon-fit": "0.10.0",
"@xterm/xterm": "^5.5.0",
"@xterm/addon-attach": "0.12.0",
"@xterm/addon-clipboard": "0.2.0",
"@xterm/addon-fit": "0.11.0",
"@xterm/xterm": "^6.0.0",
"adm-zip": "^0.6.1",
"ai": "^7.0.127",
"ai-sdk-ollama": "^4.4.0",
Expand All @@ -111,12 +111,12 @@
"micromatch": "4.0.8",
"nanoid": "3.3.19",
"next": "16.3.8",
"next-themes": "^0.2.1",
"next-themes": "^0.4.6",
"nextjs-toploader": "^3.9.17",
"node-os-utils": "2.0.5",
"node-pty": "1.1.0",
"node-schedule": "2.1.1",
"nodemailer": "10.0.13",
"nodemailer": "10.0.14",
"octokit": "3.2.2",
"pino": "10.4.0",
"pino-pretty": "13.1.3",
Expand Down Expand Up @@ -174,7 +174,7 @@
"esbuild": "0.28.2",
"lint-staged": "^15.5.2",
"memfs": "^4.80.0",
"simple-icons": "^16.33.0",
"simple-icons": "^16.34.0",
"tailwindcss": "^4.3.3",
"tsx": "^4.23.15",
"typescript": "^7.0.2",
Expand Down
3 changes: 3 additions & 0 deletions apps/dokploy/styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,9 @@ body[data-scroll-locked] [data-slot="sidebar-inset"] {

.xterm .xterm-viewport {
overflow-y: auto !important;
/* xterm 6 paints the theme background on its new scroll container and
leaves this element on xterm.css's #000, which hid our transparent theme. */
background-color: transparent !important;
}

.xterm .xterm-screen {
Expand Down
4 changes: 2 additions & 2 deletions apps/schedules/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@
"dependencies": {
"@dokploy/server": "workspace:*",
"@hono/node-server": "^1.19.17",
"@hono/zod-validator": "0.7.6",
"@hono/zod-validator": "0.9.1",
"bullmq": "5.81.5",
"dotenv": "^18.0.5",
"drizzle-orm": "0.45.3",
"hono": "^4.13.12",
"hono": "^4.13.13",
"ioredis": "5.11.1",
"pino": "10.4.0",
"pino-pretty": "13.1.3",
Expand Down
Loading
Loading