From 6dc7d16dd1247c44992d386dbc41dbe2fd7e3bfc Mon Sep 17 00:00:00 2001 From: Erwann Mest Date: Wed, 15 Apr 2026 11:24:31 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(link-styling):=20add=20per-pro?= =?UTF-8?q?file=20underline=20style=20and=20hover/active=20colours=20for?= =?UTF-8?q?=20OSC=208=20hyperlinks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../LinkColourMapAndActiveRangeTests.swift | 301 ++++++++++++++++++ sources/ITAddressBookMgr.h | 5 + sources/PTYSession.m | 5 + sources/PTYSession.swift | 13 + sources/PTYTextView+ARC.m | 10 +- sources/PTYTextView.m | 4 + .../ProfilesColorsPreferencesViewController.m | 26 ++ sources/iTermAttributedStringBuilder.m | 5 +- sources/iTermColorMap.h | 2 + sources/iTermColorMap.m | 4 + sources/iTermColorPresets.m | 2 + sources/iTermMetalPerFrameState.m | 30 +- .../iTermMetalPerFrameStateConfiguration.h | 5 + .../iTermMetalPerFrameStateConfiguration.m | 4 + sources/iTermMetalPerFrameStateRow.h | 1 + sources/iTermMetalPerFrameStateRow.m | 1 + sources/iTermProfilePreferences.m | 42 ++- sources/iTermTextDrawingHelper.h | 7 + sources/iTermTextDrawingHelper.m | 39 +++ 19 files changed, 498 insertions(+), 8 deletions(-) create mode 100644 ModernTests/LinkColourMapAndActiveRangeTests.swift diff --git a/ModernTests/LinkColourMapAndActiveRangeTests.swift b/ModernTests/LinkColourMapAndActiveRangeTests.swift new file mode 100644 index 0000000000..df4f290042 --- /dev/null +++ b/ModernTests/LinkColourMapAndActiveRangeTests.swift @@ -0,0 +1,301 @@ +// +// LinkColourMapAndActiveRangeTests.swift +// ModernTests +// +// Regression tests for the link-hover/active colour-map keys and the +// activeLinkRangeOnLine: logic introduced alongside KEY_LINK_UNDERLINE_STYLE. +// + +import XCTest +@testable import iTerm2SharedARC + +// MARK: - Colour map key tests + +final class LinkColourMapKeyTests: XCTestCase { + + // The extended-colours base sits well above the 8-bit and 24-bit ranges, + // so its absolute value doesn't matter — what matters is the *relative* + // ordering among the extended keys. + private var extendedBase: Int32 { + // kColorMapMatch is defined as kExtendedColorsBase + 0; derive from it. + return Int32(kColorMapMatch) + } + + /// kColorMapLinkHover must be kExtendedColorsBase + 2. + func testLinkHoverKeyIsExtendedBase2() { + XCTAssertEqual(Int32(kColorMapLinkHover), extendedBase + 2) + } + + /// kColorMapLinkActive must be kExtendedColorsBase + 3. + func testLinkActiveKeyIsExtendedBase3() { + XCTAssertEqual(Int32(kColorMapLinkActive), extendedBase + 3) + } + + /// The two new keys must be distinct from every pre-existing logical key. + func testLinkHoverAndActiveDoNotCollideLegacyKeys() { + let legacy: [Int32] = [ + Int32(kColorMapForeground), Int32(kColorMapBackground), + Int32(kColorMapBold), Int32(kColorMapLink), + Int32(kColorMapSelection), Int32(kColorMapSelectedText), + Int32(kColorMapCursor), Int32(kColorMapCursorText), + Int32(kColorMapInvalid), Int32(kColorMapUnderline), + Int32(kColorMapMatch), Int32(kColorMapIMECursor), + ] + XCTAssertFalse(legacy.contains(Int32(kColorMapLinkHover)), + "kColorMapLinkHover collides with an existing key") + XCTAssertFalse(legacy.contains(Int32(kColorMapLinkActive)), + "kColorMapLinkActive collides with an existing key") + } + + /// The two new keys must be distinct from each other. + func testLinkHoverAndActiveAreDifferent() { + XCTAssertNotEqual(kColorMapLinkHover, kColorMapLinkActive) + } + + /// kColorMapLinkHover and kColorMapLinkActive must be distinct from the + /// 8-bit palette range [kColorMap8bitBase, kColorMap8bitBase+256). + func testLinkKeysAreOutsideEightBitRange() { + let base = Int32(kColorMap8bitBase) + let end = base + 256 + XCTAssertFalse((base.. iTermTextDrawingHelper { + let h = iTermTextDrawingHelper() + h.gridSize = VT100GridSize(width: gridWidth, height: 24) + return h + } + + // Convenience: build a VT100GridAbsWindowedRange from scalar values. + // A columnWindow.length == 0 means "no window" (full grid width). + private func makeRange(startX: Int32, startY: Int64, + endX: Int32, endY: Int64, + windowStart: Int32 = 0, windowWidth: Int32 = 0) + -> VT100GridAbsWindowedRange + { + let coordRange = VT100GridAbsCoordRange( + start: VT100GridAbsCoord(x: startX, y: startY), + end: VT100GridAbsCoord(x: endX, y: endY)) + return VT100GridAbsWindowedRangeMake(coordRange, windowStart, windowWidth) + } + + // MARK: - No active range (sentinel) + + /// When activeLinkRange has a negative start.x the method must return an + /// empty range regardless of the queried row. + func testNoActiveRange_returnsEmpty() { + let h = makeHelper() + // Default-initialised VT100GridAbsWindowedRange has all zeros; force + // the sentinel by assigning x = -1. + var sentinel = VT100GridAbsWindowedRange() + sentinel.coordRange.start.x = -1 + h.activeLinkRange = sentinel + + let result = h.activeLinkRangeOnLine(5) + XCTAssertEqual(result.location, 0) + XCTAssertEqual(result.length, 0) + } + + // MARK: - Single-line range + + /// When start.y == end.y == queried row, return [startX, endX). + func testSingleLine_matchingRow() { + let h = makeHelper() + h.activeLinkRange = makeRange(startX: 3, startY: 10, endX: 7, endY: 10) + + let result = h.activeLinkRangeOnLine(10) + XCTAssertEqual(result.location, 3) + XCTAssertEqual(result.length, 4) // 7 - 3 + } + + /// A different row returns empty for a single-line range. + func testSingleLine_nonMatchingRow() { + let h = makeHelper() + h.activeLinkRange = makeRange(startX: 3, startY: 10, endX: 7, endY: 10) + + XCTAssertEqual(h.activeLinkRangeOnLine(9).length, 0) + XCTAssertEqual(h.activeLinkRangeOnLine(11).length, 0) + } + + // MARK: - Multi-line range, no column window + + /// On the start row of a multi-line range (no column window) the range runs + /// from startX to gridSize.width. + func testMultiLine_startRow_noWindow() { + let h = makeHelper(gridWidth: 80) + h.activeLinkRange = makeRange(startX: 20, startY: 5, endX: 10, endY: 7) + + let result = h.activeLinkRangeOnLine(5) + XCTAssertEqual(result.location, 20) + XCTAssertEqual(result.length, 60) // 80 - 20 + } + + /// On the end row of a multi-line range (no column window) the range runs + /// from 0 to endX. + func testMultiLine_endRow_noWindow() { + let h = makeHelper(gridWidth: 80) + h.activeLinkRange = makeRange(startX: 20, startY: 5, endX: 10, endY: 7) + + let result = h.activeLinkRangeOnLine(7) + XCTAssertEqual(result.location, 0) + XCTAssertEqual(result.length, 10) // 0..10 + } + + /// An interior row (strictly between start and end) returns the full grid + /// width when there is no column window. + func testMultiLine_interiorRow_noWindow() { + let h = makeHelper(gridWidth: 80) + h.activeLinkRange = makeRange(startX: 20, startY: 5, endX: 10, endY: 8) + + let result = h.activeLinkRangeOnLine(6) // interior + XCTAssertEqual(result.location, 0) + XCTAssertEqual(result.length, 80) + } + + /// A row before start or after end returns empty. + func testMultiLine_rowOutsideRange() { + let h = makeHelper(gridWidth: 80) + h.activeLinkRange = makeRange(startX: 20, startY: 5, endX: 10, endY: 7) + + XCTAssertEqual(h.activeLinkRangeOnLine(4).length, 0) + XCTAssertEqual(h.activeLinkRangeOnLine(8).length, 0) + } + + // MARK: - Multi-line range, with column window + + /// On the start row with a column window the range ends at the window max. + func testMultiLine_startRow_withWindow() { + let h = makeHelper(gridWidth: 80) + // Window covers columns 10..<50 (length 40), so max = 49, end = 50. + h.activeLinkRange = makeRange(startX: 25, startY: 3, endX: 15, endY: 5, + windowStart: 10, windowWidth: 40) + + let result = h.activeLinkRangeOnLine(3) + // start = max(25, 10) = 25 (already inside window) + // end = VT100GridRangeMax({10,40}) + 1 = 49 + 1 = 50 + XCTAssertEqual(result.location, 25) + XCTAssertEqual(result.length, 25) // 50 - 25 + } + + /// On the end row with a column window the range starts at windowStart. + func testMultiLine_endRow_withWindow() { + let h = makeHelper(gridWidth: 80) + h.activeLinkRange = makeRange(startX: 25, startY: 3, endX: 15, endY: 5, + windowStart: 10, windowWidth: 40) + + let result = h.activeLinkRangeOnLine(5) + // start = windowStart = 10 + // end = min(endX=15, max+1=50) = 15 + XCTAssertEqual(result.location, 10) + XCTAssertEqual(result.length, 5) // 15 - 10 + } + + /// An interior row with a column window returns exactly the window span. + func testMultiLine_interiorRow_withWindow() { + let h = makeHelper(gridWidth: 80) + h.activeLinkRange = makeRange(startX: 25, startY: 3, endX: 15, endY: 6, + windowStart: 10, windowWidth: 40) + + let result = h.activeLinkRangeOnLine(4) // interior + // start = windowStart = 10, end = VT100GridRangeMax({10,40}) + 1 = 50 + XCTAssertEqual(result.location, 10) + XCTAssertEqual(result.length, 40) + } +} + +// MARK: - Link underline style bounds-check tests + +/// The bounds check in iTermMetalPerFrameState is not directly unit-testable +/// without the Metal frame machinery, but the *policy* — which raw integer +/// values are considered valid — can be checked as pure constants. +final class LinkUnderlineStyleValidValuesTests: XCTestCase { + + // Mirror the whitelist from iTermMetalPerFrameState.m so that a future + // accidental removal of a valid value breaks a test rather than silently + // changing behaviour for users. + private func isValidLinkStyle(_ value: Int32) -> Bool { + return value == Int32(iTermMetalGlyphAttributesUnderlineSingle.rawValue) || + value == Int32(iTermMetalGlyphAttributesUnderlineDouble.rawValue) || + value == Int32(iTermMetalGlyphAttributesUnderlineDashedSingle.rawValue) || + value == Int32(iTermMetalGlyphAttributesUnderlineCurly.rawValue) || + value == Int32(iTermMetalGlyphAttributesUnderlineDotted.rawValue) + } + + private let fallback = Int32(iTermMetalGlyphAttributesUnderlineDashedSingle.rawValue) + + /// All five documented values (1=Single, 2=Double, 3=Dashed, 4=Curly, 6=Dotted) + /// are accepted. + func testAllDocumentedValuesAreValid() { + XCTAssertTrue(isValidLinkStyle(1), "Single should be valid") + XCTAssertTrue(isValidLinkStyle(2), "Double should be valid") + XCTAssertTrue(isValidLinkStyle(3), "DashedSingle should be valid") + XCTAssertTrue(isValidLinkStyle(4), "Curly should be valid") + XCTAssertTrue(isValidLinkStyle(6), "Dotted should be valid") + } + + /// Zero (None) is invalid and must fall back to DashedSingle. + func testZeroIsInvalidAndFallsBack() { + XCTAssertFalse(isValidLinkStyle(0)) + XCTAssertEqual(fallback, 3) // DashedSingle is the documented default + } + + /// 5 (Hyperlink) is not in the whitelist — it is a reserved internal style. + func testHyperlinkStyleIsNotDirectlyWhitelisted() { + XCTAssertFalse(isValidLinkStyle(5), + "iTermMetalGlyphAttributesUnderlineHyperlink must not be user-selectable") + } + + /// 7 (Dashed) is not in the whitelist (distinct from DashedSingle=3). + func testDashedIsNotWhitelisted() { + XCTAssertFalse(isValidLinkStyle(7)) + } + + /// Negative values are invalid. + func testNegativeIsInvalid() { + XCTAssertFalse(isValidLinkStyle(-1)) + XCTAssertFalse(isValidLinkStyle(-99)) + } + + /// Very large values are invalid. + func testLargeValueIsInvalid() { + XCTAssertFalse(isValidLinkStyle(100)) + } + + /// The default stored in iTermProfilePreferences (@3 = DashedSingle) is a + /// valid style, so it never triggers the fallback path on a fresh profile. + func testDefaultProfileValueIsValid() { + XCTAssertTrue(isValidLinkStyle(3), + "The profile default (3 = DashedSingle) must be valid") + } +} diff --git a/sources/ITAddressBookMgr.h b/sources/ITAddressBookMgr.h index 338d3e1a1f..d2f53b79ee 100644 --- a/sources/ITAddressBookMgr.h +++ b/sources/ITAddressBookMgr.h @@ -96,6 +96,10 @@ #define KEY_BRIGHTEN_BOLD_TEXT @"Brighten Bold Text" // New in 3.3.7. #define KEY_HARMONIZE_256_COLORS @"Harmonize 256 Colors" #define KEY_LINK_COLOR @"Link Color" +#define KEY_LINK_HOVER_COLOR @"Link Hover Color" +#define KEY_USE_LINK_HOVER_COLOR @"Use Link Hover Color" +#define KEY_LINK_ACTIVE_COLOR @"Link Active Color" +#define KEY_USE_LINK_ACTIVE_COLOR @"Use Link Active Color" #define KEY_MATCH_COLOR @"Match Background Color" #define KEY_SELECTION_COLOR @"Selection Color" #define KEY_SELECTED_TEXT_COLOR @"Selected Text Color" @@ -135,6 +139,7 @@ #define KEY_ACTIVE_PANE_BORDER_COLOR @"Active Pane Border Color" // End of key swith light and dark variants +#define KEY_LINK_UNDERLINE_STYLE @"Link Underline Style" #define KEY_USE_SEPARATE_COLORS_FOR_LIGHT_AND_DARK_MODE @"Use Separate Colors for Light and Dark Mode" #define COLORS_LIGHT_MODE_SUFFIX @" (Light)" #define COLORS_DARK_MODE_SUFFIX @" (Dark)" diff --git a/sources/PTYSession.m b/sources/PTYSession.m index 93c3f10813..b058ffb2ac 100644 --- a/sources/PTYSession.m +++ b/sources/PTYSession.m @@ -5072,12 +5072,16 @@ - (NSColor *)effectiveProcessedBackgroundColor { return iTermAmendedColorKey(baseKey, profile, dark); }; const BOOL useUnderline = [iTermProfilePreferences boolForKey:k(KEY_USE_UNDERLINE_COLOR) inProfile:profile]; + const BOOL useLinkHover = [iTermProfilePreferences boolForKey:k(KEY_USE_LINK_HOVER_COLOR) inProfile:profile]; + const BOOL useLinkActive = [iTermProfilePreferences boolForKey:k(KEY_USE_LINK_ACTIVE_COLOR) inProfile:profile]; NSDictionary *keyMap = @{ @(kColorMapForeground): k(KEY_FOREGROUND_COLOR), @(kColorMapBackground): k(KEY_BACKGROUND_COLOR), @(kColorMapSelection): k(KEY_SELECTION_COLOR), @(kColorMapSelectedText): k(KEY_SELECTED_TEXT_COLOR), @(kColorMapBold): k(KEY_BOLD_COLOR), @(kColorMapLink): k(KEY_LINK_COLOR), + @(kColorMapLinkHover): (useLinkHover ? k(KEY_LINK_HOVER_COLOR) : [NSNull null]), + @(kColorMapLinkActive): (useLinkActive ? k(KEY_LINK_ACTIVE_COLOR) : [NSNull null]), @(kColorMapMatch): k(KEY_MATCH_COLOR), @(kColorMapCursor): k(KEY_CURSOR_COLOR), @(kColorMapCursorText): k(KEY_CURSOR_TEXT_COLOR), @@ -5284,6 +5288,7 @@ - (void)reallySetPreferencesFromAddressBookEntry:(NSDictionary *)aePrefs _textview.cursorSmoothSlide = [iTermProfilePreferences boolForKey:KEY_CURSOR_SMOOTH_SLIDE inProfile:aDict]; [_textview setBlinkingCursor:[iTermProfilePreferences boolForKey:KEY_BLINKING_CURSOR inProfile:aDict]]; [_textview setCursorType:_cursorTypeOverride ? _cursorTypeOverride.integerValue : [iTermProfilePreferences intForKey:KEY_CURSOR_TYPE inProfile:aDict]]; + _textview.drawingHelper.linkUnderlineStyle = [iTermProfilePreferences intForKey:KEY_LINK_UNDERLINE_STYLE inProfile:aDict]; PTYTab* currentTab = [[_delegate parentWindow] currentTab]; if (currentTab == nil || [_delegate sessionBelongsToVisibleTab]) { diff --git a/sources/PTYSession.swift b/sources/PTYSession.swift index 7fb3d6df03..d574120d51 100644 --- a/sources/PTYSession.swift +++ b/sources/PTYSession.swift @@ -1369,6 +1369,19 @@ extension PTYSession { KEY_USE_UNDERLINE_COLOR: nullValue, KEY_USE_UNDERLINE_COLOR + COLORS_LIGHT_MODE_SUFFIX: nullValue, KEY_USE_UNDERLINE_COLOR + COLORS_DARK_MODE_SUFFIX: nullValue, + KEY_LINK_HOVER_COLOR: nullValue, + KEY_LINK_HOVER_COLOR + COLORS_LIGHT_MODE_SUFFIX: nullValue, + KEY_LINK_HOVER_COLOR + COLORS_DARK_MODE_SUFFIX: nullValue, + KEY_USE_LINK_HOVER_COLOR: nullValue, + KEY_USE_LINK_HOVER_COLOR + COLORS_LIGHT_MODE_SUFFIX: nullValue, + KEY_USE_LINK_HOVER_COLOR + COLORS_DARK_MODE_SUFFIX: nullValue, + KEY_LINK_ACTIVE_COLOR: nullValue, + KEY_LINK_ACTIVE_COLOR + COLORS_LIGHT_MODE_SUFFIX: nullValue, + KEY_LINK_ACTIVE_COLOR + COLORS_DARK_MODE_SUFFIX: nullValue, + KEY_USE_LINK_ACTIVE_COLOR: nullValue, + KEY_USE_LINK_ACTIVE_COLOR + COLORS_LIGHT_MODE_SUFFIX: nullValue, + KEY_USE_LINK_ACTIVE_COLOR + COLORS_DARK_MODE_SUFFIX: nullValue, + KEY_LINK_UNDERLINE_STYLE: nullValue, KEY_CURSOR_BOOST: nullValue, KEY_CURSOR_BOOST + COLORS_LIGHT_MODE_SUFFIX: nullValue, KEY_CURSOR_BOOST + COLORS_DARK_MODE_SUFFIX: nullValue, diff --git a/sources/PTYTextView+ARC.m b/sources/PTYTextView+ARC.m index db8d52bc72..8b19c6d3cf 100644 --- a/sources/PTYTextView+ARC.m +++ b/sources/PTYTextView+ARC.m @@ -516,8 +516,14 @@ - (void)finishUpdatingUnderlinesWithAction:(URLAction *)action if ([iTermAdvancedSettingsModel enableUnderlineSemanticHistoryOnCmdHover]) { DLog(@"Setting underlined range to %@", VT100GridWindowedRangeDescription(action.logicalRange)); - self.drawingHelper.underlinedRange = VT100GridAbsWindowedRangeFromRelative(action.logicalRange, - [self.dataSource totalScrollbackOverflow]); + const VT100GridAbsWindowedRange absRange = VT100GridAbsWindowedRangeFromRelative(action.logicalRange, + [self.dataSource totalScrollbackOverflow]); + self.drawingHelper.underlinedRange = absRange; + if ([NSEvent pressedMouseButtons] & 1) { + self.drawingHelper.activeLinkRange = absRange; + } else { + self.drawingHelper.activeLinkRange = VT100GridAbsWindowedRangeMake(VT100GridAbsCoordRangeMake(-1, -1, -1, -1), 0, 0); + } } DLog(@"Request redraw and update cursor"); diff --git a/sources/PTYTextView.m b/sources/PTYTextView.m index 90128124af..3db3ee90f3 100644 --- a/sources/PTYTextView.m +++ b/sources/PTYTextView.m @@ -303,6 +303,8 @@ - (instancetype)initWithFrame:(NSRect)aRect { _oldSelection = [_selection copy]; _drawingHelper.underlinedRange = VT100GridAbsWindowedRangeMake(VT100GridAbsCoordRangeMake(-1, -1, -1, -1), 0, 0); + _drawingHelper.activeLinkRange = + VT100GridAbsWindowedRangeMake(VT100GridAbsCoordRangeMake(-1, -1, -1, -1), 0, 0); _timeOfLastBlink = [NSDate timeIntervalSinceReferenceDate]; // Register for drag and drop. @@ -2147,6 +2149,8 @@ - (BOOL)removeUnderline { } _drawingHelper.underlinedRange = VT100GridAbsWindowedRangeMake(VT100GridAbsCoordRangeMake(-1, -1, -1, -1), 0, 0); + _drawingHelper.activeLinkRange = + VT100GridAbsWindowedRangeMake(VT100GridAbsCoordRangeMake(-1, -1, -1, -1), 0, 0); [self requestDelegateRedraw]; // It would be better to just display the underlined/formerly underlined area. return YES; } diff --git a/sources/ProfilesColorsPreferencesViewController.m b/sources/ProfilesColorsPreferencesViewController.m index 7bef038f8e..60c5d8012e 100644 --- a/sources/ProfilesColorsPreferencesViewController.m +++ b/sources/ProfilesColorsPreferencesViewController.m @@ -97,6 +97,11 @@ @implementation ProfilesColorsPreferencesViewController { IBOutlet NSButton *_useTabColor; IBOutlet NSButton *_useUnderlineColor; + IBOutlet NSButton *_useLinkHoverColor; + IBOutlet iTermSettingsColorWell *_linkHoverColor; + IBOutlet NSButton *_useLinkActiveColor; + IBOutlet iTermSettingsColorWell *_linkActiveColor; + IBOutlet NSPopUpButton *_linkUnderlineStylePopup; IBOutlet NSButton *_useSmartCursorColor; IBOutlet NSButton *_useActivePaneBorder; IBOutlet iTermSettingsColorWell *_activePaneBorderColor; @@ -263,6 +268,23 @@ - (void)awakeFromNib { type:kPreferenceInfoTypeCheckbox]; info.observer = ^() { [weakSelf updateColorControlsEnabled]; }; + info = [self defineControl:_useLinkHoverColor + key:KEY_USE_LINK_HOVER_COLOR + relatedView:nil + type:kPreferenceInfoTypeCheckbox]; + info.observer = ^() { [weakSelf updateColorControlsEnabled]; }; + + info = [self defineControl:_useLinkActiveColor + key:KEY_USE_LINK_ACTIVE_COLOR + relatedView:nil + type:kPreferenceInfoTypeCheckbox]; + info.observer = ^() { [weakSelf updateColorControlsEnabled]; }; + + [self defineControl:_linkUnderlineStylePopup + key:KEY_LINK_UNDERLINE_STYLE + relatedView:nil + type:kPreferenceInfoTypePopup]; + info = [self defineControl:_useSmartCursorColor key:KEY_SMART_CURSOR_COLOR relatedView:nil @@ -436,6 +458,8 @@ - (void)updateColorControlsEnabled { _tabColor.enabled = [self boolForKey:KEY_USE_TAB_COLOR]; _selectedTextColor.enabled = [self boolForKey:KEY_USE_SELECTED_TEXT_COLOR]; _underlineColor.enabled = [self boolForKey:KEY_USE_UNDERLINE_COLOR]; + _linkHoverColor.enabled = [self boolForKey:KEY_USE_LINK_HOVER_COLOR]; + _linkActiveColor.enabled = [self boolForKey:KEY_USE_LINK_ACTIVE_COLOR]; _activePaneBorderColor.enabled = [self boolForKey:KEY_USE_ACTIVE_PANE_BORDER]; const BOOL smartCursorColorSelected = [self boolForKey:KEY_SMART_CURSOR_COLOR]; @@ -470,6 +494,8 @@ - (void)updateColorControlsEnabled { KEY_BACKGROUND_COLOR: _backgroundColor, KEY_BOLD_COLOR: _boldColor, KEY_LINK_COLOR: _linkColor, + KEY_LINK_HOVER_COLOR: _linkHoverColor, + KEY_LINK_ACTIVE_COLOR: _linkActiveColor, KEY_MATCH_COLOR: _matchColor, KEY_SELECTION_COLOR: _selectionColor, KEY_SELECTED_TEXT_COLOR: _selectedTextColor, diff --git a/sources/iTermAttributedStringBuilder.m b/sources/iTermAttributedStringBuilder.m index 9d6559a479..4f7891fac1 100644 --- a/sources/iTermAttributedStringBuilder.m +++ b/sources/iTermAttributedStringBuilder.m @@ -132,8 +132,9 @@ static inline BOOL iTermCharacterAttributesUnderlineColorEqual(iTermCharacterAtt assert(rawColor); context->havePreviousCharacterAttributes = NO; } else if (inUnderlinedRange) { - // Blue link text. - rawColor = [context->colorMap colorForKey:kColorMapLink]; + NSColor *hoverColor = [context->colorMap colorForKey:kColorMapLinkHover]; + NSColor *linkColor = hoverColor ?: [context->colorMap colorForKey:kColorMapLink]; + rawColor = linkColor; assert(rawColor); context->havePreviousCharacterAttributes = NO; } else if (context->hasSelectedText && context->delegate.useSelectedTextColor) { diff --git a/sources/iTermColorMap.h b/sources/iTermColorMap.h index 5358a4e2ac..0a0368f0f1 100644 --- a/sources/iTermColorMap.h +++ b/sources/iTermColorMap.h @@ -31,6 +31,8 @@ extern const int kColorMapInvalid; extern const int kColorMapUnderline; extern const int kColorMapMatch; extern const int kColorMapIMECursor; +extern const int kColorMapLinkHover; +extern const int kColorMapLinkActive; // This value plus 0...255 are accepted. The ANSI colors below followed by their bright // variants make the first 16 entries of the 256-color space. diff --git a/sources/iTermColorMap.m b/sources/iTermColorMap.m index 95545c9e26..88050060fc 100644 --- a/sources/iTermColorMap.m +++ b/sources/iTermColorMap.m @@ -40,6 +40,8 @@ // Additional configurable colors (I ran out of space < 10). const int kColorMapMatch = kExtendedColorsBase + 0; const int kColorMapIMECursor = kExtendedColorsBase + 1; +const int kColorMapLinkHover = kExtendedColorsBase + 2; +const int kColorMapLinkActive = kExtendedColorsBase + 3; // ANSI colors that automatically get bright variants. const int kColorMapAnsiBlack = kColorMap8bitBase + 0; @@ -532,6 +534,8 @@ - (NSString *)baseProfileKeyForColorMapKey:(int)theKey { @(kColorMapBackground): KEY_BACKGROUND_COLOR, @(kColorMapBold): KEY_BOLD_COLOR, @(kColorMapLink): KEY_LINK_COLOR, + @(kColorMapLinkHover): KEY_LINK_HOVER_COLOR, + @(kColorMapLinkActive): KEY_LINK_ACTIVE_COLOR, @(kColorMapMatch): KEY_MATCH_COLOR, @(kColorMapSelection): KEY_SELECTION_COLOR, @(kColorMapSelectedText): KEY_SELECTED_TEXT_COLOR, diff --git a/sources/iTermColorPresets.m b/sources/iTermColorPresets.m index a03f185e68..207a084be8 100644 --- a/sources/iTermColorPresets.m +++ b/sources/iTermColorPresets.m @@ -188,6 +188,8 @@ @implementation ProfileModel(iTermColorPresets) KEY_BACKGROUND_COLOR, KEY_BOLD_COLOR, KEY_LINK_COLOR, + KEY_LINK_HOVER_COLOR, + KEY_LINK_ACTIVE_COLOR, KEY_MATCH_COLOR, KEY_SELECTION_COLOR, KEY_SELECTED_TEXT_COLOR, diff --git a/sources/iTermMetalPerFrameState.m b/sources/iTermMetalPerFrameState.m index 72d4bffea2..70b8d86d68 100644 --- a/sources/iTermMetalPerFrameState.m +++ b/sources/iTermMetalPerFrameState.m @@ -71,6 +71,7 @@ NS_INLINE void iTermCachedGlyphKeysBufferEnsureSize(iTermCachedGlyphKeysBuffer * typedef struct { unsigned int isMatch : 1; unsigned int inUnderlinedRange : 1; // This is the underline for semantic history + unsigned int inActiveLinkRange : 1; unsigned int selected : 1; unsigned int foregroundColor : 8; unsigned int fgGreen : 8; @@ -1390,6 +1391,7 @@ int iTermGetMetalBackgroundColors(iTermMetalPerFrameState *self, static void iTermInitializeColorKey(BOOL findMatch, BOOL inUnderlinedRange, + BOOL inActiveLinkRange, BOOL selected, BOOL isBlockCharacter, vector_float4 bgColor, @@ -1397,6 +1399,7 @@ static void iTermInitializeColorKey(BOOL findMatch, iTermTextColorKey *currentColorKey) { currentColorKey->isMatch = findMatch; currentColorKey->inUnderlinedRange = inUnderlinedRange; + currentColorKey->inActiveLinkRange = inActiveLinkRange; currentColorKey->selected = selected; currentColorKey->mode = characterPointer->foregroundColorMode; currentColorKey->foregroundColor = characterPointer->foregroundColor; @@ -1412,6 +1415,7 @@ static BOOL iTermColorKeysEqual(const iTermTextColorKey *lhs, const iTermTextColorKey *rhs) { return (lhs->isMatch == rhs->isMatch && lhs->inUnderlinedRange == rhs->inUnderlinedRange && + lhs->inActiveLinkRange == rhs->inActiveLinkRange && lhs->selected == rhs->selected && lhs->foregroundColor == rhs->foregroundColor && lhs->mode == rhs->mode && @@ -1456,7 +1460,14 @@ static BOOL iTermMetalSetUnderline(iTermMetalPerFrameState *self, break; } } else if (url != nil && underlineHyperlinks) { - attributes[visualX].underlineStyle = iTermMetalGlyphAttributesUnderlineDashedSingle; + const int linkStyle = _configuration->_linkUnderlineStyle; + const BOOL validLinkStyle = (linkStyle == iTermMetalGlyphAttributesUnderlineSingle || + linkStyle == iTermMetalGlyphAttributesUnderlineDouble || + linkStyle == iTermMetalGlyphAttributesUnderlineDashedSingle || + linkStyle == iTermMetalGlyphAttributesUnderlineCurly || + linkStyle == iTermMetalGlyphAttributesUnderlineDotted); + attributes[visualX].underlineStyle = validLinkStyle ? (iTermMetalGlyphAttributesUnderline)linkStyle + : iTermMetalGlyphAttributesUnderlineDashedSingle; } else { attributes[visualX].underlineStyle = iTermMetalGlyphAttributesUnderlineNone; } @@ -1492,6 +1503,7 @@ static int iTermEmitGlyphsAndSetAttributes(iTermMetalPerFrameState *self, NSIndexSet *annotatedIndexes, NSData *findMatches, NSRange underlinedRange, + NSRange activeLinkRange, iTermExternalAttributeIndex *eaIndex, iTermMetalPerFrameStateConfiguration *_configuration, NSMutableArray *kittyImageRuns, @@ -1576,6 +1588,7 @@ static int iTermEmitGlyphsAndSetAttributes(iTermMetalPerFrameState *self, } const BOOL annotated = [annotatedIndexes containsIndex:visualX]; const BOOL inUnderlinedRange = NSLocationInRange(logicalIndex, underlinedRange) || annotated; + const BOOL inActiveLinkRange = NSLocationInRange(logicalIndex, activeLinkRange); attributes[visualX].annotation = annotated; @@ -1604,6 +1617,7 @@ static int iTermEmitGlyphsAndSetAttributes(iTermMetalPerFrameState *self, // Build up a compact key describing all the inputs to a text color iTermInitializeColorKey(findMatch, inUnderlinedRange, + inActiveLinkRange, selected, isBlockCharacter, bgColor, @@ -1618,6 +1632,7 @@ static int iTermEmitGlyphsAndSetAttributes(iTermMetalPerFrameState *self, selected:selected findMatch:findMatch inUnderlinedRange:inUnderlinedRange && !annotated + inActiveLinkRange:inActiveLinkRange && !annotated disableMinimumContrast:isBlockCharacter caches:&caches]; attributes[visualX].foregroundColor = textColor; @@ -1722,12 +1737,14 @@ - (void)metalGetGlyphKeysData:(iTermGlyphKeyData *)glyphKeysData NSData *findMatches = _rows[row]->_matches; NSIndexSet *selectedIndexes = _rows[row]->_selectedIndexSet; NSRange underlinedRange = _rows[row]->_underlinedRange; + NSRange activeLinkRange = _rows[row]->_activeLinkRange; NSIndexSet *annotatedIndexes = _rowToAnnotationRanges[@(row)]; if (VT100GridRangeContains(_linesToSuppressDrawing, row)) { lineData = [ScreenCharArray emptyLineOfLength:width]; findMatches = nil; selectedIndexes = nil; underlinedRange = NSMakeRange(NSNotFound, 0); + activeLinkRange = NSMakeRange(NSNotFound, 0); annotatedIndexes = nil; } const screen_char_t *const line = (const screen_char_t *const)lineData.line; @@ -1809,6 +1826,7 @@ - (void)metalGetGlyphKeysData:(iTermGlyphKeyData *)glyphKeysData annotatedIndexes, findMatches, underlinedRange, + activeLinkRange, eaIndex, _configuration, kittyImageRuns, @@ -2297,6 +2315,7 @@ - (vector_float4)textColorForCharacter:(const screen_char_t *const)c selected:(BOOL)selected findMatch:(BOOL)findMatch inUnderlinedRange:(BOOL)inUnderlinedRange + inActiveLinkRange:(BOOL)inActiveLinkRange disableMinimumContrast:(BOOL)disableMinimumContrast caches:(iTermMetalPerFrameStateCaches *)caches { vector_float4 rawColor = { 0, 0, 0, 0 }; @@ -2313,8 +2332,15 @@ - (vector_float4)textColorForCharacter:(const screen_char_t *const)c rawColor = VectorForColor(iTermTextDrawingHelperTextColorForMatch(bgColor), _configuration->_colorSpace); caches->havePreviousCharacterAttributes = NO; + } else if (inActiveLinkRange && _configuration->_useLinkActiveColor) { + rawColor = VectorForColor([_configuration->_colorMap colorForKey:kColorMapLinkActive], + _configuration->_colorSpace); + caches->havePreviousCharacterAttributes = NO; + } else if (inUnderlinedRange && _configuration->_useLinkHoverColor) { + rawColor = VectorForColor([_configuration->_colorMap colorForKey:kColorMapLinkHover], + _configuration->_colorSpace); + caches->havePreviousCharacterAttributes = NO; } else if (inUnderlinedRange) { - // Blue link text. rawColor = VectorForColor([_configuration->_colorMap colorForKey:kColorMapLink], _configuration->_colorSpace); caches->havePreviousCharacterAttributes = NO; diff --git a/sources/iTermMetalPerFrameStateConfiguration.h b/sources/iTermMetalPerFrameStateConfiguration.h index e2717c8a96..6272604fb0 100644 --- a/sources/iTermMetalPerFrameStateConfiguration.h +++ b/sources/iTermMetalPerFrameStateConfiguration.h @@ -92,6 +92,11 @@ NS_ASSUME_NONNULL_BEGIN CGFloat _backgroundColorAlpha; // See iTermAlphaBlendingHelper.h iTermBackgroundImageMode _backgroundImageMode; + // Link customisation + int _linkUnderlineStyle; + BOOL _useLinkHoverColor; + BOOL _useLinkActiveColor; + // Other BOOL _showBroadcastStripes; BOOL _timestampsEnabled; diff --git a/sources/iTermMetalPerFrameStateConfiguration.m b/sources/iTermMetalPerFrameStateConfiguration.m index 5725c44e67..c26c3d5edd 100644 --- a/sources/iTermMetalPerFrameStateConfiguration.m +++ b/sources/iTermMetalPerFrameStateConfiguration.m @@ -161,6 +161,10 @@ - (void)loadSettingsWithDrawingHelper:(iTermTextDrawingHelper *)drawingHelper _selectedCommandRegion = drawingHelper.selectedCommandRegion; _selectedCommandRegion.location += drawingHelper.totalScrollbackOverflow; _totalScrollbackOverflow = drawingHelper.totalScrollbackOverflow; + + _linkUnderlineStyle = drawingHelper.linkUnderlineStyle; + _useLinkHoverColor = [_colorMap colorForKey:kColorMapLinkHover] != nil; + _useLinkActiveColor = [_colorMap colorForKey:kColorMapLinkActive] != nil; } @end diff --git a/sources/iTermMetalPerFrameStateRow.h b/sources/iTermMetalPerFrameStateRow.h index 21d0e6c758..7677b9bb0c 100644 --- a/sources/iTermMetalPerFrameStateRow.h +++ b/sources/iTermMetalPerFrameStateRow.h @@ -29,6 +29,7 @@ NS_ASSUME_NONNULL_BEGIN BOOL _belongsToBlock; NSData *_matches; NSRange _underlinedRange; // Underline for semantic history + NSRange _activeLinkRange; BOOL _x_inDeselectedRegion; id _eaIndex; } diff --git a/sources/iTermMetalPerFrameStateRow.m b/sources/iTermMetalPerFrameStateRow.m index 41296cac52..d6c4341758 100644 --- a/sources/iTermMetalPerFrameStateRow.m +++ b/sources/iTermMetalPerFrameStateRow.m @@ -74,6 +74,7 @@ - (instancetype)initWithDrawingHelper:(iTermTextDrawingHelper *)drawingHelper const long long absoluteLine = totalScrollbackOverflow + i; _underlinedRange = [drawingHelper underlinedRangeOnLine:absoluteLine]; + _activeLinkRange = [drawingHelper activeLinkRangeOnLine:absoluteLine]; _x_inDeselectedRegion = drawingHelper.selectedCommandRegion.length > 0 && !NSLocationInRange(i, drawingHelper.selectedCommandRegion); _markStyle = @([self markStyleForLine:i enabled:drawingHelper.drawMarkIndicators diff --git a/sources/iTermProfilePreferences.m b/sources/iTermProfilePreferences.m index 1952fd4c6d..9490696e2a 100644 --- a/sources/iTermProfilePreferences.m +++ b/sources/iTermProfilePreferences.m @@ -242,7 +242,8 @@ + (iTermProfilePreferencesKeyFuncs)keyFuncs { KEY_PROGRESS_BAR_COLOR_SCHEME]; NSArray *color = @[ KEY_FOREGROUND_COLOR, KEY_BACKGROUND_COLOR, KEY_BOLD_COLOR, - KEY_LINK_COLOR, KEY_MATCH_COLOR, KEY_SELECTION_COLOR, KEY_SELECTED_TEXT_COLOR, + KEY_LINK_COLOR, KEY_LINK_HOVER_COLOR, KEY_LINK_ACTIVE_COLOR, + KEY_MATCH_COLOR, KEY_SELECTION_COLOR, KEY_SELECTED_TEXT_COLOR, KEY_CURSOR_COLOR, KEY_CURSOR_TEXT_COLOR, KEY_IME_CURSOR_COLOR, KEY_ANSI_0_COLOR, KEY_ANSI_1_COLOR, KEY_ANSI_2_COLOR, KEY_ANSI_3_COLOR, KEY_ANSI_4_COLOR, KEY_ANSI_5_COLOR, KEY_ANSI_6_COLOR, KEY_ANSI_7_COLOR, KEY_ANSI_8_COLOR, @@ -273,6 +274,14 @@ + (iTermProfilePreferencesKeyFuncs)keyFuncs { KEY_USE_UNDERLINE_COLOR COLORS_DARK_MODE_SUFFIX, KEY_USE_UNDERLINE_COLOR, + KEY_USE_LINK_HOVER_COLOR COLORS_LIGHT_MODE_SUFFIX, + KEY_USE_LINK_HOVER_COLOR COLORS_DARK_MODE_SUFFIX, + KEY_USE_LINK_HOVER_COLOR, + + KEY_USE_LINK_ACTIVE_COLOR COLORS_LIGHT_MODE_SUFFIX, + KEY_USE_LINK_ACTIVE_COLOR COLORS_DARK_MODE_SUFFIX, + KEY_USE_LINK_ACTIVE_COLOR, + KEY_USE_ACTIVE_PANE_BORDER COLORS_LIGHT_MODE_SUFFIX, KEY_USE_ACTIVE_PANE_BORDER COLORS_DARK_MODE_SUFFIX, KEY_USE_ACTIVE_PANE_BORDER, @@ -371,7 +380,7 @@ + (iTermProfilePreferencesKeyFuncs)keyFuncs { KEY_CURSOR_BOOST COLORS_DARK_MODE_SUFFIX, KEY_CURSOR_BOOST, - KEY_CURSOR_TYPE, KEY_THIN_STROKES, + KEY_CURSOR_TYPE, KEY_LINK_UNDERLINE_STYLE, KEY_THIN_STROKES, KEY_UNICODE_NORMALIZATION, KEY_HORIZONTAL_SPACING, KEY_VERTICAL_SPACING, KEY_TRANSPARENCY, @@ -525,6 +534,10 @@ + (NSString *)descriptionForKey:(NSString *)key { KEY_BACKGROUND_COLOR COLORS_LIGHT_MODE_SUFFIX: @"Terminal background color in light mode", KEY_BOLD_COLOR COLORS_LIGHT_MODE_SUFFIX: @"Color for bold text in light mode", KEY_LINK_COLOR COLORS_LIGHT_MODE_SUFFIX: @"Color for clickable links in light mode", + KEY_LINK_HOVER_COLOR COLORS_LIGHT_MODE_SUFFIX: @"Color for hovered links in light mode", + KEY_USE_LINK_HOVER_COLOR COLORS_LIGHT_MODE_SUFFIX: @"Whether to use a custom hover link colour in light mode", + KEY_LINK_ACTIVE_COLOR COLORS_LIGHT_MODE_SUFFIX: @"Color for active (Cmd+clicked) links in light mode", + KEY_USE_LINK_ACTIVE_COLOR COLORS_LIGHT_MODE_SUFFIX: @"Whether to use a custom active link colour in light mode", KEY_MATCH_COLOR COLORS_LIGHT_MODE_SUFFIX: @"Highlight color for search matches in light mode", KEY_SELECTION_COLOR COLORS_LIGHT_MODE_SUFFIX: @"Background color of selected text in light mode", KEY_SELECTED_TEXT_COLOR COLORS_LIGHT_MODE_SUFFIX: @"Text color of selected text in light mode", @@ -554,6 +567,10 @@ + (NSString *)descriptionForKey:(NSString *)key { KEY_BACKGROUND_COLOR COLORS_DARK_MODE_SUFFIX: @"Terminal background color in dark mode", KEY_BOLD_COLOR COLORS_DARK_MODE_SUFFIX: @"Color for bold text in dark mode", KEY_LINK_COLOR COLORS_DARK_MODE_SUFFIX: @"Color for clickable links in dark mode", + KEY_LINK_HOVER_COLOR COLORS_DARK_MODE_SUFFIX: @"Color for hovered links in dark mode", + KEY_USE_LINK_HOVER_COLOR COLORS_DARK_MODE_SUFFIX: @"Whether to use a custom hover link colour in dark mode", + KEY_LINK_ACTIVE_COLOR COLORS_DARK_MODE_SUFFIX: @"Color for active (Cmd+clicked) links in dark mode", + KEY_USE_LINK_ACTIVE_COLOR COLORS_DARK_MODE_SUFFIX: @"Whether to use a custom active link colour in dark mode", KEY_MATCH_COLOR COLORS_DARK_MODE_SUFFIX: @"Highlight color for search matches in dark mode", KEY_SELECTION_COLOR COLORS_DARK_MODE_SUFFIX: @"Background color of selected text in dark mode", KEY_SELECTED_TEXT_COLOR COLORS_DARK_MODE_SUFFIX: @"Text color of selected text in dark mode", @@ -627,6 +644,11 @@ + (NSString *)descriptionForKey:(NSString *)key { KEY_BACKGROUND_COLOR: @"Terminal background color", KEY_BOLD_COLOR: @"Color for bold text", KEY_LINK_COLOR: @"Color for clickable links", + KEY_LINK_HOVER_COLOR: @"Color for hovered links", + KEY_USE_LINK_HOVER_COLOR: @"Whether to use a custom hover link colour", + KEY_LINK_ACTIVE_COLOR: @"Color for active (Cmd+clicked) links", + KEY_USE_LINK_ACTIVE_COLOR: @"Whether to use a custom active link colour", + KEY_LINK_UNDERLINE_STYLE: @"Underline style for links: 1=Single, 2=Double, 3=Dashed, 4=Curly, 6=Dotted", KEY_MATCH_COLOR: @"Highlight color for search matches", KEY_SELECTION_COLOR: @"Background color of selected text", KEY_SELECTED_TEXT_COLOR: @"Text color of selected text", @@ -854,6 +876,8 @@ + (NSDictionary *)defaultValueMap { KEY_BACKGROUND_COLOR: [[NSColor colorWithCalibratedRed:0.000 green:0.000 blue:0.000 alpha:1] dictionaryValue], KEY_BOLD_COLOR: [[NSColor colorWithCalibratedRed:1.000 green:1.000 blue:1.000 alpha:1] dictionaryValue], KEY_LINK_COLOR: [[NSColor colorWithCalibratedRed:0.023 green:0.270 blue:0.678 alpha:1] dictionaryValue], + KEY_LINK_HOVER_COLOR: [[NSColor colorWithCalibratedRed:0.023 green:0.270 blue:0.678 alpha:1] dictionaryValue], + KEY_LINK_ACTIVE_COLOR: [[NSColor colorWithCalibratedRed:0.023 green:0.270 blue:0.678 alpha:1] dictionaryValue], KEY_MATCH_COLOR: [[NSColor colorWithCalibratedRed:1.000 green:1.000 blue:0.000 alpha:1] dictionaryValue], KEY_SELECTION_COLOR: [[NSColor colorWithCalibratedRed:0.709 green:0.835 blue:1.000 alpha:1] dictionaryValue], KEY_SELECTED_TEXT_COLOR: [[NSColor colorWithCalibratedRed:0.000 green:0.000 blue:0.000 alpha:1] dictionaryValue], @@ -885,6 +909,8 @@ + (NSDictionary *)defaultValueMap { KEY_BACKGROUND_COLOR COLORS_LIGHT_MODE_SUFFIX: [[NSColor colorWithCalibratedRed:0.000 green:0.000 blue:0.000 alpha:1] dictionaryValue], KEY_BOLD_COLOR COLORS_LIGHT_MODE_SUFFIX: [[NSColor colorWithCalibratedRed:1.000 green:1.000 blue:1.000 alpha:1] dictionaryValue], KEY_LINK_COLOR COLORS_LIGHT_MODE_SUFFIX: [[NSColor colorWithCalibratedRed:0.023 green:0.270 blue:0.678 alpha:1] dictionaryValue], + KEY_LINK_HOVER_COLOR COLORS_LIGHT_MODE_SUFFIX: [[NSColor colorWithCalibratedRed:0.023 green:0.270 blue:0.678 alpha:1] dictionaryValue], + KEY_LINK_ACTIVE_COLOR COLORS_LIGHT_MODE_SUFFIX: [[NSColor colorWithCalibratedRed:0.023 green:0.270 blue:0.678 alpha:1] dictionaryValue], KEY_MATCH_COLOR COLORS_LIGHT_MODE_SUFFIX: [[NSColor colorWithCalibratedRed:1.000 green:1.000 blue:0.000 alpha:1] dictionaryValue], KEY_SELECTION_COLOR COLORS_LIGHT_MODE_SUFFIX: [[NSColor colorWithCalibratedRed:0.709 green:0.835 blue:1.000 alpha:1] dictionaryValue], KEY_SELECTED_TEXT_COLOR COLORS_LIGHT_MODE_SUFFIX: [[NSColor colorWithCalibratedRed:0.000 green:0.000 blue:0.000 alpha:1] dictionaryValue], @@ -915,6 +941,8 @@ + (NSDictionary *)defaultValueMap { KEY_BACKGROUND_COLOR COLORS_DARK_MODE_SUFFIX: [[NSColor colorWithCalibratedRed:0.000 green:0.000 blue:0.000 alpha:1] dictionaryValue], KEY_BOLD_COLOR COLORS_DARK_MODE_SUFFIX: [[NSColor colorWithCalibratedRed:1.000 green:1.000 blue:1.000 alpha:1] dictionaryValue], KEY_LINK_COLOR COLORS_DARK_MODE_SUFFIX: [[NSColor colorWithCalibratedRed:0.023 green:0.270 blue:0.678 alpha:1] dictionaryValue], + KEY_LINK_HOVER_COLOR COLORS_DARK_MODE_SUFFIX: [[NSColor colorWithCalibratedRed:0.023 green:0.270 blue:0.678 alpha:1] dictionaryValue], + KEY_LINK_ACTIVE_COLOR COLORS_DARK_MODE_SUFFIX: [[NSColor colorWithCalibratedRed:0.023 green:0.270 blue:0.678 alpha:1] dictionaryValue], KEY_MATCH_COLOR COLORS_DARK_MODE_SUFFIX: [[NSColor colorWithCalibratedRed:1.000 green:1.000 blue:0.000 alpha:1] dictionaryValue], KEY_SELECTION_COLOR COLORS_DARK_MODE_SUFFIX: [[NSColor colorWithCalibratedRed:0.709 green:0.835 blue:1.000 alpha:1] dictionaryValue], KEY_SELECTED_TEXT_COLOR COLORS_DARK_MODE_SUFFIX: [[NSColor colorWithCalibratedRed:0.000 green:0.000 blue:0.000 alpha:1] dictionaryValue], @@ -966,6 +994,16 @@ + (NSDictionary *)defaultValueMap { KEY_USE_UNDERLINE_COLOR COLORS_LIGHT_MODE_SUFFIX: @NO, KEY_USE_UNDERLINE_COLOR COLORS_DARK_MODE_SUFFIX: @NO, + KEY_USE_LINK_HOVER_COLOR: @NO, + KEY_USE_LINK_HOVER_COLOR COLORS_LIGHT_MODE_SUFFIX: @NO, + KEY_USE_LINK_HOVER_COLOR COLORS_DARK_MODE_SUFFIX: @NO, + + KEY_USE_LINK_ACTIVE_COLOR: @NO, + KEY_USE_LINK_ACTIVE_COLOR COLORS_LIGHT_MODE_SUFFIX: @NO, + KEY_USE_LINK_ACTIVE_COLOR COLORS_DARK_MODE_SUFFIX: @NO, + + KEY_LINK_UNDERLINE_STYLE: @3, + KEY_USE_ACTIVE_PANE_BORDER: @NO, KEY_USE_ACTIVE_PANE_BORDER COLORS_LIGHT_MODE_SUFFIX: @NO, KEY_USE_ACTIVE_PANE_BORDER COLORS_DARK_MODE_SUFFIX: @NO, diff --git a/sources/iTermTextDrawingHelper.h b/sources/iTermTextDrawingHelper.h index 7d2b152e3b..d9c84dc19f 100644 --- a/sources/iTermTextDrawingHelper.h +++ b/sources/iTermTextDrawingHelper.h @@ -229,6 +229,12 @@ extern const CGFloat iTermCursorGuideAlphaThreshold; // This is a logical range. @property(nonatomic, assign) VT100GridAbsWindowedRange underlinedRange; +// Active link range (Cmd+mouseDown), a subset of the underlined range. +@property(nonatomic, assign) VT100GridAbsWindowedRange activeLinkRange; + +// Underline style to use for links (maps to iTermMetalGlyphAttributesUnderline values). +@property(nonatomic, assign) int linkUnderlineStyle; + // If set, the last-modified time of each line on the screen is shown on the right side of the display. @property(nonatomic) BOOL shouldShowTimestamps; @@ -426,6 +432,7 @@ extern const CGFloat iTermCursorGuideAlphaThreshold; - (CGFloat)underlineThicknessForFont:(NSFont *)font; - (CGFloat)strikethroughThicknessForFont:(NSFont *)font; - (NSRange)underlinedRangeOnLine:(long long)row; +- (NSRange)activeLinkRangeOnLine:(long long)row; - (void)updateCachedMetrics; - (NSArray *)updateButtonFrames; diff --git a/sources/iTermTextDrawingHelper.m b/sources/iTermTextDrawingHelper.m index 4381580de8..b979882d7a 100644 --- a/sources/iTermTextDrawingHelper.m +++ b/sources/iTermTextDrawingHelper.m @@ -214,6 +214,13 @@ - (void)setUnderlinedRange:(VT100GridAbsWindowedRange)underlinedRange { _underlinedRange = underlinedRange; } +- (void)setActiveLinkRange:(VT100GridAbsWindowedRange)activeLinkRange { + if (VT100GridAbsWindowedRangeEquals(activeLinkRange, _activeLinkRange)) { + return; + } + _activeLinkRange = activeLinkRange; +} + #pragma mark - Drawing: General - (void)didFinishSetup { @@ -3954,6 +3961,38 @@ - (NSRange)underlinedRangeOnLine:(long long)row { } } +- (NSRange)activeLinkRangeOnLine:(long long)row { + if (_activeLinkRange.coordRange.start.x < 0) { + return NSMakeRange(0, 0); + } + + if (row == _activeLinkRange.coordRange.start.y && row == _activeLinkRange.coordRange.end.y) { + const int start = VT100GridAbsWindowedRangeStart(_activeLinkRange).x; + const int end = VT100GridAbsWindowedRangeEnd(_activeLinkRange).x; + return NSMakeRange(start, end - start); + } else if (row == _activeLinkRange.coordRange.start.y) { + const int start = VT100GridAbsWindowedRangeStart(_activeLinkRange).x; + const int end = + _activeLinkRange.columnWindow.length > 0 ? VT100GridRangeMax(_activeLinkRange.columnWindow) + 1 + : _gridSize.width; + return NSMakeRange(start, end - start); + } else if (row == _activeLinkRange.coordRange.end.y) { + const int start = + _activeLinkRange.columnWindow.length > 0 ? _activeLinkRange.columnWindow.location : 0; + const int end = VT100GridAbsWindowedRangeEnd(_activeLinkRange).x; + return NSMakeRange(start, end - start); + } else if (row > _activeLinkRange.coordRange.start.y && row < _activeLinkRange.coordRange.end.y) { + const int start = + _activeLinkRange.columnWindow.length > 0 ? _activeLinkRange.columnWindow.location : 0; + const int end = + _activeLinkRange.columnWindow.length > 0 ? VT100GridRangeMax(_activeLinkRange.columnWindow) + 1 + : _gridSize.width; + return NSMakeRange(start, end - start); + } else { + return NSMakeRange(0, 0); + } +} + #pragma mark - Cursor Utilities - (NSColor *)backgroundColorForCursor {