Initial commit
This commit is contained in:
41
src-tauri/src/commands/annotation.rs
Normal file
41
src-tauri/src/commands/annotation.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
//! Tauri commands for PDF annotation CRUD.
|
||||
|
||||
use crate::state::AppState;
|
||||
use brittle_core::{Annotation, AnnotationId, AnnotationType, ReferenceId};
|
||||
use tauri::State;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn create_annotation(
|
||||
state: State<AppState>,
|
||||
reference_id: ReferenceId,
|
||||
page: u32,
|
||||
annotation_type: AnnotationType,
|
||||
content: Option<String>,
|
||||
) -> Result<Annotation, String> {
|
||||
state.with_repo(|b| b.create_annotation(reference_id, page, annotation_type, content))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_annotations(
|
||||
state: State<AppState>,
|
||||
reference_id: ReferenceId,
|
||||
) -> Result<Vec<Annotation>, String> {
|
||||
state.with_repo_read(|b| b.get_annotations(reference_id))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn update_annotation(
|
||||
state: State<AppState>,
|
||||
annotation: Annotation,
|
||||
) -> Result<Annotation, String> {
|
||||
state.with_repo(|b| b.update_annotation(annotation))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn delete_annotation(
|
||||
state: State<AppState>,
|
||||
reference_id: ReferenceId,
|
||||
annotation_id: AnnotationId,
|
||||
) -> Result<(), String> {
|
||||
state.with_repo(|b| b.delete_annotation(reference_id, annotation_id))
|
||||
}
|
||||
44
src-tauri/src/commands/bibtex.rs
Normal file
44
src-tauri/src/commands/bibtex.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
//! Tauri commands for BibTeX export.
|
||||
|
||||
use crate::state::AppState;
|
||||
use brittle_core::{LibraryId, ReferenceId};
|
||||
use serde::Serialize;
|
||||
use tauri::State;
|
||||
|
||||
/// Result of a BibTeX export: the formatted string plus any non-fatal errors.
|
||||
#[derive(Serialize)]
|
||||
pub struct BibtexExportResult {
|
||||
pub bibtex: String,
|
||||
/// Warnings for references that were skipped due to missing required fields.
|
||||
pub errors: Vec<String>,
|
||||
}
|
||||
|
||||
/// Export a list of references as BibTeX.
|
||||
#[tauri::command]
|
||||
pub fn export_bibtex(
|
||||
state: State<AppState>,
|
||||
reference_ids: Vec<ReferenceId>,
|
||||
) -> Result<BibtexExportResult, String> {
|
||||
state.with_repo_read(|b| {
|
||||
let (bibtex, errors) = b.export_bibtex(&reference_ids)?;
|
||||
Ok(BibtexExportResult {
|
||||
bibtex,
|
||||
errors: errors.iter().map(|e| e.to_string()).collect(),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Export all references in a library as BibTeX.
|
||||
#[tauri::command]
|
||||
pub fn export_library_bibtex(
|
||||
state: State<AppState>,
|
||||
library_id: LibraryId,
|
||||
) -> Result<BibtexExportResult, String> {
|
||||
state.with_repo_read(|b| {
|
||||
let (bibtex, errors) = b.export_library_bibtex(library_id)?;
|
||||
Ok(BibtexExportResult {
|
||||
bibtex,
|
||||
errors: errors.iter().map(|e| e.to_string()).collect(),
|
||||
})
|
||||
})
|
||||
}
|
||||
73
src-tauri/src/commands/config.rs
Normal file
73
src-tauri/src/commands/config.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
//! Tauri commands for reading and writing configuration.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use crate::config::{GlobalConfig, ProjectConfig};
|
||||
|
||||
/// Load the global config from `~/.config/brittle/config.toml`.
|
||||
/// Returns the default config if the file does not yet exist.
|
||||
#[tauri::command]
|
||||
pub fn load_global_config() -> Result<GlobalConfig, String> {
|
||||
GlobalConfig::load().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Persist the global config to `~/.config/brittle/config.toml`.
|
||||
#[tauri::command]
|
||||
pub fn save_global_config(config: GlobalConfig) -> Result<(), String> {
|
||||
config.save().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Load the per-project config from `{repo_path}/.brittle/config.toml`.
|
||||
/// Returns the default config if the file does not yet exist.
|
||||
#[tauri::command]
|
||||
pub fn load_project_config(repo_path: String) -> Result<ProjectConfig, String> {
|
||||
ProjectConfig::load(Path::new(&repo_path)).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Persist the per-project config to `{repo_path}/.brittle/config.toml`.
|
||||
#[tauri::command]
|
||||
pub fn save_project_config(repo_path: String, config: ProjectConfig) -> Result<(), String> {
|
||||
config
|
||||
.save(Path::new(&repo_path))
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Return the current theme name (`"dark"` or `"light"`) from the global config.
|
||||
#[tauri::command]
|
||||
pub fn get_theme() -> Result<String, String> {
|
||||
use crate::config::Theme;
|
||||
GlobalConfig::load()
|
||||
.map(|c| match c.appearance.theme {
|
||||
Theme::Dark => "dark".to_string(),
|
||||
Theme::Light => "light".to_string(),
|
||||
})
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Persist a new theme choice to the global config.
|
||||
///
|
||||
/// `theme` must be `"dark"` or `"light"`.
|
||||
#[tauri::command]
|
||||
pub fn set_theme(theme: String) -> Result<(), String> {
|
||||
use crate::config::Theme;
|
||||
let parsed = match theme.as_str() {
|
||||
"dark" => Theme::Dark,
|
||||
"light" => Theme::Light,
|
||||
other => return Err(format!("unknown theme '{other}'")),
|
||||
};
|
||||
let mut config = GlobalConfig::load().map_err(|e| e.to_string())?;
|
||||
config.appearance.theme = parsed;
|
||||
config.save().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Return the user's keybinding overrides from the global config.
|
||||
///
|
||||
/// Map keys are action names in snake_case (e.g. `"tab_next"`); values are
|
||||
/// key-sequence strings (e.g. `"<C-Right>"`). Returns an empty map if no
|
||||
/// config file exists or the `[keybindings]` section is absent.
|
||||
#[tauri::command]
|
||||
pub fn get_keybindings() -> Result<std::collections::HashMap<String, String>, String> {
|
||||
GlobalConfig::load()
|
||||
.map(|c| c.keybindings.0)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
97
src-tauri/src/commands/library.rs
Normal file
97
src-tauri/src/commands/library.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
//! Tauri commands for library CRUD, hierarchy, and membership.
|
||||
|
||||
use crate::state::AppState;
|
||||
use brittle_core::{Library, LibraryId, ReferenceId};
|
||||
use tauri::State;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn create_library(
|
||||
state: State<AppState>,
|
||||
name: String,
|
||||
parent_id: Option<LibraryId>,
|
||||
) -> Result<Library, String> {
|
||||
state.with_repo(|b| b.create_library(name, parent_id))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_library(state: State<AppState>, id: LibraryId) -> Result<Library, String> {
|
||||
state.with_repo_read(|b| b.get_library(id))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn rename_library(
|
||||
state: State<AppState>,
|
||||
id: LibraryId,
|
||||
new_name: String,
|
||||
) -> Result<Library, String> {
|
||||
state.with_repo(|b| b.rename_library(id, new_name))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn move_library(
|
||||
state: State<AppState>,
|
||||
id: LibraryId,
|
||||
new_parent: Option<LibraryId>,
|
||||
) -> Result<Library, String> {
|
||||
state.with_repo(|b| b.move_library(id, new_parent))
|
||||
}
|
||||
|
||||
/// Delete a library. Fails if it has child libraries.
|
||||
#[tauri::command]
|
||||
pub fn delete_library(state: State<AppState>, id: LibraryId) -> Result<(), String> {
|
||||
state.with_repo(|b| b.delete_library(id))
|
||||
}
|
||||
|
||||
/// Delete a library and all its descendants (recursive).
|
||||
#[tauri::command]
|
||||
pub fn force_delete_library(state: State<AppState>, id: LibraryId) -> Result<(), String> {
|
||||
state.with_repo(|b| b.force_delete_library(id))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn list_root_libraries(state: State<AppState>) -> Result<Vec<Library>, String> {
|
||||
state.with_repo_read(|b| b.list_root_libraries())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn list_child_libraries(
|
||||
state: State<AppState>,
|
||||
parent_id: LibraryId,
|
||||
) -> Result<Vec<Library>, String> {
|
||||
state.with_repo_read(|b| b.list_child_libraries(parent_id))
|
||||
}
|
||||
|
||||
/// Return the ancestor chain of a library, ordered root → direct parent.
|
||||
#[tauri::command]
|
||||
pub fn get_library_ancestors(
|
||||
state: State<AppState>,
|
||||
id: LibraryId,
|
||||
) -> Result<Vec<Library>, String> {
|
||||
state.with_repo_read(|b| b.get_library_ancestors(id))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn add_to_library(
|
||||
state: State<AppState>,
|
||||
library_id: LibraryId,
|
||||
reference_id: ReferenceId,
|
||||
) -> Result<(), String> {
|
||||
state.with_repo(|b| b.add_to_library(library_id, reference_id))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn remove_from_library(
|
||||
state: State<AppState>,
|
||||
library_id: LibraryId,
|
||||
reference_id: ReferenceId,
|
||||
) -> Result<(), String> {
|
||||
state.with_repo(|b| b.remove_from_library(library_id, reference_id))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn list_reference_libraries(
|
||||
state: State<AppState>,
|
||||
reference_id: ReferenceId,
|
||||
) -> Result<Vec<Library>, String> {
|
||||
state.with_repo_read(|b| b.list_reference_libraries(reference_id))
|
||||
}
|
||||
9
src-tauri/src/commands/mod.rs
Normal file
9
src-tauri/src/commands/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
pub mod annotation;
|
||||
pub mod bibtex;
|
||||
pub mod config;
|
||||
pub mod library;
|
||||
pub mod pdf;
|
||||
pub mod reference;
|
||||
pub mod repository;
|
||||
pub mod snapshot;
|
||||
pub mod window;
|
||||
31
src-tauri/src/commands/pdf.rs
Normal file
31
src-tauri/src/commands/pdf.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
//! Tauri commands for PDF attachment and retrieval.
|
||||
|
||||
use crate::state::AppState;
|
||||
use brittle_core::{PdfAttachment, ReferenceId};
|
||||
use std::path::PathBuf;
|
||||
use tauri::State;
|
||||
|
||||
/// Attach a PDF file to a reference by copying it into the repository.
|
||||
///
|
||||
/// `source_path` is the absolute path to the file to copy.
|
||||
/// Returns the stored attachment metadata.
|
||||
#[tauri::command]
|
||||
pub fn attach_pdf(
|
||||
state: State<AppState>,
|
||||
reference_id: ReferenceId,
|
||||
source_path: String,
|
||||
) -> Result<PdfAttachment, String> {
|
||||
let path = PathBuf::from(&source_path);
|
||||
state.with_repo(|b| b.attach_pdf(reference_id, &path))
|
||||
}
|
||||
|
||||
/// Return the absolute filesystem path to the PDF attached to a reference.
|
||||
///
|
||||
/// Returns an error if no PDF is attached.
|
||||
#[tauri::command]
|
||||
pub fn get_pdf_path(state: State<AppState>, reference_id: ReferenceId) -> Result<String, String> {
|
||||
state.with_repo_read(|b| {
|
||||
b.get_pdf_path(reference_id)
|
||||
.map(|p| p.to_string_lossy().into_owned())
|
||||
})
|
||||
}
|
||||
87
src-tauri/src/commands/reference.rs
Normal file
87
src-tauri/src/commands/reference.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
//! Tauri commands for reference CRUD and search.
|
||||
|
||||
use crate::state::AppState;
|
||||
use brittle_core::{EntryType, LibraryId, Reference, ReferenceId, ReferenceSummary};
|
||||
use tauri::State;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn create_reference(
|
||||
state: State<AppState>,
|
||||
cite_key: String,
|
||||
entry_type: EntryType,
|
||||
) -> Result<Reference, String> {
|
||||
state.with_repo(|b| b.create_reference(cite_key, entry_type))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_reference(state: State<AppState>, id: ReferenceId) -> Result<Reference, String> {
|
||||
state.with_repo_read(|b| b.get_reference(id))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn update_reference(state: State<AppState>, reference: Reference) -> Result<Reference, String> {
|
||||
state.with_repo(|b| b.update_reference(reference))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn delete_reference(state: State<AppState>, id: ReferenceId) -> Result<(), String> {
|
||||
state.with_repo(|b| b.delete_reference(id))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn list_references(state: State<AppState>) -> Result<Vec<ReferenceSummary>, String> {
|
||||
state.with_repo_read(|b| b.list_references())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn set_field(
|
||||
state: State<AppState>,
|
||||
id: ReferenceId,
|
||||
field: String,
|
||||
value: String,
|
||||
) -> Result<(), String> {
|
||||
state.with_repo(|b| b.set_field(id, &field, value))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn remove_field(state: State<AppState>, id: ReferenceId, field: String) -> Result<(), String> {
|
||||
state.with_repo(|b| b.remove_field(id, &field))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn search_references(
|
||||
state: State<AppState>,
|
||||
query: String,
|
||||
) -> Result<Vec<ReferenceSummary>, String> {
|
||||
state.with_repo_read(|b| b.search_references(&query))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn search_library_references(
|
||||
state: State<AppState>,
|
||||
library_id: LibraryId,
|
||||
query: String,
|
||||
) -> Result<Vec<ReferenceSummary>, String> {
|
||||
state.with_repo_read(|b| b.search_library_references(library_id, &query))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn list_library_references(
|
||||
state: State<AppState>,
|
||||
library_id: LibraryId,
|
||||
) -> Result<Vec<ReferenceSummary>, String> {
|
||||
state.with_repo_read(|b| b.list_library_references(library_id))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn list_library_references_recursive(
|
||||
state: State<AppState>,
|
||||
library_id: LibraryId,
|
||||
) -> Result<Vec<ReferenceSummary>, String> {
|
||||
let result = state.with_repo_read(|b| b.list_library_references_recursive(library_id));
|
||||
match &result {
|
||||
Ok(refs) => eprintln!("[brittle] list_library_references_recursive({library_id}): {} refs", refs.len()),
|
||||
Err(e) => eprintln!("[brittle] list_library_references_recursive({library_id}): ERROR: {e}"),
|
||||
}
|
||||
result
|
||||
}
|
||||
60
src-tauri/src/commands/repository.rs
Normal file
60
src-tauri/src/commands/repository.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
//! Tauri commands for repository lifecycle (create, open, close).
|
||||
|
||||
use crate::state::AppState;
|
||||
use brittle_core::Brittle;
|
||||
use std::path::PathBuf;
|
||||
use tauri::State;
|
||||
|
||||
fn expand_tilde(path: &str) -> PathBuf {
|
||||
if let Some(rest) = path.strip_prefix("~/") {
|
||||
if let Ok(home) = std::env::var("HOME") {
|
||||
return PathBuf::from(home).join(rest);
|
||||
}
|
||||
}
|
||||
if path == "~" {
|
||||
if let Ok(home) = std::env::var("HOME") {
|
||||
return PathBuf::from(home);
|
||||
}
|
||||
}
|
||||
PathBuf::from(path)
|
||||
}
|
||||
|
||||
/// Create a new Brittle repository at `path` and open it.
|
||||
#[tauri::command]
|
||||
pub fn create_repository(state: State<AppState>, path: String) -> Result<(), String> {
|
||||
let path = expand_tilde(&path);
|
||||
let brittle = Brittle::create(&path).map_err(|e| e.to_string())?;
|
||||
*state
|
||||
.brittle
|
||||
.lock()
|
||||
.map_err(|_| "lock poisoned".to_string())? = Some(brittle);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Open an existing Brittle repository at `path`.
|
||||
#[tauri::command]
|
||||
pub fn open_repository(state: State<AppState>, path: String) -> Result<(), String> {
|
||||
let path = expand_tilde(&path);
|
||||
let brittle = Brittle::open(&path).map_err(|e| e.to_string())?;
|
||||
*state
|
||||
.brittle
|
||||
.lock()
|
||||
.map_err(|_| "lock poisoned".to_string())? = Some(brittle);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Close the currently open repository. No-op if none is open.
|
||||
#[tauri::command]
|
||||
pub fn close_repository(state: State<AppState>) -> Result<(), String> {
|
||||
*state
|
||||
.brittle
|
||||
.lock()
|
||||
.map_err(|_| "lock poisoned".to_string())? = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Return the filesystem path of the currently open repository.
|
||||
#[tauri::command]
|
||||
pub fn repository_root(state: State<AppState>) -> Result<String, String> {
|
||||
state.with_repo_read(|b| Ok(b.repository_root().to_string_lossy().into_owned()))
|
||||
}
|
||||
33
src-tauri/src/commands/snapshot.rs
Normal file
33
src-tauri/src/commands/snapshot.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
//! Tauri commands for snapshotting (git-backed history).
|
||||
|
||||
use crate::state::AppState;
|
||||
use brittle_core::Snapshot;
|
||||
use tauri::State;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn create_snapshot(state: State<AppState>, message: String) -> Result<Snapshot, String> {
|
||||
state.with_repo(|b| b.create_snapshot(&message))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn list_snapshots(state: State<AppState>) -> Result<Vec<Snapshot>, String> {
|
||||
state.with_repo_read(|b| b.list_snapshots())
|
||||
}
|
||||
|
||||
/// Restore to a named snapshot. Fails if there are uncommitted changes.
|
||||
/// Use `discard_changes` first if needed.
|
||||
#[tauri::command]
|
||||
pub fn restore_snapshot(state: State<AppState>, snapshot_id: String) -> Result<(), String> {
|
||||
state.with_repo(|b| b.restore_snapshot(&snapshot_id))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn has_uncommitted_changes(state: State<AppState>) -> Result<bool, String> {
|
||||
state.with_repo_read(|b| b.has_uncommitted_changes())
|
||||
}
|
||||
|
||||
/// Discard all uncommitted changes, reverting to the last snapshot.
|
||||
#[tauri::command]
|
||||
pub fn discard_changes(state: State<AppState>) -> Result<(), String> {
|
||||
state.with_repo(|b| b.discard_changes())
|
||||
}
|
||||
54
src-tauri/src/commands/window.rs
Normal file
54
src-tauri/src/commands/window.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
//! Tauri commands for managing PDF viewer windows.
|
||||
|
||||
use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindowBuilder};
|
||||
|
||||
/// Open a PDF viewer window for the given reference ID.
|
||||
///
|
||||
/// If a window for this reference is already open it is focused instead of
|
||||
/// creating a duplicate.
|
||||
///
|
||||
/// The window loads `brittle://app/viewer?ref_id=<ref_id>`.
|
||||
#[tauri::command]
|
||||
pub fn open_pdf_window(app: AppHandle, ref_id: String) -> Result<(), String> {
|
||||
let label = format!("pdf-{}", ref_id);
|
||||
|
||||
// If already open, just focus it.
|
||||
if let Some(win) = app.get_webview_window(&label) {
|
||||
win.set_focus().map_err(|e| e.to_string())?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let url_str = format!(
|
||||
"brittle://app/viewer?ref_id={}",
|
||||
urlencoding::encode(&ref_id)
|
||||
);
|
||||
let url = url_str.parse::<url::Url>().map_err(|e| e.to_string())?;
|
||||
|
||||
WebviewWindowBuilder::new(&app, &label, WebviewUrl::External(url))
|
||||
.title("PDF Viewer — Brittle")
|
||||
.inner_size(900.0, 750.0)
|
||||
.min_inner_size(600.0, 400.0)
|
||||
.build()
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Close the PDF viewer window for the given reference ID, if open.
|
||||
#[tauri::command]
|
||||
pub fn close_pdf_window(app: AppHandle, ref_id: String) -> Result<(), String> {
|
||||
let label = format!("pdf-{}", ref_id);
|
||||
if let Some(win) = app.get_webview_window(&label) {
|
||||
win.close().map_err(|e| e.to_string())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Return the labels of all currently open PDF viewer windows.
|
||||
#[tauri::command]
|
||||
pub fn list_pdf_windows(app: AppHandle) -> Vec<String> {
|
||||
app.webview_windows()
|
||||
.into_keys()
|
||||
.filter(|label| label.starts_with("pdf-"))
|
||||
.collect()
|
||||
}
|
||||
Reference in New Issue
Block a user