@@ 329,6 329,15 @@ dependencies = [
]
[[package]]
+name = "directories"
+version = "5.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
name = "dirs-next"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ 339,6 348,18 @@ dependencies = [
]
[[package]]
+name = "dirs-sys"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
name = "dirs-sys-next"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ 695,6 716,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
+[[package]]
name = "paste"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ 874,6 901,7 @@ dependencies = [
"camino",
"clap",
"csv",
+ "directories",
"ecow",
"fltk",
"grid",
@@ 3,6 3,7 @@
mod filter;
mod misc;
+mod recent_files;
mod select_cols;
mod table_widget;
mod tblstate;
@@ 37,6 38,7 @@ pub enum SchemaVersion {
CS5,
CustomV1,
CustomV2,
+ RecentV1(usize), // index into the "most recent schemas (v1) queue"
}
#[derive(Clone, Debug)]
@@ 99,10 101,12 @@ pub struct AppData {
selected_attributes: std::collections::HashMap<tocs::tbl::Header, indexmap::IndexSet<tocs::tbl::Attribute>>, // which attributes should be shown in which order for a given header?
warnings: std::vec::Vec<tocs::tbl::DeserializeError>,
//edited: std::collections::HashSet<(i32, i32)>, // edited since last save
+ recent_schemas_v1: recent_files::RecentFiles,
}
struct App {
app: fltk::app::App,
+ sender: fltk::app::Sender<Msg>,
receiver: fltk::app::Receiver<Msg>,
widgets: Widgets,
state: AppData,
@@ 227,6 231,10 @@ impl App {
Msg::SetVersion(SchemaVersion::CustomV2),
);
+ menu.add("&Schemas/Import/Recent (CS1)\t", fltk::enums::Shortcut::None, fltk::menu::MenuFlag::Submenu, |_| {});
+
+ menu.add("&Schemas/Import/Recent (CS2 onward)\t", fltk::enums::Shortcut::None, fltk::menu::MenuFlag::Submenu, |_| {});
+
menu.add_emit(
"&File/Load tbl with current schema\t",
fltk::enums::Shortcut::None,
@@ 348,7 356,7 @@ impl App {
flex.fixed(&flex_row, 26);
let mut table = table_widget::FilteredTable::new();
- table.emit(sender, Msg::TableEvent);
+ table.emit(sender.clone(), Msg::TableEvent);
flex.end();
@@ 358,6 366,7 @@ impl App {
let mut this = Self {
app,
+ sender,
receiver,
widgets: Widgets {
menu,
@@ 369,6 378,9 @@ impl App {
state,
};
+ this.update_recent_schemas_v1_menu();
+ this.update_recent_schemas_v2_menu();
+ this.update_recent_tbl_menu();
this.update_after_file_reload();
this
@@ 397,6 409,7 @@ impl App {
tbl_version,
path,
warnings,
+ recent_schemas_v1: _,
} = &mut self.state;
tbl_history.clear_all();
selected_attributes.clear();
@@ 450,6 463,7 @@ impl App {
}
}
}
+ SchemaVersion::RecentV1(_) => todo!(),
};
self.state.tbl_version = new_version;
self.state.tbl_history.clear_all(); // we don't want a file incompatible with the new schema
@@ 462,7 476,7 @@ impl App {
SchemaVersion::CS3 => ("cs3.json", &tocs::tbl::schemas::CS3),
SchemaVersion::CS4 => ("cs4.json", &tocs::tbl::schemas::CS4),
SchemaVersion::CS5 => ("cs5.json", &tocs::tbl::schemas::CS5),
- SchemaVersion::CustomV1 | SchemaVersion::CustomV2 => unreachable!("not present in menu"),
+ SchemaVersion::CustomV1 | SchemaVersion::CustomV2 | SchemaVersion::RecentV1(_) => unreachable!("not present in menu"),
};
let Some(path) = misc::save_file_dialog("json", name) else { return };
match misc::export_schemas(&path, schemas) {
@@ 543,6 557,7 @@ impl App {
tbl_version,
path,
warnings,
+ recent_schemas_v1: _,
} = &mut self.state;
tbl_history.clear_all();
selected_attributes.clear();
@@ 650,6 665,42 @@ impl App {
}
}
+ fn update_recent_schemas_v1_menu(&mut self) {
+ let i = self.widgets.menu.find_index("&Schemas/Import/Recent (CS1)\t");
+ self.widgets.menu.clear_submenu(i).unwrap();
+ let mut recent_menu = self.widgets.menu.at(i).unwrap();
+ if self.state.recent_schemas_v1.is_empty() {
+ recent_menu.deactivate();
+ } else {
+ recent_menu.activate();
+ }
+ drop(recent_menu);
+ for (i, path) in self.state.recent_schemas_v1.iter().enumerate() {
+ self.widgets.menu.add_emit(
+ &format!("&Schemas/Import/Recent (CS1)\t/{} (DO NOT CLICK)", misc::QuotedFltkMenuLabel::new(path.as_str())),
+ fltk::enums::Shortcut::None,
+ fltk::menu::MenuFlag::Normal,
+ self.sender.clone(),
+ Msg::SetVersion(SchemaVersion::RecentV1(i)),
+ );
+ }
+ }
+
+ fn update_recent_schemas_v2_menu(&mut self) {
+ let i = self.widgets.menu.find_index("&Schemas/Import/Recent (CS2 onward)\t");
+ let mut recent_menu = self.widgets.menu.at(i).unwrap();
+ recent_menu.deactivate();
+ }
+
+ fn update_recent_tbl_menu(&mut self) {
+ // TODO
+ }
+
+ fn update_to_schemas_v1(&mut self) {
+ // TODO: update menu
+ // TODO: save
+ }
+
fn update_table(&mut self) {
// get active entry type from header selector
self.state.active_entry_type = self.widgets.header_selector.value().and_then(|hdr| {
@@ 687,6 738,7 @@ impl App {
path: _,
tbl_history,
warnings,
+ recent_schemas_v1: _,
} = &mut self.state;
// if there are warnings, show button to display them
@@ 789,6 841,7 @@ fn main() {
tbl_version: version,
path,
warnings: vec![],
+ recent_schemas_v1: recent_files::load_recent_schemas_v1(),
},
tocs::tbl::ReadTblResult::Warn { tbl, warnings } => AppData {
tbl_history: TblStateHistory::new(tbl),
@@ 797,6 850,7 @@ fn main() {
tbl_version: version,
path,
warnings,
+ recent_schemas_v1: recent_files::load_recent_schemas_v1(),
},
tocs::tbl::ReadTblResult::Err(e) => {
log::error!("Unable to read file: {e}");
@@ 815,6 869,7 @@ fn main() {
tbl_version: tocs::tbl::Version::CS1En,
path: Utf8PathBuf::new(),
warnings: vec![],
+ recent_schemas_v1: recent_files::load_recent_schemas_v1(),
},
};
@@ 0,0 1,148 @@
+// cache of most recently used files
+//
+// Should be updated on every load/read and persisted to disk.
+// Only UTF-8 paths are supported.
+// The number of files is hardcoded to 10.
+//
+// Disk format is the concatenation of:
+// - u32 (little endian): length of the path
+// - UTF-8 encoded path (not null-terminated)
+// for every path.
+// Paths are sorted by recency of use and
+
+use camino::{Utf8Path, Utf8PathBuf};
+
+const MAX_SIZE: usize = 10;
+
+pub struct RecentFiles {
+ /// Queue of paths, sorted from most recent use to least recent use.
+ /// Must not contain exact duplicates.
+ paths: std::collections::VecDeque<Utf8PathBuf>,
+}
+
+impl RecentFiles {
+ pub fn new() -> Self {
+ Self {
+ paths: std::collections::VecDeque::with_capacity(MAX_SIZE),
+ }
+ }
+
+ pub fn iter(&self) -> impl Iterator<Item = &Utf8Path> {
+ self.paths.iter().map(|path| path.as_ref())
+ }
+
+ pub fn is_empty(&self) -> bool {
+ self.paths.is_empty()
+ }
+
+ pub fn write_to_file(&self, filename: &Utf8Path) -> std::io::Result<()> {
+ let mut data = std::vec::Vec::<u8>::with_capacity(1024);
+ for path in self.paths.iter() {
+ let bytes = path.as_str().as_bytes();
+ let len: u32 = bytes.len().try_into().unwrap();
+ data.extend_from_slice(&len.to_le_bytes());
+ data.extend_from_slice(bytes);
+ }
+ std::fs::write(filename, &data)
+ }
+
+ fn find_path_index(&self, path: &Utf8Path) -> Option<usize> {
+ for (i, p) in self.iter().enumerate() {
+ if p == path {
+ return Some(i);
+ }
+ }
+ None
+ }
+
+ pub fn mark_as_updated(&mut self, path: &Utf8Path) {
+ match self.find_path_index(path) {
+ None => {
+ // path is new, hasn't been used recently
+ if self.paths.len() >= MAX_SIZE {
+ self.paths.pop_back();
+ }
+ self.paths.push_front(path.to_owned());
+ }
+ Some(i) => {
+ if i == 0 {
+ // nothing to do, path was already the most recent one before this call
+ } else {
+ // remove path from its current position and move it to the front
+ let p = self.paths.remove(i).unwrap();
+ self.paths.push_front(p);
+ }
+ }
+ }
+ }
+
+ pub fn refresh_from_file(&mut self, filename: &Utf8Path) {
+ fn inner(this: &mut RecentFiles, filename: &Utf8Path) -> Option<()> {
+ let data = std::fs::read(filename).ok()?;
+ let mut off = 0;
+ while off < data.len() && this.paths.len() < MAX_SIZE {
+ let len_bytes = data.get(off..off + 4)?;
+ let len_bytes: [u8; 4] = len_bytes.try_into().ok()?;
+ let len: usize = u32::from_le_bytes(len_bytes) as usize;
+ off += 4;
+ let bytes = data.get(off..off + len)?;
+ let s = std::str::from_utf8(bytes).ok()?;
+ let path = Utf8Path::new(s);
+ // Add path if it hasn't appeared before.
+ // Note that we can't use the mark_as_updated method because we would reverse the correct order
+ if this.find_path_index(path).is_none() {
+ this.paths.push_back(path.to_owned());
+ }
+ off += len;
+ }
+ Some(())
+ }
+
+ self.paths.clear();
+ let result = inner(self, filename);
+ if result.is_none() {
+ self.paths.clear();
+ }
+ }
+}
+
+struct RecentFilesPaths {
+ schemas_v1: Utf8PathBuf,
+ schemas_v2: Utf8PathBuf,
+ tbls: Utf8PathBuf,
+}
+
+fn get_files_paths() -> Option<RecentFilesPaths> {
+ let project_dirs = directories::ProjectDirs::from(
+ "", // qualifier
+ "huellenoperator", // organization
+ "tbled", // application
+ )?;
+ let base: &Utf8Path = project_dirs.state_dir()?.try_into().ok()?;
+ std::fs::create_dir_all(&base).ok()?;
+ Some(RecentFilesPaths {
+ schemas_v1: base.join("recent_schemas_v1.dat"),
+ schemas_v2: base.join("recent_schemas_v2.dat"),
+ tbls: base.join("recent_tbls.dat"),
+ })
+}
+
+lazy_static::lazy_static! {
+ static ref PATHS: Option<RecentFilesPaths> = get_files_paths();
+}
+
+fn load_file(path: Option<&Utf8Path>) -> RecentFiles {
+ let mut result = RecentFiles::new();
+ if let Some(p) = path {
+ result.refresh_from_file(p);
+ }
+ result
+}
+
+pub fn load_recent_schemas_v1() -> RecentFiles {
+ load_file(PATHS.as_ref().map(|p| p.schemas_v1.as_ref()))
+}
+
+pub fn save_recent_schemas_v1(recent_files: &RecentFiles) -> Option<()> {
+ PATHS.as_ref().and_then(|p| recent_files.write_to_file(&p.schemas_v1).ok())
+}