Skip to content

Commit

Permalink
feat: improves recording permissions handling on macOS (#89)
Browse files Browse the repository at this point in the history
* feat: open welcome window instead of cropper if no recording permissions detected

* chore: document the issues in detail

* chore: adds pseudocode

* feat: adds permission prompt to app state

* feat: rely on shown_permission_prompt to decide whether or not to show manual dialog

* style: cargo fmt
  • Loading branch information
clearlysid authored May 17, 2024
1 parent 3f00fb4 commit 3e0acd0
Show file tree
Hide file tree
Showing 8 changed files with 87 additions and 22 deletions.
19 changes: 11 additions & 8 deletions src-tauri/src/cropper/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,6 @@ fn create_cropper_win(app: &AppHandle) {
}

pub fn init_cropper(app: &AppHandle) {
// Note: we need to create the record button window first
// Because the JS in cropper window needs a handle to it
create_record_button_win(app);
create_cropper_win(app);
}
Expand All @@ -151,12 +149,17 @@ pub fn toggle_cropper(app: &AppHandle) {
cropper_win.hide().unwrap();
app.emit("reset-area", ()).expect("couldn't reset area");
}
false => {
app.emit("reset-area", ()).expect("couldn't reset area");
record_win.show().unwrap();
cropper_win.show().unwrap();
cropper_win.set_focus().unwrap();
}
false => match scap::has_permission() {
true => {
app.emit("reset-area", ()).expect("couldn't reset area");
record_win.show().unwrap();
cropper_win.show().unwrap();
cropper_win.set_focus().unwrap();
}
false => {
crate::open_welcome_window(app);
}
},
}
}
}
Expand Down
6 changes: 1 addition & 5 deletions src-tauri/src/editor/frame_encoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,7 @@ impl FrameEncoder {
}

self.gif_encoder
.add_frame_rgba(
self.index,
img,
(frame_pts - start_ts) / (speed as f64),
)
.add_frame_rgba(self.index, img, (frame_pts - start_ts) / (speed as f64))
.unwrap_or_else(|err| {
eprintln!("Error adding frame to encoder: {:?}", err);
});
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/src/editor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ pub fn init_editor(app: &AppHandle, video_file: String, size: (u32, u32)) {
.with_webview(move |wv| {
use cocoa::{base::nil, foundation::NSString};
use objc::{class, msg_send, sel, sel_impl};

let id = wv.inner();
let no: cocoa::base::id = unsafe { msg_send![class!(NSNumber), numberWithBool:0] };
let _: cocoa::base::id = unsafe { msg_send![id, setValue:no forKey: NSString::alloc(nil).init_str("drawsBackground")] };
Expand Down
6 changes: 6 additions & 0 deletions src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ pub struct AppState {
recorder: Mutex<Option<Capturer>>,
cropped_area: Mutex<Vec<u32>>,
preview_path: Mutex<Option<PathBuf>>,

#[cfg(target_os = "macos")]
shown_permission_prompt: Mutex<bool>,
}

impl Default for AppState {
Expand All @@ -32,6 +35,9 @@ impl Default for AppState {
recorder: Mutex::new(None),
cropped_area: Mutex::new(Vec::new()),
preview_path: Mutex::new(None),

#[cfg(target_os = "macos")]
shown_permission_prompt: Mutex::new(false),
}
}
}
Expand Down
13 changes: 7 additions & 6 deletions src-tauri/src/recorder/mod.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
use std::{sync::mpsc, thread};

use crate::{open_welcome_window, AppState};

use henx::{VideoEncoder, VideoEncoderOptions};
use std::{sync::mpsc, thread};
use tauri::{AppHandle, Manager};
use tempfile::NamedTempFile;

mod utils;
use utils::{get_random_id, start_frame_capture};

use henx::{VideoEncoder, VideoEncoderOptions};
#[cfg(target_os = "macos")]
mod permissions;

#[tauri::command]
pub async fn start_recording(app_handle: AppHandle) {
Expand Down Expand Up @@ -149,6 +149,7 @@ pub async fn stop_recording(app_handle: AppHandle) {
}

#[tauri::command]
pub async fn request_recording_permission(_: AppHandle) -> bool {
scap::request_permission()
pub async fn request_recording_permission(app: AppHandle) {
#[cfg(target_os = "macos")]
permissions::ensure_recording_permissions(&app).await;
}
57 changes: 57 additions & 0 deletions src-tauri/src/recorder/permissions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// This file is Mac-specific, it ensures we have the right permissions.

use crate::AppState;
use std::process::Command;
use tauri::{AppHandle, Manager};
use tauri_plugin_dialog::DialogExt;

fn open_permission_settings(app: &AppHandle) {
app.dialog()
.message("Helmer Micro needs permission to record your screen.")
.title("Screen Recording")
.ok_button_label("Open Security Settings")
.show(|result| match result {
true => {
Command::new("open")
.arg("x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture")
.output()
.expect("failed to open security settings");
}
false => {
println!("User denied permission")
}
});
}

pub async fn ensure_recording_permissions(app: &AppHandle) {
// scap::request_permission returns immediately
// It uses CGRequestScreenCaptureAccess from Apple's CoreGraphics framework
// Unfortunately there are no callbacks or events we can listen to.

// FLOW 1: ✅ if the user has already granted permission, returns true.
// FLOW 2: 🔴 if the user has explicitly denied permission, returns false with no prompt
// FLOW 3: ⚠️ if user has not yet granted or denied permission, returns false but also prompts

let state = app.state::<AppState>();
let mut shown_permission_prompt = state.shown_permission_prompt.lock().await;

if !scap::request_permission() {
if shown_permission_prompt.to_owned() {
// FLOW 2

// if shown_permission_prompt is false
// assume we have already prompted the user
// they've either accidentally or explicitly denied it
// so we need to manually prompt them
open_permission_settings(&app);
}

// FLOW 3
// if shown_permission_prompt is false
// assume the system has prompted the user for permission
// they are (hopefully) about to grant it and restart app
// so we update the state to true and do nothing else
*shown_permission_prompt = true;
return;
}
}
5 changes: 4 additions & 1 deletion src-tauri/src/recorder/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ pub async fn start_frame_capture(app_handle: AppHandle) {
show_cursor: record_cursor,
show_highlight: false,
excluded_targets: None,
excluded_windows: Some(vec!["recorder window".to_string(), "cropper window".to_string()]),
excluded_windows: Some(vec![
"recorder window".to_string(),
"cropper window".to_string(),
]),
output_type: FRAME_TYPE,
output_resolution: Resolution::_1080p, // TODO: doesn't respect aspect ratio yet
source_rect: Some(CGRect {
Expand Down
1 change: 0 additions & 1 deletion src-tauri/src/tray/updater.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ pub fn check_for_update(app_handle: AppHandle, silent_if_none: bool) -> Result<(
// TODO: handle a failed update
eprintln!("failed to install update: {}", e);
}

}
}
_ => {}
Expand Down

0 comments on commit 3e0acd0

Please sign in to comment.