Easier key handling

pull/3/head
Arijit Basu 3 years ago
parent 8221140756
commit 7beaec1763
No known key found for this signature in database
GPG Key ID: 7D7BF809E7378863

2
Cargo.lock generated

@ -1133,7 +1133,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "xplr"
version = "0.1.8"
version = "0.1.9"
dependencies = [
"criterion",
"crossterm",

@ -1,6 +1,6 @@
[package]
name = "xplr"
version = "0.1.8" # Update app.rs
version = "0.1.9" # Update app.rs
authors = ["Arijit Basu <sayanarijit@gmail.com>"]
edition = "2018"
description = "An experimental, minimal, configurable TUI file explorer, stealing ideas from nnn and fzf."

@ -3,43 +3,30 @@ use std::fs;
use xplr::*;
fn criterion_benchmark(c: &mut Criterion) {
let app = app::create()
.expect("failed to create app")
.change_directory(&"/tmp/xplr_bench".to_string())
.unwrap();
fs::create_dir_all("/tmp/xplr_bench").unwrap();
(1..10000).for_each(|i| {
fs::File::create(format!("/tmp/xplr_bench/{}", i)).unwrap();
});
let app = app::create()
.expect("failed to create app")
.change_directory(&"/tmp/xplr_bench".to_string())
.unwrap();
c.bench_function("focus next item", |b| {
b.iter(|| {
app.clone()
.handle(&config::Action::Global(config::GlobalAction::FocusNext))
})
b.iter(|| app.clone().handle(&config::Action::FocusNext))
});
c.bench_function("focus previous item", |b| {
b.iter(|| {
app.clone().handle(&config::Action::Global(
config::GlobalAction::FocusPrevious,
))
})
b.iter(|| app.clone().handle(&config::Action::FocusPrevious))
});
c.bench_function("focus first item", |b| {
b.iter(|| {
app.clone()
.handle(&config::Action::Global(config::GlobalAction::FocusFirst))
})
b.iter(|| app.clone().handle(&config::Action::FocusFirst))
});
c.bench_function("focus last item", |b| {
b.iter(|| {
app.clone()
.handle(&config::Action::Global(config::GlobalAction::FocusLast))
})
b.iter(|| app.clone().handle(&config::Action::FocusLast))
});
}
criterion_group!(benches, criterion_benchmark);

@ -1,6 +1,4 @@
use crate::config::{
Action, CommandConfig, Config, ExploreModeAction, GlobalAction, Mode, SelectModeAction,
};
use crate::config::{Action, CommandConfig, Config, Mode};
use crate::error::Error;
use crate::input::Key;
use dirs;
@ -142,8 +140,8 @@ impl DirectoryBuffer {
&config.general.normal_ui
};
let is_first_item = net_idx == 0;
let is_last_item = net_idx == total.max(1) - 1;
let is_first = net_idx == 0;
let is_last = net_idx == total.max(1) - 1;
let tree = config
.general
@ -151,9 +149,9 @@ impl DirectoryBuffer {
.tree
.clone()
.map(|t| {
if is_last_item {
if is_last {
t.2.format.clone()
} else if is_first_item {
} else if is_first {
t.0.format.clone()
} else {
t.1.format.clone()
@ -191,8 +189,8 @@ impl DirectoryBuffer {
suffix: ui.suffix.clone(),
tree: tree.into(),
is_symlink,
is_first_item,
is_last_item,
is_first,
is_last,
is_dir,
is_file,
is_readonly,
@ -201,7 +199,7 @@ impl DirectoryBuffer {
index: net_idx + 1,
focus_relative_index,
buffer_relative_index: rel_idx + 1,
total_items: total,
total: total,
};
(abs.to_owned(), m)
})
@ -223,7 +221,7 @@ impl DirectoryBuffer {
})
}
pub fn focused_item(&self) -> Option<(PathBuf, DirectoryItemMetadata)> {
pub fn focused(&self) -> Option<(PathBuf, DirectoryItemMetadata)> {
self.focus.and_then(|f| {
self.items
.get(Self::relative_focus(f))
@ -242,8 +240,8 @@ pub struct DirectoryItemMetadata {
pub prefix: String,
pub suffix: String,
pub tree: String,
pub is_first_item: bool,
pub is_last_item: bool,
pub is_first: bool,
pub is_last: bool,
pub is_symlink: bool,
pub is_dir: bool,
pub is_file: bool,
@ -253,7 +251,7 @@ pub struct DirectoryItemMetadata {
pub index: usize,
pub focus_relative_index: String,
pub buffer_relative_index: usize,
pub total_items: usize,
pub total: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -264,6 +262,7 @@ pub struct App {
pub saved_buffers: HashMap<PathBuf, Option<usize>>,
pub selected_paths: HashSet<PathBuf>,
pub mode: Mode,
pub parsed_key_bindings: HashMap<Key, (String, Vec<Action>)>,
pub show_hidden: bool,
pub call: Option<CommandConfig>,
pub result: Option<String>,
@ -288,6 +287,8 @@ impl App {
directory_buffer.focus.clone(),
);
let parsed_key_bindings = config.key_bindings.clone().filtered(&mode);
Ok(Self {
version: VERSION.into(),
config: config.to_owned(),
@ -295,6 +296,7 @@ impl App {
saved_buffers,
selected_paths: selected_paths.to_owned(),
mode,
parsed_key_bindings,
show_hidden,
result: None,
call: None,
@ -342,7 +344,7 @@ impl App {
)
}
pub fn focus_first_item(self) -> Result<Self, Error> {
pub fn focus_first(self) -> Result<Self, Error> {
let focus = if self.directory_buffer.total == 0 {
None
} else {
@ -360,7 +362,7 @@ impl App {
)
}
pub fn focus_last_item(self) -> Result<Self, Error> {
pub fn focus_last(self) -> Result<Self, Error> {
let focus = if self.directory_buffer.total == 0 {
None
} else {
@ -387,7 +389,7 @@ impl App {
Ok(self)
}
pub fn focus_next_item(self) -> Result<Self, Error> {
pub fn focus_next(self) -> Result<Self, Error> {
let len = self.directory_buffer.total;
let focus = self
.directory_buffer
@ -406,7 +408,7 @@ impl App {
)
}
pub fn focus_previous_item(self) -> Result<Self, Error> {
pub fn focus_previous(self) -> Result<Self, Error> {
let len = self.directory_buffer.total;
let focus = if len == 0 {
None
@ -499,7 +501,7 @@ impl App {
pub fn enter(self) -> Result<Self, Error> {
let pwd = self
.directory_buffer
.focused_item()
.focused()
.map(|(p, _)| p)
.map(|p| {
if p.is_dir() {
@ -531,7 +533,7 @@ impl App {
pub fn select(self) -> Result<Self, Error> {
let selected_paths = self
.directory_buffer
.focused_item()
.focused()
.map(|(p, _)| {
let mut selected_paths = self.selected_paths.clone();
selected_paths.insert(p);
@ -548,7 +550,7 @@ impl App {
pub fn toggle_selection(self) -> Result<Self, Error> {
let selected_paths = self
.directory_buffer
.focused_item()
.focused()
.map(|(p, _)| {
let mut selected_paths = self.selected_paths.clone();
if selected_paths.contains(&p) {
@ -582,7 +584,7 @@ impl App {
let mut app = self;
app.result = app
.directory_buffer
.focused_item()
.focused()
.and_then(|(p, _)| p.to_str().map(|s| s.to_string()));
Ok(app)
}
@ -623,148 +625,34 @@ impl App {
Err(Error::Terminated)
}
pub fn actions_from_key(&self, key: Key) -> Option<Vec<Action>> {
self.config
.key_bindings
.global
.get(&key)
.map(|m| {
m.actions
.iter()
.map(|a| Action::Global(a.clone()))
.collect()
})
.or_else(|| match &self.mode {
Mode::Explore => self.config.key_bindings.explore_mode.get(&key).map(|m| {
m.actions
.iter()
.map(|a| Action::ExploreMode(a.clone()))
.collect()
}),
Mode::Select => self.config.key_bindings.select_mode.get(&key).map(|m| {
m.actions
.iter()
.map(|a| Action::SelectMode(a.clone()))
.collect()
}),
Mode::ExploreSubmode(sub) => self
.config
.key_bindings
.explore_submodes
.get(sub)
.and_then(|kb| {
kb.get(&key).map(|m| {
m.actions
.iter()
.map(|a| Action::ExploreMode(a.clone()))
.collect()
})
}),
Mode::SelectSubmode(sub) => self
.config
.key_bindings
.select_submodes
.get(sub)
.and_then(|kb| {
kb.get(&key).map(|m| {
m.actions
.iter()
.map(|a| Action::SelectMode(a.clone()))
.collect()
})
}),
})
pub fn actions_from_key(&self, key: &Key) -> Option<Vec<Action>> {
self.parsed_key_bindings.get(key).map(|(_, a)| a.to_owned())
}
pub fn handle(self, action: &Action) -> Result<Self, Error> {
match action {
// Global actions
Action::Global(GlobalAction::ToggleShowHidden) => self.toggle_hidden(),
Action::Global(GlobalAction::Back) => self.back(),
Action::Global(GlobalAction::Enter) => self.enter(),
Action::Global(GlobalAction::FocusNext) => self.focus_next_item(),
Action::Global(GlobalAction::FocusPrevious) => self.focus_previous_item(),
Action::Global(GlobalAction::FocusFirst) => self.focus_first_item(),
Action::Global(GlobalAction::FocusLast) => self.focus_last_item(),
Action::Global(GlobalAction::FocusPath(path)) => self.focus_path(&path.into()),
Action::Global(GlobalAction::FocusPathByIndex(n)) => self.focus_by_index(n),
Action::Global(GlobalAction::FocusPathByBufferRelativeIndex(n)) => {
self.focus_by_buffer_relative_index(&n)
}
Action::Global(GlobalAction::FocusPathByFocusRelativeIndex(n)) => {
self.focus_by_focus_relative_index(&n)
}
Action::Global(GlobalAction::ChangeDirectory(dir)) => self.change_directory(&dir),
Action::Global(GlobalAction::Call(cmd)) => self.call(&cmd),
Action::Global(GlobalAction::PrintFocused) => self.print_focused(),
Action::Global(GlobalAction::PrintPwd) => self.print_pwd(),
Action::Global(GlobalAction::PrintAppState) => self.print_app_state(),
Action::Global(GlobalAction::Quit) => self.quit(),
Action::Global(GlobalAction::Terminate) => self.terminate(),
// Explore mode
Action::ExploreMode(ExploreModeAction::ToggleShowHidden) => self.toggle_hidden(),
Action::ExploreMode(ExploreModeAction::Back) => self.back(),
Action::ExploreMode(ExploreModeAction::Enter) => self.enter(),
Action::ExploreMode(ExploreModeAction::FocusNext) => self.focus_next_item(),
Action::ExploreMode(ExploreModeAction::FocusPrevious) => self.focus_previous_item(),
Action::ExploreMode(ExploreModeAction::FocusFirst) => self.focus_first_item(),
Action::ExploreMode(ExploreModeAction::FocusLast) => self.focus_last_item(),
Action::ExploreMode(ExploreModeAction::FocusPath(path)) => {
self.focus_path(&path.into())
}
Action::ExploreMode(ExploreModeAction::FocusPathByIndex(n)) => self.focus_by_index(n),
Action::ExploreMode(ExploreModeAction::FocusPathByBufferRelativeIndex(n)) => {
self.focus_by_buffer_relative_index(&n)
}
Action::ExploreMode(ExploreModeAction::FocusPathByFocusRelativeIndex(n)) => {
self.focus_by_focus_relative_index(&n)
}
Action::ExploreMode(ExploreModeAction::ChangeDirectory(dir)) => {
self.change_directory(&dir)
}
Action::ExploreMode(ExploreModeAction::Call(cmd)) => self.call(&cmd),
Action::ExploreMode(ExploreModeAction::Select) => self.select(),
Action::ExploreMode(ExploreModeAction::EnterSubmode(submode)) => {
self.enter_submode(submode)
}
Action::ExploreMode(ExploreModeAction::ExitSubmode) => self.exit_submode(),
Action::ExploreMode(ExploreModeAction::PrintFocused) => self.print_focused(),
Action::ExploreMode(ExploreModeAction::PrintPwd) => self.print_pwd(),
Action::ExploreMode(ExploreModeAction::PrintAppState) => self.print_app_state(),
Action::ExploreMode(ExploreModeAction::Quit) => self.quit(),
// Select mode
Action::SelectMode(SelectModeAction::ToggleShowHidden) => self.toggle_hidden(),
Action::SelectMode(SelectModeAction::Back) => self.back(),
Action::SelectMode(SelectModeAction::Enter) => self.enter(),
Action::SelectMode(SelectModeAction::FocusNext) => self.focus_next_item(),
Action::SelectMode(SelectModeAction::FocusPrevious) => self.focus_previous_item(),
Action::SelectMode(SelectModeAction::FocusFirst) => self.focus_first_item(),
Action::SelectMode(SelectModeAction::FocusLast) => self.focus_last_item(),
Action::SelectMode(SelectModeAction::FocusPath(path)) => self.focus_path(&path.into()),
Action::SelectMode(SelectModeAction::FocusPathByIndex(n)) => self.focus_by_index(n),
Action::SelectMode(SelectModeAction::FocusPathByBufferRelativeIndex(n)) => {
self.focus_by_buffer_relative_index(&n)
}
Action::SelectMode(SelectModeAction::FocusPathByFocusRelativeIndex(n)) => {
self.focus_by_focus_relative_index(&n)
}
Action::SelectMode(SelectModeAction::ChangeDirectory(dir)) => {
self.change_directory(&dir)
}
Action::SelectMode(SelectModeAction::Call(cmd)) => self.call(&cmd),
Action::SelectMode(SelectModeAction::ToggleSelection) => self.toggle_selection(),
Action::SelectMode(SelectModeAction::EnterSubmode(submode)) => {
self.enter_submode(submode)
}
Action::SelectMode(SelectModeAction::ExitSubmode) => self.exit_submode(),
Action::SelectMode(SelectModeAction::PrintSelected) => self.print_selected(),
Action::SelectMode(SelectModeAction::PrintAppState) => self.print_app_state(),
Action::SelectMode(SelectModeAction::Quit) => self.quit(),
Action::ToggleShowHidden => self.toggle_hidden(),
Action::Back => self.back(),
Action::Enter => self.enter(),
Action::FocusPrevious => self.focus_previous(),
Action::FocusNext => self.focus_next(),
Action::FocusFirst => self.focus_first(),
Action::FocusLast => self.focus_last(),
Action::FocusPathByIndex(i) => self.focus_by_index(i),
Action::FocusPathByBufferRelativeIndex(i) => self.focus_by_buffer_relative_index(i),
Action::FocusPathByFocusRelativeIndex(i) => self.focus_by_focus_relative_index(i),
Action::FocusPath(p) => self.focus_path(&p.into()),
Action::ChangeDirectory(d) => self.change_directory(d.into()),
Action::Call(c) => self.call(c),
Action::EnterSubmode(s) => self.enter_submode(s),
Action::ExitSubmode => self.exit_submode(),
Action::Select => self.select(),
Action::ToggleSelection => self.toggle_selection(),
Action::PrintFocused => self.print_focused(),
Action::PrintSelected => self.print_selected(),
Action::PrintAppState => self.print_app_state(),
Action::Quit => self.quit(),
Action::Terminate => self.terminate(),
}
}
}

@ -15,6 +15,69 @@ pub enum Mode {
SelectSubmode(String),
}
impl Mode {
pub fn does_support(self, action: &Action) -> bool {
match (self, action) {
// Special
(_, Action::Terminate) => true,
// Explore mode
(Self::Explore, Action::Back) => true,
(Self::Explore, Action::Call(_)) => true,
(Self::Explore, Action::ChangeDirectory(_)) => true,
(Self::Explore, Action::Enter) => true,
(Self::Explore, Action::EnterSubmode(_)) => true,
(Self::Explore, Action::ExitSubmode) => false,
(Self::Explore, Action::FocusFirst) => true,
(Self::Explore, Action::FocusLast) => true,
(Self::Explore, Action::FocusNext) => true,
(Self::Explore, Action::FocusPath(_)) => true,
(Self::Explore, Action::FocusPathByBufferRelativeIndex(_)) => true,
(Self::Explore, Action::FocusPathByFocusRelativeIndex(_)) => true,
(Self::Explore, Action::FocusPathByIndex(_)) => true,
(Self::Explore, Action::FocusPrevious) => true,
(Self::Explore, Action::PrintAppState) => true,
(Self::Explore, Action::PrintFocused) => true,
(Self::Explore, Action::PrintSelected) => false,
(Self::Explore, Action::Quit) => true,
(Self::Explore, Action::Select) => true,
(Self::Explore, Action::ToggleSelection) => false,
(Self::Explore, Action::ToggleShowHidden) => true,
// Explore submode
(Self::ExploreSubmode(_), Action::ExitSubmode) => true,
(Self::ExploreSubmode(_), a) => Self::does_support(Self::Explore, a),
// Select mode
(Self::Select, Action::Back) => true,
(Self::Select, Action::Call(_)) => true,
(Self::Select, Action::ChangeDirectory(_)) => true,
(Self::Select, Action::Enter) => true,
(Self::Select, Action::EnterSubmode(_)) => true,
(Self::Select, Action::ExitSubmode) => true,
(Self::Select, Action::FocusFirst) => true,
(Self::Select, Action::FocusLast) => true,
(Self::Select, Action::FocusNext) => true,
(Self::Select, Action::FocusPath(_)) => true,
(Self::Select, Action::FocusPathByBufferRelativeIndex(_)) => true,
(Self::Select, Action::FocusPathByFocusRelativeIndex(_)) => true,
(Self::Select, Action::FocusPathByIndex(_)) => true,
(Self::Select, Action::FocusPrevious) => true,
(Self::Select, Action::PrintAppState) => true,
(Self::Select, Action::PrintFocused) => false,
(Self::Select, Action::PrintSelected) => true,
(Self::Select, Action::Quit) => true,
(Self::Select, Action::Select) => false,
(Self::Select, Action::ToggleSelection) => true,
(Self::Select, Action::ToggleShowHidden) => true,
// Select submode
(Self::SelectSubmode(_), Action::ExitSubmode) => true,
(Self::SelectSubmode(_), a) => Self::does_support(Self::Select, a),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Format {
Line,
@ -39,33 +102,7 @@ pub struct CommandConfig {
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum GlobalAction {
// Common actions
ToggleShowHidden,
Back,
Enter,
FocusPrevious,
FocusNext,
FocusFirst,
FocusLast,
FocusPath(String),
FocusPathByIndex(usize),
FocusPathByBufferRelativeIndex(usize),
FocusPathByFocusRelativeIndex(isize),
ChangeDirectory(String),
Call(CommandConfig),
// Quit options
PrintFocused,
PrintPwd,
PrintAppState,
Quit,
Terminate,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ExploreModeAction {
// Common actions
pub enum Action {
ToggleShowHidden,
Back,
Enter,
@ -79,99 +116,80 @@ pub enum ExploreModeAction {
FocusPath(String),
ChangeDirectory(String),
Call(CommandConfig),
// Explore mode exclusive options
EnterSubmode(String),
ExitSubmode,
Select,
// Unselect,
// SelectAll,
// SelectAllRecursive,
// Quit options
PrintFocused,
PrintPwd,
PrintAppState,
Quit,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum SelectModeAction {
// Common actions
ToggleShowHidden,
Back,
Enter,
FocusPrevious,
FocusNext,
FocusFirst,
FocusLast,
FocusPathByIndex(usize),
FocusPathByBufferRelativeIndex(usize),
FocusPathByFocusRelativeIndex(isize),
FocusPath(String),
ChangeDirectory(String),
Call(CommandConfig),
// Select mode exclusive options
EnterSubmode(String),
ExitSubmode,
// Select,
// Unselect,
// SelectAll,
// SelectAllRecursive,
// UnselectAll,
// UnSelectAllRecursive,
ToggleSelection,
// ClearSelectedPaths,
// Quit options
PrintFocused,
PrintSelected,
PrintAppState,
Quit,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Action {
Global(GlobalAction),
ExploreMode(ExploreModeAction),
SelectMode(SelectModeAction),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GlobalActionMenu {
#[serde(default)]
pub help: String,
pub actions: Vec<GlobalAction>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ExploreModeActionMenu {
#[serde(default)]
pub help: String,
pub actions: Vec<ExploreModeAction>,
Terminate,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SelectModeActionMenu {
pub struct ActionMenu {
#[serde(default)]
pub help: String,
pub actions: Vec<SelectModeAction>,
pub actions: Vec<Action>,
}
pub type ExploreSubmodeActionMenu = HashMap<Key, ExploreModeActionMenu>;
pub type SelectSubmodeActionMenu = HashMap<Key, SelectModeActionMenu>;
pub type SubmodeActionMenu = HashMap<Key, ActionMenu>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeyBindings {
pub global: HashMap<Key, GlobalActionMenu>,
pub global: HashMap<Key, ActionMenu>,
#[serde(default)]
pub explore_mode: HashMap<Key, ExploreModeActionMenu>,
pub explore_mode: HashMap<Key, ActionMenu>,
#[serde(default)]
pub explore_submodes: HashMap<String, ExploreSubmodeActionMenu>,
pub explore_submodes: HashMap<String, SubmodeActionMenu>,
#[serde(default)]
pub select_mode: HashMap<Key, SelectModeActionMenu>,
pub select_mode: HashMap<Key, ActionMenu>,
#[serde(default)]
pub select_submodes: HashMap<String, SelectSubmodeActionMenu>,
pub select_submodes: HashMap<String, SubmodeActionMenu>,
}
impl KeyBindings {
pub fn filtered(&self, mode: &Mode) -> HashMap<Key, (String, Vec<Action>)> {
let mode_bindings: Option<HashMap<Key, ActionMenu>> = match mode {
Mode::Explore => Some(self.explore_mode.clone()),
Mode::ExploreSubmode(s) => self.explore_submodes.clone().get(s).map(|a| a.to_owned()),
Mode::Select => Some(self.select_mode.clone()),
Mode::SelectSubmode(s) => self.select_submodes.clone().get(s).map(|a| a.to_owned()),
};
let kb = self.global.clone().into_iter();
let kb: HashMap<Key, ActionMenu> = if let Some(modal_kb) = mode_bindings {
kb.chain(modal_kb.into_iter()).collect()
} else {
kb.collect()
};
kb.into_iter()
.map(|(k, am)| {
(
k.clone(),
(
am.help,
am.actions
.into_iter()
.filter(|a| mode.clone().does_support(a))
.collect::<Vec<Action>>(),
),
)
})
.filter(|(_, (_, actions))| !actions.is_empty())
.collect()
}
}
impl Default for KeyBindings {

@ -52,7 +52,7 @@ fn main() -> Result<(), Error> {
let mut result = Ok(());
'outer: for key in keys {
if let Some(actions) = app.actions_from_key(key) {
if let Some(actions) = app.actions_from_key(&key) {
for action in actions.iter() {
app = match app.handle(action) {
Ok(mut a) => {
@ -71,7 +71,7 @@ fn main() -> Result<(), Error> {
if let Some(cmd) = a.call.clone() {
term::disable_raw_mode().unwrap();
std::mem::drop(terminal);
if let Some((_, meta)) = a.directory_buffer.focused_item() {
if let Some((_, meta)) = a.directory_buffer.focused() {
let _ = std::process::Command::new(cmd.command.clone())
.current_dir(&a.directory_buffer.pwd)
.args(

@ -6,7 +6,7 @@ fn test_key_down() {
assert_eq!(app.directory_buffer.focus, Some(0));
let actions = app.actions_from_key(input::Key::Down).unwrap();
let actions = app.actions_from_key(&input::Key::Down).unwrap();
for action in actions {
app = app.handle(&action).unwrap()

Loading…
Cancel
Save