diff --git a/Cargo.lock b/Cargo.lock index 4bfbbe2..f510d3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -146,6 +146,7 @@ dependencies = [ "global-hotkey", "objc2-app-kit", "objc2-foundation", + "parking_lot", "reqwew", "serde", "thiserror", @@ -154,6 +155,7 @@ dependencies = [ "tracing", "tracing-appender", "tracing-subscriber", + "xkeysym", ] [[package]] @@ -1753,6 +1755,7 @@ dependencies = [ "keyboard-types", "objc", "once_cell", + "serde", "thiserror", "windows-sys 0.52.0", "x11-dl", diff --git a/Cargo.toml b/Cargo.toml index b0c1d23..5c59774 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,8 @@ eframe = { version = "0.28", features = ["persistence"] } egui_extras = { version = "0.28", features = ["svg"] } enigo = { version = "0.2" } futures = { version = "0.3" } -global-hotkey = { version = "0.5" } +global-hotkey = { version = "0.5", features = ["serde"] } +parking_lot = { version = "0.12" } reqwew = { version = "0.2" } serde = { version = "1.0", features = ["derive"] } thiserror = { version = "1.0" } @@ -48,6 +49,9 @@ tracing-appender = { version = "0.2" } tracing-subscriber = { version = "0.3", features = ["env-filter"] } # llm_utils = { version = "0.0.6", optional = true } +[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies] +xkeysym = { version = "0.2" } + [target.'cfg(target_os = "macos")'.dependencies] objc2-app-kit = { version = "0.2", features = ["NSApplication", "NSResponder", "NSRunningApplication", "NSWindow"] } objc2-foundation = { version = "0.2" } diff --git a/src/air.rs b/src/air.rs index 503de32..d02a00d 100644 --- a/src/air.rs +++ b/src/air.rs @@ -99,12 +99,7 @@ impl App for AiR { } fn on_exit(&mut self, _: Option<&GlowContext>) { - self.services.quoter.abort(); - self.services.hotkey.abort(); - - if let Some(rt) = self.services.rt.take() { - rt.shutdown_background(); - } + self.services.abort(); } } diff --git a/src/component.rs b/src/component.rs index b7a3e2a..02e2b1f 100644 --- a/src/component.rs +++ b/src/component.rs @@ -27,6 +27,9 @@ use crate::prelude::*; #[derive(Debug)] pub struct Components { pub setting: Setting, + // Keyboard didn't implement `Send`, can't use it between threads. + // pub keyboard: Arc>, + // TODO?: move the lock to somewhere else. pub openai: Arc>, #[cfg(feature = "tokenizer")] pub tokenizer: Tokenizer, diff --git a/src/component/function.rs b/src/component/function.rs index e6b219e..3dfe86b 100644 --- a/src/component/function.rs +++ b/src/component/function.rs @@ -1,5 +1,7 @@ +// std +use std::borrow::Cow; // self -use super::setting::Translation; +use super::setting::Chat; #[derive(Debug)] pub enum Function { @@ -9,20 +11,14 @@ pub enum Function { TranslateDirectly, } impl Function { - pub fn prompt(&self, setting: &Translation) -> String { + pub fn is_directly(&self) -> bool { + matches!(self, Self::RewriteDirectly | Self::TranslateDirectly) + } + + pub fn prompt<'a>(&'a self, setting: &'a Chat) -> Cow { match self { - Self::Rewrite | Self::RewriteDirectly => - "As an English professor, assist me in refining this text. \ - Amend any grammatical errors and enhance the language to sound more like a native speaker.\ - Provide the refined text only, without any other things." - .into(), - Self::Translate | Self::TranslateDirectly => format!( - "As a language professor, assist me in translate this text from {} to {}. \ - Amend any grammatical errors and enhance the language to sound more like a native speaker.\ - Provide the translated text only, without any other things.", - setting.source.as_str(), - setting.target.as_str() - ), + Self::Rewrite | Self::RewriteDirectly => setting.rewrite.prompt(), + Self::Translate | Self::TranslateDirectly => setting.translation.prompt(), } } } diff --git a/src/component/keyboard.rs b/src/component/keyboard.rs index 49eb129..ca2b940 100644 --- a/src/component/keyboard.rs +++ b/src/component/keyboard.rs @@ -1,28 +1,30 @@ // crates.io use enigo::{Direction, Enigo, Key, Keyboard as _, Settings}; +#[cfg(all(unix, not(target_os = "macos")))] use xkeysym::key::c; // self use crate::prelude::*; #[derive(Debug)] -pub struct Keyboard { - pub enigo: Enigo, -} +pub struct Keyboard(pub Enigo); impl Keyboard { pub fn init() -> Result { - let enigo = Enigo::new(&Settings::default()).map_err(EnigoError::NewCon)?; - - Ok(Self { enigo }) + Ok(Self(Enigo::new(&Settings::default()).map_err(EnigoError::NewCon)?)) } pub fn copy(&mut self) -> Result<()> { - self.enigo.key(Key::Other(0x37), Direction::Press).map_err(EnigoError::Input)?; - self.enigo.key(Key::Other(0x08), Direction::Click).map_err(EnigoError::Input)?; - self.enigo.key(Key::Other(0x37), Direction::Release).map_err(EnigoError::Input)?; + self.0.key(Key::Meta, Direction::Press).map_err(EnigoError::Input)?; + // TODO: create a `CGKeyCode` table for macOS in `build.rs`. + #[cfg(target_os = "macos")] + self.0.key(Key::Other(0x08), Direction::Click).map_err(EnigoError::Input)?; + // TODO: Windows. + #[cfg(all(unix, not(target_os = "macos")))] + self.0.key(Key::Other(c), Direction::Click).map_err(EnigoError::Input)?; + self.0.key(Key::Meta, Direction::Release).map_err(EnigoError::Input)?; Ok(()) } pub fn text(&mut self, text: &str) -> Result<()> { - Ok(self.enigo.text(text).map_err(EnigoError::Input)?) + Ok(self.0.text(text).map_err(EnigoError::Input)?) } } diff --git a/src/component/setting.rs b/src/component/setting.rs index 1139712..a1eafc5 100644 --- a/src/component/setting.rs +++ b/src/component/setting.rs @@ -1,9 +1,9 @@ // std -use std::{fs, path::PathBuf}; +use std::{borrow::Cow, fs, path::PathBuf}; // crates.io use app_dirs2::AppDataType; use async_openai::config::OPENAI_API_BASE; -use eframe::egui::WidgetText; +use global_hotkey::hotkey::{Code, HotKey, Modifiers}; use serde::{Deserialize, Serialize}; // self use super::openai::Model; @@ -14,7 +14,8 @@ use crate::{prelude::*, APP_INFO}; pub struct Setting { pub general: General, pub ai: Ai, - pub translation: Translation, + pub chat: Chat, + pub hotkeys: Hotkeys, } impl Setting { pub fn path() -> Result { @@ -79,16 +80,58 @@ impl Default for Ai { } } -// TODO: add a super type for all the settings. +// TODO?: implement a `Prompt` trait. +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct Chat { + pub rewrite: Rewrite, + pub translation: Translation, +} +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct Rewrite { + pub prompt: String, +} +impl Rewrite { + pub fn prompt(&self) -> Cow { + Cow::Borrowed(&self.prompt) + } +} +impl Default for Rewrite { + fn default() -> Self { + Self { + prompt: "As language professor, assist me in refining this text. \ + Amend any grammatical errors and enhance the language to sound more like a native speaker.\ + Just provide the refined text only, without any other things." + .into(), + } + } +} #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct Translation { - pub source: Language, - pub target: Language, + pub prompt: String, + pub a: Language, + pub b: Language, +} +impl Translation { + pub fn prompt(&self) -> Cow { + Cow::Owned(format!( + "Assist me in translate this text between {} and {}. {}", + self.a.as_str(), + self.b.as_str(), + self.prompt + )) + } } impl Default for Translation { fn default() -> Self { - Self { source: Language::ZhCn, target: Language::EnGb } + Self { + prompt: "As a language professor, amend any grammatical errors and enhance the language to sound more like a native speaker. \ + Provide the translated text only, without any other things.".into(), + a: Language::ZhCn, + b: Language::EnGb, + } } } // https://www.alchemysoftware.com/livedocs/ezscript/Topics/Catalyst/Language.htm @@ -100,21 +143,22 @@ pub enum Language { // English (United Kingdom). EnGb, } -impl Language { - pub fn all() -> [Self; 2] { - [Self::ZhCn, Self::EnGb] - } - pub fn as_str(&self) -> &'static str { - match self { - Self::ZhCn => "zh-CN", - Self::EnGb => "en-GB", - } - } +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct Hotkeys { + pub rewrite: HotKey, + pub rewrite_directly: HotKey, + pub translate: HotKey, + pub translate_directly: HotKey, } -#[allow(clippy::from_over_into)] -impl Into for &Language { - fn into(self) -> WidgetText { - self.as_str().into() +impl Default for Hotkeys { + fn default() -> Self { + Self { + rewrite: HotKey::new(Some(Modifiers::CONTROL), Code::KeyT), + rewrite_directly: HotKey::new(Some(Modifiers::CONTROL), Code::KeyY), + translate: HotKey::new(Some(Modifiers::CONTROL), Code::KeyU), + translate_directly: HotKey::new(Some(Modifiers::CONTROL), Code::KeyI), + } } } diff --git a/src/component/tokenizer.rs b/src/component/tokenizer.rs index acff545..51cf744 100644 --- a/src/component/tokenizer.rs +++ b/src/component/tokenizer.rs @@ -1,7 +1,8 @@ // std -use std::sync::{Arc, RwLock}; +use std::sync::Arc; // crates.io use llm_utils::tokenizer::LlmTokenizer; +use parking_lot::RwLock; // TODO: get rid of the `Arc>` wrapper. #[derive(Debug)] diff --git a/src/error.rs b/src/error.rs index 40e7062..635fe37 100644 --- a/src/error.rs +++ b/src/error.rs @@ -4,8 +4,6 @@ pub enum Error { #[error(transparent)] Io(#[from] std::io::Error), - #[error(transparent)] - TryRecv(#[from] std::sync::mpsc::TryRecvError), #[error(transparent)] AppDirs2(#[from] app_dirs2::AppDirsError), diff --git a/src/main.rs b/src/main.rs index 406cbb5..38d2c93 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,7 +24,7 @@ mod prelude { } // std -use std::panic; +#[cfg(not(feature = "dev"))] use std::panic; // crates.io use app_dirs2::{AppDataType, AppInfo}; use tracing_appender::rolling::{RollingFileAppender, Rotation}; @@ -57,12 +57,7 @@ fn main() { subscriber.init(); - panic::set_hook(Box::new(|p| { - if let Some(p) = p.payload().downcast_ref::<&str>() { - tracing::error!("{p}"); - } else { - tracing::error!("panic occurred"); - } - })); + #[cfg(not(feature = "dev"))] + panic::set_hook(Box::new(|p| tracing::error!("{p}"))); air::launch().unwrap(); } diff --git a/src/service.rs b/src/service.rs index 811e40c..e1610bb 100644 --- a/src/service.rs +++ b/src/service.rs @@ -1,6 +1,9 @@ mod hotkey; use hotkey::Hotkey; +mod keyboard; +use keyboard::Keyboard; + mod quoter; use quoter::Quoter; @@ -12,16 +15,28 @@ use crate::{component::Components, prelude::*, state::State}; #[derive(Debug)] pub struct Services { + pub keyboard: Keyboard, pub rt: Option, pub quoter: Quoter, pub hotkey: Hotkey, } impl Services { pub fn init(ctx: &Context, components: &Components, state: &State) -> Result { + let keyboard = Keyboard::init(); let rt = Runtime::new()?; let quoter = Quoter::init(&rt, state.chat.quote.clone()); - let hotkey = Hotkey::init(ctx, &rt, components, state)?; + let hotkey = Hotkey::init(ctx, keyboard.clone(), &rt, components, state)?; + + Ok(Self { keyboard, rt: Some(rt), quoter, hotkey }) + } + + pub fn abort(&mut self) { + self.keyboard.abort(); + self.quoter.abort(); + self.hotkey.abort(); - Ok(Self { rt: Some(rt), quoter, hotkey }) + if let Some(rt) = self.rt.take() { + rt.shutdown_background(); + } } } diff --git a/src/service/hotkey.rs b/src/service/hotkey.rs index f43ac2d..d4f7633 100644 --- a/src/service/hotkey.rs +++ b/src/service/hotkey.rs @@ -10,46 +10,43 @@ use std::{ use arboard::Clipboard; use eframe::egui::{Context, ViewportCommand}; use futures::StreamExt; -use global_hotkey::{ - hotkey::{Code, HotKey, Modifiers}, - GlobalHotKeyEvent, GlobalHotKeyManager, HotKeyState, -}; -use tokio::{ - runtime::{Handle, Runtime}, - task::{self, AbortHandle}, - time, -}; +use global_hotkey::{GlobalHotKeyEvent, GlobalHotKeyManager, HotKeyState}; +use tokio::{runtime::Runtime, task::AbortHandle, time}; // self use crate::{ - component::{function::Function, keyboard::Keyboard, Components}, + component::{function::Function, setting::Hotkeys, Components}, os::*, prelude::*, + service::keyboard::Keyboard, state::State, }; #[derive(Debug)] pub struct Hotkey { - pub abort_handle: AbortHandle, - pub is_running: Arc, + abort_handle: AbortHandle, + is_running: Arc, } impl Hotkey { // TODO: optimize parameters. pub fn init( ctx: &Context, + keyboard: Keyboard, rt: &Runtime, components: &Components, state: &State, ) -> Result { - let manager = Manager::init()?; - let mut clipboard = Clipboard::new()?; + let ctx = ctx.to_owned(); + // TODO: use `state.setting.hotkeys`. + let manager = Manager::init(&components.setting.hotkeys)?; + let openai = components.openai.clone(); + let chat_input = state.chat.input.clone(); + let chat_output = state.chat.output.clone(); + let chat_setting = state.setting.chat.clone(); let is_running = Arc::new(AtomicBool::new(false)); let is_running_ = is_running.clone(); let receiver = GlobalHotKeyEvent::receiver(); - let ctx = ctx.to_owned(); - let openai = components.openai.clone(); - let input = state.chat.input.clone(); - let output = state.chat.output.clone(); - let translation = state.setting.translation.clone(); + let mut clipboard = Clipboard::new()?; + // TODO: handle the error. let abort_handle = rt .spawn(async move { // The manager need to be kept alive during the whole program life. @@ -59,109 +56,64 @@ impl Hotkey { is_running_.store(false, Ordering::SeqCst); // Block the thread until a hotkey event is received. - match receiver.recv() { - Ok(e) => { - // We don't care about the release event. - if let HotKeyState::Pressed = e.state { - tracing::info!("receive hotkey event: {e:?}"); - - is_running_.store(true, Ordering::SeqCst); - - let (func, to_get_selected_text) = match e.id { - i if i == manager.ids[0] => (Function::Rewrite, true), - i if i == manager.ids[1] => (Function::RewriteDirectly, true), - i if i == manager.ids[2] => (Function::Translate, true), - i if i == manager.ids[3] => (Function::TranslateDirectly, true), - _ => unreachable!(), - }; - let to_unhide = !matches!( - func, - Function::RewriteDirectly | Function::TranslateDirectly - ); - - if to_unhide { - Os::unhide(); - } - if to_get_selected_text { - // Sleep for a while to reset the keyboard state after user - // triggers the hotkey. - time::sleep(Duration::from_millis(1000)).await; - task::spawn_blocking(move || { - // TODO: handle the error. - Keyboard::init().unwrap().copy().unwrap(); - }) - .await - .unwrap(); - } - if to_unhide { - // Generally, this needs some time to wait the window available - // first, but the previous sleep in `to_get_selected_text`is - // enough. - ctx.send_viewport_cmd(ViewportCommand::Focus); - } + let e = receiver.recv().unwrap(); + + // We don't care about the release event. + if let HotKeyState::Pressed = e.state { + // TODO: reset the hotkey state so that we don't need to wait for the user + // to release the keys. + + is_running_.store(true, Ordering::SeqCst); - let content = match clipboard.get_text() { - Ok(c) if !c.is_empty() => c, - _ => continue, - }; - - // TODO: handle the error. - input.write().unwrap().clone_from(&content); - output.write().unwrap().clear(); - - // TODO: avoid clone. - let output = output.clone(); - // TODO: sometimes, we don't need this. - let translation = translation.read().unwrap().to_owned(); - - // TODO?: task spawn. - // TODO: handle the error. - let mut stream = openai - .lock() - .await - .chat(&func.prompt(&translation), &content) - .await - .unwrap(); - - // TODO: handle the error. - task::spawn_blocking(move || { - // TODO: handle the error. - // TODO: do not init this if not needed. - let mut keyboard = Keyboard::init().unwrap(); - - // TODO: handle the error. - Handle::current() - .block_on(async { - while let Some(r) = stream.next().await { - for s in r? - .choices - .into_iter() - .filter_map(|c| c.delta.content) - { - // TODO?: handle the error. - output.write().unwrap().push_str(&s); - - // TODO: move to outside of the loop. - if matches!( - func, - Function::RewriteDirectly - | Function::TranslateDirectly - ) { - // TODO?: handle the error. - keyboard.text(&s)?; - } - } - } - - Ok::<_, Error>(()) - }) - .unwrap(); - }) - .await - .unwrap(); + let func = manager.match_func(e.id); + let to_unhide = !func.is_directly(); + + if to_unhide { + Os::unhide(); + } + + // Sleep for a while to reset the keyboard state after user + // triggers the hotkey. + time::sleep(Duration::from_millis(1000)).await; + + keyboard.copy(); + + // Give some time to the system to refresh the clipboard. + time::sleep(Duration::from_millis(500)).await; + + let content = match clipboard.get_text() { + Ok(c) if !c.is_empty() => c, + _ => continue, + }; + + if to_unhide { + // Generally, this needs some time to wait the window available + // first, but the previous sleep in get selected text is enough. + ctx.send_viewport_cmd(ViewportCommand::Focus); + } + + chat_input.write().clone_from(&content); + chat_output.write().clear(); + + let chat_setting = chat_setting.read().to_owned(); + let mut stream = openai + .lock() + .await + .chat(&func.prompt(&chat_setting), &content) + .await + .unwrap(); + + while let Some(r) = stream.next().await { + for s in r.unwrap().choices.into_iter().filter_map(|c| c.delta.content) + { + chat_output.write().push_str(&s); + + // TODO: move to outside of the loop. + if !to_unhide { + keyboard.text(s); + } } - }, - Err(e) => panic!("failed to receive hotkey event: {e:?}"), + } } } }) @@ -185,17 +137,13 @@ struct Manager { ids: [u32; 4], } impl Manager { - fn init() -> Result { + fn init(hotkeys: &Hotkeys) -> Result { let _inner = GlobalHotKeyManager::new()?; let hotkeys = [ - // Rewrite. - HotKey::new(Some(Modifiers::CONTROL), Code::KeyT), - // Rewrite directly. - HotKey::new(Some(Modifiers::CONTROL), Code::KeyY), - // Translate. - HotKey::new(Some(Modifiers::CONTROL), Code::KeyU), - // Translate directly. - HotKey::new(Some(Modifiers::CONTROL), Code::KeyI), + hotkeys.rewrite, + hotkeys.rewrite_directly, + hotkeys.translate, + hotkeys.translate_directly, ]; _inner.register_all(&hotkeys)?; @@ -204,4 +152,14 @@ impl Manager { Ok(Self { _inner, ids }) } + + fn match_func(&self, id: u32) -> Function { + match id { + i if i == self.ids[0] => Function::Rewrite, + i if i == self.ids[1] => Function::RewriteDirectly, + i if i == self.ids[2] => Function::Translate, + i if i == self.ids[3] => Function::TranslateDirectly, + _ => unreachable!(), + } + } } diff --git a/src/service/keyboard.rs b/src/service/keyboard.rs new file mode 100644 index 0000000..85bd38c --- /dev/null +++ b/src/service/keyboard.rs @@ -0,0 +1,53 @@ +// std +use std::{ + sync::mpsc::{self, Sender}, + thread, +}; +// self +use crate::component::keyboard::Keyboard as Kb; + +#[derive(Clone, Debug)] +pub struct Keyboard(Sender); +impl Keyboard { + pub fn init() -> Self { + let (tx, rx) = mpsc::channel::(); + + // TODO: handle the error. + thread::spawn(move || { + let mut kb = Kb::init().unwrap(); + + loop { + let act = rx.recv().unwrap(); + + tracing::info!("receive action: {act:?}"); + + match act { + Action::Copy => kb.copy().unwrap(), + Action::Text(text) => kb.text(&text).unwrap(), + Action::Abort => return, + } + } + }); + + Self(tx) + } + + pub fn copy(&self) { + self.0.send(Action::Copy).expect("send must succeed"); + } + + pub fn text(&self, text: String) { + self.0.send(Action::Text(text)).expect("send must succeed"); + } + + pub fn abort(&self) { + self.0.send(Action::Abort).expect("send must succeed"); + } +} + +#[derive(Debug)] +enum Action { + Copy, + Text(String), + Abort, +} diff --git a/src/service/quoter.rs b/src/service/quoter.rs index 89da3bd..62ebcc8 100644 --- a/src/service/quoter.rs +++ b/src/service/quoter.rs @@ -1,17 +1,13 @@ // std -use std::{ - sync::{Arc, RwLock}, - time::Duration, -}; +use std::{sync::Arc, time::Duration}; // crates.io +use parking_lot::RwLock; use tokio::{runtime::Runtime, task::AbortHandle, time}; // self use crate::component::quote::Quoter as QuoterC; #[derive(Debug)] -pub struct Quoter { - pub abort_handle: AbortHandle, -} +pub struct Quoter(AbortHandle); impl Quoter { pub fn init(rt: &Runtime, quote: Arc>) -> Self { let quoter = QuoterC; @@ -20,25 +16,17 @@ impl Quoter { loop { // TODO: skip if the chat input is not empty. - let quote_ = quoter.fetch().await.unwrap_or(QuoterC::DEFAULT.into()); - - if let Ok(mut quote) = quote.write() { - *quote = quote_; - } else { - tracing::error!("quote got poisoned"); - - return; - } + *quote.write() = quoter.fetch().await.unwrap_or(QuoterC::DEFAULT.into()); - time::sleep(Duration::from_secs(30)).await; + time::sleep(Duration::from_secs(50)).await; } }) .abort_handle(); - Self { abort_handle } + Self(abort_handle) } pub fn abort(&self) { - self.abort_handle.abort(); + self.0.abort(); } } diff --git a/src/state.rs b/src/state.rs index f1c1c47..20bf81a 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,7 +1,9 @@ // std -use std::sync::{Arc, RwLock}; +use std::sync::Arc; +// crates.io +use parking_lot::RwLock; // self -use crate::component::setting::Translation; +use crate::component::setting::Chat as ChatSetting; #[derive(Debug, Default)] pub struct State { @@ -18,5 +20,5 @@ pub struct Chat { #[derive(Debug, Default)] pub struct Setting { - pub translation: Arc>, + pub chat: Arc>, } diff --git a/src/ui/panel/chat.rs b/src/ui/panel/chat.rs index b6bc552..7624ef4 100644 --- a/src/ui/panel/chat.rs +++ b/src/ui/panel/chat.rs @@ -22,14 +22,14 @@ impl UiT for Chat { (size.x, ui.available_height()), TextEdit::multiline({ if is_running { - if let Ok(i) = ctx.state.chat.input.try_read() { + if let Some(i) = ctx.state.chat.input.try_read() { i.clone_into(&mut self.input); } } &mut self.input }) - .hint_text(&*ctx.state.chat.quote.read().unwrap()), + .hint_text(&*ctx.state.chat.quote.read()), ); if input.has_focus() { @@ -99,7 +99,7 @@ impl UiT for Chat { ui.label({ // FIXME: `is_running` is conflict with `try_read`. if is_running { - if let Ok(o) = ctx.state.chat.output.try_read() { + if let Some(o) = ctx.state.chat.output.try_read() { o.clone_into(&mut self.output); } } diff --git a/src/ui/panel/setting.rs b/src/ui/panel/setting.rs index 9326a76..e11afe4 100644 --- a/src/ui/panel/setting.rs +++ b/src/ui/panel/setting.rs @@ -117,15 +117,16 @@ impl UiT for Setting { } }); + // TODO: [`crate::component::setting::Chat`]. ui.collapsing("Translation", |ui| { Grid::new("Translation").num_columns(2).striped(true).show(ui, |ui| { - ui.label("Source"); - ComboBox::from_id_source("Source") - .selected_text(&ctx.components.setting.translation.source) + ui.label("A"); + ComboBox::from_id_source("A") + .selected_text(&ctx.components.setting.chat.translation.a) .show_ui(ui, |ui| { Language::all().iter().for_each(|l| { ui.selectable_value( - &mut ctx.components.setting.translation.source, + &mut ctx.components.setting.chat.translation.a, l.to_owned(), l.as_str(), ); @@ -133,13 +134,13 @@ impl UiT for Setting { }); ui.end_row(); - ui.label("Target"); - ComboBox::from_id_source("Target") - .selected_text(&ctx.components.setting.translation.target) + ui.label("B"); + ComboBox::from_id_source("B") + .selected_text(&ctx.components.setting.chat.translation.b) .show_ui(ui, |ui| { Language::all().iter().for_each(|l| { ui.selectable_value( - &mut ctx.components.setting.translation.target, + &mut ctx.components.setting.chat.translation.b, l.to_owned(), l.as_str(), ); @@ -171,3 +172,22 @@ impl Default for ApiKeyWidget { Self { label: "show".into(), visibility: true } } } + +impl Language { + pub fn as_str(&self) -> &'static str { + match self { + Self::ZhCn => "zh-CN", + Self::EnGb => "en-GB", + } + } + + fn all() -> [Self; 2] { + [Self::ZhCn, Self::EnGb] + } +} +#[allow(clippy::from_over_into)] +impl Into for &Language { + fn into(self) -> WidgetText { + self.as_str().into() + } +}