mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 08:46:08 +03:00
Merge pull request #454 from mitchellh/pointer-cursor
OSC 22: Set Mouse Shape
This commit is contained in:
@ -70,6 +70,43 @@ typedef enum {
|
|||||||
GHOSTTY_MOUSE_MOMENTUM_MAY_BEGIN,
|
GHOSTTY_MOUSE_MOMENTUM_MAY_BEGIN,
|
||||||
} ghostty_input_mouse_momentum_e;
|
} ghostty_input_mouse_momentum_e;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
GHOSTTY_MOUSE_SHAPE_DEFAULT,
|
||||||
|
GHOSTTY_MOUSE_SHAPE_CONTEXT_MENU,
|
||||||
|
GHOSTTY_MOUSE_SHAPE_HELP,
|
||||||
|
GHOSTTY_MOUSE_SHAPE_POINTER,
|
||||||
|
GHOSTTY_MOUSE_SHAPE_PROGRESS,
|
||||||
|
GHOSTTY_MOUSE_SHAPE_WAIT,
|
||||||
|
GHOSTTY_MOUSE_SHAPE_CELL,
|
||||||
|
GHOSTTY_MOUSE_SHAPE_CROSSHAIR,
|
||||||
|
GHOSTTY_MOUSE_SHAPE_TEXT,
|
||||||
|
GHOSTTY_MOUSE_SHAPE_VERTICAL_TEXT,
|
||||||
|
GHOSTTY_MOUSE_SHAPE_ALIAS,
|
||||||
|
GHOSTTY_MOUSE_SHAPE_COPY,
|
||||||
|
GHOSTTY_MOUSE_SHAPE_MOVE,
|
||||||
|
GHOSTTY_MOUSE_SHAPE_NO_DROP,
|
||||||
|
GHOSTTY_MOUSE_SHAPE_NOT_ALLOWED,
|
||||||
|
GHOSTTY_MOUSE_SHAPE_GRAB,
|
||||||
|
GHOSTTY_MOUSE_SHAPE_GRABBING,
|
||||||
|
GHOSTTY_MOUSE_SHAPE_ALL_SCROLL,
|
||||||
|
GHOSTTY_MOUSE_SHAPE_COL_RESIZE,
|
||||||
|
GHOSTTY_MOUSE_SHAPE_ROW_RESIZE,
|
||||||
|
GHOSTTY_MOUSE_SHAPE_N_RESIZE,
|
||||||
|
GHOSTTY_MOUSE_SHAPE_E_RESIZE,
|
||||||
|
GHOSTTY_MOUSE_SHAPE_S_RESIZE,
|
||||||
|
GHOSTTY_MOUSE_SHAPE_W_RESIZE,
|
||||||
|
GHOSTTY_MOUSE_SHAPE_NE_RESIZE,
|
||||||
|
GHOSTTY_MOUSE_SHAPE_NW_RESIZE,
|
||||||
|
GHOSTTY_MOUSE_SHAPE_SE_RESIZE,
|
||||||
|
GHOSTTY_MOUSE_SHAPE_SW_RESIZE,
|
||||||
|
GHOSTTY_MOUSE_SHAPE_EW_RESIZE,
|
||||||
|
GHOSTTY_MOUSE_SHAPE_NS_RESIZE,
|
||||||
|
GHOSTTY_MOUSE_SHAPE_NESW_RESIZE,
|
||||||
|
GHOSTTY_MOUSE_SHAPE_NWSE_RESIZE,
|
||||||
|
GHOSTTY_MOUSE_SHAPE_ZOOM_IN,
|
||||||
|
GHOSTTY_MOUSE_SHAPE_ZOOM_OUT,
|
||||||
|
} ghostty_mouse_shape_e;
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
GHOSTTY_NON_NATIVE_FULLSCREEN_FALSE,
|
GHOSTTY_NON_NATIVE_FULLSCREEN_FALSE,
|
||||||
GHOSTTY_NON_NATIVE_FULLSCREEN_TRUE,
|
GHOSTTY_NON_NATIVE_FULLSCREEN_TRUE,
|
||||||
@ -264,6 +301,7 @@ typedef struct {
|
|||||||
typedef void (*ghostty_runtime_wakeup_cb)(void *);
|
typedef void (*ghostty_runtime_wakeup_cb)(void *);
|
||||||
typedef const ghostty_config_t (*ghostty_runtime_reload_config_cb)(void *);
|
typedef const ghostty_config_t (*ghostty_runtime_reload_config_cb)(void *);
|
||||||
typedef void (*ghostty_runtime_set_title_cb)(void *, const char *);
|
typedef void (*ghostty_runtime_set_title_cb)(void *, const char *);
|
||||||
|
typedef void (*ghostty_runtime_set_mouse_shape_cb)(void *, ghostty_mouse_shape_e);
|
||||||
typedef const char* (*ghostty_runtime_read_clipboard_cb)(void *, ghostty_clipboard_e);
|
typedef const char* (*ghostty_runtime_read_clipboard_cb)(void *, ghostty_clipboard_e);
|
||||||
typedef void (*ghostty_runtime_write_clipboard_cb)(void *, const char *, ghostty_clipboard_e);
|
typedef void (*ghostty_runtime_write_clipboard_cb)(void *, const char *, ghostty_clipboard_e);
|
||||||
typedef void (*ghostty_runtime_new_split_cb)(void *, ghostty_split_direction_e, ghostty_surface_config_s);
|
typedef void (*ghostty_runtime_new_split_cb)(void *, ghostty_split_direction_e, ghostty_surface_config_s);
|
||||||
@ -281,6 +319,7 @@ typedef struct {
|
|||||||
ghostty_runtime_wakeup_cb wakeup_cb;
|
ghostty_runtime_wakeup_cb wakeup_cb;
|
||||||
ghostty_runtime_reload_config_cb reload_config_cb;
|
ghostty_runtime_reload_config_cb reload_config_cb;
|
||||||
ghostty_runtime_set_title_cb set_title_cb;
|
ghostty_runtime_set_title_cb set_title_cb;
|
||||||
|
ghostty_runtime_set_mouse_shape_cb set_mouse_shape_cb;
|
||||||
ghostty_runtime_read_clipboard_cb read_clipboard_cb;
|
ghostty_runtime_read_clipboard_cb read_clipboard_cb;
|
||||||
ghostty_runtime_write_clipboard_cb write_clipboard_cb;
|
ghostty_runtime_write_clipboard_cb write_clipboard_cb;
|
||||||
ghostty_runtime_new_split_cb new_split_cb;
|
ghostty_runtime_new_split_cb new_split_cb;
|
||||||
|
@ -72,6 +72,7 @@ extension Ghostty {
|
|||||||
wakeup_cb: { userdata in AppState.wakeup(userdata) },
|
wakeup_cb: { userdata in AppState.wakeup(userdata) },
|
||||||
reload_config_cb: { userdata in AppState.reloadConfig(userdata) },
|
reload_config_cb: { userdata in AppState.reloadConfig(userdata) },
|
||||||
set_title_cb: { userdata, title in AppState.setTitle(userdata, title: title) },
|
set_title_cb: { userdata, title in AppState.setTitle(userdata, title: title) },
|
||||||
|
set_mouse_shape_cb: { userdata, shape in AppState.setMouseShape(userdata, shape: shape) },
|
||||||
read_clipboard_cb: { userdata, loc in AppState.readClipboard(userdata, location: loc) },
|
read_clipboard_cb: { userdata, loc in AppState.readClipboard(userdata, location: loc) },
|
||||||
write_clipboard_cb: { userdata, str, loc in AppState.writeClipboard(userdata, string: str, location: loc) },
|
write_clipboard_cb: { userdata, str, loc in AppState.writeClipboard(userdata, string: str, location: loc) },
|
||||||
new_split_cb: { userdata, direction, surfaceConfig in AppState.newSplit(userdata, direction: direction, config: surfaceConfig) },
|
new_split_cb: { userdata, direction, surfaceConfig in AppState.newSplit(userdata, direction: direction, config: surfaceConfig) },
|
||||||
@ -332,6 +333,11 @@ extension Ghostty {
|
|||||||
surfaceView.title = titleStr
|
surfaceView.title = titleStr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func setMouseShape(_ userdata: UnsafeMutableRawPointer?, shape: ghostty_mouse_shape_e) {
|
||||||
|
let surfaceView = Unmanaged<SurfaceView>.fromOpaque(userdata!).takeUnretainedValue()
|
||||||
|
surfaceView.setCursorShape(shape)
|
||||||
|
}
|
||||||
|
|
||||||
static func toggleFullscreen(_ userdata: UnsafeMutableRawPointer?, nonNativeFullscreen: ghostty_non_native_fullscreen_e) {
|
static func toggleFullscreen(_ userdata: UnsafeMutableRawPointer?, nonNativeFullscreen: ghostty_non_native_fullscreen_e) {
|
||||||
guard let surface = self.surfaceUserdata(from: userdata) else { return }
|
guard let surface = self.surfaceUserdata(from: userdata) else { return }
|
||||||
|
@ -6,7 +6,7 @@ extension Ghostty {
|
|||||||
struct Terminal: View {
|
struct Terminal: View {
|
||||||
@Environment(\.ghosttyApp) private var app
|
@Environment(\.ghosttyApp) private var app
|
||||||
@FocusedValue(\.ghosttySurfaceTitle) private var surfaceTitle: String?
|
@FocusedValue(\.ghosttySurfaceTitle) private var surfaceTitle: String?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if let app = self.app {
|
if let app = self.app {
|
||||||
SurfaceForApp(app) { surfaceView in
|
SurfaceForApp(app) { surfaceView in
|
||||||
@ -16,44 +16,44 @@ extension Ghostty {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Yields a SurfaceView for a ghostty app that can then be used however you want.
|
/// Yields a SurfaceView for a ghostty app that can then be used however you want.
|
||||||
struct SurfaceForApp<Content: View>: View {
|
struct SurfaceForApp<Content: View>: View {
|
||||||
let content: ((SurfaceView) -> Content)
|
let content: ((SurfaceView) -> Content)
|
||||||
|
|
||||||
@StateObject private var surfaceView: SurfaceView
|
@StateObject private var surfaceView: SurfaceView
|
||||||
|
|
||||||
init(_ app: ghostty_app_t, @ViewBuilder content: @escaping ((SurfaceView) -> Content)) {
|
init(_ app: ghostty_app_t, @ViewBuilder content: @escaping ((SurfaceView) -> Content)) {
|
||||||
_surfaceView = StateObject(wrappedValue: SurfaceView(app, nil))
|
_surfaceView = StateObject(wrappedValue: SurfaceView(app, nil))
|
||||||
self.content = content
|
self.content = content
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
content(surfaceView)
|
content(surfaceView)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SurfaceWrapper: View {
|
struct SurfaceWrapper: View {
|
||||||
// The surface to create a view for. This must be created upstream. As long as this
|
// The surface to create a view for. This must be created upstream. As long as this
|
||||||
// remains the same, the surface that is being rendered remains the same.
|
// remains the same, the surface that is being rendered remains the same.
|
||||||
@ObservedObject var surfaceView: SurfaceView
|
@ObservedObject var surfaceView: SurfaceView
|
||||||
|
|
||||||
// True if this surface is part of a split view. This is important to know so
|
// True if this surface is part of a split view. This is important to know so
|
||||||
// we know whether to dim the surface out of focus.
|
// we know whether to dim the surface out of focus.
|
||||||
var isSplit: Bool = false
|
var isSplit: Bool = false
|
||||||
|
|
||||||
// Maintain whether our view has focus or not
|
// Maintain whether our view has focus or not
|
||||||
@FocusState private var surfaceFocus: Bool
|
@FocusState private var surfaceFocus: Bool
|
||||||
|
|
||||||
// Maintain whether our window has focus (is key) or not
|
// Maintain whether our window has focus (is key) or not
|
||||||
@State private var windowFocus: Bool = true
|
@State private var windowFocus: Bool = true
|
||||||
|
|
||||||
@Environment(\.ghosttyConfig) private var ghostty_config
|
@Environment(\.ghosttyConfig) private var ghostty_config
|
||||||
|
|
||||||
// This is true if the terminal is considered "focused". The terminal is focused if
|
// This is true if the terminal is considered "focused". The terminal is focused if
|
||||||
// it is both individually focused and the containing window is key.
|
// it is both individually focused and the containing window is key.
|
||||||
private var hasFocus: Bool { surfaceFocus && windowFocus }
|
private var hasFocus: Bool { surfaceFocus && windowFocus }
|
||||||
|
|
||||||
// The opacity of the rectangle when unfocused.
|
// The opacity of the rectangle when unfocused.
|
||||||
private var unfocusedOpacity: Double {
|
private var unfocusedOpacity: Double {
|
||||||
var opacity: Double = 0.85
|
var opacity: Double = 0.85
|
||||||
@ -61,7 +61,7 @@ extension Ghostty {
|
|||||||
_ = ghostty_config_get(ghostty_config, &opacity, key, UInt(key.count))
|
_ = ghostty_config_get(ghostty_config, &opacity, key, UInt(key.count))
|
||||||
return 1 - opacity
|
return 1 - opacity
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
// We use a GeometryReader to get the frame bounds so that our metal surface
|
// We use a GeometryReader to get the frame bounds so that our metal surface
|
||||||
@ -73,7 +73,7 @@ extension Ghostty {
|
|||||||
let pubBecomeFocused = NotificationCenter.default.publisher(for: Notification.didBecomeFocusedSurface, object: surfaceView)
|
let pubBecomeFocused = NotificationCenter.default.publisher(for: Notification.didBecomeFocusedSurface, object: surfaceView)
|
||||||
let pubBecomeKey = NotificationCenter.default.publisher(for: NSWindow.didBecomeKeyNotification)
|
let pubBecomeKey = NotificationCenter.default.publisher(for: NSWindow.didBecomeKeyNotification)
|
||||||
let pubResign = NotificationCenter.default.publisher(for: NSWindow.didResignKeyNotification)
|
let pubResign = NotificationCenter.default.publisher(for: NSWindow.didResignKeyNotification)
|
||||||
|
|
||||||
Surface(view: surfaceView, hasFocus: hasFocus, size: geo.size)
|
Surface(view: surfaceView, hasFocus: hasFocus, size: geo.size)
|
||||||
.focused($surfaceFocus)
|
.focused($surfaceFocus)
|
||||||
.focusedValue(\.ghosttySurfaceTitle, surfaceView.title)
|
.focusedValue(\.ghosttySurfaceTitle, surfaceView.title)
|
||||||
@ -95,7 +95,7 @@ extension Ghostty {
|
|||||||
// method doesn't work properly. See the dispatch of this notification
|
// method doesn't work properly. See the dispatch of this notification
|
||||||
// for more information.
|
// for more information.
|
||||||
if #available(macOS 13, *) { return }
|
if #available(macOS 13, *) { return }
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
surfaceFocus = true
|
surfaceFocus = true
|
||||||
}
|
}
|
||||||
@ -125,13 +125,13 @@ extension Ghostty {
|
|||||||
surfaceFocus = true
|
surfaceFocus = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// I don't know how older macOS versions behave but Ghostty only
|
// I don't know how older macOS versions behave but Ghostty only
|
||||||
// supports back to macOS 12 so its moot.
|
// supports back to macOS 12 so its moot.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.ghosttySurfaceView(surfaceView)
|
.ghosttySurfaceView(surfaceView)
|
||||||
|
|
||||||
// If we're part of a split view and don't have focus, we put a semi-transparent
|
// If we're part of a split view and don't have focus, we put a semi-transparent
|
||||||
// rectangle above our view to make it look unfocused. We use "surfaceFocus"
|
// rectangle above our view to make it look unfocused. We use "surfaceFocus"
|
||||||
// because we want to keep our focused surface dark even if we don't have window
|
// because we want to keep our focused surface dark even if we don't have window
|
||||||
@ -145,7 +145,7 @@ extension Ghostty {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A surface is terminology in Ghostty for a terminal surface, or a place where a terminal is actually drawn
|
/// A surface is terminology in Ghostty for a terminal surface, or a place where a terminal is actually drawn
|
||||||
/// and interacted with. The word "surface" is used because a surface may represent a window, a tab,
|
/// and interacted with. The word "surface" is used because a surface may represent a window, a tab,
|
||||||
/// a split, a small preview pane, etc. It is ANYTHING that has a terminal drawn to it.
|
/// a split, a small preview pane, etc. It is ANYTHING that has a terminal drawn to it.
|
||||||
@ -156,12 +156,12 @@ extension Ghostty {
|
|||||||
struct Surface: NSViewRepresentable {
|
struct Surface: NSViewRepresentable {
|
||||||
/// The view to render for the terminal surface.
|
/// The view to render for the terminal surface.
|
||||||
let view: SurfaceView
|
let view: SurfaceView
|
||||||
|
|
||||||
/// This should be set to true when the surface has focus. This is up to the parent because
|
/// This should be set to true when 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
|
/// 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.
|
/// false then the surface will idle at almost 0% CPU.
|
||||||
let hasFocus: Bool
|
let hasFocus: Bool
|
||||||
|
|
||||||
/// The size of the frame containing this view. We use this to update the the underlying
|
/// The size of the frame containing this view. We use this to update the the underlying
|
||||||
/// surface. This does not actually SET the size of our frame, this only sets the size
|
/// surface. This does not actually SET the size of our frame, this only sets the size
|
||||||
/// of our Metal surface for drawing.
|
/// of our Metal surface for drawing.
|
||||||
@ -171,74 +171,76 @@ extension Ghostty {
|
|||||||
///
|
///
|
||||||
/// The best approach is to wrap this view in a GeometryReader and pass in the geo.size.
|
/// The best approach is to wrap this view in a GeometryReader and pass in the geo.size.
|
||||||
let size: CGSize
|
let size: CGSize
|
||||||
|
|
||||||
func makeNSView(context: Context) -> SurfaceView {
|
func makeNSView(context: Context) -> SurfaceView {
|
||||||
// We need the view as part of the state to be created previously because
|
// 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
|
// the view is sent to the Ghostty API so that it can manipulate it
|
||||||
// directly since we draw on a render thread.
|
// directly since we draw on a render thread.
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateNSView(_ view: SurfaceView, context: Context) {
|
func updateNSView(_ view: SurfaceView, context: Context) {
|
||||||
view.focusDidChange(hasFocus)
|
view.focusDidChange(hasFocus)
|
||||||
view.sizeDidChange(size)
|
view.sizeDidChange(size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The NSView implementation for a terminal surface.
|
/// The NSView implementation for a terminal surface.
|
||||||
class SurfaceView: NSView, NSTextInputClient, ObservableObject {
|
class SurfaceView: NSView, NSTextInputClient, ObservableObject {
|
||||||
// The current title of the surface as defined by the pty. This can be
|
// The current title of the surface as defined by the pty. This can be
|
||||||
// changed with escape codes. This is public because the callbacks go
|
// changed with escape codes. This is public because the callbacks go
|
||||||
// to the app level and it is set from there.
|
// to the app level and it is set from there.
|
||||||
@Published var title: String = "👻"
|
@Published var title: String = "👻"
|
||||||
|
|
||||||
private(set) var surface: ghostty_surface_t?
|
private(set) var surface: ghostty_surface_t?
|
||||||
var error: Error? = nil
|
var error: Error? = nil
|
||||||
|
|
||||||
private var markedText: NSMutableAttributedString;
|
private var markedText: NSMutableAttributedString
|
||||||
|
private var mouseEntered: Bool = false
|
||||||
|
private var cursor: NSCursor = .arrow
|
||||||
|
|
||||||
// We need to support being a first responder so that we can get input events
|
// We need to support being a first responder so that we can get input events
|
||||||
override var acceptsFirstResponder: Bool { return true }
|
override var acceptsFirstResponder: Bool { return true }
|
||||||
|
|
||||||
// I don't think we need this but this lets us know we should redraw our layer
|
// I don't think we need this but this lets us know we should redraw our layer
|
||||||
// so we'll use that to tell ghostty to refresh.
|
// so we'll use that to tell ghostty to refresh.
|
||||||
override var wantsUpdateLayer: Bool { return true }
|
override var wantsUpdateLayer: Bool { return true }
|
||||||
|
|
||||||
init(_ app: ghostty_app_t, _ baseConfig: ghostty_surface_config_s?) {
|
init(_ app: ghostty_app_t, _ baseConfig: ghostty_surface_config_s?) {
|
||||||
self.markedText = NSMutableAttributedString()
|
self.markedText = NSMutableAttributedString()
|
||||||
|
|
||||||
// Initialize with some default frame size. The important thing is that this
|
// Initialize with some default frame size. The important thing is that this
|
||||||
// is non-zero so that our layer bounds are non-zero so that our renderer
|
// is non-zero so that our layer bounds are non-zero so that our renderer
|
||||||
// can do SOMETHING.
|
// can do SOMETHING.
|
||||||
super.init(frame: NSMakeRect(0, 0, 800, 600))
|
super.init(frame: NSMakeRect(0, 0, 800, 600))
|
||||||
|
|
||||||
// Setup our surface. This will also initialize all the terminal IO.
|
// Setup our surface. This will also initialize all the terminal IO.
|
||||||
var surface_cfg = baseConfig ?? ghostty_surface_config_new()
|
var surface_cfg = baseConfig ?? ghostty_surface_config_new()
|
||||||
surface_cfg.userdata = Unmanaged.passUnretained(self).toOpaque()
|
surface_cfg.userdata = Unmanaged.passUnretained(self).toOpaque()
|
||||||
surface_cfg.nsview = Unmanaged.passUnretained(self).toOpaque()
|
surface_cfg.nsview = Unmanaged.passUnretained(self).toOpaque()
|
||||||
surface_cfg.scale_factor = NSScreen.main!.backingScaleFactor
|
surface_cfg.scale_factor = NSScreen.main!.backingScaleFactor
|
||||||
|
|
||||||
guard let surface = ghostty_surface_new(app, &surface_cfg) else {
|
guard let surface = ghostty_surface_new(app, &surface_cfg) else {
|
||||||
self.error = AppError.surfaceCreateError
|
self.error = AppError.surfaceCreateError
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.surface = surface;
|
self.surface = surface;
|
||||||
|
|
||||||
// Setup our tracking area so we get mouse moved events
|
// Setup our tracking area so we get mouse moved events
|
||||||
updateTrackingAreas()
|
updateTrackingAreas()
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) is not supported for this view")
|
fatalError("init(coder:) is not supported for this view")
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
trackingAreas.forEach { removeTrackingArea($0) }
|
trackingAreas.forEach { removeTrackingArea($0) }
|
||||||
|
|
||||||
guard let surface = self.surface else { return }
|
guard let surface = self.surface else { return }
|
||||||
ghostty_surface_free(surface)
|
ghostty_surface_free(surface)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Close the surface early. This will free the associated Ghostty surface and the view will
|
/// Close the surface early. This will free the associated Ghostty surface and the view will
|
||||||
/// no longer render. The view can never be used again. This is a way for us to free the
|
/// no longer render. The view can never be used again. This is a way for us to free the
|
||||||
/// Ghostty resources while references may still be held to this view. I've found that SwiftUI
|
/// Ghostty resources while references may still be held to this view. I've found that SwiftUI
|
||||||
@ -248,15 +250,15 @@ extension Ghostty {
|
|||||||
ghostty_surface_free(surface)
|
ghostty_surface_free(surface)
|
||||||
self.surface = nil
|
self.surface = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func focusDidChange(_ focused: Bool) {
|
func focusDidChange(_ focused: Bool) {
|
||||||
guard let surface = self.surface else { return }
|
guard let surface = self.surface else { return }
|
||||||
ghostty_surface_set_focus(surface, focused)
|
ghostty_surface_set_focus(surface, focused)
|
||||||
}
|
}
|
||||||
|
|
||||||
func sizeDidChange(_ size: CGSize) {
|
func sizeDidChange(_ size: CGSize) {
|
||||||
guard let surface = self.surface else { return }
|
guard let surface = self.surface else { return }
|
||||||
|
|
||||||
// Ghostty wants to know the actual framebuffer size... It is very important
|
// Ghostty wants to know the actual framebuffer size... It is very important
|
||||||
// here that we use "size" and NOT the view frame. If we're in the middle of
|
// here that we use "size" and NOT the view frame. If we're in the middle of
|
||||||
// an animation (i.e. a fullscreen animation), the frame will not yet be updated.
|
// an animation (i.e. a fullscreen animation), the frame will not yet be updated.
|
||||||
@ -264,35 +266,93 @@ extension Ghostty {
|
|||||||
let scaledSize = self.convertToBacking(size)
|
let scaledSize = self.convertToBacking(size)
|
||||||
ghostty_surface_set_size(surface, UInt32(scaledSize.width), UInt32(scaledSize.height))
|
ghostty_surface_set_size(surface, UInt32(scaledSize.width), UInt32(scaledSize.height))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setCursorShape(_ shape: ghostty_mouse_shape_e) {
|
||||||
|
switch (shape) {
|
||||||
|
case GHOSTTY_MOUSE_SHAPE_DEFAULT:
|
||||||
|
cursor = .arrow
|
||||||
|
|
||||||
|
case GHOSTTY_MOUSE_SHAPE_CONTEXT_MENU:
|
||||||
|
cursor = .contextualMenu
|
||||||
|
|
||||||
|
case GHOSTTY_MOUSE_SHAPE_TEXT:
|
||||||
|
cursor = .iBeam
|
||||||
|
|
||||||
|
case GHOSTTY_MOUSE_SHAPE_CROSSHAIR:
|
||||||
|
cursor = .crosshair
|
||||||
|
|
||||||
|
case GHOSTTY_MOUSE_SHAPE_GRAB:
|
||||||
|
cursor = .openHand
|
||||||
|
|
||||||
|
case GHOSTTY_MOUSE_SHAPE_GRABBING:
|
||||||
|
cursor = .closedHand
|
||||||
|
|
||||||
|
case GHOSTTY_MOUSE_SHAPE_POINTER:
|
||||||
|
cursor = .pointingHand
|
||||||
|
|
||||||
|
case GHOSTTY_MOUSE_SHAPE_W_RESIZE:
|
||||||
|
cursor = .resizeLeft
|
||||||
|
|
||||||
|
case GHOSTTY_MOUSE_SHAPE_E_RESIZE:
|
||||||
|
cursor = .resizeRight
|
||||||
|
|
||||||
|
case GHOSTTY_MOUSE_SHAPE_N_RESIZE:
|
||||||
|
cursor = .resizeUp
|
||||||
|
|
||||||
|
case GHOSTTY_MOUSE_SHAPE_S_RESIZE:
|
||||||
|
cursor = .resizeDown
|
||||||
|
|
||||||
|
case GHOSTTY_MOUSE_SHAPE_NS_RESIZE:
|
||||||
|
cursor = .resizeUpDown
|
||||||
|
|
||||||
|
case GHOSTTY_MOUSE_SHAPE_EW_RESIZE:
|
||||||
|
cursor = .resizeLeftRight
|
||||||
|
|
||||||
|
case GHOSTTY_MOUSE_SHAPE_VERTICAL_TEXT:
|
||||||
|
cursor = .iBeamCursorForVerticalLayout
|
||||||
|
|
||||||
|
case GHOSTTY_MOUSE_SHAPE_NOT_ALLOWED:
|
||||||
|
cursor = .operationNotAllowed
|
||||||
|
|
||||||
|
default:
|
||||||
|
// We ignore unknown shapes.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set our cursor immediately if our mouse is over our window
|
||||||
|
if (mouseEntered) {
|
||||||
|
cursor.set()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override func viewDidMoveToWindow() {
|
override func viewDidMoveToWindow() {
|
||||||
guard let window = self.window else { return }
|
guard let window = self.window else { return }
|
||||||
guard let surface = self.surface else { return }
|
guard let surface = self.surface else { return }
|
||||||
guard ghostty_surface_transparent(surface) else { return }
|
guard ghostty_surface_transparent(surface) else { return }
|
||||||
|
|
||||||
// Set the window transparency settings
|
// Set the window transparency settings
|
||||||
window.isOpaque = false
|
window.isOpaque = false
|
||||||
window.hasShadow = false
|
window.hasShadow = false
|
||||||
window.backgroundColor = .clear
|
window.backgroundColor = .clear
|
||||||
|
|
||||||
// If we have a blur, set the blur
|
// If we have a blur, set the blur
|
||||||
ghostty_set_window_background_blur(surface, Unmanaged.passUnretained(window).toOpaque())
|
ghostty_set_window_background_blur(surface, Unmanaged.passUnretained(window).toOpaque())
|
||||||
}
|
}
|
||||||
|
|
||||||
override func resignFirstResponder() -> Bool {
|
override func resignFirstResponder() -> Bool {
|
||||||
let result = super.resignFirstResponder()
|
let result = super.resignFirstResponder()
|
||||||
|
|
||||||
// We sometimes call this manually (see SplitView) as a way to force us to
|
// We sometimes call this manually (see SplitView) as a way to force us to
|
||||||
// yield our focus state.
|
// yield our focus state.
|
||||||
if (result) { focusDidChange(false) }
|
if (result) { focusDidChange(false) }
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
override func updateTrackingAreas() {
|
override func updateTrackingAreas() {
|
||||||
// To update our tracking area we just recreate it all.
|
// To update our tracking area we just recreate it all.
|
||||||
trackingAreas.forEach { removeTrackingArea($0) }
|
trackingAreas.forEach { removeTrackingArea($0) }
|
||||||
|
|
||||||
// This tracking area is across the entire frame to notify us of mouse movements.
|
// This tracking area is across the entire frame to notify us of mouse movements.
|
||||||
addTrackingArea(NSTrackingArea(
|
addTrackingArea(NSTrackingArea(
|
||||||
rect: frame,
|
rect: frame,
|
||||||
@ -300,7 +360,7 @@ extension Ghostty {
|
|||||||
.mouseEnteredAndExited,
|
.mouseEnteredAndExited,
|
||||||
.mouseMoved,
|
.mouseMoved,
|
||||||
.inVisibleRect,
|
.inVisibleRect,
|
||||||
|
|
||||||
// It is possible this is incorrect when we have splits. This will make
|
// It is possible this is incorrect when we have splits. This will make
|
||||||
// mouse events only happen while the terminal is focused. Is that what
|
// mouse events only happen while the terminal is focused. Is that what
|
||||||
// we want?
|
// we want?
|
||||||
@ -309,12 +369,12 @@ extension Ghostty {
|
|||||||
owner: self,
|
owner: self,
|
||||||
userInfo: nil))
|
userInfo: nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
override func resetCursorRects() {
|
override func resetCursorRects() {
|
||||||
discardCursorRects()
|
discardCursorRects()
|
||||||
addCursorRect(frame, cursor: .iBeam)
|
addCursorRect(frame, cursor: .iBeam)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidChangeBackingProperties() {
|
override func viewDidChangeBackingProperties() {
|
||||||
guard let surface = self.surface else { return }
|
guard let surface = self.surface else { return }
|
||||||
|
|
||||||
@ -323,71 +383,79 @@ extension Ghostty {
|
|||||||
let xScale = fbFrame.size.width / self.frame.size.width
|
let xScale = fbFrame.size.width / self.frame.size.width
|
||||||
let yScale = fbFrame.size.height / self.frame.size.height
|
let yScale = fbFrame.size.height / self.frame.size.height
|
||||||
ghostty_surface_set_content_scale(surface, xScale, yScale)
|
ghostty_surface_set_content_scale(surface, xScale, yScale)
|
||||||
|
|
||||||
// When our scale factor changes, so does our fb size so we send that too
|
// When our scale factor changes, so does our fb size so we send that too
|
||||||
ghostty_surface_set_size(surface, UInt32(fbFrame.size.width), UInt32(fbFrame.size.height))
|
ghostty_surface_set_size(surface, UInt32(fbFrame.size.width), UInt32(fbFrame.size.height))
|
||||||
}
|
}
|
||||||
|
|
||||||
override func updateLayer() {
|
override func updateLayer() {
|
||||||
guard let surface = self.surface else { return }
|
guard let surface = self.surface else { return }
|
||||||
ghostty_surface_refresh(surface);
|
ghostty_surface_refresh(surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
override func mouseDown(with event: NSEvent) {
|
override func mouseDown(with event: NSEvent) {
|
||||||
guard let surface = self.surface else { return }
|
guard let surface = self.surface else { return }
|
||||||
let mods = Self.translateFlags(event.modifierFlags)
|
let mods = Self.translateFlags(event.modifierFlags)
|
||||||
ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_PRESS, GHOSTTY_MOUSE_LEFT, mods)
|
ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_PRESS, GHOSTTY_MOUSE_LEFT, mods)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func mouseUp(with event: NSEvent) {
|
override func mouseUp(with event: NSEvent) {
|
||||||
guard let surface = self.surface else { return }
|
guard let surface = self.surface else { return }
|
||||||
let mods = Self.translateFlags(event.modifierFlags)
|
let mods = Self.translateFlags(event.modifierFlags)
|
||||||
ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_RELEASE, GHOSTTY_MOUSE_LEFT, mods)
|
ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_RELEASE, GHOSTTY_MOUSE_LEFT, mods)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func rightMouseDown(with event: NSEvent) {
|
override func rightMouseDown(with event: NSEvent) {
|
||||||
guard let surface = self.surface else { return }
|
guard let surface = self.surface else { return }
|
||||||
let mods = Self.translateFlags(event.modifierFlags)
|
let mods = Self.translateFlags(event.modifierFlags)
|
||||||
ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_PRESS, GHOSTTY_MOUSE_RIGHT, mods)
|
ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_PRESS, GHOSTTY_MOUSE_RIGHT, mods)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func rightMouseUp(with event: NSEvent) {
|
override func rightMouseUp(with event: NSEvent) {
|
||||||
guard let surface = self.surface else { return }
|
guard let surface = self.surface else { return }
|
||||||
let mods = Self.translateFlags(event.modifierFlags)
|
let mods = Self.translateFlags(event.modifierFlags)
|
||||||
ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_RELEASE, GHOSTTY_MOUSE_RIGHT, mods)
|
ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_RELEASE, GHOSTTY_MOUSE_RIGHT, mods)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func mouseMoved(with event: NSEvent) {
|
override func mouseMoved(with event: NSEvent) {
|
||||||
guard let surface = self.surface else { return }
|
guard let surface = self.surface else { return }
|
||||||
|
|
||||||
// Convert window position to view position. Note (0, 0) is bottom left.
|
// Convert window position to view position. Note (0, 0) is bottom left.
|
||||||
let pos = self.convert(event.locationInWindow, from: nil)
|
let pos = self.convert(event.locationInWindow, from: nil)
|
||||||
ghostty_surface_mouse_pos(surface, pos.x, frame.height - pos.y)
|
ghostty_surface_mouse_pos(surface, pos.x, frame.height - pos.y)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func mouseDragged(with event: NSEvent) {
|
override func mouseDragged(with event: NSEvent) {
|
||||||
self.mouseMoved(with: event)
|
self.mouseMoved(with: event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func mouseEntered(with event: NSEvent) {
|
||||||
|
mouseEntered = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override func mouseExited(with event: NSEvent) {
|
||||||
|
mouseEntered = false
|
||||||
|
}
|
||||||
|
|
||||||
override func scrollWheel(with event: NSEvent) {
|
override func scrollWheel(with event: NSEvent) {
|
||||||
guard let surface = self.surface else { return }
|
guard let surface = self.surface else { return }
|
||||||
|
|
||||||
// Builds up the "input.ScrollMods" bitmask
|
// Builds up the "input.ScrollMods" bitmask
|
||||||
var mods: Int32 = 0
|
var mods: Int32 = 0
|
||||||
|
|
||||||
var x = event.scrollingDeltaX
|
var x = event.scrollingDeltaX
|
||||||
var y = event.scrollingDeltaY
|
var y = event.scrollingDeltaY
|
||||||
if event.hasPreciseScrollingDeltas {
|
if event.hasPreciseScrollingDeltas {
|
||||||
mods = 1
|
mods = 1
|
||||||
|
|
||||||
// We do a 2x speed multiplier. This is subjective, it "feels" better to me.
|
// We do a 2x speed multiplier. This is subjective, it "feels" better to me.
|
||||||
x *= 2;
|
x *= 2;
|
||||||
y *= 2;
|
y *= 2;
|
||||||
|
|
||||||
// TODO(mitchellh): do we have to scale the x/y here by window scale factor?
|
// TODO(mitchellh): do we have to scale the x/y here by window scale factor?
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine our momentum value
|
// Determine our momentum value
|
||||||
var momentum: ghostty_input_mouse_momentum_e = GHOSTTY_MOUSE_MOMENTUM_NONE
|
var momentum: ghostty_input_mouse_momentum_e = GHOSTTY_MOUSE_MOMENTUM_NONE
|
||||||
switch (event.momentumPhase) {
|
switch (event.momentumPhase) {
|
||||||
@ -406,17 +474,21 @@ extension Ghostty {
|
|||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pack our momentum value into the mods bitmask
|
// Pack our momentum value into the mods bitmask
|
||||||
mods |= Int32(momentum.rawValue) << 1
|
mods |= Int32(momentum.rawValue) << 1
|
||||||
|
|
||||||
ghostty_surface_mouse_scroll(surface, x, y, mods)
|
ghostty_surface_mouse_scroll(surface, x, y, mods)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func cursorUpdate(with event: NSEvent) {
|
||||||
|
cursor.set()
|
||||||
|
}
|
||||||
|
|
||||||
override func keyDown(with event: NSEvent) {
|
override func keyDown(with event: NSEvent) {
|
||||||
let action = event.isARepeat ? GHOSTTY_ACTION_REPEAT : GHOSTTY_ACTION_PRESS
|
let action = event.isARepeat ? GHOSTTY_ACTION_REPEAT : GHOSTTY_ACTION_PRESS
|
||||||
keyAction(action, event: event)
|
keyAction(action, event: event)
|
||||||
|
|
||||||
// We specifically DO NOT call interpretKeyEvents because ghostty_surface_key
|
// We specifically DO NOT call interpretKeyEvents because ghostty_surface_key
|
||||||
// automatically handles all key translation, and we don't handle any commands
|
// automatically handles all key translation, and we don't handle any commands
|
||||||
// currently.
|
// currently.
|
||||||
@ -427,11 +499,11 @@ extension Ghostty {
|
|||||||
//
|
//
|
||||||
// self.interpretKeyEvents([event])
|
// self.interpretKeyEvents([event])
|
||||||
}
|
}
|
||||||
|
|
||||||
override func keyUp(with event: NSEvent) {
|
override func keyUp(with event: NSEvent) {
|
||||||
keyAction(GHOSTTY_ACTION_RELEASE, event: event)
|
keyAction(GHOSTTY_ACTION_RELEASE, event: event)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func flagsChanged(with event: NSEvent) {
|
override func flagsChanged(with event: NSEvent) {
|
||||||
let mod: UInt32;
|
let mod: UInt32;
|
||||||
switch (event.keyCode) {
|
switch (event.keyCode) {
|
||||||
@ -442,26 +514,26 @@ extension Ghostty {
|
|||||||
case 0x37, 0x36: mod = GHOSTTY_MODS_SUPER.rawValue
|
case 0x37, 0x36: mod = GHOSTTY_MODS_SUPER.rawValue
|
||||||
default: return
|
default: return
|
||||||
}
|
}
|
||||||
|
|
||||||
// The keyAction function will do this AGAIN below which sucks to repeat
|
// The keyAction function will do this AGAIN below which sucks to repeat
|
||||||
// but this is super cheap and flagsChanged isn't that common.
|
// but this is super cheap and flagsChanged isn't that common.
|
||||||
let mods = Self.translateFlags(event.modifierFlags)
|
let mods = Self.translateFlags(event.modifierFlags)
|
||||||
|
|
||||||
// If the key that pressed this is active, its a press, else release
|
// If the key that pressed this is active, its a press, else release
|
||||||
var action = GHOSTTY_ACTION_RELEASE
|
var action = GHOSTTY_ACTION_RELEASE
|
||||||
if (mods.rawValue & mod != 0) { action = GHOSTTY_ACTION_PRESS }
|
if (mods.rawValue & mod != 0) { action = GHOSTTY_ACTION_PRESS }
|
||||||
|
|
||||||
keyAction(action, event: event)
|
keyAction(action, event: event)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func keyAction(_ action: ghostty_input_action_e, event: NSEvent) {
|
private func keyAction(_ action: ghostty_input_action_e, event: NSEvent) {
|
||||||
guard let surface = self.surface else { return }
|
guard let surface = self.surface else { return }
|
||||||
let mods = Self.translateFlags(event.modifierFlags)
|
let mods = Self.translateFlags(event.modifierFlags)
|
||||||
ghostty_surface_key(surface, action, UInt32(event.keyCode), mods)
|
ghostty_surface_key(surface, action, UInt32(event.keyCode), mods)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Menu Handlers
|
// MARK: Menu Handlers
|
||||||
|
|
||||||
@IBAction func copy(_ sender: Any?) {
|
@IBAction func copy(_ sender: Any?) {
|
||||||
guard let surface = self.surface else { return }
|
guard let surface = self.surface else { return }
|
||||||
let action = "copy_to_clipboard"
|
let action = "copy_to_clipboard"
|
||||||
@ -469,7 +541,7 @@ extension Ghostty {
|
|||||||
AppDelegate.logger.warning("action failed action=\(action)")
|
AppDelegate.logger.warning("action failed action=\(action)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func paste(_ sender: Any?) {
|
@IBAction func paste(_ sender: Any?) {
|
||||||
guard let surface = self.surface else { return }
|
guard let surface = self.surface else { return }
|
||||||
let action = "paste_from_clipboard"
|
let action = "paste_from_clipboard"
|
||||||
@ -477,7 +549,7 @@ extension Ghostty {
|
|||||||
AppDelegate.logger.warning("action failed action=\(action)")
|
AppDelegate.logger.warning("action failed action=\(action)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func pasteAsPlainText(_ sender: Any?) {
|
@IBAction func pasteAsPlainText(_ sender: Any?) {
|
||||||
guard let surface = self.surface else { return }
|
guard let surface = self.surface else { return }
|
||||||
let action = "paste_from_clipboard"
|
let action = "paste_from_clipboard"
|
||||||
@ -485,75 +557,75 @@ extension Ghostty {
|
|||||||
AppDelegate.logger.warning("action failed action=\(action)")
|
AppDelegate.logger.warning("action failed action=\(action)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: NSTextInputClient
|
// MARK: NSTextInputClient
|
||||||
|
|
||||||
func hasMarkedText() -> Bool {
|
func hasMarkedText() -> Bool {
|
||||||
return markedText.length > 0
|
return markedText.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func markedRange() -> NSRange {
|
func markedRange() -> NSRange {
|
||||||
guard markedText.length > 0 else { return NSRange() }
|
guard markedText.length > 0 else { return NSRange() }
|
||||||
return NSRange(0...(markedText.length-1))
|
return NSRange(0...(markedText.length-1))
|
||||||
}
|
}
|
||||||
|
|
||||||
func selectedRange() -> NSRange {
|
func selectedRange() -> NSRange {
|
||||||
return NSRange()
|
return NSRange()
|
||||||
}
|
}
|
||||||
|
|
||||||
func setMarkedText(_ string: Any, selectedRange: NSRange, replacementRange: NSRange) {
|
func setMarkedText(_ string: Any, selectedRange: NSRange, replacementRange: NSRange) {
|
||||||
switch string {
|
switch string {
|
||||||
case let v as NSAttributedString:
|
case let v as NSAttributedString:
|
||||||
self.markedText = NSMutableAttributedString(attributedString: v)
|
self.markedText = NSMutableAttributedString(attributedString: v)
|
||||||
|
|
||||||
case let v as String:
|
case let v as String:
|
||||||
self.markedText = NSMutableAttributedString(string: v)
|
self.markedText = NSMutableAttributedString(string: v)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
print("unknown marked text: \(string)")
|
print("unknown marked text: \(string)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func unmarkText() {
|
func unmarkText() {
|
||||||
self.markedText.mutableString.setString("")
|
self.markedText.mutableString.setString("")
|
||||||
}
|
}
|
||||||
|
|
||||||
func validAttributesForMarkedText() -> [NSAttributedString.Key] {
|
func validAttributesForMarkedText() -> [NSAttributedString.Key] {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
func attributedSubstring(forProposedRange range: NSRange, actualRange: NSRangePointer?) -> NSAttributedString? {
|
func attributedSubstring(forProposedRange range: NSRange, actualRange: NSRangePointer?) -> NSAttributedString? {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func characterIndex(for point: NSPoint) -> Int {
|
func characterIndex(for point: NSPoint) -> Int {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func firstRect(forCharacterRange range: NSRange, actualRange: NSRangePointer?) -> NSRect {
|
func firstRect(forCharacterRange range: NSRange, actualRange: NSRangePointer?) -> NSRect {
|
||||||
guard let surface = self.surface else {
|
guard let surface = self.surface else {
|
||||||
return NSMakeRect(frame.origin.x, frame.origin.y, 0, 0)
|
return NSMakeRect(frame.origin.x, frame.origin.y, 0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ghostty will tell us where it thinks an IME keyboard should render.
|
// Ghostty will tell us where it thinks an IME keyboard should render.
|
||||||
var x: Double = 0;
|
var x: Double = 0;
|
||||||
var y: Double = 0;
|
var y: Double = 0;
|
||||||
ghostty_surface_ime_point(surface, &x, &y)
|
ghostty_surface_ime_point(surface, &x, &y)
|
||||||
|
|
||||||
// Ghostty coordinates are in top-left (0, 0) so we have to convert to
|
// Ghostty coordinates are in top-left (0, 0) so we have to convert to
|
||||||
// bottom-left since that is what UIKit expects
|
// bottom-left since that is what UIKit expects
|
||||||
let rect = NSMakeRect(x, frame.size.height - y, 0, 0)
|
let rect = NSMakeRect(x, frame.size.height - y, 0, 0)
|
||||||
|
|
||||||
// Convert from view to screen coordinates
|
// Convert from view to screen coordinates
|
||||||
guard let window = self.window else { return rect }
|
guard let window = self.window else { return rect }
|
||||||
return window.convertToScreen(rect)
|
return window.convertToScreen(rect)
|
||||||
}
|
}
|
||||||
|
|
||||||
func insertText(_ string: Any, replacementRange: NSRange) {
|
func insertText(_ string: Any, replacementRange: NSRange) {
|
||||||
// We must have an associated event
|
// We must have an associated event
|
||||||
guard NSApp.currentEvent != nil else { return }
|
guard NSApp.currentEvent != nil else { return }
|
||||||
guard let surface = self.surface else { return }
|
guard let surface = self.surface else { return }
|
||||||
|
|
||||||
// We want the string view of the any value
|
// We want the string view of the any value
|
||||||
var chars = ""
|
var chars = ""
|
||||||
switch (string) {
|
switch (string) {
|
||||||
@ -564,39 +636,39 @@ extension Ghostty {
|
|||||||
default:
|
default:
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for codepoint in chars.unicodeScalars {
|
for codepoint in chars.unicodeScalars {
|
||||||
ghostty_surface_char(surface, codepoint.value)
|
ghostty_surface_char(surface, codepoint.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func doCommand(by selector: Selector) {
|
override func doCommand(by selector: Selector) {
|
||||||
// This currently just prevents NSBeep from interpretKeyEvents but in the future
|
// This currently just prevents NSBeep from interpretKeyEvents but in the future
|
||||||
// we may want to make some of this work.
|
// we may want to make some of this work.
|
||||||
|
|
||||||
print("SEL: \(selector)")
|
print("SEL: \(selector)")
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func translateFlags(_ flags: NSEvent.ModifierFlags) -> ghostty_input_mods_e {
|
private static func translateFlags(_ flags: NSEvent.ModifierFlags) -> ghostty_input_mods_e {
|
||||||
var mods: UInt32 = GHOSTTY_MODS_NONE.rawValue
|
var mods: UInt32 = GHOSTTY_MODS_NONE.rawValue
|
||||||
|
|
||||||
if (flags.contains(.shift)) { mods |= GHOSTTY_MODS_SHIFT.rawValue }
|
if (flags.contains(.shift)) { mods |= GHOSTTY_MODS_SHIFT.rawValue }
|
||||||
if (flags.contains(.control)) { mods |= GHOSTTY_MODS_CTRL.rawValue }
|
if (flags.contains(.control)) { mods |= GHOSTTY_MODS_CTRL.rawValue }
|
||||||
if (flags.contains(.option)) { mods |= GHOSTTY_MODS_ALT.rawValue }
|
if (flags.contains(.option)) { mods |= GHOSTTY_MODS_ALT.rawValue }
|
||||||
if (flags.contains(.command)) { mods |= GHOSTTY_MODS_SUPER.rawValue }
|
if (flags.contains(.command)) { mods |= GHOSTTY_MODS_SUPER.rawValue }
|
||||||
if (flags.contains(.capsLock)) { mods |= GHOSTTY_MODS_CAPS.rawValue }
|
if (flags.contains(.capsLock)) { mods |= GHOSTTY_MODS_CAPS.rawValue }
|
||||||
|
|
||||||
// Handle sided input. We can't tell that both are pressed in the
|
// Handle sided input. We can't tell that both are pressed in the
|
||||||
// Ghostty structure but thats okay -- we don't use that information.
|
// Ghostty structure but thats okay -- we don't use that information.
|
||||||
let rawFlags = flags.rawValue
|
let rawFlags = flags.rawValue
|
||||||
if (rawFlags & UInt(NX_DEVICERSHIFTKEYMASK) != 0) { mods |= GHOSTTY_MODS_SHIFT_RIGHT.rawValue }
|
if (rawFlags & UInt(NX_DEVICERSHIFTKEYMASK) != 0) { mods |= GHOSTTY_MODS_SHIFT_RIGHT.rawValue }
|
||||||
if (rawFlags & UInt(NX_DEVICERCTLKEYMASK) != 0) { mods |= GHOSTTY_MODS_CTRL_RIGHT.rawValue }
|
if (rawFlags & UInt(NX_DEVICERCTLKEYMASK) != 0) { mods |= GHOSTTY_MODS_CTRL_RIGHT.rawValue }
|
||||||
if (rawFlags & UInt(NX_DEVICERALTKEYMASK) != 0) { mods |= GHOSTTY_MODS_ALT_RIGHT.rawValue }
|
if (rawFlags & UInt(NX_DEVICERALTKEYMASK) != 0) { mods |= GHOSTTY_MODS_ALT_RIGHT.rawValue }
|
||||||
if (rawFlags & UInt(NX_DEVICERCMDKEYMASK) != 0) { mods |= GHOSTTY_MODS_SUPER_RIGHT.rawValue }
|
if (rawFlags & UInt(NX_DEVICERCMDKEYMASK) != 0) { mods |= GHOSTTY_MODS_SUPER_RIGHT.rawValue }
|
||||||
|
|
||||||
return ghostty_input_mods_e(mods)
|
return ghostty_input_mods_e(mods)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mapping of event keyCode to ghostty input key values. This is cribbed from
|
// 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!
|
// glfw mostly since we started as a glfw-based app way back in the day!
|
||||||
static let keycodes: [UInt16 : ghostty_input_key_e] = [
|
static let keycodes: [UInt16 : ghostty_input_key_e] = [
|
||||||
@ -742,7 +814,7 @@ extension FocusedValues {
|
|||||||
get { self[FocusedGhosttySurface.self] }
|
get { self[FocusedGhosttySurface.self] }
|
||||||
set { self[FocusedGhosttySurface.self] = newValue }
|
set { self[FocusedGhosttySurface.self] = newValue }
|
||||||
}
|
}
|
||||||
|
|
||||||
struct FocusedGhosttySurface: FocusedValueKey {
|
struct FocusedGhosttySurface: FocusedValueKey {
|
||||||
typealias Value = Ghostty.SurfaceView
|
typealias Value = Ghostty.SurfaceView
|
||||||
}
|
}
|
||||||
@ -753,7 +825,7 @@ extension FocusedValues {
|
|||||||
get { self[FocusedGhosttySurfaceTitle.self] }
|
get { self[FocusedGhosttySurfaceTitle.self] }
|
||||||
set { self[FocusedGhosttySurfaceTitle.self] = newValue }
|
set { self[FocusedGhosttySurfaceTitle.self] = newValue }
|
||||||
}
|
}
|
||||||
|
|
||||||
struct FocusedGhosttySurfaceTitle: FocusedValueKey {
|
struct FocusedGhosttySurfaceTitle: FocusedValueKey {
|
||||||
typealias Value = String
|
typealias Value = String
|
||||||
}
|
}
|
||||||
@ -764,7 +836,7 @@ extension FocusedValues {
|
|||||||
get { self[FocusedGhosttySurfaceZoomed.self] }
|
get { self[FocusedGhosttySurfaceZoomed.self] }
|
||||||
set { self[FocusedGhosttySurfaceZoomed.self] = newValue }
|
set { self[FocusedGhosttySurfaceZoomed.self] = newValue }
|
||||||
}
|
}
|
||||||
|
|
||||||
struct FocusedGhosttySurfaceZoomed: FocusedValueKey {
|
struct FocusedGhosttySurfaceZoomed: FocusedValueKey {
|
||||||
typealias Value = Bool
|
typealias Value = Bool
|
||||||
}
|
}
|
||||||
|
@ -525,6 +525,11 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
|
|||||||
try self.rt_surface.setTitle(slice);
|
try self.rt_surface.setTitle(slice);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
.set_mouse_shape => |shape| {
|
||||||
|
log.debug("changing mouse shape: {}", .{shape});
|
||||||
|
try self.rt_surface.setMouseShape(shape);
|
||||||
|
},
|
||||||
|
|
||||||
.cell_size => |size| try self.setCellSize(size),
|
.cell_size => |size| try self.setCellSize(size),
|
||||||
|
|
||||||
.clipboard_read => |kind| try self.clipboardRead(kind),
|
.clipboard_read => |kind| try self.clipboardRead(kind),
|
||||||
|
@ -11,6 +11,7 @@ const Allocator = std.mem.Allocator;
|
|||||||
const objc = @import("objc");
|
const objc = @import("objc");
|
||||||
const apprt = @import("../apprt.zig");
|
const apprt = @import("../apprt.zig");
|
||||||
const input = @import("../input.zig");
|
const input = @import("../input.zig");
|
||||||
|
const terminal = @import("../terminal/main.zig");
|
||||||
const CoreApp = @import("../App.zig");
|
const CoreApp = @import("../App.zig");
|
||||||
const CoreSurface = @import("../Surface.zig");
|
const CoreSurface = @import("../Surface.zig");
|
||||||
const configpkg = @import("../config.zig");
|
const configpkg = @import("../config.zig");
|
||||||
@ -48,6 +49,9 @@ pub const App = struct {
|
|||||||
/// Called to set the title of the window.
|
/// Called to set the title of the window.
|
||||||
set_title: *const fn (SurfaceUD, [*]const u8) callconv(.C) void,
|
set_title: *const fn (SurfaceUD, [*]const u8) callconv(.C) void,
|
||||||
|
|
||||||
|
/// Called to set the cursor shape.
|
||||||
|
set_mouse_shape: *const fn (SurfaceUD, terminal.MouseShape) callconv(.C) void,
|
||||||
|
|
||||||
/// Read the clipboard value. The return value must be preserved
|
/// Read the clipboard value. The return value must be preserved
|
||||||
/// by the host until the next call. If there is no valid clipboard
|
/// by the host until the next call. If there is no valid clipboard
|
||||||
/// value then this should return null.
|
/// value then this should return null.
|
||||||
@ -310,6 +314,13 @@ pub const Surface = struct {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn setMouseShape(self: *Surface, shape: terminal.MouseShape) !void {
|
||||||
|
self.app.opts.set_mouse_shape(
|
||||||
|
self.opts.userdata,
|
||||||
|
shape,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn supportsClipboard(
|
pub fn supportsClipboard(
|
||||||
self: *const Surface,
|
self: *const Surface,
|
||||||
clipboard_type: apprt.Clipboard,
|
clipboard_type: apprt.Clipboard,
|
||||||
|
@ -15,6 +15,7 @@ const objc = @import("objc");
|
|||||||
const input = @import("../input.zig");
|
const input = @import("../input.zig");
|
||||||
const internal_os = @import("../os/main.zig");
|
const internal_os = @import("../os/main.zig");
|
||||||
const renderer = @import("../renderer.zig");
|
const renderer = @import("../renderer.zig");
|
||||||
|
const terminal = @import("../terminal/main.zig");
|
||||||
const Renderer = renderer.Renderer;
|
const Renderer = renderer.Renderer;
|
||||||
const apprt = @import("../apprt.zig");
|
const apprt = @import("../apprt.zig");
|
||||||
const CoreApp = @import("../App.zig");
|
const CoreApp = @import("../App.zig");
|
||||||
@ -275,7 +276,7 @@ pub const Surface = struct {
|
|||||||
window: glfw.Window,
|
window: glfw.Window,
|
||||||
|
|
||||||
/// The glfw mouse cursor handle.
|
/// The glfw mouse cursor handle.
|
||||||
cursor: glfw.Cursor,
|
cursor: ?glfw.Cursor,
|
||||||
|
|
||||||
/// The app we're part of
|
/// The app we're part of
|
||||||
app: *App,
|
app: *App,
|
||||||
@ -335,16 +336,6 @@ pub const Surface = struct {
|
|||||||
nswindow.setProperty("tabbingIdentifier", app.darwin.tabbing_id);
|
nswindow.setProperty("tabbingIdentifier", app.darwin.tabbing_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the cursor
|
|
||||||
const cursor = glfw.Cursor.createStandard(.ibeam) orelse return glfw.mustGetErrorCode();
|
|
||||||
errdefer cursor.destroy();
|
|
||||||
if ((comptime !builtin.target.isDarwin()) or internal_os.macosVersionAtLeast(13, 0, 0)) {
|
|
||||||
// We only set our cursor if we're NOT on Mac, or if we are then the
|
|
||||||
// macOS version is >= 13 (Ventura). On prior versions, glfw crashes
|
|
||||||
// since we use a tab group.
|
|
||||||
win.setCursor(cursor);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set our callbacks
|
// Set our callbacks
|
||||||
win.setUserPointer(&self.core_surface);
|
win.setUserPointer(&self.core_surface);
|
||||||
win.setSizeCallback(sizeCallback);
|
win.setSizeCallback(sizeCallback);
|
||||||
@ -360,11 +351,14 @@ pub const Surface = struct {
|
|||||||
self.* = .{
|
self.* = .{
|
||||||
.app = app,
|
.app = app,
|
||||||
.window = win,
|
.window = win,
|
||||||
.cursor = cursor,
|
.cursor = null,
|
||||||
.core_surface = undefined,
|
.core_surface = undefined,
|
||||||
};
|
};
|
||||||
errdefer self.* = undefined;
|
errdefer self.* = undefined;
|
||||||
|
|
||||||
|
// Initialize our cursor
|
||||||
|
try self.setMouseShape(.text);
|
||||||
|
|
||||||
// Add ourselves to the list of surfaces on the app.
|
// Add ourselves to the list of surfaces on the app.
|
||||||
try app.app.addSurface(self);
|
try app.app.addSurface(self);
|
||||||
errdefer app.app.deleteSurface(self);
|
errdefer app.app.deleteSurface(self);
|
||||||
@ -425,7 +419,10 @@ pub const Surface = struct {
|
|||||||
// We can now safely destroy our windows. We have to do this BEFORE
|
// We can now safely destroy our windows. We have to do this BEFORE
|
||||||
// setting up the new focused window below.
|
// setting up the new focused window below.
|
||||||
self.window.destroy();
|
self.window.destroy();
|
||||||
self.cursor.destroy();
|
if (self.cursor) |c| {
|
||||||
|
c.destroy();
|
||||||
|
self.cursor = null;
|
||||||
|
}
|
||||||
|
|
||||||
// If we have a tabgroup set, we want to manually focus the next window.
|
// If we have a tabgroup set, we want to manually focus the next window.
|
||||||
// We should NOT have to do this usually, see the comments above.
|
// We should NOT have to do this usually, see the comments above.
|
||||||
@ -508,6 +505,43 @@ pub const Surface = struct {
|
|||||||
self.window.setTitle(slice.ptr);
|
self.window.setTitle(slice.ptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the shape of the cursor.
|
||||||
|
pub fn setMouseShape(self: *Surface, shape: terminal.MouseShape) !void {
|
||||||
|
if ((comptime builtin.target.isDarwin()) and
|
||||||
|
!internal_os.macosVersionAtLeast(13, 0, 0))
|
||||||
|
{
|
||||||
|
// We only set our cursor if we're NOT on Mac, or if we are then the
|
||||||
|
// macOS version is >= 13 (Ventura). On prior versions, glfw crashes
|
||||||
|
// since we use a tab group.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const new = glfw.Cursor.createStandard(switch (shape) {
|
||||||
|
.default => .arrow,
|
||||||
|
.text => .ibeam,
|
||||||
|
.crosshair => .crosshair,
|
||||||
|
.pointer => .pointing_hand,
|
||||||
|
.ew_resize => .resize_ew,
|
||||||
|
.ns_resize => .resize_ns,
|
||||||
|
.nwse_resize => .resize_nwse,
|
||||||
|
.nesw_resize => .resize_nesw,
|
||||||
|
.all_scroll => .resize_all,
|
||||||
|
.not_allowed => .not_allowed,
|
||||||
|
else => return, // unsupported, ignore
|
||||||
|
}) orelse {
|
||||||
|
const err = glfw.mustGetErrorCode();
|
||||||
|
log.warn("error creating cursor: {}", .{err});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
errdefer new.destroy();
|
||||||
|
|
||||||
|
// Set our cursor before we destroy the old one
|
||||||
|
self.window.setCursor(new);
|
||||||
|
|
||||||
|
if (self.cursor) |c| c.destroy();
|
||||||
|
self.cursor = new;
|
||||||
|
}
|
||||||
|
|
||||||
/// Read the clipboard. The windowing system is responsible for allocating
|
/// Read the clipboard. The windowing system is responsible for allocating
|
||||||
/// a buffer as necessary. This should be a stable pointer until the next
|
/// a buffer as necessary. This should be a stable pointer until the next
|
||||||
/// time getClipboardString is called.
|
/// time getClipboardString is called.
|
||||||
|
@ -8,6 +8,7 @@ const glfw = @import("glfw");
|
|||||||
const apprt = @import("../apprt.zig");
|
const apprt = @import("../apprt.zig");
|
||||||
const font = @import("../font/main.zig");
|
const font = @import("../font/main.zig");
|
||||||
const input = @import("../input.zig");
|
const input = @import("../input.zig");
|
||||||
|
const terminal = @import("../terminal/main.zig");
|
||||||
const CoreApp = @import("../App.zig");
|
const CoreApp = @import("../App.zig");
|
||||||
const CoreSurface = @import("../Surface.zig");
|
const CoreSurface = @import("../Surface.zig");
|
||||||
const configpkg = @import("../config.zig");
|
const configpkg = @import("../config.zig");
|
||||||
@ -44,9 +45,6 @@ pub const App = struct {
|
|||||||
app: *c.GtkApplication,
|
app: *c.GtkApplication,
|
||||||
ctx: *c.GMainContext,
|
ctx: *c.GMainContext,
|
||||||
|
|
||||||
cursor_default: *c.GdkCursor,
|
|
||||||
cursor_ibeam: *c.GdkCursor,
|
|
||||||
|
|
||||||
/// This is set to false when the main loop should exit.
|
/// This is set to false when the main loop should exit.
|
||||||
running: bool = true,
|
running: bool = true,
|
||||||
|
|
||||||
@ -125,19 +123,11 @@ pub const App = struct {
|
|||||||
// https://gitlab.gnome.org/GNOME/glib/-/blob/bd2ccc2f69ecfd78ca3f34ab59e42e2b462bad65/gio/gapplication.c#L2302
|
// https://gitlab.gnome.org/GNOME/glib/-/blob/bd2ccc2f69ecfd78ca3f34ab59e42e2b462bad65/gio/gapplication.c#L2302
|
||||||
c.g_application_activate(gapp);
|
c.g_application_activate(gapp);
|
||||||
|
|
||||||
// Get our cursors
|
|
||||||
const cursor_default = c.gdk_cursor_new_from_name("default", null).?;
|
|
||||||
errdefer c.g_object_unref(cursor_default);
|
|
||||||
const cursor_ibeam = c.gdk_cursor_new_from_name("text", cursor_default).?;
|
|
||||||
errdefer c.g_object_unref(cursor_ibeam);
|
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.core_app = core_app,
|
.core_app = core_app,
|
||||||
.app = app,
|
.app = app,
|
||||||
.config = config,
|
.config = config,
|
||||||
.ctx = ctx,
|
.ctx = ctx,
|
||||||
.cursor_default = cursor_default,
|
|
||||||
.cursor_ibeam = cursor_ibeam,
|
|
||||||
|
|
||||||
// If we are NOT the primary instance, then we never want to run.
|
// If we are NOT the primary instance, then we never want to run.
|
||||||
// This means that another instance of the GTK app is running and
|
// This means that another instance of the GTK app is running and
|
||||||
@ -154,9 +144,6 @@ pub const App = struct {
|
|||||||
c.g_main_context_release(self.ctx);
|
c.g_main_context_release(self.ctx);
|
||||||
c.g_object_unref(self.app);
|
c.g_object_unref(self.app);
|
||||||
|
|
||||||
c.g_object_unref(self.cursor_ibeam);
|
|
||||||
c.g_object_unref(self.cursor_default);
|
|
||||||
|
|
||||||
self.config.deinit();
|
self.config.deinit();
|
||||||
|
|
||||||
glfw.terminate();
|
glfw.terminate();
|
||||||
@ -722,6 +709,9 @@ pub const Surface = struct {
|
|||||||
/// Our GTK area
|
/// Our GTK area
|
||||||
gl_area: *c.GtkGLArea,
|
gl_area: *c.GtkGLArea,
|
||||||
|
|
||||||
|
/// Any active cursor we may have
|
||||||
|
cursor: ?*c.GdkCursor = null,
|
||||||
|
|
||||||
/// Our title label (if there is one).
|
/// Our title label (if there is one).
|
||||||
title: Title,
|
title: Title,
|
||||||
|
|
||||||
@ -798,9 +788,6 @@ pub const Surface = struct {
|
|||||||
c.gtk_widget_set_focusable(widget, 1);
|
c.gtk_widget_set_focusable(widget, 1);
|
||||||
c.gtk_widget_set_focus_on_click(widget, 1);
|
c.gtk_widget_set_focus_on_click(widget, 1);
|
||||||
|
|
||||||
// When we're over the widget, set the cursor to the ibeam
|
|
||||||
c.gtk_widget_set_cursor(widget, app.cursor_ibeam);
|
|
||||||
|
|
||||||
// Build our result
|
// Build our result
|
||||||
self.* = .{
|
self.* = .{
|
||||||
.app = app,
|
.app = app,
|
||||||
@ -880,6 +867,8 @@ pub const Surface = struct {
|
|||||||
// Free all our GTK stuff
|
// Free all our GTK stuff
|
||||||
c.g_object_unref(self.im_context);
|
c.g_object_unref(self.im_context);
|
||||||
c.g_value_unset(&self.clipboard);
|
c.g_value_unset(&self.clipboard);
|
||||||
|
|
||||||
|
if (self.cursor) |cursor| c.g_object_unref(cursor);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render(self: *Surface) !void {
|
fn render(self: *Surface) !void {
|
||||||
@ -998,6 +987,61 @@ pub const Surface = struct {
|
|||||||
// ));
|
// ));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn setMouseShape(
|
||||||
|
self: *Surface,
|
||||||
|
shape: terminal.MouseShape,
|
||||||
|
) !void {
|
||||||
|
const name: [:0]const u8 = switch (shape) {
|
||||||
|
.default => "default",
|
||||||
|
.help => "help",
|
||||||
|
.pointer => "pointer",
|
||||||
|
.context_menu => "context-menu",
|
||||||
|
.progress => "progress",
|
||||||
|
.wait => "wait",
|
||||||
|
.cell => "cell",
|
||||||
|
.crosshair => "crosshair",
|
||||||
|
.text => "text",
|
||||||
|
.vertical_text => "vertical-text",
|
||||||
|
.alias => "alias",
|
||||||
|
.copy => "copy",
|
||||||
|
.no_drop => "no-drop",
|
||||||
|
.move => "move",
|
||||||
|
.not_allowed => "not-allowed",
|
||||||
|
.grab => "grab",
|
||||||
|
.grabbing => "grabbing",
|
||||||
|
.all_scroll => "all-scroll",
|
||||||
|
.col_resize => "col-resize",
|
||||||
|
.row_resize => "row-resize",
|
||||||
|
.n_resize => "n-resize",
|
||||||
|
.e_resize => "e-resize",
|
||||||
|
.s_resize => "s-resize",
|
||||||
|
.w_resize => "w-resize",
|
||||||
|
.ne_resize => "ne-resize",
|
||||||
|
.nw_resize => "nw-resize",
|
||||||
|
.se_resize => "se-resize",
|
||||||
|
.sw_resize => "sw-resize",
|
||||||
|
.ew_resize => "ew-resize",
|
||||||
|
.ns_resize => "ns-resize",
|
||||||
|
.nesw_resize => "nesw-resize",
|
||||||
|
.nwse_resize => "nwse-resize",
|
||||||
|
.zoom_in => "zoom-in",
|
||||||
|
.zoom_out => "zoom-out",
|
||||||
|
};
|
||||||
|
|
||||||
|
const cursor = c.gdk_cursor_new_from_name(name.ptr, null) orelse {
|
||||||
|
log.warn("unsupported cursor name={s}", .{name});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
errdefer c.g_object_unref(cursor);
|
||||||
|
|
||||||
|
// Set our new cursor
|
||||||
|
c.gtk_widget_set_cursor(@ptrCast(self.gl_area), cursor);
|
||||||
|
|
||||||
|
// Free our existing cursor
|
||||||
|
if (self.cursor) |old| c.g_object_unref(old);
|
||||||
|
self.cursor = cursor;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn getClipboardString(
|
pub fn getClipboardString(
|
||||||
self: *Surface,
|
self: *Surface,
|
||||||
clipboard_type: apprt.Clipboard,
|
clipboard_type: apprt.Clipboard,
|
||||||
|
@ -2,6 +2,7 @@ const App = @import("../App.zig");
|
|||||||
const Surface = @import("../Surface.zig");
|
const Surface = @import("../Surface.zig");
|
||||||
const renderer = @import("../renderer.zig");
|
const renderer = @import("../renderer.zig");
|
||||||
const termio = @import("../termio.zig");
|
const termio = @import("../termio.zig");
|
||||||
|
const terminal = @import("../terminal/main.zig");
|
||||||
const Config = @import("../config.zig").Config;
|
const Config = @import("../config.zig").Config;
|
||||||
|
|
||||||
/// The message types that can be sent to a single surface.
|
/// The message types that can be sent to a single surface.
|
||||||
@ -16,6 +17,9 @@ pub const Message = union(enum) {
|
|||||||
/// of any length
|
/// of any length
|
||||||
set_title: [256]u8,
|
set_title: [256]u8,
|
||||||
|
|
||||||
|
/// Set the mouse shape.
|
||||||
|
set_mouse_shape: terminal.MouseShape,
|
||||||
|
|
||||||
/// Change the cell size.
|
/// Change the cell size.
|
||||||
cell_size: renderer.CellSize,
|
cell_size: renderer.CellSize,
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ pub const parse_table = @import("parse_table.zig");
|
|||||||
pub const Charset = charsets.Charset;
|
pub const Charset = charsets.Charset;
|
||||||
pub const CharsetSlot = charsets.Slots;
|
pub const CharsetSlot = charsets.Slots;
|
||||||
pub const CharsetActiveSlot = charsets.ActiveSlot;
|
pub const CharsetActiveSlot = charsets.ActiveSlot;
|
||||||
|
pub const MouseShape = @import("mouse_shape.zig").MouseShape;
|
||||||
pub const Terminal = @import("Terminal.zig");
|
pub const Terminal = @import("Terminal.zig");
|
||||||
pub const Parser = @import("Parser.zig");
|
pub const Parser = @import("Parser.zig");
|
||||||
pub const Selection = @import("Selection.zig");
|
pub const Selection = @import("Selection.zig");
|
||||||
|
115
src/terminal/mouse_shape.zig
Normal file
115
src/terminal/mouse_shape.zig
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
/// The possible cursor shapes. Not all app runtimes support these shapes.
|
||||||
|
/// The shapes are always based on the W3C supported cursor styles so we
|
||||||
|
/// can have a cross platform list.
|
||||||
|
//
|
||||||
|
// Must be kept in sync with ghostty_cursor_shape_e
|
||||||
|
pub const MouseShape = enum(c_int) {
|
||||||
|
default,
|
||||||
|
context_menu,
|
||||||
|
help,
|
||||||
|
pointer,
|
||||||
|
progress,
|
||||||
|
wait,
|
||||||
|
cell,
|
||||||
|
crosshair,
|
||||||
|
text,
|
||||||
|
vertical_text,
|
||||||
|
alias,
|
||||||
|
copy,
|
||||||
|
move,
|
||||||
|
no_drop,
|
||||||
|
not_allowed,
|
||||||
|
grab,
|
||||||
|
grabbing,
|
||||||
|
all_scroll,
|
||||||
|
col_resize,
|
||||||
|
row_resize,
|
||||||
|
n_resize,
|
||||||
|
e_resize,
|
||||||
|
s_resize,
|
||||||
|
w_resize,
|
||||||
|
ne_resize,
|
||||||
|
nw_resize,
|
||||||
|
se_resize,
|
||||||
|
sw_resize,
|
||||||
|
ew_resize,
|
||||||
|
ns_resize,
|
||||||
|
nesw_resize,
|
||||||
|
nwse_resize,
|
||||||
|
zoom_in,
|
||||||
|
zoom_out,
|
||||||
|
|
||||||
|
/// Build cursor shape from string or null if its unknown.
|
||||||
|
pub fn fromString(v: []const u8) ?MouseShape {
|
||||||
|
return string_map.get(v);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const string_map = std.ComptimeStringMap(MouseShape, .{
|
||||||
|
// W3C
|
||||||
|
.{ "default", .default },
|
||||||
|
.{ "context-menu", .context_menu },
|
||||||
|
.{ "help", .help },
|
||||||
|
.{ "pointer", .pointer },
|
||||||
|
.{ "progress", .progress },
|
||||||
|
.{ "wait", .wait },
|
||||||
|
.{ "cell", .cell },
|
||||||
|
.{ "crosshair", .crosshair },
|
||||||
|
.{ "text", .text },
|
||||||
|
.{ "vertical-text", .vertical_text },
|
||||||
|
.{ "alias", .alias },
|
||||||
|
.{ "copy", .copy },
|
||||||
|
.{ "move", .move },
|
||||||
|
.{ "no-drop", .no_drop },
|
||||||
|
.{ "not-allowed", .not_allowed },
|
||||||
|
.{ "grab", .grab },
|
||||||
|
.{ "grabbing", .grabbing },
|
||||||
|
.{ "all-scroll", .all_scroll },
|
||||||
|
.{ "col-resize", .col_resize },
|
||||||
|
.{ "row-resize", .row_resize },
|
||||||
|
.{ "n-resize", .n_resize },
|
||||||
|
.{ "e-resize", .e_resize },
|
||||||
|
.{ "s-resize", .s_resize },
|
||||||
|
.{ "w-resize", .w_resize },
|
||||||
|
.{ "ne-resize", .ne_resize },
|
||||||
|
.{ "nw-resize", .nw_resize },
|
||||||
|
.{ "se-resize", .se_resize },
|
||||||
|
.{ "sw-resize", .sw_resize },
|
||||||
|
.{ "ew-resize", .ew_resize },
|
||||||
|
.{ "ns-resize", .ns_resize },
|
||||||
|
.{ "nesw-resize", .nesw_resize },
|
||||||
|
.{ "nwse-resize", .nwse_resize },
|
||||||
|
.{ "zoom-in", .zoom_in },
|
||||||
|
.{ "zoom-out", .zoom_out },
|
||||||
|
|
||||||
|
// xterm/foot
|
||||||
|
.{ "left_ptr", .default },
|
||||||
|
.{ "question_arrow", .help },
|
||||||
|
.{ "hand", .pointer },
|
||||||
|
.{ "left_ptr_watch", .progress },
|
||||||
|
.{ "watch", .wait },
|
||||||
|
.{ "cross", .crosshair },
|
||||||
|
.{ "xterm", .text },
|
||||||
|
.{ "dnd-link", .alias },
|
||||||
|
.{ "dnd-copy", .copy },
|
||||||
|
.{ "dnd-move", .move },
|
||||||
|
.{ "dnd-no-drop", .no_drop },
|
||||||
|
.{ "crossed_circle", .not_allowed },
|
||||||
|
.{ "hand1", .grab },
|
||||||
|
.{ "right_side", .e_resize },
|
||||||
|
.{ "top_side", .n_resize },
|
||||||
|
.{ "top_right_corner", .ne_resize },
|
||||||
|
.{ "top_left_corner", .nw_resize },
|
||||||
|
.{ "bottom_side", .s_resize },
|
||||||
|
.{ "bottom_right_corner", .se_resize },
|
||||||
|
.{ "bottom_left_corner", .sw_resize },
|
||||||
|
.{ "left_side", .w_resize },
|
||||||
|
.{ "fleur", .all_scroll },
|
||||||
|
});
|
||||||
|
|
||||||
|
test "cursor shape from string" {
|
||||||
|
const testing = std.testing;
|
||||||
|
try testing.expectEqual(MouseShape.default, MouseShape.fromString("default").?);
|
||||||
|
}
|
@ -83,6 +83,14 @@ pub const Command = union(enum) {
|
|||||||
/// be a file URL but it is up to the caller to utilize this value.
|
/// be a file URL but it is up to the caller to utilize this value.
|
||||||
value: []const u8,
|
value: []const u8,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// OSC 22. Set the mouse shape. There doesn't seem to be a standard
|
||||||
|
/// naming scheme for cursors but it looks like terminals such as Foot
|
||||||
|
/// are moving towards using the W3C CSS cursor names. For OSC parsing,
|
||||||
|
/// we just parse whatever string is given.
|
||||||
|
mouse_shape: struct {
|
||||||
|
value: []const u8,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Parser = struct {
|
pub const Parser = struct {
|
||||||
@ -130,6 +138,7 @@ pub const Parser = struct {
|
|||||||
@"13",
|
@"13",
|
||||||
@"133",
|
@"133",
|
||||||
@"2",
|
@"2",
|
||||||
|
@"22",
|
||||||
@"5",
|
@"5",
|
||||||
@"52",
|
@"52",
|
||||||
@"7",
|
@"7",
|
||||||
@ -226,6 +235,7 @@ pub const Parser = struct {
|
|||||||
},
|
},
|
||||||
|
|
||||||
.@"2" => switch (c) {
|
.@"2" => switch (c) {
|
||||||
|
'2' => self.state = .@"22",
|
||||||
';' => {
|
';' => {
|
||||||
self.command = .{ .change_window_title = undefined };
|
self.command = .{ .change_window_title = undefined };
|
||||||
|
|
||||||
@ -236,6 +246,17 @@ pub const Parser = struct {
|
|||||||
else => self.state = .invalid,
|
else => self.state = .invalid,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
.@"22" => switch (c) {
|
||||||
|
';' => {
|
||||||
|
self.command = .{ .mouse_shape = undefined };
|
||||||
|
|
||||||
|
self.state = .string;
|
||||||
|
self.temp_state = .{ .str = &self.command.mouse_shape.value };
|
||||||
|
self.buf_start = self.buf_idx;
|
||||||
|
},
|
||||||
|
else => self.state = .invalid,
|
||||||
|
},
|
||||||
|
|
||||||
.@"5" => switch (c) {
|
.@"5" => switch (c) {
|
||||||
'2' => self.state = .@"52",
|
'2' => self.state = .@"52",
|
||||||
else => self.state = .invalid,
|
else => self.state = .invalid,
|
||||||
@ -642,6 +663,19 @@ test "OSC: report pwd" {
|
|||||||
try testing.expect(std.mem.eql(u8, "file:///tmp/example", cmd.report_pwd.value));
|
try testing.expect(std.mem.eql(u8, "file:///tmp/example", cmd.report_pwd.value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "OSC: pointer cursor" {
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
var p: Parser = .{};
|
||||||
|
|
||||||
|
const input = "22;pointer";
|
||||||
|
for (input) |ch| p.next(ch);
|
||||||
|
|
||||||
|
const cmd = p.end().?;
|
||||||
|
try testing.expect(cmd == .mouse_shape);
|
||||||
|
try testing.expect(std.mem.eql(u8, "pointer", cmd.mouse_shape.value));
|
||||||
|
}
|
||||||
|
|
||||||
test "OSC: report pwd empty" {
|
test "OSC: report pwd empty" {
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ const modes = @import("modes.zig");
|
|||||||
const osc = @import("osc.zig");
|
const osc = @import("osc.zig");
|
||||||
const sgr = @import("sgr.zig");
|
const sgr = @import("sgr.zig");
|
||||||
const trace = @import("tracy").trace;
|
const trace = @import("tracy").trace;
|
||||||
|
const MouseShape = @import("mouse_shape.zig").MouseShape;
|
||||||
|
|
||||||
const log = std.log.scoped(.stream);
|
const log = std.log.scoped(.stream);
|
||||||
|
|
||||||
@ -849,6 +850,17 @@ pub fn Stream(comptime Handler: type) type {
|
|||||||
} else log.warn("unimplemented OSC callback: {}", .{cmd});
|
} else log.warn("unimplemented OSC callback: {}", .{cmd});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
.mouse_shape => |v| {
|
||||||
|
if (@hasDecl(T, "setMouseShape")) {
|
||||||
|
const shape = MouseShape.fromString(v.value) orelse {
|
||||||
|
log.warn("unknown cursor shape: {s}", .{v.value});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
try self.handler.setMouseShape(shape);
|
||||||
|
} else log.warn("unimplemented OSC callback: {}", .{cmd});
|
||||||
|
},
|
||||||
|
|
||||||
else => if (@hasDecl(T, "oscUnimplemented"))
|
else => if (@hasDecl(T, "oscUnimplemented"))
|
||||||
try self.handler.oscUnimplemented(cmd)
|
try self.handler.oscUnimplemented(cmd)
|
||||||
else
|
else
|
||||||
|
@ -1682,6 +1682,15 @@ const StreamHandler = struct {
|
|||||||
}, .{ .forever = {} });
|
}, .{ .forever = {} });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn setMouseShape(
|
||||||
|
self: *StreamHandler,
|
||||||
|
shape: terminal.MouseShape,
|
||||||
|
) !void {
|
||||||
|
_ = self.ev.surface_mailbox.push(.{
|
||||||
|
.set_mouse_shape = shape,
|
||||||
|
}, .{ .forever = {} });
|
||||||
|
}
|
||||||
|
|
||||||
pub fn clipboardContents(self: *StreamHandler, kind: u8, data: []const u8) !void {
|
pub fn clipboardContents(self: *StreamHandler, kind: u8, data: []const u8) !void {
|
||||||
// Note: we ignore the "kind" field and always use the standard clipboard.
|
// Note: we ignore the "kind" field and always use the standard clipboard.
|
||||||
// iTerm also appears to do this but other terminals seem to only allow
|
// iTerm also appears to do this but other terminals seem to only allow
|
||||||
|
Reference in New Issue
Block a user