term244: rebrand + HTML/Markdown viewer tab (Phase 1-2)

Rebrand the macOS app to term244 (product name, bundle id, executable
name, display name) and add a viewer tab that renders HTML and Markdown
files in a WKWebView with a self-contained, dependency-free Markdown
renderer. The viewer joins the native macOS window tab group.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
pull/12772/head
244t 2026-05-22 19:06:07 +09:00
parent 10c6121458
commit b8a3e24cae
7 changed files with 482 additions and 19 deletions

28
CLAUDE.md Normal file
View File

@ -0,0 +1,28 @@
# term244 作業メモ
term244 は Ghostty(ターミナルエミュレータ)のフォーク。アプリ名を term244 にリブランドし、
HTML/Markdown レンダリングタブの追加や WSL/Windows 対応を進めている。
## 知見ログ
### 2026-05-22: macOS ビルドで Metal Toolchain 不足
- **何が起きたか**: `zig build``metal Ghostty (Ghostty.ir)` ステップで
`error: cannot execute tool 'metal' due to missing Metal Toolchain` が発生。
- **なぜ起きたか**: Xcode 26 では Metal Toolchain が標準同梱されず、別ダウンロード
コンポーネントになった。Xcode 本体だけでは `metal` コンパイラが無い。
- **どう直したか**: `xcodebuild -downloadComponent MetalToolchain` で導入。
ビルドスクリプト(`/tmp/term244-build.sh`)に「`xcrun -sdk macosx metal --version`
で有無を確認し、無ければ自動 DL」する処理を組み込んだ。
### 2026-05-22: リブランドの方針(軽いリブランド)
- `ghostty` 文字列は terminfo(`xterm-ghostty`)・C API シンボル(`ghostty_*`)・
設定ディレクトリ(`~/.config/ghostty`)・GTK app id にも広く存在し、変えると
互換性が壊れる。そのため**ユーザーから見える名前だけ**を term244 に変更する。
- 変更済み: `macos/Ghostty.xcodeproj/project.pbxproj`(PRODUCT_NAME / EXECUTABLE_NAME /
CFBundleDisplayName / PRODUCT_BUNDLE_IDENTIFIER = `com.term244.term244`)、
`Ghostty.xcscheme` の BuildableName、`src/build/GhosttyXcodebuild.zig` の
旧名ハードコード(`Ghostty.app` / `ghostty`)。
- 据え置き: terminfo、C API、設定ディレクトリ、Xcode プロジェクト/スキームのファイル名・
ターゲット名(内部名のため)。

View File

@ -86,7 +86,7 @@
A586167B2B7703CC009BDB1D /* fish */ = {isa = PBXFileReference; lastKnownFileType = folder; name = fish; path = "../zig-out/share/fish"; sourceTree = "<group>"; };
A5985CE52C33060F00C57AD3 /* man */ = {isa = PBXFileReference; lastKnownFileType = folder; name = man; path = "../zig-out/share/man"; sourceTree = "<group>"; };
A5A1F8842A489D6800D1E8BC /* terminfo */ = {isa = PBXFileReference; lastKnownFileType = folder; name = terminfo; path = "../zig-out/share/terminfo"; sourceTree = "<group>"; };
A5B30531299BEAAA0047F10C /* Ghostty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ghostty.app; sourceTree = BUILT_PRODUCTS_DIR; };
A5B30531299BEAAA0047F10C /* Ghostty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = term244.app; sourceTree = BUILT_PRODUCTS_DIR; };
A5B30538299BEAAB0047F10C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Ghostty.entitlements; sourceTree = "<group>"; };
A5D4499D2B53AE7B000F5B83 /* Ghostty-iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Ghostty-iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; };
@ -748,11 +748,11 @@
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
EXECUTABLE_NAME = ghostty;
EXECUTABLE_NAME = term244;
GCC_OPTIMIZATION_LEVEL = fast;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Ghostty-Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = Ghostty;
INFOPLIST_KEY_CFBundleDisplayName = term244;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
INFOPLIST_KEY_NSAppleEventsUsageDescription = "A program running within Ghostty would like to use AppleScript.";
INFOPLIST_KEY_NSAudioCaptureUsageDescription = "A program running within Ghostty would like to access your system's audio.";
@ -779,8 +779,8 @@
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 0.1;
"OTHER_LDFLAGS[arch=*]" = "-lstdc++";
PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.ghostty;
PRODUCT_NAME = "$(TARGET_NAME)";
PRODUCT_BUNDLE_IDENTIFIER = com.term244.term244;
PRODUCT_NAME = term244;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "Sources/App/macOS/ghostty-bridging-header.h";
SWIFT_VERSION = 5.0;
@ -963,7 +963,7 @@
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Ghostty.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ghostty";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/term244.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/term244";
};
name = Debug;
};
@ -986,7 +986,7 @@
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Ghostty.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ghostty";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/term244.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/term244";
};
name = Release;
};
@ -1009,7 +1009,7 @@
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Ghostty.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ghostty";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/term244.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/term244";
};
name = ReleaseLocal;
};
@ -1147,10 +1147,10 @@
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
EXECUTABLE_NAME = ghostty;
EXECUTABLE_NAME = term244;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Ghostty-Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "Ghostty[DEBUG]";
INFOPLIST_KEY_CFBundleDisplayName = "term244[DEBUG]";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
INFOPLIST_KEY_NSAppleEventsUsageDescription = "A program running within Ghostty would like to use AppleScript.";
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "A program running within Ghostty would like to use Bluetooth.";
@ -1176,8 +1176,8 @@
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 0.1;
"OTHER_LDFLAGS[arch=*]" = "-lstdc++";
PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.ghostty.debug;
PRODUCT_NAME = "$(TARGET_NAME)";
PRODUCT_BUNDLE_IDENTIFIER = com.term244.term244.debug;
PRODUCT_NAME = term244;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "Sources/App/macOS/ghostty-bridging-header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@ -1201,11 +1201,11 @@
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
EXECUTABLE_NAME = ghostty;
EXECUTABLE_NAME = term244;
GCC_OPTIMIZATION_LEVEL = fast;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Ghostty-Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = Ghostty;
INFOPLIST_KEY_CFBundleDisplayName = term244;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
INFOPLIST_KEY_NSAppleEventsUsageDescription = "A program running within Ghostty would like to use AppleScript.";
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "A program running within Ghostty would like to use Bluetooth.";
@ -1231,8 +1231,8 @@
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 0.1;
"OTHER_LDFLAGS[arch=*]" = "-lstdc++";
PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.ghostty;
PRODUCT_NAME = "$(TARGET_NAME)";
PRODUCT_BUNDLE_IDENTIFIER = com.term244.term244;
PRODUCT_NAME = term244;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "Sources/App/macOS/ghostty-bridging-header.h";
SWIFT_VERSION = 5.0;

View File

@ -15,7 +15,7 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A5B30530299BEAAA0047F10C"
BuildableName = "Ghostty.app"
BuildableName = "term244.app"
BlueprintName = "Ghostty"
ReferencedContainer = "container:Ghostty.xcodeproj">
</BuildableReference>

View File

@ -212,6 +212,16 @@ class AppDelegate: NSObject,
// Initial config loading
ghosttyConfigDidChange(config: ghostty.config)
// Add the "Open Viewer Tab" item to the File menu, right after "New Tab".
if let newTabItem = menuNewTab, let fileMenu = newTabItem.menu {
let viewerItem = NSMenuItem(
title: "Open Viewer Tab…",
action: #selector(openViewerTab(_:)),
keyEquivalent: "")
viewerItem.target = self
fileMenu.insertItem(viewerItem, at: fileMenu.index(of: newTabItem) + 1)
}
// Start our update checker.
updateController.startUpdater()
@ -458,6 +468,17 @@ class AppDelegate: NSObject,
var isDirectory = ObjCBool(true)
guard FileManager.default.fileExists(atPath: filename, isDirectory: &isDirectory) else { return false }
// HTML and Markdown files open in a viewer tab rather than executing.
if !isDirectory.boolValue {
let url = URL(fileURLWithPath: filename)
if ViewerController.supportedExtensions.contains(url.pathExtension.lowercased()) {
ViewerController.open(
fileURL: url,
from: TerminalController.preferredParent?.window)
return true
}
}
// Set to true if confirmation is required before starting up the
// new terminal.
var requiresConfirm: Bool = false
@ -962,6 +983,21 @@ class AppDelegate: NSObject,
)
}
/// Opens a file picker and renders the chosen HTML/Markdown file in a viewer tab.
@IBAction func openViewerTab(_ sender: Any?) {
let panel = NSOpenPanel()
panel.canChooseFiles = true
panel.canChooseDirectories = false
panel.allowsMultipleSelection = false
panel.message = "Choose an HTML or Markdown file to view"
panel.begin { response in
guard response == .OK, let url = panel.url else { return }
ViewerController.open(
fileURL: url,
from: TerminalController.preferredParent?.window)
}
}
@IBAction func closeAllWindows(_ sender: Any?) {
TerminalController.closeAllWindows()
AboutController.shared.hide()

View File

@ -0,0 +1,75 @@
import AppKit
import SwiftUI
/// A non-terminal tab that renders an HTML or Markdown file.
///
/// Unlike terminal tabs (`TerminalController` / `BaseTerminalController`), this
/// is a lean `NSWindowController` that hosts a `WKWebView` through SwiftUI. Its
/// window joins the native macOS tab group of a terminal window so it appears
/// as a regular tab alongside terminal tabs.
class ViewerController: NSWindowController, NSWindowDelegate {
/// Strong references to open viewer controllers. AppKit does not retain a
/// window controller for us, so we keep them alive here while their window
/// is open and drop them in `windowWillClose`.
private static var openControllers: [ViewerController] = []
/// File extensions this viewer knows how to render.
static let supportedExtensions: Set<String> = [
"md", "markdown", "mdown", "mkd", "mkdn", "html", "htm",
]
private let fileURL: URL
init(fileURL: URL) {
self.fileURL = fileURL
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 900, height: 680),
styleMask: [.titled, .closable, .miniaturizable, .resizable],
backing: .buffered,
defer: false)
window.tabbingMode = .preferred
window.title = fileURL.lastPathComponent
window.isReleasedWhenClosed = false
super.init(window: window)
window.delegate = self
window.contentView = NSHostingView(rootView: ViewerView(fileURL: fileURL))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) is not supported")
}
/// Open `fileURL` in a new viewer tab. If `parent` is given, the viewer
/// joins that window's native tab group; otherwise it opens standalone.
@discardableResult
static func open(fileURL: URL, from parent: NSWindow? = nil) -> ViewerController {
let controller = ViewerController(fileURL: fileURL)
openControllers.append(controller)
guard let window = controller.window else { return controller }
if let parent, window.tabbingMode != .disallowed {
// If macOS already auto-tabbed our window, remove it first so we
// control the ordering (mirrors TerminalController.newTab).
if let group = parent.tabGroup,
group.windows.firstIndex(of: window) != nil {
group.removeWindow(window)
}
parent.addTabbedWindowSafely(window, ordered: .above)
}
controller.showWindow(nil)
window.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)
return controller
}
// MARK: NSWindowDelegate
func windowWillClose(_ notification: Notification) {
Self.openControllers.removeAll { $0 === self }
}
}

View File

@ -0,0 +1,324 @@
import SwiftUI
import WebKit
/// SwiftUI root view for a viewer tab. Hosts a `WKWebView` that renders the
/// given HTML or Markdown file.
struct ViewerView: View {
let fileURL: URL
var body: some View {
ViewerWebView(fileURL: fileURL)
.ignoresSafeArea()
}
}
/// `NSViewRepresentable` wrapper around `WKWebView`.
struct ViewerWebView: NSViewRepresentable {
let fileURL: URL
func makeNSView(context: Context) -> WKWebView {
let config = WKWebViewConfiguration()
// A viewer never needs cookies or localStorage; a non-persistent store
// skips disk I/O on init.
config.websiteDataStore = .nonPersistent()
let webView = WKWebView(frame: .zero, configuration: config)
webView.allowsMagnification = true
load(into: webView)
return webView
}
func updateNSView(_ webView: WKWebView, context: Context) {
// The file URL is fixed for the lifetime of the view; nothing to do.
}
private func load(into webView: WKWebView) {
let ext = fileURL.pathExtension.lowercased()
let dir = fileURL.deletingLastPathComponent()
switch ext {
case "md", "markdown", "mdown", "mkd", "mkdn":
let text = (try? String(contentsOf: fileURL, encoding: .utf8))
?? "# Could not read file\n\n`\(fileURL.path)`"
webView.loadHTMLString(ViewerHTML.markdownPage(markdown: text), baseURL: dir)
default:
// html, htm, or anything else: render the file directly. Grant
// read access to the directory so relative assets resolve.
webView.loadFileURL(fileURL, allowingReadAccessTo: dir)
}
}
}
/// Builds the self-contained HTML page used to render Markdown. The page embeds
/// its own CSS and a compact Markdown-to-HTML renderer, so no network access or
/// bundled resources are required.
///
/// The Markdown source is embedded as a base64 string. base64's alphabet
/// (`A-Za-z0-9+/=`) contains no HTML-significant characters, so it can never
/// prematurely close the `<script>` element or break out of a string literal.
enum ViewerHTML {
static func markdownPage(markdown: String) -> String {
let b64 = Data(markdown.utf8).base64EncodedString()
return template.replacingOccurrences(of: "__MARKDOWN_B64__", with: b64)
}
private static let template = #"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
:root { color-scheme: light dark; }
body {
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
font-size: 15px; line-height: 1.6;
max-width: 880px; margin: 0 auto; padding: 32px 40px;
color: #1f2328; background: #ffffff;
-webkit-text-size-adjust: 100%; word-wrap: break-word;
}
h1, h2, h3, h4, h5, h6 { font-weight: 600; line-height: 1.25; margin: 24px 0 16px; }
h1 { font-size: 2em; border-bottom: 1px solid #d1d9e0; padding-bottom: .3em; }
h2 { font-size: 1.5em; border-bottom: 1px solid #d1d9e0; padding-bottom: .3em; }
h3 { font-size: 1.25em; }
h4 { font-size: 1em; }
h5 { font-size: .875em; }
h6 { font-size: .85em; color: #59636e; }
p { margin: 0 0 16px; }
a { color: #0969da; text-decoration: none; }
a:hover { text-decoration: underline; }
code {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: .88em; background: #eff1f3; padding: .2em .4em; border-radius: 6px;
}
pre {
background: #eff1f3; padding: 16px; border-radius: 8px; overflow: auto;
line-height: 1.45;
}
pre code { background: none; padding: 0; font-size: .85em; }
blockquote {
margin: 0 0 16px; padding: 0 1em; color: #59636e;
border-left: .25em solid #d1d9e0;
}
ul, ol { margin: 0 0 16px; padding-left: 2em; }
li { margin: .25em 0; }
li > ul, li > ol { margin: .25em 0; }
img { max-width: 100%; }
hr { border: 0; height: 1px; background: #d1d9e0; margin: 24px 0; }
table { border-collapse: collapse; margin: 0 0 16px; display: block; overflow: auto; }
table th, table td { border: 1px solid #d1d9e0; padding: 6px 13px; }
table th { font-weight: 600; }
table tr:nth-child(2n) { background: #f6f8fa; }
@media (prefers-color-scheme: dark) {
body { color: #e6edf3; background: #0d1117; }
h1, h2 { border-color: #3d444d; }
h6 { color: #9198a1; }
a { color: #4493f8; }
code, pre { background: #161b22; }
blockquote { color: #9198a1; border-color: #3d444d; }
hr { background: #3d444d; }
table th, table td { border-color: #3d444d; }
table tr:nth-child(2n) { background: #161b22; }
}
</style>
</head>
<body>
<div id="content"></div>
<script>
var MD_B64 = "__MARKDOWN_B64__";
(function () {
"use strict";
function decodeBase64Utf8(b64) {
var bin = atob(b64);
var bytes = new Uint8Array(bin.length);
for (var i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
return new TextDecoder("utf-8").decode(bytes);
}
var SRC = decodeBase64Utf8(MD_B64);
function esc(s) {
return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
// Format inline spans. The text is split on inline code spans so code is
// never touched by the emphasis/link passes, and no sentinel characters
// are needed.
function inlineFmt(text) {
var parts = text.split(/(`[^`\n]+`)/);
var out = "";
for (var p = 0; p < parts.length; p++) {
var seg = parts[p];
if (seg.length >= 2 && seg.charAt(0) === "`" && seg.charAt(seg.length - 1) === "`") {
out += "<code>" + esc(seg.slice(1, -1)) + "</code>";
continue;
}
var s = esc(seg);
s = s.replace(/!\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g,
'<img alt="$1" src="$2">');
s = s.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g,
'<a href="$2">$1</a>');
s = s.replace(/\*\*([^\s](?:[\s\S]*?[^\s])?)\*\*/g, "<strong>$1</strong>");
s = s.replace(/\*([^\s*](?:[\s\S]*?[^\s*])?)\*/g, "<em>$1</em>");
s = s.replace(/~~([^~]+)~~/g, "<del>$1</del>");
out += s;
}
return out;
}
function splitRow(row) {
return row.trim().replace(/^\|/, "").replace(/\|$/, "")
.split("|").map(function (c) { return c.trim(); });
}
function blank(s) { return /^\s*$/.test(s); }
function renderList(block) {
var baseIndent = block[0].match(/^(\s*)/)[1].length;
var ordered = /^\s*\d+[.)]/.test(block[0]);
var items = [];
var cur = null;
for (var k = 0; k < block.length; k++) {
var m = block[k].match(/^(\s*)([-*+]|\d+[.)])\s+([\s\S]*)$/);
if (m && m[1].length <= baseIndent) {
cur = [m[3]];
items.push(cur);
} else if (cur) {
cur.push(block[k].replace(new RegExp("^\\s{0," + (baseIndent + 2) + "}"), ""));
}
}
var tag = ordered ? "ol" : "ul";
var html = "<" + tag + ">";
for (var n = 0; n < items.length; n++) {
var content = items[n].join("\n");
if (/\n\s*([-*+]|\d+[.)])\s+/.test("\n" + content)) {
html += "<li>" + render(content) + "</li>";
} else {
html += "<li>" + inlineFmt(content.replace(/\n/g, " ")) + "</li>";
}
}
return html + "</" + tag + ">";
}
function render(text) {
var lines = text.replace(/\r\n?/g, "\n").split("\n");
var out = [];
var i = 0;
while (i < lines.length) {
var l = lines[i];
if (blank(l)) { i++; continue; }
// Fenced code block
var f = l.match(/^\s*(`{3,}|~{3,})/);
if (f) {
var fchar = f[1].charAt(0);
var closeRe = new RegExp("^\\s*" + fchar + "{3,}\\s*$");
var code = [];
i++;
while (i < lines.length && !closeRe.test(lines[i])) { code.push(lines[i]); i++; }
i++;
out.push("<pre><code>" + esc(code.join("\n")) + "</code></pre>");
continue;
}
// ATX heading
var h = l.match(/^(#{1,6})\s+(.*?)\s*#*\s*$/);
if (h) {
var lv = h[1].length;
out.push("<h" + lv + ">" + inlineFmt(h[2]) + "</h" + lv + ">");
i++;
continue;
}
// Horizontal rule
if (/^\s{0,3}([-*_])(\s*\1){2,}\s*$/.test(l)) { out.push("<hr>"); i++; continue; }
// Blockquote
if (/^\s*>/.test(l)) {
var bq = [];
while (i < lines.length && /^\s*>/.test(lines[i])) {
bq.push(lines[i].replace(/^\s*>\s?/, ""));
i++;
}
out.push("<blockquote>" + render(bq.join("\n")) + "</blockquote>");
continue;
}
// GFM table
if (l.indexOf("|") >= 0 && i + 1 < lines.length &&
/^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$/.test(lines[i + 1])) {
var headers = splitRow(l);
var aligns = splitRow(lines[i + 1]).map(function (c) {
var L = c.charAt(0) === ":", R = c.charAt(c.length - 1) === ":";
return L && R ? "center" : R ? "right" : L ? "left" : "";
});
i += 2;
var rows = [];
while (i < lines.length && lines[i].indexOf("|") >= 0 && !blank(lines[i])) {
rows.push(splitRow(lines[i]));
i++;
}
var t = "<table><thead><tr>";
for (var c = 0; c < headers.length; c++) {
var a = aligns[c] ? ' style="text-align:' + aligns[c] + '"' : "";
t += "<th" + a + ">" + inlineFmt(headers[c]) + "</th>";
}
t += "</tr></thead><tbody>";
for (var r = 0; r < rows.length; r++) {
t += "<tr>";
for (var c2 = 0; c2 < headers.length; c2++) {
var a2 = aligns[c2] ? ' style="text-align:' + aligns[c2] + '"' : "";
t += "<td" + a2 + ">" + inlineFmt(rows[r][c2] || "") + "</td>";
}
t += "</tr>";
}
out.push(t + "</tbody></table>");
continue;
}
// Raw HTML block: passed through verbatim (CommonMark allows raw HTML).
if (/^\s*<(\/?[a-zA-Z][\w-]*|!--)/.test(l)) {
var hb = [];
while (i < lines.length && !blank(lines[i])) { hb.push(lines[i]); i++; }
out.push(hb.join("\n"));
continue;
}
// List
if (/^\s*([-*+]|\d+[.)])\s+/.test(l)) {
var blk = [];
while (i < lines.length &&
(/^\s*([-*+]|\d+[.)])\s+/.test(lines[i]) ||
(!blank(lines[i]) && /^\s+\S/.test(lines[i])))) {
blk.push(lines[i]);
i++;
}
out.push(renderList(blk));
continue;
}
// Paragraph
var para = [];
while (i < lines.length && !blank(lines[i]) &&
!/^(#{1,6})\s/.test(lines[i]) &&
!/^\s*>/.test(lines[i]) &&
!/^\s*(`{3,}|~{3,})/.test(lines[i]) &&
!/^\s*([-*+]|\d+[.)])\s+/.test(lines[i]) &&
!/^\s{0,3}([-*_])(\s*\1){2,}\s*$/.test(lines[i])) {
para.push(lines[i]);
i++;
}
out.push("<p>" + inlineFmt(para.join("\n")).replace(/\n/g, "<br>\n") + "</p>");
}
return out.join("\n");
}
document.getElementById("content").innerHTML = render(SRC);
})();
</script>
</body>
</html>
"""#
}

View File

@ -49,7 +49,7 @@ pub fn init(
};
const env = try std.process.getEnvMap(b.allocator);
const app_path = b.fmt("macos/build/{s}/Ghostty.app", .{xc_config});
const app_path = b.fmt("macos/build/{s}/term244.app", .{xc_config});
// Our step to build the Ghostty macOS app.
const build = build: {
@ -143,7 +143,7 @@ pub fn init(
open.has_side_effects = true;
open.cwd = b.path("");
open.addArgs(&.{b.fmt(
"{s}/Contents/MacOS/ghostty",
"{s}/Contents/MacOS/term244",
.{app_path},
)});