Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 0 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,6 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Install Swift 6.2
uses: swift-actions/setup-swift@v2
with:
swift-version: '6.2'

- name: Swift Format
run: swift format lint --strict --recursive Sources/

Expand Down
5 changes: 0 additions & 5 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,6 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Install Swift 6.2
uses: swift-actions/setup-swift@v2
with:
swift-version: '6.2'

- name: Import code signing certificate
env:
DEVELOPER_CERT_P12: ${{ secrets.DEVELOPER_CERT_P12 }}
Expand Down
62 changes: 61 additions & 1 deletion Sources/devtail/AppDelegate.swift
Original file line number Diff line number Diff line change
@@ -1,12 +1,34 @@
import AppKit
import SwiftUI

@MainActor
final class AppDelegate: NSObject, NSApplicationDelegate {
var store: ProcessStore?
var store: ProcessStore!

private var statusItem: NSStatusItem!
private var popover: NSPopover!
private var signalSource: DispatchSourceSignal?

func applicationDidFinishLaunching(_ notification: Notification) {
store = ProcessStore()
store.onIconChange = { [weak self] in self?.updateMenuBarIcon() }

statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
updateMenuBarIcon()

if let button = statusItem.button {
button.action = #selector(togglePopover)
button.target = self
}

popover = NSPopover()
popover.contentSize = NSSize(width: 360, height: 500)
popover.behavior = .transient
popover.contentViewController = NSHostingController(
rootView: ContentView(store: store)
.onAppear { AppNotifications.requestPermission() }
)

signal(SIGTERM, SIG_IGN)
let source = DispatchSource.makeSignalSource(signal: SIGTERM, queue: .main)
source.setEventHandler { [weak self] in
Expand All @@ -25,6 +47,44 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
}
}

@objc private func togglePopover() {
if popover.isShown {
popover.close()
} else if let button = statusItem.button {
popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
NSApp.activate()
}
}

private func updateMenuBarIcon() {
let anyRunning = store.processes.contains { $0.isRunning }
guard
let baseImage = NSImage(
systemSymbolName: "terminal",
accessibilityDescription: "devtail"
)
else { return }

let dotSize: CGFloat = 4.5
let dotGap: CGFloat = 1.5
let totalWidth = anyRunning ? baseImage.size.width + dotGap + dotSize : baseImage.size.width
let composited = NSImage(
size: NSSize(width: totalWidth, height: baseImage.size.height),
flipped: false
) { _ in
baseImage.draw(in: NSRect(origin: .zero, size: baseImage.size))
if anyRunning {
let dotY = (baseImage.size.height - dotSize) / 2
let dotRect = NSRect(x: baseImage.size.width + dotGap, y: dotY, width: dotSize, height: dotSize)
NSColor.black.setFill()
NSBezierPath(ovalIn: dotRect).fill()
}
return true
}
composited.isTemplate = true
statusItem.button?.image = composited
}

private func performCleanup() {
PopOutWindowManager.shared.closeAll()
store?.stopAllForQuit()
Expand Down
10 changes: 1 addition & 9 deletions Sources/devtail/DevtailApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,9 @@ import SwiftUI

@main
struct DevtailApp: App {
@State private var store = ProcessStore()
@NSApplicationDelegateAdaptor private var appDelegate: AppDelegate

var body: some Scene {
MenuBarExtra("devtail", systemImage: "terminal") {
ContentView(store: store)
.onAppear {
appDelegate.store = store
AppNotifications.requestPermission()
}
}
.menuBarExtraStyle(.window)
Settings { EmptyView() }
}
}
23 changes: 23 additions & 0 deletions Sources/devtail/ProcessCardView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,20 @@ struct ProcessCardView: View {

@State private var isHovered = false

private func popOutButton(buffer: TerminalBuffer, title: String) -> some View {
Button {
PopOutWindowManager.shared.openWindow(buffer: buffer, title: title)
} label: {
Image(systemName: "arrow.up.forward.app")
.font(.system(size: 11, weight: .medium))
.foregroundStyle(.tertiary)
.padding(4)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.help("Pop out")
}

var body: some View {
Button(action: onSelect) {
VStack(alignment: .leading, spacing: 8) {
Expand All @@ -30,6 +44,9 @@ struct ProcessCardView: View {
fontSize: 10
)
}
.overlay(alignment: .topTrailing) {
popOutButton(buffer: process.buffer, title: process.name)
}
}

ForEach(process.auxiliaryCommands) { aux in
Expand All @@ -54,6 +71,12 @@ struct ProcessCardView: View {
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.overlay(alignment: .topTrailing) {
popOutButton(
buffer: auxBuf,
title: "\(process.name) — \(aux.name)"
)
}
}
}
}
Expand Down
11 changes: 9 additions & 2 deletions Sources/devtail/ProcessStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import SwiftUI
@Observable
final class ProcessStore {
var processes: [DevProcess]
var onIconChange: (() -> Void)?
private var isQuitting = false

init() {
Expand All @@ -22,7 +23,10 @@ final class ProcessStore {
}

for process in processes {
process.onStateChange = { [weak self] in self?.save() }
process.onStateChange = { [weak self] in
self?.save()
self?.onIconChange?()
}
}

let autoStartIDs = Set(saved.filter(\.wasRunning).map(\.id))
Expand All @@ -44,7 +48,10 @@ final class ProcessStore {
workingDirectory: workingDirectory,
auxiliaryCommands: auxiliaryCommands
)
process.onStateChange = { [weak self] in self?.save() }
process.onStateChange = { [weak self] in
self?.save()
self?.onIconChange?()
}
withAnimation(.spring(duration: 0.35, bounce: 0.2)) {
processes.insert(process, at: 0)
}
Expand Down
Loading