mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-10-06 13:44:21 +08:00
refactor: rebuild desktop app with Tauri
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
use std::{env, fs, net::SocketAddr, path::PathBuf, time::Duration};
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
use crate::{Error, Result};
|
||||
|
||||
const DATA_DIR_NAME: &str = ".cursor-byok-v3";
|
||||
const DATABASE_FILE_NAME: &str = "cursor-byok.db";
|
||||
|
||||
pub fn managed_data_dir() -> Result<PathBuf> {
|
||||
let home_dir = dirs::home_dir()
|
||||
.ok_or_else(|| Error::Config("cannot resolve user home directory".into()))?;
|
||||
let data_dir = home_dir.join(DATA_DIR_NAME);
|
||||
fs::create_dir_all(&data_dir)?;
|
||||
#[cfg(unix)]
|
||||
fs::set_permissions(&data_dir, fs::Permissions::from_mode(0o700))?;
|
||||
Ok(data_dir)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum ProviderKind {
|
||||
OpenAiChat,
|
||||
OpenAiResponses,
|
||||
Anthropic,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ProviderConfig {
|
||||
pub kind: ProviderKind,
|
||||
pub request_url: String,
|
||||
pub api_key: String,
|
||||
pub custom_headers: reqwest::header::HeaderMap,
|
||||
pub max_output_tokens: Option<u64>,
|
||||
pub request_timeout: Duration,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Config {
|
||||
pub listen_addr: SocketAddr,
|
||||
pub database_url: String,
|
||||
pub provider_request_timeout: Duration,
|
||||
pub console: Option<ConsoleSource>,
|
||||
pub use_persisted_ports: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum ConsoleSource {
|
||||
Directory(PathBuf),
|
||||
Proxy(url::Url),
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn from_env() -> Result<Self> {
|
||||
let listen_addr = env::var("CURSOR_LISTEN_ADDR")
|
||||
.unwrap_or_else(|_| "127.0.0.1:3000".into())
|
||||
.parse()
|
||||
.map_err(|error| Error::Config(format!("invalid CURSOR_LISTEN_ADDR: {error}")))?;
|
||||
let request_timeout = match env::var("CURSOR_PROVIDER_TIMEOUT_SECONDS") {
|
||||
Ok(value) => Duration::from_secs(value.parse().map_err(|error| {
|
||||
Error::Config(format!("invalid CURSOR_PROVIDER_TIMEOUT_SECONDS: {error}"))
|
||||
})?),
|
||||
Err(env::VarError::NotPresent) => Duration::from_secs(300),
|
||||
Err(error) => {
|
||||
return Err(Error::Config(format!(
|
||||
"invalid CURSOR_PROVIDER_TIMEOUT_SECONDS: {error}"
|
||||
)))
|
||||
}
|
||||
};
|
||||
let console_dir = env::var_os("CURSOR_CONSOLE_DIR").map(PathBuf::from);
|
||||
let console_proxy = env::var("CURSOR_CONSOLE_PROXY")
|
||||
.ok()
|
||||
.map(|value| {
|
||||
value.parse().map_err(|error| {
|
||||
Error::Config(format!("invalid CURSOR_CONSOLE_PROXY: {error}"))
|
||||
})
|
||||
})
|
||||
.transpose()?;
|
||||
let console = match (console_dir, console_proxy) {
|
||||
(Some(_), Some(_)) => {
|
||||
return Err(Error::Config(
|
||||
"CURSOR_CONSOLE_DIR and CURSOR_CONSOLE_PROXY cannot both be set".into(),
|
||||
))
|
||||
}
|
||||
(Some(directory), None) => Some(ConsoleSource::Directory(directory)),
|
||||
(None, Some(proxy)) => Some(ConsoleSource::Proxy(proxy)),
|
||||
(None, None) => None,
|
||||
};
|
||||
Ok(Self {
|
||||
listen_addr,
|
||||
database_url: database_url_from_env()?,
|
||||
provider_request_timeout: request_timeout,
|
||||
console,
|
||||
use_persisted_ports: false,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn desktop() -> Result<Self> {
|
||||
Ok(Self {
|
||||
listen_addr: "127.0.0.1:0"
|
||||
.parse()
|
||||
.expect("desktop listen address is static"),
|
||||
database_url: default_database_url()?,
|
||||
provider_request_timeout: Duration::from_secs(300),
|
||||
console: None,
|
||||
use_persisted_ports: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn database_url_from_env() -> Result<String> {
|
||||
match env::var("CURSOR_DATABASE_URL") {
|
||||
Ok(database_url) => Ok(database_url),
|
||||
Err(env::VarError::NotPresent) => default_database_url(),
|
||||
Err(error) => Err(Error::Config(format!(
|
||||
"invalid CURSOR_DATABASE_URL: {error}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
fn default_database_url() -> Result<String> {
|
||||
let data_dir = managed_data_dir()?;
|
||||
database_url_for_dir(&data_dir)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn database_url_in(home_dir: &std::path::Path) -> Result<String> {
|
||||
let data_dir = home_dir.join(DATA_DIR_NAME);
|
||||
fs::create_dir_all(&data_dir)?;
|
||||
|
||||
#[cfg(unix)]
|
||||
fs::set_permissions(&data_dir, fs::Permissions::from_mode(0o700))?;
|
||||
|
||||
database_url_for_dir(&data_dir)
|
||||
}
|
||||
|
||||
fn database_url_for_dir(data_dir: &std::path::Path) -> Result<String> {
|
||||
let database_path = data_dir.join(DATABASE_FILE_NAME);
|
||||
let database_path = database_path
|
||||
.to_str()
|
||||
.ok_or_else(|| Error::Config("database path is not valid UTF-8".into()))?;
|
||||
Ok(format!("sqlite://{database_path}"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn managed_database_supports_home_paths_with_spaces() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let home_dir = directory.path().join("home with spaces");
|
||||
let database_url = database_url_in(&home_dir).unwrap();
|
||||
|
||||
let store = crate::store::Store::connect(&database_url).await.unwrap();
|
||||
drop(store);
|
||||
|
||||
let data_dir = home_dir.join(DATA_DIR_NAME);
|
||||
assert!(data_dir.join(DATABASE_FILE_NAME).is_file());
|
||||
|
||||
#[cfg(unix)]
|
||||
assert_eq!(
|
||||
fs::metadata(data_dir).unwrap().permissions().mode() & 0o777,
|
||||
0o700
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user