diff --git a/src/app.rs b/src/app.rs index 8e677e8..3fcb597 100644 --- a/src/app.rs +++ b/src/app.rs @@ -35,6 +35,10 @@ pub enum InputMode { FobMetaRegion, /// Fob export metadata: editing notes field FobMetaNotes, + /// License overlay (centered box) + License, + /// Credits overlay (centered box) + Credits, } /// Export format being used @@ -165,6 +169,9 @@ pub enum RadioEvent { StateChanged(RadioState), } +/// License text (embedded at compile time) +pub const LICENSE_TEXT: &str = include_str!("../LICENSE"); + /// Main application state pub struct App { /// Current input mode @@ -196,6 +203,10 @@ pub struct App { /// Currently selected signal menu item index pub signal_menu_index: usize, + // -- License/Credits overlay -- + /// Scroll offset for license/credits overlay (lines) + pub overlay_scroll: usize, + // -- Settings menu state -- /// Currently selected settings field pub settings_field_index: usize, @@ -312,6 +323,7 @@ impl App { last_error: None, status_message: None, signal_menu_index: 0, + overlay_scroll: 0, settings_field_index: 0, settings_value_index: 0, next_capture_id, @@ -441,6 +453,14 @@ impl App { "lock" => self.transmit_command(parts.get(1), ButtonCommand::Lock)?, "trunk" => self.transmit_command(parts.get(1), ButtonCommand::Trunk)?, "panic" => self.transmit_command(parts.get(1), ButtonCommand::Panic)?, + "license" | "licence" => { + self.input_mode = InputMode::License; + self.overlay_scroll = 0; + } + "credits" => { + self.input_mode = InputMode::Credits; + self.overlay_scroll = 0; + } "delete" => { if parts.len() < 2 { self.last_error = Some("Usage: :delete or :delete all".to_string()); diff --git a/src/main.rs b/src/main.rs index 6dddbdb..e13df1d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -161,7 +161,9 @@ fn run_app(terminal: &mut Terminal, app: &mut A let command = app.command_input.clone(); app.execute_command(&command)?; app.command_input.clear(); - app.input_mode = InputMode::Normal; + if app.input_mode == InputMode::Command { + app.input_mode = InputMode::Normal; + } } KeyCode::Char(c) => { app.command_input.push(c); @@ -392,6 +394,19 @@ fn run_app(terminal: &mut Terminal, app: &mut A } _ => {} }, + + InputMode::License | InputMode::Credits => match key.code { + KeyCode::Esc | KeyCode::Enter => { + app.input_mode = InputMode::Normal; + } + KeyCode::Up | KeyCode::Char('k') => { + app.overlay_scroll = app.overlay_scroll.saturating_sub(1); + } + KeyCode::Down | KeyCode::Char('j') => { + app.overlay_scroll = app.overlay_scroll.saturating_add(1); + } + _ => {} + }, } } } diff --git a/src/ui/command.rs b/src/ui/command.rs index 82904b9..6668cc6 100644 --- a/src/ui/command.rs +++ b/src/ui/command.rs @@ -53,6 +53,16 @@ pub fn render_command_line(frame: &mut Frame, area: Rect, app: &App) { "EXPORT", Style::default().fg(Color::Green), ), + InputMode::License => ( + String::new(), + "LICENSE", + Style::default().fg(Color::Cyan), + ), + InputMode::Credits => ( + String::new(), + "CREDITS", + Style::default().fg(Color::Cyan), + ), }; let input_line = Line::from(vec![ diff --git a/src/ui/layout.rs b/src/ui/layout.rs index de92e5b..96671ec 100644 --- a/src/ui/layout.rs +++ b/src/ui/layout.rs @@ -8,13 +8,14 @@ use ratatui::{ Frame, }; -use crate::app::{App, InputMode, RadioState}; +use crate::app::{App, InputMode, RadioState, LICENSE_TEXT}; use super::captures_list::render_captures_list; use super::command::render_command_line; use super::settings_menu::{render_settings_dropdown, render_settings_tabs}; use super::signal_menu::render_signal_menu; use super::status_bar::render_status_bar; +use super::text_overlay::render_text_overlay; use crate::app::InputMode as IM; @@ -91,6 +92,19 @@ pub fn draw_ui(frame: &mut Frame, app: &App) { ) { render_export_form(frame, app); } + + if app.input_mode == InputMode::License { + render_text_overlay(frame, app, "License", LICENSE_TEXT, Alignment::Left); + } + if app.input_mode == InputMode::Credits { + render_text_overlay( + frame, + app, + "Credits", + super::text_overlay::CREDITS_TEXT, + Alignment::Center, + ); + } } /// Render the header with title and radio status @@ -159,6 +173,7 @@ fn render_help_bar(frame: &mut Frame, area: Rect, app: &App) { | InputMode::FobMetaModel | InputMode::FobMetaRegion => "Enter: Next Field | Esc: Cancel Export", InputMode::FobMetaNotes => "Enter: Save & Export | Esc: Cancel Export", + InputMode::License | InputMode::Credits => "Esc/Enter: Close | Up/Down: Scroll", }; let help = Paragraph::new(Line::from(Span::styled( diff --git a/src/ui/mod.rs b/src/ui/mod.rs index d62fa54..44bcedf 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -4,6 +4,7 @@ mod captures_list; mod command; mod layout; pub mod signal_menu; +mod text_overlay; pub mod settings_menu; mod status_bar; diff --git a/src/ui/text_overlay.rs b/src/ui/text_overlay.rs new file mode 100644 index 0000000..e11d07c --- /dev/null +++ b/src/ui/text_overlay.rs @@ -0,0 +1,91 @@ +//! Centered text overlay for License and Credits. + +use ratatui::{ + layout::{Alignment, Rect}, + style::{Color, Style}, + text::Line, + widgets::{Block, Borders, Clear, Paragraph, Wrap}, + Frame, +}; + +use crate::app::App; + +/// Credits text for the :credits command +pub const CREDITS_TEXT: &str = r#"=============== CREDITS ============== + +Keyfob Analysis tool is developed by Kara Zajac (.leviathan) and would not be possible without ProtoPirate. I am truely standing on the shoulders of giants when I developed this application. + +ProtoPirate Development Team +---- +RocketGod +MMX +Leeroy +gullradriel +Skorp - Thanks, I sneaked a lot from Weather App! +Vadim's Radio Driver + +Protocol Magic +---- +L0rdDiakon +YougZ +RocketGod +MMX +DoobTheGoober +Skorp +Slackware +Trikk +Wootini +Li0ard +Leeroy + +Reverse Engineering Support +---- +DoobTheGoober +MMX +NeedNotApply +RocketGod +Slackware +Trikk +Li0ard"#; + +/// Render a centered overlay with a title and scrollable text (e.g. License or Credits). +pub fn render_text_overlay( + frame: &mut Frame, + app: &App, + title: &str, + text: &str, + alignment: Alignment, +) { + let area = frame.area(); + + // Use most of the terminal: 80% width, 85% height, centered + let width = (area.width as f32 * 0.80) as u16; + let height = (area.height as f32 * 0.85) as u16; + let x = area.width.saturating_sub(width) / 2; + let y = area.height.saturating_sub(height) / 2; + let overlay_area = Rect::new(x, y, width.min(area.width), height.min(area.height)); + + frame.render_widget(Clear, overlay_area); + + let total_lines = text.lines().count(); + let inner_height = overlay_area.height.saturating_sub(2) as usize; // minus border + let scroll_max = total_lines.saturating_sub(inner_height).saturating_sub(1); + let scroll = app.overlay_scroll.min(scroll_max); + + let paragraph = Paragraph::new( + text.lines() + .map(Line::from) + .collect::>(), + ) + .block( + Block::default() + .borders(Borders::ALL) + .title(format!(" {} ", title)) + .border_style(Style::default().fg(Color::Cyan)), + ) + .wrap(Wrap { trim: true }) + .scroll((scroll as u16, 0)) + .alignment(alignment); + + frame.render_widget(paragraph, overlay_area); +}