From a0ea69103c21bc4691ea566c43f925ce85e825fc Mon Sep 17 00:00:00 2001 From: Claas Lange Date: Wed, 8 Apr 2026 11:32:21 +0200 Subject: [PATCH 1/4] feat(tab-progress-bars): add tab and single-tab progress bars - add tab-level progress bars in the tab bar view - show inline session progress only for single-tab windows - keep progress bars non-interactive and synced with tab count --- sources/PTYSession.m | 2 + sources/PseudoTerminal.m | 10 ++ sources/SessionView.h | 1 + sources/SessionView.m | 8 +- sources/iTermProgressBarView.swift | 4 + sources/iTermTabBarControlView.m | 141 +++++++++++++++++++++++++++++ 6 files changed, 165 insertions(+), 1 deletion(-) diff --git a/sources/PTYSession.m b/sources/PTYSession.m index a5ae10bd32..5f183cfcc1 100644 --- a/sources/PTYSession.m +++ b/sources/PTYSession.m @@ -5316,6 +5316,8 @@ - (void)reallySetPreferencesFromAddressBookEntry:(NSDictionary *)aePrefs inProfile:aDict]]; self.view.enableProgressBars = [iTermProfilePreferences boolForKey:KEY_ENABLE_PROGRESS_BARS inProfile:aDict]; + id parentWindow = [_delegate parentWindow]; + self.view.showInlineProgressBar = (!parentWindow || [parentWindow numberOfTabs] == 1); self.view.progressBarHeight = [iTermProfilePreferences floatForKey:KEY_PROGRESS_BAR_HEIGHT inProfile:aDict]; self.view.progressBarColorScheme = [iTermProfilePreferences stringForKey:KEY_PROGRESS_BAR_COLOR_SCHEME diff --git a/sources/PseudoTerminal.m b/sources/PseudoTerminal.m index a829e1af73..6bf1a64fc0 100644 --- a/sources/PseudoTerminal.m +++ b/sources/PseudoTerminal.m @@ -7284,6 +7284,7 @@ - (void)tabViewDidChangeNumberOfTabViewItems:(NSTabView *)tabView { } [self updateTabColors]; + [self updateSessionProgressBarVisibility]; [self updateTabProgress]; [self updateToolbeltAppearance]; [self setNeedsUpdateTabObjectCounts:YES]; @@ -7758,6 +7759,8 @@ - (void)tabViewDoubleClickTabBar:(NSTabView *)tabView { - (void)tabView:(NSTabView *)tabView updateStateForTabViewItem:(NSTabViewItem *)tabViewItem { PTYTab *tab = tabViewItem.identifier; [_contentView.tabBarControl setIsProcessing:tab.isProcessing forTabWithIdentifier:tab]; + [_contentView.tabBarControl setProgress:(PSMProgress)tab.activeSession.screen.progress + forTabWithIdentifier:tab]; [_contentView.tabBarControl setIcon:tab.icon forTabWithIdentifier:tab]; [_contentView.tabBarControl setObjectCount:tab.objectCount forTabWithIdentifier:tab]; [_contentView.tabBarControl setIsPinned:tab.isPinned forTabViewItem:tabViewItem]; @@ -7790,6 +7793,13 @@ - (void)updateTabColors { [_contentView updateTitleAndBorderViews]; } +- (void)updateSessionProgressBarVisibility { + const BOOL showInlineProgressBar = (self.numberOfTabs == 1); + for (PTYSession *session in self.allSessions) { + session.view.showInlineProgressBar = showInlineProgressBar; + } +} + - (void)updateTabProgress { for (PTYTab *tab in [self tabs]) { [_contentView.tabBarControl setProgress:(PSMProgress)tab.activeSession.screen.progress diff --git a/sources/SessionView.h b/sources/SessionView.h index 2ea8abe75b..9a557efa0e 100644 --- a/sources/SessionView.h +++ b/sources/SessionView.h @@ -223,6 +223,7 @@ typedef NS_ENUM(NSUInteger, iTermSessionViewFindDriver) { @property (nonatomic, readonly) BOOL isBrowser; @property (nonatomic) VT100ScreenProgress progress; @property (nonatomic) BOOL enableProgressBars; +@property (nonatomic) BOOL showInlineProgressBar; @property (nonatomic) CGFloat progressBarHeight; @property (nonatomic, copy) NSString *progressBarColorScheme; @property (nonatomic, readonly) SessionTitleView *title; diff --git a/sources/SessionView.m b/sources/SessionView.m index bba5307641..e6ed25c257 100644 --- a/sources/SessionView.m +++ b/sources/SessionView.m @@ -186,6 +186,7 @@ - (instancetype)initWithFrame:(NSRect)frame { if (self) { [self registerForDraggedTypes:@[ iTermMovePaneDragType, @"com.iterm2.psm.controlitem" ]]; lastResizeDate_ = [NSDate date]; + _showInlineProgressBar = YES; _announcements = [[NSMutableArray alloc] init]; _imageView = [[iTermImageView alloc] init]; @@ -1967,7 +1968,7 @@ - (void)updateProgressBar { _progressBar.hidden = YES; } } - if (!self.enableProgressBars) { + if (!self.enableProgressBars || !self.showInlineProgressBar) { _progressBar.hidden = YES; } } @@ -1977,6 +1978,11 @@ - (void)setEnableProgressBars:(BOOL)enableProgressBars { [self updateProgressBar]; } +- (void)setShowInlineProgressBar:(BOOL)showInlineProgressBar { + _showInlineProgressBar = showInlineProgressBar; + [self updateProgressBar]; +} + - (void)setProgressBarHeight:(CGFloat)progressBarHeight { _progressBarHeight = progressBarHeight; if (_progressBar) { diff --git a/sources/iTermProgressBarView.swift b/sources/iTermProgressBarView.swift index ca863dd54d..0207a48bdc 100644 --- a/sources/iTermProgressBarView.swift +++ b/sources/iTermProgressBarView.swift @@ -508,6 +508,10 @@ private extension iTermProgressBarView { // MARK: - NSView overrides extension iTermProgressBarView { + override func hitTest(_ point: NSPoint) -> NSView? { + nil + } + // Layer frame updates are handled in layoutSublayers(of:), which updates frames based on mode, // ensuring the error layer fills its superlayer, the indeterminate layer animates properly, // and the determinate layer reflects the current progress percentage. diff --git a/sources/iTermTabBarControlView.m b/sources/iTermTabBarControlView.m index 5c410616d1..f0b80b9900 100644 --- a/sources/iTermTabBarControlView.m +++ b/sources/iTermTabBarControlView.m @@ -15,6 +15,8 @@ #import "NSObject+iTerm.h" #import "NSView+iTerm.h" #import "NSWindow+iTerm.h" +#import "PTYTab.h" +#import "SessionView.h" @interface NSView (Private2) - (NSRect)_opaqueRectForWindowMoveWhenInTitlebar; @@ -27,17 +29,26 @@ typedef NS_ENUM(NSInteger, iTermTabBarFlashState) { kFlashFadingOut, }; +@interface PSMTabBarControl (iTermTabBarControlViewPrivate) +- (NSMutableArray *)cells; +- (void)update:(BOOL)animate; +@end + @interface iTermTabBarControlView () @property(nonatomic, assign) iTermTabBarFlashState flashState; @end @implementation iTermTabBarControlView { iTermDelayedPerform *_flashDelayedPerform; // weak + NSMutableDictionary *_tabProgressBars; } +static const CGFloat iTermTabProgressBarHeight = 2; + - (instancetype)initWithFrame:(NSRect)frameRect { self = [super initWithFrame:frameRect]; if (self) { + _tabProgressBars = [[NSMutableDictionary alloc] init]; [self setTabsHaveCloseButtons:[iTermPreferences boolForKey:kPreferenceKeyTabsHaveCloseButton]]; self.minimumTabDragDistance = [iTermAdvancedSettingsModel minimumTabDragDistance]; // This used to depend on job but it's too difficult to do now that different sessions might @@ -58,8 +69,17 @@ - (instancetype)initWithFrame:(NSRect)frameRect { return self; } +- (void)dealloc { + for (iTermProgressBarView *progressBar in _tabProgressBars.allValues) { + [progressBar removeFromSuperview]; + } + [_tabProgressBars release]; + [super dealloc]; +} + - (void)drawRect:(NSRect)dirtyRect { [super drawRect:dirtyRect]; + [self syncTabProgressBars]; } - (void)setCmdPressed:(BOOL)cmdPressed { @@ -144,6 +164,7 @@ - (void)setFrame:(NSRect)frame { self, [NSThread callStackSymbols]); [super setFrame:frame]; + [self syncTabProgressBars]; } - (void)fadeIn { @@ -274,10 +295,130 @@ - (void)setOrientation:(PSMTabBarOrientation)orientation { self.height = tabBarHeight; } self.showAddTabButton = ![iTermAdvancedSettingsModel removeAddTabButton] && (orientation == PSMTabBarHorizontalOrientation); + [self syncTabProgressBars]; +} + +- (void)update:(BOOL)animate { + [super update:animate]; + [self syncTabProgressBars]; +} + +- (void)setProgress:(PSMProgress)progress forTabWithIdentifier:(id)identifier { + [super setProgress:progress forTabWithIdentifier:identifier]; + [self syncTabProgressBars]; +} + +- (void)removeTabForCell:(PSMTabBarCell *)cell { + [self removeProgressBarForCell:cell]; + [super removeTabForCell:cell]; +} + +- (void)removeCell:(PSMTabBarCell *)cell { + [self removeProgressBarForCell:cell]; + [super removeCell:cell]; } #pragma mark - Private +- (NSValue *)progressBarKeyForCell:(PSMTabBarCell *)cell { + return [NSValue valueWithNonretainedObject:cell]; +} + +- (BOOL)cellAllowsTabProgressBar:(PSMTabBarCell *)cell { + NSTabViewItem *item = (NSTabViewItem *)cell.representedObject; + PTYTab *tab = item.identifier; + if (![tab isKindOfClass:[PTYTab class]]) { + return YES; + } + return tab.activeSession.view.enableProgressBars; +} + +- (BOOL)cellShouldShowTabProgressBar:(PSMTabBarCell *)cell { + if (self.tabView.numberOfTabViewItems <= 1 || + cell.isPlaceholder || + cell.isInOverflowMenu || + ![self cellAllowsTabProgressBar:cell]) { + return NO; + } + switch (cell.progress) { + case PSMProgressStopped: + return NO; + case PSMProgressError: + case PSMProgressIndeterminate: + return YES; + case PSMProgressSuccessBase: + case PSMProgressWarningBase: + case PSMProgressErrorBase: + break; + } + return ((cell.progress >= PSMProgressSuccessBase && cell.progress <= PSMProgressSuccessBase + 100) || + (cell.progress >= PSMProgressErrorBase && cell.progress <= PSMProgressErrorBase + 100) || + (cell.progress >= PSMProgressWarningBase && cell.progress <= PSMProgressWarningBase + 100)); +} + +- (NSString *)tabProgressBarColorSchemeForCell:(PSMTabBarCell *)cell { + NSTabViewItem *item = (NSTabViewItem *)cell.representedObject; + PTYTab *tab = item.identifier; + if (![tab isKindOfClass:[PTYTab class]]) { + return iTermProgressBarColorSchemeDefault; + } + return tab.activeSession.view.progressBarColorScheme ?: iTermProgressBarColorSchemeDefault; +} + +- (NSRect)frameForProgressBarInCell:(PSMTabBarCell *)cell { + return NSMakeRect(cell.frame.origin.x, + cell.frame.origin.y, + cell.frame.size.width, + iTermTabProgressBarHeight); +} + +- (void)removeProgressBarForCell:(PSMTabBarCell *)cell { + NSValue *key = [self progressBarKeyForCell:cell]; + iTermProgressBarView *progressBar = _tabProgressBars[key]; + if (!progressBar) { + return; + } + [progressBar removeFromSuperview]; + [_tabProgressBars removeObjectForKey:key]; +} + +- (void)syncTabProgressBars { + NSMutableSet *visibleKeys = [NSMutableSet set]; + for (PSMTabBarCell *cell in self.cells) { + if (![self cellShouldShowTabProgressBar:cell]) { + [self removeProgressBarForCell:cell]; + continue; + } + NSValue *key = [self progressBarKeyForCell:cell]; + [visibleKeys addObject:key]; + iTermProgressBarView *progressBar = _tabProgressBars[key]; + if (!progressBar) { + progressBar = [[[iTermProgressBarView alloc] init] autorelease]; + progressBar.heightValue = iTermTabProgressBarHeight; + _tabProgressBars[key] = progressBar; + } + progressBar.darkMode = self.style.useLightControls; + progressBar.colorScheme = [self tabProgressBarColorSchemeForCell:cell]; + progressBar.state = (VT100ScreenProgress)cell.progress; + progressBar.frame = [self frameForProgressBarInCell:cell]; + progressBar.hidden = NO; + if (progressBar.superview != self) { + [self addSubview:progressBar]; + } + cell.indicator.hidden = YES; + cell.indicator.animate = NO; + [cell.indicator removeFromSuperview]; + } + + for (NSValue *key in [_tabProgressBars allKeys]) { + if (![visibleKeys containsObject:key]) { + iTermProgressBarView *progressBar = _tabProgressBars[key]; + [progressBar removeFromSuperview]; + [_tabProgressBars removeObjectForKey:key]; + } + } +} + - (void)setFlashState:(iTermTabBarFlashState)flashState { NSArray *names = @[ @"Off", @"FadeIn", @"Holding", @"Extending", @"FadeOut" ]; DLog(@"%@ -> %@ from\n%@", names[self.flashState], names[flashState], [NSThread callStackSymbols]); From 1d5dd489fd16f430f58896e4dc50d3e0e930d362 Mon Sep 17 00:00:00 2001 From: Claas Lange Date: Wed, 8 Apr 2026 20:47:06 +0200 Subject: [PATCH 2/4] fix(tab-progress-bars): address review feedback - centralize inline progress bar visibility in PseudoTerminal - show inline progress for split panes and hidden tab bars - move tab progress bar ownership into PSMTabBarControl - keep Tahoe tab progress inside the selected pill - aggregate tab progress across split panes --- .../source/PSMTabBarControl.h | 5 + .../source/PSMTabBarControl.m | 81 +++++++++++++ .../PSMTabBarControl/source/PSMTabStyle.h | 1 + .../source/PSMTahoeTabStyle.swift | 29 ++++- .../source/PSMYosemiteTabStyle.m | 8 ++ sources/PTYSession.m | 2 - sources/PTYTab.h | 1 + sources/PTYTab.m | 30 ++++- sources/PseudoTerminal.m | 28 ++++- sources/iTermTabBarControlView.m | 107 ++---------------- 10 files changed, 185 insertions(+), 107 deletions(-) diff --git a/ThirdParty/PSMTabBarControl/source/PSMTabBarControl.h b/ThirdParty/PSMTabBarControl/source/PSMTabBarControl.h index a11b751332..1675f491f5 100644 --- a/ThirdParty/PSMTabBarControl/source/PSMTabBarControl.h +++ b/ThirdParty/PSMTabBarControl/source/PSMTabBarControl.h @@ -174,6 +174,8 @@ typedef NS_ENUM(int, PSMTabPosition) { PSMTab_LeftTab = 2, }; +extern const CGFloat PSMTabBarProgressBarHeight; + // This view provides a control interface to manage a regular NSTabView. It looks and works like // the tabbed browsing interface of many popular browsers. @interface PSMTabBarControl : NSControl< @@ -256,6 +258,9 @@ typedef NS_ENUM(int, PSMTabPosition) { - (void)setIsProcessing:(BOOL)isProcessing forTabWithIdentifier:(id)identifier; - (void)setProgress:(PSMProgress)progress forTabWithIdentifier:(id)identifier; +- (BOOL)shouldShowCustomProgressBarForTabCell:(PSMTabBarCell *)cell; +- (nullable NSView *)customProgressBarViewForTabCell:(PSMTabBarCell *)cell; +- (void)configureCustomProgressBarView:(NSView *)view forTabCell:(PSMTabBarCell *)cell; - (void)setIcon:(NSImage *)icon forTabWithIdentifier:(id)identifier; - (void)setObjectCount:(NSInteger)objectCount forTabWithIdentifier:(id)identifier; - (void)graphicDidChangeForTabWithIdentifier:(id)identifier; diff --git a/ThirdParty/PSMTabBarControl/source/PSMTabBarControl.m b/ThirdParty/PSMTabBarControl/source/PSMTabBarControl.m index 2262da6357..ea6340ce06 100644 --- a/ThirdParty/PSMTabBarControl/source/PSMTabBarControl.m +++ b/ThirdParty/PSMTabBarControl/source/PSMTabBarControl.m @@ -37,6 +37,7 @@ static os_log_t PSMTabBarLog(void) { NSString *const PSMTabDragDidEndNotification = @"PSMTabDragDidEndNotification"; NSString *const PSMTabDragDidBeginNotification = @"PSMTabDragDidBeginNotification"; const CGFloat kSPMTabBarCellInternalXMargin = 6; +const CGFloat PSMTabBarProgressBarHeight = 2; const CGFloat kPSMTabBarCellPadding = 4; const CGFloat kPSMTabBarCellIconPadding = 4; @@ -140,6 +141,8 @@ - (BOOL)isEqual:(id)object { @end @interface PSMTabBarControl () +- (void)removeTabProgressBarForCell:(PSMTabBarCell *)cell; +- (void)syncTabProgressBars; @end @implementation PSMTabBarControl { @@ -177,6 +180,7 @@ @implementation PSMTabBarControl { BOOL _needsUpdateAnimate; BOOL _needsUpdate; NSInteger _preDragSelectedTabIndex; // or NSNotFound + NSMutableDictionary *_tabProgressBars; NSMutableArray *_tooltips; NSInteger _toolTipCoalescing; } @@ -251,6 +255,7 @@ - (id)initWithFrame:(NSRect)frame { _style = [[PSMYosemiteTabStyle alloc] init]; } _preDragSelectedTabIndex = NSNotFound; + _tabProgressBars = [[NSMutableDictionary alloc] init]; // the overflow button/menu [self setupButtons]; @@ -376,6 +381,10 @@ - (void)dealloc { [cell release]; } + for (NSView *progressBar in _tabProgressBars.allValues) { + [progressBar removeFromSuperview]; + } + [_tabProgressBars release]; [_overflowPopUpButton release]; [_cells release]; [_tabView release]; @@ -715,11 +724,74 @@ - (void)removeTabForCell:(PSMTabBarCell *)cell { [cell removeCloseButtonTrackingRectFrom:self]; [cell removeCellTrackingRectFrom:self]; [self removeAllToolTips]; + [self removeTabProgressBarForCell:cell]; // pull from collection [_cells removeObject:cell]; } +- (BOOL)shouldShowCustomProgressBarForTabCell:(PSMTabBarCell *)cell { + return NO; +} + +- (NSView *)customProgressBarViewForTabCell:(PSMTabBarCell *)cell { + return nil; +} + +- (void)configureCustomProgressBarView:(NSView *)view forTabCell:(PSMTabBarCell *)cell { +} + +- (NSValue *)tabProgressBarKeyForCell:(PSMTabBarCell *)cell { + return [NSValue valueWithNonretainedObject:cell]; +} + +- (void)removeTabProgressBarForCell:(PSMTabBarCell *)cell { + NSValue *key = [self tabProgressBarKeyForCell:cell]; + NSView *progressBar = _tabProgressBars[key]; + if (!progressBar) { + return; + } + [progressBar removeFromSuperview]; + [_tabProgressBars removeObjectForKey:key]; +} + +- (void)syncTabProgressBars { + NSMutableSet *visibleKeys = [NSMutableSet set]; + for (PSMTabBarCell *cell in _cells) { + if (![self shouldShowCustomProgressBarForTabCell:cell]) { + [self removeTabProgressBarForCell:cell]; + continue; + } + NSValue *key = [self tabProgressBarKeyForCell:cell]; + [visibleKeys addObject:key]; + NSView *progressBar = _tabProgressBars[key]; + if (!progressBar) { + progressBar = [self customProgressBarViewForTabCell:cell]; + if (!progressBar) { + continue; + } + _tabProgressBars[key] = progressBar; + } + [self configureCustomProgressBarView:progressBar forTabCell:cell]; + progressBar.frame = [self.style progressBarRectForTabCell:cell]; + progressBar.hidden = NO; + if (progressBar.superview != self) { + [self addSubview:progressBar]; + } + cell.indicator.hidden = YES; + cell.indicator.animate = NO; + [cell.indicator removeFromSuperview]; + } + + for (NSValue *key in [_tabProgressBars allKeys]) { + if (![visibleKeys containsObject:key]) { + NSView *progressBar = _tabProgressBars[key]; + [progressBar removeFromSuperview]; + [_tabProgressBars removeObjectForKey:key]; + } + } +} + - (void)dragDidFinish { _preDragSelectedTabIndex = NSNotFound; } @@ -1014,6 +1086,7 @@ - (void)drawRect:(NSRect)insaneRect { clipRect:rect horizontal:(_orientation == PSMTabBarHorizontalOrientation) withOverflow:_lainOutWithOverflow]; + [self syncTabProgressBars]; #if PSM_DEBUG_DRAG_PERFORMANCE CFAbsoluteTime end = CFAbsoluteTimeGetCurrent(); @@ -1066,6 +1139,11 @@ - (void)update { [self update:NO]; } +- (void)setFrame:(NSRect)frame { + [super setFrame:frame]; + [self syncTabProgressBars]; +} + - (void)update:(BOOL)animate { // This method handles all of the cell layout, and is called when something changes to require // the refresh. This method is not called during drag and drop. See the PSMTabDragAssistant's @@ -1198,6 +1276,7 @@ - (void)reallyUpdate:(BOOL)animate { [self finishUpdateWithRegularWidths:newOrigins widthsWithOverflow:newOrigins]; } + [self syncTabProgressBars]; [self setNeedsDisplay:YES]; } @@ -1401,6 +1480,7 @@ - (void)computeCellFramesInContainerOfWidth:(CGFloat)containerWidth - (void)removeCell:(PSMTabBarCell *)cell { [cell removeCloseButtonTrackingRectFrom:self]; [cell removeCellTrackingRectFrom:self]; + [self removeTabProgressBarForCell:cell]; [[self cells] removeObject:cell]; } @@ -2521,6 +2601,7 @@ - (void)setIsProcessing:(BOOL)isProcessing forTabWithIdentifier:(id)identifier { - (void)setProgress:(PSMProgress)progress forTabWithIdentifier:(id)identifier { PSMTabBarCell *cell = [self cellWithIdentifier:identifier]; cell.progress = progress; + [self syncTabProgressBars]; } - (void)graphicDidChangeForTabWithIdentifier:(id)identifier { diff --git a/ThirdParty/PSMTabBarControl/source/PSMTabStyle.h b/ThirdParty/PSMTabBarControl/source/PSMTabStyle.h index 26174f7ae4..d062e28f39 100644 --- a/ThirdParty/PSMTabBarControl/source/PSMTabStyle.h +++ b/ThirdParty/PSMTabBarControl/source/PSMTabStyle.h @@ -39,6 +39,7 @@ Protocol to be observed by all style delegate objects. These objects handle the - (NSRect)dragRectForTabCell:(PSMTabBarCell *)cell orientation:(PSMTabBarOrientation)orientation; - (NSRect)closeButtonRectForTabCell:(PSMTabBarCell *)cell; - (NSRect)indicatorRectForTabCell:(PSMTabBarCell *)cell; +- (NSRect)progressBarRectForTabCell:(PSMTabBarCell *)cell; - (NSRect)objectCounterRectForTabCell:(PSMTabBarCell *)cell; - (float)minimumWidthOfTabCell:(PSMTabBarCell *)cell; - (float)desiredWidthOfTabCell:(PSMTabBarCell *)cell; diff --git a/ThirdParty/PSMTabBarControl/source/PSMTahoeTabStyle.swift b/ThirdParty/PSMTabBarControl/source/PSMTahoeTabStyle.swift index 9252f7c51f..08da0e83e7 100644 --- a/ThirdParty/PSMTabBarControl/source/PSMTahoeTabStyle.swift +++ b/ThirdParty/PSMTabBarControl/source/PSMTahoeTabStyle.swift @@ -413,6 +413,31 @@ class PSMTahoeTabStyle: NSObject, PSMTabStyle { frame.size.height = generic.height - 1 return frame } + + private func backgroundRect(for cellFrame: NSRect) -> NSRect { + var rect = cellFrame + rect.origin.y += 2 + rect.size.height -= 3 + return rect + } + + @objc func progressBarRect(forTabCell cell: PSMTabBarCell) -> NSRect { + let cellFrame = cell.frame + guard _orientation == .horizontalOrientation, cell.state == .on else { + return NSRect(x: cellFrame.origin.x, + y: cellFrame.origin.y, + width: cellFrame.size.width, + height: PSMTabBarProgressBarHeight) + } + + let pillRect = backgroundRect(for: cellFrame) + let horizontalInset = CGFloat(4) + let verticalInset = CGFloat(1) + return NSRect(x: pillRect.origin.x + horizontalInset, + y: pillRect.maxY - PSMTabBarProgressBarHeight - verticalInset, + width: max(CGFloat(0), pillRect.size.width - horizontalInset * 2), + height: PSMTabBarProgressBarHeight) + } // MARK: - Drawing @@ -789,9 +814,7 @@ class PSMTahoeTabStyle: NSObject, PSMTabStyle { backgroundColorSelected(selected, highlightAmount: isHighlighted ? 1.0 : 0.0).set() let radius = barRadius - 2.5 - var rect = cellFrame - rect.origin.y += 2 - rect.size.height -= 3 + let rect = backgroundRect(for: cellFrame) let path = NSBezierPath(roundedRect: rect, xRadius: radius, yRadius: radius) path.fill() if selected { diff --git a/ThirdParty/PSMTabBarControl/source/PSMYosemiteTabStyle.m b/ThirdParty/PSMTabBarControl/source/PSMYosemiteTabStyle.m index 684b98d564..030be20eda 100644 --- a/ThirdParty/PSMTabBarControl/source/PSMYosemiteTabStyle.m +++ b/ThirdParty/PSMTabBarControl/source/PSMYosemiteTabStyle.m @@ -262,6 +262,14 @@ - (NSRect)indicatorRectForTabCell:(PSMTabBarCell *)cell { return result; } +- (NSRect)progressBarRectForTabCell:(PSMTabBarCell *)cell { + NSRect cellFrame = [cell frame]; + return NSMakeRect(cellFrame.origin.x, + cellFrame.origin.y, + cellFrame.size.width, + PSMTabBarProgressBarHeight); +} + - (NSRect)adjustedCellRect:(NSRect)rect generic:(NSRect)generic { return rect; } diff --git a/sources/PTYSession.m b/sources/PTYSession.m index 5f183cfcc1..a5ae10bd32 100644 --- a/sources/PTYSession.m +++ b/sources/PTYSession.m @@ -5316,8 +5316,6 @@ - (void)reallySetPreferencesFromAddressBookEntry:(NSDictionary *)aePrefs inProfile:aDict]]; self.view.enableProgressBars = [iTermProfilePreferences boolForKey:KEY_ENABLE_PROGRESS_BARS inProfile:aDict]; - id parentWindow = [_delegate parentWindow]; - self.view.showInlineProgressBar = (!parentWindow || [parentWindow numberOfTabs] == 1); self.view.progressBarHeight = [iTermProfilePreferences floatForKey:KEY_PROGRESS_BAR_HEIGHT inProfile:aDict]; self.view.progressBarColorScheme = [iTermProfilePreferences stringForKey:KEY_PROGRESS_BAR_COLOR_SCHEME diff --git a/sources/PTYTab.h b/sources/PTYTab.h index 42976ddc9d..5adf566bee 100644 --- a/sources/PTYTab.h +++ b/sources/PTYTab.h @@ -66,6 +66,7 @@ extern NSString *const PTYTabArrangementOptionsPendingJumps; @property(nonatomic, retain) NSImage *icon; // Aggregated tab status from all sessions (highest-priority wins) @property(nonatomic, readonly) iTermSessionTabStatus *aggregatedTabStatus; +@property(nonatomic, readonly) VT100ScreenProgress progress; // Size we should report to fit the current layout @property(nonatomic, readonly) NSSize tmuxSize; diff --git a/sources/PTYTab.m b/sources/PTYTab.m index 96c8af762f..b68e7c44af 100644 --- a/sources/PTYTab.m +++ b/sources/PTYTab.m @@ -7283,10 +7283,34 @@ - (void)sessionProcessInfoProviderDidChange:(PTYSession *)session { [self.delegate tabProcessInfoProviderDidChange:self]; } -- (void)session:(PTYSession *)session progressDidChange:(VT100ScreenProgress)progress { - if (session == self.activeSession) { - [self.delegate tab:self progressDidChange:progress]; +static BOOL iTermTabProgressIsVisible(VT100ScreenProgress progress) { + if (progress == VT100ScreenProgressError || progress == VT100ScreenProgressIndeterminate) { + return YES; + } + return ((progress >= VT100ScreenProgressSuccessBase && progress <= VT100ScreenProgressSuccessBase + 100) || + (progress >= VT100ScreenProgressErrorBase && progress <= VT100ScreenProgressErrorBase + 100) || + (progress >= VT100ScreenProgressWarningBase && progress <= VT100ScreenProgressWarningBase + 100)); +} + +- (VT100ScreenProgress)progress { + const VT100ScreenProgress activeProgress = self.activeSession.screen.progress; + if (iTermTabProgressIsVisible(activeProgress)) { + return activeProgress; + } + for (PTYSession *session in self.sessions) { + if (session == self.activeSession) { + continue; + } + const VT100ScreenProgress progress = session.screen.progress; + if (iTermTabProgressIsVisible(progress)) { + return progress; + } } + return VT100ScreenProgressStopped; +} + +- (void)session:(PTYSession *)session progressDidChange:(VT100ScreenProgress)progress { + [self.delegate tab:self progressDidChange:self.progress]; } - (NSScriptObjectSpecifier *)objectSpecifier { diff --git a/sources/PseudoTerminal.m b/sources/PseudoTerminal.m index 6bf1a64fc0..28d1c3fd42 100644 --- a/sources/PseudoTerminal.m +++ b/sources/PseudoTerminal.m @@ -6309,6 +6309,7 @@ - (void)updateTabBarControlIsTitlebarAccessory { DLog(@"tab bar should NOT be accessory, but is on loan."); [self returnTabBarToContentView]; } + [self updateSessionProgressBarVisibility]; DLog(@"Tab bar state after updateTabBarControlIsTitlebarAccessory: hidden=%@ alpha=%@ frame=%@", @(_contentView.tabBarControl.isHidden), @(_contentView.tabBarControl.alphaValue), @@ -7759,7 +7760,7 @@ - (void)tabViewDoubleClickTabBar:(NSTabView *)tabView { - (void)tabView:(NSTabView *)tabView updateStateForTabViewItem:(NSTabViewItem *)tabViewItem { PTYTab *tab = tabViewItem.identifier; [_contentView.tabBarControl setIsProcessing:tab.isProcessing forTabWithIdentifier:tab]; - [_contentView.tabBarControl setProgress:(PSMProgress)tab.activeSession.screen.progress + [_contentView.tabBarControl setProgress:(PSMProgress)tab.progress forTabWithIdentifier:tab]; [_contentView.tabBarControl setIcon:tab.icon forTabWithIdentifier:tab]; [_contentView.tabBarControl setObjectCount:tab.objectCount forTabWithIdentifier:tab]; @@ -7793,16 +7794,34 @@ - (void)updateTabColors { [_contentView updateTitleAndBorderViews]; } +- (BOOL)tabBarProvidesProgressVisibility { + if (togglingLionFullScreen_ || self.lionFullScreen) { + return self.shouldShowPermanentFullScreenTabBar; + } + return self.tabBarShouldBeVisible; +} + +- (BOOL)shouldShowInlineProgressBarForSession:(PTYSession *)session tabBarVisible:(BOOL)tabBarVisible { + PTYTab *tab = [self tabForSession:session]; + if (!tab) { + return YES; + } + const BOOL hasSingleTab = (self.numberOfTabs == 1); + const BOOL hasSplitPanes = (tab.sessions.count > 1); + return (hasSingleTab || hasSplitPanes || !tabBarVisible); +} + - (void)updateSessionProgressBarVisibility { - const BOOL showInlineProgressBar = (self.numberOfTabs == 1); + const BOOL tabBarVisible = [self tabBarProvidesProgressVisibility]; for (PTYSession *session in self.allSessions) { - session.view.showInlineProgressBar = showInlineProgressBar; + session.view.showInlineProgressBar = [self shouldShowInlineProgressBarForSession:session + tabBarVisible:tabBarVisible]; } } - (void)updateTabProgress { for (PTYTab *tab in [self tabs]) { - [_contentView.tabBarControl setProgress:(PSMProgress)tab.activeSession.screen.progress + [_contentView.tabBarControl setProgress:(PSMProgress)tab.progress forTabWithIdentifier:tab]; } } @@ -13015,6 +13034,7 @@ - (void)currentSessionWordAtCursorDidBecome:(NSString *)word { } - (void)numberOfSessionsDidChangeInTab:(PTYTab *)tab { + [self updateSessionProgressBarVisibility]; if (tab == self.currentTab) { [self updateUseTransparency]; } diff --git a/sources/iTermTabBarControlView.m b/sources/iTermTabBarControlView.m index f0b80b9900..e2d537e90d 100644 --- a/sources/iTermTabBarControlView.m +++ b/sources/iTermTabBarControlView.m @@ -29,26 +29,17 @@ typedef NS_ENUM(NSInteger, iTermTabBarFlashState) { kFlashFadingOut, }; -@interface PSMTabBarControl (iTermTabBarControlViewPrivate) -- (NSMutableArray *)cells; -- (void)update:(BOOL)animate; -@end - @interface iTermTabBarControlView () @property(nonatomic, assign) iTermTabBarFlashState flashState; @end @implementation iTermTabBarControlView { iTermDelayedPerform *_flashDelayedPerform; // weak - NSMutableDictionary *_tabProgressBars; } -static const CGFloat iTermTabProgressBarHeight = 2; - - (instancetype)initWithFrame:(NSRect)frameRect { self = [super initWithFrame:frameRect]; if (self) { - _tabProgressBars = [[NSMutableDictionary alloc] init]; [self setTabsHaveCloseButtons:[iTermPreferences boolForKey:kPreferenceKeyTabsHaveCloseButton]]; self.minimumTabDragDistance = [iTermAdvancedSettingsModel minimumTabDragDistance]; // This used to depend on job but it's too difficult to do now that different sessions might @@ -69,19 +60,6 @@ - (instancetype)initWithFrame:(NSRect)frameRect { return self; } -- (void)dealloc { - for (iTermProgressBarView *progressBar in _tabProgressBars.allValues) { - [progressBar removeFromSuperview]; - } - [_tabProgressBars release]; - [super dealloc]; -} - -- (void)drawRect:(NSRect)dirtyRect { - [super drawRect:dirtyRect]; - [self syncTabProgressBars]; -} - - (void)setCmdPressed:(BOOL)cmdPressed { if (cmdPressed == _cmdPressed) { return; @@ -295,35 +273,10 @@ - (void)setOrientation:(PSMTabBarOrientation)orientation { self.height = tabBarHeight; } self.showAddTabButton = ![iTermAdvancedSettingsModel removeAddTabButton] && (orientation == PSMTabBarHorizontalOrientation); - [self syncTabProgressBars]; -} - -- (void)update:(BOOL)animate { - [super update:animate]; - [self syncTabProgressBars]; -} - -- (void)setProgress:(PSMProgress)progress forTabWithIdentifier:(id)identifier { - [super setProgress:progress forTabWithIdentifier:identifier]; - [self syncTabProgressBars]; -} - -- (void)removeTabForCell:(PSMTabBarCell *)cell { - [self removeProgressBarForCell:cell]; - [super removeTabForCell:cell]; -} - -- (void)removeCell:(PSMTabBarCell *)cell { - [self removeProgressBarForCell:cell]; - [super removeCell:cell]; } #pragma mark - Private -- (NSValue *)progressBarKeyForCell:(PSMTabBarCell *)cell { - return [NSValue valueWithNonretainedObject:cell]; -} - - (BOOL)cellAllowsTabProgressBar:(PSMTabBarCell *)cell { NSTabViewItem *item = (NSTabViewItem *)cell.representedObject; PTYTab *tab = item.identifier; @@ -365,58 +318,22 @@ - (NSString *)tabProgressBarColorSchemeForCell:(PSMTabBarCell *)cell { return tab.activeSession.view.progressBarColorScheme ?: iTermProgressBarColorSchemeDefault; } -- (NSRect)frameForProgressBarInCell:(PSMTabBarCell *)cell { - return NSMakeRect(cell.frame.origin.x, - cell.frame.origin.y, - cell.frame.size.width, - iTermTabProgressBarHeight); +- (BOOL)shouldShowCustomProgressBarForTabCell:(PSMTabBarCell *)cell { + return [self cellShouldShowTabProgressBar:cell]; } -- (void)removeProgressBarForCell:(PSMTabBarCell *)cell { - NSValue *key = [self progressBarKeyForCell:cell]; - iTermProgressBarView *progressBar = _tabProgressBars[key]; - if (!progressBar) { - return; - } - [progressBar removeFromSuperview]; - [_tabProgressBars removeObjectForKey:key]; +- (NSView *)customProgressBarViewForTabCell:(PSMTabBarCell *)cell { + iTermProgressBarView *progressBar = [[[iTermProgressBarView alloc] init] autorelease]; + progressBar.heightValue = PSMTabBarProgressBarHeight; + return progressBar; } -- (void)syncTabProgressBars { - NSMutableSet *visibleKeys = [NSMutableSet set]; - for (PSMTabBarCell *cell in self.cells) { - if (![self cellShouldShowTabProgressBar:cell]) { - [self removeProgressBarForCell:cell]; - continue; - } - NSValue *key = [self progressBarKeyForCell:cell]; - [visibleKeys addObject:key]; - iTermProgressBarView *progressBar = _tabProgressBars[key]; - if (!progressBar) { - progressBar = [[[iTermProgressBarView alloc] init] autorelease]; - progressBar.heightValue = iTermTabProgressBarHeight; - _tabProgressBars[key] = progressBar; - } - progressBar.darkMode = self.style.useLightControls; - progressBar.colorScheme = [self tabProgressBarColorSchemeForCell:cell]; - progressBar.state = (VT100ScreenProgress)cell.progress; - progressBar.frame = [self frameForProgressBarInCell:cell]; - progressBar.hidden = NO; - if (progressBar.superview != self) { - [self addSubview:progressBar]; - } - cell.indicator.hidden = YES; - cell.indicator.animate = NO; - [cell.indicator removeFromSuperview]; - } - - for (NSValue *key in [_tabProgressBars allKeys]) { - if (![visibleKeys containsObject:key]) { - iTermProgressBarView *progressBar = _tabProgressBars[key]; - [progressBar removeFromSuperview]; - [_tabProgressBars removeObjectForKey:key]; - } - } +- (void)configureCustomProgressBarView:(NSView *)view forTabCell:(PSMTabBarCell *)cell { + iTermProgressBarView *progressBar = (iTermProgressBarView *)view; + progressBar.heightValue = PSMTabBarProgressBarHeight; + progressBar.darkMode = self.style.useLightControls; + progressBar.colorScheme = [self tabProgressBarColorSchemeForCell:cell]; + progressBar.state = (VT100ScreenProgress)cell.progress; } - (void)setFlashState:(iTermTabBarFlashState)flashState { From c75a7a786d26eba25755181849af65ebed0bbaec Mon Sep 17 00:00:00 2001 From: Claas Lange Date: Thu, 9 Apr 2026 10:45:01 +0200 Subject: [PATCH 3/4] feat(tab-progress-bars): support left-side tab progress - render tab progress bars vertically for left-side tab bars - place vertical progress bars along the left edge of tab cells - animate determinate and indeterminate progress in vertical mode --- .../source/PSMTahoeTabStyle.swift | 8 +- .../source/PSMYosemiteTabStyle.m | 6 + sources/iTermProgressBarView.swift | 167 +++++++++++++----- sources/iTermTabBarControlView.m | 1 + 4 files changed, 133 insertions(+), 49 deletions(-) diff --git a/ThirdParty/PSMTabBarControl/source/PSMTahoeTabStyle.swift b/ThirdParty/PSMTabBarControl/source/PSMTahoeTabStyle.swift index 08da0e83e7..e27f42719e 100644 --- a/ThirdParty/PSMTabBarControl/source/PSMTahoeTabStyle.swift +++ b/ThirdParty/PSMTabBarControl/source/PSMTahoeTabStyle.swift @@ -423,7 +423,13 @@ class PSMTahoeTabStyle: NSObject, PSMTabStyle { @objc func progressBarRect(forTabCell cell: PSMTabBarCell) -> NSRect { let cellFrame = cell.frame - guard _orientation == .horizontalOrientation, cell.state == .on else { + if _orientation == .verticalOrientation { + return NSRect(x: cellFrame.origin.x, + y: cellFrame.origin.y, + width: PSMTabBarProgressBarHeight, + height: cellFrame.size.height) + } + guard cell.state == .on else { return NSRect(x: cellFrame.origin.x, y: cellFrame.origin.y, width: cellFrame.size.width, diff --git a/ThirdParty/PSMTabBarControl/source/PSMYosemiteTabStyle.m b/ThirdParty/PSMTabBarControl/source/PSMYosemiteTabStyle.m index 030be20eda..f437a2b272 100644 --- a/ThirdParty/PSMTabBarControl/source/PSMYosemiteTabStyle.m +++ b/ThirdParty/PSMTabBarControl/source/PSMYosemiteTabStyle.m @@ -264,6 +264,12 @@ - (NSRect)indicatorRectForTabCell:(PSMTabBarCell *)cell { - (NSRect)progressBarRectForTabCell:(PSMTabBarCell *)cell { NSRect cellFrame = [cell frame]; + if (_orientation == PSMTabBarVerticalOrientation) { + return NSMakeRect(cellFrame.origin.x, + cellFrame.origin.y, + PSMTabBarProgressBarHeight, + cellFrame.size.height); + } return NSMakeRect(cellFrame.origin.x, cellFrame.origin.y, cellFrame.size.width, diff --git a/sources/iTermProgressBarView.swift b/sources/iTermProgressBarView.swift index 0207a48bdc..221d87ebe6 100644 --- a/sources/iTermProgressBarView.swift +++ b/sources/iTermProgressBarView.swift @@ -45,6 +45,14 @@ extension VT100ScreenProgress { @objc class iTermProgressBarView: NSView { @objc var heightValue: CGFloat = 2.0 + @objc var vertical = false { + didSet { + if vertical != oldValue { + updateGradientDirections() + layer?.setNeedsLayout() + } + } + } @objc var colorScheme: String = iTermProgressBarColorSchemeDefault { didSet { updateLayerColors() @@ -274,7 +282,28 @@ private extension iTermProgressBarView { updateLayerColors() } + private func updateGradientDirections() { + let startPoint: CGPoint + let endPoint: CGPoint + if vertical { + startPoint = CGPoint(x: 0.5, y: 1) + endPoint = CGPoint(x: 0.5, y: 0) + } else { + startPoint = CGPoint(x: 0, y: 0.5) + endPoint = CGPoint(x: 1, y: 0.5) + } + for layer in [errorLayer, indeterminateLayer1, indeterminateLayer2, determinateLayer] { + guard let gradientLayer = layer as? CAGradientLayer else { + continue + } + gradientLayer.startPoint = startPoint + gradientLayer.endPoint = endPoint + } + } + private func updateLayerColors() { + updateGradientDirections() + // Update error layer colors if let errorGradient = errorLayer as? CAGradientLayer { let colors = errorColors(dark: darkMode) @@ -401,17 +430,15 @@ private extension iTermProgressBarView { private func setupIndeterminateLayers() { let width = layer?.bounds.width ?? bounds.width - let height = desiredHeight - - let gradientWidth = width + let height = vertical ? (layer?.bounds.height ?? bounds.height) : desiredHeight // Set up container to clip content indeterminateContainer.frame = CGRect(x: 0, y: 0, width: width, height: height) indeterminateContainer.masksToBounds = true // Set up both gradient layers - indeterminateLayer1.frame = CGRect(x: 0, y: 0, width: gradientWidth, height: height) - indeterminateLayer2.frame = CGRect(x: 0, y: 0, width: gradientWidth, height: height) + indeterminateLayer1.frame = CGRect(x: 0, y: 0, width: width, height: height) + indeterminateLayer2.frame = CGRect(x: 0, y: 0, width: width, height: height) indeterminateContainer.addSublayer(indeterminateLayer1) indeterminateContainer.addSublayer(indeterminateLayer2) @@ -430,44 +457,71 @@ private extension iTermProgressBarView { indeterminateLayer2.removeAnimation(forKey: animationKey) let width = layer?.bounds.width ?? bounds.width - let gradientWidth = width - - // Calculate the distance to travel (one full cycle) - let distance = width + gradientWidth + let height = layer?.bounds.height ?? bounds.height let duration = darkMode ? 3.0 : 6.0 - - // Get current time to synchronize both animations precisely let now = CACurrentMediaTime() - // Layer 1 starts from off-screen left - let animation1 = CABasicAnimation(keyPath: "position.x") - animation1.fromValue = -gradientWidth / 2 - animation1.toValue = width + gradientWidth / 2 - animation1.duration = duration - animation1.repeatCount = .infinity - animation1.timingFunction = CAMediaTimingFunction(name: .linear) - animation1.beginTime = now - animation1.isRemovedOnCompletion = false - animation1.fillMode = .forwards - - indeterminateLayer1.position.x = -gradientWidth / 2 - indeterminateLayer1.add(animation1, forKey: animationKey) - - // Layer 2 follows layer 1, offset by half the duration to create wrap-around effect - let animation2 = CABasicAnimation(keyPath: "position.x") - animation2.fromValue = -gradientWidth / 2 - animation2.toValue = width + gradientWidth / 2 - animation2.duration = duration - animation2.repeatCount = .infinity - animation2.timingFunction = CAMediaTimingFunction(name: .linear) - animation2.isRemovedOnCompletion = false - animation2.fillMode = .forwards - // Start layer 2's animation offset by the time it takes to travel one gradient width - let timeOffset = (duration * Double(gradientWidth)) / Double(distance) - animation2.beginTime = now - timeOffset - - indeterminateLayer2.position.x = -gradientWidth / 2 - indeterminateLayer2.add(animation2, forKey: animationKey) + if vertical { + let gradientHeight = height + let distance = height + gradientHeight + + let animation1 = CABasicAnimation(keyPath: "position.y") + animation1.fromValue = height + gradientHeight / 2 + animation1.toValue = -gradientHeight / 2 + animation1.duration = duration + animation1.repeatCount = .infinity + animation1.timingFunction = CAMediaTimingFunction(name: .linear) + animation1.beginTime = now + animation1.isRemovedOnCompletion = false + animation1.fillMode = .forwards + + indeterminateLayer1.position = CGPoint(x: width / 2, y: height + gradientHeight / 2) + indeterminateLayer1.add(animation1, forKey: animationKey) + + let animation2 = CABasicAnimation(keyPath: "position.y") + animation2.fromValue = height + gradientHeight / 2 + animation2.toValue = -gradientHeight / 2 + animation2.duration = duration + animation2.repeatCount = .infinity + animation2.timingFunction = CAMediaTimingFunction(name: .linear) + animation2.isRemovedOnCompletion = false + animation2.fillMode = .forwards + let timeOffset = (duration * Double(gradientHeight)) / Double(distance) + animation2.beginTime = now - timeOffset + + indeterminateLayer2.position = CGPoint(x: width / 2, y: height + gradientHeight / 2) + indeterminateLayer2.add(animation2, forKey: animationKey) + } else { + let gradientWidth = width + let distance = width + gradientWidth + + let animation1 = CABasicAnimation(keyPath: "position.x") + animation1.fromValue = -gradientWidth / 2 + animation1.toValue = width + gradientWidth / 2 + animation1.duration = duration + animation1.repeatCount = .infinity + animation1.timingFunction = CAMediaTimingFunction(name: .linear) + animation1.beginTime = now + animation1.isRemovedOnCompletion = false + animation1.fillMode = .forwards + + indeterminateLayer1.position.x = -gradientWidth / 2 + indeterminateLayer1.add(animation1, forKey: animationKey) + + let animation2 = CABasicAnimation(keyPath: "position.x") + animation2.fromValue = -gradientWidth / 2 + animation2.toValue = width + gradientWidth / 2 + animation2.duration = duration + animation2.repeatCount = .infinity + animation2.timingFunction = CAMediaTimingFunction(name: .linear) + animation2.isRemovedOnCompletion = false + animation2.fillMode = .forwards + let timeOffset = (duration * Double(gradientWidth)) / Double(distance) + animation2.beginTime = now - timeOffset + + indeterminateLayer2.position.x = -gradientWidth / 2 + indeterminateLayer2.add(animation2, forKey: animationKey) + } CATransaction.commit() } @@ -475,8 +529,18 @@ private extension iTermProgressBarView { private func setDeterminate(success: Success, percentage: Int32, animated: Bool) { let clamped = max(0, min(100, percentage)) let width = layer?.bounds.width ?? bounds.width - let progressWidth = width * CGFloat(clamped) / 100.0 - let newFrame = CGRect(x: 0, y: 0, width: progressWidth, height: desiredHeight) + let height = vertical ? (layer?.bounds.height ?? bounds.height) : desiredHeight + let newFrame: CGRect + if vertical { + let progressHeight = height * CGFloat(clamped) / 100.0 + newFrame = CGRect(x: 0, + y: height - progressHeight, + width: width, + height: progressHeight) + } else { + let progressWidth = width * CGFloat(clamped) / 100.0 + newFrame = CGRect(x: 0, y: 0, width: progressWidth, height: height) + } if animated { let animation = CABasicAnimation(keyPath: "frame") animation.fromValue = determinateLayer.presentation()?.frame ?? determinateLayer.frame @@ -560,7 +624,7 @@ extension iTermProgressBarView: CALayerDelegate { // CALayerDelegate method to update sublayer frames appropriately. func layoutSublayers(of layer: CALayer) { let width = layer.bounds.width - let height = desiredHeight + let height = vertical ? layer.bounds.height : desiredHeight switch mode { case .error: // Make the error layer fill the entire width and desired height. @@ -572,9 +636,8 @@ extension iTermProgressBarView: CALayerDelegate { case .indeterminate: // Update container frame and restart animation if needed. indeterminateContainer.frame = CGRect(x: 0, y: 0, width: width, height: height) - let gradientWidth = width - indeterminateLayer1.frame = CGRect(x: 0, y: 0, width: gradientWidth, height: height) - indeterminateLayer2.frame = CGRect(x: 0, y: 0, width: gradientWidth, height: height) + indeterminateLayer1.frame = CGRect(x: 0, y: 0, width: width, height: height) + indeterminateLayer2.frame = CGRect(x: 0, y: 0, width: width, height: height) // Only restart animation if it's not already running if indeterminateLayer1.animation(forKey: "indeterminateScroll") == nil { startIndeterminateAnimation() @@ -582,8 +645,16 @@ extension iTermProgressBarView: CALayerDelegate { case let .determinate(success: success, percentage: percentage): // Set determinate layer width according to current percentage. let clamped = max(0, min(100, percentage)) - let progressWidth = width * CGFloat(clamped) / 100.0 - determinateLayer.frame = CGRect(x: 0, y: 0, width: progressWidth, height: height) + if vertical { + let progressHeight = height * CGFloat(clamped) / 100.0 + determinateLayer.frame = CGRect(x: 0, + y: height - progressHeight, + width: width, + height: progressHeight) + } else { + let progressWidth = width * CGFloat(clamped) / 100.0 + determinateLayer.frame = CGRect(x: 0, y: 0, width: progressWidth, height: height) + } updateDeterminateColors(success: success) case .ground: // No visible layer to layout. diff --git a/sources/iTermTabBarControlView.m b/sources/iTermTabBarControlView.m index e2d537e90d..41bc61d1f2 100644 --- a/sources/iTermTabBarControlView.m +++ b/sources/iTermTabBarControlView.m @@ -331,6 +331,7 @@ - (NSView *)customProgressBarViewForTabCell:(PSMTabBarCell *)cell { - (void)configureCustomProgressBarView:(NSView *)view forTabCell:(PSMTabBarCell *)cell { iTermProgressBarView *progressBar = (iTermProgressBarView *)view; progressBar.heightValue = PSMTabBarProgressBarHeight; + progressBar.vertical = (self.orientation == PSMTabBarVerticalOrientation); progressBar.darkMode = self.style.useLightControls; progressBar.colorScheme = [self tabProgressBarColorSchemeForCell:cell]; progressBar.state = (VT100ScreenProgress)cell.progress; From 15079bbf57276a609211d897e21a5363c4ac0792 Mon Sep 17 00:00:00 2001 From: Claas Lange Date: Fri, 10 Apr 2026 11:05:10 +0200 Subject: [PATCH 4/4] feat(tabs): add left-tab progress bar orientation setting - add an advanced setting for left-side tab progress bar orientation - keep vertical progress bars as the default for left-side tabs - update tab progress bar layout to respect the new setting --- .../source/PSMTahoeTabStyle.swift | 4 +++- .../source/PSMYosemiteTabStyle.m | 4 +++- sources/iTermAdvancedSettingsModel.h | 1 + sources/iTermAdvancedSettingsModel.m | 1 + sources/iTermTabBarControlView.m | 24 ++++++++++++++++++- 5 files changed, 31 insertions(+), 3 deletions(-) diff --git a/ThirdParty/PSMTabBarControl/source/PSMTahoeTabStyle.swift b/ThirdParty/PSMTabBarControl/source/PSMTahoeTabStyle.swift index e27f42719e..f470980d6b 100644 --- a/ThirdParty/PSMTabBarControl/source/PSMTahoeTabStyle.swift +++ b/ThirdParty/PSMTabBarControl/source/PSMTahoeTabStyle.swift @@ -423,7 +423,9 @@ class PSMTahoeTabStyle: NSObject, PSMTabStyle { @objc func progressBarRect(forTabCell cell: PSMTabBarCell) -> NSRect { let cellFrame = cell.frame - if _orientation == .verticalOrientation { + let showVerticalProgressBar = (_orientation == .verticalOrientation && + !iTermAdvancedSettingsModel.leftTabBarProgressBarsAreHorizontal()) + if showVerticalProgressBar { return NSRect(x: cellFrame.origin.x, y: cellFrame.origin.y, width: PSMTabBarProgressBarHeight, diff --git a/ThirdParty/PSMTabBarControl/source/PSMYosemiteTabStyle.m b/ThirdParty/PSMTabBarControl/source/PSMYosemiteTabStyle.m index f437a2b272..58a8939883 100644 --- a/ThirdParty/PSMTabBarControl/source/PSMYosemiteTabStyle.m +++ b/ThirdParty/PSMTabBarControl/source/PSMYosemiteTabStyle.m @@ -264,7 +264,9 @@ - (NSRect)indicatorRectForTabCell:(PSMTabBarCell *)cell { - (NSRect)progressBarRectForTabCell:(PSMTabBarCell *)cell { NSRect cellFrame = [cell frame]; - if (_orientation == PSMTabBarVerticalOrientation) { + const BOOL showVerticalProgressBar = (_orientation == PSMTabBarVerticalOrientation && + ![iTermAdvancedSettingsModel leftTabBarProgressBarsAreHorizontal]); + if (showVerticalProgressBar) { return NSMakeRect(cellFrame.origin.x, cellFrame.origin.y, PSMTabBarProgressBarHeight, diff --git a/sources/iTermAdvancedSettingsModel.h b/sources/iTermAdvancedSettingsModel.h index 278062639c..17d8a32111 100644 --- a/sources/iTermAdvancedSettingsModel.h +++ b/sources/iTermAdvancedSettingsModel.h @@ -151,6 +151,7 @@ extern NSString *const iTermAdvancedSettingsDidChange; + (BOOL)disableSmartSelectionActionsOnClick; + (BOOL)disableTabBarTooltips; + (BOOL)disableTopRightIndicators; ++ (BOOL)leftTabBarProgressBarsAreHorizontal; + (BOOL)disableTmuxWindowPositionRestoration; + (BOOL)disableTmuxWindowResizing; + (BOOL)disableWindowShadowWhenTransparencyOnMojave; diff --git a/sources/iTermAdvancedSettingsModel.m b/sources/iTermAdvancedSettingsModel.m index 704cafb4d7..e1977cf810 100644 --- a/sources/iTermAdvancedSettingsModel.m +++ b/sources/iTermAdvancedSettingsModel.m @@ -305,6 +305,7 @@ + (BOOL)settingIsDeprecated:(NSString *)name { DEFINE_BOOL(convertTabDragToWindowDragForSolitaryTabInCompactOrMinimalTheme, YES, SECTION_TABS @"In the Minimal and Compact themes when there is a single tab and the tab bar is visible, should dragging the tab bar move the window?\nThis also affects windows without titlebars in any theme."); DEFINE_BOOL(highVisibility, YES, SECTION_TABS @"High Contrast modes maximize visibility.\nWhen enabled, the dark high-contrast theme emphasizes visibility over beauty."); DEFINE_BOOL(drawBottomLineForHorizontalTabBar, YES, SECTION_TABS @"Draw bottom line for horizontal tabbar in Regular, Dark and Light theme?"); +DEFINE_BOOL(leftTabBarProgressBarsAreHorizontal, NO, SECTION_TABS @"Draw horizontal progress bars in left-side tab bars?\nWhen disabled, left-side tab bars show vertical progress bars along the leading edge of each tab."); DEFINE_BOOL(disableTabBarTooltips, NO, SECTION_TABS @"Disable tab bar tooltips?"); DEFINE_BOOL(useCustomTabBarFontSize, NO, SECTION_TABS @"Use custom font size for tab labels?\nSee also advanced setting “Custom tab label font size”."); DEFINE_FLOAT(customTabBarFontSize, 11.0, SECTION_TABS @"Custom tab label font size\nFor this to take effect, turn on “Use custom font size for tab labels?”."); diff --git a/sources/iTermTabBarControlView.m b/sources/iTermTabBarControlView.m index 41bc61d1f2..38d8718f70 100644 --- a/sources/iTermTabBarControlView.m +++ b/sources/iTermTabBarControlView.m @@ -22,6 +22,10 @@ @interface NSView (Private2) - (NSRect)_opaqueRectForWindowMoveWhenInTitlebar; @end +@interface PSMTabBarControl (Private) +- (void)syncTabProgressBars; +@end + typedef NS_ENUM(NSInteger, iTermTabBarFlashState) { kFlashOff, kFlashHolding, // Regular delay @@ -56,10 +60,19 @@ - (instancetype)initWithFrame:(NSRect)frameRect { } self.showAddTabButton = ![iTermAdvancedSettingsModel removeAddTabButton]; self.selectsTabsOnMouseDown = [iTermAdvancedSettingsModel selectsTabsOnMouseDown]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(advancedSettingsDidChange:) + name:iTermAdvancedSettingsDidChange + object:nil]; } return self; } +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [super dealloc]; +} + - (void)setCmdPressed:(BOOL)cmdPressed { if (cmdPressed == _cmdPressed) { return; @@ -322,6 +335,11 @@ - (BOOL)shouldShowCustomProgressBarForTabCell:(PSMTabBarCell *)cell { return [self cellShouldShowTabProgressBar:cell]; } +- (BOOL)tabProgressBarsShouldBeVertical { + return (self.orientation == PSMTabBarVerticalOrientation && + ![iTermAdvancedSettingsModel leftTabBarProgressBarsAreHorizontal]); +} + - (NSView *)customProgressBarViewForTabCell:(PSMTabBarCell *)cell { iTermProgressBarView *progressBar = [[[iTermProgressBarView alloc] init] autorelease]; progressBar.heightValue = PSMTabBarProgressBarHeight; @@ -331,12 +349,16 @@ - (NSView *)customProgressBarViewForTabCell:(PSMTabBarCell *)cell { - (void)configureCustomProgressBarView:(NSView *)view forTabCell:(PSMTabBarCell *)cell { iTermProgressBarView *progressBar = (iTermProgressBarView *)view; progressBar.heightValue = PSMTabBarProgressBarHeight; - progressBar.vertical = (self.orientation == PSMTabBarVerticalOrientation); + progressBar.vertical = [self tabProgressBarsShouldBeVertical]; progressBar.darkMode = self.style.useLightControls; progressBar.colorScheme = [self tabProgressBarColorSchemeForCell:cell]; progressBar.state = (VT100ScreenProgress)cell.progress; } +- (void)advancedSettingsDidChange:(NSNotification *)notification { + [self syncTabProgressBars]; +} + - (void)setFlashState:(iTermTabBarFlashState)flashState { NSArray *names = @[ @"Off", @"FadeIn", @"Holding", @"Extending", @"FadeOut" ]; DLog(@"%@ -> %@ from\n%@", names[self.flashState], names[flashState], [NSThread callStackSymbols]);