Skip to content
Open
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
448 changes: 443 additions & 5 deletions apps/desktop/src-tauri/src/deeplink_actions.rs

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions apps/desktop/src-tauri/src/hotkeys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ pub fn init(app: &AppHandle) {

for (action, hotkey) in &store.hotkeys {
if &Shortcut::from(*hotkey) == shortcut {
tokio::spawn(handle_hotkey(app.clone(), *action));
tokio::spawn(handle_action(app.clone(), *action));
}
}
})
Expand All @@ -136,7 +136,7 @@ pub fn init(app: &AppHandle) {
app.manage(Mutex::new(store));
}

async fn handle_hotkey(app: AppHandle, action: HotkeyAction) -> Result<(), String> {
pub async fn handle_action(app: AppHandle, action: HotkeyAction) -> Result<(), String> {
match action {
HotkeyAction::StartStudioRecording => {
let _ = RequestStartRecording {
Expand Down
31 changes: 30 additions & 1 deletion apps/desktop/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#![recursion_limit = "256"]
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

#[cfg(target_os = "macos")]
use std::ffi::CStr;
use std::sync::Arc;

use cap_desktop_lib::DynLoggingLayer;
Expand Down Expand Up @@ -56,7 +58,7 @@ fn main() {
let path = dirs::home_dir()
.unwrap()
.join("Library/Logs")
.join("so.cap.desktop");
.join(macos_log_bundle_identifier().unwrap_or_else(|| "so.cap.desktop".to_string()));

#[cfg(not(target_os = "macos"))]
let path = dirs::data_local_dir()
Expand Down Expand Up @@ -158,6 +160,33 @@ fn main() {
.block_on(cap_desktop_lib::run(handle, logs_dir));
}

#[cfg(target_os = "macos")]
fn macos_log_bundle_identifier() -> Option<String> {
use cocoa::base::{id, nil};
use cocoa::foundation::NSAutoreleasePool;
use objc::{class, msg_send, sel, sel_impl};

unsafe {
let _pool = NSAutoreleasePool::new(nil);
let bundle: id = msg_send![class!(NSBundle), mainBundle];
if bundle == nil {
return None;
}

let identifier: id = msg_send![bundle, bundleIdentifier];
if identifier == nil {
return None;
}

let utf8: *const std::os::raw::c_char = msg_send![identifier, UTF8String];
if utf8.is_null() {
return None;
}

Some(CStr::from_ptr(utf8).to_string_lossy().into_owned())
}
}

fn install_panic_hook(logs_dir: std::path::PathBuf) {
let prev = std::panic::take_hook();
let panics_log = logs_dir.join("panics.log");
Expand Down
9 changes: 8 additions & 1 deletion apps/desktop/src-tauri/src/recording_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,17 @@ impl RecordingSettingsStore {
}

pub fn set_mode(app: &AppHandle<Wry>, mode: RecordingMode) -> Result<(), String> {
Self::set_mode_option(app, Some(mode))
}

pub(crate) fn set_mode_option(
app: &AppHandle<Wry>,
mode: Option<RecordingMode>,
) -> Result<(), String> {
let store = app.store("store").map_err(|e| e.to_string())?;

let mut settings = Self::get(app)?.unwrap_or_default();
settings.mode = Some(mode);
settings.mode = mode;

store.set(Self::KEY, serde_json::json!(settings));
store.save().map_err(|e| e.to_string())
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src-tauri/src/target_select_overlay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,8 @@ pub async fn close_target_select_overlays(
state.destroy(&display_id, app.global_shortcut());
}

crate::deeplink_actions::restore_temporary_recording_mode(&app).await;

Ok(())
}

Expand Down
33 changes: 32 additions & 1 deletion apps/desktop/src-tauri/src/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ use crate::{
target_select_overlay::WindowFocusManager,
window_exclusion::WindowExclusion,
};
use cap_recording::{feeds, sources::screen_capture::ScreenCaptureTarget};
use cap_recording::{RecordingMode, feeds, sources::screen_capture::ScreenCaptureTarget};

#[cfg(target_os = "macos")]
const DEFAULT_TRAFFIC_LIGHTS_INSET: LogicalPosition<f64> = LogicalPosition::new(12.0, 12.0);
Expand Down Expand Up @@ -300,6 +300,37 @@ pub(crate) async fn restore_main_window_inputs(app: &AppHandle) {
.ok()
.flatten()
.unwrap_or_default();

if matches!(settings.mode, Some(RecordingMode::Screenshot)) {
if let Err(err) = crate::set_mic_input(state.clone(), None).await {
warn!("Failed to suspend microphone input for screenshot mode: {err}");
}

let operation_lock = app.state::<crate::CameraWindowOperationLock>();
let _operation_guard = operation_lock.lock().await;

let (camera_feed, had_camera_input) = {
let app_state = &mut *state.write().await;
let had_camera_input =
app_state.selected_camera_id.is_some() || app_state.camera_in_use;
app_state.selected_camera_id = None;
app_state.camera_in_use = false;
app_state.camera_cleanup_done = true;
app_state.camera_preview.pause();
(app_state.camera_feed.clone(), had_camera_input)
};

if had_camera_input && let Err(err) = camera_feed.ask(feeds::camera::RemoveInput).await {
warn!("Failed to suspend camera input for screenshot mode: {err}");
}

if let Some(window) = CapWindowId::Camera.get(app) {
let _ = window.hide();
}

return;
}

let stored_camera_id = settings.camera_id.clone();

if let Err(err) = crate::set_mic_input(state.clone(), settings.mic_name).await {
Expand Down
97 changes: 75 additions & 22 deletions apps/desktop/src/routes/(window-chrome)/new-main/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import {
listWindows,
listWindowsWithThumbnails,
} from "~/utils/queries";
import { createRecordingInputHandlers } from "~/utils/screenshot-recording-inputs";
import {
type CaptureDisplay,
type CaptureDisplayWithThumbnail,
Expand Down Expand Up @@ -2058,8 +2059,55 @@ function Page() {

const setCamera = createCameraMutation();

const {
restoreRecordingInputs,
suspendRecordingInputsForScreenshot,
syncRecordingInputsForMode,
} = createRecordingInputHandlers({
setMicInput: (name) => setMicInput.mutateAsync(name),
setCameraInput: (args) => setCamera.mutateAsync(args),
});

let recordingInputsToRestore: {
micName: string | null;
cameraID: DeviceOrModelID | null;
} = {
micName: rawOptions.micName ?? null,
cameraID: rawOptions.cameraID ? { ...rawOptions.cameraID } : null,
};

const rememberRecordingInputsToRestore = (inputs: {
micName: string | null;
cameraID: DeviceOrModelID | null;
}) => {
recordingInputsToRestore = {
micName: inputs.micName,
cameraID: inputs.cameraID ? { ...inputs.cameraID } : null,
};
return recordingInputsToRestore;
};

createUpdateCheck();

createEffect((wasScreenshotMode) => {
Comment thread
ryanr14 marked this conversation as resolved.
const isScreenshotMode = rawOptions.mode === "screenshot";

if (isScreenshotMode && !wasScreenshotMode) {
rememberRecordingInputsToRestore({
micName: rawOptions.micName ?? null,
cameraID: rawOptions.cameraID ?? null,
});
void suspendRecordingInputsForScreenshot();
} else if (!isScreenshotMode && wasScreenshotMode) {
void restoreRecordingInputs(
recordingInputsToRestore.micName,
recordingInputsToRestore.cameraID,
);
}

return isScreenshotMode;
}, rawOptions.mode === "screenshot");
Comment thread
ryanr14 marked this conversation as resolved.

onMount(async () => {
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
Expand Down Expand Up @@ -2091,17 +2139,18 @@ function Page() {
void emit("main-window-ready");
scheduleTargetListPrewarm();

if (rawOptions.micName) {
setMicInput
.mutateAsync(rawOptions.micName)
.catch((error) => console.error("Failed to set mic input:", error));
}

if (rawOptions.cameraID) {
setCamera
.mutateAsync({ model: rawOptions.cameraID })
.catch((error) => console.error("Failed to set camera input:", error));
}
const storedSettings = await recordingSettingsStore.get().catch((error) => {
console.error("Failed to read recording settings:", error);
return null;
});
const recordingInputs = rememberRecordingInputsToRestore({
micName: storedSettings?.micName ?? rawOptions.micName ?? null,
cameraID: storedSettings?.cameraId ?? rawOptions.cameraID ?? null,
});
Comment thread
ryanr14 marked this conversation as resolved.
await syncRecordingInputsForMode({
mode: rawOptions.mode,
...recordingInputs,
});

const unlistenFocus = currentWindow.onFocusChanged(
({ payload: focused }) => {
Expand Down Expand Up @@ -2528,19 +2577,23 @@ function Page() {
name="Area"
class="flex-1"
/>
<TargetTypeButton
selected={rawOptions.targetMode === "camera"}
Component={IconLucideVideo}
disabled={isRecording()}
onClick={() => {
toggleTargetMode("camera");
}}
name="Camera Only"
class="flex-1"
/>
<Show when={rawOptions.mode !== "screenshot"}>
<TargetTypeButton
selected={rawOptions.targetMode === "camera"}
Component={IconLucideVideo}
disabled={isRecording()}
onClick={() => {
toggleTargetMode("camera");
}}
name="Camera Only"
class="flex-1"
/>
</Show>
</div>
</div>
<BaseControls />
<Show when={rawOptions.mode !== "screenshot"}>
<BaseControls />
</Show>
</div>
</Transition>
);
Expand Down
Loading