release: 1.1.0
Release / Cria a release (push) Canceled after 0s
Release / CLI (linux-aarch64) (push) Canceled after 0s
Release / CLI (linux-x86_64) (push) Canceled after 0s
Release / CLI (macos-aarch64) (push) Canceled after 0s
Release / CLI (macos-x86_64) (push) Canceled after 0s
Release / CLI (windows-x86_64) (push) Canceled after 0s
Release / App desktop (linux-x86_64) (push) Canceled after 0s
Release / App desktop (macos-aarch64) (push) Canceled after 0s
Release / App desktop (macos-x86_64) (push) Canceled after 0s
Release / App desktop (windows-x86_64) (push) Canceled after 0s
Release / Publica a release (push) Canceled after 0s

This commit is contained in:
Lucas Rayan committed 2026-10-20 13:36:18 -03:00
1 parent 5e43fa6d20
commit b4a85722a6
28 files changed
+706 -160

No files matched your search

+3 -4
View File
@@ -9,8 +9,7 @@ pub mod filevault;
pub mod luks;
use crate::source::Source;
use aes::cipher::generic_array::GenericArray;
use aes::cipher::{BlockDecrypt, BlockEncrypt, KeyInit};
use aes::cipher::{Block, BlockCipherDecrypt, BlockCipherEncrypt, KeyInit};
use aes::{Aes128, Aes192, Aes256};
use std::io;
use std::sync::Arc;
@@ -33,7 +32,7 @@ impl AesAny {
})
}
pub fn encrypt(&self, b: &mut [u8; 16]) {
let g = GenericArray::from_mut_slice(b);
let g: &mut Block<Aes128> = (&mut b[..]).try_into().expect("bloco AES de 16 bytes");
match self {
AesAny::A128(c) => c.encrypt_block(g),
AesAny::A192(c) => c.encrypt_block(g),
@@ -41,7 +40,7 @@ impl AesAny {
}
}
pub fn decrypt(&self, b: &mut [u8; 16]) {
let g = GenericArray::from_mut_slice(b);
let g: &mut Block<Aes128> = (&mut b[..]).try_into().expect("bloco AES de 16 bytes");
match self {
AesAny::A128(c) => c.decrypt_block(g),
AesAny::A192(c) => c.decrypt_block(g),
+28 -18
View File
@@ -444,11 +444,11 @@ fn exif_epoch(s: &str) -> Option<i64> {
Some(crate::fs::days_from_civil(y, mo, d) * 86400 + h * 3600 + mi * 60 + se)
}
/// "2023:07:14 18:22:05" -> "14/07/2023 18:22:05"
/// "2023:07:14 18:22:05" -> "2023-07-14T18:22:05" (a interface formata por idioma)
fn exif_date(s: &str) -> String {
let b = s.as_bytes();
if b.len() >= 19 && b[4] == b':' && b[7] == b':' {
format!("{}/{}/{} {}", &s[8..10], &s[5..7], &s[0..4], &s[11..19])
format!("{}-{}-{}T{}", &s[0..4], &s[5..7], &s[8..10], &s[11..19])
} else {
s.to_string()
}
@@ -613,17 +613,17 @@ fn pdf_text(b: &[u8]) -> String {
b.iter().map(|&c| c as char).collect()
}
/// "D:20230714182205-03'00'" -> "14/07/2023 18:22:05"
/// "D:20230714182205-03'00'" -> "2023-07-14T18:22:05" (a interface formata por idioma)
fn pdf_date(s: &str) -> String {
let t = s.trim_start_matches("D:");
let digits: String = t.chars().take_while(|c| c.is_ascii_digit()).collect();
if digits.len() >= 8 {
let g = |a: usize, b: usize| digits.get(a..b).unwrap_or("00");
return format!(
"{}/{}/{} {}:{}:{}",
g(6, 8),
g(4, 6),
"{}-{}-{}T{}:{}:{}",
g(0, 4),
g(4, 6),
g(6, 8),
g(8, 10),
g(10, 12),
g(12, 14)
@@ -733,21 +733,18 @@ fn unescape(s: &str) -> String {
.replace("&amp;", "&")
}
/// "2023-07-14T18:22:05Z" -> "14/07/2023 18:22:05 (UTC)"
/// Datas ISO 8601 ficam como estão; a interface as formata no idioma escolhido.
fn iso_date(s: &str) -> String {
let b = s.as_bytes();
if b.len() >= 19 && b[4] == b'-' && b[10] == b'T' {
let utc = if s.ends_with('Z') { " (UTC)" } else { "" };
return format!(
"{}/{}/{} {}{}",
&s[8..10],
&s[5..7],
&s[0..4],
&s[11..19],
utc
);
let mut out = s[..19].to_string();
if s.len() == 20 && s.ends_with('Z') {
out.push('Z');
}
out
} else {
s.to_string()
}
s.to_string()
}
fn ooxml(fr: &Fr, m: &mut Meta) {
@@ -890,7 +887,7 @@ fn moov(d: &[u8], m: &mut Meta) {
if created > EPOCH_1904 {
m.add(
"Gravado em (metadado)",
fmt_unix((created - EPOCH_1904) as i64),
iso_unix((created - EPOCH_1904) as i64),
);
}
if ts > 0 && dur > 0 {
@@ -919,6 +916,19 @@ fn moov(d: &[u8], m: &mut Meta) {
}
}
/// Instante em ISO 8601 UTC ("2023-07-14T18:22:05Z"), para a interface formatar.
pub fn iso_unix(t: i64) -> String {
let days = t.div_euclid(86400);
let rem = t.rem_euclid(86400);
let (y, mo, d) = civil_from_days(days);
format!(
"{y:04}-{mo:02}-{d:02}T{:02}:{:02}:{:02}Z",
rem / 3600,
rem % 3600 / 60,
rem % 60
)
}
pub fn fmt_unix(t: i64) -> String {
let days = t.div_euclid(86400);
let rem = t.rem_euclid(86400);
+52 -2
View File
@@ -3,9 +3,59 @@
use std::process::{Command, Stdio};
/// Avisos que o núcleo envia ao sistema operacional. O texto é montado no
/// idioma efetivo da interface (`settings::ui_lang`), porque notificações não
/// passam pelo dicionário do DOM.
pub enum Notice {
ScanDone { count: usize, target: String },
RecoverDone { count: usize, destination: String },
DiskLost,
}
impl Notice {
fn text(&self, lang: &str) -> (String, String) {
let en = lang == "en";
match self {
Notice::ScanDone { count, target } => (
"Guará: scan complete".into(),
if en {
format!("{count} file(s) found in {target}.")
} else {
format!("{count} arquivo(s) encontrados em {target}.")
},
),
Notice::RecoverDone { count, destination } => (
if en {
"Guará: recovery complete".into()
} else {
"Guará: recuperação concluída".into()
},
if en {
format!("{count} file(s) saved to {destination}")
} else {
format!("{count} arquivo(s) salvos em {destination}")
},
),
Notice::DiskLost => (
if en {
"Guará: disk disconnected?".into()
} else {
"Guará: disco desconectado?".into()
},
if en {
"The scan was paused without losing anything. Reconnect the disk and continue."
.into()
} else {
"A varredura foi pausada sem perder nada. Reconecte o disco e continue.".into()
},
),
}
}
}
/// Envia uma notificação sem bloquear. Falhas são ignoradas (é só um aviso).
pub fn send(title: &str, body: &str) {
let (title, body) = (title.to_string(), body.to_string());
pub fn send(notice: Notice) {
let (title, body) = notice.text(&crate::settings::ui_lang());
std::thread::spawn(move || {
if let Some(mut c) = command(&title, &body) {
if let Ok(mut child) = c
+9 -17
View File
@@ -531,14 +531,10 @@ impl Engine {
&& s.started.elapsed().as_secs() >= 8
&& crate::settings::load().notify
{
crate::notify::send(
"Guará: recuperação concluída",
&format!(
"{} arquivo(s) salvos em {}",
rep.recovered.len(),
rep.destination
),
);
crate::notify::send(crate::notify::Notice::RecoverDone {
count: rep.recovered.len(),
destination: rep.destination.clone(),
});
}
s.status = if rep.cancelled {
Status::Stopped
@@ -1300,14 +1296,10 @@ fn spawn_scan(job: Arc<Job>, cp: Option<Checkpoint>) {
s.status = Status::Done;
s.checkpoint = None;
if s.started.elapsed().as_secs() >= 20 && crate::settings::load().notify {
crate::notify::send(
"Guará: varredura concluída",
&format!(
"{} arquivo(s) encontrados em {}.",
s.found.len(),
job.target
),
);
crate::notify::send(crate::notify::Notice::ScanDone {
count: s.found.len(),
target: job.target.clone(),
});
}
}
Outcome::Interrupted(cp) => {
@@ -1317,7 +1309,7 @@ fn spawn_scan(job: Arc<Job>, cp: Option<Checkpoint>) {
s.status = Status::Paused;
if cp.io_lost {
if crate::settings::load().notify {
crate::notify::send("Guará: disco desconectado?", "A varredura foi pausada sem perder nada. Reconecte o disco e continue.");
crate::notify::send(crate::notify::Notice::DiskLost);
}
s.warnings.push("A leitura do dispositivo falhou repetidamente: ele provavelmente foi desconectado. A varredura foi pausada sem perder nada. Reconecte o disco e clique em Continuar.".into());
}
+29
View File
@@ -13,6 +13,10 @@ pub struct Settings {
/// Idioma da interface: "auto", "pt" ou "en".
#[serde(default = "auto")]
pub lang: String,
/// Idioma que a interface resolveu de fato ("pt" ou "en"), para as
/// notificações do sistema e a saída do terminal acompanharem a tela.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ui_lang: Option<String>,
/// Notificação do sistema quando uma tarefa longa termina.
#[serde(default = "yes")]
pub notify: bool,
@@ -34,6 +38,7 @@ impl Default for Settings {
Self {
theme: auto(),
lang: auto(),
ui_lang: None,
notify: true,
maps: false,
}
@@ -101,6 +106,30 @@ pub fn load() -> Settings {
.unwrap_or_default()
}
/// Idioma efetivo para textos gerados fora da interface (notificações e CLI).
/// Usa o que a interface informou; sem isso, a preferência e o ambiente.
pub fn ui_lang() -> String {
let s = load();
if let Some(l) = s.ui_lang.as_deref() {
if l == "pt" || l == "en" {
return l.to_string();
}
}
if s.lang == "pt" || s.lang == "en" {
return s.lang;
}
let env = std::env::var("LC_ALL")
.or_else(|_| std::env::var("LC_MESSAGES"))
.or_else(|_| std::env::var("LANG"))
.unwrap_or_default()
.to_ascii_lowercase();
if env.starts_with("pt") {
"pt".into()
} else {
"en".into()
}
}
pub fn save(s: &Settings) -> Result<()> {
crate::owner::write_owned(&file(), serde_json::to_vec_pretty(s)?)?;
Ok(())