Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Barik/Barik.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@
<true/>
<key>com.apple.security.personal-information.location</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>
66 changes: 66 additions & 0 deletions Barik/Helpers/SystemUIHelper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import AppKit
import Foundation

/// Helper for triggering macOS system UI elements
final class SystemUIHelper {

/// Opens the macOS Notification Center by simulating Ctrl+Option+N keypress
static func openNotificationCenter() {
// Simulate Ctrl+Option+N keyboard shortcut
let keyCode: CGKeyCode = 45 // 'n' key
let flags: CGEventFlags = [.maskControl, .maskAlternate]

// Create and post key down event
if let keyDown = CGEvent(keyboardEventSource: nil, virtualKey: keyCode, keyDown: true) {
keyDown.flags = flags
keyDown.post(tap: .cghidEventTap)
}

// Create and post key up event
if let keyUp = CGEvent(keyboardEventSource: nil, virtualKey: keyCode, keyDown: false) {
keyUp.flags = flags
keyUp.post(tap: .cghidEventTap)
}
}

/// Opens the macOS Weather menu bar dropdown
static func openWeatherDropdown() {
let script = """
tell application "System Events"
tell process "ControlCenter"
try
click menu bar item "Weather" of menu bar 1
on error
-- Weather might not be in menu bar, try to open Weather app instead
tell application "Weather" to activate
end try
end tell
end tell
"""
runAppleScript(script)
}

/// Opens the Weather app
static func openWeatherApp() {
NSWorkspace.shared.open(URL(string: "x-apple.systempreferences:com.apple.Weather")!)
// Fallback to opening Weather app directly
if let weatherURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: "com.apple.weather") {
NSWorkspace.shared.open(weatherURL)
}
}

/// Runs an AppleScript
@discardableResult
private static func runAppleScript(_ script: String) -> String? {
guard let appleScript = NSAppleScript(source: script) else {
return nil
}
var error: NSDictionary?
let result = appleScript.executeAndReturnError(&error)
if let error = error {
print("AppleScript Error: \(error)")
return nil
}
return result.stringValue
}
}
7 changes: 6 additions & 1 deletion Barik/Info.plist
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
<dict>
<key>NSLocationUsageDescription</key>
<string>Barik needs your location to show local weather conditions.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Barik needs your location to show local weather conditions.</string>
</dict>
</plist>
45 changes: 18 additions & 27 deletions Barik/MenuBarPopup/MenuBarPopup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import SwiftUI
private var panel: NSPanel?

class HidingPanel: NSPanel, NSWindowDelegate {
var hideTimer: Timer?
var hideWorkItem: DispatchWorkItem?

override var canBecomeKey: Bool {
return true
Expand All @@ -23,13 +23,12 @@ class HidingPanel: NSPanel, NSWindowDelegate {

func windowDidResignKey(_ notification: Notification) {
NotificationCenter.default.post(name: .willHideWindow, object: nil)
hideTimer = Timer.scheduledTimer(
withTimeInterval: TimeInterval(
Constants.menuBarPopupAnimationDurationInMilliseconds) / 1000.0,
repeats: false
) { [weak self] _ in
let workItem = DispatchWorkItem { [weak self] in
self?.orderOut(nil)
}
hideWorkItem = workItem
let duration = Double(Constants.menuBarPopupAnimationDurationInMilliseconds) / 1000.0
DispatchQueue.main.asyncAfter(deadline: .now() + duration, execute: workItem)
}
}

Expand Down Expand Up @@ -59,8 +58,8 @@ class MenuBarPopup {
lastContentIdentifier = id

if let hidingPanel = panel as? HidingPanel {
hidingPanel.hideTimer?.invalidate()
hidingPanel.hideTimer = nil
hidingPanel.hideWorkItem?.cancel()
hidingPanel.hideWorkItem = nil
}

if panel.isKeyWindow {
Expand All @@ -73,13 +72,10 @@ class MenuBarPopup {
DispatchQueue.main.asyncAfter(deadline: .now() + duration) {
panel.contentView = NSHostingView(
rootView:
ZStack {
MenuBarPopupView {
content()
}
.position(x: rect.midX)
MenuBarPopupView(widgetRect: rect) {
content()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.id(UUID())
)
panel.makeKeyAndOrderFront(nil)
Expand All @@ -91,13 +87,10 @@ class MenuBarPopup {
} else {
panel.contentView = NSHostingView(
rootView:
ZStack {
MenuBarPopupView {
content()
}
.position(x: rect.midX)
MenuBarPopupView(widgetRect: rect) {
content()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
)
panel.makeKeyAndOrderFront(nil)
DispatchQueue.main.async {
Expand All @@ -108,13 +101,11 @@ class MenuBarPopup {
}

static func setup() {
guard let screen = NSScreen.main?.visibleFrame else { return }
let panelFrame = NSRect(
x: 0,
y: 0,
width: screen.size.width,
height: screen.size.height
)
guard let screen = NSScreen.main else { return }

// Use full screen frame so the panel covers the entire screen including menu bar area
// This ensures consistent positioning regardless of dock position or menu bar configuration
let panelFrame = screen.frame

let newPanel = HidingPanel(
contentRect: panelFrame,
Expand Down
41 changes: 27 additions & 14 deletions Barik/MenuBarPopup/MenuBarPopupView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import SwiftUI
struct MenuBarPopupView<Content: View>: View {
let content: Content
let isPreview: Bool
let widgetRect: CGRect

@ObservedObject var configManager = ConfigManager.shared
var foregroundHeight: CGFloat { configManager.config.experimental.foreground.resolveHeight() }
Expand All @@ -21,24 +22,30 @@ struct MenuBarPopupView<Content: View>: View {
private let willChangeContent = NotificationCenter.default.publisher(
for: .willChangeContent)

init(isPreview: Bool = false, @ViewBuilder content: () -> Content) {
init(widgetRect: CGRect = .zero, isPreview: Bool = false, @ViewBuilder content: () -> Content) {
self.widgetRect = widgetRect
self.content = content()
self.isPreview = isPreview
if isPreview {
_animationValue = State(initialValue: 1.0)
}
}

// Position popup directly below the Barik menu bar
// foregroundHeight is the exact height of the Barik bar, which overlays the system menu bar
var popupTopPosition: CGFloat {
return foregroundHeight
}

var body: some View {
ZStack(alignment: .topTrailing) {
content
.background(Color.black)
.cornerRadius(((1.0 - animationValue) * 1) + 40)
.padding(.top, foregroundHeight + 5)
.offset(x: computedOffset, y: computedYOffset)
.shadow(radius: 30)
.blur(radius: (1.0 - (0.1 + 0.9 * animationValue)) * 20)
.scaleEffect(x: 0.2 + 0.8 * animationValue, y: animationValue)
.scaleEffect(x: 0.2 + 0.8 * animationValue, y: animationValue, anchor: .top)
.offset(x: computedOffset, y: popupTopPosition)
.opacity(animationValue)
.transaction { transaction in
if isHideAnimation {
Expand Down Expand Up @@ -135,23 +142,29 @@ struct MenuBarPopupView<Content: View>: View {
.preferredColorScheme(.dark)
}

// Calculate X offset to center popup under widget, with edge constraints
var computedOffset: CGFloat {
let screenWidth = NSScreen.main?.frame.width ?? 0
let W = viewFrame.width
let M = viewFrame.midX
let newLeft = (M - W / 2) - 20
let newRight = (M + W / 2) + 20
let contentWidth = viewFrame.width > 0 ? viewFrame.width : 200 // Fallback width

// Start by centering under the widget
var xOffset = widgetRect.midX - contentWidth / 2

// Constrain to screen edges with 20pt margin
let rightEdge = xOffset + contentWidth + 20
let leftEdge = xOffset - 20

if newRight > screenWidth {
return screenWidth - newRight
} else if newLeft < 0 {
return -newLeft
if rightEdge > screenWidth {
xOffset -= (rightEdge - screenWidth)
} else if leftEdge < 0 {
xOffset -= leftEdge
}
return 0

return xOffset
}

var computedYOffset: CGFloat {
return viewFrame.height / 2
return 0
}
}

Expand Down
4 changes: 4 additions & 0 deletions Barik/Views/MenuBarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ struct MenuBarView: View {
NowPlayingWidget()
.environmentObject(config)

case "default.weather":
WeatherWidget()
.environmentObject(config)

case "spacer":
Spacer().frame(minWidth: 50, maxWidth: .infinity)

Expand Down
30 changes: 23 additions & 7 deletions Barik/Widgets/Battery/BatteryManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class BatteryManager: ObservableObject {
@Published var batteryLevel: Int = 0
@Published var isCharging: Bool = false
@Published var isPluggedIn: Bool = false
private var timer: Timer?
private var runLoopSource: CFRunLoopSource?

init() {
startMonitoring()
Expand All @@ -18,17 +18,33 @@ class BatteryManager: ObservableObject {
}

private func startMonitoring() {
// Update every 1 second.
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) {
[weak self] _ in
self?.updateBatteryStatus()
let context = UnsafeMutableRawPointer(
Unmanaged.passUnretained(self).toOpaque())

runLoopSource = IOPSNotificationCreateRunLoopSource(
{ context in
guard let context = context else { return }
let manager = Unmanaged<BatteryManager>.fromOpaque(context)
.takeUnretainedValue()
DispatchQueue.main.async {
manager.updateBatteryStatus()
}
},
context
)?.takeRetainedValue()

if let source = runLoopSource {
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, .defaultMode)
}

updateBatteryStatus()
}

private func stopMonitoring() {
timer?.invalidate()
timer = nil
if let source = runLoopSource {
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, .defaultMode)
runLoopSource = nil
}
}

/// This method updates the battery level and charging state.
Expand Down
Loading