Skip to content

Commit ee75f0d

Browse files
authored
feat: impl extension settings 'hide_before_open' (#862)
This commit implementes a new extension setting entry "hide_before_open": > Extension plugin.json ```json { "name": "Screenshot", "settings": { "hide_before_open": true } } ``` that, if set to true, Coco will hide the main window before opening the extension. Only entensions that can be opened can set their "settings" field, a check rule is added to guarantee this.
1 parent aaac874 commit ee75f0d

4 files changed

Lines changed: 205 additions & 87 deletions

File tree

docs/content.en/docs/release-notes/_index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Information about release notes of Coco App is provided here.
1818
- feat: support sending files in chat messages #764
1919
- feat: sub extension can set 'platforms' now #847
2020
- feat: add extension uninstall option in settings #855
21+
- feat: impl extension settings 'hide_before_open' #862
2122

2223
### 🐛 Bug fix
2324

src-tauri/src/common/document.rs

Lines changed: 108 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::extension::ExtensionSettings;
12
use serde::{Deserialize, Serialize};
23
use std::collections::HashMap;
34
use tauri::AppHandle;
@@ -30,17 +31,40 @@ pub struct EditorInfo {
3031
pub timestamp: Option<String>,
3132
}
3233

33-
/// Defines the action that would be performed when a document gets opened.
34+
/// Defines the action that would be performed when a [document](Document) gets opened.
35+
///
36+
/// "Document" is a uniform type that the backend uses to send the search results
37+
/// back to the frontend. Since Coco can search many sources, "Document" can
38+
/// represent different things, application, web page, local file, extensions, and
39+
/// so on. Each has its own specific open action.
3440
#[derive(Debug, Clone, Serialize, Deserialize)]
3541
pub(crate) enum OnOpened {
3642
/// Launch the application
3743
Application { app_path: String },
3844
/// Open the URL.
3945
Document { url: String },
46+
/// The document is an extension.
47+
Extension(ExtensionOnOpened),
48+
}
49+
50+
#[derive(Debug, Clone, Serialize, Deserialize)]
51+
pub(crate) struct ExtensionOnOpened {
52+
/// Different types of extensions have different open behaviors.
53+
pub(crate) ty: ExtensionOnOpenedType,
54+
/// Extensions settings. Some could affect open action.
55+
///
56+
/// Optional because not all extensions have their settings.
57+
pub(crate) settings: Option<ExtensionSettings>,
58+
}
59+
60+
#[derive(Debug, Clone, Serialize, Deserialize)]
61+
pub(crate) enum ExtensionOnOpenedType {
4062
/// Spawn a child process to run the `CommandAction`.
4163
Command {
4264
action: crate::extension::CommandAction,
4365
},
66+
/// Open the `link`.
67+
//
4468
// NOTE that this variant has the same definition as `struct Quicklink`, but we
4569
// cannot use it directly, its `link` field should be deserialized/serialized
4670
// from/to a string, but we need a JSON object here.
@@ -57,20 +81,24 @@ impl OnOpened {
5781
match self {
5882
Self::Application { app_path } => app_path.clone(),
5983
Self::Document { url } => url.clone(),
60-
Self::Command { action } => {
61-
const WHITESPACE: &str = " ";
62-
let mut ret = action.exec.clone();
63-
ret.push_str(WHITESPACE);
64-
if let Some(ref args) = action.args {
65-
ret.push_str(args.join(WHITESPACE).as_str());
84+
Self::Extension(ext_on_opened) => {
85+
match &ext_on_opened.ty {
86+
ExtensionOnOpenedType::Command { action } => {
87+
const WHITESPACE: &str = " ";
88+
let mut ret = action.exec.clone();
89+
ret.push_str(WHITESPACE);
90+
if let Some(ref args) = action.args {
91+
ret.push_str(args.join(WHITESPACE).as_str());
92+
}
93+
94+
ret
95+
}
96+
// Currently, our URL is static and does not support dynamic parameters.
97+
// The URL of a quicklink is nearly useless without such dynamic user
98+
// inputs, so until we have dynamic URL support, we just use "N/A".
99+
ExtensionOnOpenedType::Quicklink { .. } => String::from("N/A"),
66100
}
67-
68-
ret
69101
}
70-
// Currently, our URL is static and does not support dynamic parameters.
71-
// The URL of a quicklink is nearly useless without such dynamic user
72-
// inputs, so until we have dynamic URL support, we just use "N/A".
73-
Self::Quicklink { .. } => String::from("N/A"),
74102
}
75103
}
76104
}
@@ -95,65 +123,78 @@ pub(crate) async fn open(
95123

96124
homemade_tauri_shell_open(tauri_app_handle.clone(), url).await?
97125
}
98-
OnOpened::Command { action } => {
99-
log::debug!("open (execute) command [{:?}]", action);
100-
101-
let mut cmd = Command::new(action.exec);
102-
if let Some(args) = action.args {
103-
cmd.args(args);
104-
}
105-
let output = cmd.output().map_err(|e| e.to_string())?;
106-
// Sometimes, we wanna see the result in logs even though it doesn't fail.
107-
log::debug!(
108-
"executing open(Command) result, exit code: [{}], stdout: [{}], stderr: [{}]",
109-
output.status,
110-
String::from_utf8_lossy(&output.stdout),
111-
String::from_utf8_lossy(&output.stderr)
112-
);
113-
if !output.status.success() {
114-
log::warn!(
115-
"executing open(Command) failed, exit code: [{}], stdout: [{}], stderr: [{}]",
116-
output.status,
117-
String::from_utf8_lossy(&output.stdout),
118-
String::from_utf8_lossy(&output.stderr)
119-
);
120-
121-
return Err(format!(
122-
"Command failed, stderr [{}]",
123-
String::from_utf8_lossy(&output.stderr)
124-
));
125-
}
126-
}
127-
OnOpened::Quicklink {
128-
link,
129-
open_with: opt_open_with,
130-
} => {
131-
let url = link.concatenate_url(&extra_args);
132-
133-
log::debug!("open quicklink [{}] with [{:?}]", url, opt_open_with);
134-
135-
cfg_if::cfg_if! {
136-
// The `open_with` functionality is only supported on macOS, provided
137-
// by the `open -a` command.
138-
if #[cfg(target_os = "macos")] {
139-
let mut cmd = Command::new("open");
140-
if let Some(ref open_with) = opt_open_with {
141-
cmd.arg("-a");
142-
cmd.arg(open_with.as_str());
126+
OnOpened::Extension(ext_on_opened) => {
127+
// Apply the settings that would affect open behavior
128+
if let Some(settings) = ext_on_opened.settings {
129+
if let Some(should_hide) = settings.hide_before_open {
130+
if should_hide {
131+
crate::hide_coco(tauri_app_handle.clone()).await;
143132
}
144-
cmd.arg(&url);
133+
}
134+
}
145135

146-
let output = cmd.output().map_err(|e| format!("failed to spawn [open] due to error [{}]", e))?;
136+
match ext_on_opened.ty {
137+
ExtensionOnOpenedType::Command { action } => {
138+
log::debug!("open (execute) command [{:?}]", action);
147139

148-
if !output.status.success() {
149-
return Err(format!(
150-
"failed to open with app {:?}: {}",
151-
opt_open_with,
140+
let mut cmd = Command::new(action.exec);
141+
if let Some(args) = action.args {
142+
cmd.args(args);
143+
}
144+
let output = cmd.output().map_err(|e| e.to_string())?;
145+
// Sometimes, we wanna see the result in logs even though it doesn't fail.
146+
log::debug!(
147+
"executing open(Command) result, exit code: [{}], stdout: [{}], stderr: [{}]",
148+
output.status,
149+
String::from_utf8_lossy(&output.stdout),
152150
String::from_utf8_lossy(&output.stderr)
153-
));
151+
);
152+
if !output.status.success() {
153+
log::warn!(
154+
"executing open(Command) failed, exit code: [{}], stdout: [{}], stderr: [{}]",
155+
output.status,
156+
String::from_utf8_lossy(&output.stdout),
157+
String::from_utf8_lossy(&output.stderr)
158+
);
159+
160+
return Err(format!(
161+
"Command failed, stderr [{}]",
162+
String::from_utf8_lossy(&output.stderr)
163+
));
164+
}
165+
}
166+
ExtensionOnOpenedType::Quicklink {
167+
link,
168+
open_with: opt_open_with,
169+
} => {
170+
let url = link.concatenate_url(&extra_args);
171+
172+
log::debug!("open quicklink [{}] with [{:?}]", url, opt_open_with);
173+
174+
cfg_if::cfg_if! {
175+
// The `open_with` functionality is only supported on macOS, provided
176+
// by the `open -a` command.
177+
if #[cfg(target_os = "macos")] {
178+
let mut cmd = Command::new("open");
179+
if let Some(ref open_with) = opt_open_with {
180+
cmd.arg("-a");
181+
cmd.arg(open_with.as_str());
182+
}
183+
cmd.arg(&url);
184+
185+
let output = cmd.output().map_err(|e| format!("failed to spawn [open] due to error [{}]", e))?;
186+
187+
if !output.status.success() {
188+
return Err(format!(
189+
"failed to open with app {:?}: {}",
190+
opt_open_with,
191+
String::from_utf8_lossy(&output.stderr)
192+
));
193+
}
194+
} else {
195+
homemade_tauri_shell_open(tauri_app_handle.clone(), url).await?
196+
}
154197
}
155-
} else {
156-
homemade_tauri_shell_open(tauri_app_handle.clone(), url).await?
157198
}
158199
}
159200
}

src-tauri/src/extension/mod.rs

Lines changed: 60 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
pub(crate) mod built_in;
22
pub(crate) mod third_party;
33

4+
use crate::common::document::ExtensionOnOpened;
5+
use crate::common::document::ExtensionOnOpenedType;
46
use crate::common::document::OnOpened;
57
use crate::common::register::SearchSourceRegistry;
68
use crate::util::platform::Platform;
@@ -99,7 +101,7 @@ pub struct Extension {
99101

100102
/// Extension settings
101103
#[serde(skip_serializing_if = "Option::is_none")]
102-
settings: Option<Json>,
104+
settings: Option<ExtensionSettings>,
103105

104106
// We do not care about these fields, just take it regardless of what it is.
105107
screenshots: Option<Json>,
@@ -178,39 +180,71 @@ impl Extension {
178180
pub(crate) fn searchable(&self) -> bool {
179181
self.on_opened().is_some()
180182
}
183+
181184
/// Return what will happen when we open this extension.
182185
///
183186
/// `None` if it cannot be opened.
184187
pub(crate) fn on_opened(&self) -> Option<OnOpened> {
188+
let settings = self.settings.clone();
189+
185190
match self.r#type {
186-
ExtensionType::Group => None,
187-
ExtensionType::Extension => None,
188-
ExtensionType::Command => Some(OnOpened::Command {
189-
action: self.action.clone().unwrap_or_else(|| {
190-
panic!(
191-
"Command extension [{}]'s [action] field is not set, something wrong with your extension validity check", self.id
192-
)
193-
}),
194-
}),
195-
ExtensionType::Application => Some(OnOpened::Application {
196-
app_path: self.id.clone(),
197-
}),
191+
// This function, at the time of writing this comment, is primarily
192+
// used by third-party extensions.
193+
//
194+
// Built-in extensions don't use this as they are technically not
195+
// "struct Extension"s. Typically, they directly construct a
196+
// "struct Document" from their own type.
197+
ExtensionType::Calculator => unreachable!("this is handled by frontend"),
198+
ExtensionType::AiExtension => unreachable!(
199+
"currently, all AI extensions we have are non-searchable, so we won't open them"
200+
),
201+
ExtensionType::Application => {
202+
// We can have a impl like:
203+
//
204+
// Some(OnOpened::Application { app_path: self.id.clone() })
205+
//
206+
// but it won't be used.
207+
208+
unreachable!(
209+
"Applications are not \"struct Extension\" under the hood, they won't call this method"
210+
)
211+
}
212+
213+
// These 2 types of extensions cannot be opened
214+
ExtensionType::Group => return None,
215+
ExtensionType::Extension => return None,
216+
217+
ExtensionType::Command => {
218+
let ty = ExtensionOnOpenedType::Command {
219+
action: self.action.clone().unwrap_or_else(|| {
220+
panic!(
221+
"Command extension [{}]'s [action] field is not set, something wrong with your extension validity check", self.id
222+
)
223+
}),
224+
};
225+
226+
let extension_on_opened = ExtensionOnOpened { ty, settings };
227+
228+
Some(OnOpened::Extension(extension_on_opened))
229+
}
198230
ExtensionType::Quicklink => {
199231
let quicklink = self.quicklink.clone().unwrap_or_else(|| {
200232
panic!(
201233
"Quicklink extension [{}]'s [quicklink] field is not set, something wrong with your extension validity check", self.id
202234
)
203235
});
204236

205-
Some(OnOpened::Quicklink{
206-
link: quicklink.link,
207-
open_with: quicklink.open_with,
208-
})
237+
let ty = ExtensionOnOpenedType::Quicklink {
238+
link: quicklink.link,
239+
open_with: quicklink.open_with,
240+
};
241+
242+
let extension_on_opened = ExtensionOnOpened { ty, settings };
243+
244+
Some(OnOpened::Extension(extension_on_opened))
209245
}
210246
ExtensionType::Script => todo!("not supported yet"),
211247
ExtensionType::Setting => todo!("not supported yet"),
212-
ExtensionType::Calculator => None,
213-
ExtensionType::AiExtension => None,
214248
}
215249
}
216250

@@ -1078,6 +1112,13 @@ fn parse_dynamic_placeholder(content: &str) -> Result<QuicklinkLinkComponent, St
10781112
})
10791113
}
10801114

1115+
/// Built-in extension settings
1116+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1117+
pub(crate) struct ExtensionSettings {
1118+
/// If set, Coco main window would hide before opening this document/e
1119+
pub(crate) hide_before_open: Option<bool>,
1120+
}
1121+
10811122
#[cfg(test)]
10821123
mod tests {
10831124
use super::*;

0 commit comments

Comments
 (0)