use std::path::PathBuf;
use std::process::{Command, Stdio};
pub const WINDOW_MARK: &str = "window=1";
pub const CAPABILITY_KEY: &str = "cap";
fn split_fragment(url: &str) -> (&str, &str) {
match url.find('#') {
Some(i) => (&url[..i], &url[i..]),
None => (url, ""),
}
}
fn marked(url: &str) -> String {
let (head, frag) = split_fragment(url);
let sep = if head.contains('?') { '&' } else { '?' };
format!("{head}{sep}{WINDOW_MARK}{frag}")
}
fn with_capability(url: &str, cap: &str) -> String {
let (head, frag) = split_fragment(url);
match frag.strip_prefix('#').unwrap_or("") {
"" => format!("{head}#{CAPABILITY_KEY}={cap}"),
rest => format!("{head}#{CAPABILITY_KEY}={cap}&{rest}"),
}
}
pub const SCHEME: &str = "snyvi";
pub fn app_url(id: &str) -> String {
format!("{SCHEME}://d/{id}")
}
pub fn resolve(target: &str) -> String {
let base = crate::config::base_url();
if let Some(rest) = target.strip_prefix(&format!("{SCHEME}:")) {
let rest = rest.trim_start_matches('/');
return format!("{base}/{rest}");
}
if target.starts_with("http://") || target.starts_with("https://") {
return target.to_string();
}
format!("{base}/d/{target}")
}
pub fn window_installed() -> bool {
window_binary().is_some()
}
fn window_binary() -> Option<PathBuf> {
let name = crate::platform::exe("snyvi-app");
if let Ok(exe) = std::env::current_exe() {
let exe = exe.canonicalize().unwrap_or(exe);
if let Some(sibling) = exe.parent().map(|d| d.join(&name)) {
if sibling.is_file() {
return Some(sibling);
}
}
}
if let Some(path) = std::env::var_os("PATH") {
if let Some(found) = std::env::split_paths(&path)
.map(|d| d.join(&name))
.find(|c| c.is_file())
{
return Some(found);
}
}
crate::platform::app_bundles("snyvi.app")
.into_iter()
.map(|app| app.join("Contents/MacOS").join(&name))
.find(|c| c.is_file())
}
pub fn hand_to_window(url: &str) -> bool {
if !crate::platform::has_display() {
return false;
}
let Some(bin) = window_binary() else {
return false;
};
Command::new(&bin)
.arg(url)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.is_ok()
}
pub fn open(url: &str, capability: Option<&str>) -> anyhow::Result<()> {
if crate::platform::has_display() {
if let Some(bin) = window_binary() {
let marked = match capability {
Some(cap) => with_capability(&marked(url), cap),
None => marked(url),
};
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
let e = Command::new(&bin).arg(&marked).exec();
eprintln!("snyvi: {}: {e}", bin.display());
}
#[cfg(not(unix))]
match Command::new(&bin).arg(&marked).status() {
Ok(_) => return Ok(()),
Err(e) => eprintln!("snyvi: {}: {e}", bin.display()),
}
}
}
for browser in crate::platform::app_mode_browsers() {
let ok = Command::new(&browser)
.arg(format!("--app={url}"))
.arg("--window-size=1280,860")
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.is_ok();
if ok {
say_rung(&format!("this is {} in app mode", program_name(&browser)));
return Ok(());
}
}
if crate::platform::open_url(url) {
say_rung("this is the default browser");
} else {
say_rung("and no browser could be opened from here");
eprintln!(" open this yourself: {url}");
}
Ok(())
}
fn say_rung(what: &str) {
if !crate::platform::has_display() {
eprintln!("snyvi: no display, so no window; {what}");
return;
}
if window_binary().is_some() {
eprintln!("snyvi: the native window could not be started; {what}");
return;
}
let get = if cfg!(target_os = "macos") {
"put snyvi.app in Applications for one"
} else if cfg!(windows) {
"put snyvi-app.exe beside snyvi.exe for one"
} else {
"install the snyvi-app package for one"
};
eprintln!("snyvi: no native window installed, so {what}; {get}");
}
fn program_name(path: &str) -> String {
std::path::Path::new(path)
.file_stem()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| path.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_link_a_url_and_an_id_all_resolve_on_the_daemon() {
let base = crate::config::base_url();
assert_eq!(resolve("snyvi://d/abc"), format!("{base}/d/abc"));
assert_eq!(resolve("snyvi://"), format!("{base}/"));
assert_eq!(resolve("abc"), format!("{base}/d/abc"));
assert_eq!(
resolve("http://127.0.0.1:7777/d/abc"),
"http://127.0.0.1:7777/d/abc"
);
}
#[test]
fn the_mark_joins_whatever_query_is_there() {
assert_eq!(marked("http://h:1"), "http://h:1?window=1");
assert_eq!(marked("http://h:1/d/x?v=2"), "http://h:1/d/x?v=2&window=1");
}
#[test]
fn the_mark_goes_in_front_of_a_fragment_rather_than_into_it() {
assert_eq!(marked("http://h:1/d/x#top"), "http://h:1/d/x?window=1#top");
assert_eq!(
marked("http://h:1/d/x?v=2#L4-L9"),
"http://h:1/d/x?v=2&window=1#L4-L9"
);
}
#[test]
fn the_capability_leads_the_fragment_and_keeps_what_was_there() {
let cap = "a".repeat(64);
assert_eq!(
with_capability("http://h:1/?window=1", &cap),
format!("http://h:1/?window=1#cap={cap}")
);
assert_eq!(
with_capability("http://h:1/d/x?window=1#L4-L9", &cap),
format!("http://h:1/d/x?window=1#cap={cap}&L4-L9")
);
}
#[test]
fn app_url_is_the_scheme_and_the_document() {
assert_eq!(app_url("abc"), "snyvi://d/abc");
}
}