Initial Commit
This commit is contained in:
14
src-tauri/Cargo.toml
Normal file
14
src-tauri/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "cex"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
cex-core = { path = "../crates/cex-core" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-dialog = "2"
|
||||
tokio-util = { version = "0.7", features = ["rt"] }
|
||||
3
src-tauri/build.rs
Normal file
3
src-tauri/build.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
7
src-tauri/capabilities/default.json
Normal file
7
src-tauri/capabilities/default.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "main",
|
||||
"description": "Local folder selection for the code explorer",
|
||||
"windows": ["main"],
|
||||
"permissions": ["core:default", "dialog:allow-open", "dialog:allow-message"]
|
||||
}
|
||||
BIN
src-tauri/icons/128x128.png
Normal file
BIN
src-tauri/icons/128x128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
301
src-tauri/src/explanations.rs
Normal file
301
src-tauri/src/explanations.rs
Normal file
@@ -0,0 +1,301 @@
|
||||
use crate::{AppState, blocking};
|
||||
use cex_core::{
|
||||
Result,
|
||||
analysis::{Position, Range},
|
||||
explanation::{
|
||||
self, Completion, LlmSettings, ModelProvider, OpenAiCompatible, PreparedExplanation,
|
||||
},
|
||||
};
|
||||
use serde::Serialize;
|
||||
use std::{path::PathBuf, sync::Mutex};
|
||||
use tauri::{State, ipc::Channel};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ExplainState {
|
||||
inner: Mutex<Session>,
|
||||
}
|
||||
#[derive(Default)]
|
||||
struct Session {
|
||||
generation: u64,
|
||||
cancellation: CancellationToken,
|
||||
prepared: Option<(u64, PreparedExplanation)>,
|
||||
// Credential is memory-only and bound to one workspace and final endpoint.
|
||||
credential: Option<(PathBuf, String, String)>,
|
||||
}
|
||||
impl ExplainState {
|
||||
pub fn invalidate(&self, clear_key: bool) {
|
||||
let mut session = self.inner.lock().unwrap();
|
||||
session.cancellation.cancel();
|
||||
session.generation += 1;
|
||||
session.prepared = None;
|
||||
if clear_key {
|
||||
session.credential = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Serialize)]
|
||||
pub struct ModelSettingsView {
|
||||
settings: LlmSettings,
|
||||
has_session_key: bool,
|
||||
}
|
||||
#[derive(Serialize)]
|
||||
pub struct PreparedView {
|
||||
id: u64,
|
||||
#[serde(flatten)]
|
||||
explanation: PreparedExplanation,
|
||||
}
|
||||
#[tauri::command]
|
||||
pub fn get_llm_settings(root: PathBuf, state: State<'_, AppState>) -> Result<ModelSettingsView> {
|
||||
let workspace = state.current()?;
|
||||
if workspace.root != root {
|
||||
return Err("Workspace changed.".into());
|
||||
}
|
||||
let endpoint = workspace.settings.llm.endpoint()?.to_string();
|
||||
let session = state.explanations.inner.lock().unwrap();
|
||||
let has_session_key = session
|
||||
.credential
|
||||
.as_ref()
|
||||
.is_some_and(|(r, e, _)| *r == root && *e == endpoint);
|
||||
Ok(ModelSettingsView {
|
||||
settings: workspace.settings.llm,
|
||||
has_session_key,
|
||||
})
|
||||
}
|
||||
#[tauri::command]
|
||||
pub async fn save_llm_settings(
|
||||
root: PathBuf,
|
||||
settings: LlmSettings,
|
||||
api_key: Option<String>,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<()> {
|
||||
let state = state.inner().clone();
|
||||
blocking(move || {
|
||||
let _lifecycle = state.lifecycle.lock().unwrap();
|
||||
let mut guard = state.workspace.lock().unwrap();
|
||||
let workspace = guard.as_mut().ok_or("Open a project first.")?;
|
||||
if workspace.root != root {
|
||||
return Err("Workspace changed.".into());
|
||||
}
|
||||
settings.validate()?;
|
||||
if api_key
|
||||
.as_ref()
|
||||
.is_some_and(|k| k.len() > 4096 || k.contains(['\r', '\n']))
|
||||
{
|
||||
return Err("Invalid API key.".into());
|
||||
}
|
||||
let endpoint = settings.endpoint()?.to_string();
|
||||
workspace.save_llm_settings(settings)?;
|
||||
state.explanations.invalidate(false);
|
||||
let mut session = state.explanations.inner.lock().unwrap();
|
||||
if session
|
||||
.credential
|
||||
.as_ref()
|
||||
.is_some_and(|(r, e, _)| *r != root || *e != endpoint)
|
||||
{
|
||||
session.credential = None;
|
||||
}
|
||||
if let Some(key) = api_key {
|
||||
session.credential = if key.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some((root, endpoint, key))
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
#[tauri::command]
|
||||
pub async fn prepare_explanation(
|
||||
root: PathBuf,
|
||||
path: String,
|
||||
position: Position,
|
||||
selection: Option<Range>,
|
||||
question: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<PreparedView> {
|
||||
let (workspace, server, id, cancellation) =
|
||||
{
|
||||
let _lifecycle = state.lifecycle.lock().unwrap();
|
||||
let workspace = state.current()?;
|
||||
if workspace.root != root {
|
||||
return Err("Workspace changed.".into());
|
||||
}
|
||||
let server =
|
||||
state.analysis.lock().unwrap().clone().ok_or(
|
||||
"Start clangd with a build directory before preparing an explanation.",
|
||||
)?;
|
||||
let mut session = state.explanations.inner.lock().unwrap();
|
||||
session.cancellation.cancel();
|
||||
session.generation += 1;
|
||||
session.cancellation = CancellationToken::new();
|
||||
session.prepared = None;
|
||||
(
|
||||
workspace,
|
||||
server,
|
||||
session.generation,
|
||||
session.cancellation.clone(),
|
||||
)
|
||||
};
|
||||
let prepared = blocking(move || {
|
||||
explanation::prepare(
|
||||
&workspace,
|
||||
server.as_ref(),
|
||||
&path,
|
||||
position,
|
||||
selection,
|
||||
&question,
|
||||
&cancellation,
|
||||
)
|
||||
})
|
||||
.await?;
|
||||
let mut session = state.explanations.inner.lock().unwrap();
|
||||
if session.generation != id || session.cancellation.is_cancelled() {
|
||||
return Err("Context preparation cancelled or workspace changed.".into());
|
||||
}
|
||||
session.prepared = Some((id, prepared.clone()));
|
||||
Ok(PreparedView {
|
||||
id,
|
||||
explanation: prepared,
|
||||
})
|
||||
}
|
||||
#[tauri::command]
|
||||
pub async fn send_explanation(
|
||||
root: PathBuf,
|
||||
id: u64,
|
||||
on_delta: Channel<String>,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Completion> {
|
||||
let (prepared, key, cancellation) = {
|
||||
let _lifecycle = state.lifecycle.lock().unwrap();
|
||||
if state.current()?.root != root {
|
||||
return Err("Workspace changed.".into());
|
||||
}
|
||||
let mut session = state.explanations.inner.lock().unwrap();
|
||||
let prepared = session
|
||||
.prepared
|
||||
.as_ref()
|
||||
.filter(|(prepared_id, _)| *prepared_id == id)
|
||||
.map(|(_, p)| p.clone())
|
||||
.ok_or("Context expired. Prepare context again before sending.")?;
|
||||
let key = session
|
||||
.credential
|
||||
.as_ref()
|
||||
.filter(|(r, e, _)| *r == root && *e == prepared.endpoint)
|
||||
.map(|(_, _, key)| key.clone());
|
||||
session.cancellation.cancel();
|
||||
session.generation += 1;
|
||||
session.cancellation = CancellationToken::new();
|
||||
(prepared, key, session.cancellation.clone())
|
||||
};
|
||||
let key = match (key, prepared.settings.api_key_env.is_empty()) {
|
||||
(Some(key), _) => Some(key),
|
||||
(None, true) => None,
|
||||
(None, false) => Some(std::env::var(&prepared.settings.api_key_env).map_err(|_| format!("Environment variable {} is not set for CEX. Set it before launching or enter a session API key.", prepared.settings.api_key_env))?),
|
||||
};
|
||||
let stream_cancel = cancellation.clone();
|
||||
let emit = move |text: String| {
|
||||
if on_delta.send(text).is_err() {
|
||||
stream_cancel.cancel();
|
||||
}
|
||||
};
|
||||
OpenAiCompatible
|
||||
.explain(&prepared, key.as_deref(), &cancellation, &emit)
|
||||
.await
|
||||
}
|
||||
#[tauri::command]
|
||||
pub fn cancel_explanation(root: PathBuf, state: State<'_, AppState>) -> Result<()> {
|
||||
if state.current()?.root != root {
|
||||
return Ok(());
|
||||
}
|
||||
let mut session = state.explanations.inner.lock().unwrap();
|
||||
session.cancellation.cancel();
|
||||
session.generation += 1;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn update_explanation_question(
|
||||
root: PathBuf,
|
||||
id: u64,
|
||||
question: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<PreparedView> {
|
||||
let _lifecycle = state.lifecycle.lock().unwrap();
|
||||
if state.current()?.root != root {
|
||||
return Err("Workspace changed.".into());
|
||||
}
|
||||
let mut session = state.explanations.inner.lock().unwrap();
|
||||
let (_, prepared) = session
|
||||
.prepared
|
||||
.as_mut()
|
||||
.filter(|(stored, _)| *stored == id)
|
||||
.ok_or("Context expired. Prepare again.")?;
|
||||
explanation::update_question(prepared, &question)?;
|
||||
Ok(PreparedView {
|
||||
id,
|
||||
explanation: prepared.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn expand_explanation(
|
||||
root: PathBuf,
|
||||
id: u64,
|
||||
selected: Vec<String>,
|
||||
depth: u8,
|
||||
include_standard: Option<bool>,
|
||||
question: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<PreparedView> {
|
||||
let (prepared, server, next_id, cancel) = {
|
||||
let _lifecycle = state.lifecycle.lock().unwrap();
|
||||
if state.current()?.root != root {
|
||||
return Err("Workspace changed.".into());
|
||||
}
|
||||
let server = state
|
||||
.analysis
|
||||
.lock()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.ok_or("Start clangd first.")?;
|
||||
let mut session = state.explanations.inner.lock().unwrap();
|
||||
let prepared = session
|
||||
.prepared
|
||||
.as_ref()
|
||||
.filter(|(stored, _)| *stored == id)
|
||||
.map(|(_, p)| p.clone())
|
||||
.ok_or("Context expired. Prepare again.")?;
|
||||
session.cancellation.cancel();
|
||||
session.generation += 1;
|
||||
session.cancellation = CancellationToken::new();
|
||||
(
|
||||
prepared,
|
||||
server,
|
||||
session.generation,
|
||||
session.cancellation.clone(),
|
||||
)
|
||||
};
|
||||
let explanation = blocking(move || {
|
||||
explanation::expand_with_standard(
|
||||
&prepared,
|
||||
server.as_ref(),
|
||||
&selected,
|
||||
depth,
|
||||
include_standard.unwrap_or(false),
|
||||
&question,
|
||||
&cancel,
|
||||
)
|
||||
})
|
||||
.await?;
|
||||
let mut session = state.explanations.inner.lock().unwrap();
|
||||
if session.generation != next_id || session.cancellation.is_cancelled() {
|
||||
return Err("Expansion cancelled or workspace changed.".into());
|
||||
}
|
||||
session.prepared = Some((next_id, explanation.clone()));
|
||||
Ok(PreparedView {
|
||||
id: next_id,
|
||||
explanation,
|
||||
})
|
||||
}
|
||||
355
src-tauri/src/main.rs
Normal file
355
src-tauri/src/main.rs
Normal file
@@ -0,0 +1,355 @@
|
||||
mod explanations;
|
||||
use cex_core::analysis::{AnalysisResult, AnalysisStatus, Clangd, CodeAnalysis, Position, Query};
|
||||
use cex_core::{Document, Entry, Inspection, SearchResults, Workspace};
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{
|
||||
Arc, Mutex,
|
||||
atomic::{AtomicU64, Ordering},
|
||||
},
|
||||
};
|
||||
use tauri::{Manager, State};
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct AppState {
|
||||
workspace: Arc<Mutex<Option<Workspace>>>,
|
||||
search_generation: Arc<AtomicU64>,
|
||||
analysis: Arc<Mutex<Option<Arc<dyn CodeAnalysis>>>>,
|
||||
analysis_status: Arc<Mutex<AnalysisStatus>>,
|
||||
lifecycle: Arc<Mutex<()>>,
|
||||
explanations: Arc<explanations::ExplainState>,
|
||||
}
|
||||
impl AppState {
|
||||
fn current(&self) -> Result<Workspace, String> {
|
||||
self.workspace
|
||||
.lock()
|
||||
.map_err(|_| "Workspace lock failed.")?
|
||||
.clone()
|
||||
.ok_or("Open a project first.".into())
|
||||
}
|
||||
}
|
||||
async fn blocking<T: Send + 'static>(
|
||||
f: impl FnOnce() -> Result<T, String> + Send + 'static,
|
||||
) -> Result<T, String> {
|
||||
tauri::async_runtime::spawn_blocking(f)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
}
|
||||
#[tauri::command]
|
||||
async fn inspect_workspace(root: PathBuf) -> Result<Inspection, String> {
|
||||
blocking(move || cex_core::inspect(&root)).await
|
||||
}
|
||||
#[tauri::command]
|
||||
async fn open_workspace(
|
||||
root: PathBuf,
|
||||
build: Option<PathBuf>,
|
||||
reset: bool,
|
||||
exclusions: Option<Vec<String>>,
|
||||
clangd_path: Option<String>,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Workspace, String> {
|
||||
let state = state.inner().clone();
|
||||
state.search_generation.fetch_add(1, Ordering::Relaxed);
|
||||
blocking(move || {
|
||||
let _lifecycle = state.lifecycle.lock().unwrap();
|
||||
state.explanations.invalidate(true);
|
||||
if let Some(server) = state.analysis.lock().unwrap().take() {
|
||||
server.stop();
|
||||
}
|
||||
*state.analysis_status.lock().unwrap() =
|
||||
AnalysisStatus::unavailable("Select a build directory and start analysis.");
|
||||
let mut current = state
|
||||
.workspace
|
||||
.lock()
|
||||
.map_err(|_| "Workspace lock failed.")?;
|
||||
let mut workspace = cex_core::open(&root, build.as_deref(), reset)?;
|
||||
workspace.save_options(
|
||||
exclusions.unwrap_or_else(|| workspace.settings.search_exclusions.clone()),
|
||||
clangd_path.unwrap_or_else(|| workspace.settings.clangd_path.clone()),
|
||||
)?;
|
||||
*current = Some(workspace.clone());
|
||||
Ok(workspace)
|
||||
})
|
||||
.await
|
||||
}
|
||||
#[tauri::command]
|
||||
async fn list_directory(path: String, state: State<'_, AppState>) -> Result<Vec<Entry>, String> {
|
||||
let workspace = state.current()?;
|
||||
blocking(move || workspace.list(&path)).await
|
||||
}
|
||||
#[tauri::command]
|
||||
async fn read_file(path: String, state: State<'_, AppState>) -> Result<Document, String> {
|
||||
let workspace = state.current()?;
|
||||
let analysis = state.analysis.lock().unwrap().clone();
|
||||
blocking(move || {
|
||||
workspace.read(&path).or_else(|error| {
|
||||
analysis
|
||||
.as_ref()
|
||||
.map_or(Err(error), |server| server.read_target(&path))
|
||||
})
|
||||
})
|
||||
.await
|
||||
}
|
||||
#[tauri::command]
|
||||
async fn remember_file(
|
||||
path: String,
|
||||
root: PathBuf,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), String> {
|
||||
let state = state.inner().clone();
|
||||
blocking(move || {
|
||||
let mut guard = state
|
||||
.workspace
|
||||
.lock()
|
||||
.map_err(|_| "Workspace lock failed.")?;
|
||||
let workspace = guard.as_mut().ok_or("Open a project first.")?;
|
||||
if workspace.root != root {
|
||||
return Ok(());
|
||||
}
|
||||
workspace.remember_file(&path)
|
||||
})
|
||||
.await
|
||||
}
|
||||
#[tauri::command]
|
||||
async fn search(query: String, state: State<'_, AppState>) -> Result<SearchResults, String> {
|
||||
let workspace = state.current()?;
|
||||
let generation = state.search_generation.clone();
|
||||
let ticket = generation.fetch_add(1, Ordering::Relaxed) + 1;
|
||||
blocking(move || workspace.search(&query, &generation, ticket)).await
|
||||
}
|
||||
#[tauri::command]
|
||||
fn cancel_search(state: State<'_, AppState>) {
|
||||
state.search_generation.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
#[tauri::command]
|
||||
async fn save_search_exclusions(
|
||||
root: PathBuf,
|
||||
exclusions: Vec<String>,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), String> {
|
||||
let state = state.inner().clone();
|
||||
state.search_generation.fetch_add(1, Ordering::Relaxed);
|
||||
blocking(move || {
|
||||
let mut guard = state.workspace.lock().unwrap();
|
||||
let workspace = guard.as_mut().ok_or("Open a project first.")?;
|
||||
if workspace.root != root {
|
||||
return Err("Workspace changed; retry the operation.".into());
|
||||
}
|
||||
workspace.save_options(exclusions, workspace.settings.clangd_path.clone())
|
||||
})
|
||||
.await
|
||||
}
|
||||
#[tauri::command]
|
||||
async fn start_analysis(
|
||||
root: PathBuf,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<AnalysisStatus, String> {
|
||||
let state = state.inner().clone();
|
||||
blocking(move || {
|
||||
let _lifecycle = state.lifecycle.lock().unwrap();
|
||||
state.explanations.invalidate(false);
|
||||
let workspace = state.current()?;
|
||||
if workspace.root != root {
|
||||
return Err("Workspace changed; retry the operation.".into());
|
||||
}
|
||||
if let Some(server) = state.analysis.lock().unwrap().take() {
|
||||
server.stop();
|
||||
}
|
||||
*state.analysis_status.lock().unwrap() = AnalysisStatus {
|
||||
phase: "starting".into(),
|
||||
message: "Starting clangd…".into(),
|
||||
..AnalysisStatus::default()
|
||||
};
|
||||
match Clangd::start(workspace) {
|
||||
Ok(server) => {
|
||||
let status = server.status();
|
||||
*state.analysis.lock().unwrap() = Some(server);
|
||||
Ok(status)
|
||||
}
|
||||
Err(error) => {
|
||||
let status = AnalysisStatus::unavailable(error);
|
||||
*state.analysis_status.lock().unwrap() = status.clone();
|
||||
Ok(status)
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
#[tauri::command]
|
||||
fn analysis_status(root: PathBuf, state: State<'_, AppState>) -> Result<AnalysisStatus, String> {
|
||||
if state.current()?.root != root {
|
||||
return Err("Workspace changed.".into());
|
||||
}
|
||||
let server = state.analysis.lock().unwrap().clone();
|
||||
Ok(server.map_or_else(
|
||||
|| state.analysis_status.lock().unwrap().clone(),
|
||||
|s| s.status(),
|
||||
))
|
||||
}
|
||||
#[tauri::command]
|
||||
async fn analysis_request(
|
||||
root: PathBuf,
|
||||
path: String,
|
||||
position: Position,
|
||||
kind: Query,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<AnalysisResult, String> {
|
||||
if state.current()?.root != root {
|
||||
return Err("Workspace changed.".into());
|
||||
}
|
||||
let server = state
|
||||
.analysis
|
||||
.lock()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.ok_or_else(|| state.analysis_status.lock().unwrap().message.clone())?;
|
||||
blocking(move || server.query(&path, position, kind)).await
|
||||
}
|
||||
#[tauri::command]
|
||||
async fn graph_request(
|
||||
root: PathBuf,
|
||||
path: String,
|
||||
position: Position,
|
||||
options: cex_core::analysis::GraphOptions,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<cex_core::analysis::ConceptGraph, String> {
|
||||
if state.current()?.root != root {
|
||||
return Err("Workspace changed.".into());
|
||||
}
|
||||
let server = state
|
||||
.analysis
|
||||
.lock()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.ok_or("Start analysis first.")?;
|
||||
blocking(move || server.graph(&path, position, &options)).await
|
||||
}
|
||||
#[tauri::command]
|
||||
async fn save_graph_options(
|
||||
root: PathBuf,
|
||||
options: cex_core::analysis::GraphOptions,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), String> {
|
||||
let state = state.inner().clone();
|
||||
blocking(move || {
|
||||
let mut guard = state.workspace.lock().unwrap();
|
||||
let workspace = guard.as_mut().ok_or("Open a project first.")?;
|
||||
if workspace.root != root {
|
||||
return Err("Workspace changed.".into());
|
||||
}
|
||||
workspace.save_graph_options(options)
|
||||
})
|
||||
.await
|
||||
}
|
||||
#[tauri::command]
|
||||
async fn load_theme(app: tauri::AppHandle) -> Result<cex_core::appearance::Theme, String> {
|
||||
let directory = app
|
||||
.path()
|
||||
.home_dir()
|
||||
.map_err(|e| e.to_string())?
|
||||
.join(".cex");
|
||||
blocking(move || cex_core::appearance::load(&directory)).await
|
||||
}
|
||||
#[tauri::command]
|
||||
async fn save_theme(
|
||||
app: tauri::AppHandle,
|
||||
theme: cex_core::appearance::Theme,
|
||||
) -> Result<(), String> {
|
||||
let directory = app
|
||||
.path()
|
||||
.home_dir()
|
||||
.map_err(|e| e.to_string())?
|
||||
.join(".cex");
|
||||
blocking(move || cex_core::appearance::save(&directory, theme)).await
|
||||
}
|
||||
#[tauri::command]
|
||||
async fn recent_projects(
|
||||
app: tauri::AppHandle,
|
||||
) -> Result<Vec<cex_core::recents::RecentProject>, String> {
|
||||
let directory = app
|
||||
.path()
|
||||
.home_dir()
|
||||
.map_err(|e| e.to_string())?
|
||||
.join(".cex");
|
||||
blocking(move || cex_core::recents::list(&directory)).await
|
||||
}
|
||||
#[tauri::command]
|
||||
async fn remember_recent(
|
||||
app: tauri::AppHandle,
|
||||
state: State<'_, AppState>,
|
||||
root: PathBuf,
|
||||
) -> Result<(), String> {
|
||||
if state.current()?.root != root {
|
||||
return Err("Workspace changed.".into());
|
||||
}
|
||||
let directory = app
|
||||
.path()
|
||||
.home_dir()
|
||||
.map_err(|e| e.to_string())?
|
||||
.join(".cex");
|
||||
blocking(move || cex_core::recents::remember(&directory, &root)).await
|
||||
}
|
||||
#[tauri::command]
|
||||
async fn remove_recent(app: tauri::AppHandle, root: PathBuf) -> Result<(), String> {
|
||||
let directory = app
|
||||
.path()
|
||||
.home_dir()
|
||||
.map_err(|e| e.to_string())?
|
||||
.join(".cex");
|
||||
blocking(move || cex_core::recents::remove(&directory, &root)).await
|
||||
}
|
||||
fn main() {
|
||||
// WebKitGTK's DMA-BUF renderer can produce a blank window when GBM buffer
|
||||
// allocation fails (notably with proprietary NVIDIA drivers on X11).
|
||||
// Use its compatibility rendering path by default, retaining an explicit
|
||||
// environment override for machines where DMA-BUF works well.
|
||||
#[cfg(target_os = "linux")]
|
||||
if std::env::var_os("WEBKIT_DISABLE_DMABUF_RENDERER").is_none() {
|
||||
// SAFETY: this is the first operation in main, before Tauri, GTK, or
|
||||
// the async runtime is initialized and before we create any threads.
|
||||
unsafe { std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1") };
|
||||
}
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.manage(AppState::default())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
graph_request,
|
||||
save_graph_options,
|
||||
load_theme,
|
||||
save_theme,
|
||||
recent_projects,
|
||||
remember_recent,
|
||||
remove_recent,
|
||||
inspect_workspace,
|
||||
open_workspace,
|
||||
list_directory,
|
||||
read_file,
|
||||
remember_file,
|
||||
search,
|
||||
cancel_search,
|
||||
save_search_exclusions,
|
||||
start_analysis,
|
||||
analysis_status,
|
||||
analysis_request,
|
||||
explanations::get_llm_settings,
|
||||
explanations::save_llm_settings,
|
||||
explanations::prepare_explanation,
|
||||
explanations::send_explanation,
|
||||
explanations::expand_explanation,
|
||||
explanations::update_explanation_question,
|
||||
explanations::cancel_explanation
|
||||
])
|
||||
.build(tauri::generate_context!())
|
||||
.expect("Could not start CEX")
|
||||
.run(|app, event| {
|
||||
if matches!(event, tauri::RunEvent::Exit) {
|
||||
use tauri::Manager;
|
||||
let state = app.state::<AppState>();
|
||||
state.explanations.invalidate(true);
|
||||
let _lifecycle = state.lifecycle.lock().unwrap();
|
||||
if let Some(server) = state.analysis.lock().unwrap().take() {
|
||||
server.stop();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
17
src-tauri/tauri.conf.json
Normal file
17
src-tauri/tauri.conf.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "CEX",
|
||||
"version": "0.1.0",
|
||||
"identifier": "dev.cex.explorer",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
"devUrl": "http://localhost:1420",
|
||||
"beforeBuildCommand": "npm run build",
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
"app": {
|
||||
"windows": [{ "title": "CEX — Code Explorer", "incognito": true, "width": 1280, "height": 850, "minWidth": 800, "minHeight": 600 }],
|
||||
"security": { "csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; worker-src 'self' blob:; img-src 'self' data:; font-src 'self' data:; connect-src ipc: http://ipc.localhost" }
|
||||
},
|
||||
"bundle": { "active": false, "icon": ["icons/128x128.png"] }
|
||||
}
|
||||
Reference in New Issue
Block a user