From 42913c783040f9aae1f9c6ed5a6d1615246269dd Mon Sep 17 00:00:00 2001 From: Danil Ovchinnikov Date: Mon, 28 Apr 2025 03:23:24 +0300 Subject: [PATCH 01/11] i18n: fixed the translation for Russian Co-authored-by: TicClick --- po/ru_RU.UTF-8.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/po/ru_RU.UTF-8.po b/po/ru_RU.UTF-8.po index 2d13e6de7..9e9cf8077 100644 --- a/po/ru_RU.UTF-8.po +++ b/po/ru_RU.UTF-8.po @@ -45,7 +45,7 @@ msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." msgstr "" -"Конфигурация содержит ошибки. Проверьте их ниже, а затемлибо перезагрузите " +"Конфигурация содержит ошибки. Проверьте их ниже, а затем либо перезагрузите " "конфигурацию, либо проигнорируйте ошибки." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 From e5e89bcbe4f67224b24f27e4ac6bb8b4a4d39ebc Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 28 Apr 2025 14:00:35 -0700 Subject: [PATCH 02/11] macos: key input that clears preedit without text shouldn't encode Fixes #7225 --- macos/Sources/Ghostty/SurfaceView_AppKit.swift | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 5985d64a0..921c32c8b 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -975,7 +975,14 @@ extension Ghostty { event: event, translationEvent: translationEvent, text: translationEvent.ghosttyCharacters, - composing: markedText.length > 0 + + // We're composing if we have preedit (the obvious case). But we're also + // composing if we don't have preedit and we had marked text before, + // because this input probably just reset the preedit state. It shouldn't + // be encoded. Example: Japanese begin composing, the press backspace. + // This should only cancel the composing state but not actually delete + // the prior input characters (prior to the composing). + composing: markedText.length > 0 || markedTextBefore ) } } From 4e3975650187c3b6edf2687df7861403f9913b8b Mon Sep 17 00:00:00 2001 From: Kat <65649991+00-kat@users.noreply.github.com> Date: Fri, 25 Apr 2025 15:22:37 +1000 Subject: [PATCH 03/11] Link to GTK CSS docs and add some useful tips to gtk-custom-css' docs. It may not be immediately obvious how to style Ghostty despite knowing of the existence of that configuration option; one who is more accustomed to web development would likely be very reliant on their browser's inspector for modifying and debugging the style of their application. GTK CSS also differs in some important ways from the CSS found in browsers, and hence linking to the GTK CSS documentation would save time for anyone new to styling GTK applications. --- src/config/Config.zig | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/config/Config.zig b/src/config/Config.zig index f71e0972d..2db3db430 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -2276,6 +2276,18 @@ keybind: Keybinds = .{}, /// Custom CSS files to be loaded. /// +/// GTK CSS documentation can be found at the following links: +/// +/// * - An overview of GTK CSS. +/// * - A comprehensive list +/// of supported CSS properties. +/// +/// Launch Ghostty with `env GTK_DEBUG=interactive ghostty` to tweak Ghostty's +/// CSS in real time using the GTK Inspector. Errors in your CSS files would +/// also be reported in the terminal you started Ghostty from. See +/// for more +/// information about the GTK Inspector. +/// /// This configuration can be repeated multiple times to load multiple files. /// Prepend a ? character to the file path to suppress errors if the file does /// not exist. If you want to include a file that begins with a literal ? From 87107a79343a6e2de802372aa75657ca40461e07 Mon Sep 17 00:00:00 2001 From: trag1c Date: Tue, 29 Apr 2025 18:32:59 +0200 Subject: [PATCH 04/11] ci: drop l10n review workflow --- .github/scripts/request_review.py | 189 ------------------------------ .github/workflows/review.yml | 37 ------ 2 files changed, 226 deletions(-) delete mode 100644 .github/scripts/request_review.py delete mode 100644 .github/workflows/review.yml diff --git a/.github/scripts/request_review.py b/.github/scripts/request_review.py deleted file mode 100644 index d799e7c58..000000000 --- a/.github/scripts/request_review.py +++ /dev/null @@ -1,189 +0,0 @@ -# /// script -# requires-python = ">=3.9" -# dependencies = [ -# "githubkit", -# "loguru", -# ] -# /// - -from __future__ import annotations - -import asyncio -import os -import re -import sys -from collections.abc import Iterator -from contextlib import contextmanager -from itertools import chain - -from githubkit import GitHub -from githubkit.exception import RequestFailed -from loguru import logger - -ORG_NAME = "ghostty-org" -REPO_NAME = "ghostty" -ALLOWED_PARENT_TEAM = "localization" -LOCALIZATION_TEAM_NAME_PATTERN = re.compile(r"[a-z]{2}_[A-Z]{2}") -LEVEL_MAP = {"DEBUG": "DBG", "WARNING": "WRN", "ERROR": "ERR"} - -logger.remove() -logger.add( - sys.stderr, - format=lambda record: ( - "{time:YYYY-MM-DD HH:mm:ss.SSS} | " - f"{LEVEL_MAP[record['level'].name]} | " - "{function}:{line} - " - "{message}\n" - ), - backtrace=True, - diagnose=True, -) - - -@contextmanager -def log_fail(message: str, *, die: bool = True) -> Iterator[None]: - try: - yield - except RequestFailed as exc: - logger.error(message) - logger.error(exc) - logger.error(exc.response.raw_response.json()) - if die: - sys.exit(1) - - -gh = GitHub(os.environ["GITHUB_TOKEN"]) - -with log_fail("Invalid token"): - # Do the simplest request as a test - gh.rest.rate_limit.get() - - -async def fetch_and_parse_codeowners() -> dict[str, str]: - logger.debug("Fetching CODEOWNERS file...") - with log_fail("Failed to fetch CODEOWNERS file"): - content = ( - await gh.rest.repos.async_get_content( - ORG_NAME, - REPO_NAME, - "CODEOWNERS", - headers={"Accept": "application/vnd.github.raw+json"}, - ) - ).text - - logger.debug("Parsing CODEOWNERS file...") - codeowners: dict[str, str] = {} - for line in content.splitlines(): - if not line or line.lstrip().startswith("#"): - continue - - # This assumes that all entries only list one owner - # and that this owner is a team (ghostty-org/foobar) - path, owner = line.split() - path = path.lstrip("/") - owner = owner.removeprefix(f"@{ORG_NAME}/") - - if not is_localization_team(owner): - logger.debug(f"Skipping non-l11n codeowner {owner!r} for {path}") - continue - - codeowners[path] = owner - logger.debug(f"Found codeowner {owner!r} for {path}") - return codeowners - - -async def get_team_members(team_name: str) -> list[str]: - logger.debug(f"Fetching team {team_name!r}...") - with log_fail(f"Failed to fetch team {team_name!r}"): - team = (await gh.rest.teams.async_get_by_name(ORG_NAME, team_name)).parsed_data - - if team.parent and team.parent.slug == ALLOWED_PARENT_TEAM: - logger.debug(f"Fetching team {team_name!r} members...") - with log_fail(f"Failed to fetch team {team_name!r} members"): - resp = await gh.rest.teams.async_list_members_in_org(ORG_NAME, team_name) - members = [m.login for m in resp.parsed_data] - logger.debug(f"Team {team_name!r} members: {', '.join(members)}") - return members - - logger.warning(f"Team {team_name} does not have a {ALLOWED_PARENT_TEAM!r} parent") - return [] - - -async def get_changed_files(pr_number: int) -> list[str]: - logger.debug("Gathering changed files...") - with log_fail("Failed to gather changed files"): - diff_entries = ( - await gh.rest.pulls.async_list_files( - ORG_NAME, - REPO_NAME, - pr_number, - per_page=3000, - headers={"Accept": "application/vnd.github+json"}, - ) - ).parsed_data - return [d.filename for d in diff_entries] - - -async def request_review(pr_number: int, user: str, pr_author: str) -> None: - if user == pr_author: - logger.debug(f"Skipping review request for {user!r} (is PR author)") - logger.debug(f"Requesting review from {user!r}...") - with log_fail(f"Failed to request review from {user}", die=False): - await gh.rest.pulls.async_request_reviewers( - ORG_NAME, - REPO_NAME, - pr_number, - headers={"Accept": "application/vnd.github+json"}, - data={"reviewers": [user]}, - ) - - -def is_localization_team(team_name: str) -> bool: - return LOCALIZATION_TEAM_NAME_PATTERN.fullmatch(team_name) is not None - - -async def get_pr_author(pr_number: int) -> str: - logger.debug("Fetching PR author...") - with log_fail("Failed to fetch PR author"): - resp = await gh.rest.pulls.async_get(ORG_NAME, REPO_NAME, pr_number) - pr_author = resp.parsed_data.user.login - logger.debug(f"Found author: {pr_author!r}") - return pr_author - - -async def main() -> None: - logger.debug("Reading PR number...") - pr_number = int(os.environ["PR_NUMBER"]) - logger.debug(f"Starting review request process for PR #{pr_number}...") - - changed_files = await get_changed_files(pr_number) - logger.debug(f"Changed files: {', '.join(map(repr, changed_files))}") - - pr_author = await get_pr_author(pr_number) - codeowners = await fetch_and_parse_codeowners() - - found_owners = set[str]() - for file in changed_files: - logger.debug(f"Finding owner for {file!r}...") - for path, owner in codeowners.items(): - if file.startswith(path): - logger.debug(f"Found owner: {owner!r}") - break - else: - logger.debug("No owner found") - continue - found_owners.add(owner) - - member_lists = await asyncio.gather( - *(get_team_members(owner) for owner in found_owners) - ) - await asyncio.gather( - *( - request_review(pr_number, user, pr_author) - for user in chain.from_iterable(member_lists) - ) - ) - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/.github/workflows/review.yml b/.github/workflows/review.yml deleted file mode 100644 index 9abe0b5e2..000000000 --- a/.github/workflows/review.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Request Review - -on: - pull_request: - types: - - opened - - synchronize - -env: - PY_COLORS: 1 - -jobs: - review: - runs-on: namespace-profile-ghostty-xsm - steps: - - uses: actions/checkout@v4 - - - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.0 - with: - path: | - /nix - /zig - - - uses: cachix/install-nix-action@v30 - with: - nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v15 - with: - name: ghostty - authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - - - name: Request Localization Review - env: - GITHUB_TOKEN: ${{ secrets.GH_REVIEW_TOKEN }} - PR_NUMBER: ${{ github.event.pull_request.number }} - run: nix develop -c uv run .github/scripts/request_review.py From 2c1ade763fedbb8a0e402b341778d7608eac3a09 Mon Sep 17 00:00:00 2001 From: Tim Culverhouse Date: Tue, 29 Apr 2025 18:42:16 -0500 Subject: [PATCH 05/11] terminal(dcs): convert all xtgettcap queries to upper XTGETTCAP queries are a semicolon-delimited list of hex encoded terminfo capability names. Ghostty encodes a map using upper case hex encodings, meaning when an application uses a lower case encoding the capability is not found. To fix, we convert the entire list we receive in the query to upper case prior to processing further. Fixes: #7229 --- src/terminal/dcs.zig | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/terminal/dcs.zig b/src/terminal/dcs.zig index da6f3ae23..db5f95c4f 100644 --- a/src/terminal/dcs.zig +++ b/src/terminal/dcs.zig @@ -162,7 +162,12 @@ pub const Handler = struct { break :tmux .{ .tmux = .{ .exit = {} } }; }, - .xtgettcap => |list| .{ .xtgettcap = .{ .data = list } }, + .xtgettcap => |list| xtgettcap: { + for (list.items, 0..) |b, i| { + list.items[i] = std.ascii.toUpper(b); + } + break :xtgettcap .{ .xtgettcap = .{ .data = list } }; + }, .decrqss => |buffer| .{ .decrqss = switch (buffer.len) { 0 => .none, @@ -306,6 +311,21 @@ test "XTGETTCAP command" { try testing.expect(cmd.xtgettcap.next() == null); } +test "XTGETTCAP mixed case" { + const testing = std.testing; + const alloc = testing.allocator; + + var h: Handler = .{}; + defer h.deinit(); + try testing.expect(h.hook(alloc, .{ .intermediates = "+", .final = 'q' }) == null); + for ("536d756C78") |byte| _ = h.put(byte); + var cmd = h.unhook().?; + defer cmd.deinit(); + try testing.expect(cmd == .xtgettcap); + try testing.expectEqualStrings("536D756C78", cmd.xtgettcap.next().?); + try testing.expect(cmd.xtgettcap.next() == null); +} + test "XTGETTCAP command multiple keys" { const testing = std.testing; const alloc = testing.allocator; @@ -333,7 +353,7 @@ test "XTGETTCAP command invalid data" { var cmd = h.unhook().?; defer cmd.deinit(); try testing.expect(cmd == .xtgettcap); - try testing.expectEqualStrings("who", cmd.xtgettcap.next().?); + try testing.expectEqualStrings("WHO", cmd.xtgettcap.next().?); try testing.expectEqualStrings("536D756C78", cmd.xtgettcap.next().?); try testing.expect(cmd.xtgettcap.next() == null); } From 0af5a291acca1dddcb83448308b71aa3e91f5b25 Mon Sep 17 00:00:00 2001 From: Leorize Date: Thu, 1 May 2025 01:36:41 -0500 Subject: [PATCH 06/11] 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 --- src/apprt/gtk/App.zig | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 72c0d7509..5373e578c 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -1291,6 +1291,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); From f83729ba4874666023ab39b111db98ca029fb02b Mon Sep 17 00:00:00 2001 From: Martin Hettiger Date: Mon, 24 Mar 2025 22:13:50 +0100 Subject: [PATCH 07/11] macos: add float on top feature for terminal windows --- macos/Sources/App/macOS/AppDelegate.swift | 51 +++++++++++++++++++ macos/Sources/App/macOS/MainMenu.xib | 15 ++++++ .../Features/Terminal/TerminalWindow.swift | 5 ++ 3 files changed, 71 insertions(+) diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 682099e92..169f57ff2 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -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), @@ -497,6 +505,16 @@ class AppDelegate: NSObject, return event } + @objc private func windowDidBecomeKey(_ notification: Notification) { + guard let terminal = notification.object as? TerminalWindow else { + // If some other window became key we always turn this off + self.menuFloatOnTop?.state = .off + return + } + + self.menuFloatOnTop?.state = terminal.level == .floating ? .on : .off + } + @objc private func quickTerminalDidChangeVisibility(_ notification: Notification) { guard let quickController = notification.object as? QuickTerminalController else { return } self.menuQuickTerminal?.state = if (quickController.visible) { .on } else { .off } @@ -844,6 +862,22 @@ class AppDelegate: NSObject, hiddenState = nil } + @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) + } + } + @IBAction func bringAllToFront(_ sender: Any) { if !NSApp.isActive { NSApp.activate(ignoringOtherApps: true) @@ -899,3 +933,20 @@ class AppDelegate: NSObject, } } } + +// 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 + } + } +} diff --git a/macos/Sources/App/macOS/MainMenu.xib b/macos/Sources/App/macOS/MainMenu.xib index 8f7b16aa9..724f21355 100644 --- a/macos/Sources/App/macOS/MainMenu.xib +++ b/macos/Sources/App/macOS/MainMenu.xib @@ -25,6 +25,7 @@ + @@ -56,6 +57,7 @@ + @@ -402,6 +404,19 @@ + + + + + + + + + + + + + diff --git a/macos/Sources/Features/Terminal/TerminalWindow.swift b/macos/Sources/Features/Terminal/TerminalWindow.swift index 3209449e4..62b8dc5bf 100644 --- a/macos/Sources/Features/Terminal/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/TerminalWindow.swift @@ -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 { From 6e11d947e7ae6f37567faddb863b48107cdf278b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 1 May 2025 09:29:34 -0700 Subject: [PATCH 08/11] Binding for toggling window float on top (macOS only) This adds a keybinding and apprt action for #7237. --- include/ghostty.h | 9 +++ macos/Sources/App/macOS/AppDelegate.swift | 55 ++++++++------- macos/Sources/Ghostty/Ghostty.App.swift | 40 +++++++++++ macos/Sources/Ghostty/Package.swift | 22 ++++++ src/Surface.zig | 6 ++ src/apprt/action.zig | 11 +++ src/apprt/glfw.zig | 1 + src/apprt/gtk/App.zig | 1 + src/input/Binding.zig | 82 +++++++++++++---------- src/input/command.zig | 6 ++ 10 files changed, 173 insertions(+), 60 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index f7504eb7e..18c547910 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -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; diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 169f57ff2..a3a3185d9 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -414,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) @@ -506,13 +507,7 @@ class AppDelegate: NSObject, } @objc private func windowDidBecomeKey(_ notification: Notification) { - guard let terminal = notification.object as? TerminalWindow else { - // If some other window became key we always turn this off - self.menuFloatOnTop?.state = .off - return - } - - self.menuFloatOnTop?.state = terminal.level == .floating ? .on : .off + syncFloatOnTopMenu(notification.object as? NSWindow) } @objc private func quickTerminalDidChangeVisibility(_ notification: Notification) { @@ -862,22 +857,6 @@ class AppDelegate: NSObject, hiddenState = nil } - @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) - } - } - @IBAction func bringAllToFront(_ sender: Any) { if !NSApp.isActive { NSApp.activate(ignoringOtherApps: true) @@ -934,6 +913,36 @@ 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 { diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 677129960..c06287087 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -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, diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index e2c770899..366bb8113 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -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 diff --git a/src/Surface.zig b/src/Surface.zig index c776fed36..6e62f6639 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -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, diff --git a/src/apprt/action.zig b/src/apprt/action.zig index da0ebf8e6..4be296f09 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -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, diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 66b994051..9d1c8a6b5 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -235,6 +235,7 @@ pub const App = struct { .inspector, .render_inspector, .quit_timer, + .float_window, .secure_input, .key_sequence, .desktop_notification, diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 5373e578c..2de22d8c2 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -488,6 +488,7 @@ pub fn performAction( // Unimplemented .close_all_windows, + .float_window, .toggle_command_palette, .toggle_visibility, .cell_size, diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 1a2961a53..10e16f1fe 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -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, diff --git a/src/input/command.zig b/src/input/command.zig index c757736c7..701d537a1 100644 --- a/src/input/command.zig +++ b/src/input/command.zig @@ -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", From b2138eeaf004eb9b62e910df3cc95acdd086d051 Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Fri, 2 May 2025 10:59:52 -0400 Subject: [PATCH 09/11] codeowners: correct shell_integration.zig filename --- CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 0665aa407..3d8a4da3d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -144,7 +144,7 @@ # Shell /src/shell-integration/ @ghostty-org/shell -/src/termio/shell-integration.zig @ghostty-org/shell +/src/termio/shell_integration.zig @ghostty-org/shell # Terminal /src/simd/ @ghostty-org/terminal From c0f41aba45ce85f5798ff14a95586b16b0cd5da2 Mon Sep 17 00:00:00 2001 From: Leorize Date: Fri, 2 May 2025 14:06:01 -0500 Subject: [PATCH 10/11] flatpak: update GNOME runtime to 48 Notable dependencies updates: - GTK 4.16.13 -> 4.18.4 - Libadwaita 1.6.6 -> 1.7.2 --- flatpak/com.mitchellh.ghostty.Devel.yml | 2 +- flatpak/com.mitchellh.ghostty.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/flatpak/com.mitchellh.ghostty.Devel.yml b/flatpak/com.mitchellh.ghostty.Devel.yml index a939fda9a..244c3987f 100644 --- a/flatpak/com.mitchellh.ghostty.Devel.yml +++ b/flatpak/com.mitchellh.ghostty.Devel.yml @@ -1,6 +1,6 @@ app-id: com.mitchellh.ghostty.Devel runtime: org.gnome.Platform -runtime-version: "47" +runtime-version: "48" sdk: org.gnome.Sdk sdk-extensions: - org.freedesktop.Sdk.Extension.ziglang diff --git a/flatpak/com.mitchellh.ghostty.yml b/flatpak/com.mitchellh.ghostty.yml index 6f387481c..17c92633f 100644 --- a/flatpak/com.mitchellh.ghostty.yml +++ b/flatpak/com.mitchellh.ghostty.yml @@ -1,6 +1,6 @@ app-id: com.mitchellh.ghostty runtime: org.gnome.Platform -runtime-version: "47" +runtime-version: "48" sdk: org.gnome.Sdk sdk-extensions: - org.freedesktop.Sdk.Extension.ziglang From e174599533d79f0b744d570a943c536efe689027 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 3 May 2025 07:12:28 -0700 Subject: [PATCH 11/11] ci: workaround broken lxd start with snap builder https://discourse.ubuntu.com/t/lxd-doesn-t-start-snap-lxd-device-directory-nonexistent/59785 https://github.com/canonical/lxd-pkg-snap/pull/789 This is required until Namespace or further upstream fixes are made. --- .github/workflows/test.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index be28d50fb..e6e3a77a0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -374,6 +374,11 @@ jobs: /zig - run: sudo apt install -y udev - run: sudo systemctl start systemd-udevd + # Workaround until this is fixed: https://github.com/canonical/lxd-pkg-snap/pull/789 + - run: | + _LXD_SNAP_DEVCGROUP_CONFIG="/var/lib/snapd/cgroup/snap.lxd.device" + sudo mkdir -p /var/lib/snapd/cgroup + echo 'self-managed=true' | sudo tee "${_LXD_SNAP_DEVCGROUP_CONFIG}" - uses: snapcore/action-build@v1 with: path: dist