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
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions apps/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ impl Export {
compression: cap_export::mp4::ExportCompression::Maximum,
custom_bpp: None,
force_ffmpeg_decoder: false,
optimize_filesize: false,
}
.export(exporter_base, move |_f| {
// print!("\rrendered frame {f}");
Expand Down
1 change: 0 additions & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
"@tanstack/solid-query": "^5.51.21",
"@tauri-apps/api": "2.8.0",
"@tauri-apps/plugin-clipboard-manager": "^2.3.0",
"@tauri-apps/plugin-deep-link": "^2.4.1",
"@tauri-apps/plugin-dialog": "^2.4.0",
"@tauri-apps/plugin-fs": "^2.4.1",
"@tauri-apps/plugin-http": "^2.5.1",
Expand Down
190 changes: 116 additions & 74 deletions apps/desktop/src-tauri/src/deeplink_actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ pub enum DeepLinkAction {
capture_system_audio: bool,
mode: RecordingMode,
},
PauseRecording,
ResumeRecording,
TogglePauseRecording,
StopRecording,
TakeScreenshot,
OpenEditor {
project_path: PathBuf,
},
Expand All @@ -34,44 +38,8 @@ pub enum DeepLinkAction {
},
}

pub fn handle(app_handle: &AppHandle, urls: Vec<Url>) {
trace!("Handling deep actions for: {:?}", &urls);

let actions: Vec<_> = urls
.into_iter()
.filter(|url| !url.as_str().is_empty())
.filter_map(|url| {
DeepLinkAction::try_from(&url)
.map_err(|e| match e {
ActionParseFromUrlError::ParseFailed(msg) => {
eprintln!("Failed to parse deep link \"{}\": {}", &url, msg)
}
ActionParseFromUrlError::Invalid => {
eprintln!("Invalid deep link format \"{}\"", &url)
}
// Likely login action, not handled here.
ActionParseFromUrlError::NotAction => {}
})
.ok()
})
.collect();

if actions.is_empty() {
return;
}

let app_handle = app_handle.clone();
tauri::async_runtime::spawn(async move {
for action in actions {
if let Err(e) = action.execute(&app_handle).await {
eprintln!("Failed to handle deep link action: {e}");
}
}
});
}

pub enum ActionParseFromUrlError {
ParseFailed(String),
ParseFailed,
Invalid,
NotAction,
}
Expand All @@ -80,35 +48,74 @@ impl TryFrom<&Url> for DeepLinkAction {
type Error = ActionParseFromUrlError;

fn try_from(url: &Url) -> Result<Self, Self::Error> {
#[cfg(target_os = "macos")]
if url.scheme() == "file" {
return url
.to_file_path()
.map(|project_path| Self::OpenEditor { project_path })
.map_err(|_| ActionParseFromUrlError::Invalid);
if url.scheme() != "cap-desktop" {
return Err(ActionParseFromUrlError::NotAction);
}

match url.domain() {
Some(v) if v != "action" => Err(ActionParseFromUrlError::NotAction),
_ => Err(ActionParseFromUrlError::Invalid),
}?;

let params = url
.query_pairs()
.collect::<std::collections::HashMap<_, _>>();
let json_value = params
.get("value")
.ok_or(ActionParseFromUrlError::Invalid)?;
let action: Self = serde_json::from_str(json_value)
.map_err(|e| ActionParseFromUrlError::ParseFailed(e.to_string()))?;
Ok(action)
Some("pause") => Ok(Self::PauseRecording),
Some("resume") => Ok(Self::ResumeRecording),
Some("toggle-pause") => Ok(Self::TogglePauseRecording),
Some("stop") => Ok(Self::StopRecording),
Some("screenshot") => Ok(Self::TakeScreenshot),
_ => {
if url.domain() == Some("action") {
let params = url.query_pairs().collect::<std::collections::HashMap<_, _>>();
let json_value = params.get("value").ok_or(ActionParseFromUrlError::Invalid)?;
let action: Self = serde_json::from_str(json_value)
.map_err(|_| ActionParseFromUrlError::ParseFailed)?;
Ok(action)
} else {
Err(ActionParseFromUrlError::Invalid)
}
}
}
}
}

impl DeepLinkAction {
pub async fn execute(self, app: &AppHandle) -> Result<(), String> {
trace!("Executing deep link action: {:?}", self);

match self {
DeepLinkAction::StartRecording {
Self::PauseRecording => {
let state = app.state::<ArcLock<App>>();
crate::recording::pause_recording(app.clone(), state.clone()).await?;
let _ = ShowCapWindow::Main { init_target_mode: None }.show(app).await;
Ok(())
}
Self::ResumeRecording => {
let state = app.state::<ArcLock<App>>();
crate::recording::resume_recording(app.clone(), state.clone()).await?;
let _ = ShowCapWindow::Main { init_target_mode: None }.show(app).await;
Ok(())
}
Self::TogglePauseRecording => {
let state = app.state::<ArcLock<App>>();
crate::recording::toggle_pause_recording(app.clone(), state.clone()).await?;
let _ = ShowCapWindow::Main { init_target_mode: None }.show(app).await;
Ok(())
}
Self::StopRecording => {
let state = app.state::<ArcLock<App>>();
crate::recording::stop_recording(app.clone(), state.clone()).await?;
let _ = ShowCapWindow::Main { init_target_mode: None }.show(app).await;
Ok(())
}
Self::TakeScreenshot => {
use scap_targets::Display;
let display = Display::get_containing_cursor().unwrap_or_else(Display::primary);
let target = ScreenCaptureTarget::Display { id: display.id() };

match crate::recording::take_screenshot(app.clone(), target).await {
Ok(path) => {
let _ = ShowCapWindow::ScreenshotEditor { path }.show(app).await;
Ok(())
}
Err(e) => Err(format!("Failed to take screenshot: {e}")),
}
}
Self::StartRecording {
capture_mode,
camera,
mic_label,
Expand All @@ -117,42 +124,77 @@ impl DeepLinkAction {
} => {
let state = app.state::<ArcLock<App>>();

crate::set_camera_input(app.clone(), state.clone(), camera, None).await?;
crate::set_mic_input(state.clone(), mic_label).await?;

let capture_target: ScreenCaptureTarget = match capture_mode {
CaptureMode::Screen(name) => cap_recording::screen_capture::list_displays()
let capture_target = match capture_mode {
CaptureMode::Screen(name) => cap_recording::sources::screen_capture::list_displays()
.into_iter()
.find(|(s, _)| s.name == name)
.find(|(s, _)| s.name == *name)
.map(|(s, _)| ScreenCaptureTarget::Display { id: s.id })
.ok_or(format!("No screen with name \"{}\"", &name))?,
CaptureMode::Window(name) => cap_recording::screen_capture::list_windows()
CaptureMode::Window(name) => cap_recording::sources::screen_capture::list_windows()
.into_iter()
.find(|(w, _)| w.name == name)
.find(|(w, _)| w.name == *name)
.map(|(w, _)| ScreenCaptureTarget::Window { id: w.id })
.ok_or(format!("No window with name \"{}\"", &name))?,
};

let inputs = StartRecordingInputs {
mode,
capture_target,
capture_system_audio,
mode,
organization_id: None,
};

crate::recording::start_recording(app.clone(), state, inputs)
if let Some(camera_id) = camera {
crate::set_camera_input(app.clone(), state.clone(), Some(camera_id.clone()), None)
.await
.map_err(|e| e.to_string())?;
}

if let Some(mic) = mic_label {
crate::set_mic_input(state.clone(), Some(mic.clone()))
.await
.map_err(|e| e.to_string())?;
}

crate::recording::start_recording(app.clone(), state.clone(), inputs)
.await
.map(|_| ())
}
DeepLinkAction::StopRecording => {
crate::recording::stop_recording(app.clone(), app.state()).await
.map_err(|e| e.to_string())?;
Ok(())
}
DeepLinkAction::OpenEditor { project_path } => {
Self::OpenEditor { project_path } => {
crate::open_project_from_path(Path::new(&project_path), app.clone())
.map_err(|e| e.to_string())?;
Ok(())
}
DeepLinkAction::OpenSettings { page } => {
crate::show_window(app.clone(), ShowCapWindow::Settings { page }).await
Self::OpenSettings { page } => {
crate::show_window(app.clone(), ShowCapWindow::Settings { page: page.clone() })
.await
.map_err(|e| e.to_string())?;
Ok(())
}
}
}
}

pub fn handle(app_handle: &AppHandle, urls: Vec<Url>) {
trace!("Handling deep actions for: {:?}", &urls);

let actions: Vec<_> = urls
.into_iter()
.filter(|url| !url.as_str().is_empty())
.filter_map(|url| DeepLinkAction::try_from(&url).ok())
.collect();

if actions.is_empty() {
return;
}

let app_handle = app_handle.clone();
tauri::async_runtime::spawn(async move {
for action in actions {
if let Err(e) = action.execute(&app_handle).await {
trace!("Failed to handle deep link action: {}", e);
}
}
});
}
73 changes: 43 additions & 30 deletions apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,9 @@ use std::{
},
time::{Duration, SystemTime, UNIX_EPOCH},
};
use tauri::{AppHandle, Manager, State, Window, WindowEvent, ipc::Channel};
use tauri_plugin_deep_link::DeepLinkExt;
use tauri::{
ipc::Channel, AppHandle, Emitter, Manager, State, Url, Window, WindowEvent,
};
use tauri_plugin_dialog::DialogExt;
use tauri_plugin_global_shortcut::GlobalShortcutExt;
use tauri_plugin_notification::{NotificationExt, PermissionState};
Expand Down Expand Up @@ -3295,29 +3296,47 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) {
tauri::async_runtime::set(tokio::runtime::Handle::current());

#[allow(unused_mut)]
let mut builder =
tauri::Builder::default().plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
trace!("Single instance invoked with args {args:?}");

// This is also handled as a deeplink on some platforms (eg macOS), see deeplink_actions
let Some(cap_file) = args
.iter()
.find(|arg| arg.ends_with(".cap"))
.map(PathBuf::from)
else {
let app = app.clone();
tokio::spawn(async move {
ShowCapWindow::Main {
init_target_mode: None,
}
.show(&app)
.await
});
return;
};
let mut builder = tauri::Builder::default().plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
info!("Single instance invoked with args {:?}", &args);
let app_handle = app.clone();

// 1. Handle Remote Control URLs
if let Some(arg) = args.get(1) {
if arg.starts_with("cap-desktop://") {
if let Ok(url) = Url::parse(arg) {
crate::deeplink_actions::handle(app, vec![url]);
app.emit("tauri://deep-link", vec![arg.clone()]).ok();

let ah = app_handle.clone();
tauri::async_runtime::spawn(async move {
let _ = crate::windows::ShowCapWindow::Main {
init_target_mode: None,
}
.show(&ah)
.await;
});
}
}
}

let _ = open_project_from_path(&cap_file, app.clone());
}));
// 2. Handle .cap files
let cap_file = args.iter()
.find(|arg| arg.ends_with(".cap"))
.map(PathBuf::from);

if let Some(file) = cap_file {
let _ = open_project_from_path(&file, app.clone());
}

let ah = app_handle.clone();
tauri::async_runtime::spawn(async move {
let _ = crate::windows::ShowCapWindow::Main {
init_target_mode: None,
}
.show(&ah)
.await;
});
}));

#[cfg(target_os = "macos")]
{
Expand All @@ -3335,7 +3354,6 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) {
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_notification::init())
.plugin(flags::plugin::init())
.plugin(tauri_plugin_deep_link::init())
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_opener::init())
Expand Down Expand Up @@ -3597,11 +3615,6 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) {
prewarmer.request(event.force).await;
});

let app_handle = app.clone();
app.deep_link().on_open_url(move |event| {
deeplink_actions::handle(&app_handle, event.urls());
});

Ok(())
})
.on_window_event(|window, event| {
Expand Down
Loading