Merge branch 'ghostty-org:main' into invertCursor

This commit is contained in:
alienman5k
2025-05-04 08:54:42 -07:00
committed by GitHub
22 changed files with 279 additions and 270 deletions

View File

@ -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: (
"<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | "
f"<level>{LEVEL_MAP[record['level'].name]}</level> | "
"<cyan>{function}</cyan>:<cyan>{line}</cyan> - "
"<level>{message}</level>\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())

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

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

@ -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
)
}
}

View File

@ -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

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

@ -2280,6 +2280,18 @@ keybind: Keybinds = .{},
/// Custom CSS files to be loaded.
///
/// GTK CSS documentation can be found at the following links:
///
/// * <https://docs.gtk.org/gtk4/css-overview.html> - An overview of GTK CSS.
/// * <https://docs.gtk.org/gtk4/css-properties.html> - 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
/// <https://developer.gnome.org/documentation/tools/inspector.html> 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 ?

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",

View File

@ -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);
}