Initial commit

This commit is contained in:
2026-03-25 09:32:02 +01:00
commit 23ce1b7ee2
77 changed files with 21169 additions and 0 deletions

View 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))
}

View 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(),
})
})
}

View 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())
}

View 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))
}

View 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;

View 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())
})
}

View 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
}

View 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()))
}

View 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())
}

View 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()
}