use std::collections::HashMap;
#[derive(Debug, Clone)]
pub enum ProtocolVarType {
Bool,
String,
Enum(&'static [&'static str]),
Int,
Float,
}
#[derive(Debug, Clone)]
pub struct ProtocolVar {
pub name: &'static str,
pub description: &'static str,
pub example: &'static str,
pub var_type: ProtocolVarType,
}
pub static PROTOCOL_VARS: &[ProtocolVar] = &[
ProtocolVar {
name: "transparent",
description: "Transparent color for widget/background (hex RGB, e.g. ffffff for white)",
example: "transparent^ff00ff",
var_type: ProtocolVarType::String,
},
ProtocolVar {
name: "scale",
description: "Scale factor for the window (float)",
example: "scale^2.0",
var_type: ProtocolVarType::Float,
},
ProtocolVar {
name: "fit",
description: "Fit mode for ROM display (none, contain, cover, stretch)",
example: "fit^cover",
var_type: ProtocolVarType::String,
},
ProtocolVar {
name: "timeout",
description: "Timeout in seconds before the emulator exits (alias: t)",
example: "timeout^60",
var_type: ProtocolVarType::Float,
},
ProtocolVar {
name: "t",
description: "Timeout in seconds before the emulator exits (alias for timeout)",
example: "t^60",
var_type: ProtocolVarType::Float,
},
ProtocolVar {
name: "emu",
description: "Select emulator backend",
example: "emu^^buxn",
var_type: ProtocolVarType::Enum(&["buxn", "cuxn", "uxn"]),
},
ProtocolVar {
name: "widget",
description: "Enable widget mode (transparent, no decorations, always-on-top)",
example: "widget",
var_type: ProtocolVarType::Bool,
},
ProtocolVar {
name: "ontop",
description: "Control always-on-top (true/false)",
example: "ontop^false",
var_type: ProtocolVarType::Bool,
},
ProtocolVar {
name: "debug",
description: "Enable debug console (Windows only)",
example: "debug",
var_type: ProtocolVarType::Bool,
},
ProtocolVar {
name: "efx",
description: "Effect name or identifier for emulator (string)",
example: "efx^invert",
var_type: ProtocolVarType::String,
},
ProtocolVar {
name: "efxmode",
description: "Effect mode for emulator (string)",
example: "efxmode^blend",
var_type: ProtocolVarType::String,
},
];
#[derive(Debug, Clone)]
pub struct ProtocolQueryVar {
pub name: &'static str,
pub description: &'static str,
pub example: &'static str,
pub var_type: ProtocolVarType,
}
pub static BANG_VARS: &[ProtocolQueryVar] = &[
ProtocolQueryVar {
name: "fit",
description: "Fit mode for ROM display (none, contain, cover, stretch)",
example: "!fit=cover",
var_type: ProtocolVarType::String,
},
ProtocolQueryVar {
name: "timeout",
description: "Timeout in seconds before the emulator exits (alias: t)",
example: "!timeout=60",
var_type: ProtocolVarType::Float,
},
ProtocolQueryVar {
name: "t",
description: "Timeout in seconds before the emulator exits (alias for timeout)",
example: "!t=60",
var_type: ProtocolVarType::Float,
},
ProtocolQueryVar {
name: "x",
description: "Window X position (pixels or percent or complex)",
example: "!x=100",
var_type: ProtocolVarType::String,
},
ProtocolQueryVar {
name: "y",
description: "Window Y position (pixels or percent or complex)",
example: "!y=100",
var_type: ProtocolVarType::String,
},
ProtocolQueryVar {
name: "w",
description: "Window width (pixels or percent or complex)",
example: "!w=800",
var_type: ProtocolVarType::String,
},
ProtocolQueryVar {
name: "h",
description: "Window height (pixels or percent or complex)",
example: "!h=600",
var_type: ProtocolVarType::String,
},
];
#[derive(Debug, Clone)]
pub enum ProtocolVarVar {
Bool(bool),
String(String),
Enum(&'static str),
Int(i64),
Float(f64),
}
#[derive(Debug, Clone)]
pub struct ProtocolParseResult {
pub raw: HashMap<String, String>,
pub proto_vars: HashMap<String, ProtocolVarVar>,
pub query_vars: HashMap<String, ProtocolVarVar>,
pub url: String,
pub protocol: String, }
impl ProtocolParseResult {
pub fn get(&self, name: &str) -> Option<&ProtocolVarVar> {
self.proto_vars.get(name)
}
}
pub struct ProtocolParser;
impl ProtocolParser {
pub fn parse(raw_url: &str) -> ProtocolParseResult {
let mut raw_map = HashMap::new();
let mut proto_vars_map = HashMap::new();
let mut query_vars_map = HashMap::new();
if !raw_url.starts_with("uxntal:") {
return ProtocolParseResult {
raw: raw_map,
proto_vars: HashMap::new(),
query_vars: HashMap::new(),
url: String::new(),
protocol: String::new(),
};
}
let s = raw_url.trim_start_matches("uxntal:");
let (kv_part, url_part) = if let Some(idx) = s.find("//") {
let (kv, url) = (&s[..idx], &s[idx..]);
let kv = if kv.ends_with(':') {
kv.strip_suffix(':').unwrap()
} else {
kv
};
(kv, url)
} else {
(s, "")
};
let protocol = kv_part.to_string();
for part in kv_part.split(':') {
if part.is_empty() {
continue;
}
let decoded = percent_decode_str(part).decode_utf8_lossy();
let mut split_idx = None;
let mut sep_len = 0;
if let Some(idx) = decoded.find("^^") {
split_idx = Some(idx);
sep_len = 2;
} else if let Some(idx) = decoded.find('^') {
split_idx = Some(idx);
sep_len = 1;
}
let (key, value) = if let Some(idx) = split_idx {
(&decoded[..idx], &decoded[idx + sep_len..])
} else {
(decoded.as_ref(), "true") };
println!(
"[ProtocolParser::parse] parsed key='{}' value='{}'",
key, value
);
raw_map.insert(key.to_string(), value.to_string());
if let Some(var_def) = PROTOCOL_VARS.iter().find(|v| v.name == key) {
match &var_def.var_type {
ProtocolVarType::Bool => {
let val = value.trim().to_ascii_lowercase();
let parsed = match val.as_str() {
"1" | "true" | "yes" | "on" => Some(true),
"0" | "false" | "no" | "off" => Some(false),
"" => Some(true),
_ => None,
};
if let Some(b) = parsed {
proto_vars_map.insert(key.to_string(), ProtocolVarVar::Bool(b));
}
}
ProtocolVarType::Enum(valids) => {
let val = value.trim();
if let Some(&valid) = valids.iter().find(|&&v| v.eq_ignore_ascii_case(val))
{
proto_vars_map.insert(key.to_string(), ProtocolVarVar::Enum(valid));
}
}
ProtocolVarType::String => {
if !value.is_empty() {
proto_vars_map
.insert(key.to_string(), ProtocolVarVar::String(value.to_string()));
}
}
ProtocolVarType::Int => {
if let Ok(i) = value.trim().parse::<i64>() {
query_vars_map.insert(key.to_string(), ProtocolVarVar::Int(i));
}
}
ProtocolVarType::Float => {
let v = value.trim().replace('%', "");
if let Ok(f) = v.parse::<f64>() {
query_vars_map.insert(key.to_string(), ProtocolVarVar::Float(f));
}
}
}
}
if let Some(var_def) = BANG_VARS.iter().find(|v| v.name == key) {
match &var_def.var_type {
ProtocolVarType::Bool => {
let val = value.trim().to_ascii_lowercase();
let parsed = match val.as_str() {
"1" | "true" | "yes" | "on" => Some(true),
"0" | "false" | "no" | "off" => Some(false),
"" => Some(true),
_ => None,
};
if let Some(b) = parsed {
query_vars_map.insert(key.to_string(), ProtocolVarVar::Bool(b));
}
}
ProtocolVarType::Enum(valids) => {
let val = value.trim();
if let Some(&valid) = valids.iter().find(|&&v| v.eq_ignore_ascii_case(val))
{
query_vars_map.insert(key.to_string(), ProtocolVarVar::Enum(valid));
}
}
ProtocolVarType::String => {
if !value.is_empty() {
query_vars_map
.insert(key.to_string(), ProtocolVarVar::String(value.to_string()));
}
}
ProtocolVarType::Int => {
if let Ok(i) = value.trim().parse::<i64>() {
query_vars_map.insert(key.to_string(), ProtocolVarVar::Int(i));
}
}
ProtocolVarType::Float => {
let v = value.trim().replace('%', "");
if let Ok(f) = v.parse::<f64>() {
query_vars_map.insert(key.to_string(), ProtocolVarVar::Float(f));
}
}
}
}
}
let url = if url_part.starts_with("//https://")
|| url_part.starts_with("//http://")
|| url_part.starts_with("//file://")
{
let mut s = url_part.trim_start_matches("//");
if s.starts_with("https://https://") {
s = &s[8..];
} else if s.starts_with("http://http://") {
s = &s[7..];
}
s.to_string()
} else if let Some(stripped) = url_part.strip_prefix("//https//") {
format!("https://{}", stripped)
} else if let Some(stripped) = url_part.strip_prefix("//http//") {
format!("http://{}", stripped)
} else if let Some(stripped) = url_part.strip_prefix("//file//") {
format!("file://{}", stripped)
} else {
url_part.to_string()
};
ProtocolParseResult {
raw: raw_map,
proto_vars: proto_vars_map,
query_vars: query_vars_map,
url,
protocol,
}
}
pub fn extract_target(url: &str) -> Option<String> {
use std::borrow::Cow;
fn pct_decode(s: &str) -> String {
percent_encoding::percent_decode_str(s)
.decode_utf8()
.unwrap_or(Cow::from(s))
.into_owned()
}
fn qs_get(query: &str, key: &str) -> Option<String> {
for pair in query.split('&') {
let mut it = pair.splitn(2, '=');
let k = it.next().unwrap_or("");
let v = it.next().unwrap_or("");
if k.eq_ignore_ascii_case(key) {
return Some(pct_decode(v));
}
}
None
}
let s = url;
let s = if s.starts_with("//https://")
|| s.starts_with("//http://")
|| s.starts_with("//file://")
{
s.trim_start_matches("//").to_string()
} else if let Some(stripped) = s.strip_prefix("//https//") {
format!("https://{}", stripped)
} else if let Some(stripped) = s.strip_prefix("//http//") {
format!("http://{}", stripped)
} else if let Some(stripped) = s.strip_prefix("//file//") {
format!("file://{}", stripped)
} else {
s.to_string()
};
if s.starts_with("open") {
let (path, rest) = if let Some(qpos) = s.find('?') {
(&s[..qpos], &s[qpos + 1..])
} else {
(&s[..], "")
};
if path == "open" || path == "open/" {
if let Some(v) = qs_get(rest, "url") {
return Some(v);
}
}
}
if let Some(rest) = s.strip_prefix("b64,") {
use base64::Engine;
if let Ok(bytes) = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(rest) {
if let Ok(strv) = String::from_utf8(bytes) {
return Some(strv);
}
}
}
for (bad, good, cut) in [
("https///", "https://", 8usize),
("http///", "http://", 7usize),
("file///", "file://", 7usize),
("https//", "https://", 7usize),
("http//", "http://", 6usize),
("file//", "file://", 7usize),
] {
if s.starts_with(bad) {
return Some(format!("{}{}", good, &s[cut..]));
}
}
if s.contains('%') {
let dec = pct_decode(&s);
if dec.starts_with("http://")
|| dec.starts_with("https://")
|| dec.starts_with("file://")
{
return Some(dec);
}
}
if s.starts_with("http://") || s.starts_with("https://") || s.starts_with("file://") {
return Some(s.to_string());
}
Some(s.to_string())
}
}
pub trait EmulatorArgMapper {
fn map_args(&self, result: &ProtocolParseResult) -> Vec<String>;
}
use percent_encoding::percent_decode_str;
pub trait EmulatorPathCheck {
fn is_available_in_path(result: &ProtocolParseResult) -> Option<std::path::PathBuf>;
}
pub fn get_emulator_launcher(
result: &ProtocolParseResult,
) -> Option<(Box<dyn EmulatorLauncher>, std::path::PathBuf)> {
match result.proto_vars.get("emu") {
Some(ProtocolVarVar::Enum("buxn")) => {
if let Some(path) = crate::emu_buxn::BuxnMapper::is_available_in_path(result) {
Some((Box::new(crate::emu_buxn::BuxnMapper), path))
} else {
None
}
}
Some(ProtocolVarVar::Enum("uxn")) => {
if let Some(path) = crate::emu_uxn::UxnMapper::is_available_in_path(result) {
Some((Box::new(crate::emu_uxn::UxnMapper), path))
} else {
None
}
}
_ => {
if let Some(path) = crate::emu_cuxn::CuxnMapper::is_available_in_path(result) {
Some((Box::new(crate::emu_cuxn::CuxnMapper), path))
} else {
None
}
}
}
}
pub fn get_emulator_mapper(
result: &ProtocolParseResult,
) -> Option<(Box<dyn EmulatorArgMapper>, std::path::PathBuf)> {
match result.proto_vars.get("emu") {
Some(ProtocolVarVar::Enum("buxn")) => {
if let Some(path) = crate::emu_buxn::BuxnMapper::is_available_in_path(result) {
Some((Box::new(crate::emu_buxn::BuxnMapper), path))
} else {
None
}
}
Some(ProtocolVarVar::Enum("uxn")) => {
if let Some(path) = crate::emu_uxn::UxnMapper::is_available_in_path(result) {
Some((Box::new(crate::emu_uxn::UxnMapper), path))
} else {
None
}
}
_ => {
if let Some(path) = crate::emu_cuxn::CuxnMapper::is_available_in_path(result) {
Some((Box::new(crate::emu_cuxn::CuxnMapper), path))
} else {
None
}
}
}
}
pub trait EmulatorLauncher {
fn build_command(
&self,
result: &ProtocolParseResult,
rom_path: &str,
emulator_path: &std::path::Path,
) -> std::process::Command;
}