Initial GuruConnect implementation - Phase 1 MVP
- Agent: DXGI/GDI screen capture, mouse/keyboard input, WebSocket transport - Server: Axum relay, session management, REST API - Dashboard: React viewer components with TypeScript - Protocol: Protobuf definitions for all message types 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
199
agent/src/config.rs
Normal file
199
agent/src/config.rs
Normal file
@@ -0,0 +1,199 @@
|
||||
//! Agent configuration management
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Agent configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
/// Server WebSocket URL (e.g., wss://connect.example.com/ws)
|
||||
pub server_url: String,
|
||||
|
||||
/// Agent API key for authentication
|
||||
pub api_key: String,
|
||||
|
||||
/// Optional hostname override
|
||||
pub hostname_override: Option<String>,
|
||||
|
||||
/// Capture settings
|
||||
#[serde(default)]
|
||||
pub capture: CaptureConfig,
|
||||
|
||||
/// Encoding settings
|
||||
#[serde(default)]
|
||||
pub encoding: EncodingConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CaptureConfig {
|
||||
/// Target frames per second (1-60)
|
||||
#[serde(default = "default_fps")]
|
||||
pub fps: u32,
|
||||
|
||||
/// Use DXGI Desktop Duplication (recommended)
|
||||
#[serde(default = "default_true")]
|
||||
pub use_dxgi: bool,
|
||||
|
||||
/// Fall back to GDI if DXGI fails
|
||||
#[serde(default = "default_true")]
|
||||
pub gdi_fallback: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EncodingConfig {
|
||||
/// Preferred codec (auto, raw, vp9, h264)
|
||||
#[serde(default = "default_codec")]
|
||||
pub codec: String,
|
||||
|
||||
/// Quality (1-100, higher = better quality, more bandwidth)
|
||||
#[serde(default = "default_quality")]
|
||||
pub quality: u32,
|
||||
|
||||
/// Use hardware encoding if available
|
||||
#[serde(default = "default_true")]
|
||||
pub hardware_encoding: bool,
|
||||
}
|
||||
|
||||
fn default_fps() -> u32 {
|
||||
30
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_codec() -> String {
|
||||
"auto".to_string()
|
||||
}
|
||||
|
||||
fn default_quality() -> u32 {
|
||||
75
|
||||
}
|
||||
|
||||
impl Default for CaptureConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
fps: default_fps(),
|
||||
use_dxgi: true,
|
||||
gdi_fallback: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for EncodingConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
codec: default_codec(),
|
||||
quality: default_quality(),
|
||||
hardware_encoding: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Load configuration from file or environment
|
||||
pub fn load() -> Result<Self> {
|
||||
// Try loading from config file
|
||||
let config_path = Self::config_path();
|
||||
|
||||
if config_path.exists() {
|
||||
let contents = std::fs::read_to_string(&config_path)
|
||||
.with_context(|| format!("Failed to read config from {:?}", config_path))?;
|
||||
|
||||
let config: Config = toml::from_str(&contents)
|
||||
.with_context(|| "Failed to parse config file")?;
|
||||
|
||||
return Ok(config);
|
||||
}
|
||||
|
||||
// Fall back to environment variables
|
||||
let server_url = std::env::var("GURUCONNECT_SERVER_URL")
|
||||
.unwrap_or_else(|_| "wss://localhost:3002/ws".to_string());
|
||||
|
||||
let api_key = std::env::var("GURUCONNECT_API_KEY")
|
||||
.unwrap_or_else(|_| "dev-key".to_string());
|
||||
|
||||
Ok(Config {
|
||||
server_url,
|
||||
api_key,
|
||||
hostname_override: std::env::var("GURUCONNECT_HOSTNAME").ok(),
|
||||
capture: CaptureConfig::default(),
|
||||
encoding: EncodingConfig::default(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the configuration file path
|
||||
fn config_path() -> PathBuf {
|
||||
// Check for config in current directory first
|
||||
let local_config = PathBuf::from("guruconnect.toml");
|
||||
if local_config.exists() {
|
||||
return local_config;
|
||||
}
|
||||
|
||||
// Check in program data directory (Windows)
|
||||
#[cfg(windows)]
|
||||
{
|
||||
if let Ok(program_data) = std::env::var("ProgramData") {
|
||||
let path = PathBuf::from(program_data)
|
||||
.join("GuruConnect")
|
||||
.join("agent.toml");
|
||||
if path.exists() {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default to local config
|
||||
local_config
|
||||
}
|
||||
|
||||
/// Get the hostname to use
|
||||
pub fn hostname(&self) -> String {
|
||||
self.hostname_override
|
||||
.clone()
|
||||
.unwrap_or_else(|| {
|
||||
hostname::get()
|
||||
.map(|h| h.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|_| "unknown".to_string())
|
||||
})
|
||||
}
|
||||
|
||||
/// Save current configuration to file
|
||||
pub fn save(&self) -> Result<()> {
|
||||
let config_path = Self::config_path();
|
||||
|
||||
// Ensure parent directory exists
|
||||
if let Some(parent) = config_path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
let contents = toml::to_string_pretty(self)?;
|
||||
std::fs::write(&config_path, contents)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Example configuration file content
|
||||
pub fn example_config() -> &'static str {
|
||||
r#"# GuruConnect Agent Configuration
|
||||
|
||||
# Server connection
|
||||
server_url = "wss://connect.example.com/ws"
|
||||
api_key = "your-agent-api-key"
|
||||
|
||||
# Optional: override hostname
|
||||
# hostname_override = "custom-hostname"
|
||||
|
||||
[capture]
|
||||
fps = 30
|
||||
use_dxgi = true
|
||||
gdi_fallback = true
|
||||
|
||||
[encoding]
|
||||
codec = "auto" # auto, raw, vp9, h264
|
||||
quality = 75 # 1-100
|
||||
hardware_encoding = true
|
||||
"#
|
||||
}
|
||||
Reference in New Issue
Block a user