mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
Merge branch 'ghostty-org:main' into ms_MY
This commit is contained in:
120
.github/scripts/request_review.py
vendored
120
.github/scripts/request_review.py
vendored
@ -2,25 +2,66 @@
|
|||||||
# requires-python = ">=3.9"
|
# requires-python = ">=3.9"
|
||||||
# dependencies = [
|
# dependencies = [
|
||||||
# "githubkit",
|
# "githubkit",
|
||||||
|
# "loguru",
|
||||||
# ]
|
# ]
|
||||||
# ///
|
# ///
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
|
from collections.abc import Iterator
|
||||||
|
from contextlib import contextmanager
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
|
|
||||||
from githubkit import GitHub
|
from githubkit import GitHub
|
||||||
|
from githubkit.exception import RequestFailed
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
ORG_NAME = "ghostty-org"
|
ORG_NAME = "ghostty-org"
|
||||||
REPO_NAME = "ghostty"
|
REPO_NAME = "ghostty"
|
||||||
ALLOWED_PARENT_TEAM = "localization"
|
ALLOWED_PARENT_TEAM = "localization"
|
||||||
LOCALIZATION_TEAM_NAME_PATTERN = re.compile(r"[a-z]{2}_[A-Z]{2}")
|
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"])
|
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]:
|
async def fetch_and_parse_codeowners() -> dict[str, str]:
|
||||||
|
logger.debug("Fetching CODEOWNERS file...")
|
||||||
|
with log_fail("Failed to fetch CODEOWNERS file"):
|
||||||
content = (
|
content = (
|
||||||
await gh.rest.repos.async_get_content(
|
await gh.rest.repos.async_get_content(
|
||||||
ORG_NAME,
|
ORG_NAME,
|
||||||
@ -30,28 +71,47 @@ async def fetch_and_parse_codeowners() -> dict[str, str]:
|
|||||||
)
|
)
|
||||||
).text
|
).text
|
||||||
|
|
||||||
|
logger.debug("Parsing CODEOWNERS file...")
|
||||||
codeowners: dict[str, str] = {}
|
codeowners: dict[str, str] = {}
|
||||||
for line in content.splitlines():
|
for line in content.splitlines():
|
||||||
if not line or line.lstrip().startswith("#"):
|
if not line or line.lstrip().startswith("#"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# This assumes that all entries only list one owner
|
# This assumes that all entries only list one owner
|
||||||
# and that this owner is a team (ghostty-org/foobar)
|
# and that this owner is a team (ghostty-org/foobar)
|
||||||
path, owner = line.split()
|
path, owner = line.split()
|
||||||
codeowners[path.lstrip("/")] = owner.removeprefix(f"@{ORG_NAME}/")
|
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
|
return codeowners
|
||||||
|
|
||||||
|
|
||||||
async def get_team_members(team_name: str) -> list[str]:
|
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
|
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:
|
if team.parent and team.parent.slug == ALLOWED_PARENT_TEAM:
|
||||||
members = (
|
logger.debug(f"Fetching team {team_name!r} members...")
|
||||||
await gh.rest.teams.async_list_members_in_org(ORG_NAME, team_name)
|
with log_fail(f"Failed to fetch team {team_name!r} members"):
|
||||||
).parsed_data
|
resp = await gh.rest.teams.async_list_members_in_org(ORG_NAME, team_name)
|
||||||
return [m.login for m in members]
|
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 []
|
return []
|
||||||
|
|
||||||
|
|
||||||
async def get_changed_files(pr_number: int) -> list[str]:
|
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 = (
|
diff_entries = (
|
||||||
await gh.rest.pulls.async_list_files(
|
await gh.rest.pulls.async_list_files(
|
||||||
ORG_NAME,
|
ORG_NAME,
|
||||||
@ -64,51 +124,65 @@ async def get_changed_files(pr_number: int) -> list[str]:
|
|||||||
return [d.filename for d in diff_entries]
|
return [d.filename for d in diff_entries]
|
||||||
|
|
||||||
|
|
||||||
async def request_review(pr_number: int, pr_author: str, *users: str) -> None:
|
async def request_review(pr_number: int, user: str, pr_author: str) -> None:
|
||||||
await asyncio.gather(
|
if user == pr_author:
|
||||||
*(
|
logger.debug(f"Skipping review request for {user!r} (is PR author)")
|
||||||
gh.rest.pulls.async_request_reviewers(
|
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,
|
ORG_NAME,
|
||||||
REPO_NAME,
|
REPO_NAME,
|
||||||
pr_number,
|
pr_number,
|
||||||
headers={"Accept": "application/vnd.github+json"},
|
headers={"Accept": "application/vnd.github+json"},
|
||||||
data={"reviewers": [user]},
|
data={"reviewers": [user]},
|
||||||
)
|
)
|
||||||
for user in users
|
|
||||||
if user != pr_author
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def is_localization_team(team_name: str) -> bool:
|
def is_localization_team(team_name: str) -> bool:
|
||||||
return LOCALIZATION_TEAM_NAME_PATTERN.fullmatch(team_name) is not None
|
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:
|
async def main() -> None:
|
||||||
|
logger.debug("Reading PR number...")
|
||||||
pr_number = int(os.environ["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)
|
changed_files = await get_changed_files(pr_number)
|
||||||
pr_author = (
|
logger.debug(f"Changed files: {', '.join(map(repr, changed_files))}")
|
||||||
await gh.rest.pulls.async_get(ORG_NAME, REPO_NAME, pr_number)
|
|
||||||
).parsed_data.user.login
|
pr_author = await get_pr_author(pr_number)
|
||||||
localization_codewners = {
|
codeowners = await fetch_and_parse_codeowners()
|
||||||
path: owner
|
|
||||||
for path, owner in (await fetch_and_parse_codeowners()).items()
|
|
||||||
if is_localization_team(owner)
|
|
||||||
}
|
|
||||||
|
|
||||||
found_owners = set[str]()
|
found_owners = set[str]()
|
||||||
for file in changed_files:
|
for file in changed_files:
|
||||||
for path, owner in localization_codewners.items():
|
logger.debug(f"Finding owner for {file!r}...")
|
||||||
|
for path, owner in codeowners.items():
|
||||||
if file.startswith(path):
|
if file.startswith(path):
|
||||||
|
logger.debug(f"Found owner: {owner!r}")
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
|
logger.debug("No owner found")
|
||||||
continue
|
continue
|
||||||
found_owners.add(owner)
|
found_owners.add(owner)
|
||||||
|
|
||||||
member_lists = await asyncio.gather(
|
member_lists = await asyncio.gather(
|
||||||
*(get_team_members(owner) for owner in found_owners)
|
*(get_team_members(owner) for owner in found_owners)
|
||||||
)
|
)
|
||||||
await request_review(pr_number, pr_author, *chain.from_iterable(member_lists))
|
await asyncio.gather(
|
||||||
|
*(
|
||||||
|
request_review(pr_number, user, pr_author)
|
||||||
|
for user in chain.from_iterable(member_lists)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
@ -601,6 +601,7 @@ typedef enum {
|
|||||||
GHOSTTY_ACTION_RELOAD_CONFIG,
|
GHOSTTY_ACTION_RELOAD_CONFIG,
|
||||||
GHOSTTY_ACTION_CONFIG_CHANGE,
|
GHOSTTY_ACTION_CONFIG_CHANGE,
|
||||||
GHOSTTY_ACTION_CLOSE_WINDOW,
|
GHOSTTY_ACTION_CLOSE_WINDOW,
|
||||||
|
GHOSTTY_ACTION_RING_BELL,
|
||||||
} ghostty_action_tag_e;
|
} ghostty_action_tag_e;
|
||||||
|
|
||||||
typedef union {
|
typedef union {
|
||||||
|
@ -55,6 +55,8 @@
|
|||||||
A571AB1D2A206FCF00248498 /* GhosttyKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */; };
|
A571AB1D2A206FCF00248498 /* GhosttyKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */; };
|
||||||
A57D79272C9C879B001D522E /* SecureInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = A57D79262C9C8798001D522E /* SecureInput.swift */; };
|
A57D79272C9C879B001D522E /* SecureInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = A57D79262C9C8798001D522E /* SecureInput.swift */; };
|
||||||
A586167C2B7703CC009BDB1D /* fish in Resources */ = {isa = PBXBuildFile; fileRef = A586167B2B7703CC009BDB1D /* fish */; };
|
A586167C2B7703CC009BDB1D /* fish in Resources */ = {isa = PBXBuildFile; fileRef = A586167B2B7703CC009BDB1D /* fish */; };
|
||||||
|
A5874D992DAD751B00E83852 /* CGS.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5874D982DAD751A00E83852 /* CGS.swift */; };
|
||||||
|
A5874D9D2DAD786100E83852 /* NSWindow+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5874D9C2DAD785F00E83852 /* NSWindow+Extension.swift */; };
|
||||||
A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59444F629A2ED5200725BBA /* SettingsView.swift */; };
|
A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59444F629A2ED5200725BBA /* SettingsView.swift */; };
|
||||||
A59630972AEE163600D64628 /* HostingWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59630962AEE163600D64628 /* HostingWindow.swift */; };
|
A59630972AEE163600D64628 /* HostingWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59630962AEE163600D64628 /* HostingWindow.swift */; };
|
||||||
A596309A2AEE1C6400D64628 /* Terminal.xib in Resources */ = {isa = PBXBuildFile; fileRef = A59630992AEE1C6400D64628 /* Terminal.xib */; };
|
A596309A2AEE1C6400D64628 /* Terminal.xib in Resources */ = {isa = PBXBuildFile; fileRef = A59630992AEE1C6400D64628 /* Terminal.xib */; };
|
||||||
@ -154,6 +156,8 @@
|
|||||||
A571AB1C2A206FC600248498 /* Ghostty-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "Ghostty-Info.plist"; sourceTree = "<group>"; };
|
A571AB1C2A206FC600248498 /* Ghostty-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "Ghostty-Info.plist"; sourceTree = "<group>"; };
|
||||||
A57D79262C9C8798001D522E /* SecureInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureInput.swift; sourceTree = "<group>"; };
|
A57D79262C9C8798001D522E /* SecureInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureInput.swift; sourceTree = "<group>"; };
|
||||||
A586167B2B7703CC009BDB1D /* fish */ = {isa = PBXFileReference; lastKnownFileType = folder; name = fish; path = "../zig-out/share/fish"; sourceTree = "<group>"; };
|
A586167B2B7703CC009BDB1D /* fish */ = {isa = PBXFileReference; lastKnownFileType = folder; name = fish; path = "../zig-out/share/fish"; sourceTree = "<group>"; };
|
||||||
|
A5874D982DAD751A00E83852 /* CGS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGS.swift; sourceTree = "<group>"; };
|
||||||
|
A5874D9C2DAD785F00E83852 /* NSWindow+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSWindow+Extension.swift"; sourceTree = "<group>"; };
|
||||||
A59444F629A2ED5200725BBA /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
A59444F629A2ED5200725BBA /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||||
A59630962AEE163600D64628 /* HostingWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostingWindow.swift; sourceTree = "<group>"; };
|
A59630962AEE163600D64628 /* HostingWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostingWindow.swift; sourceTree = "<group>"; };
|
||||||
A59630992AEE1C6400D64628 /* Terminal.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = Terminal.xib; sourceTree = "<group>"; };
|
A59630992AEE1C6400D64628 /* Terminal.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = Terminal.xib; sourceTree = "<group>"; };
|
||||||
@ -274,13 +278,13 @@
|
|||||||
A534263D2A7DCBB000EBB7A2 /* Helpers */ = {
|
A534263D2A7DCBB000EBB7A2 /* Helpers */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
A5874D9B2DAD781100E83852 /* Private */,
|
||||||
A5AEB1642D5BE7BF00513529 /* LastWindowPosition.swift */,
|
A5AEB1642D5BE7BF00513529 /* LastWindowPosition.swift */,
|
||||||
A5A6F7292CC41B8700B232A5 /* Xcode.swift */,
|
A5A6F7292CC41B8700B232A5 /* Xcode.swift */,
|
||||||
A5CEAFFE29C2410700646FDA /* Backport.swift */,
|
A5CEAFFE29C2410700646FDA /* Backport.swift */,
|
||||||
A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */,
|
A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */,
|
||||||
A5CBD0572C9F30860017A1AE /* Cursor.swift */,
|
A5CBD0572C9F30860017A1AE /* Cursor.swift */,
|
||||||
A5D0AF3C2B37804400D21823 /* CodableBridge.swift */,
|
A5D0AF3C2B37804400D21823 /* CodableBridge.swift */,
|
||||||
A5A2A3C92D4445E20033CF96 /* Dock.swift */,
|
|
||||||
A52FFF582CAA4FF1000C6A5B /* Fullscreen.swift */,
|
A52FFF582CAA4FF1000C6A5B /* Fullscreen.swift */,
|
||||||
A59630962AEE163600D64628 /* HostingWindow.swift */,
|
A59630962AEE163600D64628 /* HostingWindow.swift */,
|
||||||
A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */,
|
A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */,
|
||||||
@ -293,6 +297,7 @@
|
|||||||
A52FFF5C2CAB4D05000C6A5B /* NSScreen+Extension.swift */,
|
A52FFF5C2CAB4D05000C6A5B /* NSScreen+Extension.swift */,
|
||||||
C1F26EA62B738B9900404083 /* NSView+Extension.swift */,
|
C1F26EA62B738B9900404083 /* NSView+Extension.swift */,
|
||||||
AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */,
|
AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */,
|
||||||
|
A5874D9C2DAD785F00E83852 /* NSWindow+Extension.swift */,
|
||||||
A5985CD62C320C4500C57AD3 /* String+Extension.swift */,
|
A5985CD62C320C4500C57AD3 /* String+Extension.swift */,
|
||||||
A5CC36142C9CDA03004D6760 /* View+Extension.swift */,
|
A5CC36142C9CDA03004D6760 /* View+Extension.swift */,
|
||||||
A5CA378D2D31D6C100931030 /* Weak.swift */,
|
A5CA378D2D31D6C100931030 /* Weak.swift */,
|
||||||
@ -403,6 +408,15 @@
|
|||||||
path = "Secure Input";
|
path = "Secure Input";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
A5874D9B2DAD781100E83852 /* Private */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
A5874D982DAD751A00E83852 /* CGS.swift */,
|
||||||
|
A5A2A3C92D4445E20033CF96 /* Dock.swift */,
|
||||||
|
);
|
||||||
|
path = Private;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
A59630982AEE1C4400D64628 /* Terminal */ = {
|
A59630982AEE1C4400D64628 /* Terminal */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@ -634,6 +648,7 @@
|
|||||||
A59630A42AF059BB00D64628 /* Ghostty.SplitNode.swift in Sources */,
|
A59630A42AF059BB00D64628 /* Ghostty.SplitNode.swift in Sources */,
|
||||||
A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */,
|
A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */,
|
||||||
A54B0CEB2D0CFB4C00CBEFF8 /* NSImage+Extension.swift in Sources */,
|
A54B0CEB2D0CFB4C00CBEFF8 /* NSImage+Extension.swift in Sources */,
|
||||||
|
A5874D9D2DAD786100E83852 /* NSWindow+Extension.swift in Sources */,
|
||||||
A54D786C2CA7978E001B19B1 /* BaseTerminalController.swift in Sources */,
|
A54D786C2CA7978E001B19B1 /* BaseTerminalController.swift in Sources */,
|
||||||
A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */,
|
A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */,
|
||||||
CFBB5FEA2D231E5000FD62EE /* QuickTerminalSpaceBehavior.swift in Sources */,
|
CFBB5FEA2D231E5000FD62EE /* QuickTerminalSpaceBehavior.swift in Sources */,
|
||||||
@ -669,6 +684,7 @@
|
|||||||
A5CDF1952AAFA19600513312 /* ConfigurationErrorsView.swift in Sources */,
|
A5CDF1952AAFA19600513312 /* ConfigurationErrorsView.swift in Sources */,
|
||||||
A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */,
|
A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */,
|
||||||
A5333E1C2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */,
|
A5333E1C2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */,
|
||||||
|
A5874D992DAD751B00E83852 /* CGS.swift in Sources */,
|
||||||
A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */,
|
A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */,
|
||||||
A56D58862ACDDB4100508D2C /* Ghostty.Shell.swift in Sources */,
|
A56D58862ACDDB4100508D2C /* Ghostty.Shell.swift in Sources */,
|
||||||
A5985CD72C320C4500C57AD3 /* String+Extension.swift in Sources */,
|
A5985CD72C320C4500C57AD3 /* String+Extension.swift in Sources */,
|
||||||
|
@ -3,12 +3,6 @@ import Cocoa
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import GhosttyKit
|
import GhosttyKit
|
||||||
|
|
||||||
// This is a Apple's private function that we need to call to get the active space.
|
|
||||||
@_silgen_name("CGSGetActiveSpace")
|
|
||||||
func CGSGetActiveSpace(_ cid: Int) -> size_t
|
|
||||||
@_silgen_name("CGSMainConnectionID")
|
|
||||||
func CGSMainConnectionID() -> Int
|
|
||||||
|
|
||||||
/// Controller for the "quick" terminal.
|
/// Controller for the "quick" terminal.
|
||||||
class QuickTerminalController: BaseTerminalController {
|
class QuickTerminalController: BaseTerminalController {
|
||||||
override var windowNibName: NSNib.Name? { "QuickTerminal" }
|
override var windowNibName: NSNib.Name? { "QuickTerminal" }
|
||||||
@ -25,7 +19,7 @@ class QuickTerminalController: BaseTerminalController {
|
|||||||
private var previousApp: NSRunningApplication? = nil
|
private var previousApp: NSRunningApplication? = nil
|
||||||
|
|
||||||
// The active space when the quick terminal was last shown.
|
// The active space when the quick terminal was last shown.
|
||||||
private var previousActiveSpace: size_t = 0
|
private var previousActiveSpace: CGSSpace? = nil
|
||||||
|
|
||||||
/// Non-nil if we have hidden dock state.
|
/// Non-nil if we have hidden dock state.
|
||||||
private var hiddenDock: HiddenDock? = nil
|
private var hiddenDock: HiddenDock? = nil
|
||||||
@ -51,7 +45,7 @@ class QuickTerminalController: BaseTerminalController {
|
|||||||
object: nil)
|
object: nil)
|
||||||
center.addObserver(
|
center.addObserver(
|
||||||
self,
|
self,
|
||||||
selector: #selector(onToggleFullscreen),
|
selector: #selector(onToggleFullscreen(notification:)),
|
||||||
name: Ghostty.Notification.ghosttyToggleFullscreen,
|
name: Ghostty.Notification.ghosttyToggleFullscreen,
|
||||||
object: nil)
|
object: nil)
|
||||||
center.addObserver(
|
center.addObserver(
|
||||||
@ -154,14 +148,24 @@ class QuickTerminalController: BaseTerminalController {
|
|||||||
animateOut()
|
animateOut()
|
||||||
|
|
||||||
case .move:
|
case .move:
|
||||||
let currentActiveSpace = CGSGetActiveSpace(CGSMainConnectionID())
|
let currentActiveSpace = CGSSpace.active()
|
||||||
if previousActiveSpace == currentActiveSpace {
|
if previousActiveSpace == currentActiveSpace {
|
||||||
// We haven't moved spaces. We lost focus to another app on the
|
// We haven't moved spaces. We lost focus to another app on the
|
||||||
// current space. Animate out.
|
// current space. Animate out.
|
||||||
animateOut()
|
animateOut()
|
||||||
} else {
|
} else {
|
||||||
// We've moved to a different space. Bring the quick terminal back
|
// We've moved to a different space.
|
||||||
// into view.
|
|
||||||
|
// If we're fullscreen, we need to exit fullscreen because the visible
|
||||||
|
// bounds may have changed causing a new behavior.
|
||||||
|
if let fullscreenStyle, fullscreenStyle.isFullscreen {
|
||||||
|
fullscreenStyle.exit()
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.onToggleFullscreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make the window visible again on this space
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.window?.makeKeyAndOrderFront(nil)
|
self.window?.makeKeyAndOrderFront(nil)
|
||||||
}
|
}
|
||||||
@ -224,7 +228,7 @@ class QuickTerminalController: BaseTerminalController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set previous active space
|
// Set previous active space
|
||||||
self.previousActiveSpace = CGSGetActiveSpace(CGSMainConnectionID())
|
self.previousActiveSpace = CGSSpace.active()
|
||||||
|
|
||||||
// Animate the window in
|
// Animate the window in
|
||||||
animateWindowIn(window: window, from: position)
|
animateWindowIn(window: window, from: position)
|
||||||
@ -485,9 +489,23 @@ class QuickTerminalController: BaseTerminalController {
|
|||||||
@objc private func onToggleFullscreen(notification: SwiftUI.Notification) {
|
@objc private func onToggleFullscreen(notification: SwiftUI.Notification) {
|
||||||
guard let target = notification.object as? Ghostty.SurfaceView else { return }
|
guard let target = notification.object as? Ghostty.SurfaceView else { return }
|
||||||
guard target == self.focusedSurface else { return }
|
guard target == self.focusedSurface else { return }
|
||||||
|
onToggleFullscreen()
|
||||||
|
}
|
||||||
|
|
||||||
// We ignore the requested mode and always use non-native for the quick terminal
|
private func onToggleFullscreen() {
|
||||||
toggleFullscreen(mode: .nonNative)
|
// We ignore the configured fullscreen style and always use non-native
|
||||||
|
// because the way the quick terminal works doesn't support native.
|
||||||
|
//
|
||||||
|
// An additional detail is that if the is NOT frontmost, then our
|
||||||
|
// NSApp.presentationOptions will not take effect so we must always
|
||||||
|
// do the visible menu mode since we can't get rid of the menu.
|
||||||
|
let mode: FullscreenMode = if (NSApp.isFrontmost) {
|
||||||
|
.nonNative
|
||||||
|
} else {
|
||||||
|
.nonNativeVisibleMenu
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleFullscreen(mode: mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func ghosttyConfigDidChange(_ notification: Notification) {
|
@objc private func ghosttyConfigDidChange(_ notification: Notification) {
|
||||||
|
@ -743,6 +743,13 @@ extension Ghostty {
|
|||||||
override func mouseExited(with event: NSEvent) {
|
override func mouseExited(with event: NSEvent) {
|
||||||
guard let surface = self.surface else { return }
|
guard let surface = self.surface else { return }
|
||||||
|
|
||||||
|
// If the mouse is being dragged then we don't have to emit
|
||||||
|
// this because we get mouse drag events even if we've already
|
||||||
|
// exited the viewport (i.e. mouseDragged)
|
||||||
|
if NSEvent.pressedMouseButtons != 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Negative values indicate cursor has left the viewport
|
// Negative values indicate cursor has left the viewport
|
||||||
let mods = Ghostty.ghosttyMods(event.modifierFlags)
|
let mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||||
ghostty_surface_mouse_pos(surface, -1, -1, mods)
|
ghostty_surface_mouse_pos(surface, -1, -1, mods)
|
||||||
|
@ -180,7 +180,7 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Hide the menu if requested
|
// Hide the menu if requested
|
||||||
if (properties.hideMenu) {
|
if (properties.hideMenu && savedState.menu) {
|
||||||
hideMenu()
|
hideMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -224,7 +224,9 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle {
|
|||||||
if savedState.dock {
|
if savedState.dock {
|
||||||
unhideDock()
|
unhideDock()
|
||||||
}
|
}
|
||||||
|
if (properties.hideMenu && savedState.menu) {
|
||||||
unhideMenu()
|
unhideMenu()
|
||||||
|
}
|
||||||
|
|
||||||
// Restore our saved state
|
// Restore our saved state
|
||||||
window.styleMask = savedState.styleMask
|
window.styleMask = savedState.styleMask
|
||||||
@ -273,7 +275,8 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle {
|
|||||||
// calculate this ourselves.
|
// calculate this ourselves.
|
||||||
var frame = screen.frame
|
var frame = screen.frame
|
||||||
|
|
||||||
if (!properties.hideMenu) {
|
if (!NSApp.presentationOptions.contains(.autoHideMenuBar) &&
|
||||||
|
!NSApp.presentationOptions.contains(.hideMenuBar)) {
|
||||||
// We need to subtract the menu height since we're still showing it.
|
// We need to subtract the menu height since we're still showing it.
|
||||||
frame.size.height -= NSApp.mainMenu?.menuBarHeight ?? 0
|
frame.size.height -= NSApp.mainMenu?.menuBarHeight ?? 0
|
||||||
|
|
||||||
@ -340,6 +343,7 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle {
|
|||||||
let contentFrame: NSRect
|
let contentFrame: NSRect
|
||||||
let styleMask: NSWindow.StyleMask
|
let styleMask: NSWindow.StyleMask
|
||||||
let dock: Bool
|
let dock: Bool
|
||||||
|
let menu: Bool
|
||||||
|
|
||||||
init?(_ window: NSWindow) {
|
init?(_ window: NSWindow) {
|
||||||
guard let contentView = window.contentView else { return nil }
|
guard let contentView = window.contentView else { return nil }
|
||||||
@ -350,6 +354,18 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle {
|
|||||||
self.contentFrame = window.convertToScreen(contentView.frame)
|
self.contentFrame = window.convertToScreen(contentView.frame)
|
||||||
self.styleMask = window.styleMask
|
self.styleMask = window.styleMask
|
||||||
self.dock = window.screen?.hasDock ?? false
|
self.dock = window.screen?.hasDock ?? false
|
||||||
|
|
||||||
|
// We hide the menu only if this window is not on any fullscreen
|
||||||
|
// spaces. We do this because fullscreen spaces already hide the
|
||||||
|
// menu and if we insert/remove this presentation option we get
|
||||||
|
// issues (see #7075)
|
||||||
|
let activeSpace = CGSSpace.active()
|
||||||
|
let spaces = CGSSpace.list(for: window.cgWindowId)
|
||||||
|
if spaces.contains(activeSpace) {
|
||||||
|
self.menu = activeSpace.type != .fullscreen
|
||||||
|
} else {
|
||||||
|
self.menu = spaces.allSatisfy { $0.type != .fullscreen }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
|
// MARK: Presentation Options
|
||||||
|
|
||||||
extension NSApplication {
|
extension NSApplication {
|
||||||
private static var presentationOptionCounts: [NSApplication.PresentationOptions.Element: UInt] = [:]
|
private static var presentationOptionCounts: [NSApplication.PresentationOptions.Element: UInt] = [:]
|
||||||
|
|
||||||
@ -29,3 +31,13 @@ extension NSApplication.PresentationOptions.Element: @retroactive Hashable {
|
|||||||
hasher.combine(rawValue)
|
hasher.combine(rawValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Frontmost
|
||||||
|
|
||||||
|
extension NSApplication {
|
||||||
|
/// True if the application is frontmost. This isn't exactly the same as isActive because
|
||||||
|
/// an app can be active but not be frontmost if the window with activity is an NSPanel.
|
||||||
|
var isFrontmost: Bool {
|
||||||
|
NSWorkspace.shared.frontmostApplication?.bundleIdentifier == Bundle.main.bundleIdentifier
|
||||||
|
}
|
||||||
|
}
|
||||||
|
8
macos/Sources/Helpers/NSWindow+Extension.swift
Normal file
8
macos/Sources/Helpers/NSWindow+Extension.swift
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import AppKit
|
||||||
|
|
||||||
|
extension NSWindow {
|
||||||
|
/// Get the CGWindowID type for the window (used for low level CoreGraphics APIs).
|
||||||
|
var cgWindowId: CGWindowID {
|
||||||
|
CGWindowID(windowNumber)
|
||||||
|
}
|
||||||
|
}
|
81
macos/Sources/Helpers/Private/CGS.swift
Normal file
81
macos/Sources/Helpers/Private/CGS.swift
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import AppKit
|
||||||
|
|
||||||
|
// MARK: - CGS Private API Declarations
|
||||||
|
|
||||||
|
typealias CGSConnectionID = Int32
|
||||||
|
typealias CGSSpaceID = size_t
|
||||||
|
|
||||||
|
@_silgen_name("CGSMainConnectionID")
|
||||||
|
private func CGSMainConnectionID() -> CGSConnectionID
|
||||||
|
|
||||||
|
@_silgen_name("CGSGetActiveSpace")
|
||||||
|
private func CGSGetActiveSpace(_ cid: CGSConnectionID) -> CGSSpaceID
|
||||||
|
|
||||||
|
@_silgen_name("CGSSpaceGetType")
|
||||||
|
private func CGSSpaceGetType(_ cid: CGSConnectionID, _ spaceID: CGSSpaceID) -> CGSSpaceType
|
||||||
|
|
||||||
|
@_silgen_name("CGSCopySpacesForWindows")
|
||||||
|
func CGSCopySpacesForWindows(
|
||||||
|
_ cid: CGSConnectionID,
|
||||||
|
_ mask: CGSSpaceMask,
|
||||||
|
_ windowIDs: CFArray
|
||||||
|
) -> Unmanaged<CFArray>?
|
||||||
|
|
||||||
|
// MARK: - CGS Space
|
||||||
|
|
||||||
|
/// https://github.com/NUIKit/CGSInternal/blob/c4f6f559d624dc1cfc2bf24c8c19dbf653317fcf/CGSSpace.h#L40
|
||||||
|
/// converted to Swift
|
||||||
|
struct CGSSpaceMask: OptionSet {
|
||||||
|
let rawValue: UInt32
|
||||||
|
|
||||||
|
static let includesCurrent = CGSSpaceMask(rawValue: 1 << 0)
|
||||||
|
static let includesOthers = CGSSpaceMask(rawValue: 1 << 1)
|
||||||
|
static let includesUser = CGSSpaceMask(rawValue: 1 << 2)
|
||||||
|
|
||||||
|
static let includesVisible = CGSSpaceMask(rawValue: 1 << 16)
|
||||||
|
|
||||||
|
static let currentSpace: CGSSpaceMask = [.includesUser, .includesCurrent]
|
||||||
|
static let otherSpaces: CGSSpaceMask = [.includesOthers, .includesCurrent]
|
||||||
|
static let allSpaces: CGSSpaceMask = [.includesUser, .includesOthers, .includesCurrent]
|
||||||
|
static let allVisibleSpaces: CGSSpaceMask = [.includesVisible, .allSpaces]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a unique identifier for a macOS Space (Desktop, Fullscreen, etc).
|
||||||
|
struct CGSSpace: Hashable, CustomStringConvertible {
|
||||||
|
let rawValue: CGSSpaceID
|
||||||
|
|
||||||
|
var description: String {
|
||||||
|
"SpaceID(\(rawValue))"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the currently active space.
|
||||||
|
static func active() -> CGSSpace {
|
||||||
|
let space = CGSGetActiveSpace(CGSMainConnectionID())
|
||||||
|
return .init(rawValue: space)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List the spaces for the given window.
|
||||||
|
static func list(for windowID: CGWindowID, mask: CGSSpaceMask = .allSpaces) -> [CGSSpace] {
|
||||||
|
guard let spaces = CGSCopySpacesForWindows(
|
||||||
|
CGSMainConnectionID(),
|
||||||
|
mask,
|
||||||
|
[windowID] as CFArray
|
||||||
|
) else { return [] }
|
||||||
|
guard let spaceIDs = spaces.takeRetainedValue() as? [CGSSpaceID] else { return [] }
|
||||||
|
return spaceIDs.map(CGSSpace.init)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - CGS Space Types
|
||||||
|
|
||||||
|
enum CGSSpaceType: UInt32 {
|
||||||
|
case user = 0
|
||||||
|
case system = 2
|
||||||
|
case fullscreen = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CGSSpace {
|
||||||
|
var type: CGSSpaceType {
|
||||||
|
CGSSpaceGetType(CGSMainConnectionID(), rawValue)
|
||||||
|
}
|
||||||
|
}
|
@ -4,13 +4,14 @@
|
|||||||
# Hanna Rose <hanna@hanna.lol>, 2025.
|
# Hanna Rose <hanna@hanna.lol>, 2025.
|
||||||
# Uzair Aftab <uzaaft@outlook.com>, 2025.
|
# Uzair Aftab <uzaaft@outlook.com>, 2025.
|
||||||
# Christoffer Tønnessen <christoffer@cto.gg>, 2025.
|
# Christoffer Tønnessen <christoffer@cto.gg>, 2025.
|
||||||
|
# cryptocode <cryptocode@zolo.io>, 2025.
|
||||||
#
|
#
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: com.mitchellh.ghostty\n"
|
"Project-Id-Version: com.mitchellh.ghostty\n"
|
||||||
"Report-Msgid-Bugs-To: m@mitchellh.com\n"
|
"Report-Msgid-Bugs-To: m@mitchellh.com\n"
|
||||||
"PO-Revision-Date: 2025-03-19 09:52+0100\n"
|
"PO-Revision-Date: 2025-04-14 16:25+0200\n"
|
||||||
"Last-Translator: Christoffer Tønnessen <christoffer@cto.gg>\n"
|
"Last-Translator: cryptocode <cryptocode@zolo.io>\n"
|
||||||
"Language-Team: Norwegian Bokmal <l10n-no@lister.huftis.org>\n"
|
"Language-Team: Norwegian Bokmal <l10n-no@lister.huftis.org>\n"
|
||||||
"Language: nb\n"
|
"Language: nb\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
@ -162,7 +163,7 @@ msgstr "Avslutt"
|
|||||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6
|
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6
|
||||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6
|
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6
|
||||||
msgid "Authorize Clipboard Access"
|
msgid "Authorize Clipboard Access"
|
||||||
msgstr "Gi tilgang til utklippstavle"
|
msgstr "Gi tilgang til utklippstavlen"
|
||||||
|
|
||||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7
|
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7
|
||||||
msgid ""
|
msgid ""
|
||||||
@ -187,7 +188,7 @@ msgid ""
|
|||||||
"An application is attempting to write to the clipboard. The current "
|
"An application is attempting to write to the clipboard. The current "
|
||||||
"clipboard contents are shown below."
|
"clipboard contents are shown below."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"En applikasjon er forsøker å skrive til utklippstavlen. Gjeldende "
|
"En applikasjon forsøker å skrive til utklippstavlen. Gjeldende "
|
||||||
"utklippstavleinnhold er vist nedenfor."
|
"utklippstavleinnhold er vist nedenfor."
|
||||||
|
|
||||||
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6
|
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6
|
||||||
@ -208,7 +209,7 @@ msgstr "Ghostty: Terminalinspektør"
|
|||||||
|
|
||||||
#: src/apprt/gtk/Surface.zig:1243
|
#: src/apprt/gtk/Surface.zig:1243
|
||||||
msgid "Copied to clipboard"
|
msgid "Copied to clipboard"
|
||||||
msgstr "Kopiert til utklippstavle"
|
msgstr "Kopiert til utklippstavlen"
|
||||||
|
|
||||||
#: src/apprt/gtk/CloseDialog.zig:47
|
#: src/apprt/gtk/CloseDialog.zig:47
|
||||||
msgid "Close"
|
msgid "Close"
|
||||||
|
100
src/Surface.zig
100
src/Surface.zig
@ -930,6 +930,16 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
|
|||||||
.present_surface => try self.presentSurface(),
|
.present_surface => try self.presentSurface(),
|
||||||
|
|
||||||
.password_input => |v| try self.passwordInput(v),
|
.password_input => |v| try self.passwordInput(v),
|
||||||
|
|
||||||
|
.ring_bell => {
|
||||||
|
_ = self.rt_app.performAction(
|
||||||
|
.{ .surface = self },
|
||||||
|
.ring_bell,
|
||||||
|
{},
|
||||||
|
) catch |err| {
|
||||||
|
log.warn("apprt failed to ring bell={}", .{err});
|
||||||
|
};
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1031,9 +1041,64 @@ fn mouseRefreshLinks(
|
|||||||
// If the position is outside our viewport, do nothing
|
// If the position is outside our viewport, do nothing
|
||||||
if (pos.x < 0 or pos.y < 0) return;
|
if (pos.x < 0 or pos.y < 0) return;
|
||||||
|
|
||||||
|
// Update the last point that we checked for links so we don't
|
||||||
|
// recheck if the mouse moves some pixels to the same point.
|
||||||
self.mouse.link_point = pos_vp;
|
self.mouse.link_point = pos_vp;
|
||||||
|
|
||||||
if (try self.linkAtPos(pos)) |link| {
|
// We use an arena for everything below to make things easy to clean up.
|
||||||
|
// In the case we don't do any allocs this is very cheap to setup
|
||||||
|
// (effectively just struct init).
|
||||||
|
var arena = ArenaAllocator.init(self.alloc);
|
||||||
|
defer arena.deinit();
|
||||||
|
const alloc = arena.allocator();
|
||||||
|
|
||||||
|
// Get our link at the current position. This returns null if there
|
||||||
|
// isn't a link OR if we shouldn't be showing links for some reason
|
||||||
|
// (see further comments for cases).
|
||||||
|
const link_: ?apprt.action.MouseOverLink = link: {
|
||||||
|
// If we clicked and our mouse moved cells then we never
|
||||||
|
// highlight links until the mouse is unclicked. This follows
|
||||||
|
// standard macOS and Linux behavior where a click and drag cancels
|
||||||
|
// mouse actions.
|
||||||
|
const left_idx = @intFromEnum(input.MouseButton.left);
|
||||||
|
if (self.mouse.click_state[left_idx] == .press) click: {
|
||||||
|
const pin = self.mouse.left_click_pin orelse break :click;
|
||||||
|
const click_pt = self.io.terminal.screen.pages.pointFromPin(
|
||||||
|
.viewport,
|
||||||
|
pin.*,
|
||||||
|
) orelse break :click;
|
||||||
|
|
||||||
|
if (!click_pt.coord().eql(pos_vp)) {
|
||||||
|
log.debug("mouse moved while left click held, ignoring link hover", .{});
|
||||||
|
break :link null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const link = (try self.linkAtPos(pos)) orelse break :link null;
|
||||||
|
switch (link[0]) {
|
||||||
|
.open => {
|
||||||
|
const str = try self.io.terminal.screen.selectionString(alloc, .{
|
||||||
|
.sel = link[1],
|
||||||
|
.trim = false,
|
||||||
|
});
|
||||||
|
break :link .{ .url = str };
|
||||||
|
},
|
||||||
|
|
||||||
|
._open_osc8 => {
|
||||||
|
// Show the URL in the status bar
|
||||||
|
const pin = link[1].start();
|
||||||
|
const uri = self.osc8URI(pin) orelse {
|
||||||
|
log.warn("failed to get URI for OSC8 hyperlink", .{});
|
||||||
|
break :link null;
|
||||||
|
};
|
||||||
|
break :link .{ .url = uri };
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// If we found a link, setup our internal state and notify the
|
||||||
|
// apprt so it can highlight it.
|
||||||
|
if (link_) |link| {
|
||||||
self.renderer_state.mouse.point = pos_vp;
|
self.renderer_state.mouse.point = pos_vp;
|
||||||
self.mouse.over_link = true;
|
self.mouse.over_link = true;
|
||||||
self.renderer_state.terminal.screen.dirty.hyperlink_hover = true;
|
self.renderer_state.terminal.screen.dirty.hyperlink_hover = true;
|
||||||
@ -1042,38 +1107,18 @@ fn mouseRefreshLinks(
|
|||||||
.mouse_shape,
|
.mouse_shape,
|
||||||
.pointer,
|
.pointer,
|
||||||
);
|
);
|
||||||
|
|
||||||
switch (link[0]) {
|
|
||||||
.open => {
|
|
||||||
const str = try self.io.terminal.screen.selectionString(self.alloc, .{
|
|
||||||
.sel = link[1],
|
|
||||||
.trim = false,
|
|
||||||
});
|
|
||||||
defer self.alloc.free(str);
|
|
||||||
_ = try self.rt_app.performAction(
|
_ = try self.rt_app.performAction(
|
||||||
.{ .surface = self },
|
.{ .surface = self },
|
||||||
.mouse_over_link,
|
.mouse_over_link,
|
||||||
.{ .url = str },
|
link,
|
||||||
);
|
);
|
||||||
},
|
try self.queueRender();
|
||||||
|
return;
|
||||||
._open_osc8 => link: {
|
|
||||||
// Show the URL in the status bar
|
|
||||||
const pin = link[1].start();
|
|
||||||
const uri = self.osc8URI(pin) orelse {
|
|
||||||
log.warn("failed to get URI for OSC8 hyperlink", .{});
|
|
||||||
break :link;
|
|
||||||
};
|
|
||||||
_ = try self.rt_app.performAction(
|
|
||||||
.{ .surface = self },
|
|
||||||
.mouse_over_link,
|
|
||||||
.{ .url = uri },
|
|
||||||
);
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try self.queueRender();
|
// No link, if we're previously over a link then we need to clear
|
||||||
} else if (over_link) {
|
// the over-link apprt state.
|
||||||
|
if (over_link) {
|
||||||
_ = try self.rt_app.performAction(
|
_ = try self.rt_app.performAction(
|
||||||
.{ .surface = self },
|
.{ .surface = self },
|
||||||
.mouse_shape,
|
.mouse_shape,
|
||||||
@ -1085,6 +1130,7 @@ fn mouseRefreshLinks(
|
|||||||
.{ .url = "" },
|
.{ .url = "" },
|
||||||
);
|
);
|
||||||
try self.queueRender();
|
try self.queueRender();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -244,6 +244,8 @@ pub const Action = union(Key) {
|
|||||||
/// Closes the currently focused window.
|
/// Closes the currently focused window.
|
||||||
close_window,
|
close_window,
|
||||||
|
|
||||||
|
ring_bell,
|
||||||
|
|
||||||
/// Sync with: ghostty_action_tag_e
|
/// Sync with: ghostty_action_tag_e
|
||||||
pub const Key = enum(c_int) {
|
pub const Key = enum(c_int) {
|
||||||
quit,
|
quit,
|
||||||
@ -287,6 +289,7 @@ pub const Action = union(Key) {
|
|||||||
reload_config,
|
reload_config,
|
||||||
config_change,
|
config_change,
|
||||||
close_window,
|
close_window,
|
||||||
|
ring_bell,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Sync with: ghostty_action_u
|
/// Sync with: ghostty_action_u
|
||||||
|
@ -246,6 +246,7 @@ pub const App = struct {
|
|||||||
.toggle_maximize,
|
.toggle_maximize,
|
||||||
.prompt_title,
|
.prompt_title,
|
||||||
.reset_window_size,
|
.reset_window_size,
|
||||||
|
.ring_bell,
|
||||||
=> {
|
=> {
|
||||||
log.info("unimplemented action={}", .{action});
|
log.info("unimplemented action={}", .{action});
|
||||||
return false;
|
return false;
|
||||||
|
@ -484,6 +484,7 @@ pub fn performAction(
|
|||||||
.prompt_title => try self.promptTitle(target),
|
.prompt_title => try self.promptTitle(target),
|
||||||
.toggle_quick_terminal => return try self.toggleQuickTerminal(),
|
.toggle_quick_terminal => return try self.toggleQuickTerminal(),
|
||||||
.secure_input => self.setSecureInput(target, value),
|
.secure_input => self.setSecureInput(target, value),
|
||||||
|
.ring_bell => try self.ringBell(target),
|
||||||
|
|
||||||
// Unimplemented
|
// Unimplemented
|
||||||
.close_all_windows,
|
.close_all_windows,
|
||||||
@ -775,6 +776,13 @@ fn toggleQuickTerminal(self: *App) !bool {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn ringBell(_: *App, target: apprt.Target) !void {
|
||||||
|
switch (target) {
|
||||||
|
.app => {},
|
||||||
|
.surface => |surface| try surface.rt_surface.ringBell(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn quitTimer(self: *App, mode: apprt.action.QuitTimer) void {
|
fn quitTimer(self: *App, mode: apprt.action.QuitTimer) void {
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
.start => self.startQuitTimer(),
|
.start => self.startQuitTimer(),
|
||||||
|
@ -2439,3 +2439,25 @@ pub fn setSecureInput(self: *Surface, value: apprt.action.SecureInput) void {
|
|||||||
.toggle => self.is_secure_input = !self.is_secure_input,
|
.toggle => self.is_secure_input = !self.is_secure_input,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn ringBell(self: *Surface) !void {
|
||||||
|
const features = self.app.config.@"bell-features";
|
||||||
|
const window = self.container.window() orelse {
|
||||||
|
log.warn("failed to ring bell: surface is not attached to any window", .{});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// System beep
|
||||||
|
if (features.system) system: {
|
||||||
|
const surface = window.window.as(gtk.Native).getSurface() orelse break :system;
|
||||||
|
surface.beep();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark tab as needing attention
|
||||||
|
if (self.container.tab()) |tab| tab: {
|
||||||
|
const page = window.notebook.getTabPage(tab) orelse break :tab;
|
||||||
|
|
||||||
|
// Need attention if we're not the currently selected tab
|
||||||
|
if (page.getSelected() == 0) page.setNeedsAttention(@intFromBool(true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -114,9 +114,12 @@ pub fn gotoNthTab(self: *TabView, position: c_int) bool {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn getTabPage(self: *TabView, tab: *Tab) ?*adw.TabPage {
|
||||||
|
return self.tab_view.getPage(tab.box.as(gtk.Widget));
|
||||||
|
}
|
||||||
|
|
||||||
pub fn getTabPosition(self: *TabView, tab: *Tab) ?c_int {
|
pub fn getTabPosition(self: *TabView, tab: *Tab) ?c_int {
|
||||||
const page = self.tab_view.getPage(tab.box.as(gtk.Widget));
|
return self.tab_view.getPagePosition(self.getTabPage(tab) orelse return null);
|
||||||
return self.tab_view.getPagePosition(page);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn gotoPreviousTab(self: *TabView, tab: *Tab) bool {
|
pub fn gotoPreviousTab(self: *TabView, tab: *Tab) bool {
|
||||||
@ -161,17 +164,16 @@ pub fn moveTab(self: *TabView, tab: *Tab, position: c_int) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn reorderPage(self: *TabView, tab: *Tab, position: c_int) void {
|
pub fn reorderPage(self: *TabView, tab: *Tab, position: c_int) void {
|
||||||
const page = self.tab_view.getPage(tab.box.as(gtk.Widget));
|
_ = self.tab_view.reorderPage(self.getTabPage(tab) orelse return, position);
|
||||||
_ = self.tab_view.reorderPage(page, position);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setTabTitle(self: *TabView, tab: *Tab, title: [:0]const u8) void {
|
pub fn setTabTitle(self: *TabView, tab: *Tab, title: [:0]const u8) void {
|
||||||
const page = self.tab_view.getPage(tab.box.as(gtk.Widget));
|
const page = self.getTabPage(tab) orelse return;
|
||||||
page.setTitle(title.ptr);
|
page.setTitle(title.ptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setTabTooltip(self: *TabView, tab: *Tab, tooltip: [:0]const u8) void {
|
pub fn setTabTooltip(self: *TabView, tab: *Tab, tooltip: [:0]const u8) void {
|
||||||
const page = self.tab_view.getPage(tab.box.as(gtk.Widget));
|
const page = self.getTabPage(tab) orelse return;
|
||||||
page.setTooltip(tooltip.ptr);
|
page.setTooltip(tooltip.ptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -203,8 +205,7 @@ pub fn closeTab(self: *TabView, tab: *Tab) void {
|
|||||||
if (n > 1) self.forcing_close = false;
|
if (n > 1) self.forcing_close = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const page = self.tab_view.getPage(tab.box.as(gtk.Widget));
|
if (self.getTabPage(tab)) |page| self.tab_view.closePage(page);
|
||||||
self.tab_view.closePage(page);
|
|
||||||
|
|
||||||
// If we have no more tabs we close the window
|
// If we have no more tabs we close the window
|
||||||
if (self.nPages() == 0) {
|
if (self.nPages() == 0) {
|
||||||
@ -260,6 +261,11 @@ fn adwTabViewCreateWindow(
|
|||||||
|
|
||||||
fn adwSelectPage(_: *adw.TabView, _: *gobject.ParamSpec, self: *TabView) callconv(.C) void {
|
fn adwSelectPage(_: *adw.TabView, _: *gobject.ParamSpec, self: *TabView) callconv(.C) void {
|
||||||
const page = self.tab_view.getSelectedPage() orelse return;
|
const page = self.tab_view.getSelectedPage() orelse return;
|
||||||
|
|
||||||
|
// If the tab was previously marked as needing attention
|
||||||
|
// (e.g. due to a bell character), we now unmark that
|
||||||
|
page.setNeedsAttention(@intFromBool(false));
|
||||||
|
|
||||||
const title = page.getTitle();
|
const title = page.getTitle();
|
||||||
self.window.setTitle(std.mem.span(title));
|
self.window.setTitle(std.mem.span(title));
|
||||||
}
|
}
|
||||||
|
@ -81,6 +81,9 @@ pub const Message = union(enum) {
|
|||||||
/// The terminal has reported a change in the working directory.
|
/// The terminal has reported a change in the working directory.
|
||||||
pwd_change: WriteReq,
|
pwd_change: WriteReq,
|
||||||
|
|
||||||
|
/// The terminal encountered a bell character.
|
||||||
|
ring_bell,
|
||||||
|
|
||||||
pub const ReportTitleStyle = enum {
|
pub const ReportTitleStyle = enum {
|
||||||
csi_21_t,
|
csi_21_t,
|
||||||
|
|
||||||
|
@ -1861,6 +1861,22 @@ keybind: Keybinds = .{},
|
|||||||
/// open terminals.
|
/// open terminals.
|
||||||
@"custom-shader-animation": CustomShaderAnimation = .true,
|
@"custom-shader-animation": CustomShaderAnimation = .true,
|
||||||
|
|
||||||
|
/// The list of enabled features that are activated after encountering
|
||||||
|
/// a bell character.
|
||||||
|
///
|
||||||
|
/// Valid values are:
|
||||||
|
///
|
||||||
|
/// * `system` (default)
|
||||||
|
///
|
||||||
|
/// Instructs the system to notify the user using built-in system functions.
|
||||||
|
/// This could result in an audiovisual effect, a notification, or something
|
||||||
|
/// else entirely. Changing these effects require altering system settings:
|
||||||
|
/// for instance under the "Sound > Alert Sound" setting in GNOME,
|
||||||
|
/// or the "Accessibility > System Bell" settings in KDE Plasma.
|
||||||
|
///
|
||||||
|
/// Currently only implemented on Linux.
|
||||||
|
@"bell-features": BellFeatures = .{},
|
||||||
|
|
||||||
/// Control the in-app notifications that Ghostty shows.
|
/// Control the in-app notifications that Ghostty shows.
|
||||||
///
|
///
|
||||||
/// On Linux (GTK), in-app notifications show up as toasts. Toasts appear
|
/// On Linux (GTK), in-app notifications show up as toasts. Toasts appear
|
||||||
@ -5691,6 +5707,11 @@ pub const AppNotifications = packed struct {
|
|||||||
@"clipboard-copy": bool = true,
|
@"clipboard-copy": bool = true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// See bell-features
|
||||||
|
pub const BellFeatures = packed struct {
|
||||||
|
system: bool = false,
|
||||||
|
};
|
||||||
|
|
||||||
/// See mouse-shift-capture
|
/// See mouse-shift-capture
|
||||||
pub const MouseShiftCapture = enum {
|
pub const MouseShiftCapture = enum {
|
||||||
false,
|
false,
|
||||||
|
@ -325,9 +325,8 @@ pub const StreamHandler = struct {
|
|||||||
try self.terminal.printRepeat(count);
|
try self.terminal.printRepeat(count);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn bell(self: StreamHandler) !void {
|
pub fn bell(self: *StreamHandler) !void {
|
||||||
_ = self;
|
self.surfaceMessageWriter(.ring_bell);
|
||||||
log.info("BELL", .{});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn backspace(self: *StreamHandler) !void {
|
pub fn backspace(self: *StreamHandler) !void {
|
||||||
|
Reference in New Issue
Block a user