47951a615b
- domain/trilium.rs: Runtime-Download (gepinnt 0.104.0), Instanz pro Projekt auf 127.0.0.1 mit FNV-Port, ETAPI (archive_note, search_notes), Token im Keyring - Setup bis aufs Passwort automatisiert: trilium_connect macht new-document (skipDemoDb), set-password und ETAPI-Login; Modul-Flag erst nach Token-Erfolg - write_panel legt direkt eine Trilium-Notiz an (text|path, folder/description/tags); noteopen-Kanal, das Terminal-Fenster öffnet die Notiz (ersetzt archive_panel) - MD-Altbestand raus: archive*/wiki*, archiveHome samt Permissions, panel_set/panel_load/ wiki_open, panel_archive*/archive_docs/*_archive_home, reveal_path, spellcheck - Dokument-Tab raus: Panel = ToDo/Befehle/Suche + Archiv-Knopf (trilium_open); panel-view/archive-form gelöscht, Markup/CSS/Messages bereinigt - Lösch-Stufe Archiv: Instanz stoppen, Trilium-Datadir und Keyring-Token löschen
493 lines
16 KiB
Rust
493 lines
16 KiB
Rust
//! Tauri-Commands der Verwaltungs-UI (Haupt-App, Popup, Terminal-Header) —
|
|
//! dünne Delegation an domain/ und platform/.
|
|
|
|
use std::time::{Duration, Instant};
|
|
|
|
use crate::domain::credentials::keychain_service;
|
|
use crate::domain::paths::Paths;
|
|
use crate::domain::pool::{
|
|
create_apikey_pool_in, create_oauth_pool_in, create_reference_pool_in, delete_pool_in,
|
|
ensure_oauth_pool, link_pool_runtime_in, list_pools_in, offered_default_dir, read_pool,
|
|
rename_pool_in, set_apikey_in, PoolInfo,
|
|
};
|
|
use crate::domain::project::{
|
|
add_project_in, add_work_dir_in, assign_pool_in, create_project_full_in, delete_preview_in,
|
|
delete_project_scoped_in, display_name_in, is_running, kill_terminals, list_projects_in,
|
|
project_work_dirs_in, read_project_config_in, remove_work_dir_in, resolve_icon_path,
|
|
running_projects_using_pool, set_project_dir_in, set_terminal_config_in, unassign_pool_in,
|
|
DeletePreview, Project, TerminalConfig,
|
|
};
|
|
use crate::domain::settings;
|
|
use crate::domain::usage::{usage_stats_in, UsageRow};
|
|
use crate::platform::KeychainStore;
|
|
use crate::terminal;
|
|
|
|
// ---------- Projekte ----------
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn list_projects() -> Result<Vec<Project>, String> {
|
|
list_projects_in(&Paths::real())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn create_project_full(
|
|
name: String,
|
|
dir: Option<String>,
|
|
pool: Option<String>,
|
|
work_dir: Option<String>,
|
|
create_work_dir: bool,
|
|
terminal: TerminalConfig,
|
|
) -> Result<String, String> {
|
|
create_project_full_in(
|
|
&Paths::real(),
|
|
&name,
|
|
dir.as_deref(),
|
|
pool.as_deref(),
|
|
work_dir.as_deref(),
|
|
create_work_dir,
|
|
terminal,
|
|
)
|
|
}
|
|
|
|
/// Bestehenden Ordner als Projekt aufnehmen (nur Registry-Eintrag).
|
|
#[tauri::command]
|
|
pub(crate) fn add_project(path: String) -> Result<(), String> {
|
|
add_project_in(&Paths::real(), &path)
|
|
}
|
|
|
|
/// Umbau-Sperre: eine laufende Session des Projekts zuerst beenden.
|
|
fn ensure_not_running(project: &str) -> Result<(), String> {
|
|
if is_running(project) {
|
|
let name = display_name_in(&Paths::real(), project)?;
|
|
return Err(format!("{name} läuft noch — erst beenden"));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Projektordner neu zuordnen; bei laufender Session gesperrt.
|
|
#[tauri::command]
|
|
pub(crate) fn set_project_dir(project: String, dir: String) -> Result<(), String> {
|
|
ensure_not_running(&project)?;
|
|
set_project_dir_in(&Paths::real(), &project, &dir)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn add_work_dir(project: String, dir: String) -> Result<(), String> {
|
|
add_work_dir_in(&Paths::real(), &project, &dir)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn remove_work_dir(project: String, dir: String) -> Result<(), String> {
|
|
remove_work_dir_in(&Paths::real(), &project, &dir)
|
|
}
|
|
|
|
/// Artefakt-Vorschau für den Lösch-Dialog.
|
|
#[tauri::command]
|
|
pub(crate) fn delete_preview(project: String) -> Result<DeletePreview, String> {
|
|
delete_preview_in(&Paths::real(), &project)
|
|
}
|
|
|
|
/// Löschen in drei Stufen: "integration" (nur ai-control-Spuren),
|
|
/// "archive" (zusätzlich Archiv), "full" (zusätzlich Projektordner,
|
|
/// Arbeitsordner per Flag).
|
|
#[tauri::command]
|
|
pub(crate) fn delete_project_scoped(
|
|
project: String,
|
|
scope: String,
|
|
delete_work_dirs: bool,
|
|
) -> Result<(), String> {
|
|
ensure_not_running(&project)?;
|
|
delete_project_scoped_in(&Paths::real(), &project, &scope, delete_work_dirs)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn project_work_dirs(project: String) -> Result<Vec<String>, String> {
|
|
project_work_dirs_in(&Paths::real(), &project)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn set_terminal_config(
|
|
project: String,
|
|
theme: Option<String>,
|
|
icon: Option<String>,
|
|
title: Option<String>,
|
|
) -> Result<(), String> {
|
|
set_terminal_config_in(
|
|
&Paths::real(),
|
|
&project,
|
|
TerminalConfig { theme, icon, title, ..Default::default() },
|
|
)
|
|
}
|
|
|
|
/// Icon eines Projekts als data-URL für die Übersicht (PNG oder SVG).
|
|
#[tauri::command]
|
|
pub(crate) fn project_icon(project: String) -> Result<Option<String>, String> {
|
|
use base64::{engine::general_purpose::STANDARD, Engine};
|
|
let paths = Paths::real();
|
|
let Some(icon) = read_project_config_in(&paths, &project)?.terminal.icon else {
|
|
return Ok(None);
|
|
};
|
|
let path = resolve_icon_path(&paths, &project, &icon)?;
|
|
let mime = match path.extension().and_then(|e| e.to_str()) {
|
|
Some(e) if e.eq_ignore_ascii_case("svg") => "image/svg+xml",
|
|
_ => "image/png",
|
|
};
|
|
let bytes = std::fs::read(&path).map_err(|e| format!("{}: {e}", path.display()))?;
|
|
Ok(Some(format!("data:{mime};base64,{}", STANDARD.encode(bytes))))
|
|
}
|
|
|
|
// ---------- Pools ----------
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn list_pools() -> Result<Vec<PoolInfo>, String> {
|
|
list_pools_in(&Paths::real(), &KeychainStore)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn create_oauth_pool(name: String) -> Result<String, String> {
|
|
create_oauth_pool_in(&Paths::real(), &name)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn create_apikey_pool(
|
|
name: String,
|
|
key: String,
|
|
allow_file: bool,
|
|
) -> Result<String, String> {
|
|
create_apikey_pool_in(&Paths::real(), &KeychainStore, &name, &key, allow_file)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn rename_pool(pool: String, name: String) -> Result<(), String> {
|
|
rename_pool_in(&Paths::real(), &pool, &name)
|
|
}
|
|
|
|
/// Anzeige-Namen der Projekte (für Fehlermeldungen mit Projektliste).
|
|
fn display_names(paths: &Paths, ids: &[String]) -> Result<Vec<String>, String> {
|
|
ids.iter().map(|id| display_name_in(paths, id)).collect()
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn delete_pool(pool: String) -> Result<(), String> {
|
|
let paths = Paths::real();
|
|
let running = running_projects_using_pool(&paths, &pool)?;
|
|
if !running.is_empty() {
|
|
return Err(format!(
|
|
"Pool wird noch benutzt — läuft: {}",
|
|
display_names(&paths, &running)?.join(", ")
|
|
));
|
|
}
|
|
delete_pool_in(&paths, &KeychainStore, &pool)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn assign_pool(project: String, pool: String) -> Result<(), String> {
|
|
assign_pool_in(&Paths::real(), &project, &pool)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn unassign_pool(project: String) -> Result<(), String> {
|
|
unassign_pool_in(&Paths::real(), &project)
|
|
}
|
|
|
|
/// Anzeigename eines Pools (pool.json) — die ID ist der Ordnername.
|
|
#[tauri::command]
|
|
pub(crate) fn pool_label(pool: String) -> Result<String, String> {
|
|
Ok(read_pool(&Paths::real(), &pool)?.name)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn set_apikey(pool: String, key: String, allow_file: bool) -> Result<(), String> {
|
|
set_apikey_in(&Paths::real(), &KeychainStore, &pool, &key, allow_file)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn keychain_status(pool: String) -> Result<bool, String> {
|
|
crate::platform::oauth_keychain_exists(&keychain_service(&Paths::real(), &pool)?)
|
|
}
|
|
|
|
/// Claudes Default-Verzeichnis, sofern vorhanden und noch nicht als Pool
|
|
/// eingebunden — die Pool-Anlage bietet es dann als fertigen Pool an.
|
|
#[tauri::command]
|
|
pub(crate) fn default_config_dir() -> Option<String> {
|
|
offered_default_dir(&Paths::real())
|
|
}
|
|
|
|
/// Pool, der auf ein bestehendes Config-Verzeichnis verweist (statt eines neu
|
|
/// angelegten). Der vorhandene Login dort bleibt gültig.
|
|
#[tauri::command]
|
|
pub(crate) fn create_reference_pool(name: String, dir: String) -> Result<String, String> {
|
|
create_reference_pool_in(&Paths::real(), &name, &dir)
|
|
}
|
|
|
|
/// Setzt einen oauth-Pool zurück: löscht den suffixierten Keychain-Eintrag,
|
|
/// damit claude beim nächsten Start des Pools erneut `/login` verlangt. Nur
|
|
/// bei ungenutztem Pool. Der Login selbst passiert in der Session, nicht hier.
|
|
#[tauri::command]
|
|
pub(crate) fn oauth_login(pool: String) -> Result<(), String> {
|
|
let paths = Paths::real();
|
|
ensure_oauth_pool(&paths, &pool)?;
|
|
// Bei einem verwiesenen Pool hinge daran der Login des fremden
|
|
// Verzeichnisses — den löscht die App nicht.
|
|
if let Some(dir) = read_pool(&paths, &pool)?.dir {
|
|
return Err(format!(
|
|
"Neuanmeldung geht bei einem verwiesenen Pool nicht ({dir}) — dort meldet claude selbst an"
|
|
));
|
|
}
|
|
|
|
let running = running_projects_using_pool(&paths, &pool)?;
|
|
if !running.is_empty() {
|
|
return Err(format!(
|
|
"Neuanmeldung nur bei ungenutztem Pool möglich — läuft: {}",
|
|
display_names(&paths, &running)?.join(", ")
|
|
));
|
|
}
|
|
|
|
let service = keychain_service(&paths, &pool)?;
|
|
if crate::platform::oauth_keychain_exists(&service)? {
|
|
crate::platform::oauth_keychain_delete(&service)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Verlinkt die synced Runtime eines bestehenden Pools. Nur bei idlem Pool —
|
|
/// sonst würde das Transkript der laufenden Session ersetzt.
|
|
#[tauri::command]
|
|
pub(crate) fn link_pool_runtime(pool: String) -> Result<(), String> {
|
|
let paths = Paths::real();
|
|
if !running_projects_using_pool(&paths, &pool)?.is_empty() {
|
|
return Err(format!("{pool} wird benutzt — erst Session beenden"));
|
|
}
|
|
link_pool_runtime_in(&paths, &pool)
|
|
}
|
|
|
|
// ---------- Sessions ----------
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn stop_project(project: String) -> Result<(), String> {
|
|
if crate::platform::terminal_pids(&project).is_empty() {
|
|
let name = display_name_in(&Paths::real(), &project)?;
|
|
return Err(format!("{name} läuft nicht"));
|
|
}
|
|
kill_terminals(&project)
|
|
}
|
|
|
|
/// Beendet die laufenden Terminal-Prozesse, wartet auf ihr Ende und öffnet das
|
|
/// interne Terminal neu.
|
|
#[tauri::command]
|
|
pub(crate) fn restart_project(app: tauri::AppHandle, project: String) -> Result<(), String> {
|
|
kill_terminals(&project)?;
|
|
let deadline = Instant::now() + Duration::from_secs(30);
|
|
while is_running(&project) {
|
|
if Instant::now() > deadline {
|
|
let name = display_name_in(&Paths::real(), &project)?;
|
|
return Err(format!("{name} hat sich nach 30 s nicht beendet"));
|
|
}
|
|
std::thread::sleep(Duration::from_millis(250));
|
|
}
|
|
terminal::open_terminal(app, project)
|
|
}
|
|
|
|
/// Tray-Klick auf ein Projekt: läuft es, kommt das Terminal-Fenster nach vorn,
|
|
/// sonst startet es.
|
|
fn start_or_focus(app: &tauri::AppHandle, project: &str) {
|
|
match crate::platform::terminal_pids(project).first() {
|
|
Some(pid) => crate::platform::focus_terminal(*pid),
|
|
None => {
|
|
if let Err(e) = terminal::open_terminal(app.clone(), project.to_string()) {
|
|
eprintln!("{project} starten: {e}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Tray-/Popup-Klick: Projekt starten oder das laufende Terminal fokussieren.
|
|
#[tauri::command]
|
|
pub(crate) fn start_or_focus_cmd(app: tauri::AppHandle, project: String) {
|
|
start_or_focus(&app, &project);
|
|
}
|
|
|
|
// ---------- Popup-Fußzeile ----------
|
|
|
|
/// Popup-Fußzeile „Öffnen": das Hauptfenster zeigen.
|
|
#[tauri::command]
|
|
pub(crate) fn open_main_window(app: tauri::AppHandle) {
|
|
use tauri::Manager;
|
|
if let Some(w) = app.get_webview_window("main") {
|
|
let _ = w.show();
|
|
let _ = w.set_focus();
|
|
}
|
|
}
|
|
|
|
/// Popup-Fußzeile „Beenden": App verlassen.
|
|
#[tauri::command]
|
|
pub(crate) fn quit_app(app: tauri::AppHandle) {
|
|
app.exit(0);
|
|
}
|
|
|
|
// ---------- App-Settings ----------
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn sync_setting() -> Result<bool, String> {
|
|
Ok(settings::sync_on_session_end(&Paths::real()))
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn set_sync_setting(enabled: bool) -> Result<(), String> {
|
|
settings::set_sync_on_session_end_in(&Paths::real(), enabled)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn terminal_font_size() -> u32 {
|
|
settings::terminal_font_size(&Paths::real())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn set_terminal_font_size(size: u32) -> Result<(), String> {
|
|
settings::set_terminal_font_size_in(&Paths::real(), size)
|
|
}
|
|
|
|
// ---------- Verbrauch ----------
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn usage_stats(days: u32) -> Result<Vec<UsageRow>, String> {
|
|
usage_stats_in(&Paths::real(), days)
|
|
}
|
|
|
|
/// Modul-Registry für den Settings-Dialog (Checkbox-Gruppe „Module").
|
|
#[tauri::command]
|
|
pub(crate) fn module_registry(project: String) -> Result<Vec<crate::domain::modules::ModuleInfo>, String> {
|
|
crate::domain::modules::module_infos_in(&Paths::real(), &project)
|
|
}
|
|
|
|
/// Schaltet ein Modul im Projekt an/ab (Settings-Dialog, schreibt sofort).
|
|
#[tauri::command]
|
|
pub(crate) fn set_module(project: String, module: String, enabled: bool) -> Result<(), String> {
|
|
crate::domain::modules::set_module_in(&Paths::real(), &project, &module, enabled)
|
|
}
|
|
|
|
/// Aktive Module fürs Frontend (Registry-Enablement der Projekt-Config).
|
|
#[tauri::command]
|
|
pub(crate) fn enabled_modules(project: String) -> Result<Vec<String>, String> {
|
|
Ok(
|
|
crate::domain::modules::active_in(&Paths::real(), &project)?
|
|
.iter()
|
|
.map(|m| m.id.to_string())
|
|
.collect(),
|
|
)
|
|
}
|
|
|
|
// ---------- Trilium ----------
|
|
|
|
/// Wizard-Status: Runtime da, Instanz erreichbar, Token gültig. Reine
|
|
/// Checks, keine Schreiber.
|
|
#[derive(serde::Serialize)]
|
|
pub(crate) struct TriliumStatus {
|
|
version: &'static str,
|
|
port: u16,
|
|
data_dir: String,
|
|
runtime_ready: bool,
|
|
instance_running: bool,
|
|
token_ok: bool,
|
|
}
|
|
|
|
fn trilium_status_for(project: &str) -> TriliumStatus {
|
|
let paths = Paths::real();
|
|
let instance_running = crate::domain::trilium::instance_running(project);
|
|
TriliumStatus {
|
|
version: crate::domain::trilium::VERSION,
|
|
port: crate::domain::trilium::port_for(project),
|
|
data_dir: crate::domain::paths::contract_home(
|
|
&paths,
|
|
&crate::domain::trilium::data_dir(&paths, project),
|
|
),
|
|
runtime_ready: crate::domain::trilium::runtime_ready(&paths),
|
|
instance_running,
|
|
token_ok: instance_running && crate::domain::trilium::token_ok(project),
|
|
}
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) async fn trilium_status(project: String) -> TriliumStatus {
|
|
trilium_status_for(&project)
|
|
}
|
|
|
|
/// Richtet die Projekt-Instanz ein: Runtime laden (gepinnt, Checksum),
|
|
/// Instanz starten. Schreibt KEINE Config — das Modul-Flag setzt erst
|
|
/// trilium_connect nach erfolgreichem Token-Login.
|
|
#[tauri::command]
|
|
pub(crate) async fn trilium_setup(project: String) -> Result<TriliumStatus, String> {
|
|
crate::domain::trilium::ensure_instance(&Paths::real(), &project)?;
|
|
Ok(trilium_status_for(&project))
|
|
}
|
|
|
|
/// Richtet die Instanz mit dem Passwort aus dem Wizard vollständig ein:
|
|
/// Erstlauf legt das Dokument an und setzt das Passwort, dann ETAPI-Token
|
|
/// holen, im Keyring ablegen und ERST DANN das Modul in der Projekt-Config
|
|
/// freischalten. Das Passwort wird nicht gespeichert.
|
|
#[tauri::command]
|
|
pub(crate) async fn trilium_connect(
|
|
project: String,
|
|
password: String,
|
|
) -> Result<TriliumStatus, String> {
|
|
let paths = Paths::real();
|
|
crate::domain::trilium::ensure_instance(&paths, &project)?;
|
|
if !crate::domain::trilium::initialized(&project)? {
|
|
crate::domain::trilium::setup_new_document(&project)?;
|
|
crate::domain::trilium::set_password(&project, &password)?;
|
|
}
|
|
let token = crate::domain::trilium::login(&project, &password)?;
|
|
crate::domain::trilium::store_token(&project, &token)?;
|
|
if !crate::domain::trilium::token_ok(&project) {
|
|
crate::domain::trilium::delete_token(&project);
|
|
return Err("Token angenommen, aber ETAPI-Zugriff scheitert".into());
|
|
}
|
|
crate::domain::modules::set_module_in(&paths, &project, "trilium", true)?;
|
|
Ok(trilium_status_for(&project))
|
|
}
|
|
|
|
/// Öffnet das Trilium-Fenster des Projekts (startet die Instanz bei Bedarf);
|
|
/// `note_id` springt zur Notiz. Async wie open_panel_window (Fenster-Erzeugung
|
|
/// aus synchronen Commands klemmt auf dem GTK-Mainloop).
|
|
#[tauri::command]
|
|
pub(crate) async fn trilium_open(
|
|
app: tauri::AppHandle,
|
|
project: String,
|
|
note_id: Option<String>,
|
|
) -> Result<(), String> {
|
|
use tauri::Manager;
|
|
crate::domain::trilium::ensure_instance(&Paths::real(), &project)?;
|
|
let mut url = crate::domain::trilium::base_url(&project);
|
|
if let Some(id) = ¬e_id {
|
|
url.push_str(&format!("/#root/{id}"));
|
|
}
|
|
let label = format!("trilium-{project}");
|
|
if let Some(win) = app.get_webview_window(&label) {
|
|
if note_id.is_some() {
|
|
win
|
|
.eval(&format!("window.location.href = {}", serde_json::json!(url)))
|
|
.map_err(|e| e.to_string())?;
|
|
}
|
|
win.set_focus().map_err(|e| e.to_string())?;
|
|
return Ok(());
|
|
}
|
|
let parsed: tauri::Url = url.parse().map_err(|e| format!("{url}: {e}"))?;
|
|
tauri::WebviewWindowBuilder::new(&app, &label, tauri::WebviewUrl::External(parsed))
|
|
.title(format!("Trilium — {}", crate::domain::project::display_name_in(&Paths::real(), &project)?))
|
|
.inner_size(1100.0, 780.0)
|
|
.build()
|
|
.map_err(|e| e.to_string())?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Wählt das Modul ab und stoppt die Projekt-Instanz. Daten und Token
|
|
/// bleiben — erneutes Aktivieren verbindet sich wieder.
|
|
#[tauri::command]
|
|
pub(crate) async fn trilium_disable(project: String) -> Result<(), String> {
|
|
crate::domain::modules::set_module_in(&Paths::real(), &project, "trilium", false)?;
|
|
crate::domain::trilium::stop_instance(&project);
|
|
Ok(())
|
|
}
|
|
|
|
|