diff --git a/agent/agent_test.go b/agent/agent_test.go index e21806f845..410b0941f9 100644 --- a/agent/agent_test.go +++ b/agent/agent_test.go @@ -2229,12 +2229,25 @@ func TestAgent_ReconnectingPTY(t *testing.T) { return strings.Contains(line, "exit") || strings.Contains(line, "logout") } - // Wait for the prompt before writing commands. If the command arrives before the prompt is written, screen - // will sometimes put the command output on the same line as the command and the test will flake + // Wait for the prompt before writing commands. If the command + // arrives before the prompt is written, screen will sometimes put + // the command output on the same line as the command and the test + // will flake. require.NoError(t, tr1.ReadUntil(ctx, matchPrompt), "find prompt") require.NoError(t, tr2.ReadUntil(ctx, matchPrompt), "find prompt") data, err := json.Marshal(workspacesdk.ReconnectingPTYRequest{ + Data: "printf '%s\\n' \"$TERM\"\r", + }) + require.NoError(t, err) + _, err = netConn1.Write(data) + require.NoError(t, err) + require.NoError(t, tr1.ReadUntilString(ctx, "xterm-256color"), "find TERM output") + require.NoError(t, tr2.ReadUntilString(ctx, "xterm-256color"), "find TERM output") + require.NoError(t, tr1.ReadUntil(ctx, matchPrompt), "find prompt") + require.NoError(t, tr2.ReadUntil(ctx, matchPrompt), "find prompt") + + data, err = json.Marshal(workspacesdk.ReconnectingPTYRequest{ Data: "echo test\r", }) require.NoError(t, err) diff --git a/agent/reconnectingpty/buffered.go b/agent/reconnectingpty/buffered.go index 25ba1ee136..385d6ef7f7 100644 --- a/agent/reconnectingpty/buffered.go +++ b/agent/reconnectingpty/buffered.go @@ -60,7 +60,7 @@ func newBuffered(ctx context.Context, logger slog.Logger, execer agentexec.Exece // first argument so remove it. cmdWithEnv := execer.PTYCommandContext(ctx, cmd.Path, cmd.Args[1:]...) //nolint:gocritic - cmdWithEnv.Env = append(rpty.command.Env, "TERM=xterm-256color") + cmdWithEnv.Env = append(rpty.command.Env, "TERM="+xterm256Color) cmdWithEnv.Dir = rpty.command.Dir ptty, process, err := pty.Start(cmdWithEnv) if err != nil { diff --git a/agent/reconnectingpty/reconnectingpty.go b/agent/reconnectingpty/reconnectingpty.go index 82b018cf7b..c3fa833b41 100644 --- a/agent/reconnectingpty/reconnectingpty.go +++ b/agent/reconnectingpty/reconnectingpty.go @@ -19,11 +19,17 @@ import ( "github.com/coder/coder/v2/pty" ) -// attachTimeout is the initial timeout for attaching and will probably be far -// shorter than the reconnect timeout in most cases; in tests it might be -// longer. It should be at least long enough for the first screen attach to be -// able to start up the daemon and for the buffered pty to start. -const attachTimeout = 30 * time.Second +const ( + // attachTimeout is the initial timeout for attaching and will probably be far + // shorter than the reconnect timeout in most cases; in tests it might be + // longer. It should be at least long enough for the first screen attach to be + // able to start up the daemon and for the buffered pty to start. + attachTimeout = 30 * time.Second + + // xterm256Color is the terminal type exposed to commands running in the web + // terminal. + xterm256Color = "xterm-256color" +) // Options allows configuring the reconnecting pty. type Options struct { diff --git a/agent/reconnectingpty/screen.go b/agent/reconnectingpty/screen.go index 221713d212..a2fcb4fba2 100644 --- a/agent/reconnectingpty/screen.go +++ b/agent/reconnectingpty/screen.go @@ -103,6 +103,13 @@ func newScreen(ctx context.Context, logger slog.Logger, execer agentexec.Execer, // output when scrolling back with the mouse wheel (copy mode still works // since that is screen itself scrolling). "altscreen on", + // Match the background color erase capability advertised by xterm-256color. + "defbce on", + // Keep the shell environment aligned with the web terminal emulator. Some + // terminal applications, including tmux, render differently when they see + // screen.xterm-256color even though screen is only an implementation + // detail for reconnecting. + "term " + xterm256Color, // Remap the control key to C-s since C-a may be used in applications. C-s // is chosen because it cannot actually be used because by default it will // pause and C-q to resume will just kill the browser window. We may not @@ -230,7 +237,7 @@ func (rpty *screenReconnectingPTY) doAttach(ctx context.Context, conn net.Conn, // pty.Cmd duplicates Path as the first argument so remove it. }, rpty.command.Args[1:]...)...) //nolint:gocritic - cmd.Env = append(rpty.command.Env, "TERM=xterm-256color") + cmd.Env = append(rpty.command.Env, "TERM="+xterm256Color) cmd.Dir = rpty.command.Dir ptty, process, err := pty.Start(cmd, pty.WithPTYOption( pty.WithSSHRequest(ssh.Pty{ @@ -346,7 +353,7 @@ func (rpty *screenReconnectingPTY) sendCommand(ctx context.Context, command stri "-X", command, ) //nolint:gocritic - cmd.Env = append(rpty.command.Env, "TERM=xterm-256color") + cmd.Env = append(rpty.command.Env, "TERM="+xterm256Color) cmd.Dir = rpty.command.Dir cmd.Stdout = &stdout err := cmd.Run() diff --git a/site/src/modules/terminal/WorkspaceTerminal.tsx b/site/src/modules/terminal/WorkspaceTerminal.tsx index 418140c200..3cad12fd46 100644 --- a/site/src/modules/terminal/WorkspaceTerminal.tsx +++ b/site/src/modules/terminal/WorkspaceTerminal.tsx @@ -164,6 +164,12 @@ export const WorkspaceTerminal = ({ const nextTerminal = new Terminal({ allowProposedApi: true, allowTransparency: true, + // Use the selected terminal font for block element, box drawing, + // quadrant, and powerline glyphs. xterm's custom glyph renderer helps + // when fonts lack powerline glyphs, but it changes block and quadrant + // shapes and breaks TUIs that use them for pixel art, like Claude + // Code's logo. + customGlyphs: false, disableStdin: false, fontFamily: terminalFontFamily, fontSize: 16, diff --git a/site/src/theme/constants.test.ts b/site/src/theme/constants.test.ts new file mode 100644 index 0000000000..3ea061fcb4 --- /dev/null +++ b/site/src/theme/constants.test.ts @@ -0,0 +1,9 @@ +import { terminalFonts } from "./constants"; + +describe("terminalFonts", () => { + it("uses the terminal symbol fallback before generic monospace", () => { + for (const fontFamily of Object.values(terminalFonts)) { + expect(fontFamily).toMatch(/'Coder Terminal Symbols', monospace$/); + } + }); +}); diff --git a/site/src/theme/constants.ts b/site/src/theme/constants.ts index 62274d5001..8b54c8d1a6 100644 --- a/site/src/theme/constants.ts +++ b/site/src/theme/constants.ts @@ -3,30 +3,30 @@ import type { TerminalFontName } from "#/api/typesGenerated"; export const borderRadius = 8; const MONOSPACE_DEFAULT_FONT = "Geist Mono Variable"; +const TERMINAL_SYMBOL_FONT = "'Coder Terminal Symbols'"; export const MONOSPACE_FONT_FAMILY = "'Geist Mono Variable', 'IBM Plex Mono', 'Lucida Console', 'Lucida Sans Typewriter', 'Liberation Mono', 'Monaco', 'Courier New', Courier, monospace"; export const BODY_FONT_FAMILY = `"Geist Variable", system-ui, sans-serif`; -export const terminalFonts: Record = { - "fira-code": MONOSPACE_FONT_FAMILY.replace( - MONOSPACE_DEFAULT_FONT, - "Fira Code", - ), - "jetbrains-mono": MONOSPACE_FONT_FAMILY.replace( - MONOSPACE_DEFAULT_FONT, - "JetBrains Mono", - ), - "source-code-pro": MONOSPACE_FONT_FAMILY.replace( - MONOSPACE_DEFAULT_FONT, - "Source Code Pro", - ), - "ibm-plex-mono": MONOSPACE_FONT_FAMILY.replace( - MONOSPACE_DEFAULT_FONT, - "IBM Plex Mono", - ), - "geist-mono": MONOSPACE_FONT_FAMILY, +const withTerminalSymbolFallback = (fontFamily: string) => + fontFamily.replace(", monospace", `, ${TERMINAL_SYMBOL_FONT}, monospace`); - "": MONOSPACE_FONT_FAMILY, +export const terminalFonts: Record = { + "fira-code": withTerminalSymbolFallback( + MONOSPACE_FONT_FAMILY.replace(MONOSPACE_DEFAULT_FONT, "Fira Code"), + ), + "jetbrains-mono": withTerminalSymbolFallback( + MONOSPACE_FONT_FAMILY.replace(MONOSPACE_DEFAULT_FONT, "JetBrains Mono"), + ), + "source-code-pro": withTerminalSymbolFallback( + MONOSPACE_FONT_FAMILY.replace(MONOSPACE_DEFAULT_FONT, "Source Code Pro"), + ), + "ibm-plex-mono": withTerminalSymbolFallback( + MONOSPACE_FONT_FAMILY.replace(MONOSPACE_DEFAULT_FONT, "IBM Plex Mono"), + ), + "geist-mono": withTerminalSymbolFallback(MONOSPACE_FONT_FAMILY), + + "": withTerminalSymbolFallback(MONOSPACE_FONT_FAMILY), }; export const terminalFontLabels: Record = { "geist-mono": "Geist Mono", diff --git a/site/src/theme/fonts/coder-terminal-symbols-LICENSE.txt b/site/src/theme/fonts/coder-terminal-symbols-LICENSE.txt new file mode 100644 index 0000000000..06eb073d6b --- /dev/null +++ b/site/src/theme/fonts/coder-terminal-symbols-LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Ryan L McIntyre + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/site/src/theme/fonts/coder-terminal-symbols-powerline.woff2 b/site/src/theme/fonts/coder-terminal-symbols-powerline.woff2 new file mode 100644 index 0000000000..37471be442 Binary files /dev/null and b/site/src/theme/fonts/coder-terminal-symbols-powerline.woff2 differ diff --git a/site/src/theme/fonts/coder-terminal-symbols.css b/site/src/theme/fonts/coder-terminal-symbols.css new file mode 100644 index 0000000000..2bbf395694 --- /dev/null +++ b/site/src/theme/fonts/coder-terminal-symbols.css @@ -0,0 +1,12 @@ +/* + * Powerline fallback glyphs subset from Symbols Nerd Font Mono v3.4.0. + * Source: https://github.com/ryanoasis/nerd-fonts/releases/tag/v3.4.0 + */ +@font-face { + font-family: "Coder Terminal Symbols"; + font-style: normal; + font-display: swap; + font-weight: 400 700; + src: url("./coder-terminal-symbols-powerline.woff2?no-inline") format("woff2"); + unicode-range: U+E0A0, U+E0B0-E0BF; +} diff --git a/site/src/theme/globalFonts.ts b/site/src/theme/globalFonts.ts index aab84ed7c2..2edf8cc550 100644 --- a/site/src/theme/globalFonts.ts +++ b/site/src/theme/globalFonts.ts @@ -5,6 +5,7 @@ import "@fontsource/ibm-plex-mono/600.css"; import "@fontsource-variable/geist"; import "@fontsource-variable/geist-mono"; // Alternative fonts for Terminal +import "./fonts/coder-terminal-symbols.css"; import "@fontsource/fira-code/400.css"; import "@fontsource/fira-code/600.css"; import "@fontsource/source-code-pro/400.css";