mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-01 22:48:16 +00:00
start on specta integration
This commit is contained in:
@@ -30,7 +30,7 @@ import { HighlightsProvider } from "@/context/highlights"
|
||||
import Layout from "@/pages/layout"
|
||||
import DirectoryLayout from "@/pages/directory-layout"
|
||||
import { ErrorPage } from "./pages/error"
|
||||
import { Suspense } from "solid-js"
|
||||
import { Suspense, JSX } from "solid-js"
|
||||
|
||||
const Home = lazy(() => import("@/pages/home"))
|
||||
const Session = lazy(() => import("@/pages/session"))
|
||||
@@ -84,7 +84,7 @@ function ServerKey(props: ParentProps) {
|
||||
)
|
||||
}
|
||||
|
||||
export function AppInterface(props: { defaultUrl?: string }) {
|
||||
export function AppInterface(props: { defaultUrl?: string, root?: (props: ParentProps) => JSX.Element }) {
|
||||
const platform = usePlatform()
|
||||
|
||||
const stored = (() => {
|
||||
@@ -111,7 +111,7 @@ export function AppInterface(props: { defaultUrl?: string }) {
|
||||
<GlobalSDKProvider>
|
||||
<GlobalSyncProvider>
|
||||
<Router
|
||||
root={(props) => (
|
||||
root={(rootProps) => (
|
||||
<SettingsProvider>
|
||||
<PermissionProvider>
|
||||
<LayoutProvider>
|
||||
@@ -119,7 +119,7 @@ export function AppInterface(props: { defaultUrl?: string }) {
|
||||
<ModelsProvider>
|
||||
<CommandProvider>
|
||||
<HighlightsProvider>
|
||||
<Layout>{props.children}</Layout>
|
||||
<Layout>{props.root?.(rootProps) ?? rootProps.children}</Layout>
|
||||
</HighlightsProvider>
|
||||
</CommandProvider>
|
||||
</ModelsProvider>
|
||||
|
||||
1087
packages/desktop/src-tauri/Cargo.lock
generated
1087
packages/desktop/src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -43,6 +43,10 @@ reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"
|
||||
uuid = { version = "1.19.0", features = ["v4"] }
|
||||
tauri-plugin-decorum = "1.1.1"
|
||||
comrak = { version = "0.50", default-features = false }
|
||||
url = "2"
|
||||
specta = { git = "https://github.com/specta-rs/specta", rev = "5409c179845bce2dc4a7315870c83c8d80c0dee4" }
|
||||
specta-typescript = { git = "https://github.com/specta-rs/specta", rev = "f1e55f8c774a9506e3ca6bec1265ce5dda6831a0" }
|
||||
tauri-specta = { git = "https://github.com/specta-rs/tauri-specta", rev = "c0cd3c58e29191845e2bd2b30238246469903727", features = ["derive", "typescript"] }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
gtk = "0.18.2"
|
||||
@@ -59,3 +63,8 @@ windows = { version = "0.61", features = [
|
||||
"Win32_System_Threading",
|
||||
"Win32_Security"
|
||||
] }
|
||||
|
||||
[patch.crates-io]
|
||||
specta = { git = "https://github.com/specta-rs/specta", rev = "5409c179845bce2dc4a7315870c83c8d80c0dee4" }
|
||||
specta-typescript = { git = "https://github.com/specta-rs/specta", rev = "f1e55f8c774a9506e3ca6bec1265ce5dda6831a0" }
|
||||
tauri-specta = { git = "https://github.com/specta-rs/tauri-specta", rev = "c0cd3c58e29191845e2bd2b30238246469903727" }
|
||||
|
||||
@@ -51,6 +51,7 @@ fn is_cli_installed() -> bool {
|
||||
const INSTALL_SCRIPT: &str = include_str!("../../../../install");
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn install_cli(app: tauri::AppHandle) -> Result<String, String> {
|
||||
if cfg!(not(unix)) {
|
||||
return Err("CLI installation is only supported on macOS & Linux".to_string());
|
||||
|
||||
28
packages/desktop/src-tauri/src/deep_link.rs
Normal file
28
packages/desktop/src-tauri/src/deep_link.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[derive(serde::Serialize, Clone, tauri_specta::Event, specta::Type)]
|
||||
pub enum DeepLinkAction {
|
||||
OpenProject {
|
||||
directory: String,
|
||||
session: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
impl DeepLinkAction {
|
||||
pub fn from_url(url: url::Url) -> Option<Self> {
|
||||
if url.scheme() != "opencode" {
|
||||
return None;
|
||||
}
|
||||
|
||||
let action = url.path().trim_start_matches('/');
|
||||
let mut query_pairs: BTreeMap<_, _> = url.query_pairs().collect();
|
||||
|
||||
match action {
|
||||
"open-project" => Some(DeepLinkAction::OpenProject {
|
||||
directory: query_pairs.remove("directory")?.to_string(),
|
||||
session: query_pairs.remove("session").map(|v| v.to_string()),
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +1,42 @@
|
||||
mod cli;
|
||||
mod deep_link;
|
||||
#[cfg(windows)]
|
||||
mod job_object;
|
||||
mod markdown;
|
||||
mod window_customizer;
|
||||
|
||||
use cli::{install_cli, sync_cli};
|
||||
use futures::FutureExt;
|
||||
use futures::future;
|
||||
use futures::channel::mpsc;
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use futures::{SinkExt, future};
|
||||
#[cfg(windows)]
|
||||
use job_object::*;
|
||||
use specta_typescript::Typescript;
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
net::TcpListener,
|
||||
sync::{Arc, Mutex},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tauri::{AppHandle, LogicalSize, Manager, RunEvent, State, WebviewWindowBuilder};
|
||||
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
use tauri::{AppHandle, Emitter, LogicalSize, Manager, RunEvent, State, WebviewWindowBuilder};
|
||||
#[cfg(windows)]
|
||||
use tauri_plugin_decorum::WebviewWindowExt;
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogResult};
|
||||
use tauri_plugin_shell::process::{CommandChild, CommandEvent};
|
||||
use tauri_plugin_store::StoreExt;
|
||||
use tauri_specta::{Builder, collect_commands};
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
use crate::deep_link::DeepLinkAction;
|
||||
use crate::window_customizer::PinchZoomDisablePlugin;
|
||||
|
||||
const SETTINGS_STORE: &str = "opencode.settings.dat";
|
||||
const DEFAULT_SERVER_URL_KEY: &str = "defaultServerUrl";
|
||||
|
||||
#[derive(Clone, serde::Serialize)]
|
||||
#[derive(Clone, serde::Serialize, specta::Type)]
|
||||
struct ServerReadyData {
|
||||
url: String,
|
||||
password: Option<String>,
|
||||
@@ -64,6 +70,7 @@ struct LogState(Arc<Mutex<VecDeque<String>>>);
|
||||
const MAX_LOG_ENTRIES: usize = 200;
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
fn kill_sidecar(app: AppHandle) {
|
||||
let Some(server_state) = app.try_state::<ServerState>() else {
|
||||
println!("Server not running");
|
||||
@@ -97,6 +104,7 @@ async fn get_logs(app: AppHandle) -> Result<String, String> {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
async fn ensure_server_ready(state: State<'_, ServerState>) -> Result<ServerReadyData, String> {
|
||||
state
|
||||
.status
|
||||
@@ -106,6 +114,7 @@ async fn ensure_server_ready(state: State<'_, ServerState>) -> Result<ServerRead
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
fn get_default_server_url(app: AppHandle) -> Result<Option<String>, String> {
|
||||
let store = app
|
||||
.store(SETTINGS_STORE)
|
||||
@@ -119,6 +128,7 @@ fn get_default_server_url(app: AppHandle) -> Result<Option<String>, String> {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
async fn set_default_server_url(app: AppHandle, url: Option<String>) -> Result<(), String> {
|
||||
let store = app
|
||||
.store(SETTINGS_STORE)
|
||||
@@ -257,6 +267,22 @@ pub fn run() {
|
||||
.arg("opencode-cli")
|
||||
.output();
|
||||
|
||||
let mut builder = Builder::<tauri::Wry>::new()
|
||||
// Then register them (separated by a comma)
|
||||
.commands(collect_commands![
|
||||
kill_sidecar,
|
||||
install_cli,
|
||||
ensure_server_ready,
|
||||
get_default_server_url,
|
||||
set_default_server_url,
|
||||
markdown::parse_markdown_command
|
||||
]);
|
||||
|
||||
#[cfg(debug_assertions)] // <- Only export on non-release builds
|
||||
builder
|
||||
.export(Typescript::default(), "../src/bindings.ts")
|
||||
.expect("Failed to export typescript bindings");
|
||||
|
||||
let mut builder = tauri::Builder::default()
|
||||
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
|
||||
// Focus existing window when another instance is launched
|
||||
@@ -285,15 +311,11 @@ pub fn run() {
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
.plugin(PinchZoomDisablePlugin)
|
||||
.plugin(tauri_plugin_decorum::init())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
kill_sidecar,
|
||||
install_cli,
|
||||
ensure_server_ready,
|
||||
get_default_server_url,
|
||||
set_default_server_url,
|
||||
markdown::parse_markdown_command
|
||||
])
|
||||
.invoke_handler(builder.invoke_handler())
|
||||
.setup(move |app| {
|
||||
// This is also required if you want to use events
|
||||
builder.mount_events(app);
|
||||
|
||||
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
|
||||
app.deep_link().register_all().ok();
|
||||
|
||||
@@ -344,14 +366,28 @@ pub fn run() {
|
||||
)
|
||||
.decorations(false);
|
||||
|
||||
let window = window_builder.build().expect("Failed to create window");
|
||||
let _window = window_builder.build().expect("Failed to create window");
|
||||
|
||||
#[cfg(windows)]
|
||||
let _ = window.create_overlay_titlebar();
|
||||
let _ = _window.create_overlay_titlebar();
|
||||
|
||||
let (tx, rx) = oneshot::channel();
|
||||
app.manage(ServerState::new(None, rx));
|
||||
|
||||
let (deeplink_tx, deeplink_rx) = mpsc::unbounded();
|
||||
|
||||
for url in app.deep_link().get_current().ok().flatten().unwrap_or_default() {
|
||||
let _ = deeplink_tx.unbounded_send(url);
|
||||
}
|
||||
|
||||
app.deep_link().on_open_url(move |e| {
|
||||
for url in e.urls() {
|
||||
let _ = deeplink_tx.unbounded_send(url);
|
||||
}
|
||||
});
|
||||
|
||||
let (ready_tx, ready_rx) = oneshot::channel::<()>();
|
||||
|
||||
{
|
||||
let app = app.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
@@ -398,6 +434,25 @@ pub fn run() {
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let app = app.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let Ok(_) = ready_rx.await else {
|
||||
return
|
||||
};
|
||||
|
||||
tauri::async_runtime::spawn(deeplink_rx.for_each(move |url| {
|
||||
if let Some(action) = DeepLinkAction::from_url(url)
|
||||
&& let Some(window) = app.get_webview_window("main") {
|
||||
let _ = window.emit("opencode:deep-link", action);
|
||||
}
|
||||
|
||||
future::ready(())
|
||||
}));
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use comrak::{create_formatter, parse_document, Arena, Options, html::ChildRendering, nodes::NodeValue};
|
||||
use comrak::{
|
||||
Arena, Options, create_formatter, html::ChildRendering, nodes::NodeValue, parse_document,
|
||||
};
|
||||
use std::fmt::Write;
|
||||
|
||||
create_formatter!(ExternalLinkFormatter, {
|
||||
@@ -55,6 +57,7 @@ pub fn parse_markdown(input: &str) -> String {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn parse_markdown_command(markdown: String) -> Result<String, String> {
|
||||
Ok(parse_markdown(&markdown))
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use tauri::{plugin::Plugin, Manager, Runtime, Window};
|
||||
use tauri::{Manager, Runtime, Window, plugin::Plugin};
|
||||
|
||||
pub struct PinchZoomDisablePlugin;
|
||||
|
||||
@@ -21,8 +21,8 @@ impl<R: Runtime> Plugin<R> for PinchZoomDisablePlugin {
|
||||
let _ = webview_window.with_webview(|_webview| {
|
||||
#[cfg(target_os = "linux")]
|
||||
unsafe {
|
||||
use gtk::glib::ObjectExt;
|
||||
use gtk::GestureZoom;
|
||||
use gtk::glib::ObjectExt;
|
||||
use webkit2gtk::glib::gobject_ffi;
|
||||
|
||||
if let Some(data) = _webview.inner().data::<GestureZoom>("wk-view-zoom-gesture") {
|
||||
|
||||
123
packages/desktop/src/bindings.ts
Normal file
123
packages/desktop/src/bindings.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
|
||||
// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually.
|
||||
|
||||
/** user-defined commands **/
|
||||
|
||||
|
||||
export const commands = {
|
||||
async killSidecar() : Promise<void> {
|
||||
await TAURI_INVOKE("kill_sidecar");
|
||||
},
|
||||
async installCli() : Promise<Result<string, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("install_cli") };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async ensureServerReady() : Promise<Result<ServerReadyData, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("ensure_server_ready") };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getDefaultServerUrl() : Promise<Result<string | null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_default_server_url") };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async setDefaultServerUrl(url: string | null) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("set_default_server_url", { url }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async parseMarkdownCommand(markdown: string) : Promise<Result<string, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("parse_markdown_command", { markdown }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** user-defined events **/
|
||||
|
||||
|
||||
|
||||
/** user-defined constants **/
|
||||
|
||||
|
||||
|
||||
/** user-defined types **/
|
||||
|
||||
export type ServerReadyData = { url: string; password: string | null }
|
||||
|
||||
/** tauri-specta globals **/
|
||||
|
||||
import {
|
||||
invoke as TAURI_INVOKE,
|
||||
Channel as TAURI_CHANNEL,
|
||||
} from "@tauri-apps/api/core";
|
||||
import * as TAURI_API_EVENT from "@tauri-apps/api/event";
|
||||
import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow";
|
||||
|
||||
type __EventObj__<T> = {
|
||||
listen: (
|
||||
cb: TAURI_API_EVENT.EventCallback<T>,
|
||||
) => ReturnType<typeof TAURI_API_EVENT.listen<T>>;
|
||||
once: (
|
||||
cb: TAURI_API_EVENT.EventCallback<T>,
|
||||
) => ReturnType<typeof TAURI_API_EVENT.once<T>>;
|
||||
emit: null extends T
|
||||
? (payload?: T) => ReturnType<typeof TAURI_API_EVENT.emit>
|
||||
: (payload: T) => ReturnType<typeof TAURI_API_EVENT.emit>;
|
||||
};
|
||||
|
||||
export type Result<T, E> =
|
||||
| { status: "ok"; data: T }
|
||||
| { status: "error"; error: E };
|
||||
|
||||
function __makeEvents__<T extends Record<string, any>>(
|
||||
mappings: Record<keyof T, string>,
|
||||
) {
|
||||
return new Proxy(
|
||||
{} as unknown as {
|
||||
[K in keyof T]: __EventObj__<T[K]> & {
|
||||
(handle: __WebviewWindow__): __EventObj__<T[K]>;
|
||||
};
|
||||
},
|
||||
{
|
||||
get: (_, event) => {
|
||||
const name = mappings[event as keyof T];
|
||||
|
||||
return new Proxy((() => {}) as any, {
|
||||
apply: (_, __, [window]: [__WebviewWindow__]) => ({
|
||||
listen: (arg: any) => window.listen(name, arg),
|
||||
once: (arg: any) => window.once(name, arg),
|
||||
emit: (arg: any) => window.emit(name, arg),
|
||||
}),
|
||||
get: (_, command: keyof __EventObj__<any>) => {
|
||||
switch (command) {
|
||||
case "listen":
|
||||
return (arg: any) => TAURI_API_EVENT.listen(name, arg);
|
||||
case "once":
|
||||
return (arg: any) => TAURI_API_EVENT.once(name, arg);
|
||||
case "emit":
|
||||
return (arg: any) => TAURI_API_EVENT.emit(name, arg);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user