mirror of
https://github.com/priyanshujain/margin.git
synced 2026-10-02 19:17:03 +00:00
feat(macos): trigger Apple Intelligence writing tools from the editor
This commit is contained in:
1 parent
fe39274ed7
commit
9e310a34bb
5 files changed
+108
-2
No files matched your search
@@ -45,7 +45,15 @@ url = "2"
|
|||||||
# in the graph via Tauri, so this adds nothing new to the build.
|
# in the graph via Tauri, so this adds nothing new to the build.
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
objc2 = "0.6"
|
objc2 = "0.6"
|
||||||
objc2-app-kit = { version = "0.3", features = ["NSSpellChecker"] }
|
objc2-app-kit = { version = "0.3", features = [
|
||||||
|
"NSSpellChecker",
|
||||||
|
"NSApplication",
|
||||||
|
"NSEvent",
|
||||||
|
"NSMenu",
|
||||||
|
"NSMenuItem",
|
||||||
|
"NSResponder",
|
||||||
|
"NSWritingToolsCoordinator",
|
||||||
|
] }
|
||||||
objc2-foundation = { version = "0.3", features = ["NSString", "NSArray", "NSRange", "NSTextCheckingResult"] }
|
objc2-foundation = { version = "0.3", features = ["NSString", "NSArray", "NSRange", "NSTextCheckingResult"] }
|
||||||
|
|
||||||
# Other platforms fall back to the bundled Hunspell dictionary via spellbook.
|
# Other platforms fall back to the bundled Hunspell dictionary via spellbook.
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ mod macspell;
|
|||||||
mod pdf;
|
mod pdf;
|
||||||
mod project;
|
mod project;
|
||||||
mod proofing;
|
mod proofing;
|
||||||
|
mod writingtools;
|
||||||
|
|
||||||
#[cfg(desktop)]
|
#[cfg(desktop)]
|
||||||
use tauri::menu::{Menu, MenuItemBuilder, MenuItemKind, PredefinedMenuItem, SubmenuBuilder};
|
use tauri::menu::{Menu, MenuItemBuilder, MenuItemKind, PredefinedMenuItem, SubmenuBuilder};
|
||||||
@@ -151,6 +152,7 @@ pub fn run() {
|
|||||||
.manage(gdrive::GDriveState::default())
|
.manage(gdrive::GDriveState::default())
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
gdrive::init_session(app.handle());
|
gdrive::init_session(app.handle());
|
||||||
|
writingtools::label_writing_tool_shortcuts();
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -193,6 +195,7 @@ pub fn run() {
|
|||||||
library::delete_book,
|
library::delete_book,
|
||||||
proofing::proof_text,
|
proofing::proof_text,
|
||||||
proofing::remember_word,
|
proofing::remember_word,
|
||||||
|
writingtools::run_writing_tool,
|
||||||
gdrive::gdrive_connect,
|
gdrive::gdrive_connect,
|
||||||
gdrive::gdrive_disconnect,
|
gdrive::gdrive_disconnect,
|
||||||
gdrive::gdrive_status,
|
gdrive::gdrive_status,
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
mod mac {
|
||||||
|
use objc2::rc::Retained;
|
||||||
|
use objc2::MainThreadMarker;
|
||||||
|
use objc2_app_kit::{NSApplication, NSEventModifierFlags, NSMenu, NSMenuItem};
|
||||||
|
use objc2_foundation::{NSArray, NSString};
|
||||||
|
|
||||||
|
const SHORTCUTS: [(&str, &str); 2] = [("Proofread", "f"), ("Rewrite", "r")];
|
||||||
|
|
||||||
|
fn submenu_named(items: &NSArray<NSMenuItem>, title: &str) -> Option<Retained<NSMenu>> {
|
||||||
|
for i in 0..items.count() {
|
||||||
|
let item = items.objectAtIndex(i);
|
||||||
|
if item.title().to_string() == title {
|
||||||
|
return item.submenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn edit_menu(mtm: MainThreadMarker) -> Option<Retained<NSMenu>> {
|
||||||
|
let main = NSApplication::sharedApplication(mtm).mainMenu()?;
|
||||||
|
for i in 0..main.numberOfItems() {
|
||||||
|
let Some(item) = main.itemAtIndex(i) else { continue };
|
||||||
|
let Some(submenu) = item.submenu() else { continue };
|
||||||
|
if submenu.title().to_string() == "Edit" || item.title().to_string() == "Edit" {
|
||||||
|
return Some(submenu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn writing_tools_menu(mtm: MainThreadMarker) -> Option<Retained<NSMenu>> {
|
||||||
|
submenu_named(&edit_menu(mtm)?.itemArray(), "Writing Tools")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn label_shortcuts() {
|
||||||
|
let Some(mtm) = MainThreadMarker::new() else { return };
|
||||||
|
let Some(menu) = writing_tools_menu(mtm) else { return };
|
||||||
|
let items = menu.itemArray();
|
||||||
|
for i in 0..items.count() {
|
||||||
|
let item = items.objectAtIndex(i);
|
||||||
|
let title = item.title().to_string();
|
||||||
|
let Some((_, key)) = SHORTCUTS.iter().find(|(name, _)| *name == title) else { continue };
|
||||||
|
item.setKeyEquivalent(&NSString::from_str(key));
|
||||||
|
item.setKeyEquivalentModifierMask(
|
||||||
|
NSEventModifierFlags::Shift | NSEventModifierFlags::Option,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn perform(tool: &str) {
|
||||||
|
let Some(mtm) = MainThreadMarker::new() else { return };
|
||||||
|
let Some(menu) = writing_tools_menu(mtm) else { return };
|
||||||
|
let items = menu.itemArray();
|
||||||
|
for i in 0..items.count() {
|
||||||
|
if items.objectAtIndex(i).title().to_string() == tool {
|
||||||
|
menu.performActionForItemAtIndex(i as isize);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn label_writing_tool_shortcuts() {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
mac::label_shortcuts();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn run_writing_tool(app: tauri::AppHandle, tool: String) -> Result<(), String> {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
app.run_on_main_thread(move || mac::perform(&tool))
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
{
|
||||||
|
let _ = (app, tool);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,11 +23,16 @@ import { WIDTH_OPTIONS } from "../width";
|
|||||||
import { bodyNumber, chapterKind, partNumber, partRoman } from "../model/book";
|
import { bodyNumber, chapterKind, partNumber, partRoman } from "../model/book";
|
||||||
import { saveBook } from "../library";
|
import { saveBook } from "../library";
|
||||||
import { useEscapeLayer } from "../escape";
|
import { useEscapeLayer } from "../escape";
|
||||||
import { isDesktop } from "../ipc";
|
import { isDesktop, runWritingTool } from "../ipc";
|
||||||
import { useCompact } from "../useMedia";
|
import { useCompact } from "../useMedia";
|
||||||
import { issueSignature, rememberWord, runProof } from "../proofing";
|
import { issueSignature, rememberWord, runProof } from "../proofing";
|
||||||
import { runExport } from "../export/run";
|
import { runExport } from "../export/run";
|
||||||
|
|
||||||
|
const WRITING_TOOLS: Record<string, "Proofread" | "Rewrite"> = {
|
||||||
|
KeyF: "Proofread",
|
||||||
|
KeyR: "Rewrite",
|
||||||
|
};
|
||||||
|
|
||||||
export function EditorView() {
|
export function EditorView() {
|
||||||
const book = useBook((s) => s.book);
|
const book = useBook((s) => s.book);
|
||||||
const activeChapterId = useBook((s) => s.activeChapterId);
|
const activeChapterId = useBook((s) => s.activeChapterId);
|
||||||
@@ -172,6 +177,10 @@ export function EditorView() {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
switchChapter(e.shiftKey ? -1 : 1);
|
switchChapter(e.shiftKey ? -1 : 1);
|
||||||
|
} else if (e.shiftKey && e.altKey && !e.metaKey && !e.ctrlKey && WRITING_TOOLS[e.code]) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
runWritingTool(WRITING_TOOLS[e.code]).catch(() => {});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
window.addEventListener("keydown", onKey, true);
|
window.addEventListener("keydown", onKey, true);
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ import { invoke } from "@tauri-apps/api/core";
|
|||||||
|
|
||||||
export const isDesktop = typeof window !== "undefined" && "__TAURI_INTERNALS__" in window;
|
export const isDesktop = typeof window !== "undefined" && "__TAURI_INTERNALS__" in window;
|
||||||
|
|
||||||
|
export async function runWritingTool(tool: "Proofread" | "Rewrite"): Promise<void> {
|
||||||
|
if (!isDesktop) return;
|
||||||
|
await invoke("run_writing_tool", { tool });
|
||||||
|
}
|
||||||
|
|
||||||
export interface ImageInput {
|
export interface ImageInput {
|
||||||
path: string;
|
path: string;
|
||||||
data: string;
|
data: string;
|
||||||
|
|||||||
Reference in new issue
Block a user