Initial Commit
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
//! 조립부 — .NET 판의 `App.xaml.cs` 에 대응한다.
|
||||
//!
|
||||
//! CodeAssist 고유 결정(어느 핫키가 뭘 하는지, route 이름, 토글 규칙)은 **전부 여기에만** 있다.
|
||||
//! `shell/` 은 이걸 하나도 모르는 재사용 층이고, `bridge/` 는 프론트 계약만 안다.
|
||||
|
||||
mod bridge;
|
||||
mod shell;
|
||||
|
||||
use tauri::{AppHandle, Manager};
|
||||
use tauri_plugin_window_state::{AppHandleExt, StateFlags};
|
||||
|
||||
use bridge::{commands, Push};
|
||||
use shell::hotkey::HotkeySpec;
|
||||
use shell::paste;
|
||||
use shell::tray::TrayAction;
|
||||
use shell::window;
|
||||
|
||||
const HOTKEYS: &[HotkeySpec] = &[
|
||||
HotkeySpec {
|
||||
id: "snap",
|
||||
accelerator: "Ctrl+Shift+8",
|
||||
},
|
||||
HotkeySpec {
|
||||
id: "snippet",
|
||||
accelerator: "Ctrl+Shift+7",
|
||||
},
|
||||
HotkeySpec {
|
||||
id: "capture",
|
||||
accelerator: "Ctrl+Shift+9",
|
||||
},
|
||||
];
|
||||
|
||||
const TOOLTIP: &str = "CodeAssist";
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
// 두 번째 실행은 새 창을 만들지 않고 기존 창을 소환함.
|
||||
.plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| {
|
||||
let _ = show_palette(app);
|
||||
}))
|
||||
.plugin(
|
||||
tauri_plugin_window_state::Builder::default()
|
||||
.with_state_flags(StateFlags::SIZE | StateFlags::POSITION)
|
||||
.build(),
|
||||
)
|
||||
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
|
||||
.manage(commands::RouteState::default())
|
||||
.manage(paste::PasteState::default())
|
||||
.manage(window::TemporaryWindowState::default())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
commands::window_hide,
|
||||
commands::window_drag,
|
||||
commands::window_snippet_layout,
|
||||
commands::report_route,
|
||||
commands::paste_code,
|
||||
commands::snippets_list,
|
||||
commands::snippets_create,
|
||||
commands::snippets_update,
|
||||
commands::snippets_delete,
|
||||
commands::snippets_record_use,
|
||||
])
|
||||
.setup(|app| {
|
||||
let handle = app.handle().clone();
|
||||
|
||||
let pinned = window::load_pinned(&handle);
|
||||
let _ = window::set_pinned(&handle, pinned);
|
||||
|
||||
shell::tray::init(&handle, TOOLTIP, pinned, |app, action| match action {
|
||||
TrayAction::Open => {
|
||||
let _ = show_palette(app);
|
||||
}
|
||||
TrayAction::TogglePin(on) => {
|
||||
let _ = window::set_pinned(app, on);
|
||||
}
|
||||
TrayAction::Quit => {
|
||||
if let Err(error) = window::restore_size(app) {
|
||||
eprintln!("[window] {error}");
|
||||
}
|
||||
app.exit(0);
|
||||
}
|
||||
})?;
|
||||
|
||||
for accelerator in shell::hotkey::init(&handle, HOTKEYS, on_hotkey) {
|
||||
// 죽이지 않는다 — 트레이 '열기'로 여전히 쓸 수 있음.
|
||||
eprintln!("[hotkey] {accelerator} 등록 실패 — 다른 앱이 선점함");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.on_window_event(|window, event| {
|
||||
if matches!(event, tauri::WindowEvent::CloseRequested { .. }) {
|
||||
if let Err(error) = window::restore_size(window.app_handle()) {
|
||||
eprintln!("[window] {error}");
|
||||
}
|
||||
// CloseRequested 리스너 순서는 보장되지 않음. 창이 사라지기 전에
|
||||
// 복원된 실측값을 캐시에도 반영해야 축소 크기가 저장되지 않음.
|
||||
if let Err(error) = window
|
||||
.app_handle()
|
||||
.save_window_state(StateFlags::SIZE | StateFlags::POSITION)
|
||||
{
|
||||
eprintln!("[window-state] {error}");
|
||||
}
|
||||
}
|
||||
})
|
||||
.build(tauri::generate_context!())
|
||||
.expect("Tauri 앱 실행 실패")
|
||||
.run(|app, event| {
|
||||
// window-state 플러그인이 Exit에서 저장하기 전에 원래 크기로 돌림.
|
||||
if matches!(event, tauri::RunEvent::ExitRequested { .. }) {
|
||||
if let Err(error) = window::restore_size(app) {
|
||||
eprintln!("[window] {error}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn chat_toggle_target(visible: bool, current: &str, last_snap: &str) -> Option<String> {
|
||||
if visible && (current == "/snap" || current.starts_with("/snap/")) {
|
||||
None
|
||||
} else {
|
||||
Some(last_snap.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// 팔레트를 띄우기 직전에 대상 창 값을 복사해 둔다. 대상이 닫혀도 배지 문자열은 유지됨.
|
||||
fn show_palette(app: &AppHandle) -> tauri::Result<()> {
|
||||
let own_hwnd = window::main_window(app)
|
||||
.and_then(|window| window.hwnd().ok())
|
||||
.map(|hwnd| hwnd.0 as isize);
|
||||
let captured = paste::capture_foreground(own_hwnd);
|
||||
let state = app.state::<paste::PasteState>();
|
||||
let target = match state.lock() {
|
||||
Ok(mut stored) => {
|
||||
if let Some(captured) = captured {
|
||||
*stored = captured;
|
||||
}
|
||||
stored.clone()
|
||||
}
|
||||
Err(_) => paste::PasteTarget::default(),
|
||||
};
|
||||
|
||||
window::show(app)?;
|
||||
bridge::push(
|
||||
app,
|
||||
Push::PasteTarget {
|
||||
name: target.name,
|
||||
app: target.app,
|
||||
},
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 표준 전역 단축키가 실행할 CodeAssist 동작을 한 곳에서 정함.
|
||||
fn on_hotkey(app: &AppHandle, id: &str) {
|
||||
match id {
|
||||
"snippet" => {
|
||||
let _ = show_palette(app);
|
||||
bridge::push(
|
||||
app,
|
||||
Push::Navigate {
|
||||
path: "/snippet".into(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
"capture" => {
|
||||
shell::capture::start(app.clone(), |app, data_url| {
|
||||
let _ = show_palette(&app);
|
||||
bridge::push(
|
||||
&app,
|
||||
Push::Navigate {
|
||||
path: "/snap/new".into(),
|
||||
},
|
||||
);
|
||||
bridge::push(&app, Push::CaptureImage { data_url });
|
||||
});
|
||||
}
|
||||
|
||||
// 챗봇 토글: 이미 챗봇 화면이 떠 있으면 숨기고, 아니면 마지막 챗봇 위치로 데려온다.
|
||||
_ => {
|
||||
let visible = window::is_visible(app);
|
||||
let state = app.state::<commands::RouteState>();
|
||||
let target = state
|
||||
.lock()
|
||||
.map(|routes| chat_toggle_target(visible, &routes.current, &routes.last_snap))
|
||||
.unwrap_or_else(|_| Some("/snap".to_string()));
|
||||
|
||||
match target {
|
||||
None => {
|
||||
let _ = window::hide(app);
|
||||
}
|
||||
Some(path) => {
|
||||
let _ = show_palette(app);
|
||||
bridge::push(app, Push::Navigate { path });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{chat_toggle_target, commands::Routes};
|
||||
|
||||
#[test]
|
||||
fn 챗봇_토글은_현재_화면과_창_표시에_따라_결정한다() {
|
||||
assert_eq!(chat_toggle_target(true, "/snap", "/snap/last"), None);
|
||||
assert_eq!(
|
||||
chat_toggle_target(true, "/files", "/snap/last"),
|
||||
Some("/snap/last".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
chat_toggle_target(true, "/snippet", "/snap/last"),
|
||||
Some("/snap/last".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
chat_toggle_target(false, "/snap", "/snap/last"),
|
||||
Some("/snap/last".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn 마지막_챗봇_위치는_챗봇_화면에서만_바뀐다() {
|
||||
let mut routes = Routes::default();
|
||||
routes.report("/snap/session-1");
|
||||
routes.report("/snippet");
|
||||
routes.report("/files");
|
||||
|
||||
assert_eq!(routes.current, "/files");
|
||||
assert_eq!(routes.last_snap, "/snap/session-1");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user