mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 00:36:07 +03:00
Merge pull request #613 from mitchellh/macos-drag-drop
macos: support dropping folders onto dock icon
This commit is contained in:
@ -309,6 +309,7 @@ typedef struct {
|
|||||||
void *nsview;
|
void *nsview;
|
||||||
double scale_factor;
|
double scale_factor;
|
||||||
uint16_t font_size;
|
uint16_t font_size;
|
||||||
|
const char *working_directory;
|
||||||
} ghostty_surface_config_s;
|
} ghostty_surface_config_s;
|
||||||
|
|
||||||
typedef void (*ghostty_runtime_wakeup_cb)(void *);
|
typedef void (*ghostty_runtime_wakeup_cb)(void *);
|
||||||
|
@ -2,6 +2,21 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>CFBundleDocumentTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>Folders</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Alternate</string>
|
||||||
|
<key>LSItemContentTypes</key>
|
||||||
|
<array>
|
||||||
|
<string>public.directory</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
<key>LSEnvironment</key>
|
<key>LSEnvironment</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>GHOSTTY_MAC_APP</key>
|
<key>GHOSTTY_MAC_APP</key>
|
||||||
|
@ -138,6 +138,38 @@ class AppDelegate: NSObject, ObservableObject, NSApplicationDelegate, GhosttyApp
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func application(_ sender: NSApplication, openFile filename: String) -> Bool {
|
||||||
|
// Ghostty will validate as well but we can avoid creating an entirely new
|
||||||
|
// surface by doing our own validation here. We can also show a useful error
|
||||||
|
// this way.
|
||||||
|
var isDirectory = ObjCBool(true)
|
||||||
|
guard FileManager.default.fileExists(atPath: filename, isDirectory: &isDirectory) else { return false }
|
||||||
|
guard isDirectory.boolValue else {
|
||||||
|
let alert = NSAlert()
|
||||||
|
alert.messageText = "Dropped File is Not a Directory"
|
||||||
|
alert.informativeText = "Ghostty can currently only open directory paths."
|
||||||
|
alert.addButton(withTitle: "OK")
|
||||||
|
alert.alertStyle = .warning
|
||||||
|
_ = alert.runModal()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build our config
|
||||||
|
var config = Ghostty.SurfaceConfiguration()
|
||||||
|
config.workingDirectory = filename
|
||||||
|
|
||||||
|
// If we don't have a window open through the window manager, we launch
|
||||||
|
// a new window.
|
||||||
|
guard let mainWindow = windowManager.mainWindow else {
|
||||||
|
windowManager.addNewWindow(withBaseConfig: config)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a new tab
|
||||||
|
windowManager.addNewTab(to: mainWindow, withBaseConfig: config)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
/// This is called for the dock right-click menu.
|
/// This is called for the dock right-click menu.
|
||||||
func applicationDockMenu(_ sender: NSApplication) -> NSMenu? {
|
func applicationDockMenu(_ sender: NSApplication) -> NSMenu? {
|
||||||
return dockMenu
|
return dockMenu
|
||||||
|
@ -12,7 +12,7 @@ struct PrimaryView: View {
|
|||||||
let focusedSurfaceWrapper: FocusedSurfaceWrapper
|
let focusedSurfaceWrapper: FocusedSurfaceWrapper
|
||||||
|
|
||||||
// If this is set, this is the base configuration that we build our surface out of.
|
// If this is set, this is the base configuration that we build our surface out of.
|
||||||
let baseConfig: ghostty_surface_config_s?
|
let baseConfig: Ghostty.SurfaceConfiguration?
|
||||||
|
|
||||||
// We need access to our window to know if we're the key window to determine
|
// We need access to our window to know if we're the key window to determine
|
||||||
// if we show the quit confirmation or not.
|
// if we show the quit confirmation or not.
|
||||||
|
@ -24,7 +24,7 @@ class PrimaryWindow: NSWindow {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
static func create(ghostty: Ghostty.AppState, appDelegate: AppDelegate, baseConfig: ghostty_surface_config_s? = nil) -> PrimaryWindow {
|
static func create(ghostty: Ghostty.AppState, appDelegate: AppDelegate, baseConfig: Ghostty.SurfaceConfiguration? = nil) -> PrimaryWindow {
|
||||||
let window = PrimaryWindow(
|
let window = PrimaryWindow(
|
||||||
contentRect: NSRect(x: 0, y: 0, width: 800, height: 600),
|
contentRect: NSRect(x: 0, y: 0, width: 800, height: 600),
|
||||||
styleMask: getStyleMask(renderDecoration: ghostty.windowDecorations),
|
styleMask: getStyleMask(renderDecoration: ghostty.windowDecorations),
|
||||||
|
@ -94,7 +94,7 @@ class PrimaryWindowManager {
|
|||||||
ghostty.newWindow(surface: surface)
|
ghostty.newWindow(surface: surface)
|
||||||
}
|
}
|
||||||
|
|
||||||
func addNewWindow(withBaseConfig config: ghostty_surface_config_s? = nil) {
|
func addNewWindow(withBaseConfig config: Ghostty.SurfaceConfiguration? = nil) {
|
||||||
guard let controller = createWindowController(withBaseConfig: config) else { return }
|
guard let controller = createWindowController(withBaseConfig: config) else { return }
|
||||||
controller.showWindow(self)
|
controller.showWindow(self)
|
||||||
guard let newWindow = addManagedWindow(windowController: controller)?.window else { return }
|
guard let newWindow = addManagedWindow(windowController: controller)?.window else { return }
|
||||||
@ -103,7 +103,7 @@ class PrimaryWindowManager {
|
|||||||
|
|
||||||
@objc private func onNewWindow(notification: SwiftUI.Notification) {
|
@objc private func onNewWindow(notification: SwiftUI.Notification) {
|
||||||
let configAny = notification.userInfo?[Ghostty.Notification.NewSurfaceConfigKey]
|
let configAny = notification.userInfo?[Ghostty.Notification.NewSurfaceConfigKey]
|
||||||
let config = configAny as? ghostty_surface_config_s
|
let config = configAny as? Ghostty.SurfaceConfiguration
|
||||||
|
|
||||||
self.addNewWindow(withBaseConfig: config)
|
self.addNewWindow(withBaseConfig: config)
|
||||||
}
|
}
|
||||||
@ -128,19 +128,19 @@ class PrimaryWindowManager {
|
|||||||
guard let window = surfaceView.window else { return }
|
guard let window = surfaceView.window else { return }
|
||||||
|
|
||||||
let configAny = notification.userInfo?[Ghostty.Notification.NewSurfaceConfigKey]
|
let configAny = notification.userInfo?[Ghostty.Notification.NewSurfaceConfigKey]
|
||||||
let config = configAny as? ghostty_surface_config_s
|
let config = configAny as? Ghostty.SurfaceConfiguration
|
||||||
|
|
||||||
self.addNewTab(to: window, withBaseConfig: config)
|
self.addNewTab(to: window, withBaseConfig: config)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addNewTab(to window: NSWindow, withBaseConfig config: ghostty_surface_config_s? = nil) {
|
func addNewTab(to window: NSWindow, withBaseConfig config: Ghostty.SurfaceConfiguration? = nil) {
|
||||||
guard let controller = createWindowController(withBaseConfig: config, cascade: false) else { return }
|
guard let controller = createWindowController(withBaseConfig: config, cascade: false) else { return }
|
||||||
guard let newWindow = addManagedWindow(windowController: controller)?.window else { return }
|
guard let newWindow = addManagedWindow(windowController: controller)?.window else { return }
|
||||||
window.addTabbedWindow(newWindow, ordered: .above)
|
window.addTabbedWindow(newWindow, ordered: .above)
|
||||||
newWindow.makeKeyAndOrderFront(nil)
|
newWindow.makeKeyAndOrderFront(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createWindowController(withBaseConfig config: ghostty_surface_config_s? = nil, cascade: Bool = true) -> PrimaryWindowController? {
|
private func createWindowController(withBaseConfig config: Ghostty.SurfaceConfiguration? = nil, cascade: Bool = true) -> PrimaryWindowController? {
|
||||||
guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { return nil }
|
guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { return nil }
|
||||||
|
|
||||||
let window = PrimaryWindow.create(ghostty: ghostty, appDelegate: appDelegate, baseConfig: config)
|
let window = PrimaryWindow.create(ghostty: ghostty, appDelegate: appDelegate, baseConfig: config)
|
||||||
|
@ -282,7 +282,7 @@ extension Ghostty {
|
|||||||
guard let surface = self.surfaceUserdata(from: userdata) else { return }
|
guard let surface = self.surfaceUserdata(from: userdata) else { return }
|
||||||
NotificationCenter.default.post(name: Notification.ghosttyNewSplit, object: surface, userInfo: [
|
NotificationCenter.default.post(name: Notification.ghosttyNewSplit, object: surface, userInfo: [
|
||||||
"direction": direction,
|
"direction": direction,
|
||||||
Notification.NewSurfaceConfigKey: config,
|
Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: config),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -439,7 +439,7 @@ extension Ghostty {
|
|||||||
name: Notification.ghosttyNewTab,
|
name: Notification.ghosttyNewTab,
|
||||||
object: surface,
|
object: surface,
|
||||||
userInfo: [
|
userInfo: [
|
||||||
Notification.NewSurfaceConfigKey: config
|
Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: config),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -451,7 +451,7 @@ extension Ghostty {
|
|||||||
name: Notification.ghosttyNewWindow,
|
name: Notification.ghosttyNewWindow,
|
||||||
object: surface,
|
object: surface,
|
||||||
userInfo: [
|
userInfo: [
|
||||||
Notification.NewSurfaceConfigKey: config
|
Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: config),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ extension Ghostty {
|
|||||||
/// split direction by splitting the terminal.
|
/// split direction by splitting the terminal.
|
||||||
struct TerminalSplit: View {
|
struct TerminalSplit: View {
|
||||||
let onClose: (() -> Void)?
|
let onClose: (() -> Void)?
|
||||||
let baseConfig: ghostty_surface_config_s?
|
let baseConfig: SurfaceConfiguration?
|
||||||
|
|
||||||
@Environment(\.ghosttyApp) private var app
|
@Environment(\.ghosttyApp) private var app
|
||||||
|
|
||||||
@ -118,7 +118,7 @@ extension Ghostty {
|
|||||||
@Published var surface: SurfaceView
|
@Published var surface: SurfaceView
|
||||||
|
|
||||||
/// Initialize a new leaf which creates a new terminal surface.
|
/// Initialize a new leaf which creates a new terminal surface.
|
||||||
init(_ app: ghostty_app_t, _ baseConfig: ghostty_surface_config_s?) {
|
init(_ app: ghostty_app_t, _ baseConfig: SurfaceConfiguration?) {
|
||||||
self.app = app
|
self.app = app
|
||||||
self.surface = SurfaceView(app, baseConfig)
|
self.surface = SurfaceView(app, baseConfig)
|
||||||
}
|
}
|
||||||
@ -132,7 +132,7 @@ extension Ghostty {
|
|||||||
/// A container is always initialized from some prior leaf because a split has to originate
|
/// A container is always initialized from some prior leaf because a split has to originate
|
||||||
/// from a non-split value. When initializing, we inherit the leaf's surface and then
|
/// from a non-split value. When initializing, we inherit the leaf's surface and then
|
||||||
/// initialize a new surface for the new pane.
|
/// initialize a new surface for the new pane.
|
||||||
init(from: Leaf, baseConfig: ghostty_surface_config_s? = nil) {
|
init(from: Leaf, baseConfig: SurfaceConfiguration? = nil) {
|
||||||
self.app = from.app
|
self.app = from.app
|
||||||
|
|
||||||
// Initially, both topLeft and bottomRight are in the "nosplit"
|
// Initially, both topLeft and bottomRight are in the "nosplit"
|
||||||
@ -197,7 +197,7 @@ extension Ghostty {
|
|||||||
@State private var node: SplitNode
|
@State private var node: SplitNode
|
||||||
@State private var requestClose: Bool = false
|
@State private var requestClose: Bool = false
|
||||||
let onClose: (() -> Void)?
|
let onClose: (() -> Void)?
|
||||||
let baseConfig: ghostty_surface_config_s?
|
let baseConfig: SurfaceConfiguration?
|
||||||
|
|
||||||
/// Keeps track of whether we're in a zoomed split state or not. If one of the splits we own
|
/// Keeps track of whether we're in a zoomed split state or not. If one of the splits we own
|
||||||
/// is in the zoomed state, we clear our body since we expect a zoomed split to overlay
|
/// is in the zoomed state, we clear our body since we expect a zoomed split to overlay
|
||||||
@ -209,7 +209,7 @@ extension Ghostty {
|
|||||||
init(app: ghostty_app_t,
|
init(app: ghostty_app_t,
|
||||||
zoomedSurface: Binding<SurfaceView?>,
|
zoomedSurface: Binding<SurfaceView?>,
|
||||||
onClose: (() ->Void)? = nil,
|
onClose: (() ->Void)? = nil,
|
||||||
baseConfig: ghostty_surface_config_s? = nil) {
|
baseConfig: SurfaceConfiguration? = nil) {
|
||||||
self.onClose = onClose
|
self.onClose = onClose
|
||||||
self.baseConfig = baseConfig
|
self.baseConfig = baseConfig
|
||||||
self._zoomedSurface = zoomedSurface
|
self._zoomedSurface = zoomedSurface
|
||||||
@ -395,7 +395,7 @@ extension Ghostty {
|
|||||||
|
|
||||||
private func onNewSplit(notification: SwiftUI.Notification) {
|
private func onNewSplit(notification: SwiftUI.Notification) {
|
||||||
let configAny = notification.userInfo?[Ghostty.Notification.NewSurfaceConfigKey]
|
let configAny = notification.userInfo?[Ghostty.Notification.NewSurfaceConfigKey]
|
||||||
let config = configAny as? ghostty_surface_config_s
|
let config = configAny as? SurfaceConfiguration
|
||||||
|
|
||||||
// Determine our desired direction
|
// Determine our desired direction
|
||||||
guard let directionAny = notification.userInfo?["direction"] else { return }
|
guard let directionAny = notification.userInfo?["direction"] else { return }
|
||||||
|
@ -185,6 +185,39 @@ extension Ghostty {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The configuration for a surface. For any configuration not set, defaults will be chosen from
|
||||||
|
/// libghostty, usually from the Ghostty configuration.
|
||||||
|
struct SurfaceConfiguration {
|
||||||
|
/// Explicit font size to use in points
|
||||||
|
var fontSize: UInt16? = nil
|
||||||
|
|
||||||
|
/// Explicit working directory to set
|
||||||
|
var workingDirectory: String? = nil
|
||||||
|
|
||||||
|
init() {}
|
||||||
|
|
||||||
|
init(from config: ghostty_surface_config_s) {
|
||||||
|
self.fontSize = config.font_size
|
||||||
|
self.workingDirectory = String.init(cString: config.working_directory, encoding: .utf8)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the ghostty configuration for this surface configuration struct. The memory
|
||||||
|
/// in the returned struct is only valid as long as this struct is retained.
|
||||||
|
func ghosttyConfig(view: SurfaceView) -> ghostty_surface_config_s {
|
||||||
|
var config = ghostty_surface_config_new()
|
||||||
|
config.userdata = Unmanaged.passUnretained(view).toOpaque()
|
||||||
|
config.nsview = Unmanaged.passUnretained(view).toOpaque()
|
||||||
|
config.scale_factor = NSScreen.main!.backingScaleFactor
|
||||||
|
|
||||||
|
if let fontSize = fontSize { config.font_size = fontSize }
|
||||||
|
if let workingDirectory = workingDirectory {
|
||||||
|
config.working_directory = (workingDirectory as NSString).utf8String
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 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
|
||||||
@ -221,7 +254,7 @@ extension Ghostty {
|
|||||||
case pendingHidden
|
case pendingHidden
|
||||||
}
|
}
|
||||||
|
|
||||||
init(_ app: ghostty_app_t, _ baseConfig: ghostty_surface_config_s?) {
|
init(_ app: ghostty_app_t, _ baseConfig: SurfaceConfiguration?) {
|
||||||
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
|
||||||
@ -230,12 +263,10 @@ extension Ghostty {
|
|||||||
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 ?? SurfaceConfiguration()
|
||||||
surface_cfg.userdata = Unmanaged.passUnretained(self).toOpaque()
|
var surface_cfg_c = surface_cfg.ghosttyConfig(view: self)
|
||||||
surface_cfg.nsview = Unmanaged.passUnretained(self).toOpaque()
|
|
||||||
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_c) else {
|
||||||
self.error = AppError.surfaceCreateError
|
self.error = AppError.surfaceCreateError
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -208,6 +208,9 @@ pub const Surface = struct {
|
|||||||
|
|
||||||
/// The font size to inherit. If 0, default font size will be used.
|
/// The font size to inherit. If 0, default font size will be used.
|
||||||
font_size: u16 = 0,
|
font_size: u16 = 0,
|
||||||
|
|
||||||
|
/// The working directory to load into.
|
||||||
|
working_directory: [*:0]const u8 = "",
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn init(self: *Surface, app: *App, opts: Options) !void {
|
pub fn init(self: *Surface, app: *App, opts: Options) !void {
|
||||||
@ -236,6 +239,37 @@ pub const Surface = struct {
|
|||||||
var config = try apprt.surface.newConfig(app.core_app, app.config);
|
var config = try apprt.surface.newConfig(app.core_app, app.config);
|
||||||
defer config.deinit();
|
defer config.deinit();
|
||||||
|
|
||||||
|
// If we have a working directory from the options then we set it.
|
||||||
|
const wd = std.mem.sliceTo(opts.working_directory, 0);
|
||||||
|
if (wd.len > 0) wd: {
|
||||||
|
var dir = std.fs.openDirAbsolute(wd, .{}) catch |err| {
|
||||||
|
log.warn(
|
||||||
|
"error opening requested working directory dir={s} err={}",
|
||||||
|
.{ wd, err },
|
||||||
|
);
|
||||||
|
break :wd;
|
||||||
|
};
|
||||||
|
defer dir.close();
|
||||||
|
|
||||||
|
const stat = dir.stat() catch |err| {
|
||||||
|
log.warn(
|
||||||
|
"failed to stat requested working directory dir={s} err={}",
|
||||||
|
.{ wd, err },
|
||||||
|
);
|
||||||
|
break :wd;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (stat.kind != .directory) {
|
||||||
|
log.warn(
|
||||||
|
"requested working directory is not a directory dir={s}",
|
||||||
|
.{wd},
|
||||||
|
);
|
||||||
|
break :wd;
|
||||||
|
}
|
||||||
|
|
||||||
|
config.@"working-directory" = wd;
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize our surface right away. We're given a view that is
|
// Initialize our surface right away. We're given a view that is
|
||||||
// ready to use.
|
// ready to use.
|
||||||
try self.core_surface.init(
|
try self.core_surface.init(
|
||||||
|
Reference in New Issue
Block a user