Initial Commit
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
# docs-lib
|
||||
|
||||
이 모듈(`4_rust_tauri`)이 쓰는 라이브러리 참조 문서 모음. **구현 전에 여기 먼저 보고 감 — 기억으로 API 쓰지 말 것** (CLAUDE.md §3, spec-kit `specs/004-tauri-shell/research.md` R3).
|
||||
|
||||
파일이 크면 통째로 읽지 말고 `Grep` 으로 필요한 부분만 꺼내 쓰기.
|
||||
|
||||
## 카탈로그
|
||||
|
||||
| 파일 | 라이브러리 | 출처 | 갱신법 |
|
||||
|---|---|---|---|
|
||||
| `tauri-v2.md` | tauri 2.11.5 + 플러그인 3종 | **crate 소스 직접 확인** (`~/.cargo/registry/src/.../tauri-2.11.5/` 등) | 아래 "갱신법" 참고 |
|
||||
| `tauri-v2-llms-full.md` | Tauri v2 공식 가이드 전문 (2.4MB) | https://tauri.app/llms-full.txt | `curl -sL https://tauri.app/llms-full.txt -o tauri-v2-llms-full.md` |
|
||||
| `windows-rs.md` | windows 0.61.3 Win32 바인딩 | **crate 소스 직접 확인** (`~/.cargo/registry/src/.../windows-0.61.3/`) | Cargo 버전 변경 시 실제 시그니처와 feature 재확인 |
|
||||
|
||||
## 왜 두 개인가
|
||||
|
||||
`tauri-v2-llms-full.md` 는 **공식 가이드 전문**이라 "어떻게 쓰는지"는 잘 나오는데 **시그니처가 못 믿을 데가 있다.** 실제로 이 파일 안에서 `global_shortcut` 핸들러가 2인자(1948줄)와 3인자(23080줄)로 **서로 다르게** 나온다 — 옛 버전 예제가 안 지워지고 남아있음. research R3 이 경고한 함정이 문서 안에 그대로 있는 셈.
|
||||
|
||||
그래서 `tauri-v2.md` 는 **`cargo fetch` 로 받은 실제 crate 소스에서 뽑은 것**만 적는다. 우리가 컴파일할 바로 그 코드라 틀릴 수가 없다.
|
||||
|
||||
- **"이거 어떻게 쓰지"** → `tauri-v2-llms-full.md` 를 Grep
|
||||
- **"시그니처가 정확히 뭐지"** → `tauri-v2.md`. 여기 없으면 소스에서 직접 확인하고 여기에 추가
|
||||
|
||||
## 갱신법
|
||||
|
||||
`tauri-v2-llms-full.md` 는 위 표의 curl 명령으로 덮어쓰기.
|
||||
|
||||
`tauri-v2.md` 는 crate 버전이 올라갔을 때 소스에서 다시 확인:
|
||||
|
||||
```powershell
|
||||
cd 4_rust_tauri\src-tauri
|
||||
cargo update # 버전 올릴 때만
|
||||
cargo fetch # 소스 내려받기
|
||||
# 소스 위치: %USERPROFILE%\.cargo\registry\src\index.crates.io-*\<crate>-<version>\
|
||||
```
|
||||
|
||||
버전이 바뀌면 `tauri-v2.md` 맨 위의 **박제 기준 버전** 표부터 고칠 것. 새 라이브러리 문서를 추가하면 위 카탈로그에 한 줄 등록.
|
||||
@@ -0,0 +1,11 @@
|
||||
# rusqlite 목록 조회
|
||||
|
||||
설치된 `rusqlite-0.32.1` 소스 기준.
|
||||
|
||||
- `Connection::open_with_flags(path, OpenFlags::SQLITE_OPEN_READ_ONLY)`: 기존 DB만 읽기 전용으로 열기. 파일이 없으면 에러이며 새 DB를 만들지 않음. `src/lib.rs:472`.
|
||||
- `Statement::query_map([], callback)`: 매개변수 없는 SELECT 결과를 행별 변환. `src/statement.rs:277`.
|
||||
- `Connection::execute(sql, params![...])`: 쓰기 SQL 실행 후 바뀐 행 수 반환.
|
||||
- `Connection::transaction()` + `Transaction::commit()`: 스니펫과 usage를 한 덩어리로 삭제.
|
||||
- `OptionalExtension::optional()`: usage처럼 행이 없을 수 있는 단건 SELECT를 `Option`으로 변환.
|
||||
- 닷넷 저장소와 같은 `%LOCALAPPDATA%/CodeAssist/snippets.db` 경로 및 snippets/usage 스키마 사용.
|
||||
- SQLite는 `bundled` 기능으로 포함해 별도 설치 의존성을 없앰.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,332 @@
|
||||
# Tauri v2 — 우리가 쓸 API 표면 박제
|
||||
|
||||
**작성**: 2026-08-15 | **근거**: `specs/004-tauri-shell/research.md` R3
|
||||
|
||||
**여기 적힌 건 전부 `cargo fetch` 로 받은 실제 crate 소스에서 뽑은 것이다.** 공식 가이드(`tauri-v2-llms-full.md`)의 예제는 옛 버전이 섞여 있어서 시그니처 근거로 쓰지 않는다. 자세한 건 `README.md` 의 "왜 두 개인가".
|
||||
|
||||
## 박제 기준 버전
|
||||
|
||||
`4_rust_tauri/src-tauri/Cargo.lock` 이 잠근 값 (2026-08-15):
|
||||
|
||||
| crate | 버전 |
|
||||
|---|---|
|
||||
| `tauri` | **2.11.5** |
|
||||
| `tauri-build` | 2.6.3 |
|
||||
| `tauri-plugin-global-shortcut` | 2.3.2 |
|
||||
| `tauri-plugin-single-instance` | 2.4.3 |
|
||||
| `tauri-plugin-window-state` | 2.4.1 |
|
||||
| `tao` (창·이벤트루프, tauri 가 물고 옴) | 0.35.3 |
|
||||
| `global-hotkey` (global-shortcut 가 물고 옴) | 0.8.0 |
|
||||
| `windows` (Win32, tao 가 물고 옴) | 0.61.3 |
|
||||
|
||||
소스 위치: `%USERPROFILE%\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\<crate>-<version>\`
|
||||
|
||||
---
|
||||
|
||||
## 1. 전역 핫키 — `tauri-plugin-global-shortcut` 2.3.2
|
||||
|
||||
### 핸들러는 **3인자**, `ShortcutEvent` 는 **값**으로 온다
|
||||
|
||||
research R3 이 "2개→3개로 바뀐 자리"라고 찍었던 바로 그것. 소스에서 확인한 정답:
|
||||
|
||||
```rust
|
||||
// src/lib.rs:143
|
||||
pub fn on_shortcut<S, F>(&self, shortcut: S, handler: F) -> Result<()>
|
||||
where
|
||||
S: TryInto<ShortcutWrapper>,
|
||||
S::Error: std::error::Error,
|
||||
F: Fn(&AppHandle<R>, &Shortcut, ShortcutEvent) + Send + Sync + 'static,
|
||||
```
|
||||
|
||||
- 인자 **3개**: `(&AppHandle, &Shortcut, ShortcutEvent)`
|
||||
- 3번째는 **`&ShortcutEvent` 가 아니라 `ShortcutEvent` 값**
|
||||
- `shortcut` 은 `TryInto<ShortcutWrapper>` — **`"Ctrl+Shift+8"` 문자열 그대로 넘겨도 된다**
|
||||
- 반환은 `Result<()>` — 등록 실패는 `Err` 로 오지 패닉이 아님 (FR-010 이 요구하는 "죽지 않기" 가 공짜)
|
||||
|
||||
같은 시그니처를 쓰는 다른 진입점:
|
||||
|
||||
```rust
|
||||
pub fn register<S>(&self, shortcut: S) -> Result<()> // 핸들러 없이 등록만 (lib.rs:131)
|
||||
pub fn on_shortcuts<S, T, F>(&self, shortcuts: S, handler: F) // 여러 개 한 번에 (lib.rs:167)
|
||||
pub fn with_handler<F>(self, handler: F) -> Self // Builder 에 공용 핸들러 (lib.rs:380)
|
||||
```
|
||||
|
||||
> ⚠️ `tauri-v2-llms-full.md` 1948줄에는 `with_handler(|app, shortcut| ...)` 로 **2인자** 예제가 남아있다. **그건 옛날 것.** 23080줄의 3인자가 맞다.
|
||||
|
||||
### `ShortcutState` / `ShortcutEvent` 는 `global-hotkey` 것의 별명
|
||||
|
||||
```rust
|
||||
// src/lib.rs:24
|
||||
GlobalHotKeyEvent as ShortcutEvent, HotKeyState as ShortcutState,
|
||||
```
|
||||
|
||||
`global-hotkey` 0.8.0 정의:
|
||||
|
||||
```rust
|
||||
pub enum HotKeyState { Pressed, Released } // = ShortcutState
|
||||
|
||||
pub struct GlobalHotKeyEvent { // = ShortcutEvent
|
||||
pub id: u32,
|
||||
pub state: HotKeyState,
|
||||
}
|
||||
impl GlobalHotKeyEvent {
|
||||
pub fn id(&self) -> u32 { self.id }
|
||||
pub fn state(&self) -> HotKeyState { self.state }
|
||||
}
|
||||
```
|
||||
|
||||
**필드와 메서드가 둘 다 공개**라 `event.state` 도 `event.state()` 도 컴파일된다. 둘 중 뭘 써도 됨.
|
||||
|
||||
**누를 때/뗄 때 둘 다 온다** — `ShortcutState::Pressed` 로 안 거르면 한 번 눌러 두 번 발동한다.
|
||||
|
||||
### 배선
|
||||
|
||||
```rust
|
||||
use tauri_plugin_global_shortcut::{GlobalShortcutExt, ShortcutState};
|
||||
|
||||
// 플러그인 등록
|
||||
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
|
||||
|
||||
// setup 안에서
|
||||
app.global_shortcut().on_shortcut("Ctrl+Shift+8", move |app, _shortcut, event| {
|
||||
if event.state() == ShortcutState::Pressed { /* ... */ }
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 창 — `tauri::WebviewWindow` (tauri 2.11.5)
|
||||
|
||||
전부 `src/webview/webview_window.rs`. **전부 `crate::Result<...>` 를 돌려준다** — `let _ =` 로 무시하든 `?` 로 올리든 결정할 것.
|
||||
|
||||
| 메서드 | 시그니처 | 줄 |
|
||||
|---|---|---|
|
||||
| `is_visible` | `pub fn is_visible(&self) -> crate::Result<bool>` | 1800 |
|
||||
| `set_always_on_top` | `pub fn set_always_on_top(&self, always_on_top: bool) -> crate::Result<()>` | 2049 |
|
||||
| `start_dragging` | `pub fn start_dragging(&self) -> crate::Result<()>` | 2138 |
|
||||
| `show` | `pub fn show(&self) -> crate::Result<()>` | 2207 |
|
||||
| `hide` | `pub fn hide(&self) -> crate::Result<()>` | 2212 |
|
||||
| `set_focus` | `pub fn set_focus(&self) -> crate::Result<()>` | 2260 |
|
||||
|
||||
`is_visible` 만 `bool` 을 감싸 돌려주므로 토글 판정에서 `unwrap_or(false)` 같은 처리가 필요하다.
|
||||
|
||||
창 꺼내는 법은 `Manager` trait 의 `get_webview_window("main")` — 라벨은 `tauri.conf.json` 의 창 `label` 과 같아야 한다.
|
||||
|
||||
---
|
||||
|
||||
## 3. 푸시 (Rust → JS) — `Emitter` trait
|
||||
|
||||
```rust
|
||||
// tauri-2.11.5/src/lib.rs:946
|
||||
fn emit<S: Serialize + Clone>(&self, event: &str, payload: S) -> Result<()>
|
||||
```
|
||||
|
||||
- **`use tauri::Emitter;` 를 안 하면 `app.emit(...)` 이 안 보인다.** trait 메서드라서 import 필수 — research R3 이 "어디서 import 하는지"를 함정으로 찍은 자리가 이것
|
||||
- payload 는 `Serialize + Clone`
|
||||
- `Manager` 를 구현한 것(`App`, `AppHandle`, `WebviewWindow`)이면 다 부를 수 있다
|
||||
|
||||
우리 계약은 **이벤트 이름 하나(`"bridge"`)에 `{type, ...}` 를 그대로** 싣는 것 (research R5, `contracts/transport-mapping.md` §2):
|
||||
|
||||
```rust
|
||||
use tauri::Emitter;
|
||||
app.emit("bridge", serde_json::json!({ "type": "navigate", "path": "/snippet" }))
|
||||
```
|
||||
|
||||
관련 trait 정리:
|
||||
|
||||
| trait | 뭘 주나 | import |
|
||||
|---|---|---|
|
||||
| `Emitter` | `emit`, `emit_to`, `emit_filter` | `use tauri::Emitter;` |
|
||||
| `Manager` | `state`, `path`, `get_webview_window` 등 | `use tauri::Manager;` |
|
||||
| `GlobalShortcutExt` | `global_shortcut()` | `use tauri_plugin_global_shortcut::GlobalShortcutExt;` |
|
||||
|
||||
---
|
||||
|
||||
## 4. 경로 — `app.path().app_local_data_dir()`
|
||||
|
||||
```rust
|
||||
// tauri-2.11.5/src/path/desktop.rs:256
|
||||
pub fn app_local_data_dir(&self) -> Result<PathBuf> {
|
||||
dirs::data_local_dir()
|
||||
.ok_or(Error::UnknownPath)
|
||||
.map(|dir| dir.join(&self.0.config().identifier))
|
||||
}
|
||||
```
|
||||
|
||||
**`%LocalAppData%\<identifier>`다.** identifier가 `com.codeassist.app`이면 `%LocalAppData%\com.codeassist.app\`.
|
||||
|
||||
창 상태와 핀은 이 경로에 둔다. 스니펫 DB는 기존 사용자 데이터를 이어야 하므로 이 함수를 쓰지 않고 `%LocalAppData%\CodeAssist\snippets.db`를 직접 지정한다(R8).
|
||||
|
||||
---
|
||||
|
||||
## 5. 트레이 — `tauri::tray::TrayIconBuilder`
|
||||
|
||||
`show_menu_on_left_click` 는 **2.11.5 에 존재한다** (`src/tray/mod.rs:319). research R3 의 "이름·존재 여부" 걱정은 해소.
|
||||
|
||||
```rust
|
||||
pub fn show_menu_on_left_click(mut self, enable: bool) -> Self // 빌더, mod.rs:319
|
||||
pub fn set_show_menu_on_left_click(&self, enable: bool) -> crate::Result<()> // 런타임 변경, mod.rs:607
|
||||
```
|
||||
|
||||
`mod.rs:307` 에 **옛 이름이 `#[deprecated]` 로 남아있다** — 자동완성이 옛 이름을 물어올 수 있으니 위 이름을 쓸 것.
|
||||
|
||||
메뉴는 `tauri::menu::{MenuBuilder, MenuItemBuilder, CheckMenuItemBuilder}`:
|
||||
|
||||
```rust
|
||||
let toggle = MenuItemBuilder::with_id("toggle", "열기").build(app)?;
|
||||
let pin = CheckMenuItemBuilder::new("항상 위에 고정").build(app)?;
|
||||
let menu = MenuBuilder::new(app).items(&[&toggle, &pin]).build()?;
|
||||
|
||||
TrayIconBuilder::new()
|
||||
.menu(&menu)
|
||||
.on_menu_event(move |app, event| match event.id().as_ref() {
|
||||
"toggle" => { /* ... */ }
|
||||
_ => {}
|
||||
})
|
||||
.build(app)?;
|
||||
```
|
||||
|
||||
> v1 → v2 이름 변경: `SystemTray` → `tray::TrayIconBuilder`, `SystemTrayMenu` → `menu::Menu`, `SystemTrayMenuItem` → `menu::PredefinedMenuItem`. `Builder::on_menu_event` 는 **없어졌고** `App`/`AppHandle::on_menu_event` 나 위처럼 트레이 빌더에 붙인다.
|
||||
|
||||
---
|
||||
|
||||
## 6. 창 상태 저장 — `tauri-plugin-window-state` 2.4.1
|
||||
|
||||
### ⚠️ 기본값이 우리한테 함정이다
|
||||
|
||||
```rust
|
||||
// src/lib.rs:52
|
||||
pub struct StateFlags: u32 {
|
||||
const SIZE = 1 << 0;
|
||||
const POSITION = 1 << 1;
|
||||
const MAXIMIZED = 1 << 2;
|
||||
const VISIBLE = 1 << 3;
|
||||
const DECORATIONS = 1 << 4;
|
||||
const FULLSCREEN = 1 << 5;
|
||||
}
|
||||
impl Default for StateFlags { fn default() -> Self { Self::all() } } // ← 전부 켜짐
|
||||
```
|
||||
|
||||
`Builder::default()` 로 쓰면 **`VISIBLE` 과 `DECORATIONS` 까지 저장·복원한다.**
|
||||
|
||||
우리 앱은 **트레이 상주 + 제목표시줄 없음(`decorations: false`)** 이라 이게 문제가 된다:
|
||||
|
||||
- `VISIBLE` — 끌 때 창이 보이는 상태였으면 다음에 켤 때 **창이 저절로 뜬다.** 트레이 앱은 조용히 시작해야 하는데 어긋남
|
||||
- `DECORATIONS` — 프레임리스 설정과 겹쳐 싸울 수 있는 자리
|
||||
|
||||
**우리가 필요한 건 위치·크기뿐**이다(FR-007, 핀 상태는 R8 대로 직접 저장). 그래서:
|
||||
|
||||
```rust
|
||||
use tauri_plugin_window_state::StateFlags;
|
||||
|
||||
.plugin(
|
||||
tauri_plugin_window_state::Builder::default()
|
||||
.with_state_flags(StateFlags::SIZE | StateFlags::POSITION) // ← 기본값 쓰지 말 것
|
||||
.build()
|
||||
)
|
||||
```
|
||||
|
||||
### 저장 파일
|
||||
|
||||
```rust
|
||||
pub const DEFAULT_FILENAME: &str = ".window-state.json"; // src/lib.rs:36
|
||||
```
|
||||
|
||||
→ `%LocalAppData%\com.codeassist.app\.window-state.json`. 핀(항상 위) 상태는 플래그에 없으므로 같은 폴더에 따로 저장한다(R8·R9).
|
||||
|
||||
기타 빌더 메서드: `skip_initial_state(label)` (lib.rs:369), `build()` (lib.rs:385).
|
||||
|
||||
---
|
||||
|
||||
## 7. 단일 인스턴스 — `tauri-plugin-single-instance` 2.4.3
|
||||
|
||||
```rust
|
||||
// src/lib.rs:31
|
||||
pub fn init<R: Runtime, F: FnMut(&AppHandle<R>, Vec<String>, String) + Send + Sync + 'static>(
|
||||
f: F,
|
||||
) -> TauriPlugin<R>
|
||||
```
|
||||
|
||||
- 콜백 인자 3개: `(&AppHandle, argv: Vec<String>, cwd: String)`
|
||||
- `FnMut` 이다 (`Fn` 아님) — 캡쳐한 걸 안에서 바꿔도 됨
|
||||
- **두 번째 실행에서 이 콜백이 첫 번째 프로세스 쪽에서 불린다.** 거기서 창을 소환하면 "두 번 실행해도 새 창 안 뜸"(FR-009)이 된다
|
||||
|
||||
```rust
|
||||
.plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| {
|
||||
let _ = window::show(app);
|
||||
}))
|
||||
```
|
||||
|
||||
> **첫 플러그인으로 등록할 것** — 플러그인 문서의 요구사항.
|
||||
|
||||
---
|
||||
|
||||
## 8. DPI 인지 (PerMonitorV2) — **할 일 없음** ✅
|
||||
|
||||
research R11 이 "Tauri v2 에서 DPI 인지를 어디서 켜는지 미확정, docs-lib 만들면서 확인"으로 남겨둔 것. **확인 결과: 우리가 켤 필요가 없다.**
|
||||
|
||||
`tao` 0.35.3 이 이벤트 루프를 만들 때 **무조건** 부른다:
|
||||
|
||||
```rust
|
||||
// tao-0.35.3/src/platform_impl/windows/dpi.rs:20
|
||||
pub fn become_dpi_aware() {
|
||||
static ENABLE_DPI_AWARENESS: Once = Once::new();
|
||||
ENABLE_DPI_AWARENESS.call_once(|| unsafe {
|
||||
if let Some(SetProcessDpiAwarenessContext) = *SET_PROCESS_DPI_AWARENESS_CONTEXT {
|
||||
if !SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2).as_bool() {
|
||||
let _ = SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE);
|
||||
}
|
||||
} else if let Some(SetProcessDpiAwareness) = *SET_PROCESS_DPI_AWARENESS {
|
||||
let _ = SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE);
|
||||
} // ... 더 옛날 폴백
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
호출 지점: `tao-0.35.3/src/platform_impl/windows/event_loop.rs:190` — 이벤트 루프 생성 시.
|
||||
|
||||
**즉 Windows 10 1703 이상이면 PerMonitorV2 가 자동으로 켜진다.** .NET 판이 `app.manifest` 를 따로 넣어야 했던 것과 다름.
|
||||
|
||||
### 그럼 매니페스트는?
|
||||
|
||||
`tauri-build` 2.6.3 이 기본으로 박아넣는 매니페스트(`src/windows-app-manifest.xml`)에는 **`<dpiAware>` 항목이 아예 없다.** Common-Controls v6 의존성 하나뿐. 위 런타임 호출이 그 역할을 대신하므로 그대로 둔다.
|
||||
|
||||
> **바꿔야 할 일이 생기면** `tauri_build::WindowsAttributes::new().app_manifest(include_str!("app.manifest"))` 로 통째 교체하고 `Attributes::new().windows_attributes(..)` 에 물려 `try_build` 에 넘긴다 (tauri-build `src/lib.rs:337`). **지금은 필요 없다.**
|
||||
|
||||
**US4(캡쳐) 작업 시 함의**: 좌표가 어긋나면 원인이 DPI 인지 설정이 아니다. 논리↔물리 좌표 변환 쪽을 봐야 한다.
|
||||
|
||||
---
|
||||
|
||||
## 9. 커맨드 (JS → Rust) 와 capabilities
|
||||
|
||||
```rust
|
||||
#[tauri::command]
|
||||
fn window_hide(app: tauri::AppHandle) -> Result<(), String> { /* ... */ }
|
||||
|
||||
// lib.rs 에서
|
||||
.invoke_handler(tauri::generate_handler![window_hide, /* ... */])
|
||||
```
|
||||
|
||||
- 공유 상태는 `.manage(MyState::default())` 로 넣고 커맨드 인자에서 `state: tauri::State<MyState>` 로 받는다
|
||||
- **에러 타입은 `Serialize` 여야 한다.** `String` 으로 돌려주면 프론트에서 `Error` 로 reject 됨 → 우리 계약의 한글 오류 메시지가 그대로 토스트로 흐른다 (`contracts/transport-mapping.md` §3)
|
||||
|
||||
### capabilities
|
||||
|
||||
현재 `capabilities/default.json` 은 `"permissions": ["core:default"]` 하나다. 프론트가 core API 를 직접 안 부르고 **우리 `#[tauri::command]` 만 invoke** 하므로 이걸로 충분하다 — `invoke`/`listen`/`emit` 은 `core:default` 에 들어있다.
|
||||
|
||||
플러그인 권한(`global-shortcut`·`window-state` 는 `permissions/default.toml` 을 가지고 있음)은 **프론트에서 그 플러그인의 JS API 를 직접 부를 때만** 필요하다. 우리는 Rust 쪽에서만 쓰므로 추가 안 해도 된다.
|
||||
|
||||
> 빌드 후 `src-tauri/gen/schemas/desktop-schema.json` 이 생기면 에디터가 권한 이름을 자동완성해준다. 권한이 모자라면 **런타임에 "not allowed" 로 거부**되지 컴파일 에러가 안 나므로, 프론트에서 플러그인 API 를 직접 부르기 시작하면 그때 여기를 의심할 것.
|
||||
|
||||
---
|
||||
|
||||
## 10. 한 장 요약 — import 안 하면 안 보이는 것들
|
||||
|
||||
컴파일 에러 중 제일 헷갈리는 게 "메서드가 없다"인데, 대부분 trait import 누락이다.
|
||||
|
||||
```rust
|
||||
use tauri::Manager; // state(), path(), get_webview_window()
|
||||
use tauri::Emitter; // emit()
|
||||
use tauri_plugin_global_shortcut::GlobalShortcutExt; // global_shortcut()
|
||||
use tauri_plugin_window_state::StateFlags; // with_state_flags()
|
||||
```
|
||||
@@ -0,0 +1,60 @@
|
||||
# windows 0.61.3 — CodeAssist가 쓰는 Win32 표면
|
||||
|
||||
**작성**: 2026-09-09
|
||||
**근거**: 로컬 crate 소스 `%USERPROFILE%/.cargo/registry/src/index.crates.io-*/windows-0.61.3/`에서 직접 확인함.
|
||||
|
||||
## Cargo features
|
||||
|
||||
```toml
|
||||
windows = { version = "0.61.3", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_System_DataExchange",
|
||||
"Win32_System_Memory",
|
||||
"Win32_System_Threading",
|
||||
"Win32_UI_Input_KeyboardAndMouse",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
] }
|
||||
```
|
||||
|
||||
## 직전 창 스냅샷
|
||||
|
||||
```rust
|
||||
GetForegroundWindow() -> HWND
|
||||
GetWindowTextW(HWND, &mut [u16]) -> i32
|
||||
GetWindowThreadProcessId(HWND, Option<*mut u32>) -> u32
|
||||
OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) -> Result<HANDLE>
|
||||
QueryFullProcessImageNameW(HANDLE, PROCESS_NAME_FORMAT, PWSTR, *mut u32) -> Result<()>
|
||||
CloseHandle(HANDLE) -> Result<()>
|
||||
```
|
||||
|
||||
창 핸들은 앱 상태에서 `isize`로 보관한다. `HWND`의 raw pointer를 그대로 앱 상태에 넣으면 Tauri가 요구하는 `Send + Sync` 경계와 부딪힐 수 있다.
|
||||
|
||||
## 유니코드 텍스트 클립보드
|
||||
|
||||
```rust
|
||||
OpenClipboard(None) -> Result<()>
|
||||
EmptyClipboard() -> Result<()>
|
||||
GlobalAlloc(GMEM_MOVEABLE, bytes) -> Result<HGLOBAL>
|
||||
GlobalLock(HGLOBAL) -> *mut c_void
|
||||
GlobalUnlock(HGLOBAL) -> Result<()>
|
||||
SetClipboardData(13, Some(HANDLE(hglobal.0))) -> Result<HANDLE> // 13 = CF_UNICODETEXT
|
||||
CloseClipboard() -> Result<()>
|
||||
```
|
||||
|
||||
`SetClipboardData`가 성공하면 메모리 소유권이 Windows로 넘어간다. 성공 뒤 `GlobalFree`를 부르면 안 된다. 실패한 경우에만 직접 해제한다.
|
||||
|
||||
## 대상 활성화와 Ctrl+V (2026-09-11 갱신)
|
||||
|
||||
```rust
|
||||
SetForegroundWindow(HWND) -> BOOL
|
||||
GetAsyncKeyState(i32) -> i16 // 상위 비트가 1이면 현재 눌린 상태
|
||||
IsWindow(Option<HWND>) -> BOOL
|
||||
SendInput(&[INPUT], size_of::<INPUT>() as i32) -> u32 // 실제 전송한 이벤트 수
|
||||
```
|
||||
|
||||
순서: 클립보드 적재 → 키·마우스 해제 대기(최대 2초, 10ms 간격) → 대상 HWND·PID·thread 재확인 → `SetForegroundWindow` → 활성화·포커스·키 상태 확인(80ms) → `SendInput`으로 Ctrl/V 누름·해제 4개 전송 → 성공 때만 창 숨김. 클립보드는 원복하지 않음.
|
||||
|
||||
- `SetForegroundWindow` 반환 직후에는 대상 활성화가 아직 끝나지 않을 수 있음. 실제 검사에서 약 3ms 뒤 대상이 활성화되는 전환을 관찰함. 첫 활성화 전에는 원래 팔레트나 NULL만 허용하고, 다른 창 또는 활성화 뒤 이탈은 취소함.
|
||||
- 이벤트 4개가 전부 전송되지 않으면 오류. 이미 누른 키의 해제만 정리하고 붙여넣기를 재시도하지 않음. 큐 전송 성공과 대상 편집기의 실제 내용 반영은 별개임.
|
||||
- `paste_code`는 `spawn_blocking`에서 대기·Win32 작업을 실행하므로 UI 스레드를 막지 않음. 대상 잠금은 스냅샷 복사 때만 사용하며, 진행 중 중복 요청은 바로 거절함.
|
||||
- 실패하면 팔레트를 다시 표시하고 오류 안내를 유지. 클립보드 기록 이후 실패에만 원문이 남았다고 안내함.
|
||||
Reference in New Issue
Block a user