Skip to content
Merged
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 apps/tauri/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@ cpal = "0.15"
rubato = "0.16"
whisper-rs = "0.15"
reqwest = { version = "0.12", features = ["stream"] }
tokio = { version = "1", features = ["fs"] }
tokio = { version = "1", features = ["fs", "net", "io-util"] }
futures-util = "0.3"
sha2 = "0.10"
24 changes: 24 additions & 0 deletions apps/tauri/src-tauri/src/cli_installer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,30 @@ use std::path::PathBuf;
use std::process::Command;

const CLI_SCRIPT: &str = r#"#!/bin/bash
SOCKET="$HOME/.arandu/arandu.sock"

# Se socket existe, usar IPC (caminho rápido)
if [ -S "$SOCKET" ]; then
if [ "$#" -eq 0 ]; then
if printf '{"command":"show"}\n' | nc -U "$SOCKET" -w 2 2>/dev/null; then
exit 0
fi
else
FAILED=0
for f in "$@"; do
ABS="$(cd "$(dirname "$f")" 2>/dev/null && echo "$PWD/$(basename "$f")")"
ESCAPED=${ABS//\\/\\\\}
ESCAPED=${ESCAPED//\"/\\\"}
if ! printf '{"command":"open","path":"%s"}\n' "$ESCAPED" | nc -U "$SOCKET" -w 2 2>/dev/null; then
FAILED=1
break
fi
done
[ "$FAILED" -eq 0 ] && exit 0
fi
fi
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# Fallback: método tradicional com open (inicia app se necessário)
APP=""
for p in "/Applications/Arandu.app" "$HOME/Applications/Arandu.app"; do
[ -d "$p" ] && APP="$p" && break
Expand Down
193 changes: 193 additions & 0 deletions apps/tauri/src-tauri/src/ipc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use tauri::{Emitter, Manager};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::{UnixListener, UnixStream};

#[derive(Deserialize)]
struct IpcCommand {
command: String,
#[serde(default)]
path: Option<String>,
}

#[derive(Serialize)]
struct IpcResponse {
success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
error: Option<String>,
}

pub struct SocketState(pub Mutex<Option<PathBuf>>);

pub fn setup(app: &tauri::App) -> Result<(), String> {
let sock_path = socket_path()?;
cleanup_stale_socket(&sock_path)?;

let app_handle = app.handle().clone();
tauri::async_runtime::spawn(async move {
match UnixListener::bind(&sock_path) {
Ok(listener) => {
let state = app_handle.state::<SocketState>();
if let Ok(mut guard) = state.0.lock() {
*guard = Some(sock_path.clone());
}

#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = std::fs::Permissions::from_mode(0o600);
let _ = std::fs::set_permissions(&sock_path, perms);
}

socket_listener_loop(listener, app_handle).await;
}
Err(e) => {
eprintln!("Failed to bind socket: {}", e);
}
}
});

Ok(())
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

pub fn cleanup(state: tauri::State<SocketState>) {
if let Ok(guard) = state.0.lock() {
if let Some(path) = guard.as_ref() {
let _ = std::fs::remove_file(path);
}
}
}

fn socket_path() -> Result<PathBuf, String> {
let home = std::env::var("HOME")
.map_err(|_| "HOME environment variable not set".to_string())?;
let arandu_dir = PathBuf::from(home).join(".arandu");

std::fs::create_dir_all(&arandu_dir)
.map_err(|e| format!("Failed to create ~/.arandu: {}", e))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&arandu_dir, std::fs::Permissions::from_mode(0o700))
.map_err(|e| format!("Failed to set ~/.arandu permissions: {}", e))?;
}

Ok(arandu_dir.join("arandu.sock"))
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

fn cleanup_stale_socket(path: &Path) -> Result<(), String> {
if !path.exists() {
return Ok(());
}

match std::os::unix::net::UnixStream::connect(path) {
Ok(_) => Err("Socket already in use by another instance".to_string()),
Err(_) => {
std::fs::remove_file(path)
.map_err(|e| format!("Failed to remove stale socket: {}", e))
}
}
}

async fn socket_listener_loop(listener: UnixListener, app: tauri::AppHandle) {
loop {
match listener.accept().await {
Ok((stream, _addr)) => {
let app_clone = app.clone();
tauri::async_runtime::spawn(async move {
if let Err(e) = handle_client(stream, app_clone).await {
eprintln!("Client error: {}", e);
}
});
}
Err(e) => {
eprintln!("Accept error: {}", e);
}
}
}
}

async fn handle_client(stream: UnixStream, app: tauri::AppHandle) -> Result<(), String> {
let (reader, mut writer) = stream.into_split();
let reader = BufReader::new(reader);
let mut lines = reader.lines();

while let Some(line) = lines.next_line().await.map_err(|e| e.to_string())? {
let response = match serde_json::from_str::<IpcCommand>(&line) {
Ok(cmd) => process_command(cmd, &app),
Err(e) => IpcResponse {
success: false,
error: Some(format!("Invalid JSON: {}", e)),
},
};

let json = serde_json::to_string(&response).unwrap_or_default();
writer
.write_all(format!("{}\n", json).as_bytes())
.await
.map_err(|e| e.to_string())?;
}

Ok(())
}

fn process_command(cmd: IpcCommand, app: &tauri::AppHandle) -> IpcResponse {
match cmd.command.as_str() {
"open" => {
if let Some(path) = cmd.path {
match std::fs::canonicalize(&path) {
Ok(abs_path) => {
let path_str = abs_path.to_string_lossy().to_string();

if let Some(window) = app.get_webview_window("main") {
let _ = window.unminimize();
let _ = window.show();
let _ = window.set_focus();
}

match app.emit("open-file", &path_str) {
Ok(_) => IpcResponse {
success: true,
error: None,
},
Err(e) => IpcResponse {
success: false,
error: Some(format!("Failed to emit event: {}", e)),
},
}
}
Err(e) => IpcResponse {
success: false,
error: Some(format!("Invalid path: {}", e)),
},
}
} else {
IpcResponse {
success: false,
error: Some("Missing 'path' field".to_string()),
}
}
}
"ping" => IpcResponse {
success: true,
error: None,
},
"show" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.unminimize();
let _ = window.show();
let _ = window.set_focus();
}
IpcResponse {
success: true,
error: None,
}
}
_ => IpcResponse {
success: false,
error: Some(format!("Unknown command: {}", cmd.command)),
},
}
}
23 changes: 21 additions & 2 deletions apps/tauri/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ use tauri_plugin_global_shortcut::{GlobalShortcutExt, ShortcutState};

#[cfg(target_os = "macos")]
mod cli_installer;
#[cfg(unix)]
mod ipc;
mod tray;
mod whisper;

Expand Down Expand Up @@ -334,7 +336,7 @@ fn setup_macos_menu(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>>

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
let builder = tauri::Builder::default()
.plugin(tauri_plugin_cli::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
Expand Down Expand Up @@ -370,13 +372,25 @@ pub fn run() {
.manage(ExplicitQuit(Arc::new(AtomicBool::new(false))))
.manage(IsRecording(Arc::new(AtomicBool::new(false))))
.manage(whisper::commands::RecorderState(Mutex::new(None)))
.manage(whisper::commands::TranscriberState(Mutex::new(None)))
.manage(whisper::commands::TranscriberState(Mutex::new(None)));

#[cfg(unix)]
let builder = builder.manage(ipc::SocketState(Mutex::new(None)));

builder
.setup(|app| {
#[cfg(target_os = "macos")]
setup_macos_menu(app)?;

tray::setup(app)?;

#[cfg(unix)]
{
if let Err(e) = ipc::setup(app) {
eprintln!("Failed to setup IPC socket: {}", e);
}
}

let shortcut_str = if let Ok(app_data_dir) = app.path().app_data_dir() {
let settings = whisper::model_manager::load_settings(&app_data_dir);
settings.shortcut
Expand Down Expand Up @@ -475,6 +489,11 @@ pub fn run() {
if let tauri::RunEvent::ExitRequested { api, .. } = &event {
let quit_flag = app_handle.state::<ExplicitQuit>();
if quit_flag.0.load(Ordering::Relaxed) {
#[cfg(unix)]
{
let socket_state = app_handle.state::<ipc::SocketState>();
ipc::cleanup(socket_state);
}
return;
}
api.prevent_exit();
Expand Down