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:
@@ -2,6 +2,24 @@
|
||||
|
||||
All notable changes to KAT are documented here.
|
||||
|
||||
## [1.1.3] - 2026-02-20
|
||||
|
||||
### Added
|
||||
|
||||
- **Command field** — Capture metadata (press **i**) and .fob export now include **Command** (e.g. Unlock, Lock, Trunk, Panic). Export filename for unknown protocol uses Year_Make_Model_Region_Command; 8-hex suffix is shown in the filename field and saved in uppercase (e.g. `…_A1B2C3D4.fob`). .fob vehicle info and import support optional `command`.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Unknown signals** — Shown by default (`research_mode` default is now `true`). Config comment and storage docs clarify that no keystore directory is used or created (keys are embedded).
|
||||
- **Signal separation** — End-of-signal gap increased from 20 ms to **80 ms** so one button press (multiple bursts with 25–50 ms gaps) produces a single capture instead of 3–4.
|
||||
- **Short signals** — Demodulator now emits captures with **5+** level-duration pairs (was 10), so short or weak unknown keyfob bursts are no longer dropped (RSSI spike but no capture).
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Export filename** — Unknown-protocol .fob exports always get the 8-hex suffix (including when filename ends with `Unknown`); suffix is uppercase.
|
||||
|
||||
---
|
||||
|
||||
## [1.1.2] - 2026-02-20
|
||||
|
||||
### Added
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "kat"
|
||||
version = "1.1.2"
|
||||
version = "1.1.3"
|
||||
edition = "2021"
|
||||
description = "Keyfob Analysis Toolkit - HackRF/RTL-SDR signal capture, decode, and transmit (HackRF only)"
|
||||
authors = ["KAT Team"]
|
||||
|
||||
+113
-7
@@ -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,13 +1307,53 @@ 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 {
|
||||
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
|
||||
pub fn export_fob(&mut self, id: u32) -> Result<()> {
|
||||
@@ -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;
|
||||
|
||||
@@ -76,6 +76,9 @@ pub struct Capture {
|
||||
/// Region (e.g. NA, EU) for vulnerability lookup and .fob export.
|
||||
#[serde(default)]
|
||||
pub region: Option<String>,
|
||||
/// User-editable command label (e.g. Unlock, Lock) for .fob export and filename; set via 'i' or export form.
|
||||
#[serde(default)]
|
||||
pub command: Option<String>,
|
||||
/// Source file path when imported from .sub or .fob; None for live captures.
|
||||
#[serde(default)]
|
||||
pub source_file: Option<String>,
|
||||
@@ -157,6 +160,7 @@ impl Capture {
|
||||
make: None,
|
||||
model: None,
|
||||
region: None,
|
||||
command: None,
|
||||
source_file: None,
|
||||
}
|
||||
}
|
||||
|
||||
+21
-4
@@ -14,6 +14,8 @@ pub struct FobMetadata {
|
||||
pub make: String,
|
||||
pub model: String,
|
||||
pub region: String,
|
||||
/// Command label (e.g. Unlock, Lock) for export filename and .fob vehicle info.
|
||||
pub command: String,
|
||||
pub notes: String,
|
||||
}
|
||||
|
||||
@@ -61,6 +63,9 @@ pub struct FobVehicleInfo {
|
||||
pub model: Option<String>,
|
||||
#[serde(default)]
|
||||
pub region: Option<String>,
|
||||
/// Command label (e.g. Unlock, Lock); optional for backwards compatibility.
|
||||
#[serde(default)]
|
||||
pub command: Option<String>,
|
||||
#[serde(default)]
|
||||
pub notes: Option<String>,
|
||||
}
|
||||
@@ -119,6 +124,14 @@ pub fn export_fob(
|
||||
Some(m.notes.clone())
|
||||
}
|
||||
});
|
||||
let command = metadata.and_then(|m| {
|
||||
let s = m.command.trim();
|
||||
if s.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(s.to_string())
|
||||
}
|
||||
});
|
||||
|
||||
let raw_pairs = if include_raw && !capture.raw_pairs.is_empty() {
|
||||
Some(
|
||||
@@ -163,6 +176,7 @@ pub fn export_fob(
|
||||
make,
|
||||
model,
|
||||
region,
|
||||
command,
|
||||
notes,
|
||||
},
|
||||
capture: FobCapture {
|
||||
@@ -287,6 +301,7 @@ fn import_fob_v2(fob: &FobFile, next_id: u32) -> Result<Capture> {
|
||||
CaptureStatus::Unknown
|
||||
};
|
||||
|
||||
let vehicle = &fob.vehicle;
|
||||
Ok(Capture {
|
||||
id: next_id,
|
||||
timestamp,
|
||||
@@ -302,10 +317,11 @@ fn import_fob_v2(fob: &FobFile, next_id: u32) -> Result<Capture> {
|
||||
raw_pairs,
|
||||
status,
|
||||
received_rf: None,
|
||||
year: None,
|
||||
make: None,
|
||||
model: None,
|
||||
region: None,
|
||||
year: vehicle.year.map(|y| y.to_string()),
|
||||
make: Some(vehicle.make.clone()).filter(|s| !s.is_empty()),
|
||||
model: vehicle.model.clone().filter(|s| !s.is_empty()),
|
||||
region: vehicle.region.clone().filter(|s| !s.is_empty()),
|
||||
command: vehicle.command.clone().filter(|s| !s.is_empty()),
|
||||
source_file: None,
|
||||
})
|
||||
}
|
||||
@@ -377,6 +393,7 @@ fn import_fob_v1(fob: &FobFileV1, next_id: u32) -> Result<Capture> {
|
||||
make: None,
|
||||
model: None,
|
||||
region: None,
|
||||
command: None,
|
||||
source_file: None,
|
||||
})
|
||||
}
|
||||
|
||||
+37
-4
@@ -388,10 +388,10 @@ fn run_app<B: ratatui::backend::Backend>(terminal: &mut Terminal<B>, app: &mut A
|
||||
_ => {}
|
||||
},
|
||||
|
||||
// .fob export metadata: Region -> Notes
|
||||
// .fob export metadata: Region -> Command
|
||||
InputMode::FobMetaRegion => match key.code {
|
||||
KeyCode::Enter => {
|
||||
app.input_mode = InputMode::FobMetaNotes;
|
||||
app.input_mode = InputMode::FobMetaCommand;
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
app.fob_meta_region.push(c);
|
||||
@@ -406,6 +406,24 @@ fn run_app<B: ratatui::backend::Backend>(terminal: &mut Terminal<B>, app: &mut A
|
||||
_ => {}
|
||||
},
|
||||
|
||||
// .fob export metadata: Command -> Notes
|
||||
InputMode::FobMetaCommand => match key.code {
|
||||
KeyCode::Enter => {
|
||||
app.input_mode = InputMode::FobMetaNotes;
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
app.fob_meta_command.push(c);
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
app.fob_meta_command.pop();
|
||||
}
|
||||
KeyCode::Esc => {
|
||||
app.export_capture_id = None;
|
||||
app.input_mode = InputMode::Normal;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
|
||||
// .fob export metadata: Notes -> Export
|
||||
InputMode::FobMetaNotes => match key.code {
|
||||
KeyCode::Enter => {
|
||||
@@ -425,7 +443,7 @@ fn run_app<B: ratatui::backend::Backend>(terminal: &mut Terminal<B>, app: &mut A
|
||||
_ => {}
|
||||
},
|
||||
|
||||
// Capture metadata (Year/Make/Model/Region for vuln lookup)
|
||||
// Capture metadata (Year/Make/Model/Region/Command for vuln lookup)
|
||||
InputMode::CaptureMetaYear => match key.code {
|
||||
KeyCode::Enter => {
|
||||
app.input_mode = InputMode::CaptureMetaMake;
|
||||
@@ -475,7 +493,7 @@ fn run_app<B: ratatui::backend::Backend>(terminal: &mut Terminal<B>, app: &mut A
|
||||
},
|
||||
InputMode::CaptureMetaRegion => match key.code {
|
||||
KeyCode::Enter => {
|
||||
app.save_capture_meta();
|
||||
app.input_mode = InputMode::CaptureMetaCommand;
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
app.capture_meta_region.push(c);
|
||||
@@ -488,6 +506,21 @@ fn run_app<B: ratatui::backend::Backend>(terminal: &mut Terminal<B>, app: &mut A
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
InputMode::CaptureMetaCommand => match key.code {
|
||||
KeyCode::Enter => {
|
||||
app.save_capture_meta();
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
app.capture_meta_command.push(c);
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
app.capture_meta_command.pop();
|
||||
}
|
||||
KeyCode::Esc => {
|
||||
app.cancel_capture_meta();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
|
||||
InputMode::LoadFileBrowser => {
|
||||
const VISIBLE: usize = 16;
|
||||
|
||||
@@ -113,7 +113,7 @@ impl Demodulator {
|
||||
pairs: Vec::with_capacity(2048),
|
||||
total_samples: 0,
|
||||
min_duration_us: 40, // 40µs debounce (was 50 — slightly more permissive)
|
||||
max_gap_us: 20_000, // 20ms gap = end of signal (was 10ms — wider to avoid splitting signals with internal gaps)
|
||||
max_gap_us: 80_000, // 80ms gap = end of signal; keeps multi-burst keyfob presses (e.g. 3–4 bursts with 25–50ms gaps) as one capture
|
||||
samples_since_edge: 0,
|
||||
}
|
||||
}
|
||||
@@ -253,11 +253,11 @@ impl Demodulator {
|
||||
.push(LevelDuration::new(self.current_level, duration_us));
|
||||
}
|
||||
|
||||
// Return the pairs and reset
|
||||
// Return the pairs and reset (min 5 pairs so short/unknown keyfob bursts still show)
|
||||
let result = std::mem::take(&mut self.pairs);
|
||||
self.reset_state();
|
||||
|
||||
if result.len() >= 10 {
|
||||
if result.len() >= 5 {
|
||||
return Some(result);
|
||||
}
|
||||
}
|
||||
@@ -405,7 +405,7 @@ impl FmDemodulator {
|
||||
pending_count: 0,
|
||||
pairs: Vec::with_capacity(2048),
|
||||
min_duration_us: 40,
|
||||
max_gap_us: 20_000,
|
||||
max_gap_us: 80_000, // match AM: 80ms so one button press (multi-burst) stays one capture
|
||||
samples_since_edge: 0,
|
||||
}
|
||||
}
|
||||
@@ -495,7 +495,7 @@ impl FmDemodulator {
|
||||
}
|
||||
let result = std::mem::take(&mut self.pairs);
|
||||
self.fm_reset_state();
|
||||
if result.len() >= 10 {
|
||||
if result.len() >= 5 {
|
||||
return Some(result);
|
||||
}
|
||||
}
|
||||
@@ -559,8 +559,8 @@ mod tests {
|
||||
// Process (won't return signal yet since no long gap)
|
||||
let _ = demod.process_samples(&buf);
|
||||
|
||||
// Add a long gap to flush
|
||||
let gap_buf: Vec<i8> = vec![1, 0].repeat(50_000); // 25ms LOW
|
||||
// Add a long gap to flush (>= max_gap_us: 80ms at 2MHz = 160k samples)
|
||||
let gap_buf: Vec<i8> = vec![1, 0].repeat(80_000); // 80ms LOW
|
||||
if let Some(pairs) = demod.process_samples(&gap_buf) {
|
||||
// Verify no consecutive same-level pairs
|
||||
for window in pairs.windows(2) {
|
||||
|
||||
+5
-3
@@ -1,6 +1,7 @@
|
||||
//! Storage management for configuration, exports, and keystores.
|
||||
//! Storage management for configuration and exports.
|
||||
//!
|
||||
//! All application data lives under `~/.config/KAT/`:
|
||||
//! All application data lives under `~/.config/KAT/`. **No keystore directory is created**
|
||||
//! — keys are embedded in the binary (see [crate::protocols::keys] and [crate::keystore]).
|
||||
//!
|
||||
//! ```text
|
||||
//! ~/.config/KAT/
|
||||
@@ -57,7 +58,7 @@ impl Config {
|
||||
export_directory: config_dir.join("exports"),
|
||||
import_directory: config_dir.join("import"),
|
||||
max_captures: 100,
|
||||
research_mode: false,
|
||||
research_mode: true, // show unknown signals by default (researchers need to see them)
|
||||
default_frequency: 433_920_000,
|
||||
default_lna_gain: 24,
|
||||
default_vga_gain: 20,
|
||||
@@ -160,6 +161,7 @@ impl Config {
|
||||
; Location: {path}
|
||||
;
|
||||
; Edit this file to change default settings.
|
||||
; Keys are embedded in the program — no keystore directory is used or created.
|
||||
; Lines starting with ; or # are comments.
|
||||
|
||||
[general]
|
||||
|
||||
+3
-1
@@ -53,6 +53,7 @@ pub fn render_command_line(frame: &mut Frame, area: Rect, app: &App) {
|
||||
| InputMode::FobMetaMake
|
||||
| InputMode::FobMetaModel
|
||||
| InputMode::FobMetaRegion
|
||||
| InputMode::FobMetaCommand
|
||||
| InputMode::FobMetaNotes => (
|
||||
String::new(),
|
||||
"EXPORT",
|
||||
@@ -61,7 +62,8 @@ pub fn render_command_line(frame: &mut Frame, area: Rect, app: &App) {
|
||||
InputMode::CaptureMetaYear
|
||||
| InputMode::CaptureMetaMake
|
||||
| InputMode::CaptureMetaModel
|
||||
| InputMode::CaptureMetaRegion => (
|
||||
| InputMode::CaptureMetaRegion
|
||||
| InputMode::CaptureMetaCommand => (
|
||||
String::new(),
|
||||
"META",
|
||||
Style::default().fg(Color::Cyan),
|
||||
|
||||
+23
-5
@@ -105,6 +105,7 @@ pub fn draw_ui(frame: &mut Frame, app: &App) {
|
||||
| InputMode::FobMetaMake
|
||||
| InputMode::FobMetaModel
|
||||
| InputMode::FobMetaRegion
|
||||
| InputMode::FobMetaCommand
|
||||
| InputMode::FobMetaNotes
|
||||
) {
|
||||
render_export_form(frame, app);
|
||||
@@ -116,6 +117,7 @@ pub fn draw_ui(frame: &mut Frame, app: &App) {
|
||||
| InputMode::CaptureMetaMake
|
||||
| InputMode::CaptureMetaModel
|
||||
| InputMode::CaptureMetaRegion
|
||||
| InputMode::CaptureMetaCommand
|
||||
) {
|
||||
render_capture_meta_form(frame, app);
|
||||
}
|
||||
@@ -250,12 +252,14 @@ fn render_help_bar(frame: &mut Frame, area: Rect, app: &App) {
|
||||
InputMode::FobMetaYear
|
||||
| InputMode::FobMetaMake
|
||||
| InputMode::FobMetaModel
|
||||
| InputMode::FobMetaRegion => "Enter: Next Field | Esc: Cancel Export",
|
||||
| InputMode::FobMetaRegion
|
||||
| InputMode::FobMetaCommand => "Enter: Next Field | Esc: Cancel Export",
|
||||
InputMode::FobMetaNotes => "Enter: Save & Export | Esc: Cancel Export",
|
||||
InputMode::CaptureMetaYear
|
||||
| InputMode::CaptureMetaMake
|
||||
| InputMode::CaptureMetaModel => "Enter: Next Field | Esc: Cancel",
|
||||
InputMode::CaptureMetaRegion => "Enter: Save | Esc: Cancel",
|
||||
| InputMode::CaptureMetaModel
|
||||
| InputMode::CaptureMetaRegion => "Enter: Next Field | Esc: Cancel",
|
||||
InputMode::CaptureMetaCommand => "Enter: Save | Esc: Cancel",
|
||||
InputMode::License | InputMode::Credits => "Esc/Enter: Close | Up/Down: Scroll",
|
||||
InputMode::LoadFileBrowser => "Up/Down: Navigate | Enter: Open/Import | Esc: Close",
|
||||
};
|
||||
@@ -464,6 +468,7 @@ fn render_export_form(frame: &mut Frame, app: &App) {
|
||||
InputMode::FobMetaMake,
|
||||
InputMode::FobMetaModel,
|
||||
InputMode::FobMetaRegion,
|
||||
InputMode::FobMetaCommand,
|
||||
InputMode::FobMetaNotes,
|
||||
]
|
||||
} else {
|
||||
@@ -566,11 +571,17 @@ fn render_export_form(frame: &mut Frame, app: &App) {
|
||||
placeholder: "(e.g. NA, EU, APAC, MEA)",
|
||||
idx: 4,
|
||||
},
|
||||
FormField {
|
||||
label: " Command: ",
|
||||
value: &app.fob_meta_command,
|
||||
placeholder: "(e.g. Unlock, Lock, Trunk, Panic)",
|
||||
idx: 5,
|
||||
},
|
||||
FormField {
|
||||
label: " Notes: ",
|
||||
value: &app.fob_meta_notes,
|
||||
placeholder: "(optional — color, trim, VIN, etc.)",
|
||||
idx: 5,
|
||||
idx: 6,
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -670,6 +681,7 @@ fn render_capture_meta_form(frame: &mut Frame, app: &App) {
|
||||
InputMode::CaptureMetaMake,
|
||||
InputMode::CaptureMetaModel,
|
||||
InputMode::CaptureMetaRegion,
|
||||
InputMode::CaptureMetaCommand,
|
||||
];
|
||||
let current_idx = field_modes
|
||||
.iter()
|
||||
@@ -726,7 +738,7 @@ fn render_capture_meta_form(frame: &mut Frame, app: &App) {
|
||||
placeholder: &'a str,
|
||||
idx: usize,
|
||||
}
|
||||
let fields: [FormField; 4] = [
|
||||
let fields: [FormField; 5] = [
|
||||
FormField {
|
||||
label: " Year: ",
|
||||
value: &app.capture_meta_year,
|
||||
@@ -751,6 +763,12 @@ fn render_capture_meta_form(frame: &mut Frame, app: &App) {
|
||||
placeholder: "(e.g. NA, EU, or ALL)",
|
||||
idx: 3,
|
||||
},
|
||||
FormField {
|
||||
label: " Command: ",
|
||||
value: &app.capture_meta_command,
|
||||
placeholder: "(e.g. Unlock, Lock, Trunk, Panic)",
|
||||
idx: 4,
|
||||
},
|
||||
];
|
||||
|
||||
for field in &fields {
|
||||
|
||||
Reference in New Issue
Block a user