Release 1.1.3: command field, unknown signals default, 80ms gap, 5-pair min, export hex uppercase

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
KaraZajac
2026-02-22 20:59:43 -05:00
parent 39944e072b
commit 334491db4e
10 changed files with 237 additions and 37 deletions
+118 -12
View File
@@ -125,13 +125,16 @@ pub enum InputMode {
FobMetaModel,
/// Fob export metadata: editing region field
FobMetaRegion,
/// Fob export metadata: editing command field
FobMetaCommand,
/// Fob export metadata: editing notes field
FobMetaNotes,
/// Capture metadata (Year/Make/Model/Region) for vuln lookup — press i on a capture
/// Capture metadata (Year/Make/Model/Region/Command) for vuln lookup — press i on a capture
CaptureMetaYear,
CaptureMetaMake,
CaptureMetaModel,
CaptureMetaRegion,
CaptureMetaCommand,
/// License overlay (centered box)
License,
/// Credits overlay (centered box)
@@ -357,14 +360,17 @@ pub struct App {
pub fob_meta_model: String,
/// Region input buffer (e.g. NA, EU, APAC, etc.)
pub fob_meta_region: String,
/// Command input buffer (e.g. Unlock, Lock)
pub fob_meta_command: String,
/// Notes input buffer
pub fob_meta_notes: String,
// -- Capture metadata (Year/Make/Model/Region for vuln lookup, set via 'i') --
// -- Capture metadata (Year/Make/Model/Region/Command for vuln lookup, set via 'i') --
pub capture_meta_year: String,
pub capture_meta_make: String,
pub capture_meta_model: String,
pub capture_meta_region: String,
pub capture_meta_command: String,
/// Which capture is being edited (when in CaptureMeta* modes)
pub capture_meta_capture_id: Option<u32>,
@@ -486,11 +492,13 @@ impl App {
fob_meta_make: String::new(),
fob_meta_model: String::new(),
fob_meta_region: String::new(),
fob_meta_command: String::new(),
fob_meta_notes: String::new(),
capture_meta_year: String::new(),
capture_meta_make: String::new(),
capture_meta_model: String::new(),
capture_meta_region: String::new(),
capture_meta_command: String::new(),
capture_meta_capture_id: None,
pending_transmit_queue: Vec::new(),
pending_transmit_restore: None,
@@ -1299,12 +1307,52 @@ impl App {
.unwrap_or_else(|_| path.to_string_lossy().to_string())
}
/// For unknown protocol: Year_Make_Model_Region_Command (user can edit; we append _<8 hex> on save).
/// For known protocol: protocol_serial as before.
fn default_export_filename(capture: &Capture) -> String {
format!(
"{}_{}",
capture.protocol_name().replace(' ', "_").to_lowercase(),
capture.serial_hex()
)
if capture.protocol_name().eq_ignore_ascii_case("unknown") {
let year = capture
.year
.as_deref()
.unwrap_or("Unknown")
.trim()
.replace(' ', "_");
let make = capture
.make
.as_deref()
.unwrap_or("Unknown")
.trim()
.replace(' ', "_");
let model = capture
.model
.as_deref()
.unwrap_or("Unknown")
.trim()
.replace(' ', "_");
let region = capture
.region
.as_deref()
.unwrap_or("Unknown")
.trim()
.replace(' ', "_");
let cmd_str = capture
.command
.as_deref()
.unwrap_or_else(|| capture.button_name())
.trim();
let command = if cmd_str.is_empty() || cmd_str == "-" {
"Unknown".to_string()
} else {
cmd_str.replace(' ', "_")
};
format!("{}_{}_{}_{}_{}", year, make, model, region, command)
} else {
format!(
"{}_{}",
capture.protocol_name().replace(' ', "_").to_lowercase(),
capture.serial_hex()
)
}
}
/// Start .fob export by entering filename input mode
@@ -1314,13 +1362,23 @@ impl App {
return Ok(());
}
// Pre-fill filename from protocol + serial
let default_name = self.captures.iter().find(|c| c.id == id)
// Pre-fill filename from protocol + serial; for unknown captures include 8-hex suffix so user sees it
let capture = self.captures.iter().find(|c| c.id == id);
let default_name = capture
.map(|c| Self::default_export_filename(c))
.unwrap_or_else(|| format!("capture_{}", id));
self.export_filename = if capture.map(|c| c.protocol.is_none()).unwrap_or(false) {
let suffix_nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos() as u64;
let suffix = (suffix_nanos.wrapping_add(id as u64 * 2654435761) % 0x100_000_000) as u32;
format!("{}_{:08X}", default_name, suffix)
} else {
default_name.clone()
};
// Pre-fill metadata from capture if set, otherwise make from protocol
let capture = self.captures.iter().find(|c| c.id == id);
let make = capture
.and_then(|c| c.make.as_ref().map(String::clone))
.filter(|s| !s.is_empty())
@@ -1330,7 +1388,6 @@ impl App {
.unwrap_or_default()
});
self.export_capture_id = Some(id);
self.export_filename = default_name;
self.export_format = Some(ExportFormat::Fob);
self.fob_meta_year = capture
.and_then(|c| c.year.as_ref())
@@ -1345,6 +1402,16 @@ impl App {
.and_then(|c| c.region.as_ref())
.map(String::clone)
.unwrap_or_default();
self.fob_meta_command = capture
.and_then(|c| c.command.clone())
.unwrap_or_else(|| {
let b = capture.map(|c| c.button_name().to_string()).unwrap_or_default();
if b.is_empty() || b == "-" {
String::new()
} else {
b
}
});
self.fob_meta_notes = String::new();
self.input_mode = InputMode::ExportFilename;
Ok(())
@@ -1378,10 +1445,37 @@ impl App {
make: self.fob_meta_make.clone(),
model: self.fob_meta_model.clone(),
region: self.fob_meta_region.clone(),
command: self.fob_meta_command.clone(),
notes: self.fob_meta_notes.clone(),
};
let filename = format!("{}.fob", self.export_filename);
// Unknown captures or user-edited vehicle-style filenames always get 8-hex suffix to avoid overwrites.
let use_hex_suffix = capture.protocol.is_none()
|| self.export_filename.ends_with("_Unknown")
|| self.export_filename == "Unknown";
let already_has_8hex = self.export_filename.len() >= 9
&& self.export_filename.as_bytes()[self.export_filename.len() - 9] == b'_'
&& self.export_filename[self.export_filename.len() - 8..]
.chars()
.all(|c| c.is_ascii_hexdigit());
let filename = if use_hex_suffix && already_has_8hex {
format!("{}.fob", self.export_filename.trim())
} else if use_hex_suffix {
let base = self.export_filename.trim();
let suffix_nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos() as u64;
let suffix = (suffix_nanos.wrapping_add(id as u64 * 2654435761) % 0x100_000_000) as u32;
let hex_suffix = format!("{:08X}", suffix);
if base.is_empty() {
format!("unknown_{}_{}.fob", id, hex_suffix)
} else {
format!("{}_{}.fob", base, hex_suffix)
}
} else {
format!("{}.fob", self.export_filename)
};
let path = export_dir.join(&filename);
crate::export::fob::export_fob(
@@ -1416,6 +1510,16 @@ impl App {
.and_then(|c| c.region.as_ref())
.map(|s| s.to_string())
.unwrap_or_default();
self.capture_meta_command = capture
.and_then(|c| c.command.clone())
.unwrap_or_else(|| {
let b = capture.map(|c| c.button_name().to_string()).unwrap_or_default();
if b.is_empty() || b == "-" {
String::new()
} else {
b
}
});
self.capture_meta_capture_id = Some(capture_id);
self.input_mode = InputMode::CaptureMetaYear;
}
@@ -1435,6 +1539,7 @@ impl App {
capture.make = Some(self.capture_meta_make.clone()).filter(|s| !s.is_empty());
capture.model = Some(self.capture_meta_model.clone()).filter(|s| !s.is_empty());
capture.region = Some(self.capture_meta_region.clone()).filter(|s| !s.is_empty());
capture.command = Some(self.capture_meta_command.clone()).filter(|s| !s.is_empty());
}
self.input_mode = InputMode::Normal;
self.capture_meta_capture_id = None;
@@ -1835,6 +1940,7 @@ impl App {
make: None,
model: None,
region: None,
command: None,
source_file: None,
};
self.next_capture_id += 1;