Files
tatsumi/src-tauri/src/lib.rs
T
hikari f2c4fb34b7
CI / Lint & Check (push) Failing after 12s
CI / Build Windows (push) Has been skipped
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m5s
feat: initial Tatsumi release
Tatsumi is a Tauri desktop app for generating AI character art of Naomi
using Google Gemini's image model. Features three generation modes
(avatar, art, replace), persistent conversation threads, message
editing and deletion, retry support, cost tracking, and an about modal
with lore-accurate self-introduction from Emi Carrigan.
2026-04-09 20:16:54 -07:00

120 lines
3.1 KiB
Rust

mod gemini;
mod storage;
use gemini::{call_gemini, read_reference_image_base64};
use serde::Serialize;
use storage::{
delete_thread_from_disk, load_config_from_disk, load_threads_from_disk, save_config_to_disk,
save_thread_to_disk, Config, MessagePart, Thread, ThreadMessage,
};
#[derive(Serialize)]
struct SendMessageResult {
parts: Vec<MessagePart>,
#[serde(rename = "costUsd")]
cost_usd: f64,
}
#[tauri::command]
async fn load_threads() -> Result<Vec<Thread>, String> {
Ok(load_threads_from_disk())
}
#[tauri::command]
async fn save_thread(thread: Thread) -> Result<(), String> {
save_thread_to_disk(thread)
}
#[tauri::command]
async fn delete_thread(thread_id: String) -> Result<(), String> {
delete_thread_from_disk(&thread_id)
}
#[tauri::command]
async fn read_reference_image() -> String {
read_reference_image_base64()
}
#[tauri::command]
async fn load_config() -> Result<Config, String> {
Ok(load_config_from_disk())
}
#[tauri::command]
async fn save_config(config: Config) -> Result<(), String> {
save_config_to_disk(config)
}
#[tauri::command]
async fn send_message(
api_key: String,
mode: String,
history: Vec<ThreadMessage>,
user_text: Option<String>,
user_image_base64: Option<String>,
user_image_mime: Option<String>,
) -> Result<SendMessageResult, String> {
let (parts, cost_usd) =
call_gemini(api_key, mode, history, user_text, user_image_base64, user_image_mime).await?;
Ok(SendMessageResult { parts, cost_usd })
}
#[tauri::command]
async fn open_url(url: String) -> Result<(), String> {
open::that(&url).map_err(|e| format!("Failed to open URL: {e}"))
}
#[tauri::command]
async fn save_image(
app: tauri::AppHandle,
base64_data: String,
mime_type: String,
file_name: String,
) -> Result<(), String> {
use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
use tauri_plugin_dialog::DialogExt;
let extension = if mime_type.contains("jpeg") || mime_type.contains("jpg") {
"jpg"
} else {
"png"
};
let path = app
.dialog()
.file()
.add_filter("Image", &[extension])
.set_file_name(&file_name)
.blocking_save_file();
if let Some(file_path) = path {
let bytes = BASE64
.decode(&base64_data)
.map_err(|e| format!("Failed to decode image: {}", e))?;
let path_buf: std::path::PathBuf = file_path.into_path().map_err(|e| format!("Invalid path: {}", e))?;
std::fs::write(&path_buf, bytes).map_err(|e| format!("Failed to save: {}", e))?;
}
Ok(())
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
.invoke_handler(tauri::generate_handler![
delete_thread,
load_config,
load_threads,
open_url,
read_reference_image,
save_config,
save_image,
save_thread,
send_message,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}