Rewrite README in Chinese and English, add drone examples

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-06-12 21:56:20 +08:00
parent a2c7c0fa71
commit e939b16d2d
67 changed files with 1101 additions and 675 deletions

View File

@@ -0,0 +1,175 @@
use std::env;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use tracing::warn;
use pipeview_client::config::SessionConfig;
const GUI_STATE_FILE_NAME: &str = "gui-state.json";
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SessionLogConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub file_path: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PersistedGuiState {
#[serde(default)]
pub sessions: Vec<SessionConfig>,
#[serde(default)]
pub sessions_log: Vec<SessionLogConfig>,
#[serde(default)]
pub active: usize,
#[serde(default = "default_true")]
pub show_timestamp: bool,
#[serde(default = "default_true")]
pub show_direction: bool,
#[serde(default = "default_true")]
pub show_pipeline: bool,
}
pub fn load_gui_state() -> PersistedGuiState {
match load_gui_state_from_path(&gui_state_path()) {
Ok(state) => state,
Err(err) if err.kind() == io::ErrorKind::NotFound => PersistedGuiState::default(),
Err(err) => {
warn!(error = %err, "Failed to load persisted GUI state");
PersistedGuiState::default()
}
}
}
pub fn save_gui_state(state: &PersistedGuiState) {
if let Err(err) = save_gui_state_to_path(state, &gui_state_path()) {
warn!(error = %err, "Failed to persist GUI state");
}
}
fn gui_state_path() -> PathBuf {
gui_state_path_for_os_and_env(env::consts::OS, |key| env::var(key).ok())
}
/// Returns the pipeview configuration directory (the parent of gui-state.json).
pub fn config_dir() -> PathBuf {
gui_state_path()
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from("."))
}
fn gui_state_path_for_os_and_env(os: &str, get_env: impl Fn(&str) -> Option<String>) -> PathBuf {
match os {
"windows" => {
if let Some(path) = get_env("APPDATA").filter(|path| !path.trim().is_empty()) {
return PathBuf::from(path)
.join("pipeview")
.join(GUI_STATE_FILE_NAME);
}
if let Some(path) = get_env("LOCALAPPDATA").filter(|path| !path.trim().is_empty()) {
return PathBuf::from(path)
.join("pipeview")
.join(GUI_STATE_FILE_NAME);
}
}
"macos" => {
if let Some(home) = get_env("HOME").filter(|path| !path.trim().is_empty()) {
return PathBuf::from(home)
.join("Library")
.join("Application Support")
.join("pipeview")
.join(GUI_STATE_FILE_NAME);
}
}
_ => {
if let Some(path) = get_env("XDG_CONFIG_HOME").filter(|path| !path.trim().is_empty()) {
return PathBuf::from(path)
.join("pipeview")
.join(GUI_STATE_FILE_NAME);
}
if let Some(home) = get_env("HOME").filter(|path| !path.trim().is_empty()) {
return PathBuf::from(home)
.join(".config")
.join("pipeview")
.join(GUI_STATE_FILE_NAME);
}
}
}
PathBuf::from(GUI_STATE_FILE_NAME)
}
fn load_gui_state_from_path(path: &Path) -> io::Result<PersistedGuiState> {
let text = fs::read_to_string(path)?;
serde_json::from_str(&text).map_err(io::Error::other)
}
fn save_gui_state_to_path(state: &PersistedGuiState, path: &Path) -> io::Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let json = serde_json::to_string_pretty(state).map_err(io::Error::other)?;
fs::write(path, json)
}
const fn default_true() -> bool {
true
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::{SystemTime, UNIX_EPOCH};
use pipeview_core::transport::TransportConfig;
#[test]
fn windows_gui_state_path_uses_appdata() {
let path = gui_state_path_for_os_and_env("windows", |key| match key {
"APPDATA" => Some(String::from(r"C:\Users\Test\AppData\Roaming")),
_ => None,
});
assert_eq!(
path,
PathBuf::from(r"C:\Users\Test\AppData\Roaming")
.join("pipeview")
.join(GUI_STATE_FILE_NAME)
);
}
#[test]
fn gui_state_roundtrip() {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let dir = env::temp_dir().join(format!("pipeview-gui-state-{unique}"));
let path = dir.join(GUI_STATE_FILE_NAME);
let state = PersistedGuiState {
sessions: vec![SessionConfig {
transport: TransportConfig::Tcp {
addr: String::from("127.0.0.1:9000"),
},
..SessionConfig::default()
}],
sessions_log: vec![SessionLogConfig::default()],
active: 0,
show_timestamp: false,
show_direction: true,
show_pipeline: false,
};
save_gui_state_to_path(&state, &path).unwrap();
let loaded = load_gui_state_from_path(&path).unwrap();
assert_eq!(loaded.sessions.len(), 1);
assert!(!loaded.show_timestamp);
assert!(loaded.show_direction);
assert!(!loaded.show_pipeline);
let _ = fs::remove_file(&path);
let _ = fs::remove_dir_all(&dir);
}
}