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
parent
10c6121458
commit
b8a3e24cae
|
|
@ -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 プロジェクト/スキームのファイル名・
|
||||
ターゲット名(内部名のため)。
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "A5B30530299BEAAA0047F10C"
|
||||
BuildableName = "Ghostty.app"
|
||||
BuildableName = "term244.app"
|
||||
BlueprintName = "Ghostty"
|
||||
ReferencedContainer = "container:Ghostty.xcodeproj">
|
||||
</BuildableReference>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
@ -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, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
}
|
||||
|
||||
// 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>
|
||||
"""#
|
||||
}
|
||||
|
|
@ -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},
|
||||
)});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue