Skip to content

Commit 218b79b

Browse files
committed
Settings: Make shortcut editing really work
- Fix searching for shortcuts Settings-wide - Fix saving/loading settings - Update Tauri menus when changing shortcuts - Some more fixes - Improve MCP server along the way - Also remove some excessive debug logs
1 parent 9c39db3 commit 218b79b

23 files changed

Lines changed: 1542 additions & 260 deletions

.mcp.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"mcpServers": {
3+
"cmdr": {
4+
"type": "http",
5+
"url": "http://127.0.0.1:9224/mcp"
6+
},
7+
"tauri": {
8+
"type": "stdio",
9+
"command": "npx",
10+
"args": [
11+
"-y",
12+
"@hypothesi/tauri-mcp-server"
13+
],
14+
"env": {}
15+
}
16+
}
17+
}

CONTRIBUTING.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ This snippet will likely come handy:
126126
}
127127
```
128128

129+
Or add it via CLI like:
130+
129131
Since the agent shares the context with your IDE/client, enabling the MCP server makes the tools available to the agent
130132
automatically.
131133

apps/desktop/src-tauri/src/commands/settings.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
33
use std::net::TcpListener;
44

5+
use tauri::{AppHandle, Manager};
6+
57
use crate::file_system::update_debounce_ms;
8+
use crate::menu::{MenuState, frontend_shortcut_to_accelerator, update_view_mode_accelerator};
69
#[cfg(target_os = "macos")]
710
use crate::network::bonjour::update_resolve_timeout;
811

@@ -47,6 +50,59 @@ pub fn update_service_resolve_timeout(_timeout_ms: u64) {
4750
// No-op on non-macOS platforms
4851
}
4952

53+
/// Update menu accelerator for a command.
54+
/// Called from frontend when keyboard shortcuts are changed.
55+
/// Currently supports: view.fullMode, view.briefMode
56+
#[tauri::command]
57+
pub fn update_menu_accelerator(app: AppHandle, command_id: &str, shortcut: &str) -> Result<(), String> {
58+
let menu_state = app.state::<MenuState<tauri::Wry>>();
59+
60+
// Convert frontend shortcut format to Tauri accelerator format
61+
let accelerator = frontend_shortcut_to_accelerator(shortcut);
62+
63+
match command_id {
64+
"view.fullMode" => {
65+
// Get current checked state before updating
66+
let is_checked = menu_state
67+
.view_mode_full
68+
.lock()
69+
.unwrap()
70+
.as_ref()
71+
.and_then(|item| item.is_checked().ok())
72+
.unwrap_or(false);
73+
74+
let new_item = update_view_mode_accelerator(&app, &menu_state, true, accelerator.as_deref(), is_checked)
75+
.map_err(|e| format!("Failed to update Full view accelerator: {e}"))?;
76+
77+
// Update the reference in MenuState
78+
*menu_state.view_mode_full.lock().unwrap() = Some(new_item);
79+
Ok(())
80+
}
81+
"view.briefMode" => {
82+
// Get current checked state before updating
83+
let is_checked = menu_state
84+
.view_mode_brief
85+
.lock()
86+
.unwrap()
87+
.as_ref()
88+
.and_then(|item| item.is_checked().ok())
89+
.unwrap_or(true);
90+
91+
let new_item = update_view_mode_accelerator(&app, &menu_state, false, accelerator.as_deref(), is_checked)
92+
.map_err(|e| format!("Failed to update Brief view accelerator: {e}"))?;
93+
94+
// Update the reference in MenuState
95+
*menu_state.view_mode_brief.lock().unwrap() = Some(new_item);
96+
Ok(())
97+
}
98+
_ => {
99+
// Silently succeed for commands that don't have menu items
100+
// This allows the frontend to call this for all shortcuts without errors
101+
Ok(())
102+
}
103+
}
104+
}
105+
50106
#[cfg(test)]
51107
mod tests {
52108
use super::*;

apps/desktop/src-tauri/src/lib.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,9 @@ pub fn run() {
150150
*menu_state.show_hidden_files.lock().unwrap() = Some(menu_items.show_hidden_files);
151151
*menu_state.view_mode_full.lock().unwrap() = Some(menu_items.view_mode_full);
152152
*menu_state.view_mode_brief.lock().unwrap() = Some(menu_items.view_mode_brief);
153+
*menu_state.view_submenu.lock().unwrap() = Some(menu_items.view_submenu);
154+
*menu_state.view_mode_full_position.lock().unwrap() = menu_items.view_mode_full_position;
155+
*menu_state.view_mode_brief_position.lock().unwrap() = menu_items.view_mode_brief_position;
153156
app.manage(menu_state);
154157

155158
// Set window title based on license status
@@ -162,6 +165,9 @@ pub fn run() {
162165
// Initialize pane state store for MCP context tools
163166
app.manage(mcp::PaneStateStore::new());
164167

168+
// Initialize settings state store for MCP settings tools
169+
app.manage(mcp::SettingsStateStore::new());
170+
165171
// Start MCP server for AI agent integration
166172
// Use settings from user preferences, with env vars as override for dev
167173
let mcp_config = mcp::McpConfig::from_settings_and_env(
@@ -327,6 +333,12 @@ pub fn run() {
327333
mcp::pane_state::update_left_pane_state,
328334
mcp::pane_state::update_right_pane_state,
329335
mcp::pane_state::update_focused_pane,
336+
mcp::settings_state::mcp_update_settings_state,
337+
mcp::settings_state::mcp_update_settings_open,
338+
mcp::settings_state::mcp_update_settings_section,
339+
mcp::settings_state::mcp_update_settings_sections,
340+
mcp::settings_state::mcp_update_current_settings,
341+
mcp::settings_state::mcp_update_shortcuts,
330342
// Sync status (macOS uses real implementation, others use stub in commands)
331343
commands::sync_status::get_sync_status,
332344
// Volume commands (platform-specific)
@@ -447,7 +459,8 @@ pub fn run() {
447459
commands::settings::check_port_available,
448460
commands::settings::find_available_port,
449461
commands::settings::update_file_watcher_debounce,
450-
commands::settings::update_service_resolve_timeout
462+
commands::settings::update_service_resolve_timeout,
463+
commands::settings::update_menu_accelerator
451464
])
452465
.on_window_event(|window, event| {
453466
// When the main window is closed, quit the entire app (including settings/debug/viewer windows)

apps/desktop/src-tauri/src/mcp/executor.rs

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use tauri::{AppHandle, Emitter, Manager, Runtime};
88

99
use super::pane_state::PaneStateStore;
1010
use super::protocol::{INTERNAL_ERROR, INVALID_PARAMS};
11+
use super::settings_state::SettingsStateStore;
1112
use crate::commands::ui::{
1213
copy_to_clipboard, get_info, open_in_editor, quick_look, set_view_mode, show_in_finder, toggle_hidden_files,
1314
};
@@ -59,6 +60,10 @@ pub fn execute_tool<R: Runtime>(app: &AppHandle<R>, name: &str, params: &Value)
5960
n if n.starts_with("selection_") => execute_selection_command(app, n, params),
6061
// Context commands
6162
n if n.starts_with("context_") => execute_context_command(app, n),
63+
// Settings commands
64+
n if n.starts_with("settings_") => execute_settings_command(app, n, params),
65+
// Shortcuts commands
66+
n if n.starts_with("shortcuts_") => execute_shortcuts_command(app, n, params),
6267
_ => Err(ToolError::invalid_params(format!("Unknown tool: {name}"))),
6368
}
6469
}
@@ -355,6 +360,196 @@ fn execute_context_command<R: Runtime>(app: &AppHandle<R>, name: &str) -> ToolRe
355360
}
356361
}
357362

363+
/// Execute a settings command.
364+
/// These manage the Settings window and its values.
365+
fn execute_settings_command<R: Runtime>(app: &AppHandle<R>, name: &str, params: &Value) -> ToolResult {
366+
match name {
367+
"settings_open" => {
368+
// Emit event to main window to open settings
369+
app.emit_to("main", "open-settings", ())
370+
.map_err(|e| ToolError::internal(e.to_string()))?;
371+
Ok(json!({"success": true, "message": "Settings window opening"}))
372+
}
373+
"settings_close" => {
374+
// Emit event to settings window to close
375+
app.emit_to("settings", "mcp-settings-close", ())
376+
.map_err(|e| ToolError::internal(e.to_string()))?;
377+
Ok(json!({"success": true, "message": "Settings window closing"}))
378+
}
379+
"settings_listSections" => {
380+
let store = app
381+
.try_state::<SettingsStateStore>()
382+
.ok_or_else(|| ToolError::internal("Settings state not initialized"))?;
383+
384+
let state = store.get_state();
385+
Ok(json!({
386+
"sections": state.sections,
387+
"selectedSection": state.selected_section
388+
}))
389+
}
390+
"settings_selectSection" => {
391+
let section_path = params
392+
.get("sectionPath")
393+
.and_then(|v| v.as_array())
394+
.map(|arr| {
395+
arr.iter()
396+
.filter_map(|v| v.as_str().map(String::from))
397+
.collect::<Vec<_>>()
398+
})
399+
.ok_or_else(|| ToolError::invalid_params("Missing 'sectionPath' parameter"))?;
400+
401+
// Emit event to settings window to select section
402+
app.emit_to(
403+
"settings",
404+
"mcp-settings-select-section",
405+
json!({"sectionPath": section_path}),
406+
)
407+
.map_err(|e| ToolError::internal(e.to_string()))?;
408+
Ok(json!({"success": true, "selectedSection": section_path}))
409+
}
410+
"settings_listItems" => {
411+
let store = app
412+
.try_state::<SettingsStateStore>()
413+
.ok_or_else(|| ToolError::internal("Settings state not initialized"))?;
414+
415+
let state = store.get_state();
416+
Ok(json!({
417+
"selectedSection": state.selected_section,
418+
"settings": state.current_settings
419+
}))
420+
}
421+
"settings_getValue" => {
422+
let setting_id = params
423+
.get("settingId")
424+
.and_then(|v| v.as_str())
425+
.ok_or_else(|| ToolError::invalid_params("Missing 'settingId' parameter"))?;
426+
427+
let store = app
428+
.try_state::<SettingsStateStore>()
429+
.ok_or_else(|| ToolError::internal("Settings state not initialized"))?;
430+
431+
let state = store.get_state();
432+
let setting = state.current_settings.iter().find(|s| s.id == setting_id).cloned();
433+
434+
match setting {
435+
Some(s) => Ok(json!({
436+
"settingId": s.id,
437+
"value": s.value,
438+
"isModified": s.is_modified,
439+
"defaultValue": s.default_value
440+
})),
441+
None => Err(ToolError::invalid_params(format!("Setting not found: {setting_id}"))),
442+
}
443+
}
444+
"settings_setValue" => {
445+
let setting_id = params
446+
.get("settingId")
447+
.and_then(|v| v.as_str())
448+
.ok_or_else(|| ToolError::invalid_params("Missing 'settingId' parameter"))?;
449+
450+
let value = params
451+
.get("value")
452+
.ok_or_else(|| ToolError::invalid_params("Missing 'value' parameter"))?;
453+
454+
// Emit event to settings window to set the value
455+
app.emit_to(
456+
"settings",
457+
"mcp-settings-set-value",
458+
json!({"settingId": setting_id, "value": value}),
459+
)
460+
.map_err(|e| ToolError::internal(e.to_string()))?;
461+
462+
Ok(json!({"success": true, "settingId": setting_id, "value": value}))
463+
}
464+
_ => Err(ToolError::invalid_params(format!("Unknown settings command: {name}"))),
465+
}
466+
}
467+
468+
/// Execute a shortcuts command.
469+
/// These manage keyboard shortcuts configuration.
470+
fn execute_shortcuts_command<R: Runtime>(app: &AppHandle<R>, name: &str, params: &Value) -> ToolResult {
471+
match name {
472+
"shortcuts_list" => {
473+
let store = app
474+
.try_state::<SettingsStateStore>()
475+
.ok_or_else(|| ToolError::internal("Settings state not initialized"))?;
476+
477+
let state = store.get_state();
478+
Ok(json!({
479+
"commands": state.shortcuts
480+
}))
481+
}
482+
"shortcuts_set" => {
483+
let command_id = params
484+
.get("commandId")
485+
.and_then(|v| v.as_str())
486+
.ok_or_else(|| ToolError::invalid_params("Missing 'commandId' parameter"))?;
487+
488+
let index = params
489+
.get("index")
490+
.and_then(|v| v.as_i64())
491+
.ok_or_else(|| ToolError::invalid_params("Missing 'index' parameter"))?;
492+
493+
let shortcut = params
494+
.get("shortcut")
495+
.and_then(|v| v.as_str())
496+
.ok_or_else(|| ToolError::invalid_params("Missing 'shortcut' parameter"))?;
497+
498+
// Emit event to settings window (or main window if settings is closed)
499+
// to set the shortcut
500+
app.emit(
501+
"mcp-shortcuts-set",
502+
json!({"commandId": command_id, "index": index, "shortcut": shortcut}),
503+
)
504+
.map_err(|e| ToolError::internal(e.to_string()))?;
505+
506+
Ok(json!({
507+
"success": true,
508+
"commandId": command_id,
509+
"index": index,
510+
"shortcut": shortcut
511+
}))
512+
}
513+
"shortcuts_remove" => {
514+
let command_id = params
515+
.get("commandId")
516+
.and_then(|v| v.as_str())
517+
.ok_or_else(|| ToolError::invalid_params("Missing 'commandId' parameter"))?;
518+
519+
let index = params
520+
.get("index")
521+
.and_then(|v| v.as_i64())
522+
.ok_or_else(|| ToolError::invalid_params("Missing 'index' parameter"))?;
523+
524+
// Emit event to remove the shortcut
525+
app.emit("mcp-shortcuts-remove", json!({"commandId": command_id, "index": index}))
526+
.map_err(|e| ToolError::internal(e.to_string()))?;
527+
528+
Ok(json!({
529+
"success": true,
530+
"commandId": command_id,
531+
"index": index
532+
}))
533+
}
534+
"shortcuts_reset" => {
535+
let command_id = params
536+
.get("commandId")
537+
.and_then(|v| v.as_str())
538+
.ok_or_else(|| ToolError::invalid_params("Missing 'commandId' parameter"))?;
539+
540+
// Emit event to reset the shortcut
541+
app.emit("mcp-shortcuts-reset", json!({"commandId": command_id}))
542+
.map_err(|e| ToolError::internal(e.to_string()))?;
543+
544+
Ok(json!({
545+
"success": true,
546+
"commandId": command_id
547+
}))
548+
}
549+
_ => Err(ToolError::invalid_params(format!("Unknown shortcuts command: {name}"))),
550+
}
551+
}
552+
358553
#[cfg(test)]
359554
mod tests {
360555
use super::*;

apps/desktop/src-tauri/src/mcp/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ pub mod pane_state;
99
mod protocol;
1010
mod resources;
1111
mod server;
12+
pub mod settings_state;
1213
mod tools;
1314

1415
#[cfg(test)]
@@ -17,3 +18,4 @@ mod tests;
1718
pub use config::McpConfig;
1819
pub use pane_state::PaneStateStore;
1920
pub use server::start_mcp_server;
21+
pub use settings_state::SettingsStateStore;

0 commit comments

Comments
 (0)