Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion Sources/SwiftTerm/Mac/MacTerminalView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1048,7 +1048,19 @@ open class TerminalView: NSView, NSTextInputClient, NSUserInterfaceValidations,

public override func cursorUpdate(with event: NSEvent)
{
NSCursor.iBeam.set ()
let hit = calculateMouseHit(with: event).grid
let hasCommandModifier = commandActive || event.modifierFlags.contains(.command)
updateHoverLink(at: hit, commandOverride: hasCommandModifier)
linkCursor(at: hit, hasCommandModifier: hasCommandModifier).set()
}

func linkCursor(at position: Position, hasCommandModifier: Bool) -> NSCursor
{
guard let match = terminal.linkMatch(at: .buffer(position), mode: .explicitAndImplicit),

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to use withTerminal, as we are now multi-threaded. I am

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additionally, the same call always uses .explicitAndImplicit. With the default .hoverWithModifier mode, it runs the full implicit-link regex even when Command is not pressed. When Command is pressed, updateHoverLink performs the lookup first, and linkCursor immediately performs it again. Use implicitLinkCouldBeVisible to select the mode, or reuse the result from updateHoverLink.

linkVisibleForClick(match: match, hasCommandModifier: hasCommandModifier) else {
return .iBeam
}
return .pointingHand
}

func makeFirstResponder ()
Expand Down
11 changes: 5 additions & 6 deletions Sources/SwiftTerm/Terminal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7423,11 +7423,13 @@ open class Terminal {
let pathChars = #"[\w\-.~:\/?#@!$&*+;=%]"#
let noTrailingPunctuation = #"(?<![,.])"#
let noTrailingColon = #"(?<!:)"#
let trailingSpacesAtEOL = #"(?: +(?= *$))?"#
let dottedPathLookahead = #"(?=[\w\-.~:\/?#@!$&*+;=%]*\.)"#
let nonDottedPathLookahead = #"(?![\w\-.~:\/?#@!$&*+;=%]*\.)"#
let dottedPathSpaceSegments = #"(?:(?<!:) (?!\w+:\/\/)[\w\-.~:\/?#@!$&*+;=%]*[\/.])*"#
let anyPathSpaceSegments = #"(?:(?<!:) (?!\w+:\/\/)[\w\-.~:\/?#@!$&*+;=%]+)*"#
// A prose attribution such as `/path/to/worktree at f455181` is not
// part of the filesystem path. Keep ordinary spaces in filenames, but
// treat the conventional " at " separator as a link boundary.
let anyPathSpaceSegments = #"(?:(?<!:) (?!at(?: |$))(?!\w+:\/\/)[\w\-.~:\/?#@!$&*+;=%]+)*"#

// The body used to be `(?:IPV6|CHARS+SUFFIX?)+`: a `+` nested directly inside a `+`, so a
// run of N body characters could be split across iterations in exponentially many ways.
Expand Down Expand Up @@ -7459,13 +7461,11 @@ open class Terminal {
pathChars + "+" +
dottedPathSpaceSegments +
noTrailingColon +
trailingSpacesAtEOL +
"|" +
nonDottedPathLookahead +
pathChars + "+" +
anyPathSpaceSegments +
noTrailingColon +
trailingSpacesAtEOL +
")"

// Ghostty uses (?<!\$\d*) here, which is unsupported by ICU.
Expand All @@ -7476,8 +7476,7 @@ open class Terminal {
bareRelativePathPrefix +
pathChars + "+" +
dottedPathSpaceSegments +
noTrailingColon +
trailingSpacesAtEOL
noTrailingColon

let regex = schemeURLBranch + "|" + rootedOrRelativePathBranch + "|" + bareRelativePathBranch
return try? NSRegularExpression(pattern: regex, options: [])
Expand Down
10 changes: 7 additions & 3 deletions Tests/SwiftTermTests/GhosttyImplicitLinkDetectionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ final class GhosttyImplicitLinkDetectionTests: TerminalDelegate {
("IPv6 address https://[2001:db8::1]:8080/path", "https://[2001:db8::1]:8080/path"),
("IPv6 address https://[2001:db8::1]:8080(foo)", "https://[2001:db8::1]:8080"),
("../example.py", "../example.py"),
("../example.py ", "../example.py "),
("../example.py ", "../example.py"),
("first time ../example.py contributor ", "../example.py"),
("src/config/url.zig", "src/config/url.zig"),
("app/folder/file.rb:1", "app/folder/file.rb:1"),
Expand All @@ -90,8 +90,12 @@ final class GhosttyImplicitLinkDetectionTests: TerminalDelegate {
(" - shared/src/foo/SomeItem.m:12, shared/src/", "shared/src/foo/SomeItem.m:12"),
("foo.local/share", "foo.local/share"),
("2024/report.txt", "2024/report.txt"),
("./spaces-end. ", "./spaces-end. "),
("./space middle", "./space middle")
("./spaces-end. ", "./spaces-end."),
("./space middle", "./space middle"),
(
"/Volumes/dev/dev/labs-OO-Agents-pr185-fullscreen at f455181",
"/Volumes/dev/dev/labs-OO-Agents-pr185-fullscreen"
)
]

for (input, expected) in cases {
Expand Down
13 changes: 13 additions & 0 deletions Tests/SwiftTermTests/MacDefaultLinkTests.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
#if os(macOS)
import AppKit
import Foundation
import Testing

@testable import SwiftTerm

@Suite("Mac default link handling")
struct MacDefaultLinkTests {

@Test @MainActor func usesPointingHandForVisibleCommandLink() {
let view = TerminalView(frame: CGRect(x: 0, y: 0, width: 320, height: 160))
view.linkHighlightMode = .hoverWithModifier
view.terminal.feed(text: "https://example.com")
let position = Position(col: 5, row: 0)

view.updateHoverLink(at: position, commandOverride: true)

#expect(view.linkCursor(at: position, hasCommandModifier: true) === NSCursor.pointingHand)
#expect(view.linkCursor(at: position, hasCommandModifier: false) === NSCursor.iBeam)
}
@Test func keepsExplicitURL() throws {
let expected = try #require(URL(string: "https://example.com/path"))

Expand Down