diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 80e3328..1380e64 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - + - name: Extract version (Unix/Win) id: ver shell: bash @@ -37,7 +37,7 @@ jobs: VERSION=$(grep '^version' Cargo.toml | head -n1 | cut -d '"' -f2) echo "VERSION=$VERSION" >> "$GITHUB_ENV" echo "version=$VERSION" >> "$GITHUB_OUTPUT" - + - name: Cache cargo / target uses: actions/cache@v4 with: @@ -46,13 +46,20 @@ jobs: ~/.cargo/git target key: ${{ runner.os }}-cargo-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }} - + - name: Install Rust target run: rustup target add ${{ matrix.target }} - - - name: Build - run: cargo build --release --target ${{ matrix.target }} - + + - name: Build (with icon embedding on Windows) + shell: bash + run: | + if [[ "${{ matrix.os }}" == "windows-latest" ]]; then + rustup default stable-x86_64-pc-windows-msvc + cargo build --release --target ${{ matrix.target }} + else + cargo build --release --target ${{ matrix.target }} + fi + - name: Package shell: bash run: | @@ -69,7 +76,7 @@ jobs: tar -czf librius-${VERSION}-${{ matrix.target }}.tar.gz librius-${{ matrix.target }} cp librius-${VERSION}-${{ matrix.target }}.tar.gz $GITHUB_WORKSPACE/release_artifacts/${{ matrix.target }}/ fi - + - name: Upload artifact (lnx/win) uses: actions/upload-artifact@v4 with: @@ -84,7 +91,7 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - + - name: Extract version id: ver shell: bash @@ -92,7 +99,7 @@ jobs: VERSION=$(grep '^version' Cargo.toml | head -n1 | cut -d '"' -f2) echo "VERSION=$VERSION" >> "$GITHUB_ENV" echo "version=$VERSION" >> "$GITHUB_OUTPUT" - + - name: Cache cargo / target uses: actions/cache@v4 with: @@ -101,15 +108,15 @@ jobs: ~/.cargo/git target key: macos-cargo-dual-${{ hashFiles('**/Cargo.lock') }} - + - name: Install Rust targets (x86_64 + aarch64) run: | rustup target add x86_64-apple-darwin rustup target add aarch64-apple-darwin - + - name: Build x86_64 run: cargo build --release --target x86_64-apple-darwin - + - name: Package x86_64 shell: bash run: | @@ -120,10 +127,10 @@ jobs: cp $GITHUB_WORKSPACE/{README.md,LICENSE,CHANGELOG.md} librius-x86_64-apple-darwin/ tar -czf librius-${VERSION}-x86_64-apple-darwin.tar.gz librius-x86_64-apple-darwin cp librius-${VERSION}-x86_64-apple-darwin.tar.gz $GITHUB_WORKSPACE/release_artifacts/x86_64-apple-darwin/ - + - name: Build aarch64 run: cargo build --release --target aarch64-apple-darwin - + - name: Package aarch64 shell: bash run: | @@ -134,7 +141,7 @@ jobs: cp $GITHUB_WORKSPACE/{README.md,LICENSE,CHANGELOG.md} librius-aarch64-apple-darwin/ tar -czf librius-${VERSION}-aarch64-apple-darwin.tar.gz librius-aarch64-apple-darwin cp librius-${VERSION}-aarch64-apple-darwin.tar.gz $GITHUB_WORKSPACE/release_artifacts/aarch64-apple-darwin/ - + - name: Upload artifacts (macOS) uses: actions/upload-artifact@v4 with: @@ -154,13 +161,13 @@ jobs: pattern: release_artifacts-* path: temp_download merge-multiple: true - + - name: Combine into single folder shell: bash run: | mkdir -p release_artifacts find temp_download -type f -exec cp {} release_artifacts/ \; - + - name: Upload consolidated uses: actions/upload-artifact@v4 with: @@ -174,20 +181,20 @@ jobs: steps: - name: Checkout (for Cargo.toml/CHANGELOG) uses: actions/checkout@v4 - + - name: Download consolidated uses: actions/download-artifact@v4 with: name: release_artifacts path: release_artifacts - + - name: Extract version id: extract_version run: | VERSION=$(grep '^version' Cargo.toml | head -n1 | cut -d '"' -f2) echo "version=$VERSION" >> $GITHUB_OUTPUT echo "VERSION=$VERSION" >> $GITHUB_ENV - + - name: Generate SHA256 for all files shell: bash run: | @@ -197,7 +204,7 @@ jobs: [[ -f "$f" ]] || continue sha256sum "$f" > "$f.sha256" || shasum -a 256 "$f" > "$f.sha256" done - + - name: Import GPG key shell: bash run: | @@ -207,7 +214,7 @@ jobs: echo "$GPG_PRIVATE_KEY" | base64 --decode | gpg --batch --import env: GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} - + - name: Sign all artifacts (detach .sig) shell: bash run: | @@ -220,7 +227,7 @@ jobs: done env: GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - + - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..ea567e2 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,39 @@ +# ================================================================ +# Git Submodules configuration for Librius +# ================================================================ +# This file defines the external Git repositories ("submodules") +# that are linked inside this project. +# +# Submodules are used to include external or private components +# (e.g. developer scripts, shared configs, documentation assets) +# without merging their contents directly into the main repo. +# ================================================================ + +# ------------------------------------------------ +# Private developer utilities (not public) +# ------------------------------------------------ +# Path: tools_private/ +# Repository: github.com/umpire274/librius-dev-scripts.git +# Access: private (owner/collaborators only) +# ------------------------------------------------ +[submodule "tools_private"] + path = tools_private + url = git@github.com:umpire274/rust_dev_scripts.git + +# ================================================================ +# Tips: +# - Clone with submodules: +# git clone --recurse-submodules git@github.com:umpire274/librius.git +# +# - If you already cloned: +# git submodule update --init --recursive +# +# - Update the submodule (inside main repo): +# cd tools_private && git pull origin main && cd .. +# git add tools_private +# git commit -m "Update private scripts submodule" +# +# - If you move the submodule folder: +# git mv old_path new_path +# and update the 'path' entry here accordingly. +# ================================================================ diff --git a/Cargo.lock b/Cargo.lock index 3e19e86..ccb410e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1156,7 +1156,7 @@ dependencies = [ [[package]] name = "librius" -version = "0.4.0" +version = "0.4.1" dependencies = [ "chrono", "clap", @@ -1174,6 +1174,7 @@ dependencies = [ "tabled", "tar", "umya-spreadsheet", + "winresource", "zip 6.0.0", ] @@ -1788,6 +1789,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2124,6 +2134,47 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap 2.11.4", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "tower" version = "0.5.2" @@ -2670,6 +2721,25 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] + +[[package]] +name = "winresource" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edcacf11b6f48dd21b9ba002f991bdd5de29b2da8cc2800412f4b80f677e4957" +dependencies = [ + "toml", + "version_check", +] + [[package]] name = "wit-bindgen" version = "0.46.0" diff --git a/Cargo.toml b/Cargo.toml index 8c3c59c..2aef2a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librius" -version = "0.4.0" +version = "0.4.1" edition = "2024" authors = ["Alessandro Maestri "] description = "A personal library manager CLI written in Rust." @@ -52,5 +52,8 @@ zip = "6.0.0" flate2 = "1.1.4" tar = "0.4.44" +[build-dependencies] +winresource = "0.1.23" + [profile.release] opt-level = 3 diff --git a/build.rs b/build.rs index f816630..8417ae0 100644 --- a/build.rs +++ b/build.rs @@ -1,43 +1,18 @@ -#[cfg(windows)] -use std::path::Path; -#[cfg(windows)] -use std::process::Command; - +#[cfg(target_os = "windows")] fn main() { - #[cfg(windows)] - { - let rc_path = Path::new("res/librius.rc"); - - // Rebuild only if the .rc file changes - println!("cargo:rerun-if-changed=res/librius.rc"); - - if rc_path.exists() { - // Compile the .rc file into a .res file using windres - // For MSVC toolchains, ensure MinGW is available in PATH - let result = Command::new("windres") - .env("PATH", "X:\\mingw64\\bin") // Adjust path if MinGW is installed elsewhere - .args(["res/librius.rc", "-O", "coff", "-o", "res/librius.res"]) - .status(); + use winresource::WindowsResource; - match result { - Ok(status) if status.success() => { - // Link the compiled resource file into the final executable - println!("cargo:rustc-link-arg=res/librius.res"); - } - Ok(status) => { - println!( - "cargo:warning=windres exited with non-zero status: {:?}", - status.code() - ); - } - Err(_) => { - println!("cargo:warning=windres not found or failed to execute."); - } - } - } else { - println!("cargo:warning=res/librius.rc not found, skipping icon embedding."); - } - } - - // Non-Windows platforms: no resource embedding required. + // Assicurati che res/librius.ico esista + let mut res = WindowsResource::new(); + res.set_icon("res/librius.ico") + .set("FileDescription", "Librius CLI") + .set("ProductName", "Librius") + .set("OriginalFilename", "librius.exe") + .set("FileVersion", env!("CARGO_PKG_VERSION")) + .set("ProductVersion", env!("CARGO_PKG_VERSION")) + .compile() + .expect("Failed to embed icon resource"); } + +#[cfg(not(target_os = "windows"))] +fn main() {} diff --git a/src/cli.rs b/src/cli.rs index e04ee5d..5cac9b9 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -272,6 +272,28 @@ pub fn build_cli() -> Command { cmd }), ) + .subcommand( + Command::new("del") + .about(tr("help.del.about")) + .display_order(90) + .arg( + Arg::new("key") + .help(tr("help.del.key")) // e.g. "Book ID or ISBN to delete" + .required(true) + .value_name("ID|ISBN") + .num_args(1) + .display_order(91), + ) + .arg( + Arg::new("force") + .long("force") + .short('f') + .help(tr("help.del.force")) // e.g. "Force deletion without confirmation" + .action(ArgAction::SetTrue) + .num_args(0) + .display_order(92), + ), + ) .subcommand( Command::new("help") .about(tr_s("help_flag_about")) @@ -320,6 +342,12 @@ pub fn run_cli( handle_edit_book(conn, book_m)?; // ✅ integrazione comando edit book } Ok(()) + } else if let Some(("del", sub_m)) = matches.subcommand() { + if let Some(key) = sub_m.get_one::("key") { + let force = sub_m.get_flag("force"); + crate::commands::handle_del_book(conn, key, force)?; + } + Ok(()) } else if let Some(("backup", sub_m)) = matches.subcommand() { let compress = sub_m.get_flag("compress"); crate::commands::handle_backup(conn, compress)?; diff --git a/src/commands/del_book.rs b/src/commands/del_book.rs new file mode 100644 index 0000000..8f079f7 --- /dev/null +++ b/src/commands/del_book.rs @@ -0,0 +1,75 @@ +use crate::{print_err, print_info, print_ok, print_warn, tr_with, write_log}; +use colored::*; +use rusqlite::Connection; +use std::io::{self, Write}; + +pub fn handle_del_book(conn: &Connection, key: &str, force: bool) -> rusqlite::Result<()> { + println!(); + + // 1️⃣ Determina se è ISBN o ID + let is_isbn = key.len() >= 10 && !key.chars().all(|c| c.is_ascii_digit()); + + // 2️⃣ Controlla se il libro esiste + let exists_query = if is_isbn { + "SELECT COUNT(*) FROM books WHERE isbn = ?1" + } else { + "SELECT COUNT(*) FROM books WHERE id = ?1" + }; + + let exists: i64 = conn.query_row(exists_query, [key], |row| row.get(0))?; + + if exists == 0 { + print_warn(&tr_with("del.book.not_found", &[("key", key)]).yellow()); + return Ok(()); + } + + // 3️⃣ Conferma interattiva (solo se il libro esiste), se non forzato + if !force { + print!( + "{} ", + tr_with("del.book.confirm", &[("key", key)]) // es. "Are you sure you want to delete book {key}? [y/N]:" + ); + io::stdout().flush().unwrap(); + + let mut answer = String::new(); + io::stdin().read_line(&mut answer).unwrap(); + + if !matches!(answer.trim().to_lowercase().as_str(), "y" | "yes") { + print_info(&tr_with("del.book.cancelled", &[("key", key)]).cyan(), true); + return Ok(()); + } + + println!(); + } + + // 4️⃣ Esegue la DELETE solo dopo conferma + let delete_sql = if is_isbn { + "DELETE FROM books WHERE isbn = ?1" + } else { + "DELETE FROM books WHERE id = ?1" + }; + + let affected = conn.execute(delete_sql, [key])?; + + if affected > 0 { + // Log the action + let action_type = if force { "forced" } else { "confirmed" }; + let log_msg = format!("Book {} deleted ({})", key, action_type); + if let Err(e) = write_log(conn, "DELETE_BOOK", "", &log_msg) { + print_err( + &tr_with( + "log.record.unable_to_write", + &[("log_error", &e.to_string())], + ) + .red() + .bold(), + ); + } + + print_ok(&tr_with("del.book.success", &[("key", key)]).green(), true); + } else { + print_err(&tr_with("del.book.not_found", &[("key", key)]).red().bold()); + } + + Ok(()) +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 7a3aefc..77684e0 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -8,6 +8,7 @@ pub mod add; pub mod add_book; pub mod backup; pub mod config; +pub mod del_book; pub mod edit_book; pub mod export; pub mod import; @@ -17,6 +18,7 @@ pub use add::handle_add; pub use add_book::handle_add_book; pub use backup::handle_backup; pub use config::handle_config; +pub use del_book::handle_del_book; pub use edit_book::handle_edit_book; pub use export::handle_export_csv; pub use export::handle_export_json; diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 2d7079c..f25ff33 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -160,5 +160,13 @@ "edit.book.not_found": "No matching book found.", "edit.book.error_updating": "Error updating book: {error}", "edit.book.error_invalid_id": "Invalid ID format.", - "edit.book.error_no_field": "No fields specified to update." + "edit.book.error_no_field": "No fields specified to update.", + "help.del.about": "Delete a book by its ID or ISBN.", + "help.del.key": "Specify the book ID or ISBN to delete.", + "del.book.success": "Book {key} deleted successfully.", + "del.book.not_found": "No matching book found for ID|ISBN {key}.", + "del.book.confirm": "Are you sure you want to delete book {key}? [y/N]:", + "del.book.cancelled": "Deletion of book {key} cancelled.", + "help.del.force": "Delete without asking for confirmation.", + "log.record.unable_to_write": "Unable to record log entry: {log_error}", } diff --git a/src/i18n/locales/it.json b/src/i18n/locales/it.json index 14da2f1..083b462 100644 --- a/src/i18n/locales/it.json +++ b/src/i18n/locales/it.json @@ -160,5 +160,13 @@ "edit.book.not_found": "Nessun libro corrispondente trovato.", "edit.book.error_updating": "Errore durante l'aggiornamento del libro: {error}", "edit.book.error_invalid_id": "Formato ID non valido.", - "edit.book.error_no_field": "Nessun campo specificato da aggiornare." + "edit.book.error_no_field": "Nessun campo specificato da aggiornare.", + "help.del.about": "Elimina un libro tramite ID o ISBN.", + "help.del.key": "Specifica l'ID o l'ISBN del libro da eliminare.", + "del.book.success": "Libro {key} eliminato correttamente.", + "del.book.not_found": "Nessun libro trovato per ID|ISBN {key}.", + "del.book.confirm": "Sei sicuro di voler eliminare il libro {key}? [y/N]:", + "del.book.cancelled": "Eliminazione del libro {key} annullata.", + "help.del.force": "Elimina senza chiedere conferma.", + "log.record.unable_to_write": "Impossibile scrivere il log: {log_error}" } diff --git a/tools/check_submodule.ps1 b/tools/check_submodule.ps1 new file mode 100644 index 0000000..c5eb90c --- /dev/null +++ b/tools/check_submodule.ps1 @@ -0,0 +1,45 @@ +# ============================================================ +# Librius - Submodule Check Script (PowerShell) +# ============================================================ +# Verifica lo stato del submodule tools_private +# e controlla se è sincronizzato con l'ultimo commit remoto. +# ============================================================ + +$submodulePath = "tools_private" +$remoteUrl = "git@github.com:umpire274/rust_dev_scripts.git" +$branch = "main" + +Write-Host "🔍 Checking submodule status for '$submodulePath'..." -ForegroundColor Cyan + +if (-not (Test-Path "$submodulePath/.git")) +{ + Write-Host "❌ Submodule not initialized. Run:" -ForegroundColor Red + Write-Host " git submodule update --init --recursive" + exit 1 +} + +Set-Location $submodulePath + +# Get local HEAD commit +$localCommit = git rev-parse HEAD +# Fetch latest remote +git fetch origin $branch --quiet +$remoteCommit = git rev-parse origin/$branch + +Set-Location .. + +if ($localCommit -eq $remoteCommit) +{ + Write-Host "✅ Submodule '$submodulePath' is up to date with origin/$branch." -ForegroundColor Green + exit 0 +} +else +{ + Write-Host "⚠️ Submodule '$submodulePath' is out of sync!" -ForegroundColor Yellow + Write-Host " Local : $localCommit" + Write-Host " Remote: $remoteCommit" + Write-Host "" + Write-Host "👉 To update, run:" -ForegroundColor Cyan + Write-Host " cd $submodulePath; git pull origin $branch; cd .." + exit 2 +} diff --git a/tools/check_submodule.sh b/tools/check_submodule.sh new file mode 100644 index 0000000..96d73f5 --- /dev/null +++ b/tools/check_submodule.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# ============================================================ +# Librius - Submodule Check Script (Bash) +# ============================================================ +# Verifica lo stato del submodule tools_private +# e controlla se è aggiornato al branch remoto. +# ============================================================ + +set -e + +SUBMODULE_PATH="tools_private" +REMOTE_URL="git@github.com:umpire274/rust_dev_scripts.git" +BRANCH="main" + +echo "🔍 Checking submodule status for '$SUBMODULE_PATH'..." + +if [ ! -d "$SUBMODULE_PATH/.git" ]; then + echo "❌ Submodule not initialized." + echo " Run: git submodule update --init --recursive" + exit 1 +fi + +cd "$SUBMODULE_PATH" + +LOCAL_COMMIT=$(git rev-parse HEAD) +git fetch origin "$BRANCH" --quiet +REMOTE_COMMIT=$(git rev-parse origin/"$BRANCH") + +cd .. + +if [ "$LOCAL_COMMIT" = "$REMOTE_COMMIT" ]; then + echo "✅ Submodule '$SUBMODULE_PATH' is up to date with origin/$BRANCH." + exit 0 +else + echo "⚠️ Submodule '$SUBMODULE_PATH' is out of sync!" + echo " Local : $LOCAL_COMMIT" + echo " Remote: $REMOTE_COMMIT" + echo "" + echo "👉 To update, run:" + echo " cd $SUBMODULE_PATH && git pull origin $BRANCH && cd .." + exit 2 +fi diff --git a/tools_private b/tools_private new file mode 160000 index 0000000..e388cb0 --- /dev/null +++ b/tools_private @@ -0,0 +1 @@ +Subproject commit e388cb058f9baac622e2255433ee7984eea734d8