Compare commits

...

6 Commits

Author SHA1 Message Date
Mitchell Hashimoto
e79bf71f23
Binding for toggling window float on top (macOS only) (#7246)
Some checks are pending
Test / Required Checks: Test (push) Blocked by required conditions
Test / build-bench (push) Blocked by required conditions
Test / build-flatpak (push) Blocked by required conditions
Test / build-linux (namespace-profile-ghostty-md) (push) Blocked by required conditions
Test / build-linux (namespace-profile-ghostty-md-arm64) (push) Blocked by required conditions
Test / build-linux-libghostty (push) Blocked by required conditions
Test / build-nix (namespace-profile-ghostty-md) (push) Blocked by required conditions
Test / build-nix (namespace-profile-ghostty-md-arm64) (push) Blocked by required conditions
Test / build-dist (push) Blocked by required conditions
Test / build-macos (push) Blocked by required conditions
Test / build-macos-matrix (push) Blocked by required conditions
Test / build-snap (namespace-profile-ghostty-snap) (push) Blocked by required conditions
Test / build-snap (namespace-profile-ghostty-snap-arm64) (push) Blocked by required conditions
Test / build-windows (push) Blocked by required conditions
Test / build-windows-cross (namespace-profile-ghostty-md, x86-windows-gnu) (push) Blocked by required conditions
Test / build-windows-cross (namespace-profile-ghostty-md, x86_64-windows-gnu) (push) Blocked by required conditions
Test / test (push) Waiting to run
Test / GTK x11=false wayland=false (push) Blocked by required conditions
Test / GTK x11=true wayland=false (push) Blocked by required conditions
Test / GTK x11=false wayland=true (push) Blocked by required conditions
Test / GTK x11=true wayland=true (push) Blocked by required conditions
Test / Build -Dsentry=false (push) Blocked by required conditions
Test / Build -Dsentry=true (push) Blocked by required conditions
Test / test-macos (push) Blocked by required conditions
Test / blueprint-compiler (push) Waiting to run
Test / Test pkg/wuffs (push) Blocked by required conditions
Test / Test build on Debian 12 (push) Blocked by required conditions
Test / flatpak-check-zig-cache (push) Waiting to run
Test / Flatpak (map[arch:aarch64 runner:namespace-profile-ghostty-md-arm64]) (push) Blocked by required conditions
Test / Flatpak (map[arch:x86_64 runner:namespace-profile-ghostty-md]) (push) Blocked by required conditions
This adds a keybinding and apprt action for #7237.
2025-05-01 09:51:09 -07:00
Mitchell Hashimoto
6e11d947e7
Binding for toggling window float on top (macOS only)
This adds a keybinding and apprt action for #7237.
2025-05-01 09:47:17 -07:00
Mitchell Hashimoto
a6fd499019
macos: add float on top feature for terminal windows (#7237)
Closes #5188

**See:** 

-
https://github.com/ghostty-org/ghostty/discussions/5188#discussioncomment-12607781
-
https://github.com/ghostty-org/ghostty/discussions/5188#discussioncomment-12984898
2025-05-01 09:24:15 -07:00
Martin Hettiger
f83729ba48 macos: add float on top feature for terminal windows 2025-05-01 09:13:33 -07:00
Mitchell Hashimoto
4b3a49a56e
apprt/gtk: ensure configuration is loaded on startup (#7241)
Restores the app configuration code removed in
https://github.com/ghostty-org/ghostty/pull/6792.

The was unnoticed due to `colorSchemeEvent` triggering a configuration
reload if `window-theme` deviates from the default (i.e. dark mode is
used).

Fixes https://github.com/ghostty-org/ghostty/discussions/7206
2025-05-01 08:30:22 -07:00
Leorize
0af5a291ac
apprt/gtk: ensure configuration is loaded on startup
Restores the app configuration code removed in
https://github.com/ghostty-org/ghostty/pull/6792.

The was unnoticed due to `colorSchemeEvent` triggering a
configuration reload if `window-theme` deviates from the default
(i.e. dark mode is used).

Fixes https://github.com/ghostty-org/ghostty/discussions/7206
2025-05-01 01:59:36 -05:00
12 changed files with 228 additions and 37 deletions

View File

@ -429,6 +429,13 @@ typedef enum {
GHOSTTY_FULLSCREEN_NON_NATIVE_PADDED_NOTCH,
} ghostty_action_fullscreen_e;
// apprt.action.FloatWindow
typedef enum {
GHOSTTY_FLOAT_WINDOW_ON,
GHOSTTY_FLOAT_WINDOW_OFF,
GHOSTTY_FLOAT_WINDOW_TOGGLE,
} ghostty_action_float_window_e;
// apprt.action.SecureInput
typedef enum {
GHOSTTY_SECURE_INPUT_ON,
@ -610,6 +617,7 @@ typedef enum {
GHOSTTY_ACTION_RENDERER_HEALTH,
GHOSTTY_ACTION_OPEN_CONFIG,
GHOSTTY_ACTION_QUIT_TIMER,
GHOSTTY_ACTION_FLOAT_WINDOW,
GHOSTTY_ACTION_SECURE_INPUT,
GHOSTTY_ACTION_KEY_SEQUENCE,
GHOSTTY_ACTION_COLOR_CHANGE,
@ -638,6 +646,7 @@ typedef union {
ghostty_action_mouse_over_link_s mouse_over_link;
ghostty_action_renderer_health_e renderer_health;
ghostty_action_quit_timer_e quit_timer;
ghostty_action_float_window_e float_window;
ghostty_action_secure_input_e secure_input;
ghostty_action_key_sequence_s key_sequence;
ghostty_action_color_change_s color_change;

View File

@ -52,6 +52,8 @@ class AppDelegate: NSObject,
@IBOutlet private var menuSelectSplitLeft: NSMenuItem?
@IBOutlet private var menuSelectSplitRight: NSMenuItem?
@IBOutlet private var menuReturnToDefaultSize: NSMenuItem?
@IBOutlet private var menuFloatOnTop: NSMenuItem?
@IBOutlet private var menuUseAsDefault: NSMenuItem?
@IBOutlet private var menuIncreaseFontSize: NSMenuItem?
@IBOutlet private var menuDecreaseFontSize: NSMenuItem?
@ -175,6 +177,12 @@ class AppDelegate: NSObject,
handler: localEventHandler)
// Notifications
NotificationCenter.default.addObserver(
self,
selector: #selector(windowDidBecomeKey),
name: NSWindow.didBecomeKeyNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(quickTerminalDidChangeVisibility),
@ -406,6 +414,7 @@ class AppDelegate: NSObject,
syncMenuShortcut(config, action: "prompt_surface_title", menuItem: self.menuChangeTitle)
syncMenuShortcut(config, action: "toggle_quick_terminal", menuItem: self.menuQuickTerminal)
syncMenuShortcut(config, action: "toggle_visibility", menuItem: self.menuToggleVisibility)
syncMenuShortcut(config, action: "toggle_window_float_on_top", menuItem: self.menuFloatOnTop)
syncMenuShortcut(config, action: "inspector:toggle", menuItem: self.menuTerminalInspector)
syncMenuShortcut(config, action: "toggle_command_palette", menuItem: self.menuCommandPalette)
@ -497,6 +506,10 @@ class AppDelegate: NSObject,
return event
}
@objc private func windowDidBecomeKey(_ notification: Notification) {
syncFloatOnTopMenu(notification.object as? NSWindow)
}
@objc private func quickTerminalDidChangeVisibility(_ notification: Notification) {
guard let quickController = notification.object as? QuickTerminalController else { return }
self.menuQuickTerminal?.state = if (quickController.visible) { .on } else { .off }
@ -899,3 +912,50 @@ class AppDelegate: NSObject,
}
}
}
// MARK: Floating Windows
extension AppDelegate {
func syncFloatOnTopMenu(_ window: NSWindow?) {
guard let window = (window ?? NSApp.keyWindow) as? TerminalWindow else {
// If some other window became key we always turn this off
self.menuFloatOnTop?.state = .off
return
}
self.menuFloatOnTop?.state = window.level == .floating ? .on : .off
}
@IBAction func floatOnTop(_ menuItem: NSMenuItem) {
menuItem.state = menuItem.state == .on ? .off : .on
guard let window = NSApp.keyWindow else { return }
window.level = menuItem.state == .on ? .floating : .normal
}
@IBAction func useAsDefault(_ sender: NSMenuItem) {
let ud = UserDefaults.standard
let key = TerminalWindow.defaultLevelKey
if (menuFloatOnTop?.state == .on) {
ud.set(NSWindow.Level.floating, forKey: key)
} else {
ud.removeObject(forKey: key)
}
}
}
// MARK: NSMenuItemValidation
extension AppDelegate: NSMenuItemValidation {
func validateMenuItem(_ item: NSMenuItem) -> Bool {
switch item.action {
case #selector(floatOnTop(_:)),
#selector(useAsDefault(_:)):
// Float on top items only active if the key window is a primary
// terminal window (not quick terminal).
return NSApp.keyWindow is TerminalWindow
default:
return true
}
}
}

View File

@ -25,6 +25,7 @@
<outlet property="menuCopy" destination="Jqf-pv-Zcu" id="bKd-1C-oy9"/>
<outlet property="menuDecreaseFontSize" destination="kzb-SZ-dOA" id="Y1B-Vh-6Z2"/>
<outlet property="menuEqualizeSplits" destination="3gH-VD-vL9" id="SiZ-ce-FOF"/>
<outlet property="menuFloatOnTop" destination="uRj-7z-1Nh" id="94n-o9-Jol"/>
<outlet property="menuIncreaseFontSize" destination="CIH-ey-Z6x" id="hkc-9C-80E"/>
<outlet property="menuMoveSplitDividerDown" destination="Zj7-2W-fdF" id="997-LL-nlN"/>
<outlet property="menuMoveSplitDividerLeft" destination="wSR-ny-j1a" id="HCZ-CI-2ob"/>
@ -56,6 +57,7 @@
<outlet property="menuTerminalInspector" destination="QwP-M5-fvh" id="wJi-Dh-S9f"/>
<outlet property="menuToggleFullScreen" destination="8kY-Pi-KaY" id="yQg-6V-OO6"/>
<outlet property="menuToggleVisibility" destination="DOX-wA-ilh" id="iBj-Bc-2bq"/>
<outlet property="menuUseAsDefault" destination="TrB-O8-g8H" id="af4-Jh-2HU"/>
<outlet property="menuZoomSplit" destination="oPd-mn-IEH" id="wTu-jK-egI"/>
</connections>
</customObject>
@ -402,6 +404,19 @@
<action selector="returnToDefaultSize:" target="-1" id="Bpt-GO-UU1"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="Cm3-gn-vtj"/>
<menuItem title="Float on Top" id="uRj-7z-1Nh">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="floatOnTop:" target="bbz-4X-AYv" id="N58-PO-7pj"/>
</connections>
</menuItem>
<menuItem title="Use as Default" id="TrB-O8-g8H">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="useAsDefault:" target="bbz-4X-AYv" id="RHA-Nl-L2U"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="CpM-rI-Sc1"/>
<menuItem title="Bring All to Front" id="LE2-aR-0XJ">
<modifierMask key="keyEquivalentModifierMask"/>

View File

@ -1,6 +1,9 @@
import Cocoa
class TerminalWindow: NSWindow {
/// This is the key in UserDefaults to use for the default `level` value.
static let defaultLevelKey: String = "TerminalDefaultLevel"
@objc dynamic var keyEquivalent: String = ""
/// This is used to determine if certain elements should be drawn light or dark and should
@ -63,6 +66,8 @@ class TerminalWindow: NSWindow {
if titlebarTabs {
generateToolbar()
}
level = UserDefaults.standard.value(forKey: Self.defaultLevelKey) as? NSWindow.Level ?? .normal
}
deinit {

View File

@ -496,6 +496,9 @@ extension Ghostty {
case GHOSTTY_ACTION_OPEN_CONFIG:
ghostty_config_open()
case GHOSTTY_ACTION_FLOAT_WINDOW:
toggleFloatWindow(app, target: target, mode: action.action.float_window)
case GHOSTTY_ACTION_SECURE_INPUT:
toggleSecureInput(app, target: target, mode: action.action.secure_input)
@ -1026,6 +1029,43 @@ extension Ghostty {
}
}
private static func toggleFloatWindow(
_ app: ghostty_app_t,
target: ghostty_target_s,
mode mode_raw: ghostty_action_float_window_e
) {
guard let mode = SetFloatWIndow.from(mode_raw) else { return }
switch (target.tag) {
case GHOSTTY_TARGET_APP:
Ghostty.logger.warning("toggle float window does nothing with an app target")
return
case GHOSTTY_TARGET_SURFACE:
guard let surface = target.target.surface else { return }
guard let surfaceView = self.surfaceView(from: surface) else { return }
guard let window = surfaceView.window as? TerminalWindow else { return }
switch (mode) {
case .on:
window.level = .floating
case .off:
window.level = .normal
case .toggle:
window.level = window.level == .floating ? .normal : .floating
}
if let appDelegate = NSApplication.shared.delegate as? AppDelegate {
appDelegate.syncFloatOnTopMenu(window)
}
default:
assertionFailure()
}
}
private static func toggleSecureInput(
_ app: ghostty_app_t,
target: ghostty_target_s,

View File

@ -42,6 +42,28 @@ extension Ghostty {
// MARK: Swift Types for C Types
extension Ghostty {
enum SetFloatWIndow {
case on
case off
case toggle
static func from(_ c: ghostty_action_float_window_e) -> Self? {
switch (c) {
case GHOSTTY_FLOAT_WINDOW_ON:
return .on
case GHOSTTY_FLOAT_WINDOW_OFF:
return .off
case GHOSTTY_FLOAT_WINDOW_TOGGLE:
return .toggle
default:
return nil
}
}
}
enum SetSecureInput {
case on
case off

View File

@ -4289,6 +4289,12 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
{},
),
.toggle_window_float_on_top => return try self.rt_app.performAction(
.{ .surface = self },
.float_window,
.toggle,
),
.toggle_secure_input => return try self.rt_app.performAction(
.{ .surface = self },
.secure_input,

View File

@ -205,6 +205,10 @@ pub const Action = union(Key) {
/// happen and can be ignored or cause a restart it isn't that important.
quit_timer: QuitTimer,
/// Set the window floating state. A floating window is one that is
/// always on top of other windows even when not focused.
float_window: FloatWindow,
/// Set the secure input functionality on or off. "Secure input" means
/// that the user is currently at some sort of prompt where they may be
/// entering a password or other sensitive information. This can be used
@ -289,6 +293,7 @@ pub const Action = union(Key) {
renderer_health,
open_config,
quit_timer,
float_window,
secure_input,
key_sequence,
color_change,
@ -425,6 +430,12 @@ pub const Fullscreen = enum(c_int) {
macos_non_native_padded_notch,
};
pub const FloatWindow = enum(c_int) {
on,
off,
toggle,
};
pub const SecureInput = enum(c_int) {
on,
off,

View File

@ -235,6 +235,7 @@ pub const App = struct {
.inspector,
.render_inspector,
.quit_timer,
.float_window,
.secure_input,
.key_sequence,
.desktop_notification,

View File

@ -488,6 +488,7 @@ pub fn performAction(
// Unimplemented
.close_all_windows,
.float_window,
.toggle_command_palette,
.toggle_visibility,
.cell_size,
@ -1291,6 +1292,13 @@ pub fn run(self: *App) !void {
// Setup our actions
self.initActions();
// On startup, we want to check for configuration errors right away
// so we can show our error window. We also need to setup other initial
// state.
self.syncConfigChanges(null) catch |err| {
log.warn("error handling configuration changes err={}", .{err});
};
while (self.running) {
_ = glib.MainContext.iteration(self.ctx, 1);

View File

@ -222,12 +222,12 @@ pub fn lessThan(_: void, lhs: Binding, rhs: Binding) bool {
pub const Action = union(enum) {
/// Ignore this key combination, don't send it to the child process, just
/// black hole it.
ignore: void,
ignore,
/// This action is used to flag that the binding should be removed from
/// the set. This should never exist in an active set and `set.put` has an
/// assertion to verify this.
unbind: void,
unbind,
/// Send a CSI sequence. The value should be the CSI sequence without the
/// CSI header (`ESC [` or `\x1b[`).
@ -252,35 +252,35 @@ pub const Action = union(enum) {
/// If you do this while in a TUI program such as vim, this may break
/// the program. If you do this while in a shell, you may have to press
/// enter after to get a new prompt.
reset: void,
reset,
/// Copy and paste.
copy_to_clipboard: void,
paste_from_clipboard: void,
paste_from_selection: void,
copy_to_clipboard,
paste_from_clipboard,
paste_from_selection,
/// Copy the URL under the cursor to the clipboard. If there is no
/// URL under the cursor, this does nothing.
copy_url_to_clipboard: void,
copy_url_to_clipboard,
/// Increase/decrease the font size by a certain amount.
increase_font_size: f32,
decrease_font_size: f32,
/// Reset the font size to the original configured size.
reset_font_size: void,
reset_font_size,
/// Clear the screen. This also clears all scrollback.
clear_screen: void,
clear_screen,
/// Select all text on the screen.
select_all: void,
select_all,
/// Scroll the screen varying amounts.
scroll_to_top: void,
scroll_to_bottom: void,
scroll_page_up: void,
scroll_page_down: void,
scroll_to_top,
scroll_to_bottom,
scroll_page_up,
scroll_page_down,
scroll_page_fractional: f32,
scroll_page_lines: i16,
@ -321,19 +321,19 @@ pub const Action = union(enum) {
/// Open a new window. If the application isn't currently focused,
/// this will bring it to the front.
new_window: void,
new_window,
/// Open a new tab.
new_tab: void,
new_tab,
/// Go to the previous tab.
previous_tab: void,
previous_tab,
/// Go to the next tab.
next_tab: void,
next_tab,
/// Go to the last tab (the one with the highest index)
last_tab: void,
last_tab,
/// Go to the tab with the specific number, 1-indexed. If the tab number
/// is higher than the number of tabs, this will go to the last tab.
@ -346,10 +346,10 @@ pub const Action = union(enum) {
/// Toggle the tab overview.
/// This only works with libadwaita enabled currently.
toggle_tab_overview: void,
toggle_tab_overview,
/// Change the title of the current focused surface via a prompt.
prompt_surface_title: void,
prompt_surface_title,
/// Create a new split in the given direction.
///
@ -365,7 +365,7 @@ pub const Action = union(enum) {
goto_split: SplitFocusDirection,
/// zoom/unzoom the current split.
toggle_split_zoom: void,
toggle_split_zoom,
/// Resize the current split in a given direction.
///
@ -378,12 +378,12 @@ pub const Action = union(enum) {
resize_split: SplitResizeParameter,
/// Equalize all splits in the current window
equalize_splits: void,
equalize_splits,
/// Reset the window to the default size. The "default size" is the
/// size that a new window would be created with. This has no effect
/// if the window is fullscreen.
reset_window_size: void,
reset_window_size,
/// Control the terminal inspector visibility.
///
@ -397,39 +397,46 @@ pub const Action = union(enum) {
/// Open the configuration file in the default OS editor. If your default OS
/// editor isn't configured then this will fail. Currently, any failures to
/// open the configuration will show up only in the logs.
open_config: void,
open_config,
/// Reload the configuration. The exact meaning depends on the app runtime
/// in use but this usually involves re-reading the configuration file
/// and applying any changes. Note that not all changes can be applied at
/// runtime.
reload_config: void,
reload_config,
/// Close the current "surface", whether that is a window, tab, split, etc.
/// This only closes ONE surface. This will trigger close confirmation as
/// configured.
close_surface: void,
close_surface,
/// Close the current tab, regardless of how many splits there may be.
/// This will trigger close confirmation as configured.
close_tab: void,
close_tab,
/// Close the window, regardless of how many tabs or splits there may be.
/// This will trigger close confirmation as configured.
close_window: void,
close_window,
/// Close all windows. This will trigger close confirmation as configured.
/// This only works for macOS currently.
close_all_windows: void,
close_all_windows,
/// Toggle maximized window state. This only works on Linux.
toggle_maximize: void,
toggle_maximize,
/// Toggle fullscreen mode of window.
toggle_fullscreen: void,
toggle_fullscreen,
/// Toggle window decorations on and off. This only works on Linux.
toggle_window_decorations: void,
toggle_window_decorations,
/// Toggle whether the terminal window is always on top of other
/// windows even when it is not focused. Terminal windows always start
/// as normal (not always on top) windows.
///
/// This only works on macOS.
toggle_window_float_on_top,
/// Toggle secure input mode on or off. This is used to prevent apps
/// that monitor input from seeing what you type. This is useful for
@ -439,7 +446,7 @@ pub const Action = union(enum) {
/// terminal. You must toggle it off to disable it, or quit Ghostty.
///
/// This only works on macOS, since this is a system API on macOS.
toggle_secure_input: void,
toggle_secure_input,
/// Toggle the command palette. The command palette is a UI element
/// that lets you see what actions you can perform, their associated
@ -488,7 +495,7 @@ pub const Action = union(enum) {
/// plugin enabled, open System Settings > Apps & Windows > Window
/// Management > Desktop Effects, and enable the plugin in the plugin list.
/// Ghostty would then need to be restarted for this to take effect.
toggle_quick_terminal: void,
toggle_quick_terminal,
/// Show/hide all windows. If all windows become shown, we also ensure
/// Ghostty becomes focused. When hiding all windows, focus is yielded
@ -497,10 +504,10 @@ pub const Action = union(enum) {
/// Note: When the focused surface is fullscreen, this method does nothing.
///
/// This currently only works on macOS.
toggle_visibility: void,
toggle_visibility,
/// Quit ghostty.
quit: void,
quit,
/// Crash ghostty in the desired thread for the focused surface.
///
@ -797,6 +804,7 @@ pub const Action = union(enum) {
.toggle_maximize,
.toggle_fullscreen,
.toggle_window_decorations,
.toggle_window_float_on_top,
.toggle_secure_input,
.toggle_command_palette,
.reset_window_size,

View File

@ -346,6 +346,12 @@ fn actionCommands(action: Action.Key) []const Command {
.description = "Toggle the window decorations.",
}},
.toggle_window_float_on_top => comptime &.{.{
.action = .toggle_window_float_on_top,
.title = "Toggle Float on Top",
.description = "Toggle the float on top state of the current window.",
}},
.toggle_secure_input => comptime &.{.{
.action = .toggle_secure_input,
.title = "Toggle Secure Input",