Update sidebar badges and key modifier translation

This commit is contained in:
Lawrence Chen 2026-01-22 19:30:30 -08:00
parent 5acb4e47b1
commit b715f0cebe
3 changed files with 72 additions and 17 deletions

View file

@ -137,6 +137,7 @@ struct VerticalTabsSidebar: View {
struct TabItemView: View {
@EnvironmentObject var tabManager: TabManager
@EnvironmentObject var notificationStore: TerminalNotificationStore
@ObservedObject var tab: Tab
@Binding var selection: SidebarSelection
@State private var isHovering = false
@ -147,9 +148,17 @@ struct TabItemView: View {
var body: some View {
HStack(spacing: 8) {
Image(systemName: "terminal")
.font(.system(size: 12))
.foregroundColor(isSelected ? .white : .secondary)
let unreadCount = notificationStore.unreadCount(forTabId: tab.id)
if unreadCount > 0 {
ZStack {
Circle()
.fill(isSelected ? Color.white.opacity(0.25) : Color.accentColor)
Text("\(unreadCount)")
.font(.system(size: 9, weight: .semibold))
.foregroundColor(.white)
}
.frame(width: 16, height: 16)
}
Text(tab.title)
.font(.system(size: 12))
@ -159,15 +168,15 @@ struct TabItemView: View {
Spacer()
if isHovering || isSelected {
Button(action: { tabManager.closeTab(tab) }) {
Image(systemName: "xmark")
.font(.system(size: 9, weight: .medium))
.foregroundColor(isSelected ? .white.opacity(0.7) : .secondary)
}
.buttonStyle(.plain)
.opacity(tabManager.tabs.count > 1 ? 1 : 0)
Button(action: { tabManager.closeTab(tab) }) {
Image(systemName: "xmark")
.font(.system(size: 9, weight: .medium))
.foregroundColor(isSelected ? .white.opacity(0.7) : .secondary)
}
.buttonStyle(.plain)
.frame(width: 16, height: 16)
.opacity((isHovering || isSelected) && tabManager.tabs.count > 1 ? 1 : 0)
.allowsHitTesting((isHovering || isSelected) && tabManager.tabs.count > 1)
}
.padding(.horizontal, 10)
.padding(.vertical, 8)

View file

@ -704,12 +704,54 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
let action = event.isARepeat ? GHOSTTY_ACTION_REPEAT : GHOSTTY_ACTION_PRESS
// Translate mods to respect Ghostty config (e.g., macos-option-as-alt)
let translationModsGhostty = ghostty_surface_key_translation_mods(surface, modsFromEvent(event))
var translationMods = event.modifierFlags
for flag in [NSEvent.ModifierFlags.shift, .control, .option, .command] {
let hasFlag: Bool
switch flag {
case .shift:
hasFlag = (translationModsGhostty.rawValue & GHOSTTY_MODS_SHIFT.rawValue) != 0
case .control:
hasFlag = (translationModsGhostty.rawValue & GHOSTTY_MODS_CTRL.rawValue) != 0
case .option:
hasFlag = (translationModsGhostty.rawValue & GHOSTTY_MODS_ALT.rawValue) != 0
case .command:
hasFlag = (translationModsGhostty.rawValue & GHOSTTY_MODS_SUPER.rawValue) != 0
default:
hasFlag = translationMods.contains(flag)
}
if hasFlag {
translationMods.insert(flag)
} else {
translationMods.remove(flag)
}
}
let translationEvent: NSEvent
if translationMods == event.modifierFlags {
translationEvent = event
} else {
translationEvent = NSEvent.keyEvent(
with: event.type,
location: event.locationInWindow,
modifierFlags: translationMods,
timestamp: event.timestamp,
windowNumber: event.windowNumber,
context: nil,
characters: event.characters(byApplyingModifiers: translationMods) ?? "",
charactersIgnoringModifiers: event.charactersIgnoringModifiers ?? "",
isARepeat: event.isARepeat,
keyCode: event.keyCode
) ?? event
}
// Set up text accumulator for interpretKeyEvents
keyTextAccumulator = []
defer { keyTextAccumulator = nil }
// Let the input system handle the event (for IME, dead keys, etc.)
interpretKeyEvents([event])
interpretKeyEvents([translationEvent])
// Build the key event
var keyEvent = ghostty_input_key_s()
@ -717,7 +759,7 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
keyEvent.keycode = UInt32(event.keyCode)
keyEvent.mods = modsFromEvent(event)
// Control and Command never contribute to text translation
keyEvent.consumed_mods = consumedModsFromEvent(event)
keyEvent.consumed_mods = consumedModsFromFlags(translationMods)
keyEvent.composing = markedText.length > 0
keyEvent.unshifted_codepoint = unshiftedCodepointFromEvent(event)
@ -733,7 +775,7 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
// Get the appropriate text for this key event
// For control characters, this returns the unmodified character
// so Ghostty's KeyEncoder can handle ctrl encoding
if let text = textForKeyEvent(event) {
if let text = textForKeyEvent(translationEvent) {
text.withCString { ptr in
keyEvent.text = ptr
_ = ghostty_surface_key(surface, keyEvent)
@ -789,12 +831,12 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
/// Consumed mods are modifiers that were used for text translation.
/// Control and Command never contribute to text translation, so they
/// should be excluded from consumed_mods.
private func consumedModsFromEvent(_ event: NSEvent) -> ghostty_input_mods_e {
private func consumedModsFromFlags(_ flags: NSEvent.ModifierFlags) -> ghostty_input_mods_e {
var mods = GHOSTTY_MODS_NONE.rawValue
// Only include Shift and Option as potentially consumed
// Control and Command are never consumed for text translation
if event.modifierFlags.contains(.shift) { mods |= GHOSTTY_MODS_SHIFT.rawValue }
if event.modifierFlags.contains(.option) { mods |= GHOSTTY_MODS_ALT.rawValue }
if flags.contains(.shift) { mods |= GHOSTTY_MODS_SHIFT.rawValue }
if flags.contains(.option) { mods |= GHOSTTY_MODS_ALT.rawValue }
return ghostty_input_mods_e(rawValue: mods)
}

View file

@ -29,6 +29,10 @@ final class TerminalNotificationStore: ObservableObject {
notifications.filter { !$0.isRead }.count
}
func unreadCount(forTabId tabId: UUID) -> Int {
notifications.filter { $0.tabId == tabId && !$0.isRead }.count
}
func addNotification(tabId: UUID, surfaceId: UUID?, title: String, body: String) {
let isActiveTab = AppDelegate.shared?.tabManager?.selectedTabId == tabId
let focusedSurfaceId = AppDelegate.shared?.tabManager?.focusedSurfaceId(for: tabId)