Host: developer mode (dev_* commands and the watcher) is served for every app that sets AppConfig::with_dev; SiteHooks::dev_theme_apply and dev_theme_end carry the app's theme file; commands::DEV, testkit::assert_dev_guards
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
c83f541006
commit
3402c1d85d
5 changed files with 396 additions and 5 deletions
|
|
@ -92,6 +92,10 @@ pub struct AppConfig {
|
|||
/// Encrypted folder sync of the settings backup: serves [`commands::SYNC`] and needs
|
||||
/// [`SiteHooks::backup_sections`]. ([`commands::WORKSPACES`] is served for every app.)
|
||||
pub sync: bool,
|
||||
/// Developer mode (live reload of plugins, custom CSS and a theme file): serves
|
||||
/// [`commands::DEV`] and runs the watcher while it is on; the theme file is the app's
|
||||
/// ([`SiteHooks::dev_theme_apply`]).
|
||||
pub dev: bool,
|
||||
/// Native updates and the hotfix channel: serves [`commands::UPDATE`] and runs the update loop.
|
||||
/// Needs `updater` (the plugin) and the `net` feature.
|
||||
#[cfg(feature = "net")]
|
||||
|
|
@ -121,6 +125,7 @@ impl AppConfig {
|
|||
media: false,
|
||||
fonts: false,
|
||||
sync: false,
|
||||
dev: false,
|
||||
#[cfg(feature = "net")]
|
||||
updates: None,
|
||||
}
|
||||
|
|
@ -133,6 +138,12 @@ impl AppConfig {
|
|||
self
|
||||
}
|
||||
|
||||
/// Serves the developer-mode commands ([`commands::DEV`]).
|
||||
pub const fn with_dev(mut self) -> Self {
|
||||
self.dev = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Serves the sync commands ([`commands::SYNC`]).
|
||||
pub const fn with_sync(mut self) -> Self {
|
||||
self.sync = true;
|
||||
|
|
@ -251,6 +262,16 @@ pub trait SiteHooks: Send + Sync + 'static {
|
|||
&[]
|
||||
}
|
||||
|
||||
/// Developer mode: show the theme file the owner picked (read `path`, make it the theme preview
|
||||
/// in every site tab). An error is shown with its file and line. By default themes are not
|
||||
/// supported and this does nothing.
|
||||
fn dev_theme_apply(&self, _app: &AppHandle, _path: &std::path::Path) -> Result<(), crate::devmode::DevError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Developer mode ended (or the theme file was cleared): end the preview [`Self::dev_theme_apply`] made.
|
||||
fn dev_theme_end(&self, _app: &AppHandle) {}
|
||||
|
||||
/// `tab` was laid out for `item` (existing or new), before it is placed.
|
||||
fn tab_placed(&self, _app: &AppHandle, _tab: &Webview, _item: &TabLayout) {}
|
||||
|
||||
|
|
@ -1169,6 +1190,11 @@ fn store_handler() -> impl Fn(tauri::ipc::Invoke<Wry>) -> bool + Send + Sync + '
|
|||
|_| false
|
||||
}
|
||||
|
||||
/// The developer-mode commands ([`commands::DEV`]).
|
||||
fn dev_handler() -> impl Fn(tauri::ipc::Invoke<Wry>) -> bool + Send + Sync + 'static {
|
||||
crate::dev_commands::handler()
|
||||
}
|
||||
|
||||
/// The workspaces and sync commands ([`commands::WORKSPACES`], [`commands::SYNC`]).
|
||||
fn sync_handler() -> impl Fn(tauri::ipc::Invoke<Wry>) -> bool + Send + Sync + 'static {
|
||||
crate::sync_commands::handler()
|
||||
|
|
@ -1236,6 +1262,7 @@ impl App {
|
|||
let fonts_handler = fonts_handler();
|
||||
let update_handler = update_handler();
|
||||
let sync_handler = sync_handler();
|
||||
let dev_handler = dev_handler();
|
||||
let builder = tauri::Builder::<Wry>::default()
|
||||
// Must be first: a second launch focuses the running window instead of fighting over
|
||||
// the app's data and WebView profile (and hands over its link).
|
||||
|
|
@ -1317,6 +1344,7 @@ impl App {
|
|||
crate::store_commands::init(&handle, &config);
|
||||
crate::media_commands::init(&handle);
|
||||
crate::sync_commands::init(&handle);
|
||||
crate::dev_commands::init(&handle);
|
||||
#[cfg(feature = "net")]
|
||||
crate::fonts_commands::init(&handle);
|
||||
#[cfg(feature = "net")]
|
||||
|
|
@ -1345,6 +1373,8 @@ impl App {
|
|||
update_handler(invoke)
|
||||
} else if commands::WORKSPACES.contains(&invoke.message.command()) || commands::SYNC.contains(&invoke.message.command()) {
|
||||
sync_handler(invoke)
|
||||
} else if commands::DEV.contains(&invoke.message.command()) {
|
||||
dev_handler(invoke)
|
||||
} else if commands::is_host(invoke.message.command()) {
|
||||
host_handler(invoke)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -109,9 +109,13 @@ pub const SYNC: &[&str] = &[
|
|||
"sync_mark_pulled",
|
||||
];
|
||||
|
||||
/// Shell-only, optional: developer mode (live reload of plugins, custom CSS and one theme file),
|
||||
/// served when the app sets `AppConfig::dev`.
|
||||
pub const DEV: &[&str] = &["dev_get", "dev_set", "dev_pick_theme", "dev_clear_theme", "dev_reload_plugin", "dev_report_error"];
|
||||
|
||||
/// Whether `command` is one of the host's (dispatched to the host, never to the app's handler).
|
||||
pub fn is_host(command: &str) -> bool {
|
||||
SHELL.contains(&command) || TAB.contains(&command) || SITE.contains(&command) || STORE.contains(&command) || MEDIA.contains(&command) || GFONTS.contains(&command) || UPDATE.contains(&command) || WORKSPACES.contains(&command) || SYNC.contains(&command)
|
||||
SHELL.contains(&command) || TAB.contains(&command) || SITE.contains(&command) || STORE.contains(&command) || MEDIA.contains(&command) || GFONTS.contains(&command) || UPDATE.contains(&command) || WORKSPACES.contains(&command) || SYNC.contains(&command) || DEV.contains(&command)
|
||||
}
|
||||
|
||||
/// The capability permission tauri-build generates for `command` (`tabs_sync` → `allow-tabs-sync`).
|
||||
|
|
@ -126,8 +130,8 @@ pub fn permission(command: &str) -> String {
|
|||
/// tauri_build::AppManifest::new().commands(waybetter_host::commands::manifest(&["draft_list"]))
|
||||
/// ```
|
||||
pub fn manifest(app: &[&'static str]) -> &'static [&'static str] {
|
||||
let mut all: Vec<&'static str> = Vec::with_capacity(app.len() + SHELL.len() + TAB.len() + SITE.len() + STORE.len() + MEDIA.len() + GFONTS.len() + UPDATE.len() + WORKSPACES.len() + SYNC.len());
|
||||
for command in app.iter().chain(SHELL).chain(TAB).chain(SITE).chain(STORE).chain(MEDIA).chain(GFONTS).chain(UPDATE).chain(WORKSPACES).chain(SYNC) {
|
||||
let mut all: Vec<&'static str> = Vec::with_capacity(app.len() + SHELL.len() + TAB.len() + SITE.len() + STORE.len() + MEDIA.len() + GFONTS.len() + UPDATE.len() + WORKSPACES.len() + SYNC.len() + DEV.len());
|
||||
for command in app.iter().chain(SHELL).chain(TAB).chain(SITE).chain(STORE).chain(MEDIA).chain(GFONTS).chain(UPDATE).chain(WORKSPACES).chain(SYNC).chain(DEV) {
|
||||
if !all.contains(command) {
|
||||
all.push(command);
|
||||
}
|
||||
|
|
@ -144,7 +148,7 @@ mod tests {
|
|||
let all = manifest(&["draft_list", "tabs_sync"]);
|
||||
assert_eq!(all[0], "draft_list");
|
||||
assert_eq!(all.iter().filter(|c| **c == "tabs_sync").count(), 1);
|
||||
for command in SHELL.iter().chain(TAB).chain(SITE).chain(STORE).chain(MEDIA).chain(GFONTS).chain(UPDATE).chain(WORKSPACES).chain(SYNC) {
|
||||
for command in SHELL.iter().chain(TAB).chain(SITE).chain(STORE).chain(MEDIA).chain(GFONTS).chain(UPDATE).chain(WORKSPACES).chain(SYNC).chain(DEV) {
|
||||
assert!(all.contains(command), "{command}");
|
||||
assert!(is_host(command));
|
||||
}
|
||||
|
|
@ -154,7 +158,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn shell_and_tab_commands_do_not_overlap() {
|
||||
assert!(!SHELL.iter().any(|command| TAB.contains(command) || SITE.contains(command) || STORE.contains(command) || MEDIA.contains(command) || GFONTS.contains(command) || UPDATE.contains(command) || WORKSPACES.contains(command) || SYNC.contains(command)));
|
||||
assert!(!SHELL.iter().any(|command| TAB.contains(command) || SITE.contains(command) || STORE.contains(command) || MEDIA.contains(command) || GFONTS.contains(command) || UPDATE.contains(command) || WORKSPACES.contains(command) || SYNC.contains(command) || DEV.contains(command)));
|
||||
assert!(!TAB.iter().any(|command| SITE.contains(command)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
346
crates/waybetter-host/src/dev_commands.rs
Normal file
346
crates/waybetter-host/src/dev_commands.rs
Normal file
|
|
@ -0,0 +1,346 @@
|
|||
//! Developer mode (live reload), native side, for every app that sets
|
||||
//! [`crate::app::AppConfig::dev`] ([`crate::commands::DEV`]). Off by default. While on, a polling
|
||||
//! watcher ([`crate::devmode`]) sees the plugins folder, `custom.css` and one theme file the owner
|
||||
//! picked, and reloads what changed in every site tab: the same plugin start and stop scripts, the
|
||||
//! same custom-CSS script and the app's own theme preview the settings use. Syntax and start
|
||||
//! errors are kept (and shown as a toast in the site tabs) with the file and line.
|
||||
//!
|
||||
//! Every command is shell-only; site tabs gain no command, they only receive scripts native code
|
||||
//! already evaluates. The theme file is the app's ([`crate::app::SiteHooks::dev_theme_apply`]).
|
||||
|
||||
use crate::app::{config, hooks, page, tr};
|
||||
use crate::devmode::{self, DevError};
|
||||
use crate::host::{emit_shell, eval_in_site_tabs, read_json, shell_only, write_json};
|
||||
use crate::plugins;
|
||||
use serde::Serialize;
|
||||
use std::collections::VecDeque;
|
||||
use std::path::Path;
|
||||
use std::sync::Mutex;
|
||||
use tauri::{AppHandle, Manager, Webview};
|
||||
use tauri_plugin_dialog::DialogExt;
|
||||
|
||||
const MAX_DEV_ERRORS: usize = 20;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct DevState {
|
||||
inner: Mutex<DevInner>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct DevInner {
|
||||
settings: devmode::DevSettings,
|
||||
errors: VecDeque<DevErrorEntry>,
|
||||
/// The theme preview in every site tab is developer mode's (so turning it off ends it).
|
||||
owns_preview: bool,
|
||||
/// Bumped when what is watched changes; the watcher then takes a new baseline.
|
||||
generation: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct DevErrorEntry {
|
||||
#[serde(flatten)]
|
||||
error: DevError,
|
||||
at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase", tag = "kind")]
|
||||
enum DevEvent {
|
||||
/// A plugin file changed; the shell checks its syntax, then asks `dev_reload_plugin`.
|
||||
Check { file: String, source: String },
|
||||
Reloaded { target: String, file: Option<String> },
|
||||
Error { error: DevError },
|
||||
Changed,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct DevStatus {
|
||||
enabled: bool,
|
||||
theme_file: Option<String>,
|
||||
plugins_folder: String,
|
||||
errors: Vec<DevErrorEntry>,
|
||||
}
|
||||
|
||||
fn dev_state(app: &AppHandle) -> Result<tauri::State<'_, DevState>, String> {
|
||||
app.try_state::<DevState>().ok_or_else(|| "UNAVAILABLE: this app has no developer mode".to_owned())
|
||||
}
|
||||
|
||||
fn now() -> String {
|
||||
chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true)
|
||||
}
|
||||
|
||||
fn eval_in_site(app: &AppHandle, script: &str) {
|
||||
eval_in_site_tabs(app, config(app).site, script);
|
||||
}
|
||||
|
||||
fn dev_status(app: &AppHandle) -> Result<DevStatus, String> {
|
||||
let dev = dev_state(app)?;
|
||||
let inner = dev.inner.lock().map_err(|_| "DEV: busy")?;
|
||||
let folder = page(app)?.plugins.dir().to_string_lossy().into_owned();
|
||||
Ok(DevStatus { enabled: inner.settings.enabled, theme_file: inner.settings.theme_file.clone(), plugins_folder: folder, errors: inner.errors.iter().cloned().collect() })
|
||||
}
|
||||
|
||||
/// A danger toast through the plugin UI kit of the page runtime, in the top frame of a site tab.
|
||||
fn toast_script(app: &AppHandle, message: &str) -> String {
|
||||
let site = config(app).site;
|
||||
let message = serde_json::to_string(message).unwrap_or_else(|_| "\"\"".into());
|
||||
let origin = serde_json::to_string(&site.primary_origin()).unwrap_or_else(|_| "\"\"".into());
|
||||
format!(
|
||||
"(()=>{{if(window.top!==window||location.origin!=={origin})return;const W=window.{};\
|
||||
try{{W&&W.api&&W.api.ui&&W.api.ui.toast({message},{{tone:'danger',duration:12000}});}}catch(e){{}}}})();",
|
||||
site.runtime_global
|
||||
)
|
||||
}
|
||||
|
||||
fn record_error(app: &AppHandle, error: DevError) {
|
||||
let where_ = match (error.line, error.column) {
|
||||
(Some(line), Some(column)) => format!("{}:{line}:{column}", error.file),
|
||||
(Some(line), None) => format!("{}:{line}", error.file),
|
||||
_ => error.file.clone(),
|
||||
};
|
||||
eval_in_site(app, &toast_script(app, &format!("{where_} — {}", error.message)));
|
||||
if let Ok(dev) = dev_state(app) {
|
||||
if let Ok(mut inner) = dev.inner.lock() {
|
||||
inner.errors.push_front(DevErrorEntry { error: error.clone(), at: now() });
|
||||
inner.errors.truncate(MAX_DEV_ERRORS);
|
||||
}
|
||||
}
|
||||
emit_shell(app, devmode::EVENT, DevEvent::Error { error });
|
||||
}
|
||||
|
||||
fn clear_errors_for(app: &AppHandle, file: &str) {
|
||||
if let Ok(dev) = dev_state(app) {
|
||||
if let Ok(mut inner) = dev.inner.lock() {
|
||||
inner.errors.retain(|entry| entry.error.file != file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads the saved settings and starts the watcher (idle until developer mode is turned on). Does
|
||||
/// nothing unless the app set [`crate::app::AppConfig::dev`].
|
||||
pub(crate) fn init(app: &AppHandle) {
|
||||
if !config(app).dev {
|
||||
return;
|
||||
}
|
||||
let settings = read_json::<devmode::DevSettings>(app, devmode::FILE).and_then(|settings| settings.validated().ok()).unwrap_or_default();
|
||||
app.manage(DevState { inner: Mutex::new(DevInner { settings, ..Default::default() }) });
|
||||
let handle = app.clone();
|
||||
std::thread::Builder::new().name("wb-dev-watch".into()).spawn(move || watch_loop(handle)).ok();
|
||||
}
|
||||
|
||||
fn watch_loop(app: AppHandle) {
|
||||
let mut watcher = devmode::Watcher::new();
|
||||
let mut generation = u64::MAX;
|
||||
loop {
|
||||
std::thread::sleep(devmode::POLL);
|
||||
let Ok(dev) = dev_state(&app) else { return };
|
||||
let (enabled, theme_file, current) = match dev.inner.lock() {
|
||||
Ok(inner) => (inner.settings.enabled, inner.settings.theme_file.clone(), inner.generation),
|
||||
Err(_) => continue,
|
||||
};
|
||||
drop(dev);
|
||||
if !enabled {
|
||||
generation = u64::MAX;
|
||||
std::thread::sleep(std::time::Duration::from_millis(800));
|
||||
continue;
|
||||
}
|
||||
if current != generation {
|
||||
watcher.reset();
|
||||
generation = current;
|
||||
}
|
||||
let Ok(plugins_dir) = page(&app).map(|page| page.plugins.dir().to_path_buf()) else { continue };
|
||||
let scan = devmode::scan(&plugins_dir, theme_file.as_deref().map(Path::new));
|
||||
for change in watcher.observe(std::time::Instant::now(), scan) {
|
||||
handle_change(&app, &change, theme_file.as_deref());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_dev_theme(app: &AppHandle, path: &str) {
|
||||
match hooks(app).dev_theme_apply(app, Path::new(path)) {
|
||||
Ok(()) => {
|
||||
if let Ok(dev) = dev_state(app) {
|
||||
if let Ok(mut inner) = dev.inner.lock() {
|
||||
inner.owns_preview = true;
|
||||
}
|
||||
}
|
||||
let file = Path::new(path).file_name().map(|name| name.to_string_lossy().into_owned());
|
||||
if let Some(file) = &file {
|
||||
clear_errors_for(app, file);
|
||||
}
|
||||
emit_shell(app, devmode::EVENT, DevEvent::Reloaded { target: "theme".into(), file });
|
||||
}
|
||||
Err(error) => record_error(app, error),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_change(app: &AppHandle, change: &devmode::Change, theme_file: Option<&str>) {
|
||||
use devmode::{Change, Target};
|
||||
let plugins = config(app).plugins;
|
||||
match change {
|
||||
Change::Changed(Target::Theme) => {
|
||||
if let Some(path) = theme_file {
|
||||
apply_dev_theme(app, path);
|
||||
}
|
||||
}
|
||||
Change::Removed(Target::Theme) => {
|
||||
let file = theme_file.and_then(|path| Path::new(path).file_name()).map(|name| name.to_string_lossy().into_owned()).unwrap_or_default();
|
||||
record_error(app, DevError { file, line: None, column: None, message: "The theme file was deleted or renamed.".into() });
|
||||
}
|
||||
Change::Changed(Target::Css) | Change::Removed(Target::Css) => {
|
||||
let css = page(app).map(|page| page.plugins.css()).unwrap_or_default();
|
||||
if let Ok(script) = plugins::css_script(plugins, &css) {
|
||||
eval_in_site(app, &script);
|
||||
}
|
||||
emit_shell(app, devmode::EVENT, DevEvent::Reloaded { target: "css".into(), file: Some("custom.css".into()) });
|
||||
}
|
||||
Change::Changed(Target::Plugin(file)) => {
|
||||
let (enabled, source) = match page(app) {
|
||||
Ok(page) => {
|
||||
let enabled = page.plugins.list().map(|list| list.iter().any(|plugin| &plugin.file == file && plugin.enabled)).unwrap_or(false);
|
||||
(enabled, std::fs::read_to_string(page.plugins.dir().join(file)).ok())
|
||||
}
|
||||
Err(_) => return,
|
||||
};
|
||||
match (enabled, source) {
|
||||
(true, Some(source)) if source.len() as u64 <= plugins::MAX_PLUGIN_BYTES => {
|
||||
emit_shell(app, devmode::EVENT, DevEvent::Check { file: file.clone(), source });
|
||||
}
|
||||
(true, _) => record_error(app, DevError { file: file.clone(), line: None, column: None, message: "Not readable as UTF-8 text under 2 MiB.".into() }),
|
||||
(false, _) => emit_shell(app, devmode::EVENT, DevEvent::Changed),
|
||||
}
|
||||
}
|
||||
Change::Removed(Target::Plugin(file)) => {
|
||||
if let Ok(script) = plugins::stop_script(plugins, file) {
|
||||
eval_in_site(app, &script);
|
||||
}
|
||||
emit_shell(app, devmode::EVENT, DevEvent::Changed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn save_dev(app: &AppHandle, settings: &devmode::DevSettings) -> Result<(), String> {
|
||||
write_json(app, devmode::FILE, settings)
|
||||
}
|
||||
|
||||
fn end_dev_preview(app: &AppHandle) {
|
||||
let owned = dev_state(app).ok().and_then(|dev| dev.inner.lock().ok().map(|mut inner| std::mem::take(&mut inner.owns_preview))).unwrap_or(false);
|
||||
if owned {
|
||||
hooks(app).dev_theme_end(app);
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn dev_get(webview: Webview, app: AppHandle) -> Result<DevStatus, String> {
|
||||
shell_only(&webview)?;
|
||||
dev_status(&app)
|
||||
}
|
||||
|
||||
/// Turns developer mode on or off. Off ends the watched theme's preview.
|
||||
#[tauri::command]
|
||||
pub(crate) async fn dev_set(webview: Webview, app: AppHandle, enabled: bool) -> Result<DevStatus, String> {
|
||||
shell_only(&webview)?;
|
||||
let (settings, theme_file) = {
|
||||
let dev = dev_state(&app)?;
|
||||
let mut inner = dev.inner.lock().map_err(|_| "DEV: busy")?;
|
||||
inner.settings.enabled = enabled;
|
||||
inner.generation = inner.generation.wrapping_add(1);
|
||||
(inner.settings.clone(), inner.settings.theme_file.clone())
|
||||
};
|
||||
save_dev(&app, &settings)?;
|
||||
emit_shell(&app, devmode::EVENT, DevEvent::Changed);
|
||||
if enabled {
|
||||
if let Some(path) = theme_file {
|
||||
apply_dev_theme(&app, &path);
|
||||
}
|
||||
} else {
|
||||
end_dev_preview(&app);
|
||||
}
|
||||
dev_status(&app)
|
||||
}
|
||||
|
||||
/// Picks the theme JSON file to live-apply (shown in every site tab while developer mode is on).
|
||||
#[tauri::command]
|
||||
pub(crate) async fn dev_pick_theme(webview: Webview, app: AppHandle) -> Result<DevStatus, String> {
|
||||
shell_only(&webview)?;
|
||||
let title = tr(&app, "native.dialog.themeFile", "Choose a theme file to live-reload");
|
||||
let filter = tr(&app, "native.dialog.themeFilter", "Theme");
|
||||
let picked = app.dialog().file().set_parent(&webview.window()).set_title(title).add_filter(filter, &["json"]).blocking_pick_file();
|
||||
let Some(picked) = picked else { return dev_status(&app) };
|
||||
let path = picked.into_path().map_err(|_| "DEV: not a local file")?;
|
||||
let settings = {
|
||||
let dev = dev_state(&app)?;
|
||||
let mut inner = dev.inner.lock().map_err(|_| "DEV: busy")?;
|
||||
let next = devmode::DevSettings { theme_file: Some(path.to_string_lossy().into_owned()), ..inner.settings.clone() }.validated()?;
|
||||
inner.settings = next.clone();
|
||||
inner.generation = inner.generation.wrapping_add(1);
|
||||
next
|
||||
};
|
||||
save_dev(&app, &settings)?;
|
||||
if settings.enabled {
|
||||
apply_dev_theme(&app, settings.theme_file.as_deref().unwrap_or_default());
|
||||
}
|
||||
dev_status(&app)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn dev_clear_theme(webview: Webview, app: AppHandle) -> Result<DevStatus, String> {
|
||||
shell_only(&webview)?;
|
||||
let settings = {
|
||||
let dev = dev_state(&app)?;
|
||||
let mut inner = dev.inner.lock().map_err(|_| "DEV: busy")?;
|
||||
inner.settings.theme_file = None;
|
||||
inner.generation = inner.generation.wrapping_add(1);
|
||||
inner.settings.clone()
|
||||
};
|
||||
save_dev(&app, &settings)?;
|
||||
end_dev_preview(&app);
|
||||
dev_status(&app)
|
||||
}
|
||||
|
||||
/// Reloads plugin `file` in every site tab after the shell checked its syntax: stop, then start the
|
||||
/// new version (evaluated with a sourceURL so errors name the file and line). Developer mode only.
|
||||
#[tauri::command]
|
||||
pub(crate) async fn dev_reload_plugin(webview: Webview, app: AppHandle, file: String) -> Result<(), String> {
|
||||
shell_only(&webview)?;
|
||||
if !dev_state(&app)?.inner.lock().map(|inner| inner.settings.enabled).unwrap_or(false) {
|
||||
return Err("DEV: developer mode is off".into());
|
||||
}
|
||||
let host = config(&app).plugins;
|
||||
let (stop, start) = {
|
||||
let page = page(&app)?;
|
||||
let enabled = page.plugins.list()?.iter().any(|plugin| plugin.file == file && plugin.enabled);
|
||||
if !enabled {
|
||||
return Err("DEV: that plugin is not enabled".into());
|
||||
}
|
||||
let source = std::fs::read_to_string(page.plugins.dir().join(&file)).map_err(|_| "DEV: plugin file not readable")?;
|
||||
(plugins::stop_script(host, &file)?, plugins::dev_start_script(host, &file, &source, devmode::SOURCE_PREFIX)?)
|
||||
};
|
||||
// The old version is stopped, then the new one starts, evaluated on its own so its sourceURL
|
||||
// line numbers are the file's. Scripts run in order in each tab.
|
||||
eval_in_site(&app, &stop);
|
||||
eval_in_site(&app, &start);
|
||||
clear_errors_for(&app, &file);
|
||||
emit_shell(&app, devmode::EVENT, DevEvent::Reloaded { target: "plugin".into(), file: Some(file) });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The shell found a syntax error in a changed plugin (it was not reloaded): recorded, and shown
|
||||
/// as a toast in the site tabs.
|
||||
#[tauri::command]
|
||||
pub(crate) async fn dev_report_error(webview: Webview, app: AppHandle, file: String, line: Option<usize>, column: Option<usize>, message: String) -> Result<(), String> {
|
||||
shell_only(&webview)?;
|
||||
if !plugins::valid_file_name(&file) {
|
||||
return Err("VALIDATION: invalid plugin file name".into());
|
||||
}
|
||||
let message: String = message.chars().filter(|ch| !ch.is_control()).take(300).collect();
|
||||
record_error(&app, DevError { file, line, column, message });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The developer-mode commands ([`crate::commands::DEV`]).
|
||||
pub(crate) fn handler() -> impl Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Send + Sync + 'static {
|
||||
tauri::generate_handler![dev_get, dev_set, dev_pick_theme, dev_clear_theme, dev_reload_plugin, dev_report_error]
|
||||
}
|
||||
|
|
@ -87,6 +87,8 @@ mod store_commands;
|
|||
pub mod media_commands;
|
||||
#[cfg(feature = "app")]
|
||||
mod sync_commands;
|
||||
#[cfg(feature = "app")]
|
||||
mod dev_commands;
|
||||
#[cfg(all(feature = "app", feature = "net"))]
|
||||
mod fonts_commands;
|
||||
#[cfg(all(feature = "app", feature = "net"))]
|
||||
|
|
|
|||
|
|
@ -251,6 +251,14 @@ pub fn assert_sync_guards() {
|
|||
}
|
||||
}
|
||||
|
||||
/// The developer-mode commands' wrappers check their caller first. Runs against the host's source.
|
||||
pub fn assert_dev_guards() {
|
||||
let source = include_str!("dev_commands.rs");
|
||||
for command in commands::DEV {
|
||||
assert_guarded(source, command, "shell_only(&webview)?");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -262,6 +270,7 @@ mod tests {
|
|||
assert_fonts_guards();
|
||||
assert_update_guards();
|
||||
assert_sync_guards();
|
||||
assert_dev_guards();
|
||||
}
|
||||
|
||||
fn caps_with(shell_extra: &str, tab_extra: &str, site_extra: &str) -> Capabilities {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue