//go:build darwin package main /* #cgo CFLAGS: -x objective-c -fobjc-arc -fblocks #cgo LDFLAGS: -framework AppKit -framework QuartzCore #include #include #import #import static NSPanel *operationPanel = nil; static NSTextField *operationTitleLabel = nil; static NSTextField *operationDetailLabel = nil; static NSTextField *operationIconLabel = nil; static NSProgressIndicator *operationSpinner = nil; static NSString *statusString(const char *value) { if (value == NULL) { return @""; } NSString *text = [NSString stringWithUTF8String:value]; return text ?: @""; } static void ensureOperationPanel(void) { if (operationPanel != nil) { return; } NSRect frame = NSMakeRect(0, 0, 360, 118); operationPanel = [[NSPanel alloc] initWithContentRect:frame styleMask:NSWindowStyleMaskBorderless backing:NSBackingStoreBuffered defer:NO]; [operationPanel setOpaque:NO]; [operationPanel setBackgroundColor:[NSColor clearColor]]; [operationPanel setLevel:NSFloatingWindowLevel]; [operationPanel setHidesOnDeactivate:NO]; [operationPanel setCollectionBehavior: NSWindowCollectionBehaviorCanJoinAllSpaces | NSWindowCollectionBehaviorFullScreenAuxiliary | NSWindowCollectionBehaviorStationary]; NSView *root = [[NSView alloc] initWithFrame:frame]; [root setWantsLayer:YES]; [[root layer] setCornerRadius:14]; [[root layer] setBackgroundColor:[[NSColor colorWithCalibratedWhite:0.08 alpha:0.92] CGColor]]; [[root layer] setShadowColor:[[NSColor blackColor] CGColor]]; [[root layer] setShadowOpacity:0.28]; [[root layer] setShadowRadius:18]; [[root layer] setShadowOffset:CGSizeMake(0, -8)]; operationSpinner = [[NSProgressIndicator alloc] initWithFrame:NSMakeRect(24, 42, 34, 34)]; [operationSpinner setStyle:NSProgressIndicatorStyleSpinning]; [operationSpinner setIndeterminate:YES]; operationIconLabel = [NSTextField labelWithString:@"OK"]; [operationIconLabel setFrame:NSMakeRect(21, 43, 42, 30)]; [operationIconLabel setAlignment:NSTextAlignmentCenter]; [operationIconLabel setFont:[NSFont systemFontOfSize:17 weight:NSFontWeightSemibold]]; [operationIconLabel setTextColor:[NSColor colorWithCalibratedRed:74.0/255.0 green:222.0/255.0 blue:128.0/255.0 alpha:1.0]]; [operationIconLabel setHidden:YES]; operationTitleLabel = [NSTextField labelWithString:@""]; [operationTitleLabel setFrame:NSMakeRect(76, 61, 260, 24)]; [operationTitleLabel setFont:[NSFont systemFontOfSize:15 weight:NSFontWeightSemibold]]; [operationTitleLabel setTextColor:[NSColor whiteColor]]; operationDetailLabel = [NSTextField labelWithString:@""]; [operationDetailLabel setFrame:NSMakeRect(76, 34, 260, 22)]; [operationDetailLabel setFont:[NSFont systemFontOfSize:12 weight:NSFontWeightRegular]]; [operationDetailLabel setTextColor:[NSColor colorWithCalibratedWhite:0.82 alpha:1.0]]; [operationDetailLabel setLineBreakMode:NSLineBreakByTruncatingTail]; [root addSubview:operationSpinner]; [root addSubview:operationIconLabel]; [root addSubview:operationTitleLabel]; [root addSubview:operationDetailLabel]; [operationPanel setContentView:root]; } static void snipShowOperationStatus(const char *titleC, const char *detailC, int state) { NSString *title = statusString(titleC); NSString *detail = statusString(detailC); dispatch_async(dispatch_get_main_queue(), ^{ ensureOperationPanel(); [operationTitleLabel setStringValue:title]; [operationDetailLabel setStringValue:detail]; if (state == 0) { [operationIconLabel setHidden:YES]; [operationSpinner setHidden:NO]; [operationSpinner startAnimation:nil]; } else { [operationSpinner stopAnimation:nil]; [operationSpinner setHidden:YES]; [operationIconLabel setHidden:NO]; if (state == 2) { [operationIconLabel setStringValue:@"!"]; [operationIconLabel setTextColor:[NSColor colorWithCalibratedRed:248.0/255.0 green:113.0/255.0 blue:113.0/255.0 alpha:1.0]]; } else { [operationIconLabel setStringValue:@"OK"]; [operationIconLabel setTextColor:[NSColor colorWithCalibratedRed:74.0/255.0 green:222.0/255.0 blue:128.0/255.0 alpha:1.0]]; } } NSScreen *screen = [NSScreen mainScreen]; if (screen != nil) { NSRect visible = [screen visibleFrame]; NSRect panelFrame = [operationPanel frame]; panelFrame.origin.x = NSMidX(visible) - panelFrame.size.width / 2; panelFrame.origin.y = NSMaxY(visible) - panelFrame.size.height - 48; [operationPanel setFrame:panelFrame display:YES]; } [operationPanel orderFrontRegardless]; }); } static void snipHideOperationStatus(void) { dispatch_async(dispatch_get_main_queue(), ^{ if (operationPanel != nil) { [operationSpinner stopAnimation:nil]; [operationPanel orderOut:nil]; } }); } */ import "C" import ( "sync/atomic" "time" "unsafe" ) const ( operationStatusRunning = iota operationStatusSuccess operationStatusError ) var operationStatusSeq atomic.Uint64 func showOperationStatus(title, detail string, state int) { operationStatusSeq.Add(1) ctitle := C.CString(title) cdetail := C.CString(detail) defer C.free(unsafe.Pointer(ctitle)) defer C.free(unsafe.Pointer(cdetail)) C.snipShowOperationStatus(ctitle, cdetail, C.int(state)) } func hideOperationStatusAfter(delay time.Duration) { seq := operationStatusSeq.Load() time.AfterFunc(delay, func() { if operationStatusSeq.Load() == seq { C.snipHideOperationStatus() } }) }