Add screen recording, crop, and a Cap-style result window

Recording: Ctrl+Shift+Cmd+R (or the menu) opens the same selection
flow — a marquee records that region, clicking a window records it
(following moves). Video is H.264 .mov at 60fps via SCStream ->
AVAssetWriter with the cursor included, auto-saved to ~/Movies and
revealed in Finder on stop. A red border and a floating timer/stop bar
show during recording, both excluded from the video. Audio is wired
behind a flag for later.

The result window gains a Crop button (crop to the selection;
annotations translate and stay editable) and is restyled after Cap:
sliding-pill Screenshot/Text switcher, capsule buttons in Cap blue,
circular icon buttons, and the content on a rounded card — light and
dark.

Generated with OpenCode

Assisted-by: Kimi:k3
This commit is contained in:
2026-10-24 21:48:59 +08:00
parent 04b84e188a
commit 6a2afef1d2
8 changed files with 706 additions and 140 deletions
+29 -3
View File
@@ -79,16 +79,22 @@ final class CaptureController: NSObject {
/// Frozen mode: the screen is snapshotted at hotkey press, so the pointer
/// and any hover state (tooltips, open menus) end up in the capture.
private(set) var isFrozenMode = false
/// Recording mode: the selection flow ends in a recording, not a still.
private(set) var isRecordingMode = false
private var frozenSnapshots: [ScreenCapture.DisplaySnapshot] = []
func begin(frozen: Bool = false) {
func begin(frozen: Bool = false, recording: Bool = false) {
guard !isSelecting else { return }
// No new captures while a recording is running (the selection
// overlays would end up in the video).
guard !RecordingController.shared.state.isRecording else { return }
guard PermissionManager.isGranted else {
PermissionManager.request()
return
}
isSelecting = true
isFrozenMode = frozen
isFrozenMode = frozen && !recording
isRecordingMode = recording
previousApp = NSWorkspace.shared.frontmostApplication
// With the event tap driving input we must NOT activate: stealing
// focus turns the target app into background mode (dimmed chrome),
@@ -98,7 +104,7 @@ final class CaptureController: NSObject {
if !usesTap {
NSApp.activate(ignoringOtherApps: true)
}
CaptureController.log("begin: frozen=\(frozen) usesTap=\(usesTap) active=\(NSApp.isActive)")
CaptureController.log("begin: frozen=\(isFrozenMode) recording=\(recording) usesTap=\(usesTap) active=\(NSApp.isActive)")
if frozen {
ScreenCapture.shared.snapshotAllDisplays(includeCursor: true) { [weak self] snapshots in
@@ -137,9 +143,20 @@ final class CaptureController: NSObject {
/// corners come from the window's own alpha).
private func finishWindowPick(_ windowID: CGWindowID, _ rect: NSRect, ownedRisk: Bool) {
let ids = overlayIDs
let recording = isRecordingMode
hideOverlays()
isSelecting = false
restorePreviousApp()
if recording {
// Owned-popup risk: record the screen region instead of the
// window filter, which could composite the parent into the video.
RecordingController.shared.begin(
region: rect,
windowID: ownedRisk ? nil : windowID,
excludingWindowIDs: ids
)
return
}
DispatchQueue.main.async { [self] in
if ownedRisk {
capture(rect, excludingWindowIDs: ids, roundedCorners: true)
@@ -153,6 +170,7 @@ final class CaptureController: NSObject {
let ids = overlayIDs
let frozen = frozenSnapshots
let wasFrozen = isFrozenMode
let recording = isRecordingMode
hideOverlays()
guard rect.width >= 2, rect.height >= 2 else {
cancelSelection()
@@ -175,6 +193,13 @@ final class CaptureController: NSObject {
return
}
if recording {
isSelecting = false
restorePreviousApp()
RecordingController.shared.begin(region: rect, windowID: nil, excludingWindowIDs: ids)
return
}
DispatchQueue.main.async { [self] in
capture(rect, excludingWindowIDs: ids)
}
@@ -195,6 +220,7 @@ final class CaptureController: NSObject {
marqueeStart = nil
frozenSnapshots = []
isFrozenMode = false
isRecordingMode = false
}
private func restorePreviousApp() {