Skip to content

Commit f5b33af

Browse files
committed
feat: index both en/zh_CN app names and show app name in Coco app language
After this commit, we index both English and Chinese application names so that searches in either language will work. And the names of the applications in search results and application list will be in the app language. Pizza index structure has been updated, but backward compatibility is preserved by keeping the support code for the old index field. The changes in this commit are not macOS-specific, it applies to all supported platforms. Though this feature won't work on Linux and Windows until we implement the localized app name support in the underlying applications-rs crate.
1 parent 993da9a commit f5b33af

File tree

4 files changed

+121
-30
lines changed

4 files changed

+121
-30
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Information about release notes of Coco App is provided here.
1919
- feat: sub extension can set 'platforms' now #847
2020
- feat: add extension uninstall option in settings #855
2121
- feat: impl extension settings 'hide_before_open' #862
22+
- feat: index both en/zh_CN app names and show app name in chosen language #875
2223

2324
### 🐛 Bug fix
2425

src-tauri/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ tauri-plugin-drag = "2"
6161
tauri-plugin-macos-permissions = "2"
6262
tauri-plugin-fs-pro = "2"
6363
tauri-plugin-screenshots = "2"
64-
applications = { git = "https://github.com/infinilabs/applications-rs", rev = "814b16ea84fc0c5aa432fc094dfd9aceb83f7e46" }
64+
applications = { git = "https://github.com/infinilabs/applications-rs", rev = "2f1f88d1880404c5f8d70ad950b859bd49922bee" }
6565
tokio-native-tls = "0.3" # For wss connections
6666
tokio = { version = "1", features = ["full"] }
6767
tokio-tungstenite = { version = "0.20", features = ["native-tls"] }

src-tauri/src/extension/built_in/application/with_feature.rs

Lines changed: 118 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,20 @@ use serde_json::Value as Json;
2828
use std::path::Path;
2929
use std::path::PathBuf;
3030
use tauri::{AppHandle, Manager, async_runtime};
31-
use tauri_plugin_fs_pro::{IconOptions, icon, metadata, name};
31+
use tauri_plugin_fs_pro::{IconOptions, icon, metadata};
3232
use tauri_plugin_global_shortcut::GlobalShortcutExt;
3333
use tauri_plugin_global_shortcut::Shortcut;
3434
use tauri_plugin_global_shortcut::ShortcutEvent;
3535
use tauri_plugin_global_shortcut::ShortcutState;
3636
use tauri_plugin_store::StoreExt;
3737
use tokio::sync::oneshot::Sender as OneshotSender;
3838

39+
// Deprecated. We no longer index this field, but to be backward-compatible, we
40+
// have to keep it.
3941
const FIELD_APP_NAME: &str = "app_name";
42+
43+
const FIELD_APP_NAME_ZH: &str = "app_name_zh";
44+
const FIELD_APP_NAME_EN: &str = "app_name_en";
4045
const FIELD_ICON_PATH: &str = "icon_path";
4146
const FIELD_APP_ALIAS: &str = "app_alias";
4247
const APPLICATION_SEARCH_SOURCE_ID: &str = "application";
@@ -96,17 +101,34 @@ fn get_app_path(app: &App) -> String {
96101
.expect("should be UTF-8 encoded")
97102
}
98103

99-
/// Helper function to return `app`'s path.
100-
///
101-
/// * macOS: extract `app_path`'s file name and remove the file extension
102-
/// * Windows/Linux: return the name specified in `.desktop` file
103-
async fn get_app_name(app: &App) -> String {
104-
if cfg!(any(target_os = "linux", target_os = "windows")) {
105-
app.name.clone()
106-
} else {
107-
let app_path = get_app_path(app);
108-
name(app_path.into()).await
104+
/// Helper function to return `app`'s Chinese name.
105+
async fn get_app_name_zh(app: &App) -> String {
106+
// First try zh_CN
107+
if let Some(name) = app.localized_app_names.get("zh_CN") {
108+
return name.clone();
109+
}
110+
// Then try zh_Hans
111+
if let Some(name) = app.localized_app_names.get("zh_Hans") {
112+
return name.clone();
113+
}
114+
115+
// Fall back to base name
116+
app.name.clone()
117+
}
118+
119+
/// Helper function to return `app`'s English name.
120+
async fn get_app_name_en(app: &App) -> String {
121+
// First try en_US
122+
if let Some(name) = app.localized_app_names.get("en_US") {
123+
return name.clone();
109124
}
125+
// Then try en
126+
if let Some(name) = app.localized_app_names.get("en") {
127+
return name.clone();
128+
}
129+
130+
// Fall back to base name
131+
app.name.clone()
110132
}
111133

112134
/// Helper function to return an absolute path to `app`'s icon.
@@ -202,9 +224,13 @@ async fn index_applications_if_not_indexed(
202224
pizza_engine_builder.set_data_store(disk_store);
203225

204226
let mut schema = Schema::new();
205-
let field_app_name = Property::builder(FieldType::Text).build();
227+
let field_app_name_zh = Property::builder(FieldType::Text).build();
206228
schema
207-
.add_property(FIELD_APP_NAME, field_app_name)
229+
.add_property(FIELD_APP_NAME_ZH, field_app_name_zh)
230+
.expect("no collision could happen");
231+
let field_app_name_en = Property::builder(FieldType::Text).build();
232+
schema
233+
.add_property(FIELD_APP_NAME_EN, field_app_name_en)
208234
.expect("no collision could happen");
209235
let property_icon = Property::builder(FieldType::Text).index(false).build();
210236
schema
@@ -249,21 +275,33 @@ async fn index_applications_if_not_indexed(
249275

250276
for app in apps.iter() {
251277
let app_path = get_app_path(app);
252-
let app_name = get_app_name(app).await;
278+
let app_name_zh = get_app_name_zh(app).await;
279+
let app_name_en = get_app_name_en(app).await;
253280
let app_icon_path = get_app_icon_path(&tauri_app_handle, app)
254281
.await
255282
.map_err(|str| anyhow::anyhow!(str))?;
256283
let app_alias = get_app_alias(&tauri_app_handle, &app_path).unwrap_or(String::new());
257284

258-
if app_name.is_empty() || app_name.eq(&tauri_app_handle.package_info().name) {
285+
// Skip if both names are empty
286+
if app_name_zh.is_empty() && app_name_en.is_empty() {
287+
continue;
288+
}
289+
290+
// Skip if this is Coco itself
291+
//
292+
// Coco does not have localized app names, so app_name_en and app_name_zh
293+
// should both have value "Coco-AI", so either should work.
294+
if app_name_en == tauri_app_handle.package_info().name {
259295
continue;
260296
}
261297

262298
// You cannot write `app_name.clone()` within the `doc!()` macro, we should fix this.
263-
let app_name_clone = app_name.clone();
299+
let app_name_zh_clone = app_name_zh.clone();
300+
let app_name_en_clone = app_name_en.clone();
264301
let app_path_clone = app_path.clone();
265302
let document = doc!( app_path_clone, {
266-
FIELD_APP_NAME => app_name_clone,
303+
FIELD_APP_NAME_ZH => app_name_zh_clone,
304+
FIELD_APP_NAME_EN => app_name_en_clone,
267305
FIELD_ICON_PATH => app_icon_path,
268306
FIELD_APP_ALIAS => app_alias,
269307
}
@@ -272,8 +310,8 @@ async fn index_applications_if_not_indexed(
272310
// We don't error out because one failure won't break the whole thing
273311
if let Err(e) = writer.create_document(document).await {
274312
warn!(
275-
"failed to index application [app name: '{}', app path: '{}'] due to error [{}]",
276-
app_name, app_path, e
313+
"failed to index application [app name zh: '{}', app name en: '{}', app path: '{}'] due to error [{}]",
314+
app_name_zh, app_name_en, app_path, e
277315
)
278316
}
279317
}
@@ -402,9 +440,17 @@ impl Task for SearchApplicationsTask {
402440
//
403441
// It will be passed to Pizza like "Google\nChrome". Using Display impl would result
404442
// in an invalid query DSL and serde will complain.
443+
//
444+
// In order to be backward compatible, we still do match and prefix queries to the
445+
// app_name field.
405446
let dsl = format!(
406-
"{{ \"query\": {{ \"bool\": {{ \"should\": [ {{ \"match\": {{ \"{FIELD_APP_NAME}\": {:?} }} }}, {{ \"prefix\": {{ \"{FIELD_APP_NAME}\": {:?} }} }} ] }} }} }}",
407-
self.query_string, self.query_string
447+
"{{ \"query\": {{ \"bool\": {{ \"should\": [ {{ \"match\": {{ \"{FIELD_APP_NAME_ZH}\": {:?} }} }}, {{ \"prefix\": {{ \"{FIELD_APP_NAME_ZH}\": {:?} }} }}, {{ \"match\": {{ \"{FIELD_APP_NAME_EN}\": {:?} }} }}, {{ \"prefix\": {{ \"{FIELD_APP_NAME_EN}\": {:?} }} }}, {{ \"match\": {{ \"{FIELD_APP_NAME}\": {:?} }} }}, {{ \"prefix\": {{ \"{FIELD_APP_NAME}\": {:?} }} }} ] }} }} }}",
448+
self.query_string,
449+
self.query_string,
450+
self.query_string,
451+
self.query_string,
452+
self.query_string,
453+
self.query_string
408454
);
409455

410456
let state = state
@@ -601,7 +647,7 @@ impl SearchSource for ApplicationSearchSource {
601647

602648
let total_hits = search_result.total_hits;
603649
let source = self.get_type();
604-
let hits = pizza_engine_hits_to_coco_hits(search_result.hits);
650+
let hits = pizza_engine_hits_to_coco_hits(search_result.hits).await;
605651

606652
Ok(QueryResponse {
607653
source,
@@ -611,9 +657,11 @@ impl SearchSource for ApplicationSearchSource {
611657
}
612658
}
613659

614-
fn pizza_engine_hits_to_coco_hits(
660+
async fn pizza_engine_hits_to_coco_hits(
615661
pizza_engine_hits: Option<Vec<PizzaEngineDocument>>,
616662
) -> Vec<(Document, f64)> {
663+
use crate::util::app_lang::{Lang, get_app_lang};
664+
617665
let Some(engine_hits) = pizza_engine_hits else {
618666
return Vec::new();
619667
};
@@ -622,10 +670,43 @@ fn pizza_engine_hits_to_coco_hits(
622670
for engine_hit in engine_hits {
623671
let score = engine_hit.score.unwrap_or(0.0) as f64;
624672
let mut document_fields = engine_hit.fields;
625-
let app_name = match document_fields.remove(FIELD_APP_NAME).unwrap() {
626-
FieldValue::Text(string) => string,
627-
_ => unreachable!("field name is of type Text"),
673+
674+
// Get both Chinese and English names
675+
let opt_app_name_zh = match document_fields.remove(FIELD_APP_NAME_ZH) {
676+
Some(FieldValue::Text(string)) => Some(string),
677+
_ => None,
678+
};
679+
let opt_app_name_en = match document_fields.remove(FIELD_APP_NAME_EN) {
680+
Some(FieldValue::Text(string)) => Some(string),
681+
_ => None,
628682
};
683+
let opt_app_name_deprecated = match document_fields.remove(FIELD_APP_NAME) {
684+
Some(FieldValue::Text(string)) => Some(string),
685+
_ => None,
686+
};
687+
688+
let app_name: String = {
689+
if let Some(legacy_app_name) = opt_app_name_deprecated {
690+
// Old version of index, which only contains the field app_name.
691+
legacy_app_name
692+
} else {
693+
// New version of index store the following 2 fields
694+
695+
let panic_msg = format!(
696+
"new version of index should contain field [{}] and [{}]",
697+
FIELD_APP_NAME_EN, FIELD_APP_NAME_ZH
698+
);
699+
let app_name_zh = opt_app_name_zh.expect(&panic_msg);
700+
let app_name_en = opt_app_name_en.expect(&panic_msg);
701+
702+
// Choose the appropriate name based on current language
703+
match get_app_lang().await {
704+
Lang::zh_CN => app_name_zh,
705+
Lang::en_US => app_name_en,
706+
}
707+
}
708+
};
709+
629710
let app_path = engine_hit.key.expect("key should be set to app path");
630711
let app_icon_path = match document_fields.remove(FIELD_ICON_PATH).unwrap() {
631712
FieldValue::Text(string) => string,
@@ -645,7 +726,7 @@ fn pizza_engine_hits_to_coco_hits(
645726
}),
646727
id: app_path.clone(),
647728
category: Some("Application".to_string()),
648-
title: Some(app_name.clone()),
729+
title: Some(app_name),
649730
icon: Some(app_icon_path),
650731
on_opened: Some(on_opened),
651732
url: Some(url),
@@ -1033,15 +1114,24 @@ pub async fn get_app_search_path(tauri_app_handle: AppHandle) -> Vec<String> {
10331114

10341115
#[tauri::command]
10351116
pub async fn get_app_list(tauri_app_handle: AppHandle) -> Result<Vec<Extension>, String> {
1117+
use crate::util::app_lang::{Lang, get_app_lang};
1118+
10361119
let search_paths = get_app_search_path(tauri_app_handle.clone()).await;
10371120
let apps = list_app_in(search_paths)?;
10381121

10391122
let mut app_entries = Vec::with_capacity(apps.len());
1123+
let lang = get_app_lang().await;
10401124

10411125
for app in apps {
1042-
let name = get_app_name(&app).await;
1126+
let name = match lang {
1127+
Lang::zh_CN => get_app_name_zh(&app).await,
1128+
Lang::en_US => get_app_name_en(&app).await,
1129+
};
10431130

10441131
// filter out Coco-AI
1132+
//
1133+
// Coco does not have localized app names, so regardless the chosen language, name
1134+
// should have value "Coco-AI".
10451135
if name.eq(&tauri_app_handle.package_info().name) {
10461136
continue;
10471137
}

0 commit comments

Comments
 (0)