#![cfg_attr(windows, windows_subsystem = "windows")]
use std::path::PathBuf;
use std::sync::Once;
use std::time::Duration;
use tauri::{
image::Image,
ipc::CapabilityBuilder,
menu::{Menu, MenuItem, PredefinedMenuItem},
tray::TrayIconBuilder,
webview::PageLoadEvent,
Manager, WebviewUrl, WebviewWindow, WebviewWindowBuilder, WindowEvent,
};
use tauri_plugin_global_shortcut::{GlobalShortcutExt, ShortcutState};
const WINDOW_ICON: &[u8] = include_bytes!("../../icons/256.png");
const TRAY_ICON: &[u8] = include_bytes!("../../icons/tray.png");
#[cfg(target_os = "macos")]
const SHORTCUT: &str = "Cmd+Shift+Space";
#[cfg(not(target_os = "macos"))]
const SHORTCUT: &str = "Ctrl+Shift+Space";
const SCHEME: &str = "snyvi";
const DEFAULT_PORT: u16 = 7777;
const WINDOW_MARK: &str = concat!("window=", env!("CARGO_PKG_VERSION"));
const PAGE_WINDOW_COMMANDS: [&str; 9] = [
"core:window:allow-start-dragging",
"core:window:allow-internal-toggle-maximize",
"core:window:allow-minimize",
"core:window:allow-toggle-maximize",
"core:window:allow-close",
"core:window:allow-is-maximized",
"core:window:allow-set-fullscreen",
"core:window:allow-set-decorations",
"allow-allow-foreground",
];
#[tauri::command]
fn allow_foreground() {
#[cfg(windows)]
unsafe {
use windows_sys::Win32::UI::WindowsAndMessaging::{AllowSetForegroundWindow, ASFW_ANY};
AllowSetForegroundWindow(ASFW_ANY);
}
}
const FRAME_CHECK: &str = "window.__TAURI_INTERNALS__.invoke('plugin:window|set_decorations', \
{ value: !document.querySelector('[data-tauri-drag-region]') })";
const REMEMBERED: tauri_plugin_window_state::StateFlags =
tauri_plugin_window_state::StateFlags::from_bits_truncate(
tauri_plugin_window_state::StateFlags::all().bits()
& !tauri_plugin_window_state::StateFlags::DECORATIONS.bits()
& !tauri_plugin_window_state::StateFlags::FULLSCREEN.bits(),
);
fn main() {
let (parsed, quit) = target();
let home = parsed.origin().ascii_serialization();
let shortcut = shortcut_wanted();
let run = plugins(shortcut.is_some())
.setup(move |app| {
if quit {
std::process::exit(0);
}
let w = window(app, parsed, &home)?;
let tray = match tray(app.handle(), &w) {
Ok(()) => true,
Err(e) => {
eprintln!("snyvi-app: no tray icon ({e})");
false
}
};
if let Some(key) = &shortcut {
match app.global_shortcut().register(key.as_str()) {
Ok(()) => eprintln!("snyvi-app: {key} shows or hides the window from anywhere"),
Err(e) => eprintln!("snyvi-app: no global shortcut ({key}: {e})"),
}
}
#[cfg(any(target_os = "linux", windows))]
{
let handle = app.handle().clone();
std::thread::spawn(move || claim_scheme(&handle));
}
hide_on_close(app.handle(), &w, tray);
Ok(())
})
.invoke_handler(tauri::generate_handler![allow_foreground])
.build(tauri::generate_context!());
let app = match run {
Ok(app) => app,
Err(e) => {
eprintln!("snyvi-app: {e}");
std::process::exit(1);
}
};
app.run(|_app, _event| {
#[cfg(target_os = "macos")]
if let tauri::RunEvent::Opened { urls } = _event {
let Some(w) = _app.get_webview_window("main") else {
return;
};
if let Some(u) = urls.into_iter().find(|u| u.scheme() == SCHEME) {
open_in(&w, u);
}
reveal(&w);
}
});
}
fn target() -> (tauri::Url, bool) {
let url = match std::env::args().nth(1) {
Some(u) => u,
None => hand_to_snyvi(None),
};
let quit = url == "--quit";
let url = if quit { base_url(None) } else { url };
#[cfg(target_os = "linux")]
if std::env::var_os("DISPLAY").is_none() && std::env::var_os("WAYLAND_DISPLAY").is_none() {
eprintln!("snyvi-app: no display server");
std::process::exit(3);
}
let parsed: tauri::Url = match url.parse() {
Ok(u) => u,
Err(e) => {
eprintln!("snyvi-app: {url}: {e}");
std::process::exit(2);
}
};
let parsed = stamped(if parsed.scheme() == SCHEME {
if snyvi_binary().is_some() || !daemon_up() {
hand_to_snyvi(Some(&url));
}
resolve(&parsed, &base_url(None))
} else {
parsed
});
(parsed, quit)
}
fn plugins(shortcut: bool) -> tauri::Builder<tauri::Wry> {
let builder = tauri::Builder::default()
.plugin(tauri_plugin_single_instance::init(|app, argv, _cwd| {
if argv.get(1).map(String::as_str) == Some("--quit") {
app.exit(0);
return;
}
let Some(w) = app.get_webview_window("main") else {
return;
};
if let Some(u) = argv.get(1).and_then(|u| u.parse::<tauri::Url>().ok()) {
open_in(&w, u);
}
reveal(&w);
}))
.plugin(
tauri_plugin_window_state::Builder::default()
.with_state_flags(REMEMBERED)
.build(),
)
.plugin(tauri_plugin_deep_link::init());
match shortcut {
true => builder.plugin(
tauri_plugin_global_shortcut::Builder::new()
.with_handler(|app, _key, event| {
if event.state() != ShortcutState::Pressed {
return;
}
if let Some(w) = app.get_webview_window("main") {
toggle(&w);
}
})
.build(),
),
false => builder,
}
}
fn window(app: &tauri::App, parsed: tauri::Url, home: &str) -> tauri::Result<WebviewWindow> {
use tauri_plugin_window_state::WindowExt;
let at_home = home.to_string();
let frame = frame_wanted();
let mut cap = CapabilityBuilder::new("page-frame")
.local(false)
.remote(home.to_string())
.window("main");
for permission in PAGE_WINDOW_COMMANDS {
cap = cap.permission(permission);
}
if let Err(e) = app.add_capability(cap) {
eprintln!("snyvi-app: the page cannot drag or close the window ({e})");
}
let builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::External(parsed))
.title("snyvi")
.inner_size(1280.0, 860.0)
.min_inner_size(480.0, 320.0)
.decorations(frame);
#[cfg(target_os = "macos")]
let builder = match frame {
true => builder,
false => builder
.decorations(true)
.title_bar_style(tauri::TitleBarStyle::Overlay)
.hidden_title(true),
};
let w = builder
.on_navigation(move |url| {
if stays_home(url, &at_home) {
return true;
}
hand_to_desktop(url.as_str());
false
})
.on_new_window(|url, _features| {
hand_to_desktop(url.as_str());
tauri::webview::NewWindowResponse::Deny
})
.on_page_load(move |w, payload| {
if cfg!(target_os = "macos") || frame {
return;
}
if payload.event() == PageLoadEvent::Finished {
let _ = w.eval(FRAME_CHECK);
}
})
.icon(Image::from_bytes(WINDOW_ICON)?)?
.build()?;
let _ = w.restore_state(REMEMBERED);
#[cfg(target_os = "linux")]
paste_pictures(&w);
Ok(w)
}
fn hide_on_close(handle: &tauri::AppHandle, w: &WebviewWindow, tray: bool) {
use tauri_plugin_window_state::AppHandleExt;
let window = w.clone();
let handle = handle.clone();
w.on_window_event(move |event| {
if let WindowEvent::CloseRequested { api, .. } = event {
if !tray {
return;
}
api.prevent_close();
let _ = handle.save_window_state(REMEMBERED);
let _ = window.hide();
said_where_it_went();
}
});
}
fn open_in(w: &WebviewWindow, url: tauri::Url) {
let url = if url.scheme() == SCHEME {
resolve(&url, &base_url(Some(w)))
} else {
url
};
let _ = w.navigate(url);
}
fn stamped(mut url: tauri::Url) -> tauri::Url {
let pairs: Vec<(String, String)> = url
.query_pairs()
.map(|(k, v)| (k.into_owned(), v.into_owned()))
.collect();
if !pairs.iter().any(|(k, _)| k == "window") {
return url;
}
{
let mut q = url.query_pairs_mut();
q.clear();
for (k, v) in &pairs {
if k == "window" {
q.append_pair("window", env!("CARGO_PKG_VERSION"));
} else {
q.append_pair(k, v);
}
}
}
url
}
fn resolve(link: &tauri::Url, base: &str) -> tauri::Url {
let mut path = String::new();
if let Some(host) = link.host_str().filter(|h| !h.is_empty()) {
path.push('/');
path.push_str(host);
}
path.push_str(link.path());
if path.is_empty() {
path.push('/');
}
let frag = match link.fragment() {
Some(f) => format!("#{f}"),
None => String::new(),
};
let url = match link.query() {
Some(q) => format!("{base}{path}?{q}&{WINDOW_MARK}{frag}"),
None => format!("{base}{path}?{WINDOW_MARK}{frag}"),
};
url.parse()
.unwrap_or_else(|_| format!("{base}/").parse().expect("base url"))
}
fn base_url(w: Option<&WebviewWindow>) -> String {
if let Some(u) = w.and_then(|w| w.url().ok()) {
if u.scheme().starts_with("http") {
return u.origin().ascii_serialization();
}
}
format!("http://127.0.0.1:{}", port())
}
fn port() -> u16 {
std::env::var("SNYVI_PORT")
.ok()
.and_then(|p| p.parse().ok())
.unwrap_or(DEFAULT_PORT)
}
#[cfg(target_os = "linux")]
fn paste_pictures(w: &WebviewWindow) {
use gtk::gdk::{keys::constants as key, Atom, ModifierType, SELECTION_CLIPBOARD};
use gtk::prelude::*;
let page = w.clone();
let _ = w.with_webview(move |view| {
view.inner().connect_key_press_event(move |_, ev| {
if ev.state().contains(ModifierType::CONTROL_MASK) && ev.keyval().to_lower() == key::v {
let page = page.clone();
gtk::Clipboard::get(&SELECTION_CLIPBOARD).request_contents(&Atom::intern("TARGETS"), move |clip, offer| {
if !offer.targets_include_image(false) || offer.targets_include_text() {
return;
}
clip.request_image(move |_, pic| {
let Some(png) = pic.and_then(|p| p.save_to_bufferv("png", &[]).ok()) else {
return;
};
let url = format!("data:image/png;base64,{}", gtk::glib::base64_encode(&png));
let _ = page.eval(format!("dispatchEvent(new CustomEvent('snyvi-paste-image', {{ detail: '{url}' }}))"));
});
});
}
gtk::glib::Propagation::Proceed
});
});
}
fn daemon_up() -> bool {
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], port()));
std::net::TcpStream::connect_timeout(&addr, Duration::from_millis(300)).is_ok()
}
#[cfg(any(target_os = "linux", windows))]
fn claim_scheme(app: &tauri::AppHandle) {
use tauri_plugin_deep_link::DeepLinkExt;
#[cfg(target_os = "linux")]
{
let owner = std::process::Command::new("xdg-mime")
.args(["query", "default", &format!("x-scheme-handler/{SCHEME}")])
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.unwrap_or_default();
if !owner.is_empty() && !owner.contains("snyvi-app-handler") {
return;
}
}
#[cfg(windows)]
if app.deep_link().is_registered(SCHEME).unwrap_or(false) {
return;
}
if let Err(e) = app.deep_link().register(SCHEME) {
eprintln!("snyvi-app: {SCHEME}:// links will not open here ({e})");
}
}
fn stays_home(url: &tauri::Url, home: &str) -> bool {
url.scheme() == "about" || url.origin().ascii_serialization() == home
}
fn hand_to_desktop(url: &str) {
use std::process::{Command, Stdio};
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
let safe: String = url
.chars()
.filter(|c| *c != '"' && *c != '\n' && *c != '\r')
.collect();
if Command::new("cmd")
.arg("/C")
.raw_arg(format!("start \"\" \"{safe}\""))
.creation_flags(CREATE_NO_WINDOW)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.is_ok()
{
return;
}
}
#[cfg(not(windows))]
for opener in ["xdg-open", "open"] {
if Command::new(opener)
.arg(url)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.is_ok()
{
return;
}
}
eprintln!("snyvi-app: nothing on this desktop opens {url}");
}
fn tray(app: &tauri::AppHandle, window: &WebviewWindow) -> tauri::Result<()> {
let show = MenuItem::with_id(app, "show", "Show snyvi", true, None::<&str>)?;
let quit = MenuItem::with_id(app, "quit", "Quit snyvi", true, None::<&str>)?;
let menu = Menu::with_items(app, &[&show, &PredefinedMenuItem::separator(app)?, &quit])?;
let _ = window;
let builder = TrayIconBuilder::with_id("snyvi")
.icon(Image::from_bytes(TRAY_ICON)?)
.tooltip("snyvi")
.menu(&menu)
.on_menu_event(|app, event| match event.id().as_ref() {
"show" => {
if let Some(w) = app.get_webview_window("main") {
reveal(&w);
}
}
"quit" => app.exit(0),
_ => {}
});
#[cfg(target_os = "windows")]
let builder = {
use tauri::tray::{MouseButton, MouseButtonState, TrayIconEvent};
builder
.show_menu_on_left_click(false)
.on_tray_icon_event(|tray, event| {
if let TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Up,
..
} = event
{
if let Some(w) = tray.app_handle().get_webview_window("main") {
if w.is_visible().unwrap_or(false) {
let _ = w.hide();
} else {
reveal(&w);
}
}
}
})
};
builder.build(app)?;
Ok(())
}
fn reveal(w: &WebviewWindow) {
let _ = w.show();
let _ = w.unminimize();
let _ = w.set_focus();
}
fn toggle(w: &WebviewWindow) {
if w.is_visible().unwrap_or(false) && w.is_focused().unwrap_or(false) {
let _ = w.hide();
} else {
reveal(w);
}
}
fn shortcut_wanted() -> Option<String> {
let key = match std::env::var("SNYVI_SHORTCUT") {
Ok(v) => match v.trim() {
"" | "0" | "off" | "none" | "no" | "false" => return None,
k => k.to_string(),
},
Err(_) => SHORTCUT.to_string(),
};
#[cfg(target_os = "linux")]
if std::env::var_os("WAYLAND_DISPLAY").is_some() || std::env::var_os("DISPLAY").is_none() {
eprintln!(
"snyvi-app: no global shortcut on Wayland; bind a key to `snyvi app` in the desktop's settings"
);
return None;
}
Some(key)
}
fn frame_wanted() -> bool {
matches!(
std::env::var("SNYVI_FRAME").as_deref().map(str::trim),
Ok("1" | "yes" | "on" | "true")
)
}
fn snyvi_binary() -> Option<PathBuf> {
std::env::current_exe()
.ok()
.and_then(|p| p.canonicalize().ok())
.and_then(|p| {
p.parent()
.map(|d| d.join(if cfg!(windows) { "snyvi.exe" } else { "snyvi" }))
})
.filter(|p| p.is_file())
}
fn hand_to_snyvi(link: Option<&str>) -> ! {
if let Some(snyvi) = snyvi_binary() {
let mut cmd = std::process::Command::new(&snyvi);
cmd.arg("app");
if let Some(l) = link {
cmd.arg(l);
}
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
let e = cmd.exec();
eprintln!("snyvi-app: {}: {e}", snyvi.display());
}
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
cmd.creation_flags(CREATE_NO_WINDOW);
}
#[cfg(not(unix))]
match cmd.status() {
Ok(s) => std::process::exit(s.code().unwrap_or(1)),
Err(e) => eprintln!("snyvi-app: {}: {e}", snyvi.display()),
}
}
match link {
Some(l) => eprintln!(
"snyvi-app: {l}: no daemon on port {} and no snyvi beside this executable to start one; run `snyvi app` first",
port()
),
None => eprintln!("usage: snyvi-app <url> (normally run for you by `snyvi app`)"),
}
std::process::exit(2);
}
fn said_where_it_went() {
static ONCE: Once = Once::new();
ONCE.call_once(|| {
eprintln!(
"snyvi-app: hidden to the tray. Click the tray icon to show it, or Quit from its menu."
);
});
}
#[cfg(test)]
mod tests {
use super::*;
fn r(link: &str) -> String {
resolve(&link.parse().unwrap(), "http://127.0.0.1:7777").to_string()
}
fn goes(url: &str) -> bool {
stays_home(&url.parse().unwrap(), "http://127.0.0.1:7777")
}
#[test]
fn the_window_keeps_the_daemon_and_gives_the_web_away() {
assert!(goes("http://127.0.0.1:7777/"));
assert!(goes("http://127.0.0.1:7777/d/abc?window=1"));
assert!(goes("http://127.0.0.1:7777/api/docs/abc/raw"));
assert!(goes("about:blank"));
assert!(!goes("https://github.com/snymrova/snyvi"));
assert!(!goes("http://127.0.0.1:7778/d/abc"));
assert!(!goes("https://127.0.0.1:7777/d/abc"));
assert!(!goes("mailto:a@b.c"));
assert!(!goes("file:///etc/hosts"));
}
#[test]
fn a_link_is_the_daemon_address_with_the_mark() {
assert_eq!(
r("snyvi://d/abc"),
format!("http://127.0.0.1:7777/d/abc?{WINDOW_MARK}")
);
assert_eq!(
r("snyvi:///d/abc"),
format!("http://127.0.0.1:7777/d/abc?{WINDOW_MARK}")
);
assert_eq!(
r("snyvi://"),
format!("http://127.0.0.1:7777/?{WINDOW_MARK}")
);
assert_eq!(
r("snyvi://d/abc?v=2"),
format!("http://127.0.0.1:7777/d/abc?v=2&{WINDOW_MARK}")
);
}
#[test]
fn the_mark_carries_this_windows_version() {
let v = env!("CARGO_PKG_VERSION");
let s = |u: &str| stamped(u.parse().unwrap()).to_string();
assert_eq!(
s("http://127.0.0.1:7777/?window=1#cap=x"),
format!("http://127.0.0.1:7777/?window={v}#cap=x")
);
assert_eq!(
s("http://127.0.0.1:7777/d/abc?v=2&window=1"),
format!("http://127.0.0.1:7777/d/abc?v=2&window={v}")
);
assert_eq!(
s("http://127.0.0.1:7777/d/abc?v=2"),
"http://127.0.0.1:7777/d/abc?v=2",
"no mark, nothing added"
);
}
}