From adc30774c5c57bf10d661e2e7cddf76bb19b8e9f Mon Sep 17 00:00:00 2001 From: misson20000 Date: Mon, 11 Nov 2024 23:27:26 -0500 Subject: [PATCH] implement exporting data (still a little rough around the edges) --- Cargo.toml | 2 +- src/model/datapath.rs | 41 ++- src/model/datapath/fetcher.rs | 20 +- src/model/listing/cursor/hexdump.rs | 2 +- src/model/listing/cursor/hexstring.rs | 2 +- src/view/action/tree.rs | 1 + src/view/action/tree/export_binary_node.rs | 79 +++++ src/view/charm.cmb | 136 +++++++- src/view/export-dialog.ui | 227 +++++++++++++ src/view/export.rs | 376 +++++++++++++++++++++ src/view/listing/bucket/hexdump.rs | 2 +- src/view/listing/token_view.rs | 2 +- src/view/mod.rs | 1 + src/view/window.rs | 2 + 14 files changed, 856 insertions(+), 37 deletions(-) create mode 100644 src/view/action/tree/export_binary_node.rs create mode 100644 src/view/export-dialog.ui create mode 100644 src/view/export.rs diff --git a/Cargo.toml b/Cargo.toml index 4354df7..c270e8f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ tokio = { version = "1.41.0", features = ["full"] } hex-literal = "0.4.1" send_wrapper = "0.6.0" conv = "0.3.3" -parking_lot = "0.12.3" +parking_lot = { version = "0.12.3", features = ["send_guard"] } imbl = "3.0.0" enum_dispatch = "0.3.13" byteorder = "1.5.0" diff --git a/src/model/datapath.rs b/src/model/datapath.rs index ad0cd74..bec500d 100644 --- a/src/model/datapath.rs +++ b/src/model/datapath.rs @@ -50,13 +50,14 @@ pub struct FetchRequest<'a> { addr: u64, data: &'a [Atomic], flags: Option<&'a [Atomic]>, + ignore_edits: bool, } pub struct FetchResult { /// The bitwise-OR accumulation of all the flags from the request - flags: FetchFlags, + pub flags: FetchFlags, /// How many bytes were processed. Advance your request by this much and try again if you didn't get everything. - loaded: usize, + pub loaded: usize, } type LoadFuture = std::pin::Pin>>; @@ -173,24 +174,27 @@ impl DataPath { pub fn iter_filters(&self) -> impl std::iter::DoubleEndedIterator { self.filters.iter() } -} -async fn fetch_filters(filters: imbl::Vector, mut rq: FetchRequest<'_>) -> FetchResult { - for filter in filters.iter().rev() { - rq = match filter.load(std::mem::replace(&mut rq, FetchRequest::default())).await { - FilterFetchResult::Pass(rq) => rq, - FilterFetchResult::Done(rs) => return rs, + pub async fn fetch(&self, mut rq: FetchRequest<'_>) -> FetchResult { + let filters = self.filters.clone(); + + for filter in filters.iter().rev() { + rq = match filter.load(std::mem::replace(&mut rq, FetchRequest::default())).await { + FilterFetchResult::Pass(rq) => rq, + FilterFetchResult::Done(rs) => return rs, + } + } + + FetchResult { + flags: FetchFlags::default(), + loaded: rq.len() as usize, } } - - FetchResult { - flags: FetchFlags::default(), - loaded: rq.len() as usize, - } + } impl<'a> FetchRequest<'a> { - pub fn new(addr: u64, data: &'a [Atomic], flags: Option<&'a [Atomic]>) -> Self { + pub fn new(addr: u64, data: &'a [Atomic], flags: Option<&'a [Atomic]>, ignore_edits: bool) -> Self { if let Some(flags) = flags.as_ref() { assert_eq!(flags.len(), data.len()); } @@ -199,6 +203,7 @@ impl<'a> FetchRequest<'a> { addr, data, flags, + ignore_edits, } } @@ -221,6 +226,7 @@ impl<'a> FetchRequest<'a> { addr: self.addr, data: self.data, flags: self.flags, + ignore_edits: self.ignore_edits, }) } else if self.addr < addr && self.addr + self.len() as u64 > addr { /* if we overlap the split point */ @@ -234,10 +240,12 @@ impl<'a> FetchRequest<'a> { addr: self.addr, data: ad, flags: af, + ignore_edits: self.ignore_edits, }, Self { addr, data: bd, flags: bf, + ignore_edits: self.ignore_edits, }) } else if self.addr < addr && self.addr + self.len() as u64 <= addr { /* if we are entirely before the split point */ @@ -245,6 +253,7 @@ impl<'a> FetchRequest<'a> { addr: self.addr, data: self.data, flags: self.flags, + ignore_edits: self.ignore_edits, }, Self::default()) } else { panic!("unreachable") @@ -413,6 +422,10 @@ impl LoadSpaceFilter { impl OverwriteFilter { fn load<'a>(&self, rq: FetchRequest<'a>) -> FilterFetchResult<'a> { + if rq.ignore_edits { + return FilterFetchResult::Pass(rq); + } + let (before, overlap, after) = rq.split3(self.offset, Some(self.bytes.len() as u64)); /* Process this immediately since callers can observe that this data loads instantly even if data before it takes a while to load asynchronously. */ diff --git a/src/model/datapath/fetcher.rs b/src/model/datapath/fetcher.rs index 71f2c3c..e71c597 100644 --- a/src/model/datapath/fetcher.rs +++ b/src/model/datapath/fetcher.rs @@ -1,11 +1,9 @@ use std::vec; use crate::model::datapath::DataPath; -use crate::model::datapath::fetch_filters; use crate::model::datapath::FetchFlags; use crate::model::datapath::FetchRequest; use crate::model::datapath::FetchResult; -use crate::model::datapath::Filter; use atomig::Atomic; use atomig::Ordering; @@ -18,7 +16,7 @@ struct FetcherInterior { #[borrows(data, flags)] #[not_covariant] - future: Option], &'this [Atomic], imbl::Vector)> + 'this>>>, + future: Option], &'this [Atomic], DataPath)> + 'this>>>, } pub struct Fetcher { @@ -38,9 +36,7 @@ impl Fetcher { }.build() } - pub fn new(datapath: &DataPath, addr: u64, size: usize) -> Fetcher { - let filters = datapath.filters.clone(); - + pub fn new(datapath: DataPath, addr: u64, size: usize) -> Fetcher { let mut data = vec![]; data.resize_with(size, || Atomic::new(0)); let mut flags = vec![]; @@ -54,12 +50,12 @@ impl Fetcher { interior: FetcherInteriorBuilder { data, flags, - future_builder: move |data_ref, flags_ref| Self::make_future(addr, &data_ref[..], &flags_ref[..], filters), + future_builder: move |data_ref, flags_ref| Self::make_future(addr, &data_ref[..], &flags_ref[..], datapath), }.build() } } - pub fn reset(&mut self, datapath: &DataPath, addr: u64, size: usize) { + pub fn reset(&mut self, datapath: DataPath, addr: u64, size: usize) { self.addr = addr; self.progress = 0; self.size = size; @@ -71,12 +67,10 @@ impl Fetcher { interior.flags.clear(); interior.flags.resize_with(size, || Atomic::new(FetchFlags::empty())); - let filters = datapath.filters.clone(); - self.interior = FetcherInteriorBuilder { data: interior.data, flags: interior.flags, - future_builder: move |data_ref, flags_ref| Self::make_future(addr, &data_ref[..], &flags_ref[..], filters), + future_builder: move |data_ref, flags_ref| Self::make_future(addr, &data_ref[..], &flags_ref[..], datapath), }.build(); } @@ -108,10 +102,10 @@ impl Fetcher { (data, flags) } - fn make_future<'data>(addr: u64, data: &'data [Atomic], flags: &'data [Atomic], filters: imbl::Vector) -> Option], &'data [Atomic], imbl::Vector)> + 'data>>> { + fn make_future<'data>(addr: u64, data: &'data [Atomic], flags: &'data [Atomic], datapath: DataPath) -> Option], &'data [Atomic], DataPath)> + 'data>>> { if data.len() > 0 { Some(Box::pin(async move { - (fetch_filters(filters.clone(), FetchRequest::new(addr, &*data, Some(&*flags))).await, data, flags, filters) + (datapath.fetch(FetchRequest::new(addr, &*data, Some(&*flags), false)).await, data, flags, datapath) })) } else { None diff --git a/src/model/listing/cursor/hexdump.rs b/src/model/listing/cursor/hexdump.rs index fdb3fdc..976fb0f 100644 --- a/src/model/listing/cursor/hexdump.rs +++ b/src/model/listing/cursor/hexdump.rs @@ -222,7 +222,7 @@ impl cursor::CursorClassExt for Cursor { Some(fetcher) => fetcher, None => { let (begin_byte, size) = self.token.absolute_extent().round_out(); - datapath::Fetcher::new(&document.datapath, begin_byte, size as usize) + datapath::Fetcher::new(document.datapath.clone(), begin_byte, size as usize) } }; diff --git a/src/model/listing/cursor/hexstring.rs b/src/model/listing/cursor/hexstring.rs index 2f9e39b..f1f9ea4 100644 --- a/src/model/listing/cursor/hexstring.rs +++ b/src/model/listing/cursor/hexstring.rs @@ -220,7 +220,7 @@ impl cursor::CursorClassExt for Cursor { Some(fetcher) => fetcher, None => { let (begin_byte, size) = self.token.absolute_extent().round_out(); - datapath::Fetcher::new(&document.datapath, begin_byte, size as usize) + datapath::Fetcher::new(document.datapath.clone(), begin_byte, size as usize) } }; diff --git a/src/view/action/tree.rs b/src/view/action/tree.rs index b5f4657..4b55dbd 100644 --- a/src/view/action/tree.rs +++ b/src/view/action/tree.rs @@ -1,3 +1,4 @@ pub mod delete_node; pub mod destructure; pub mod nest; +pub mod export_binary_node; diff --git a/src/view/action/tree/export_binary_node.rs b/src/view/action/tree/export_binary_node.rs new file mode 100644 index 0000000..8770f48 --- /dev/null +++ b/src/view/action/tree/export_binary_node.rs @@ -0,0 +1,79 @@ +use gtk::prelude::*; +use gtk::glib; +use gtk::glib::clone; + +use std::cell; +use std::rc; +use std::sync; + +use crate::model::document; +use crate::model::document::structure; +use crate::model::selection; +use crate::view::export; +use crate::view::helpers; +use crate::view::window; + +struct ExportBinaryNodeAction { + window: rc::Weak, + document_host: sync::Arc, + selection_host: sync::Arc, + selection: cell::RefCell<(sync::Arc, Option)>, + subscriber: once_cell::unsync::OnceCell, +} + +pub fn add_action(window_context: &window::WindowContext) { + let selection = window_context.tree_selection_host.get(); + + /* + let dialog = gtk::FileChooserNative::builder() + .accept_label("Export") + .cancel_label("Cancel") + .title("Charm: Export Node as Raw Binary") + .modal(true) + .transient_for(&window.window) + .action(gtk::FileChooserAction::Save) + .select_multiple(false) + .create_folders(true) + .build(); + */ + + + let action_impl = rc::Rc::new(ExportBinaryNodeAction { + window: window_context.window.clone(), + document_host: window_context.project.document_host.clone(), + selection_host: window_context.tree_selection_host.clone(), + selection: cell::RefCell::new((selection.document.clone(), selection.single_selected())), + subscriber: Default::default(), + }); + + let action = helpers::create_simple_action_strong(action_impl.clone(), "export_binary_node", |action| action.activate()); + action.set_enabled(action_impl.enabled()); + + action_impl.subscriber.set(helpers::subscribe_to_updates(rc::Rc::downgrade(&action_impl), action_impl.selection_host.clone(), selection, clone!(#[weak] action, move |action_impl, selection| { + *action_impl.selection.borrow_mut() = (selection.document.clone(), selection.single_selected()); + action.set_enabled(action_impl.enabled()); + }))).unwrap(); + + window_context.action_group.add_action(&action); +} + +impl ExportBinaryNodeAction { + fn enabled(&self) -> bool { + self.selection.borrow().1.is_some() + } + + fn activate(&self) { + let Some(window) = self.window.upgrade() else { return }; + let guard = self.selection.borrow(); + let Some(path) = guard.1.as_ref() else { return }; + + let (node, addr) = guard.0.lookup_node(&path); + + let dialog = glib::Object::builder::() + .property("application", window.application.application.clone()) + .property("transient-for", window.window.clone()) + .build(); + dialog.set(self.document_host.clone(), addr, node.size); + dialog.show(); + } +} diff --git a/src/view/charm.cmb b/src/view/charm.cmb index 55dd8b2..43d9f16 100644 --- a/src/view/charm.cmb +++ b/src/view/charm.cmb @@ -10,7 +10,8 @@ (8,1,None,"crash-document-recovery.ui",None,None,None,None,None,None,None), (9,None,None,"unsaved-dialog.ui",None,None,None,None,None,None,None), (10,None,None,"charm.ui",None,None,None,None,None,None,None), - (11,None,None,"settings.ui",None,None,None,None,None,None,None) + (11,None,None,"settings.ui",None,None,None,None,None,None,None), + (12,1,None,"export-dialog.ui",None,None,None,None,None,None,None) (2,"gtk","4.0",None), @@ -162,7 +163,30 @@ (11,48,"GtkAdjustment","scroll-wheel-impulse",46,None,None,None,-1,None,None), (11,49,"GtkAdjustment","scroll-deceleration",47,None,None,None,-1,None,None), (11,50,"GtkLabel",None,43,None,None,None,None,None,None), - (11,51,"GtkSwitch","scroll-enable-kinetic",43,None,None,None,2,None,None) + (11,51,"GtkSwitch","scroll-enable-kinetic",43,None,None,None,2,None,None), + (12,1,"GtkApplicationWindow","CharmExportDialog",None,None,None,None,-1,None,None), + (12,2,"GtkGrid",None,1,None,None,None,None,None,None), + (12,3,"GtkLabel",None,2,None,None,None,None,None,None), + (12,4,"GtkBox",None,2,None,None,None,1,None,None), + (12,5,"GtkEntry","file_display",4,None,None,None,None,None,None), + (12,6,"GtkButton","file_button",4,None,None,None,1,None,None), + (12,7,"GtkLabel",None,2,None,None,None,2,None,None), + (12,8,"GtkLabel",None,2,None,None,None,3,None,None), + (12,9,"GtkSeparator",None,2,None,None,None,4,None,None), + (12,10,"GtkProgressBar","progress",2,None,None,None,5,None,None), + (12,11,"GtkEntry","address_entry",2,None,None,None,6,None,None), + (12,12,"GtkBox",None,2,None,None,None,7,None,None), + (12,13,"GtkEntry","size_entry",12,None,None,None,None,None,None), + (12,14,"GtkLabel","size_display",12,None,None,None,1,None,None), + (12,15,"GtkGrid",None,2,None,None,None,8,None,None), + (12,16,"GtkCheckButton","ignore_edits",15,None,None,None,None,None,None), + (12,17,"GtkLabel",None,15,None,None,None,1,None,None), + (12,18,"GtkLabel",None,15,None,None,None,2,None,None), + (12,19,"GtkCheckButton","ignore_read_errors",15,None,None,None,3,None,None), + (12,20,"GtkSeparator",None,2,None,None,None,9,None,None), + (12,21,"GtkBox",None,2,None,None,None,10,None,None), + (12,22,"GtkButton","cancel_button",21,None,None,None,None,None,None), + (12,23,"GtkButton","export_button",21,None,None,None,1,None,None) (2,20,"GtkGrid","column-spacing","30",None,None,None,None,None,None,None,None,None), @@ -489,7 +513,46 @@ (11,49,"GtkAdjustment","upper","1000.0",None,None,None,None,None,None,None,None,None), (11,49,"GtkAdjustment","value","400.0",None,None,None,None,None,None,None,None,None), (11,50,"GtkLabel","label","Kinetic Scrolling",None,None,None,None,None,None,None,None,None), - (11,51,"GtkWidget","halign","start",None,None,None,None,None,None,None,None,None) + (11,51,"GtkWidget","halign","start",None,None,None,None,None,None,None,None,None), + (12,1,"GtkWindow","title","Export",None,None,None,None,None,None,None,None,None), + (12,2,"GtkGrid","column-spacing","10",None,None,None,None,None,None,None,None,None), + (12,2,"GtkGrid","row-spacing","10",None,None,None,None,None,None,None,None,None), + (12,2,"GtkWidget","margin-bottom","20",None,None,None,None,None,None,None,None,None), + (12,2,"GtkWidget","margin-end","20",None,None,None,None,None,None,None,None,None), + (12,2,"GtkWidget","margin-start","20",None,None,None,None,None,None,None,None,None), + (12,2,"GtkWidget","margin-top","20",None,None,None,None,None,None,None,None,None), + (12,3,"GtkLabel","label","File",None,None,None,None,None,None,None,None,None), + (12,3,"GtkWidget","halign","start",None,None,None,None,None,None,None,None,None), + (12,4,"GtkBox","spacing","5",None,None,None,None,None,None,None,None,None), + (12,4,"GtkWidget","hexpand","True",None,None,None,None,None,None,None,None,None), + (12,5,"GtkEditable","editable","False",None,None,None,None,None,None,None,None,None), + (12,5,"GtkWidget","hexpand","True",None,None,None,None,None,None,None,None,None), + (12,6,"GtkButton","icon-name","folder-symbolic",None,None,None,None,None,None,None,None,None), + (12,7,"GtkLabel","label","Address",None,None,None,None,None,None,None,None,None), + (12,7,"GtkWidget","halign","start",None,None,None,None,None,None,None,None,None), + (12,8,"GtkLabel","label","Size",None,None,None,None,None,None,None,None,None), + (12,8,"GtkWidget","halign","start",None,None,None,None,None,None,None,None,None), + (12,9,"GtkWidget","hexpand","True",None,None,None,None,None,None,None,None,None), + (12,10,"GtkWidget","hexpand","True",None,None,None,None,None,None,None,None,None), + (12,11,"GtkWidget","hexpand","True",None,None,None,None,None,None,None,None,None), + (12,12,"GtkBox","spacing","10",None,None,None,None,None,None,None,None,None), + (12,12,"GtkWidget","hexpand","True",None,None,None,None,None,None,None,None,None), + (12,13,"GtkWidget","hexpand","True",None,None,None,None,None,None,None,None,None), + (12,14,"GtkLabel","label","X MiB",None,None,None,None,None,None,None,None,None), + (12,15,"GtkWidget","hexpand","True",None,None,None,None,None,None,None,None,None), + (12,16,"GtkWidget","halign","end",None,None,None,None,None,None,None,None,None), + (12,17,"GtkLabel","label","Ignore edits",None,None,None,None,None,None,None,None,None), + (12,17,"GtkWidget","halign","start",None,None,None,None,None,None,None,None,None), + (12,17,"GtkWidget","hexpand","True",None,None,None,None,None,None,None,None,None), + (12,18,"GtkLabel","label","Ignore read errors",None,None,None,None,None,None,None,None,None), + (12,18,"GtkWidget","halign","start",None,None,None,None,None,None,None,None,None), + (12,20,"GtkWidget","hexpand","True",None,None,None,None,None,None,None,None,None), + (12,21,"GtkBox","spacing","10",None,None,None,None,None,None,None,None,None), + (12,21,"GtkWidget","halign","end",None,None,None,None,None,None,None,None,None), + (12,22,"GtkButton","label","Cancel",None,None,None,None,None,None,None,None,None), + (12,23,"GtkButton","label","Export",None,None,None,None,None,None,None,None,None), + (12,23,"GtkWidget","css-classes","suggested-action",None,None,None,None,None,None,None,None,None), + (12,23,"GtkWidget","receives-default","True",None,None,None,None,None,None,None,None,None) (2,20,21,"GtkGridLayoutChild","column","0",None,None,None,None), @@ -755,10 +818,73 @@ (11,43,50,"GtkGridLayoutChild","row-span","1",None,None,None,None), (11,43,51,"GtkGridLayoutChild","column","1",None,None,None,None), (11,43,51,"GtkGridLayoutChild","column-span","1",None,None,None,None), - (11,43,51,"GtkGridLayoutChild","row-span","1",None,None,None,None) + (11,43,51,"GtkGridLayoutChild","row-span","1",None,None,None,None), + (12,2,3,"GtkGridLayoutChild","column","0",None,None,None,None), + (12,2,3,"GtkGridLayoutChild","column-span","1",None,None,None,None), + (12,2,3,"GtkGridLayoutChild","row","6",None,None,None,None), + (12,2,3,"GtkGridLayoutChild","row-span","1",None,None,None,None), + (12,2,4,"GtkGridLayoutChild","column","1",None,None,None,None), + (12,2,4,"GtkGridLayoutChild","column-span","1",None,None,None,None), + (12,2,4,"GtkGridLayoutChild","row","6",None,None,None,None), + (12,2,4,"GtkGridLayoutChild","row-span","1",None,None,None,None), + (12,2,7,"GtkGridLayoutChild","column","0",None,None,None,None), + (12,2,7,"GtkGridLayoutChild","column-span","1",None,None,None,None), + (12,2,7,"GtkGridLayoutChild","row","1",None,None,None,None), + (12,2,7,"GtkGridLayoutChild","row-span","1",None,None,None,None), + (12,2,8,"GtkGridLayoutChild","column","0",None,None,None,None), + (12,2,8,"GtkGridLayoutChild","column-span","1",None,None,None,None), + (12,2,8,"GtkGridLayoutChild","row","2",None,None,None,None), + (12,2,8,"GtkGridLayoutChild","row-span","1",None,None,None,None), + (12,2,9,"GtkGridLayoutChild","column","0",None,None,None,None), + (12,2,9,"GtkGridLayoutChild","column-span","2",None,None,None,None), + (12,2,9,"GtkGridLayoutChild","row","3",None,None,None,None), + (12,2,9,"GtkGridLayoutChild","row-span","1",None,None,None,None), + (12,2,10,"GtkGridLayoutChild","column","0",None,None,None,None), + (12,2,10,"GtkGridLayoutChild","column-span","2",None,None,None,None), + (12,2,10,"GtkGridLayoutChild","row","7",None,None,None,None), + (12,2,10,"GtkGridLayoutChild","row-span","1",None,None,None,None), + (12,2,11,"GtkGridLayoutChild","column","1",None,None,None,None), + (12,2,11,"GtkGridLayoutChild","column-span","1",None,None,None,None), + (12,2,11,"GtkGridLayoutChild","row","1",None,None,None,None), + (12,2,11,"GtkGridLayoutChild","row-span","1",None,None,None,None), + (12,2,12,"GtkGridLayoutChild","column","1",None,None,None,None), + (12,2,12,"GtkGridLayoutChild","column-span","1",None,None,None,None), + (12,2,12,"GtkGridLayoutChild","row","2",None,None,None,None), + (12,2,12,"GtkGridLayoutChild","row-span","1",None,None,None,None), + (12,2,15,"GtkGridLayoutChild","column","0",None,None,None,None), + (12,2,15,"GtkGridLayoutChild","column-span","2",None,None,None,None), + (12,2,15,"GtkGridLayoutChild","row","4",None,None,None,None), + (12,2,15,"GtkGridLayoutChild","row-span","1",None,None,None,None), + (12,2,20,"GtkGridLayoutChild","column","0",None,None,None,None), + (12,2,20,"GtkGridLayoutChild","column-span","2",None,None,None,None), + (12,2,20,"GtkGridLayoutChild","row","5",None,None,None,None), + (12,2,20,"GtkGridLayoutChild","row-span","1",None,None,None,None), + (12,2,21,"GtkGridLayoutChild","column","0",None,None,None,None), + (12,2,21,"GtkGridLayoutChild","column-span","2",None,None,None,None), + (12,2,21,"GtkGridLayoutChild","row","8",None,None,None,None), + (12,2,21,"GtkGridLayoutChild","row-span","1",None,None,None,None), + (12,15,16,"GtkGridLayoutChild","column","1",None,None,None,None), + (12,15,16,"GtkGridLayoutChild","column-span","1",None,None,None,None), + (12,15,16,"GtkGridLayoutChild","row","0",None,None,None,None), + (12,15,16,"GtkGridLayoutChild","row-span","1",None,None,None,None), + (12,15,17,"GtkGridLayoutChild","column","0",None,None,None,None), + (12,15,17,"GtkGridLayoutChild","column-span","1",None,None,None,None), + (12,15,17,"GtkGridLayoutChild","row","0",None,None,None,None), + (12,15,17,"GtkGridLayoutChild","row-span","1",None,None,None,None), + (12,15,18,"GtkGridLayoutChild","column","0",None,None,None,None), + (12,15,18,"GtkGridLayoutChild","column-span","1",None,None,None,None), + (12,15,18,"GtkGridLayoutChild","row","1",None,None,None,None), + (12,15,18,"GtkGridLayoutChild","row-span","1",None,None,None,None), + (12,15,19,"GtkGridLayoutChild","column","1",None,None,None,None), + (12,15,19,"GtkGridLayoutChild","column-span","1",None,None,None,None), + (12,15,19,"GtkGridLayoutChild","row","1",None,None,None,None), + (12,15,19,"GtkGridLayoutChild","row-span","1",None,None,None,None) - (2,8,6,"GtkButton","clicked","handle_save_as",None,None,1,None,None) + (2,8,6,"GtkButton","clicked","handle_save_as",None,None,1,None,None), + (3,12,23,"GtkButton","clicked","export",None,None,1,None,None), + (4,12,22,"GtkButton","clicked","cancel",None,None,1,None,None), + (5,12,6,"GtkButton","clicked","open_file_chooser",None,None,1,None,None) (8,1,"GtkWidget",2,2,None,1,None,None,None,None) diff --git a/src/view/export-dialog.ui b/src/view/export-dialog.ui new file mode 100644 index 0000000..42b8de3 --- /dev/null +++ b/src/view/export-dialog.ui @@ -0,0 +1,227 @@ + + + + + + diff --git a/src/view/export.rs b/src/view/export.rs new file mode 100644 index 0000000..1bd28b1 --- /dev/null +++ b/src/view/export.rs @@ -0,0 +1,376 @@ +use crate::catch_panic; +use crate::model::addr; +use crate::model::datapath; +use crate::model::document; +use crate::view::helpers; + +use atomig::Atomic; +use atomig::Ordering; +use gtk::gio; +use gtk::glib; +use gtk::glib::clone; +use gtk::prelude::*; +use gtk::subclass::prelude::*; + +use std::cell; +use std::sync; + +glib::wrapper! { + pub struct CharmExportDialog(ObjectSubclass) + @extends gtk::Widget, gtk::Window, gtk::ApplicationWindow, + @implements gtk::Buildable; +} + +impl CharmExportDialog { + pub fn new() -> Self { + glib::Object::builder().build() + } +} + +#[derive(Debug)] +enum ExportError { + GlibIoError(glib::Error), + JoinError(tokio::task::JoinError), + DatapathIoError, + NoDocumentError, + NoFileError, + WriteTaskDied, + ReadThreadPanic, +} + +impl From for ExportError { + fn from(e: glib::Error) -> Self { + ExportError::GlibIoError(e) + } +} + +impl From for ExportError { + fn from(e: tokio::task::JoinError) -> Self { + ExportError::JoinError(e) + } +} + +mod imp { + use super::*; + use gtk::CompositeTemplate; + + #[derive(CompositeTemplate, Default)] + #[template(file = "export-dialog.ui")] + pub struct CharmExportDialog { + #[template_child] + address_entry: gtk::TemplateChild, + #[template_child] + size_entry: gtk::TemplateChild, + #[template_child] + size_display: gtk::TemplateChild, + #[template_child] + export_button: gtk::TemplateChild, + #[template_child] + file_display: gtk::TemplateChild, + #[template_child] + progress: gtk::TemplateChild, + #[template_child] + ignore_edits: gtk::TemplateChild, + #[template_child] + ignore_read_errors: gtk::TemplateChild, + + file_chooser: gtk::FileChooserNative, + document: cell::RefCell>>, + begin_addr: cell::Cell, + size: cell::Cell, + entries_valid: cell::Cell, + task: cell::RefCell>, + } + + #[glib::object_subclass] + impl ObjectSubclass for CharmExportDialog { + const NAME: &'static str = "CharmExportDialog"; + type Type = super::CharmExportDialog; + type ParentType = gtk::ApplicationWindow; + + fn class_init(klass: &mut Self::Class) { + /* FFI CALLBACK: assumed panic-safe */ + klass.bind_template(); + klass.bind_template_callbacks(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + /* FFI CALLBACK: assumed panic-safe */ + obj.init_template(); + } + } + + impl ObjectImpl for CharmExportDialog { + fn constructed(&self) { + /* FFI CALLBACK: assumed panic-safe */ + + self.parent_constructed(); + + self.address_entry.connect_text_notify(clone!(#[weak(rename_to = this)] self.obj(), move |_| catch_panic! { + this.imp().entry_updated(); + })); + + self.size_entry.connect_text_notify(clone!(#[weak(rename_to = this)] self.obj(), move |_| catch_panic! { + this.imp().entry_updated(); + })); + + self.file_chooser.connect_response(clone!(#[weak(rename_to = this)] self.obj(), move |fc, r| catch_panic! { + println!("got file chooser response {:?}, {:?}", r, fc.file()); + + match r { + gtk::ResponseType::Cancel => { + this.imp().file_chooser.hide(); + this.imp().file_chooser.destroy(); + this.hide(); + this.destroy(); + }, + + gtk::ResponseType::Accept => match fc.file() { + Some(file) => match file.path().as_ref().and_then(|path| path.to_str()) { + Some(path) => this.imp().file_display.buffer().set_text(path), + None => this.imp().file_display.buffer().set_text(file.uri()), + } + None => { + this.imp().file_display.buffer().set_text(""); + } + }, + + _ => {}, + } + + this.imp().update_enabled(); + })); + + self.file_chooser.set_action(gtk::FileChooserAction::Save); + self.file_chooser.set_transient_for(Some(&*self.obj())); + + self.export_button.set_sensitive(false); + } + } + + impl WidgetImpl for CharmExportDialog { + } + + impl WindowImpl for CharmExportDialog { + } + + impl ApplicationWindowImpl for CharmExportDialog { + } + + #[gtk::template_callbacks] + impl CharmExportDialog { + #[template_callback] + fn cancel(&self, _button: >k::Button) { + /* FFI CALLBACK: we catch panics */ + catch_panic! { + self.file_chooser.hide(); + self.file_chooser.destroy(); + self.obj().hide(); + self.obj().destroy(); + } + } + + #[template_callback] + fn export(&self, _button: >k::Button) { + /* FFI CALLBACK: we catch panics */ + catch_panic! { + let mut task_guard = self.task.borrow_mut(); + + if task_guard.is_none() { + let obj = self.obj().clone(); + *task_guard = Some(helpers::spawn_on_main_context(async move { + if let Err(e) = obj.imp().perform().await { + todo!("handle error: {:?}", e); + } + + obj.imp().task.borrow_mut().take(); + obj.imp().update_enabled(); + })); + } + + std::mem::drop(task_guard); + + self.update_enabled(); + } + } + + #[template_callback] + fn open_file_chooser(&self, _button: >k::Button) { + /* FFI CALLBACK: we catch panics */ + catch_panic! { + self.file_chooser.show(); + } + } + } + + impl CharmExportDialog { + pub fn set_document(&self, document: sync::Arc) { + *self.document.borrow_mut() = Some(document); + } + + pub fn set_addr(&self, addr: u64) { + self.begin_addr.set(addr); + self.address_entry.buffer().set_text(&format!("0x{:x}", addr)); + } + + pub fn set_size(&self, size: u64) { + self.size.set(size); + self.size_entry.buffer().set_text(&format!("0x{:x}", size)); + + self.update_size_display(); + } + + fn update_size_display(&self) { + self.size_display.set_label(&match self.size.get() { + x if x < 1024 => format!("{} bytes", x), + x if x < 1024 * 1024 => format!("{} KiB", x / 1024), + x if x < 1024 * 1024 * 1024 => format!("{:.2} MiB", x as f64 / (1024.0*1024.0)), + x if x < 1024 * 1024 * 1024 * 1024 => format!("{:.2} GiB", x as f64 / (1024.0*1024.0*1024.0)), + x => format!("{:.2} TiB", x as f64 / (1024.0*1024.0*1024.0*1024.0)), + }); + } + + fn entry_updated(&self) { + let mut all_valid = true; + + match match self.address_entry.buffer().text().as_str() { + x if x.strip_prefix("0x").is_some() => u64::from_str_radix(&x[2..], 16), + x => u64::from_str_radix(x, 10) + } { + Ok(addr) => { + self.begin_addr.set(addr); + self.address_entry.remove_css_class("error"); + }, + Err(_e) => { + self.address_entry.add_css_class("error"); + all_valid = false; + }, + } + + match match self.size_entry.buffer().text().as_str() { + x if x.strip_prefix("0x").is_some() => u64::from_str_radix(&x[2..], 16), + x => u64::from_str_radix(x, 10) + } { + Ok(size) => { + self.size.set(size); + self.size_entry.remove_css_class("error"); + self.update_size_display(); + }, + Err(_e) => { + self.size_entry.add_css_class("error"); + all_valid = false; + }, + } + + self.entries_valid.set(all_valid); + + self.update_enabled(); + } + + fn update_enabled(&self) { + let task = self.task.borrow(); + self.ignore_edits.set_sensitive(task.is_none()); + self.ignore_read_errors.set_sensitive(task.is_none()); + self.export_button.set_sensitive(self.entries_valid.get() && self.file_chooser.file().is_some() && task.is_none()); + } + + async fn perform(&self) -> Result<(), ExportError> { + const BLOCK_SIZE: usize = 1 * 1024 * 1024; + + let document = self.document.borrow().as_ref().cloned().ok_or(ExportError::NoDocumentError)?.get(); + + let file = self.file_chooser.file().ok_or(ExportError::NoFileError)?; + let fos = file.replace_future(None, false, gio::FileCreateFlags::REPLACE_DESTINATION, glib::Priority::LOW).await?; + let mut atomic_buffer = Vec::new(); + atomic_buffer.resize_with(BLOCK_SIZE, || Atomic::new(0)); + + let (free_data_buffers_tx, mut free_data_buffers_rx) = tokio::sync::mpsc::channel(16); + let (full_data_buffers_tx, mut full_data_buffers_rx) = tokio::sync::mpsc::channel(16); + + for _ in 0..free_data_buffers_tx.max_capacity() { + free_data_buffers_tx.send(Vec::new()).await.unwrap(); + } + + let begin_addr = self.begin_addr.get(); + let end = begin_addr + self.size.get(); + + let ignore_edits = self.ignore_edits.is_active(); + let ignore_read_errors = self.ignore_read_errors.is_active(); + + /* Datapath fetching has to be done on a tokio runtime. This also means we get to parallelize the reading and the writing though! */ + let read_thread = std::thread::spawn(move || { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build().unwrap(); + let _guard = rt.enter(); + + rt.block_on(async move { + let mut addr = begin_addr; + + while addr < end { + atomic_buffer.resize_with(std::cmp::min(end - addr, BLOCK_SIZE as u64) as usize, || Atomic::new(0)); + + let result = document.datapath.fetch(datapath::FetchRequest::new( + addr, + &atomic_buffer[..], + None, + ignore_edits + )).await; + + if result.flags.intersects(datapath::FetchFlags::IO_ERROR) && !ignore_read_errors { + return Err(ExportError::DatapathIoError); + } + + let mut data_buffer = match free_data_buffers_rx.recv().await { + Some(b) => b, + None => return Err(ExportError::WriteTaskDied), + }; + + data_buffer.clear(); + data_buffer.extend(atomic_buffer[0..result.loaded].iter().map(|b| b.load(Ordering::Relaxed))); + + let progress_fraction = (addr - begin_addr) as f64 / (end - begin_addr) as f64; + if let Err(_) = full_data_buffers_tx.send((progress_fraction, data_buffer)).await { + return Err(ExportError::WriteTaskDied); + } + + addr+= result.loaded as u64; + } + + Ok(()) + }) + }); + + while let Some((progress_fraction, data_buffer)) = full_data_buffers_rx.recv().await { + self.progress.set_fraction(progress_fraction); + + let data_buffer = match fos.write_all_future(data_buffer, glib::Priority::LOW).await { + Ok((buffer, _bytes, None)) => buffer, + Ok((_buffer, _bytes, Some(err))) => return Err(err.into()), + Err((_buffer, err)) => return Err(err.into()), + }; + + if free_data_buffers_tx.send(data_buffer).await.is_err() { + break; + } + } + + /* Since the channel closed, the thread should exit in a timely manner. */ + read_thread.join().map_err(|_| ExportError::ReadThreadPanic)??; + + fos.close_future(glib::Priority::LOW).await?; + + self.progress.set_fraction(1.0); + + Ok(()) + } + } +} + +impl CharmExportDialog { + pub fn set(&self, document_host: sync::Arc, addr: addr::Address, size: addr::Size) { + self.imp().set_document(document_host); + self.imp().set_addr(addr.byte); + self.imp().set_size(size.bytes); + } +} diff --git a/src/view/listing/bucket/hexdump.rs b/src/view/listing/bucket/hexdump.rs index bb04e43..7072d0a 100644 --- a/src/view/listing/bucket/hexdump.rs +++ b/src/view/listing/bucket/hexdump.rs @@ -283,7 +283,7 @@ impl bucket::WorkableBucket for HexdumpBucket { Some(fetcher) => fetcher, None => { let (begin_byte, size) = self.line_extent.rebase(self.node_addr).round_out(); - datapath::Fetcher::new(&document.datapath, begin_byte, size as usize) + datapath::Fetcher::new(document.datapath.clone(), begin_byte, size as usize) } }; diff --git a/src/view/listing/token_view.rs b/src/view/listing/token_view.rs index cd4c0c0..6fdf3ce 100644 --- a/src/view/listing/token_view.rs +++ b/src/view/listing/token_view.rs @@ -249,7 +249,7 @@ impl TokenView { Some(fetcher) => fetcher, None => { let (begin_byte, size) = absolute_extent.round_out(); - datapath::Fetcher::new(&document.datapath, begin_byte, size as usize) + datapath::Fetcher::new(document.datapath.clone(), begin_byte, size as usize) } }; diff --git a/src/view/mod.rs b/src/view/mod.rs index cd9c0d3..98b5c33 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -6,6 +6,7 @@ pub mod action; pub mod config; pub mod error; pub mod ext; +pub mod export; pub mod window; pub mod helpers; pub mod gsc; diff --git a/src/view/window.rs b/src/view/window.rs index 1d3c442..9cb8e7f 100644 --- a/src/view/window.rs +++ b/src/view/window.rs @@ -211,6 +211,7 @@ impl CharmWindow { menu.append(Some("Nest"), Some("ctx.nest")); menu.append(Some("Destructure"), Some("ctx.destructure")); menu.append(Some("Delete"), Some("ctx.delete_node")); + menu.append(Some("Export..."), Some("ctx.export_binary_node")); let popover = gtk::PopoverMenu::from_model(Some(&menu)); popover.set_parent(&hierarchy_editor); @@ -541,6 +542,7 @@ impl WindowContext { action::tree::delete_node::add_action(&wc); action::tree::nest::add_action(&wc); action::tree::destructure::add_action(&wc); + action::tree::export_binary_node::add_action(&wc); action::debug::revert_document::add_action(&wc); wc