mirror of
https://github.com/priyanshujain/margin-mail.git
synced 2026-10-02 19:17:05 +00:00
scaffold the app: tauri crate, vite front end, icons, ci
This commit is contained in:
commit
088ec9c6e4
267 files changed
+18617
No files matched your search
@@ -0,0 +1 @@
|
||||
// The IPC contract. Every type here has a matching declaration in src/ipc.ts.
|
||||
@@ -0,0 +1,229 @@
|
||||
mod dto;
|
||||
mod library;
|
||||
|
||||
#[cfg(desktop)]
|
||||
use tauri::menu::{Menu, MenuItemBuilder, MenuItemKind, PredefinedMenuItem, SubmenuBuilder};
|
||||
#[cfg(desktop)]
|
||||
use tauri::Runtime;
|
||||
use tauri::{Emitter, Manager};
|
||||
|
||||
/// The frontend treats this as an invalidation signal with a scope: the reason names what moved
|
||||
/// (`threads`, `thread:<key>`, `accounts`, `settings`, `state`), so a note landing does not make
|
||||
/// the list refetch its bodies. Mailspring's typed delta stream in one string.
|
||||
pub fn emit_store_changed(app: &tauri::AppHandle, reason: &str) {
|
||||
let _ = app.emit("store-changed", reason);
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
fn build_menu<R: Runtime>(handle: &tauri::AppHandle<R>) -> tauri::Result<Menu<R>> {
|
||||
let menu = Menu::default(handle)?;
|
||||
|
||||
let new_message = MenuItemBuilder::with_id("new-message", "New Message")
|
||||
.accelerator("CmdOrCtrl+N")
|
||||
.build(handle)?;
|
||||
let command_palette = MenuItemBuilder::with_id("command-palette", "Command Palette…")
|
||||
.accelerator("CmdOrCtrl+K")
|
||||
.build(handle)?;
|
||||
let sync_now = MenuItemBuilder::with_id("sync-now", "Sync Now")
|
||||
.accelerator("CmdOrCtrl+R")
|
||||
.build(handle)?;
|
||||
let check_updates =
|
||||
MenuItemBuilder::with_id("check-updates", "Check for Updates…").build(handle)?;
|
||||
let settings = MenuItemBuilder::with_id("settings", "Settings…")
|
||||
.accelerator("CmdOrCtrl+,")
|
||||
.build(handle)?;
|
||||
let search = MenuItemBuilder::with_id("search", "Search…")
|
||||
.accelerator("CmdOrCtrl+F")
|
||||
.build(handle)?;
|
||||
let shortcuts = MenuItemBuilder::with_id("shortcuts", "Keyboard Shortcuts")
|
||||
.accelerator("CmdOrCtrl+/")
|
||||
.build(handle)?;
|
||||
let report_issue = MenuItemBuilder::with_id("report-issue", "Report an Issue…").build(handle)?;
|
||||
|
||||
let submenus: Vec<_> = menu
|
||||
.items()?
|
||||
.into_iter()
|
||||
.filter_map(|item| match item {
|
||||
MenuItemKind::Submenu(submenu) => Some(submenu),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let find_submenu = |name: &str| {
|
||||
submenus
|
||||
.iter()
|
||||
.find(|submenu| submenu.text().map(|t| t == name).unwrap_or(false))
|
||||
.cloned()
|
||||
};
|
||||
|
||||
match find_submenu("File") {
|
||||
Some(submenu) => {
|
||||
submenu.prepend_items(&[
|
||||
&new_message,
|
||||
&command_palette,
|
||||
&PredefinedMenuItem::separator(handle)?,
|
||||
&sync_now,
|
||||
&PredefinedMenuItem::separator(handle)?,
|
||||
])?;
|
||||
}
|
||||
None => {
|
||||
let submenu = SubmenuBuilder::new(handle, "File")
|
||||
.item(&new_message)
|
||||
.item(&command_palette)
|
||||
.item(&PredefinedMenuItem::separator(handle)?)
|
||||
.item(&sync_now)
|
||||
.build()?;
|
||||
menu.insert(&submenu, 1)?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(edit) = find_submenu("Edit") {
|
||||
edit.append_items(&[&PredefinedMenuItem::separator(handle)?, &search])?;
|
||||
}
|
||||
|
||||
if let Some(help) = find_submenu("Help") {
|
||||
help.append_items(&[&shortcuts, &report_issue])?;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
if let Some(app_submenu) = submenus.first() {
|
||||
app_submenu.insert(&check_updates, 1)?;
|
||||
app_submenu.insert(&settings, 3)?;
|
||||
app_submenu.insert(&PredefinedMenuItem::separator(handle)?, 4)?;
|
||||
}
|
||||
if let Some(view) = find_submenu("View") {
|
||||
let inbox = MenuItemBuilder::with_id("place-inbox", "Inbox")
|
||||
.accelerator("CmdOrCtrl+1")
|
||||
.build(handle)?;
|
||||
let feed = MenuItemBuilder::with_id("place-feed", "Feed")
|
||||
.accelerator("CmdOrCtrl+2")
|
||||
.build(handle)?;
|
||||
let paper_trail = MenuItemBuilder::with_id("place-paper-trail", "Paper Trail")
|
||||
.accelerator("CmdOrCtrl+3")
|
||||
.build(handle)?;
|
||||
let toggle_pane = MenuItemBuilder::with_id("toggle-pane", "Reading Pane")
|
||||
.accelerator("CmdOrCtrl+\\")
|
||||
.build(handle)?;
|
||||
view.prepend_items(&[
|
||||
&inbox,
|
||||
&feed,
|
||||
&paper_trail,
|
||||
&PredefinedMenuItem::separator(handle)?,
|
||||
&toggle_pane,
|
||||
&PredefinedMenuItem::separator(handle)?,
|
||||
])?;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
if let Some(file) = find_submenu("File") {
|
||||
file.append_items(&[&PredefinedMenuItem::separator(handle)?, &check_updates])?;
|
||||
}
|
||||
if let Some(edit) = find_submenu("Edit") {
|
||||
edit.append_items(&[&settings])?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(menu)
|
||||
}
|
||||
|
||||
/// The other half of closing on macOS. The red button and Cmd+W hide the window rather than destroy
|
||||
/// it, the way every mail client does: the app stays in the Dock with sync still running, and Cmd+Q
|
||||
/// is what quits. AppKit does nothing of its own for a Dock click when it can see no window.
|
||||
#[cfg(target_os = "macos")]
|
||||
fn show_main_window(app: &tauri::AppHandle) {
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
let _ = window.unminimize();
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
|
||||
/// The package manager that owns this install, when one does. The Nix wrapper sets it to "nix":
|
||||
/// the binary lives in a read-only store there, so the updater may announce a version but not
|
||||
/// install it.
|
||||
#[tauri::command]
|
||||
fn packaged_by() -> Option<String> {
|
||||
std::env::var("MARGIN_MAIL_PACKAGED_BY")
|
||||
.ok()
|
||||
.filter(|manager| !manager.is_empty())
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
// generate_context! first, so the updater plugin registers only when the merged config actually
|
||||
// has an `updater` key. Ported from margin's lib.rs.
|
||||
let context = tauri::generate_context!();
|
||||
|
||||
#[cfg_attr(mobile, allow(unused_mut))]
|
||||
let mut builder = tauri::Builder::default()
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_os::init())
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
.plugin(tauri_plugin_deep_link::init());
|
||||
|
||||
#[cfg(desktop)]
|
||||
{
|
||||
builder = builder.plugin(tauri_plugin_process::init());
|
||||
if context.config().plugins.0.contains_key("updater") {
|
||||
builder = builder.plugin(tauri_plugin_updater::Builder::new().build());
|
||||
}
|
||||
}
|
||||
|
||||
builder = builder.setup(|_app| Ok(()));
|
||||
|
||||
#[cfg(desktop)]
|
||||
{
|
||||
builder = builder
|
||||
.menu(|handle| build_menu(handle))
|
||||
.on_menu_event(|app, event| {
|
||||
if matches!(
|
||||
event.id().0.as_str(),
|
||||
"new-message"
|
||||
| "command-palette"
|
||||
| "sync-now"
|
||||
| "check-updates"
|
||||
| "settings"
|
||||
| "search"
|
||||
| "shortcuts"
|
||||
| "place-inbox"
|
||||
| "place-feed"
|
||||
| "place-paper-trail"
|
||||
| "toggle-pane"
|
||||
| "report-issue"
|
||||
) {
|
||||
app.emit("menu-action", event.id().0.as_str()).ok();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
builder = builder.on_window_event(|window, event| {
|
||||
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
|
||||
api.prevent_close();
|
||||
let _ = window.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let app = builder
|
||||
.invoke_handler(tauri::generate_handler![packaged_by])
|
||||
.build(context)
|
||||
.expect("error while building Margin Mail");
|
||||
|
||||
app.run(|app, event| {
|
||||
#[cfg(target_os = "macos")]
|
||||
if let tauri::RunEvent::Reopen {
|
||||
has_visible_windows: false,
|
||||
..
|
||||
} = event
|
||||
{
|
||||
show_main_window(app);
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let _ = (app, event);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// The two filesystem chores every module needs and nobody should write twice.
|
||||
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tauri::Manager;
|
||||
|
||||
pub fn app_data_dir(app: &tauri::AppHandle) -> Result<PathBuf, String> {
|
||||
let dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
|
||||
fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
|
||||
Ok(dir)
|
||||
}
|
||||
|
||||
/// Write through a sibling temporary file and rename over the target.
|
||||
///
|
||||
/// Ported from margin's `project.rs`. Every settings write and every keymap write goes through it,
|
||||
/// because the alternative is a truncated `settings.json` after a crash mid-write, and a truncated
|
||||
/// settings file is an app that opens on the welcome screen with the accounts still on disk.
|
||||
pub fn atomic_write(path: &Path, bytes: &[u8]) -> Result<(), String> {
|
||||
let dir = path.parent().ok_or("that path has no directory")?;
|
||||
fs::create_dir_all(dir).map_err(|e| e.to_string())?;
|
||||
|
||||
let temp = path.with_extension(format!(
|
||||
"{}.tmp",
|
||||
path.extension().and_then(|e| e.to_str()).unwrap_or("")
|
||||
));
|
||||
{
|
||||
let mut file = fs::File::create(&temp).map_err(|e| e.to_string())?;
|
||||
file.write_all(bytes).map_err(|e| e.to_string())?;
|
||||
file.sync_all().map_err(|e| e.to_string())?;
|
||||
}
|
||||
fs::rename(&temp, path).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn atomic_write_replaces_the_file_and_leaves_no_temporary() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let path = dir.path().join("settings.json");
|
||||
|
||||
atomic_write(&path, b"{\"one\":1}").expect("first write");
|
||||
atomic_write(&path, b"{\"two\":2}").expect("second write");
|
||||
|
||||
assert_eq!(fs::read_to_string(&path).unwrap(), "{\"two\":2}");
|
||||
let left: Vec<_> = fs::read_dir(dir.path())
|
||||
.unwrap()
|
||||
.map(|e| e.unwrap().file_name().to_string_lossy().to_string())
|
||||
.collect();
|
||||
assert_eq!(left, vec!["settings.json".to_string()]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
margin_mail_lib::run()
|
||||
}
|
||||
Reference in new issue
Block a user