use std::fs;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::collection::Collection;
use crate::environment::{Environment, PendingSecret, parse_vars_pending};
use crate::git_remote::GitOrigin;
use crate::hurl::HurlEntry;
use crate::i18n::Language;
use crate::request::RequestView;
use crate::tui::remote::WorkspaceGitOrigin;
#[derive(Serialize, Deserialize, Default, Clone)]
pub struct PersistedEnv {
pub name: String,
#[serde(default)]
pub vars: Vec<PersistedVar>,
#[serde(default)]
pub path: Option<String>,
#[serde(default)]
pub git_origin: Option<GitOrigin>,
}
#[derive(Serialize, Deserialize, Default, Clone)]
pub struct PersistedVar {
pub key: String,
pub raw: String,
#[serde(default)]
pub user_added: bool,
}
impl PersistedEnv {
pub fn from_environment(env: &Environment) -> Self {
Self {
name: env.name.clone(),
vars: env
.vars
.iter()
.map(|v| PersistedVar {
key: v.key.clone(),
raw: v.raw.clone(),
user_added: v.user_added,
})
.collect(),
path: env.path.as_ref().map(|p| p.to_string_lossy().into_owned()),
git_origin: env.git_origin.clone(),
}
}
pub fn restore(&self) -> (Environment, Vec<PendingSecret>) {
let content = self
.vars
.iter()
.map(|v| format!("{}={}", v.key, v.raw))
.collect::<Vec<_>>()
.join("\n");
let (mut env, pending) = parse_vars_pending(self.name.clone(), &content);
for pv in &self.vars {
if pv.user_added
&& let Some(var) = env.vars.iter_mut().find(|x| x.key == pv.key)
{
var.user_added = true;
}
}
env.path = self
.path
.clone()
.filter(|s| !s.is_empty())
.map(std::path::PathBuf::from);
env.git_origin = self.git_origin.clone();
(env, pending)
}
}
#[derive(Serialize, Deserialize, Default)]
pub struct PersistedTab {
pub name: String,
#[serde(default)]
pub entries: Vec<HurlEntry>,
#[serde(default)]
pub selected_entry: usize,
#[serde(default)]
pub path: Option<String>,
#[serde(default)]
pub git_origin: Option<GitOrigin>,
#[serde(default)]
pub linked_env_index: Option<usize>,
#[serde(default)]
pub workspace_root: Option<String>,
#[serde(default)]
pub workspace_filter_hurl_json: Option<bool>,
#[serde(default)]
pub workspace_downloaded_from_git: bool,
#[serde(default)]
pub workspace_git_origin: Option<WorkspaceGitOrigin>,
}
pub struct PendingWorkspaceReload {
pub tab_name: String,
pub origin: WorkspaceGitOrigin,
pub relative_selected_path: Option<String>,
}
impl PersistedTab {
pub fn from_collection(c: &Collection, linked_env_index: Option<usize>) -> Self {
Self {
name: c.name.clone(),
entries: if c.workspace_root.is_some() {
Vec::new()
} else {
c.entries.clone()
},
selected_entry: c.selected_entry,
path: c.path.as_ref().map(|p| p.to_string_lossy().into_owned()),
git_origin: c.git_origin.clone(),
linked_env_index,
workspace_root: c
.workspace_root
.as_ref()
.map(|p| p.to_string_lossy().into_owned()),
workspace_filter_hurl_json: c
.workspace_root
.is_some()
.then_some(c.workspace_filter_hurl_json),
workspace_downloaded_from_git: c.workspace_downloaded_from_git,
workspace_git_origin: c.workspace_git_origin.clone(),
}
}
pub fn into_collection(
self,
linked_env_id: Option<u64>,
) -> (Collection, Option<PendingWorkspaceReload>) {
let workspace_root = self
.workspace_root
.filter(|s| !s.is_empty())
.map(std::path::PathBuf::from);
let path = self
.path
.filter(|s| !s.is_empty())
.map(std::path::PathBuf::from);
let root_missing = workspace_root.as_ref().is_some_and(|r| !r.exists());
let pending_reload = if root_missing {
self.workspace_git_origin.clone().map(|origin| {
let relative_selected_path = match (&path, &workspace_root) {
(Some(p), Some(root)) => p
.strip_prefix(root)
.ok()
.map(|rel| rel.to_string_lossy().into_owned()),
_ => None,
};
PendingWorkspaceReload {
tab_name: self.name.clone(),
origin,
relative_selected_path,
}
})
} else {
None
};
let workspace_root = if root_missing { None } else { workspace_root };
let (entries, path) = if workspace_root.is_some() {
match path
.as_ref()
.filter(|p| p.exists())
.and_then(|p| std::fs::read_to_string(p).ok())
{
Some(content) => (crate::postman::parse_collection(&content), path),
None => (Vec::new(), None),
}
} else if root_missing {
(Vec::new(), None)
} else {
(self.entries, path)
};
let mut c = Collection::new(self.name, entries);
c.selected_entry = self.selected_entry.min(c.entries.len().saturating_sub(1));
c.path = path;
c.git_origin = self.git_origin;
c.linked_env_id = linked_env_id;
c.workspace_root = workspace_root;
c.workspace_filter_hurl_json = self.workspace_filter_hurl_json.unwrap_or(true);
c.workspace_downloaded_from_git = self.workspace_downloaded_from_git && !root_missing;
c.workspace_git_origin = if root_missing {
None
} else {
self.workspace_git_origin
};
c.sync_folder_to_selected();
c.set_workspace_browse_from_path();
c.sync_ws_cursor();
(c, pending_reload)
}
}
#[derive(Serialize, Deserialize)]
pub struct PersistedState {
#[serde(default)]
pub language: Language,
#[serde(default)]
pub base_url: String,
#[serde(default)]
pub tabs: Vec<PersistedTab>,
#[serde(default)]
pub active_tab: usize,
#[serde(default)]
pub last_browse_dir: Option<String>,
#[serde(default)]
pub last_env_dir: Option<String>,
#[serde(default = "yes")]
pub confirm_on_exit: bool,
#[serde(default = "yes")]
pub confirm_on_clear: bool,
#[serde(default)]
pub always_save_when_prompted: bool,
#[serde(default = "default_list_width")]
pub list_width: u16,
#[serde(default = "default_response_pct")]
pub response_pct: u16,
#[serde(default)]
pub recent_git_urls: Vec<String>,
#[serde(default)]
pub default_request_view: RequestView,
#[serde(default)]
pub global_envs: Vec<PersistedEnv>,
#[serde(default)]
pub active_global_env: Option<usize>,
}
fn yes() -> bool {
true
}
fn default_list_width() -> u16 {
38
}
fn default_response_pct() -> u16 {
42
}
impl Default for PersistedState {
fn default() -> Self {
Self {
language: Language::default(),
base_url: String::new(),
tabs: Vec::new(),
active_tab: 0,
last_browse_dir: None,
last_env_dir: None,
confirm_on_exit: true,
confirm_on_clear: true,
always_save_when_prompted: false,
list_width: default_list_width(),
response_pct: default_response_pct(),
recent_git_urls: Vec::new(),
default_request_view: RequestView::default(),
global_envs: Vec::new(),
active_global_env: None,
}
}
}
fn state_path() -> Option<PathBuf> {
if let Some(dir) = std::env::var_os("PAPERBOY_STATE_DIR") {
return Some(PathBuf::from(dir).join("state.json"));
}
let base = std::env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from)
.or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".config")))
.or_else(|| std::env::var_os("APPDATA").map(PathBuf::from))?;
Some(base.join("paperboy").join("state.json"))
}
pub fn load_state() -> Option<PersistedState> {
let text = fs::read_to_string(state_path()?).ok()?;
serde_json::from_str(&text).ok()
}
pub fn save_state(state: &PersistedState) {
if cfg!(test) {
return;
}
let Some(path) = state_path() else { return };
if let Some(parent) = path.parent() {
let _ = fs::create_dir_all(parent);
}
if let Ok(json) = serde_json::to_string_pretty(state) {
let _ = fs::write(path, json);
}
}