feat(sdk,plugins): API publique d'accès fichiers/workspace + analyse structure (#124,#129)
This commit is contained in:
@ -397,6 +397,21 @@ pub fn run() {
|
||||
plugins::plugin_set_enabled,
|
||||
plugins::plugin_uninstall,
|
||||
plugins::plugin_list_runtime_contributions,
|
||||
plugins::plugin_workspace_read_text,
|
||||
plugins::plugin_workspace_read_binary,
|
||||
plugins::plugin_workspace_write_text,
|
||||
plugins::plugin_workspace_write_binary,
|
||||
plugins::plugin_workspace_list_dir,
|
||||
plugins::plugin_workspace_stat,
|
||||
plugins::plugin_query_project_structure,
|
||||
plugins::plugin_config_read_document,
|
||||
plugins::plugin_config_update_document,
|
||||
plugins::plugin_task_run_command,
|
||||
plugins::plugin_task_get_status,
|
||||
plugins::plugin_toolchain_diagnose,
|
||||
plugins::plugin_events_subscribe,
|
||||
plugins::plugin_events_poll,
|
||||
plugins::plugin_events_unsubscribe,
|
||||
plugins::plugin_open_plugins_folder,
|
||||
commands::get_server_exposure_settings,
|
||||
commands::save_server_exposure_settings,
|
||||
@ -415,6 +430,28 @@ pub fn run() {
|
||||
.expect("error while running IdeA Tauri application");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn plugin_workspace_invoke_handler<R: tauri::Runtime>(
|
||||
) -> impl Fn(tauri::ipc::Invoke<R>) -> bool + Send + Sync + 'static {
|
||||
tauri::generate_handler![
|
||||
plugins::plugin_workspace_read_text,
|
||||
plugins::plugin_workspace_read_binary,
|
||||
plugins::plugin_workspace_write_text,
|
||||
plugins::plugin_workspace_write_binary,
|
||||
plugins::plugin_workspace_list_dir,
|
||||
plugins::plugin_workspace_stat,
|
||||
plugins::plugin_query_project_structure,
|
||||
plugins::plugin_config_read_document,
|
||||
plugins::plugin_config_update_document,
|
||||
plugins::plugin_task_run_command,
|
||||
plugins::plugin_task_get_status,
|
||||
plugins::plugin_toolchain_diagnose,
|
||||
plugins::plugin_events_subscribe,
|
||||
plugins::plugin_events_poll,
|
||||
plugins::plugin_events_unsubscribe,
|
||||
]
|
||||
}
|
||||
|
||||
async fn app_exit_work_guard_state(
|
||||
handle: &tauri::AppHandle,
|
||||
) -> Result<application::AppExitWorkGuardState, AppError> {
|
||||
@ -713,6 +750,7 @@ fn persisted_monitor_is_available(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::plugin_workspace_invoke_handler;
|
||||
use super::{
|
||||
apply_main_close_decision, confirm_next_main_window_close, consume_exit_guard_confirmation,
|
||||
decide_main_close_action, persisted_view_identity_from_label, persisted_window_identity,
|
||||
@ -720,7 +758,10 @@ mod tests {
|
||||
};
|
||||
use super::{should_close_with_main_window, PersistedWindowKind};
|
||||
use application::AppExitWorkGuardState;
|
||||
use serde_json::json;
|
||||
use std::cell::Cell;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tauri::test::{get_ipc_response, mock_builder, mock_context, noop_assets, INVOKE_KEY};
|
||||
|
||||
#[test]
|
||||
fn main_close_without_work_allows_shutdown_without_preventing_close() {
|
||||
@ -880,4 +921,289 @@ mod tests {
|
||||
);
|
||||
assert!(persisted_window_identity("view-tickets-not-a-project").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dto_plugins_workspace_commands_are_registered_in_tauri_invoke_handler() {
|
||||
let app_data = test_app_data_dir("plugin-workspace-commands");
|
||||
let app = mock_builder()
|
||||
.manage(crate::state::AppState::build(app_data.clone()))
|
||||
.invoke_handler(plugin_workspace_invoke_handler())
|
||||
.build(mock_context(noop_assets()))
|
||||
.expect("mock app builds");
|
||||
let webview = tauri::WebviewWindowBuilder::new(&app, "main", Default::default())
|
||||
.build()
|
||||
.expect("mock webview builds");
|
||||
let missing_project = uuid::Uuid::from_u128(124).to_string();
|
||||
|
||||
for command in [
|
||||
"plugin_workspace_read_text",
|
||||
"plugin_workspace_read_binary",
|
||||
"plugin_workspace_list_dir",
|
||||
"plugin_workspace_stat",
|
||||
"plugin_query_project_structure",
|
||||
] {
|
||||
let err = invoke_plugin_command(
|
||||
&webview,
|
||||
command,
|
||||
json!({
|
||||
"input": {
|
||||
"projectId": missing_project.clone(),
|
||||
"path": "src/main.rs",
|
||||
"maxDepth": 2,
|
||||
"maxEntries": 10
|
||||
}
|
||||
}),
|
||||
)
|
||||
.expect_err("missing project must surface through the registered command");
|
||||
assert_eq!(err["code"], "NOT_FOUND", "{command}");
|
||||
}
|
||||
|
||||
for (command, input) in [
|
||||
(
|
||||
"plugin_workspace_write_text",
|
||||
json!({
|
||||
"projectId": missing_project.clone(),
|
||||
"path": "generated.txt",
|
||||
"content": "hello\n"
|
||||
}),
|
||||
),
|
||||
(
|
||||
"plugin_workspace_write_binary",
|
||||
json!({
|
||||
"projectId": missing_project.clone(),
|
||||
"path": "generated.bin",
|
||||
"bytes": [1, 2, 3]
|
||||
}),
|
||||
),
|
||||
] {
|
||||
let err = invoke_plugin_command(&webview, command, json!({ "input": input }))
|
||||
.expect_err("missing project must surface through the registered command");
|
||||
assert_eq!(err["code"], "NOT_FOUND", "{command}");
|
||||
}
|
||||
|
||||
std::fs::remove_dir_all(app_data).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dto_plugins_config_document_commands_are_registered_in_tauri_invoke_handler() {
|
||||
let app_data = test_app_data_dir("plugin-config-document-commands");
|
||||
let app = mock_builder()
|
||||
.manage(crate::state::AppState::build(app_data.clone()))
|
||||
.invoke_handler(plugin_workspace_invoke_handler())
|
||||
.build(mock_context(noop_assets()))
|
||||
.expect("mock app builds");
|
||||
let webview = tauri::WebviewWindowBuilder::new(&app, "main", Default::default())
|
||||
.build()
|
||||
.expect("mock webview builds");
|
||||
let missing_project = uuid::Uuid::from_u128(130).to_string();
|
||||
|
||||
let err = invoke_plugin_command(
|
||||
&webview,
|
||||
"plugin_config_read_document",
|
||||
json!({
|
||||
"input": {
|
||||
"projectId": missing_project.clone(),
|
||||
"path": "config/settings.json",
|
||||
"format": "json"
|
||||
}
|
||||
}),
|
||||
)
|
||||
.expect_err("missing project must surface through the registered command");
|
||||
assert_eq!(err["code"], "NOT_FOUND");
|
||||
|
||||
let err = invoke_plugin_command(
|
||||
&webview,
|
||||
"plugin_config_update_document",
|
||||
json!({
|
||||
"input": {
|
||||
"projectId": missing_project,
|
||||
"path": "config/settings.json",
|
||||
"format": "json",
|
||||
"mode": "mergePatch",
|
||||
"value": {"enabled": true}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.expect_err("missing project must surface through the registered command");
|
||||
assert_eq!(err["code"], "NOT_FOUND");
|
||||
|
||||
std::fs::remove_dir_all(app_data).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dto_plugins_command_task_commands_are_registered_in_tauri_invoke_handler() {
|
||||
let app_data = test_app_data_dir("plugin-task-commands");
|
||||
let app = mock_builder()
|
||||
.manage(crate::state::AppState::build(app_data.clone()))
|
||||
.invoke_handler(plugin_workspace_invoke_handler())
|
||||
.build(mock_context(noop_assets()))
|
||||
.expect("mock app builds");
|
||||
let webview = tauri::WebviewWindowBuilder::new(&app, "main", Default::default())
|
||||
.build()
|
||||
.expect("mock webview builds");
|
||||
let missing_project = uuid::Uuid::from_u128(125).to_string();
|
||||
let owner = uuid::Uuid::from_u128(126).to_string();
|
||||
|
||||
let err = invoke_plugin_command(
|
||||
&webview,
|
||||
"plugin_task_run_command",
|
||||
json!({
|
||||
"input": {
|
||||
"projectId": missing_project,
|
||||
"ownerAgentId": owner,
|
||||
"label": "cargo test",
|
||||
"command": "cargo",
|
||||
"args": ["test"],
|
||||
"cwd": ".",
|
||||
"env": [["RUST_LOG", "debug"]],
|
||||
"recordOnly": true
|
||||
}
|
||||
}),
|
||||
)
|
||||
.expect_err("missing project must surface through the registered command");
|
||||
assert_eq!(err["code"], "NOT_FOUND");
|
||||
|
||||
let task_id = uuid::Uuid::from_u128(127).to_string();
|
||||
let value = invoke_plugin_command(
|
||||
&webview,
|
||||
"plugin_task_get_status",
|
||||
json!({
|
||||
"input": {
|
||||
"taskId": task_id
|
||||
}
|
||||
}),
|
||||
)
|
||||
.expect("unknown task is a successful empty status");
|
||||
assert_eq!(value, serde_json::Value::Null);
|
||||
|
||||
std::fs::remove_dir_all(app_data).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dto_plugins_toolchain_diagnostic_command_is_registered_in_tauri_invoke_handler() {
|
||||
let app_data = test_app_data_dir("plugin-toolchain-diagnostic-command");
|
||||
let app = mock_builder()
|
||||
.manage(crate::state::AppState::build(app_data.clone()))
|
||||
.invoke_handler(plugin_workspace_invoke_handler())
|
||||
.build(mock_context(noop_assets()))
|
||||
.expect("mock app builds");
|
||||
let webview = tauri::WebviewWindowBuilder::new(&app, "main", Default::default())
|
||||
.build()
|
||||
.expect("mock webview builds");
|
||||
let missing_project = uuid::Uuid::from_u128(126).to_string();
|
||||
|
||||
let err = invoke_plugin_command(
|
||||
&webview,
|
||||
"plugin_toolchain_diagnose",
|
||||
json!({
|
||||
"input": {
|
||||
"projectId": missing_project,
|
||||
"cwd": ".",
|
||||
"tools": [{
|
||||
"id": "rust",
|
||||
"executable": "cargo",
|
||||
"versionArgs": ["--version"],
|
||||
"required": true
|
||||
}],
|
||||
"env": [{
|
||||
"name": "RUSTUP_HOME",
|
||||
"required": false
|
||||
}],
|
||||
"files": [{
|
||||
"path": "Cargo.toml",
|
||||
"required": true,
|
||||
"kind": "file"
|
||||
}]
|
||||
}
|
||||
}),
|
||||
)
|
||||
.expect_err("missing project must surface through the registered command");
|
||||
assert_eq!(err["code"], "NOT_FOUND");
|
||||
|
||||
std::fs::remove_dir_all(app_data).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dto_plugins_event_commands_are_registered_in_tauri_invoke_handler() {
|
||||
let app_data = test_app_data_dir("plugin-event-commands");
|
||||
let app = mock_builder()
|
||||
.manage(crate::state::AppState::build(app_data.clone()))
|
||||
.invoke_handler(plugin_workspace_invoke_handler())
|
||||
.build(mock_context(noop_assets()))
|
||||
.expect("mock app builds");
|
||||
let webview = tauri::WebviewWindowBuilder::new(&app, "main", Default::default())
|
||||
.build()
|
||||
.expect("mock webview builds");
|
||||
let missing_project = uuid::Uuid::from_u128(127).to_string();
|
||||
|
||||
let err = invoke_plugin_command(
|
||||
&webview,
|
||||
"plugin_events_subscribe",
|
||||
json!({
|
||||
"input": {
|
||||
"projectId": missing_project,
|
||||
"eventTypes": ["workspaceFileChanged", "backgroundTaskChanged"],
|
||||
"capacity": 10
|
||||
}
|
||||
}),
|
||||
)
|
||||
.expect_err("missing project must surface through the registered command");
|
||||
assert_eq!(err["code"], "NOT_FOUND");
|
||||
|
||||
let subscription_id = uuid::Uuid::from_u128(128).to_string();
|
||||
let err = invoke_plugin_command(
|
||||
&webview,
|
||||
"plugin_events_poll",
|
||||
json!({
|
||||
"input": {
|
||||
"subscriptionId": subscription_id.clone(),
|
||||
"maxEvents": 10
|
||||
}
|
||||
}),
|
||||
)
|
||||
.expect_err("unknown subscription must surface through the registered command");
|
||||
assert_eq!(err["code"], "NOT_FOUND");
|
||||
|
||||
let disposed = invoke_plugin_command(
|
||||
&webview,
|
||||
"plugin_events_unsubscribe",
|
||||
json!({
|
||||
"input": {
|
||||
"subscriptionId": subscription_id
|
||||
}
|
||||
}),
|
||||
)
|
||||
.expect("unsubscribe is idempotent");
|
||||
assert_eq!(disposed["retention"], "disposed");
|
||||
|
||||
std::fs::remove_dir_all(app_data).ok();
|
||||
}
|
||||
|
||||
fn invoke_plugin_command<W: AsRef<tauri::Webview<tauri::test::MockRuntime>>>(
|
||||
webview: &W,
|
||||
command: &str,
|
||||
body: serde_json::Value,
|
||||
) -> Result<serde_json::Value, serde_json::Value> {
|
||||
get_ipc_response(
|
||||
webview,
|
||||
tauri::webview::InvokeRequest {
|
||||
cmd: command.to_owned(),
|
||||
callback: tauri::ipc::CallbackFn(0),
|
||||
error: tauri::ipc::CallbackFn(1),
|
||||
url: "tauri://localhost".parse().unwrap(),
|
||||
body: tauri::ipc::InvokeBody::Json(body),
|
||||
headers: Default::default(),
|
||||
invoke_key: INVOKE_KEY.to_owned(),
|
||||
},
|
||||
)
|
||||
.map(|body| body.deserialize::<serde_json::Value>().unwrap())
|
||||
}
|
||||
|
||||
fn test_app_data_dir(label: &str) -> std::path::PathBuf {
|
||||
let nanos = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
std::env::temp_dir().join(format!("idea-{label}-{}-{nanos}", std::process::id()))
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,8 +5,16 @@ use std::path::{Path, PathBuf};
|
||||
|
||||
use application::{ReviewPluginPackageInput, SetPluginEnabledInput, UninstallPluginInput};
|
||||
use backend::dto::{
|
||||
ErrorDto, PluginAdminDto, PluginInstallResultDto, PluginReviewDto,
|
||||
PluginRuntimeContributionCatalogDto, PluginUninstallResultDto, ReviewPluginPackageDto,
|
||||
ErrorDto, PluginAdminDto, PluginConfigDocumentDto, PluginConfigDocumentReadDto,
|
||||
PluginConfigDocumentUpdateDto, PluginConfigDocumentWriteResultDto, PluginEventBatchDto,
|
||||
PluginEventPollDto, PluginEventSubscribeDto, PluginEventSubscriptionDto,
|
||||
PluginEventUnsubscribeDto, PluginInstallResultDto, PluginProjectStructureDto,
|
||||
PluginProjectStructureQueryDto, PluginReviewDto, PluginRunCommandDto,
|
||||
PluginRuntimeContributionCatalogDto, PluginTaskDto, PluginTaskStatusDto,
|
||||
PluginToolchainDiagnosticDto, PluginToolchainDiagnosticRequestDto, PluginUninstallResultDto,
|
||||
PluginWorkspaceBinaryFileDto, PluginWorkspaceDirectoryListingDto, PluginWorkspacePathDto,
|
||||
PluginWorkspaceStatDto, PluginWorkspaceTextFileDto, PluginWorkspaceWriteBinaryDto,
|
||||
PluginWorkspaceWriteTextDto, ReviewPluginPackageDto,
|
||||
};
|
||||
use domain::ports::{PluginManifestValidator, PluginPackageStore, PluginRegistryStore};
|
||||
use domain::{PluginId, RelativePath};
|
||||
@ -156,6 +164,198 @@ pub async fn plugin_list_runtime_contributions(
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// Reads a UTF-8 workspace file for the public plugin API.
|
||||
#[tauri::command]
|
||||
pub async fn plugin_workspace_read_text(
|
||||
input: PluginWorkspacePathDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<PluginWorkspaceTextFileDto, ErrorDto> {
|
||||
state
|
||||
.plugin_workspace_access
|
||||
.read_text(input.into())
|
||||
.await
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// Reads a binary workspace file for the public plugin API.
|
||||
#[tauri::command]
|
||||
pub async fn plugin_workspace_read_binary(
|
||||
input: PluginWorkspacePathDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<PluginWorkspaceBinaryFileDto, ErrorDto> {
|
||||
state
|
||||
.plugin_workspace_access
|
||||
.read_binary(input.into())
|
||||
.await
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// Writes a UTF-8 workspace file for the public plugin API.
|
||||
#[tauri::command]
|
||||
pub async fn plugin_workspace_write_text(
|
||||
input: PluginWorkspaceWriteTextDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), ErrorDto> {
|
||||
state
|
||||
.plugin_workspace_access
|
||||
.write_text(input.into())
|
||||
.await
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// Writes a binary workspace file for the public plugin API.
|
||||
#[tauri::command]
|
||||
pub async fn plugin_workspace_write_binary(
|
||||
input: PluginWorkspaceWriteBinaryDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), ErrorDto> {
|
||||
state
|
||||
.plugin_workspace_access
|
||||
.write_binary(input.into())
|
||||
.await
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// Lists a workspace directory for the public plugin API.
|
||||
#[tauri::command]
|
||||
pub async fn plugin_workspace_list_dir(
|
||||
input: PluginWorkspacePathDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<PluginWorkspaceDirectoryListingDto, ErrorDto> {
|
||||
state
|
||||
.plugin_workspace_access
|
||||
.list_dir(input.into())
|
||||
.await
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// Stats a workspace path for the public plugin API.
|
||||
#[tauri::command]
|
||||
pub async fn plugin_workspace_stat(
|
||||
input: PluginWorkspacePathDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<PluginWorkspaceStatDto, ErrorDto> {
|
||||
state
|
||||
.plugin_workspace_access
|
||||
.stat(input.into())
|
||||
.await
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// Queries a bounded generic project structure for the public plugin API.
|
||||
#[tauri::command]
|
||||
pub async fn plugin_query_project_structure(
|
||||
input: PluginProjectStructureQueryDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<PluginProjectStructureDto, ErrorDto> {
|
||||
state
|
||||
.query_project_structure
|
||||
.execute(input.into())
|
||||
.await
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// Reads a structured configuration document for the public plugin API.
|
||||
#[tauri::command]
|
||||
pub async fn plugin_config_read_document(
|
||||
input: PluginConfigDocumentReadDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<PluginConfigDocumentDto, ErrorDto> {
|
||||
state
|
||||
.plugin_config_documents
|
||||
.read(input.into())
|
||||
.await
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// Updates a structured configuration document for the public plugin API.
|
||||
#[tauri::command]
|
||||
pub async fn plugin_config_update_document(
|
||||
input: PluginConfigDocumentUpdateDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<PluginConfigDocumentWriteResultDto, ErrorDto> {
|
||||
state
|
||||
.plugin_config_documents
|
||||
.update(input.into())
|
||||
.await
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// Launches a command-backed background task for the public plugin API.
|
||||
#[tauri::command]
|
||||
pub async fn plugin_task_run_command(
|
||||
input: PluginRunCommandDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<PluginTaskDto, ErrorDto> {
|
||||
state
|
||||
.plugin_command_tasks
|
||||
.run_command(input.into())
|
||||
.await
|
||||
.map(PluginTaskDto::from)
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// Reads one command task status for the public plugin API.
|
||||
#[tauri::command]
|
||||
pub async fn plugin_task_get_status(
|
||||
input: PluginTaskStatusDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Option<PluginTaskDto>, ErrorDto> {
|
||||
state
|
||||
.plugin_command_tasks
|
||||
.get_status(input.into())
|
||||
.await
|
||||
.map(|task| task.map(PluginTaskDto::from))
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// Diagnoses generic external toolchain prerequisites for the public plugin API.
|
||||
#[tauri::command]
|
||||
pub async fn plugin_toolchain_diagnose(
|
||||
input: PluginToolchainDiagnosticRequestDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<PluginToolchainDiagnosticDto, ErrorDto> {
|
||||
state
|
||||
.plugin_toolchain_diagnostics
|
||||
.diagnose(input.into())
|
||||
.await
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// Subscribes to stable public plugin events.
|
||||
#[tauri::command]
|
||||
pub async fn plugin_events_subscribe(
|
||||
input: PluginEventSubscribeDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<PluginEventSubscriptionDto, ErrorDto> {
|
||||
state
|
||||
.plugin_event_subscriptions
|
||||
.subscribe(input.into())
|
||||
.await
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// Drains retained public plugin events for one subscription.
|
||||
#[tauri::command]
|
||||
pub fn plugin_events_poll(
|
||||
input: PluginEventPollDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<PluginEventBatchDto, ErrorDto> {
|
||||
state
|
||||
.plugin_event_subscriptions
|
||||
.poll(input.into())
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// Disposes a public plugin event subscription.
|
||||
#[tauri::command]
|
||||
pub fn plugin_events_unsubscribe(
|
||||
input: PluginEventUnsubscribeDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> PluginEventSubscriptionDto {
|
||||
state.plugin_event_subscriptions.unsubscribe(input.into())
|
||||
}
|
||||
|
||||
/// Opens the plugin store folder, or one plugin folder when an id is provided.
|
||||
#[tauri::command]
|
||||
pub fn plugin_open_plugins_folder(
|
||||
|
||||
Reference in New Issue
Block a user