Fix notification focus and indicators
This commit is contained in:
parent
4b01de1ba9
commit
c353131f53
7 changed files with 297 additions and 24 deletions
|
|
@ -52,7 +52,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
|
||||
if let surfaceId,
|
||||
let tab = tabManager.tabs.first(where: { $0.id == tabId }) {
|
||||
tab.triggerNotificationFocusFlash(surfaceId: surfaceId, requiresSplit: false)
|
||||
tab.triggerNotificationFocusFlash(surfaceId: surfaceId, requiresSplit: false, shouldFocus: false)
|
||||
}
|
||||
notificationStore.markRead(forTabId: tabId, surfaceId: surfaceId)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1528,6 +1528,21 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
private var lastSentRow: Int?
|
||||
private var isActive = true
|
||||
private var focusWorkItem: DispatchWorkItem?
|
||||
#if DEBUG
|
||||
private static var flashCounts: [UUID: Int] = [:]
|
||||
|
||||
static func flashCount(for surfaceId: UUID) -> Int {
|
||||
flashCounts[surfaceId, default: 0]
|
||||
}
|
||||
|
||||
static func resetFlashCounts() {
|
||||
flashCounts.removeAll()
|
||||
}
|
||||
|
||||
private static func recordFlash(for surfaceId: UUID) {
|
||||
flashCounts[surfaceId, default: 0] += 1
|
||||
}
|
||||
#endif
|
||||
|
||||
init(surfaceView: GhosttyNSView) {
|
||||
self.surfaceView = surfaceView
|
||||
|
|
@ -1712,6 +1727,11 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
func triggerFlash() {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self else { return }
|
||||
#if DEBUG
|
||||
if let surfaceId = self.surfaceView.terminalSurface?.id {
|
||||
Self.recordFlash(for: surfaceId)
|
||||
}
|
||||
#endif
|
||||
self.updateFlashPath()
|
||||
self.flashLayer.removeAllAnimations()
|
||||
self.flashLayer.opacity = 0
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ struct TerminalSplitTreeView: View {
|
|||
@ObservedObject var tab: Tab
|
||||
let isTabActive: Bool
|
||||
@State private var config = GhosttyConfig.load()
|
||||
@EnvironmentObject var notificationStore: TerminalNotificationStore
|
||||
|
||||
var body: some View {
|
||||
let appearance = SplitAppearance(
|
||||
|
|
@ -20,6 +21,8 @@ struct TerminalSplitTreeView: View {
|
|||
isTabActive: isTabActive,
|
||||
focusedSurfaceId: tab.focusedSurfaceId,
|
||||
appearance: appearance,
|
||||
tabId: tab.id,
|
||||
notificationStore: notificationStore,
|
||||
onFocus: { tab.focusSurface($0) },
|
||||
onTriggerFlash: { tab.triggerDebugFlash(surfaceId: $0) },
|
||||
onResize: { tab.updateSplitRatio(node: $0, ratio: $1) },
|
||||
|
|
@ -44,6 +47,8 @@ fileprivate struct TerminalSplitSubtreeView: View {
|
|||
let isTabActive: Bool
|
||||
let focusedSurfaceId: UUID?
|
||||
let appearance: SplitAppearance
|
||||
let tabId: UUID
|
||||
let notificationStore: TerminalNotificationStore
|
||||
let onFocus: (UUID) -> Void
|
||||
let onTriggerFlash: (UUID) -> Void
|
||||
let onResize: (SplitTree<TerminalSurface>.Node, Double) -> Void
|
||||
|
|
@ -53,7 +58,7 @@ fileprivate struct TerminalSplitSubtreeView: View {
|
|||
switch node {
|
||||
case .leaf(let surface):
|
||||
let isFocused = isTabActive && focusedSurfaceId == surface.id
|
||||
ZStack {
|
||||
ZStack(alignment: .topLeading) {
|
||||
GhosttyTerminalView(
|
||||
terminalSurface: surface,
|
||||
isActive: isFocused,
|
||||
|
|
@ -62,6 +67,15 @@ fileprivate struct TerminalSplitSubtreeView: View {
|
|||
)
|
||||
.background(Color.clear)
|
||||
|
||||
if notificationStore.hasUnreadNotification(forTabId: tabId, surfaceId: surface.id) {
|
||||
Circle()
|
||||
.stroke(Color(nsColor: .systemBlue), lineWidth: 2.5)
|
||||
.frame(width: 14, height: 14)
|
||||
.shadow(color: Color(nsColor: .systemBlue).opacity(0.35), radius: 2)
|
||||
.padding(6)
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
|
||||
if isSplit && !isFocused && appearance.unfocusedOverlayOpacity > 0 {
|
||||
Rectangle()
|
||||
.fill(appearance.unfocusedOverlayColor)
|
||||
|
|
@ -92,6 +106,8 @@ fileprivate struct TerminalSplitSubtreeView: View {
|
|||
isTabActive: isTabActive,
|
||||
focusedSurfaceId: focusedSurfaceId,
|
||||
appearance: appearance,
|
||||
tabId: tabId,
|
||||
notificationStore: notificationStore,
|
||||
onFocus: onFocus,
|
||||
onTriggerFlash: onTriggerFlash,
|
||||
onResize: onResize,
|
||||
|
|
@ -106,6 +122,8 @@ fileprivate struct TerminalSplitSubtreeView: View {
|
|||
isTabActive: isTabActive,
|
||||
focusedSurfaceId: focusedSurfaceId,
|
||||
appearance: appearance,
|
||||
tabId: tabId,
|
||||
notificationStore: notificationStore,
|
||||
onFocus: onFocus,
|
||||
onTriggerFlash: onTriggerFlash,
|
||||
onResize: onResize,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,12 @@ class Tab: Identifiable, ObservableObject {
|
|||
@Published var title: String
|
||||
@Published var currentDirectory: String
|
||||
@Published var splitTree: SplitTree<TerminalSurface>
|
||||
@Published var focusedSurfaceId: UUID?
|
||||
@Published var focusedSurfaceId: UUID? {
|
||||
didSet {
|
||||
guard let focusedSurfaceId else { return }
|
||||
AppDelegate.shared?.tabManager?.rememberFocusedSurface(tabId: id, surfaceId: focusedSurfaceId)
|
||||
}
|
||||
}
|
||||
@Published var surfaceDirectories: [UUID: String] = [:]
|
||||
var splitViewSize: CGSize = .zero
|
||||
|
||||
|
|
@ -33,7 +38,7 @@ class Tab: Identifiable, ObservableObject {
|
|||
return nil
|
||||
}
|
||||
|
||||
func focusSurface(_ id: UUID, shouldFlash: Bool = true) {
|
||||
func focusSurface(_ id: UUID) {
|
||||
let wasFocused = focusedSurfaceId == id
|
||||
focusedSurfaceId = id
|
||||
let isSelectedTab = AppDelegate.shared?.tabManager?.selectedTabId == self.id
|
||||
|
|
@ -44,9 +49,6 @@ class Tab: Identifiable, ObservableObject {
|
|||
guard isSelectedTab && isAppFocused else { return }
|
||||
guard let notificationStore = AppDelegate.shared?.notificationStore else { return }
|
||||
if notificationStore.hasUnreadNotification(forTabId: self.id, surfaceId: id) {
|
||||
if shouldFlash {
|
||||
triggerNotificationFocusFlash(surfaceId: id, requiresSplit: false)
|
||||
}
|
||||
notificationStore.markRead(forTabId: self.id, surfaceId: id)
|
||||
return
|
||||
}
|
||||
|
|
@ -64,17 +66,26 @@ class Tab: Identifiable, ObservableObject {
|
|||
currentDirectory = trimmed
|
||||
}
|
||||
|
||||
func triggerNotificationFocusFlash(surfaceId: UUID, requiresSplit: Bool = false) {
|
||||
triggerPanelFlash(surfaceId: surfaceId, requiresSplit: requiresSplit)
|
||||
func triggerNotificationFocusFlash(
|
||||
surfaceId: UUID,
|
||||
requiresSplit: Bool = false,
|
||||
shouldFocus: Bool = true
|
||||
) {
|
||||
triggerPanelFlash(surfaceId: surfaceId, requiresSplit: requiresSplit, shouldFocus: shouldFocus)
|
||||
}
|
||||
|
||||
func triggerDebugFlash(surfaceId: UUID) {
|
||||
triggerPanelFlash(surfaceId: surfaceId, requiresSplit: false)
|
||||
triggerPanelFlash(surfaceId: surfaceId, requiresSplit: false, shouldFocus: true)
|
||||
}
|
||||
|
||||
private func triggerPanelFlash(surfaceId: UUID, requiresSplit: Bool) {
|
||||
private func triggerPanelFlash(surfaceId: UUID, requiresSplit: Bool, shouldFocus: Bool) {
|
||||
guard let surface = surface(for: surfaceId) else { return }
|
||||
focusSurface(surfaceId)
|
||||
if shouldFocus {
|
||||
if focusedSurfaceId != surfaceId {
|
||||
focusSurface(surfaceId)
|
||||
}
|
||||
surface.hostedView.ensureFocus(for: self.id, surfaceId: surfaceId)
|
||||
}
|
||||
if requiresSplit && !splitTree.isSplit {
|
||||
return
|
||||
}
|
||||
|
|
@ -249,6 +260,10 @@ class TabManager: ObservableObject {
|
|||
didSet {
|
||||
guard selectedTabId != oldValue else { return }
|
||||
let previousTabId = oldValue
|
||||
if let previousTabId,
|
||||
let previousSurfaceId = focusedSurfaceId(for: previousTabId) {
|
||||
lastFocusedSurfaceByTab[previousTabId] = previousSurfaceId
|
||||
}
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.focusSelectedTabSurface(previousTabId: previousTabId)
|
||||
self?.updateWindowTitleForSelectedTab()
|
||||
|
|
@ -260,6 +275,7 @@ class TabManager: ObservableObject {
|
|||
}
|
||||
private var observers: [NSObjectProtocol] = []
|
||||
private var suppressFocusFlash = false
|
||||
private var lastFocusedSurfaceByTab: [UUID: UUID] = [:]
|
||||
|
||||
init() {
|
||||
addTab()
|
||||
|
|
@ -415,6 +431,10 @@ class TabManager: ObservableObject {
|
|||
tabs.first(where: { $0.id == tabId })?.focusedSurfaceId
|
||||
}
|
||||
|
||||
func rememberFocusedSurface(tabId: UUID, surfaceId: UUID) {
|
||||
lastFocusedSurfaceByTab[tabId] = surfaceId
|
||||
}
|
||||
|
||||
func applyWindowBackgroundForSelectedTab() {
|
||||
guard let selectedTabId,
|
||||
let tab = tabs.first(where: { $0.id == selectedTabId }),
|
||||
|
|
@ -424,8 +444,13 @@ class TabManager: ObservableObject {
|
|||
|
||||
private func focusSelectedTabSurface(previousTabId: UUID?) {
|
||||
guard let selectedTabId,
|
||||
let tab = tabs.first(where: { $0.id == selectedTabId }),
|
||||
let surface = tab.focusedSurface else { return }
|
||||
let tab = tabs.first(where: { $0.id == selectedTabId }) else { return }
|
||||
if let restoredSurfaceId = lastFocusedSurfaceByTab[selectedTabId],
|
||||
tab.surface(for: restoredSurfaceId) != nil,
|
||||
tab.focusedSurfaceId != restoredSurfaceId {
|
||||
tab.focusedSurfaceId = restoredSurfaceId
|
||||
}
|
||||
guard let surface = tab.focusedSurface else { return }
|
||||
let previousSurface = previousTabId.flatMap { id in
|
||||
tabs.first(where: { $0.id == id })?.focusedSurface
|
||||
}
|
||||
|
|
@ -436,14 +461,11 @@ class TabManager: ObservableObject {
|
|||
private func markFocusedPanelReadIfActive(tabId: UUID) {
|
||||
let shouldSuppressFlash = suppressFocusFlash
|
||||
suppressFocusFlash = false
|
||||
guard !shouldSuppressFlash else { return }
|
||||
guard AppFocusState.isAppFocused() else { return }
|
||||
guard let surfaceId = focusedSurfaceId(for: tabId) else { return }
|
||||
guard let notificationStore = AppDelegate.shared?.notificationStore else { return }
|
||||
guard notificationStore.hasUnreadNotification(forTabId: tabId, surfaceId: surfaceId) else { return }
|
||||
if !shouldSuppressFlash,
|
||||
let tab = tabs.first(where: { $0.id == tabId }) {
|
||||
tab.triggerNotificationFocusFlash(surfaceId: surfaceId, requiresSplit: false)
|
||||
}
|
||||
notificationStore.markRead(forTabId: tabId, surfaceId: surfaceId)
|
||||
}
|
||||
|
||||
|
|
@ -501,14 +523,18 @@ class TabManager: ObservableObject {
|
|||
}
|
||||
|
||||
if let surfaceId {
|
||||
focusSurface(tabId: tabId, surfaceId: surfaceId, shouldFlash: !suppressFlash)
|
||||
if !suppressFlash {
|
||||
focusSurface(tabId: tabId, surfaceId: surfaceId)
|
||||
} else if let tab = tabs.first(where: { $0.id == tabId }) {
|
||||
tab.focusedSurfaceId = surfaceId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func focusTabFromNotification(_ tabId: UUID, surfaceId: UUID? = nil) {
|
||||
let wasSelected = selectedTabId == tabId
|
||||
suppressFocusFlash = true
|
||||
focusTab(tabId, surfaceId: surfaceId)
|
||||
focusTab(tabId)
|
||||
if wasSelected {
|
||||
suppressFocusFlash = false
|
||||
}
|
||||
|
|
@ -521,13 +547,14 @@ class TabManager: ObservableObject {
|
|||
tab.surface(for: targetSurfaceId) != nil else { return }
|
||||
guard let notificationStore = AppDelegate.shared?.notificationStore else { return }
|
||||
guard notificationStore.hasUnreadNotification(forTabId: tabId, surfaceId: targetSurfaceId) else { return }
|
||||
tab.triggerNotificationFocusFlash(surfaceId: targetSurfaceId, requiresSplit: false)
|
||||
tab.triggerNotificationFocusFlash(surfaceId: targetSurfaceId, requiresSplit: false, shouldFocus: true)
|
||||
notificationStore.markRead(forTabId: tabId, surfaceId: targetSurfaceId)
|
||||
}
|
||||
}
|
||||
|
||||
func focusSurface(tabId: UUID, surfaceId: UUID, shouldFlash: Bool = true) {
|
||||
func focusSurface(tabId: UUID, surfaceId: UUID) {
|
||||
guard let tab = tabs.first(where: { $0.id == tabId }) else { return }
|
||||
tab.focusSurface(surfaceId, shouldFlash: shouldFlash)
|
||||
tab.focusSurface(surfaceId)
|
||||
}
|
||||
|
||||
func selectNextTab() {
|
||||
|
|
|
|||
|
|
@ -194,6 +194,17 @@ class TerminalController {
|
|||
case "simulate_app_active":
|
||||
return simulateAppDidBecomeActive()
|
||||
|
||||
#if DEBUG
|
||||
case "focus_notification":
|
||||
return focusFromNotification(args)
|
||||
|
||||
case "flash_count":
|
||||
return flashCount(args)
|
||||
|
||||
case "reset_flash_counts":
|
||||
return resetFlashCounts()
|
||||
#endif
|
||||
|
||||
case "help":
|
||||
return helpText()
|
||||
|
||||
|
|
@ -203,7 +214,7 @@ class TerminalController {
|
|||
}
|
||||
|
||||
private func helpText() -> String {
|
||||
return """
|
||||
var text = """
|
||||
Available commands:
|
||||
ping - Check if server is running
|
||||
list_tabs - List all tabs with IDs
|
||||
|
|
@ -226,6 +237,15 @@ class TerminalController {
|
|||
simulate_app_active - Trigger app active handler
|
||||
help - Show this help
|
||||
"""
|
||||
#if DEBUG
|
||||
text += """
|
||||
|
||||
focus_notification <tab|idx> [surface|idx] - Focus via notification flow
|
||||
flash_count <id|idx> - Read flash count for a surface
|
||||
reset_flash_counts - Reset flash counters
|
||||
"""
|
||||
#endif
|
||||
return text
|
||||
}
|
||||
|
||||
private func listTabs() -> String {
|
||||
|
|
@ -420,6 +440,60 @@ class TerminalController {
|
|||
return "OK"
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
private func focusFromNotification(_ args: String) -> String {
|
||||
guard let tabManager else { return "ERROR: TabManager not available" }
|
||||
let trimmed = args.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let parts = trimmed.split(separator: " ", maxSplits: 1).map(String.init)
|
||||
let tabArg = parts.first ?? ""
|
||||
let surfaceArg = parts.count > 1 ? parts[1] : ""
|
||||
|
||||
var result = "OK"
|
||||
DispatchQueue.main.sync {
|
||||
guard let tab = resolveTab(from: tabArg, tabManager: tabManager) else {
|
||||
result = "ERROR: Tab not found"
|
||||
return
|
||||
}
|
||||
let surfaceId = surfaceArg.isEmpty ? nil : resolveSurfaceId(from: surfaceArg, tab: tab)
|
||||
if !surfaceArg.isEmpty && surfaceId == nil {
|
||||
result = "ERROR: Surface not found"
|
||||
return
|
||||
}
|
||||
tabManager.focusTabFromNotification(tab.id, surfaceId: surfaceId)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private func flashCount(_ args: String) -> String {
|
||||
guard let tabManager else { return "ERROR: TabManager not available" }
|
||||
let trimmed = args.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return "ERROR: Missing surface id or index" }
|
||||
|
||||
var result = "ERROR: Surface not found"
|
||||
DispatchQueue.main.sync {
|
||||
guard let tabId = tabManager.selectedTabId,
|
||||
let tab = tabManager.tabs.first(where: { $0.id == tabId }) else {
|
||||
result = "ERROR: No tab selected"
|
||||
return
|
||||
}
|
||||
guard let surfaceId = resolveSurfaceId(from: trimmed, tab: tab) else {
|
||||
result = "ERROR: Surface not found"
|
||||
return
|
||||
}
|
||||
let count = GhosttySurfaceScrollView.flashCount(for: surfaceId)
|
||||
result = "OK \(count)"
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private func resetFlashCounts() -> String {
|
||||
DispatchQueue.main.sync {
|
||||
GhosttySurfaceScrollView.resetFlashCounts()
|
||||
}
|
||||
return "OK"
|
||||
}
|
||||
#endif
|
||||
|
||||
private func parseSplitDirection(_ value: String) -> SplitTree<TerminalSurface>.NewDirection? {
|
||||
switch value.lowercased() {
|
||||
case "left", "l":
|
||||
|
|
|
|||
|
|
@ -303,6 +303,29 @@ class cmux:
|
|||
if not response.startswith("OK"):
|
||||
raise cmuxError(response)
|
||||
|
||||
def focus_notification(self, tab: str | int, surface: str | int | None = None) -> None:
|
||||
"""Focus tab/surface using the notification flow."""
|
||||
if surface is None:
|
||||
command = f"focus_notification {tab}"
|
||||
else:
|
||||
command = f"focus_notification {tab} {surface}"
|
||||
response = self._send_command(command)
|
||||
if not response.startswith("OK"):
|
||||
raise cmuxError(response)
|
||||
|
||||
def flash_count(self, surface: str | int) -> int:
|
||||
"""Get flash count for a surface by ID or index."""
|
||||
response = self._send_command(f"flash_count {surface}")
|
||||
if response.startswith("OK "):
|
||||
return int(response.split(" ", 1)[1])
|
||||
raise cmuxError(response)
|
||||
|
||||
def reset_flash_counts(self) -> None:
|
||||
"""Reset flash counters."""
|
||||
response = self._send_command("reset_flash_counts")
|
||||
if not response.startswith("OK"):
|
||||
raise cmuxError(response)
|
||||
|
||||
|
||||
def main():
|
||||
"""CLI interface for cmux"""
|
||||
|
|
|
|||
|
|
@ -195,6 +195,114 @@ def test_mark_read_on_tab_switch(client: cmux) -> TestResult:
|
|||
return result
|
||||
|
||||
|
||||
def test_no_flash_on_tab_switch(client: cmux) -> TestResult:
|
||||
result = TestResult("No Flash On Tab Switch")
|
||||
try:
|
||||
client.clear_notifications()
|
||||
client.reset_flash_counts()
|
||||
|
||||
tab1 = client.current_tab()
|
||||
surfaces = client.list_surfaces()
|
||||
focused = next((s for s in surfaces if s[2]), None)
|
||||
if focused is None:
|
||||
result.failure("Unable to identify focused surface")
|
||||
return result
|
||||
|
||||
client.set_app_focus(False)
|
||||
client.notify("tabswitchflash")
|
||||
time.sleep(0.1)
|
||||
|
||||
client.new_tab()
|
||||
time.sleep(0.1)
|
||||
|
||||
client.set_app_focus(True)
|
||||
client.select_tab(tab1)
|
||||
time.sleep(0.2)
|
||||
|
||||
count = client.flash_count(focused[1])
|
||||
if count != 0:
|
||||
result.failure(f"Expected flash count 0, got {count}")
|
||||
else:
|
||||
result.success("No flash triggered on tab switch")
|
||||
except Exception as e:
|
||||
result.failure(f"Exception: {e}")
|
||||
return result
|
||||
|
||||
|
||||
def test_focus_on_notification_click(client: cmux) -> TestResult:
|
||||
result = TestResult("Focus On Notification Click")
|
||||
try:
|
||||
client.clear_notifications()
|
||||
client.reset_flash_counts()
|
||||
|
||||
surfaces = ensure_two_surfaces(client)
|
||||
focused = next((s for s in surfaces if s[2]), None)
|
||||
other = next((s for s in surfaces if not s[2]), None)
|
||||
if focused is None or other is None:
|
||||
result.failure("Unable to identify focused and unfocused surfaces")
|
||||
return result
|
||||
|
||||
client.set_app_focus(False)
|
||||
client.notify_surface(other[0], "notifyfocus")
|
||||
time.sleep(0.1)
|
||||
|
||||
client.set_app_focus(True)
|
||||
tab_id = client.current_tab()
|
||||
client.focus_notification(tab_id, other[0])
|
||||
time.sleep(0.2)
|
||||
|
||||
surfaces = client.list_surfaces()
|
||||
target = next((s for s in surfaces if s[1] == other[1]), None)
|
||||
if target is None or not target[2]:
|
||||
result.failure("Expected notification surface to be focused")
|
||||
return result
|
||||
|
||||
count = client.flash_count(other[1])
|
||||
if count < 1:
|
||||
result.failure(f"Expected flash count >= 1, got {count}")
|
||||
else:
|
||||
result.success("Notification click focuses and flashes panel")
|
||||
except Exception as e:
|
||||
result.failure(f"Exception: {e}")
|
||||
return result
|
||||
|
||||
|
||||
def test_restore_focus_on_tab_switch(client: cmux) -> TestResult:
|
||||
result = TestResult("Restore Focus On Tab Switch")
|
||||
try:
|
||||
client.clear_notifications()
|
||||
client.set_app_focus(True)
|
||||
|
||||
surfaces = ensure_two_surfaces(client)
|
||||
focused = next((s for s in surfaces if s[2]), None)
|
||||
other = next((s for s in surfaces if not s[2]), None)
|
||||
if focused is None or other is None:
|
||||
result.failure("Unable to identify focused and unfocused surfaces")
|
||||
return result
|
||||
|
||||
client.focus_surface(other[0])
|
||||
time.sleep(0.1)
|
||||
|
||||
tab1 = client.current_tab()
|
||||
client.new_tab()
|
||||
time.sleep(0.1)
|
||||
|
||||
client.select_tab(tab1)
|
||||
time.sleep(0.2)
|
||||
|
||||
surfaces = client.list_surfaces()
|
||||
target = next((s for s in surfaces if s[1] == other[1]), None)
|
||||
if target is None:
|
||||
result.failure("Unable to find previously focused surface")
|
||||
elif not target[2]:
|
||||
result.failure("Expected previously focused surface to be focused after tab switch")
|
||||
else:
|
||||
result.success("Restored last focused surface after tab switch")
|
||||
except Exception as e:
|
||||
result.failure(f"Exception: {e}")
|
||||
return result
|
||||
|
||||
|
||||
def run_tests() -> int:
|
||||
results = []
|
||||
with cmux() as client:
|
||||
|
|
@ -204,6 +312,9 @@ def run_tests() -> int:
|
|||
results.append(test_mark_read_on_focus_change(client))
|
||||
results.append(test_mark_read_on_app_active(client))
|
||||
results.append(test_mark_read_on_tab_switch(client))
|
||||
results.append(test_no_flash_on_tab_switch(client))
|
||||
results.append(test_focus_on_notification_click(client))
|
||||
results.append(test_restore_focus_on_tab_switch(client))
|
||||
client.set_app_focus(None)
|
||||
client.clear_notifications()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue