diff --git a/macos/Sources/TerminalSurface.swift b/macos/Sources/TerminalSurface.swift index ccde2e3ec..d1817988b 100644 --- a/macos/Sources/TerminalSurface.swift +++ b/macos/Sources/TerminalSurface.swift @@ -15,6 +15,9 @@ struct TerminalSurface: NSViewRepresentable { category: String(describing: TerminalSurfaceView.self) ) + /// The view to render for the terminal surface. + let view: TerminalSurfaceView + /// This should be set to true wen the surface has focus. This is up to the parent because /// focus is also defined by window focus. It is important this is set correctly since if it is /// false then the surface will idle at almost 0% CPU. @@ -34,27 +37,18 @@ struct TerminalSurface: NSViewRepresentable { /// set the appropriate title of the window/tab/split/etc. if they care. @Binding var title: String - @StateObject private var state: TerminalSurfaceView - - init(_ app: ghostty_app_t, hasFocus: Bool, size: CGSize, title: Binding) { - self._state = StateObject(wrappedValue: TerminalSurfaceView(app)) - self._title = title - self.hasFocus = hasFocus - self.size = size - } - func makeNSView(context: Context) -> TerminalSurfaceView { // We need the view as part of the state to be created previously because // the view is sent to the Ghostty API so that it can manipulate it // directly since we draw on a render thread. - state.delegate = context.coordinator - return state; + view.delegate = context.coordinator + return view; } func updateNSView(_ view: TerminalSurfaceView, context: Context) { - state.delegate = context.coordinator - state.focusDidChange(hasFocus) - state.sizeDidChange(size) + view.delegate = context.coordinator + view.focusDidChange(hasFocus) + view.sizeDidChange(size) } func makeCoordinator() -> Coordinator { @@ -81,11 +75,13 @@ protocol TerminalSurfaceDelegate: AnyObject { /// The actual NSView implementation for the terminal surface. class TerminalSurfaceView: NSView, NSTextInputClient, ObservableObject { + // This can be set to receive event callbacks. weak var delegate: TerminalSurfaceDelegate? // The current title of the surface as defined by the pty. This can be - // changed with escape codes. - var title: String = "" { + // changed with escape codes. This is public because the callbacks go + // to the app level and it is set from there. + @Published var title: String = "" { didSet { if let delegate = self.delegate { delegate.titleDidChange(to: title) @@ -93,8 +89,9 @@ class TerminalSurfaceView: NSView, NSTextInputClient, ObservableObject { } } - var surface: ghostty_surface_t? = nil + private(set) var surface: ghostty_surface_t? var error: Error? = nil + private var markedText: NSMutableAttributedString; // We need to support being a first responder so that we can get input events @@ -103,123 +100,6 @@ class TerminalSurfaceView: NSView, NSTextInputClient, ObservableObject { // I don't thikn we need this but this lets us know we should redraw our layer // so we'll use that to tell ghostty to refresh. override var wantsUpdateLayer: Bool { return true } - - // Mapping of event keyCode to ghostty input key values. This is cribbed from - // glfw mostly since we started as a glfw-based app way back in the day! - static let keycodes: [UInt16 : ghostty_input_key_e] = [ - 0x1D: GHOSTTY_KEY_ZERO, - 0x12: GHOSTTY_KEY_ONE, - 0x13: GHOSTTY_KEY_TWO, - 0x14: GHOSTTY_KEY_THREE, - 0x15: GHOSTTY_KEY_FOUR, - 0x17: GHOSTTY_KEY_FIVE, - 0x16: GHOSTTY_KEY_SIX, - 0x1A: GHOSTTY_KEY_SEVEN, - 0x1C: GHOSTTY_KEY_EIGHT, - 0x19: GHOSTTY_KEY_NINE, - 0x00: GHOSTTY_KEY_A, - 0x0B: GHOSTTY_KEY_B, - 0x08: GHOSTTY_KEY_C, - 0x02: GHOSTTY_KEY_D, - 0x0E: GHOSTTY_KEY_E, - 0x03: GHOSTTY_KEY_F, - 0x05: GHOSTTY_KEY_G, - 0x04: GHOSTTY_KEY_H, - 0x22: GHOSTTY_KEY_I, - 0x26: GHOSTTY_KEY_J, - 0x28: GHOSTTY_KEY_K, - 0x25: GHOSTTY_KEY_L, - 0x2E: GHOSTTY_KEY_M, - 0x2D: GHOSTTY_KEY_N, - 0x1F: GHOSTTY_KEY_O, - 0x23: GHOSTTY_KEY_P, - 0x0C: GHOSTTY_KEY_Q, - 0x0F: GHOSTTY_KEY_R, - 0x01: GHOSTTY_KEY_S, - 0x11: GHOSTTY_KEY_T, - 0x20: GHOSTTY_KEY_U, - 0x09: GHOSTTY_KEY_V, - 0x0D: GHOSTTY_KEY_W, - 0x07: GHOSTTY_KEY_X, - 0x10: GHOSTTY_KEY_Y, - 0x06: GHOSTTY_KEY_Z, - - 0x27: GHOSTTY_KEY_APOSTROPHE, - 0x2A: GHOSTTY_KEY_BACKSLASH, - 0x2B: GHOSTTY_KEY_COMMA, - 0x18: GHOSTTY_KEY_EQUAL, - 0x32: GHOSTTY_KEY_GRAVE_ACCENT, - 0x21: GHOSTTY_KEY_LEFT_BRACKET, - 0x1B: GHOSTTY_KEY_MINUS, - 0x2F: GHOSTTY_KEY_PERIOD, - 0x1E: GHOSTTY_KEY_RIGHT_BRACKET, - 0x29: GHOSTTY_KEY_SEMICOLON, - 0x2C: GHOSTTY_KEY_SLASH, - - 0x33: GHOSTTY_KEY_BACKSPACE, - 0x39: GHOSTTY_KEY_CAPS_LOCK, - 0x75: GHOSTTY_KEY_DELETE, - 0x7D: GHOSTTY_KEY_DOWN, - 0x77: GHOSTTY_KEY_END, - 0x24: GHOSTTY_KEY_ENTER, - 0x35: GHOSTTY_KEY_ESCAPE, - 0x7A: GHOSTTY_KEY_F1, - 0x78: GHOSTTY_KEY_F2, - 0x63: GHOSTTY_KEY_F3, - 0x76: GHOSTTY_KEY_F4, - 0x60: GHOSTTY_KEY_F5, - 0x61: GHOSTTY_KEY_F6, - 0x62: GHOSTTY_KEY_F7, - 0x64: GHOSTTY_KEY_F8, - 0x65: GHOSTTY_KEY_F9, - 0x6D: GHOSTTY_KEY_F10, - 0x67: GHOSTTY_KEY_F11, - 0x6F: GHOSTTY_KEY_F12, - 0x69: GHOSTTY_KEY_PRINT_SCREEN, - 0x6B: GHOSTTY_KEY_F14, - 0x71: GHOSTTY_KEY_F15, - 0x6A: GHOSTTY_KEY_F16, - 0x40: GHOSTTY_KEY_F17, - 0x4F: GHOSTTY_KEY_F18, - 0x50: GHOSTTY_KEY_F19, - 0x5A: GHOSTTY_KEY_F20, - 0x73: GHOSTTY_KEY_HOME, - 0x72: GHOSTTY_KEY_INSERT, - 0x7B: GHOSTTY_KEY_LEFT, - 0x3A: GHOSTTY_KEY_LEFT_ALT, - 0x3B: GHOSTTY_KEY_LEFT_CONTROL, - 0x38: GHOSTTY_KEY_LEFT_SHIFT, - 0x37: GHOSTTY_KEY_LEFT_SUPER, - 0x47: GHOSTTY_KEY_NUM_LOCK, - 0x79: GHOSTTY_KEY_PAGE_DOWN, - 0x74: GHOSTTY_KEY_PAGE_UP, - 0x7C: GHOSTTY_KEY_RIGHT, - 0x3D: GHOSTTY_KEY_RIGHT_ALT, - 0x3E: GHOSTTY_KEY_RIGHT_CONTROL, - 0x3C: GHOSTTY_KEY_RIGHT_SHIFT, - 0x36: GHOSTTY_KEY_RIGHT_SUPER, - 0x31: GHOSTTY_KEY_SPACE, - 0x30: GHOSTTY_KEY_TAB, - 0x7E: GHOSTTY_KEY_UP, - - 0x52: GHOSTTY_KEY_KP_0, - 0x53: GHOSTTY_KEY_KP_1, - 0x54: GHOSTTY_KEY_KP_2, - 0x55: GHOSTTY_KEY_KP_3, - 0x56: GHOSTTY_KEY_KP_4, - 0x57: GHOSTTY_KEY_KP_5, - 0x58: GHOSTTY_KEY_KP_6, - 0x59: GHOSTTY_KEY_KP_7, - 0x5B: GHOSTTY_KEY_KP_8, - 0x5C: GHOSTTY_KEY_KP_9, - 0x45: GHOSTTY_KEY_KP_ADD, - 0x41: GHOSTTY_KEY_KP_DECIMAL, - 0x4B: GHOSTTY_KEY_KP_DIVIDE, - 0x4C: GHOSTTY_KEY_KP_ENTER, - 0x51: GHOSTTY_KEY_KP_EQUAL, - 0x43: GHOSTTY_KEY_KP_MULTIPLY, - 0x4E: GHOSTTY_KEY_KP_SUBTRACT, - ]; init(_ app: ghostty_app_t) { self.markedText = NSMutableAttributedString() @@ -483,4 +363,121 @@ class TerminalSurfaceView: NSView, NSTextInputClient, ObservableObject { return ghostty_input_mods_e(mods) } + + // Mapping of event keyCode to ghostty input key values. This is cribbed from + // glfw mostly since we started as a glfw-based app way back in the day! + static let keycodes: [UInt16 : ghostty_input_key_e] = [ + 0x1D: GHOSTTY_KEY_ZERO, + 0x12: GHOSTTY_KEY_ONE, + 0x13: GHOSTTY_KEY_TWO, + 0x14: GHOSTTY_KEY_THREE, + 0x15: GHOSTTY_KEY_FOUR, + 0x17: GHOSTTY_KEY_FIVE, + 0x16: GHOSTTY_KEY_SIX, + 0x1A: GHOSTTY_KEY_SEVEN, + 0x1C: GHOSTTY_KEY_EIGHT, + 0x19: GHOSTTY_KEY_NINE, + 0x00: GHOSTTY_KEY_A, + 0x0B: GHOSTTY_KEY_B, + 0x08: GHOSTTY_KEY_C, + 0x02: GHOSTTY_KEY_D, + 0x0E: GHOSTTY_KEY_E, + 0x03: GHOSTTY_KEY_F, + 0x05: GHOSTTY_KEY_G, + 0x04: GHOSTTY_KEY_H, + 0x22: GHOSTTY_KEY_I, + 0x26: GHOSTTY_KEY_J, + 0x28: GHOSTTY_KEY_K, + 0x25: GHOSTTY_KEY_L, + 0x2E: GHOSTTY_KEY_M, + 0x2D: GHOSTTY_KEY_N, + 0x1F: GHOSTTY_KEY_O, + 0x23: GHOSTTY_KEY_P, + 0x0C: GHOSTTY_KEY_Q, + 0x0F: GHOSTTY_KEY_R, + 0x01: GHOSTTY_KEY_S, + 0x11: GHOSTTY_KEY_T, + 0x20: GHOSTTY_KEY_U, + 0x09: GHOSTTY_KEY_V, + 0x0D: GHOSTTY_KEY_W, + 0x07: GHOSTTY_KEY_X, + 0x10: GHOSTTY_KEY_Y, + 0x06: GHOSTTY_KEY_Z, + + 0x27: GHOSTTY_KEY_APOSTROPHE, + 0x2A: GHOSTTY_KEY_BACKSLASH, + 0x2B: GHOSTTY_KEY_COMMA, + 0x18: GHOSTTY_KEY_EQUAL, + 0x32: GHOSTTY_KEY_GRAVE_ACCENT, + 0x21: GHOSTTY_KEY_LEFT_BRACKET, + 0x1B: GHOSTTY_KEY_MINUS, + 0x2F: GHOSTTY_KEY_PERIOD, + 0x1E: GHOSTTY_KEY_RIGHT_BRACKET, + 0x29: GHOSTTY_KEY_SEMICOLON, + 0x2C: GHOSTTY_KEY_SLASH, + + 0x33: GHOSTTY_KEY_BACKSPACE, + 0x39: GHOSTTY_KEY_CAPS_LOCK, + 0x75: GHOSTTY_KEY_DELETE, + 0x7D: GHOSTTY_KEY_DOWN, + 0x77: GHOSTTY_KEY_END, + 0x24: GHOSTTY_KEY_ENTER, + 0x35: GHOSTTY_KEY_ESCAPE, + 0x7A: GHOSTTY_KEY_F1, + 0x78: GHOSTTY_KEY_F2, + 0x63: GHOSTTY_KEY_F3, + 0x76: GHOSTTY_KEY_F4, + 0x60: GHOSTTY_KEY_F5, + 0x61: GHOSTTY_KEY_F6, + 0x62: GHOSTTY_KEY_F7, + 0x64: GHOSTTY_KEY_F8, + 0x65: GHOSTTY_KEY_F9, + 0x6D: GHOSTTY_KEY_F10, + 0x67: GHOSTTY_KEY_F11, + 0x6F: GHOSTTY_KEY_F12, + 0x69: GHOSTTY_KEY_PRINT_SCREEN, + 0x6B: GHOSTTY_KEY_F14, + 0x71: GHOSTTY_KEY_F15, + 0x6A: GHOSTTY_KEY_F16, + 0x40: GHOSTTY_KEY_F17, + 0x4F: GHOSTTY_KEY_F18, + 0x50: GHOSTTY_KEY_F19, + 0x5A: GHOSTTY_KEY_F20, + 0x73: GHOSTTY_KEY_HOME, + 0x72: GHOSTTY_KEY_INSERT, + 0x7B: GHOSTTY_KEY_LEFT, + 0x3A: GHOSTTY_KEY_LEFT_ALT, + 0x3B: GHOSTTY_KEY_LEFT_CONTROL, + 0x38: GHOSTTY_KEY_LEFT_SHIFT, + 0x37: GHOSTTY_KEY_LEFT_SUPER, + 0x47: GHOSTTY_KEY_NUM_LOCK, + 0x79: GHOSTTY_KEY_PAGE_DOWN, + 0x74: GHOSTTY_KEY_PAGE_UP, + 0x7C: GHOSTTY_KEY_RIGHT, + 0x3D: GHOSTTY_KEY_RIGHT_ALT, + 0x3E: GHOSTTY_KEY_RIGHT_CONTROL, + 0x3C: GHOSTTY_KEY_RIGHT_SHIFT, + 0x36: GHOSTTY_KEY_RIGHT_SUPER, + 0x31: GHOSTTY_KEY_SPACE, + 0x30: GHOSTTY_KEY_TAB, + 0x7E: GHOSTTY_KEY_UP, + + 0x52: GHOSTTY_KEY_KP_0, + 0x53: GHOSTTY_KEY_KP_1, + 0x54: GHOSTTY_KEY_KP_2, + 0x55: GHOSTTY_KEY_KP_3, + 0x56: GHOSTTY_KEY_KP_4, + 0x57: GHOSTTY_KEY_KP_5, + 0x58: GHOSTTY_KEY_KP_6, + 0x59: GHOSTTY_KEY_KP_7, + 0x5B: GHOSTTY_KEY_KP_8, + 0x5C: GHOSTTY_KEY_KP_9, + 0x45: GHOSTTY_KEY_KP_ADD, + 0x41: GHOSTTY_KEY_KP_DECIMAL, + 0x4B: GHOSTTY_KEY_KP_DIVIDE, + 0x4C: GHOSTTY_KEY_KP_ENTER, + 0x51: GHOSTTY_KEY_KP_EQUAL, + 0x43: GHOSTTY_KEY_KP_MULTIPLY, + 0x4E: GHOSTTY_KEY_KP_SUBTRACT, + ]; } diff --git a/macos/Sources/TerminalView.swift b/macos/Sources/TerminalView.swift index 830b3de68..6db2ff349 100644 --- a/macos/Sources/TerminalView.swift +++ b/macos/Sources/TerminalView.swift @@ -2,7 +2,9 @@ import SwiftUI import GhosttyKit struct TerminalView: View { - let app: ghostty_app_t + // The surface to create a view for + let surfaceView: TerminalSurfaceView + @FocusState private var surfaceFocus: Bool @Environment(\.isKeyWindow) private var isKeyWindow: Bool @State private var title: String = "Ghostty" @@ -11,12 +13,17 @@ struct TerminalView: View { // it is both individually focused and the containing window is key. private var hasFocus: Bool { surfaceFocus && isKeyWindow } + // Initialize a TerminalView with a new surface view state. + init(_ app: ghostty_app_t) { + self.surfaceView = TerminalSurfaceView(app) + } + var body: some View { // We use a GeometryReader to get the frame bounds so that our metal surface // is up to date. See TerminalSurfaceView for why we don't use the NSView // resize callback. GeometryReader { geo in - TerminalSurface(app, hasFocus: hasFocus, size: geo.size, title: $title) + TerminalSurface(view: surfaceView, hasFocus: hasFocus, size: geo.size, title: $title) .focused($surfaceFocus) .navigationTitle(title) } @@ -54,7 +61,7 @@ struct TerminalSplittableView: View { Button("Split Vertical") { splitDirection = .vertical } } - TerminalView(app: app) + TerminalView(app) .focused($focusedSide, equals: .TopLeft) } case .horizontal: