diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 331f26c974..1e8e4c2141 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -273,6 +273,28 @@ class TerminalController: BaseTerminalController { } } + private func setInitialWindowPosition(x: Int16?, y: Int16?, windowDecorations: Bool) { + guard let window else { return } + + // If we don't have both an X and Y we center. + guard let x, let y else { + window.center() + return + } + + // Prefer the screen our window is being placed on otherwise our primary screen. + guard let screen = window.screen ?? NSScreen.screens.first else { + window.center() + return + } + + // Orient based on the top left of the primary monitor + let frame = screen.visibleFrame + window.setFrameOrigin(.init( + x: frame.minX + CGFloat(x), + y: frame.maxY - (CGFloat(y) + window.frame.height))) + } + //MARK: - NSWindowController override func windowWillLoad() { @@ -368,9 +390,12 @@ class TerminalController: BaseTerminalController { } } - // Center the window to start, we'll move the window frame automatically - // when cascading. - window.center() + // Set our window positioning to coordinates if config value exists, otherwise + // fallback to original centering behavior + setInitialWindowPosition( + x: config.windowPositionX, + y: config.windowPositionY, + windowDecorations: config.windowDecorations) // Make sure our theme is set on the window so styling is correct. if let windowTheme = config.windowTheme { diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index 1e733c5e1f..82d17a2de6 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -149,6 +149,20 @@ extension Ghostty { guard let ptr = v else { return "" } return String(cString: ptr) } + + var windowPositionX: Int16? { + guard let config = self.config else { return nil } + var v: Int16 = 0 + let key = "window-position-x" + return ghostty_config_get(config, &v, key, UInt(key.count)) ? v : nil + } + + var windowPositionY: Int16? { + guard let config = self.config else { return nil } + var v: Int16 = 0 + let key = "window-position-y" + return ghostty_config_get(config, &v, key, UInt(key.count)) ? v : nil + } var windowNewTabPosition: String { guard let config = self.config else { return "" } diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 64b0cbe811..3fbef0f229 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -510,6 +510,13 @@ pub const Surface = struct { ) orelse return glfw.mustGetErrorCode(); errdefer win.destroy(); + // Setup our + setInitialWindowPosition( + win, + app.config.@"window-position-x", + app.config.@"window-position-y", + ); + // Get our physical DPI - debug only because we don't have a use for // this but the logging of it may be useful if (builtin.mode == .Debug) { @@ -663,6 +670,17 @@ pub const Surface = struct { }); } + /// Set the initial window position. This is called exactly once at + /// surface initialization time. This may be called before "self" + /// is fully initialized. + fn setInitialWindowPosition(win: glfw.Window, x: ?i16, y: ?i16) void { + const start_position_x = x orelse return; + const start_position_y = y orelse return; + + log.debug("setting initial window position ({},{})", .{ start_position_x, start_position_y }); + win.setPos(.{ .x = start_position_x, .y = start_position_y }); + } + /// Set the size limits of the window. /// Note: this interface is not good, we should redo it if we plan /// to use this more. i.e. you can't set max width but no max height, diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index c10ba79935..3d05bc2c55 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -786,6 +786,7 @@ fn setInitialSize( ), } } + fn showDesktopNotification( self: *App, target: apprt.Target, diff --git a/src/config/Config.zig b/src/config/Config.zig index e9052a66e1..083bcc1920 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1108,6 +1108,32 @@ keybind: Keybinds = .{}, @"window-height": u32 = 0, @"window-width": u32 = 0, +/// The starting window position. This position is in pixels and is relative +/// to the top-left corner of the primary monitor. Both values must be set to take +/// effect. If only one value is set, it is ignored. +/// +/// Note that the window manager may put limits on the position or override +/// the position. For example, a tiling window manager may force the window +/// to be a certain position to fit within the grid. There is nothing Ghostty +/// will do about this, but it will make an effort. +/// +/// Also note that negative values are also up to the operating system and +/// window manager. Some window managers may not allow windows to be placed +/// off-screen. +/// +/// Invalid positions are runtime-specific, but generally the positions are +/// clamped to the nearest valid position. +/// +/// On macOS, the window position is relative to the top-left corner of +/// the visible screen area. This means that if the menu bar is visible, the +/// window will be placed below the menu bar. +/// +/// Note: this is only supported on macOS and Linux GLFW builds. The GTK +/// runtime does not support setting the window position (this is a limitation +/// of GTK 4.0). +@"window-position-x": ?i16 = null, +@"window-position-y": ?i16 = null, + /// Whether to enable saving and restoring window state. Window state includes /// their position, size, tabs, splits, etc. Some window state requires shell /// integration, such as preserving working directories. See `shell-integration` diff --git a/src/config/c_get.zig b/src/config/c_get.zig index dd7c7cce81..d3f38415ea 100644 --- a/src/config/c_get.zig +++ b/src/config/c_get.zig @@ -42,6 +42,11 @@ fn getValue(ptr_raw: *anyopaque, value: anytype) bool { ptr.* = @intCast(value); }, + i16 => { + const ptr: *c_short = @ptrCast(@alignCast(ptr_raw)); + ptr.* = @intCast(value); + }, + f32, f64 => |Float| { const ptr: *Float = @ptrCast(@alignCast(ptr_raw)); ptr.* = @floatCast(value);