Skip to content

Commit 58868d2

Browse files
authored
feat: shortcut (#3)
* chore: refactor them into modules * feat: register shortcut * refactor: transform functions * refactor: init logic * feat: gpt-4o-mini * chore: release * ui: welcome page * fix: enable
1 parent 9b5147f commit 58868d2

16 files changed

+662
-338
lines changed

Diff for: package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "milo",
33
"private": true,
4-
"version": "0.1.1",
4+
"version": "0.1.2",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",

Diff for: src-tauri/Cargo.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "milo"
3-
version = "0.1.1"
3+
version = "0.1.2"
44
description = "Milo - AI Text Transformation Tool"
55
authors = ["you"]
66
edition = "2021"
@@ -19,6 +19,7 @@ tauri = { version = "2.2.0", features = [ "macos-private-api", "protocol-asset",
1919
tauri-plugin-shell = "2.0.0"
2020
tauri-plugin-clipboard-manager = "2"
2121
tauri-plugin-notification = "2"
22+
tauri-plugin-global-shortcut = "2"
2223
serde = { version = "1.0", features = ["derive"] }
2324
serde_json = "1.0"
2425
keyring = "2.0"

Diff for: src-tauri/capabilities/deafult.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
]
2525
},
2626
"clipboard-manager:default",
27-
"notification:default"
28-
27+
"notification:default",
28+
"global-shortcut:default"
2929
]
3030
}

Diff for: src-tauri/src/api.rs

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
use tauri::Manager;
2+
3+
use std::fs;
4+
use crate::{settings::{api_key_file_path, Settings}, state::AppState};
5+
6+
#[tauri::command]
7+
pub async fn save_api_key(key: String) -> Result<(), String> {
8+
fs::write(api_key_file_path(), key).map_err(|e| e.to_string())
9+
}
10+
11+
#[tauri::command]
12+
pub async fn get_api_key() -> Result<String, String> {
13+
fs::read_to_string(api_key_file_path()).map_err(|e| e.to_string())
14+
}
15+
16+
#[tauri::command]
17+
pub async fn save_settings(state: tauri::State<'_, AppState>, settings: Settings) -> Result<(), String> {
18+
settings.save()?;
19+
*state.settings.lock().await = settings;
20+
Ok(())
21+
}
22+
23+
#[tauri::command]
24+
pub async fn get_settings(state: tauri::State<'_, AppState>) -> Result<Settings, String> {
25+
Ok(state.settings.lock().await.clone())
26+
}
27+
28+
#[tauri::command]
29+
pub async fn show_settings(window: tauri::Window) -> Result<(), String> {
30+
let app = window.app_handle();
31+
if let Some(settings_window) = app.get_webview_window("main") {
32+
settings_window.show().map_err(|e| e.to_string())?;
33+
settings_window.set_focus().map_err(|e| e.to_string())?;
34+
}
35+
Ok(())
36+
}

Diff for: src-tauri/src/lib.rs

+19-170
Original file line numberDiff line numberDiff line change
@@ -1,191 +1,40 @@
11
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
22
mod transform;
3+
mod settings;
4+
mod state;
5+
mod tray;
6+
mod api;
7+
mod shortcuts;
38

4-
use anyhow::Result;
5-
use dirs::config_dir;
6-
use serde::{Deserialize, Serialize};
7-
use std::{fs, path::PathBuf, sync::Mutex, collections::HashMap};
8-
use tauri::{
9-
Manager,
10-
menu::{MenuBuilder},
11-
tray::{TrayIconBuilder},
12-
Emitter
13-
};
14-
use tokio::sync::Mutex as TokioMutex;
15-
16-
17-
#[derive(Debug, Serialize, Deserialize, Clone)]
18-
pub struct Settings {
19-
openai_model: String,
20-
custom_prompts: HashMap<String, String>,
21-
selected_tone: Option<String>,
22-
}
23-
24-
impl Default for Settings {
25-
fn default() -> Self {
26-
let mut custom_prompts = HashMap::new();
27-
custom_prompts.insert(
28-
"Improve Writing".to_string(),
29-
"Improve this text while maintaining its meaning:".to_string(),
30-
);
31-
Self {
32-
openai_model: "gpt-3.5-turbo".to_string(),
33-
custom_prompts,
34-
selected_tone: Some("Improve Writing".to_string()),
35-
}
36-
}
37-
}
38-
39-
impl Settings {
40-
fn load() -> Self {
41-
fs::read_to_string(settings_file_path())
42-
.ok()
43-
.and_then(|contents| serde_json::from_str(&contents).ok())
44-
.unwrap_or_default()
45-
}
46-
47-
fn save(&self) -> Result<(), String> {
48-
let json = serde_json::to_string_pretty(self).map_err(|e| e.to_string())?;
49-
fs::write(settings_file_path(), json).map_err(|e| e.to_string())
50-
}
51-
}
52-
53-
pub struct AppState {
54-
settings: TokioMutex<Settings>,
55-
is_transforming: std::sync::Mutex<bool>,
56-
}
57-
58-
fn api_key_file_path() -> PathBuf {
59-
let mut path = config_dir().unwrap_or_else(|| PathBuf::from("."));
60-
path.push("milo");
61-
fs::create_dir_all(&path).unwrap();
62-
path.push("api_key.txt");
63-
path
64-
}
65-
66-
fn settings_file_path() -> PathBuf {
67-
let mut path = config_dir().unwrap_or_else(|| PathBuf::from("."));
68-
path.push("milo");
69-
fs::create_dir_all(&path).unwrap();
70-
path.push("settings.json");
71-
path
72-
}
73-
74-
#[tauri::command]
75-
async fn save_api_key(key: String) -> Result<(), String> {
76-
fs::write(api_key_file_path(), key).map_err(|e| e.to_string())
77-
}
78-
79-
#[tauri::command]
80-
async fn get_api_key() -> Result<String, String> {
81-
fs::read_to_string(api_key_file_path()).map_err(|e| e.to_string())
82-
}
83-
84-
#[tauri::command]
85-
async fn save_settings(state: tauri::State<'_, AppState>, settings: Settings) -> Result<(), String> {
86-
settings.save()?;
87-
*state.settings.lock().await = settings;
88-
Ok(())
89-
}
90-
91-
#[tauri::command]
92-
async fn get_settings(state: tauri::State<'_, AppState>) -> Result<Settings, String> {
93-
Ok(state.settings.lock().await.clone())
94-
}
95-
96-
#[tauri::command]
97-
async fn show_settings(window: tauri::Window) -> Result<(), String> {
98-
let app = window.app_handle();
99-
if let Some(settings_window) = app.get_webview_window("main") {
100-
settings_window.show().map_err(|e| e.to_string())?;
101-
settings_window.set_focus().map_err(|e| e.to_string())?;
102-
}
103-
Ok(())
104-
}
105-
106-
107-
fn create_tray_menu(app: &tauri::App) -> Result<tauri::tray::TrayIcon, tauri::Error> {
108-
println!("Creating tray menu... 22");
109-
110-
println!("Creating menu...");
111-
let menu = MenuBuilder::new(app)
112-
.text("transform", "Transform")
113-
.text("settings", "Settings")
114-
.separator()
115-
.text("quit", "Quit")
116-
.build()?;
117-
118-
let tray = TrayIconBuilder::new()
119-
.icon(app.default_window_icon().unwrap().clone())
120-
.menu(&menu)
121-
.show_menu_on_left_click(true)
122-
.on_menu_event(|app, event| {
123-
println!("Menu event received: {:?}", event.id());
124-
match event.id().as_ref() {
125-
"quit" => {
126-
println!("Quit menu item clicked");
127-
app.exit(0);
128-
}
129-
"settings" => {
130-
println!("Settings menu item clicked");
131-
if let Some(window) = app.get_webview_window("main") {
132-
window.show().unwrap();
133-
window.set_focus().unwrap();
134-
}
135-
}
136-
"transform" => {
137-
let state = app.state::<AppState>();
138-
let is_transforming = *state.is_transforming.lock().unwrap();
139-
if !is_transforming {
140-
println!("Starting transformation...");
141-
app.emit("transform_clipboard", ()).unwrap();
142-
} else {
143-
println!("Transformation already in progress");
144-
}
145-
}
146-
_ => {
147-
println!("Unknown menu item clicked: {:?}", event.id());
148-
}
149-
}
150-
})
151-
.build(app)?;
152-
153-
Ok(tray)
154-
}
9+
use settings::Settings;
10+
use state::AppState;
15511

15612
#[cfg_attr(mobile, tauri::mobile_entry_point)]
15713
pub fn run() {
15814
let settings = Settings::load();
159-
160-
let app_state = AppState {
161-
settings: TokioMutex::new(settings),
162-
is_transforming: Mutex::new(false),
163-
};
15+
let app_state = AppState::new(settings);
16416

16517
tauri::Builder::default()
16618
.plugin(tauri_plugin_notification::init())
16719
.setup(|app| {
16820
println!("Starting Milo app...");
169-
let _tray = create_tray_menu(app)?;
21+
let _tray = tray::create_tray_menu(app)?;
22+
23+
// Register global shortcuts
24+
#[cfg(desktop)]
25+
shortcuts::register_shortcuts(&app.handle())?;
26+
17027
Ok(())
17128
})
17229
.manage(app_state)
17330
.invoke_handler(tauri::generate_handler![
174-
save_api_key,
175-
get_api_key,
176-
save_settings,
177-
get_settings,
178-
show_settings,
31+
api::save_api_key,
32+
api::get_api_key,
33+
api::save_settings,
34+
api::get_settings,
35+
api::show_settings,
17936
transform::transform_clipboard,
18037
])
181-
.on_window_event(|_app, event| match event {
182-
tauri::WindowEvent::CloseRequested { api, .. } => {
183-
let window = _app.get_webview_window("main").unwrap();
184-
window.hide().unwrap();
185-
api.prevent_close();
186-
}
187-
_ => {}
188-
})
18938
.run(tauri::generate_context!())
19039
.expect("error while running tauri application");
19140
}

Diff for: src-tauri/src/settings.rs

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
use anyhow::Result;
2+
use dirs::config_dir;
3+
use serde::{Deserialize, Serialize};
4+
use std::{collections::HashMap, fs, path::PathBuf};
5+
6+
#[derive(Debug, Serialize, Deserialize, Clone)]
7+
pub struct Settings {
8+
pub openai_model: String,
9+
pub custom_prompts: HashMap<String, String>,
10+
pub selected_tone: Option<String>,
11+
pub first_visit_complete: Option<bool>,
12+
pub shortcut_enabled: Option<bool>,
13+
}
14+
15+
impl Default for Settings {
16+
fn default() -> Self {
17+
let mut custom_prompts = HashMap::new();
18+
custom_prompts.insert(
19+
"Improve Writing".to_string(),
20+
"Improve this text while maintaining its meaning:".to_string(),
21+
);
22+
Self {
23+
openai_model: "gpt-4o-mini".to_string(),
24+
custom_prompts,
25+
selected_tone: Some("Improve Writing".to_string()),
26+
first_visit_complete: Some(false),
27+
shortcut_enabled: Some(true),
28+
}
29+
}
30+
}
31+
32+
impl Settings {
33+
pub fn load() -> Self {
34+
fs::read_to_string(settings_file_path())
35+
.ok()
36+
.and_then(|contents| serde_json::from_str(&contents).ok())
37+
.unwrap_or_default()
38+
}
39+
40+
pub fn save(&self) -> Result<(), String> {
41+
let json = serde_json::to_string_pretty(self).map_err(|e| e.to_string())?;
42+
fs::write(settings_file_path(), json).map_err(|e| e.to_string())
43+
}
44+
45+
pub fn is_shortcut_enabled(&self) -> bool {
46+
self.shortcut_enabled.unwrap_or(true)
47+
}
48+
}
49+
50+
pub fn settings_file_path() -> PathBuf {
51+
let mut path = config_dir().unwrap_or_else(|| PathBuf::from("."));
52+
path.push("milo");
53+
fs::create_dir_all(&path).unwrap();
54+
path.push("settings.json");
55+
path
56+
}
57+
58+
pub fn api_key_file_path() -> PathBuf {
59+
let mut path = config_dir().unwrap_or_else(|| PathBuf::from("."));
60+
path.push("milo");
61+
fs::create_dir_all(&path).unwrap();
62+
path.push("api_key.txt");
63+
path
64+
}

0 commit comments

Comments
 (0)