use std::sync::Mutex;
use serde_json::{json, Value};
use tauri::{AppHandle, Emitter, Manager, Runtime, State, Window};
use crate::config::Mode;
use crate::session::Session;
use crate::storage::{append_events_file, append_lifecycle, finalize_session, now_ms, recordings_root};
type SessionState = Mutex<Session>;
#[tauri::command]
pub fn start_session<R: Runtime>(app: AppHandle<R>, state: State<'_, SessionState>) -> Result<String, String> {
let id = format!("{}", now_ms());
let started_at = now_ms();
let mode = {
let mut s = state.lock().map_err(|e| e.to_string())?;
s.id = Some(id.clone());
s.started_at = started_at;
s.active = true;
s.segment_seq.clear();
s.current.clear();
s.remote_session_id = None;
s.dir = None;
s.config.mode
};
let dir = if mode == Mode::Local {
let dir = recordings_root(&app).join(&id);
std::fs::create_dir_all(dir.join("segments")).map_err(|e| e.to_string())?;
std::fs::write(
dir.join("session.json"),
json!({ "id": id, "source": "self", "startedAt": started_at }).to_string(),
)
.map_err(|e| e.to_string())?;
state.lock().map_err(|e| e.to_string())?.dir = Some(dir.clone());
Some(dir)
} else {
None
};
if let Some(dir) = dir {
let skip_prefix = state
.lock()
.map_err(|e| e.to_string())?
.config
.skip_focus_prefix
.clone();
for (label, w) in app.webview_windows() {
if !skip_prefix.is_empty() && label.starts_with(&skip_prefix) {
continue;
}
if w.is_focused().unwrap_or(false) {
let _ = append_lifecycle(
&dir,
json!({ "type": "focus", "label": label, "t": now_ms() }),
);
break;
}
}
}
app.emit("recording-session", json!({ "active": true, "id": id }))
.map_err(|e| e.to_string())?;
Ok(id)
}
#[tauri::command]
pub fn bind_session<R: Runtime>(
app: AppHandle<R>,
state: State<'_, SessionState>,
session_id: String,
) -> Result<(), String> {
{
let mut s = state.lock().map_err(|e| e.to_string())?;
s.active = true;
s.remote_session_id = Some(session_id.clone());
}
app.emit(
"recording-session",
json!({ "active": true, "sessionId": session_id }),
)
.map_err(|e| e.to_string())?;
Ok(())
}
#[tauri::command]
pub fn session_id(state: State<'_, SessionState>) -> Result<Option<String>, String> {
Ok(state
.lock()
.map_err(|e| e.to_string())?
.remote_session_id
.clone())
}
#[tauri::command]
pub fn stop_session<R: Runtime>(app: AppHandle<R>, state: State<'_, SessionState>) -> Result<(), String> {
let (dir, id, started_at, mode) = {
let mut s = state.lock().map_err(|e| e.to_string())?;
s.active = false;
s.remote_session_id = None;
let open_segs: Vec<(String, String)> = s.current.drain().collect();
if let Some(dir) = &s.dir {
let now = now_ms();
for (label, seg) in open_segs {
let _ = append_lifecycle(
dir,
json!({ "type": "hidden", "label": label, "segmentId": seg, "t": now }),
);
}
}
(s.dir.clone(), s.id.clone(), s.started_at, s.config.mode)
};
if mode == Mode::Local {
if let Some(dir) = dir {
let ended_at = now_ms();
let _ = finalize_session(&dir, ended_at);
let meta = json!({ "id": id, "source": "self", "startedAt": started_at, "endedAt": ended_at });
let _ = std::fs::write(dir.join("session.json"), meta.to_string());
}
}
app.emit("recording-session", json!({ "active": false }))
.map_err(|e| e.to_string())?;
Ok(())
}
#[tauri::command]
pub fn is_recording_active(state: State<'_, SessionState>) -> bool {
state.lock().map(|s| s.active).unwrap_or(false)
}
#[tauri::command]
pub fn begin_segment<R: Runtime>(
app: AppHandle<R>,
state: State<'_, SessionState>,
window: Window<R>,
) -> Result<String, String> {
let label = window.label().to_string();
let (seg, dir, mode) = {
let mut s = state.lock().map_err(|e| e.to_string())?;
if !s.active {
return Err("session not active".into());
}
if s.config.mode == Mode::Local {
let n = s.segment_seq.entry(label.clone()).or_insert(0);
let seg = format!("{}#{}", label, n);
*n += 1;
s.current.insert(label.clone(), seg.clone());
(seg, s.dir.clone(), Mode::Local)
} else {
(String::new(), None, Mode::Remote)
}
};
if mode == Mode::Local {
if let Some(dir) = dir {
append_lifecycle(
&dir,
json!({ "type": "shown", "label": label, "segmentId": seg, "t": now_ms() }),
)?;
}
}
let _ = app; Ok(seg)
}
#[tauri::command]
pub fn append_events(
state: State<'_, SessionState>,
segment_id: String,
events: Vec<Value>,
) -> Result<(), String> {
let (dir, mode) = {
let s = state.lock().map_err(|e| e.to_string())?;
(s.dir.clone(), s.config.mode)
};
if mode == Mode::Local {
if let Some(dir) = dir {
append_events_file(&dir, &segment_id, &events)?;
}
}
Ok(())
}
#[tauri::command]
pub fn notify_segment_start<R: Runtime>(app: AppHandle<R>, label: String) -> Result<bool, String> {
Ok(crate::emit_segment_start_if_active(&app, &label))
}