use std::{fs::ReadDir, path::PathBuf};
use directories::ProjectDirs;
use ulid::Ulid;
use crate::{settings::AppSettings, tab::Tab, TabCtx};
#[derive(Debug)]
pub enum TabAction {
LoadFile {
file_path: PathBuf,
},
CreateFile {
folder_path: PathBuf,
},
CreateFolder {
parent_path: PathBuf,
},
RenameFolder {
folder_path: PathBuf,
current_name: String,
},
}
#[derive(Debug)]
pub struct Storage {
base: PathBuf,
list: Vec<StorageItem>,
}
pub struct TabIterator {
iter: ReadDir,
ctx: TabCtx,
}
impl Iterator for TabIterator {
type Item = anyhow::Result<Tab>;
fn next(&mut self) -> Option<Self::Item> {
loop {
match self.iter.next() {
Some(Ok(entry)) => {
let path = entry.path();
if path.is_file() {
return Some(Tab::new_from_file(&path, self.ctx.clone()));
}
}
Some(Err(e)) => return Some(Err(e.into())),
None => return None,
}
}
}
}
impl Storage {
pub fn new() -> anyhow::Result<Self> {
let base = if let Some(proj_dirs) = ProjectDirs::from("com", "httpman", "httpman") {
proj_dirs.data_dir().to_path_buf()
} else {
std::env::current_dir()?.join(".httpman")
};
std::fs::create_dir_all(&base)?;
let me = Self { base, list: vec![] };
let tabs = me.tabs_folder();
std::fs::create_dir_all(&tabs)?;
let saved = me.saved_folder();
std::fs::create_dir_all(&saved)?;
Ok(me)
}
pub fn load_active_tabs(&self, ctx: TabCtx) -> anyhow::Result<TabIterator> {
let tabs = self.tabs_folder();
let iter = std::fs::read_dir(tabs)?;
Ok(TabIterator { iter, ctx })
}
pub fn tabs_root(&self, ctx: TabCtx) -> StorageItem {
let path = self.tabs_folder();
StorageItem::new_from_folder(path, ctx)
}
pub fn refresh_list(&mut self, ctx: &TabCtx) -> anyhow::Result<&mut [StorageItem]> {
self.list.clear();
self.load_list(ctx)
}
pub fn load_list(&mut self, ctx: &TabCtx) -> anyhow::Result<&mut [StorageItem]> {
if self.list.is_empty() {
let saved_folder = self.saved_folder();
for item in std::fs::read_dir(saved_folder)?.flatten() {
self.list.push(StorageItem::new(ctx.clone(), item.path())?);
}
self.list.sort_by_key(|i| if i.is_folder() { 0 } else { 1 });
}
Ok(&mut self.list)
}
pub fn save_tab(&self, tab: &Tab) -> anyhow::Result<()> {
let path = self.tabs_folder().join(tab.file_name());
tab.save_to_file(&path)
}
pub fn delete_tab(&self, tab: &Tab) -> anyhow::Result<()> {
let path = self.tabs_folder().join(tab.file_name());
if path.exists() {
std::fs::remove_file(path)?;
}
Ok(())
}
pub fn save_settings(&self, settings: &AppSettings) -> anyhow::Result<()> {
let path = AppSettings::settings_path(&self.base);
settings.save_to_file(path)
}
pub fn load_settings(&self) -> AppSettings {
let path = AppSettings::settings_path(&self.base);
AppSettings::load_from_file(path)
}
pub fn save_tab_to_saved(&self, tab: &Tab, name: &str) -> anyhow::Result<()> {
let file_name = format!("{}.httpmand", name);
let path = self.saved_folder().join(file_name);
tab.save_to_file(&path)
}
pub fn create_saved_folder(&self, name: &str) -> anyhow::Result<()> {
let path = self.saved_folder().join(name);
std::fs::create_dir_all(path)?;
Ok(())
}
pub fn load_saved_tab(&self, path: &std::path::Path, ctx: TabCtx) -> anyhow::Result<Tab> {
Tab::new_from_file(path, ctx)
}
pub fn rename_folder(&self, old_path: &std::path::Path, new_name: &str) -> anyhow::Result<()> {
if let Some(parent) = old_path.parent() {
let new_path = parent.join(new_name);
std::fs::rename(old_path, new_path)?;
}
Ok(())
}
pub fn is_empty(&self) -> bool {
let tabs_empty = self
.tabs_folder()
.read_dir()
.map(|entries| entries.count() == 0)
.unwrap_or(true);
let saved_empty = self
.saved_folder()
.read_dir()
.map(|entries| entries.count() == 0)
.unwrap_or(true);
tabs_empty && saved_empty
}
pub fn create_example_data(&self, ctx: &TabCtx) -> anyhow::Result<()> {
let examples_folder = self.saved_folder().join("Examples");
std::fs::create_dir_all(&examples_folder)?;
let bitcoin_tab = crate::tab::Tab::new_bitcoin_api_example(ctx.clone());
let bitcoin_path = examples_folder.join("Bitcoin_Price_API.httpmand");
bitcoin_tab.save_to_file(&bitcoin_path)?;
let image_tab = crate::tab::Tab::new_image_example(ctx.clone());
let image_path = examples_folder.join("Random_Image.httpmand");
image_tab.save_to_file(&image_path)?;
Ok(())
}
fn requests_folder(&self) -> PathBuf {
self.base.join("requests")
}
fn tabs_folder(&self) -> PathBuf {
self.requests_folder().join("Tabs")
}
pub fn saved_folder(&self) -> PathBuf {
self.requests_folder().join("Saved")
}
}
#[derive(Debug)]
pub enum StorageItem {
Folder {
name: String,
path: PathBuf,
items: Vec<StorageItem>,
},
File {
title: String,
path: PathBuf,
tab_id: Ulid,
},
}
impl StorageItem {
pub fn new(ctx: TabCtx, item: PathBuf) -> anyhow::Result<Self> {
if item.is_dir() {
Ok(Self::new_from_folder(item, ctx))
} else if item.is_file() {
Self::new_from_file(item, ctx.clone())
} else {
Err(anyhow::anyhow!("Not a file or folder"))
}
}
pub fn is_folder(&self) -> bool {
match self {
StorageItem::Folder { .. } => true,
_ => false,
}
}
fn new_from_folder(item: PathBuf, ctx: TabCtx) -> Self {
let name = item
.file_name()
.map(|f| f.to_string_lossy().to_string())
.unwrap_or_else(|| item.to_string_lossy().to_string());
let mut items = vec![];
if let Ok(entries) = std::fs::read_dir(&item) {
for entry in entries.flatten() {
if let Ok(storage_item) = Self::new(ctx.clone(), entry.path()) {
items.push(storage_item);
}
}
}
Self::Folder {
name,
path: item,
items,
}
}
fn new_from_file(item: PathBuf, ctx: TabCtx) -> anyhow::Result<Self> {
let file_name = item
.file_name()
.map(|f| f.to_string_lossy())
.unwrap_or_default();
if file_name.ends_with(".httpmand") {
let tab = Tab::new_from_file(&item, ctx.clone())?;
Ok(StorageItem::File {
title: tab.title(),
path: item,
tab_id: tab.id,
})
} else {
Err(anyhow::anyhow!("Not a tab file"))
}
}
pub fn name(&self) -> &str {
match self {
Self::Folder { name, .. } => name,
Self::File { title, .. } => title,
}
}
pub fn render_tree(&self, ui: &mut egui::Ui, ctx: &TabCtx) -> Option<TabAction> {
match self {
StorageItem::Folder { name, items, path } => {
let mut action = None;
let header_response = egui::CollapsingHeader::new(format!("📁 {}", name))
.default_open(false)
.show(ui, |ui| {
ui.indent("folder_indent", |ui| {
ui.allocate_response(ui.available_size(), egui::Sense::hover())
.context_menu(|ui| {
if ui.button("📄 New Request").clicked() {
action = Some(TabAction::CreateFile {
folder_path: path.clone(),
});
ui.close_menu();
}
if ui.button("📁 New Folder").clicked() {
action = Some(TabAction::CreateFolder {
parent_path: path.clone(),
});
ui.close_menu();
}
if ui.button("✏️ Rename").clicked() {
action = Some(TabAction::RenameFolder {
folder_path: path.clone(),
current_name: name.clone(),
});
ui.close_menu();
}
});
for item in items {
if let Some(item_action) = item.render_tree(ui, ctx) {
action = Some(item_action);
}
}
});
});
if header_response.header_response.double_clicked() {
action = Some(TabAction::RenameFolder {
folder_path: path.clone(),
current_name: name.clone(),
});
}
action
}
StorageItem::File { title, path, .. } => {
let mut action = None;
ui.horizontal(|ui| {
if ui.link(format!("📄 {}", title)).clicked() {
action = Some(TabAction::LoadFile {
file_path: path.clone(),
});
}
});
action
}
}
}
}