fix merge conflicts
This commit is contained in:
commit
1ffb3b9870
9 changed files with 780 additions and 324 deletions
800
Cargo.lock
generated
800
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -33,6 +33,7 @@ rayon = "1"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
serde = { version = "1", features = ["serde_derive"] }
|
serde = { version = "1", features = ["serde_derive"] }
|
||||||
shlex = { version = "1.3" }
|
shlex = { version = "1.3" }
|
||||||
|
tar = "0.4.41"
|
||||||
tokio = { version = "1", features = ["sync"] }
|
tokio = { version = "1", features = ["sync"] }
|
||||||
trash = { git = "https://github.com/jackpot51/trash-rs.git", branch = "delete-info" }
|
trash = { git = "https://github.com/jackpot51/trash-rs.git", branch = "delete-info" }
|
||||||
xdg = { version = "2.5.2", optional = true }
|
xdg = { version = "2.5.2", optional = true }
|
||||||
|
|
@ -47,6 +48,7 @@ i18n-embed-fl = "0.7"
|
||||||
rust-embed = "8"
|
rust-embed = "8"
|
||||||
slotmap = "1.0.7"
|
slotmap = "1.0.7"
|
||||||
recently-used-xbel = "1.0.0"
|
recently-used-xbel = "1.0.0"
|
||||||
|
zip = "2.1.6"
|
||||||
|
|
||||||
[dependencies.libcosmic]
|
[dependencies.libcosmic]
|
||||||
git = "https://github.com/pop-os/libcosmic.git"
|
git = "https://github.com/pop-os/libcosmic.git"
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ open-file = Open file
|
||||||
open-folder = Open folder
|
open-folder = Open folder
|
||||||
open-in-new-tab = Open in new tab
|
open-in-new-tab = Open in new tab
|
||||||
open-in-new-window = Open in new window
|
open-in-new-window = Open in new window
|
||||||
|
open-item-location = Open item location
|
||||||
open-multiple-files = Open multiple files
|
open-multiple-files = Open multiple files
|
||||||
open-multiple-folders = Open multiple folders
|
open-multiple-folders = Open multiple folders
|
||||||
save = Save
|
save = Save
|
||||||
|
|
@ -83,6 +84,14 @@ copied = Copied {$items} {$items ->
|
||||||
} from {$from} to {$to}
|
} from {$from} to {$to}
|
||||||
emptying-trash = Emptying {trash}
|
emptying-trash = Emptying {trash}
|
||||||
emptied-trash = Emptied {trash}
|
emptied-trash = Emptied {trash}
|
||||||
|
extracting = Extracting {$items} {$items ->
|
||||||
|
[one] item
|
||||||
|
*[other] items
|
||||||
|
} from {$from} to {$to}
|
||||||
|
extracted = Extracted {$items} {$items ->
|
||||||
|
[one] item
|
||||||
|
*[other] items
|
||||||
|
} from {$from} to {$to}
|
||||||
moving = Moving {$items} {$items ->
|
moving = Moving {$items} {$items ->
|
||||||
[one] item
|
[one] item
|
||||||
*[other] items
|
*[other] items
|
||||||
|
|
@ -131,6 +140,7 @@ dark = Dark
|
||||||
light = Light
|
light = Light
|
||||||
|
|
||||||
# Context menu
|
# Context menu
|
||||||
|
extract-here = Extract
|
||||||
add-to-sidebar = Add to sidebar
|
add-to-sidebar = Add to sidebar
|
||||||
new-file = New file...
|
new-file = New file...
|
||||||
new-folder = New folder...
|
new-folder = New folder...
|
||||||
|
|
|
||||||
114
src/app.rs
114
src/app.rs
|
|
@ -71,6 +71,7 @@ pub enum Action {
|
||||||
Cut,
|
Cut,
|
||||||
EditHistory,
|
EditHistory,
|
||||||
EditLocation,
|
EditLocation,
|
||||||
|
ExtractHere,
|
||||||
HistoryNext,
|
HistoryNext,
|
||||||
HistoryPrevious,
|
HistoryPrevious,
|
||||||
ItemDown,
|
ItemDown,
|
||||||
|
|
@ -84,6 +85,7 @@ pub enum Action {
|
||||||
Open,
|
Open,
|
||||||
OpenInNewTab,
|
OpenInNewTab,
|
||||||
OpenInNewWindow,
|
OpenInNewWindow,
|
||||||
|
OpenItemLocation,
|
||||||
OpenTerminal,
|
OpenTerminal,
|
||||||
OpenWith,
|
OpenWith,
|
||||||
Paste,
|
Paste,
|
||||||
|
|
@ -119,6 +121,7 @@ impl Action {
|
||||||
Action::Cut => Message::Cut(entity_opt),
|
Action::Cut => Message::Cut(entity_opt),
|
||||||
Action::EditHistory => Message::ToggleContextPage(ContextPage::EditHistory),
|
Action::EditHistory => Message::ToggleContextPage(ContextPage::EditHistory),
|
||||||
Action::EditLocation => Message::EditLocation(entity_opt),
|
Action::EditLocation => Message::EditLocation(entity_opt),
|
||||||
|
Action::ExtractHere => Message::ExtractHere(entity_opt),
|
||||||
Action::HistoryNext => Message::TabMessage(entity_opt, tab::Message::GoNext),
|
Action::HistoryNext => Message::TabMessage(entity_opt, tab::Message::GoNext),
|
||||||
Action::HistoryPrevious => Message::TabMessage(entity_opt, tab::Message::GoPrevious),
|
Action::HistoryPrevious => Message::TabMessage(entity_opt, tab::Message::GoPrevious),
|
||||||
Action::ItemDown => Message::TabMessage(entity_opt, tab::Message::ItemDown),
|
Action::ItemDown => Message::TabMessage(entity_opt, tab::Message::ItemDown),
|
||||||
|
|
@ -132,6 +135,7 @@ impl Action {
|
||||||
Action::Open => Message::TabMessage(entity_opt, tab::Message::Open),
|
Action::Open => Message::TabMessage(entity_opt, tab::Message::Open),
|
||||||
Action::OpenInNewTab => Message::OpenInNewTab(entity_opt),
|
Action::OpenInNewTab => Message::OpenInNewTab(entity_opt),
|
||||||
Action::OpenInNewWindow => Message::OpenInNewWindow(entity_opt),
|
Action::OpenInNewWindow => Message::OpenInNewWindow(entity_opt),
|
||||||
|
Action::OpenItemLocation => Message::OpenItemLocation(entity_opt),
|
||||||
Action::OpenTerminal => Message::OpenTerminal(entity_opt),
|
Action::OpenTerminal => Message::OpenTerminal(entity_opt),
|
||||||
Action::OpenWith => Message::ToggleContextPage(ContextPage::OpenWith),
|
Action::OpenWith => Message::ToggleContextPage(ContextPage::OpenWith),
|
||||||
Action::Paste => Message::Paste(entity_opt),
|
Action::Paste => Message::Paste(entity_opt),
|
||||||
|
|
@ -216,6 +220,7 @@ pub enum Message {
|
||||||
DialogPush(DialogPage),
|
DialogPush(DialogPage),
|
||||||
DialogUpdate(DialogPage),
|
DialogUpdate(DialogPage),
|
||||||
EditLocation(Option<Entity>),
|
EditLocation(Option<Entity>),
|
||||||
|
ExtractHere(Option<Entity>),
|
||||||
Key(Modifiers, Key),
|
Key(Modifiers, Key),
|
||||||
LaunchUrl(String),
|
LaunchUrl(String),
|
||||||
MaybeExit,
|
MaybeExit,
|
||||||
|
|
@ -233,6 +238,7 @@ pub enum Message {
|
||||||
OpenWith(PathBuf, mime_app::MimeApp),
|
OpenWith(PathBuf, mime_app::MimeApp),
|
||||||
OpenInNewTab(Option<Entity>),
|
OpenInNewTab(Option<Entity>),
|
||||||
OpenInNewWindow(Option<Entity>),
|
OpenInNewWindow(Option<Entity>),
|
||||||
|
OpenItemLocation(Option<Entity>),
|
||||||
Paste(Option<Entity>),
|
Paste(Option<Entity>),
|
||||||
PasteContents(PathBuf, ClipboardPaste),
|
PasteContents(PathBuf, ClipboardPaste),
|
||||||
PendingComplete(u64),
|
PendingComplete(u64),
|
||||||
|
|
@ -254,7 +260,7 @@ pub enum Message {
|
||||||
TabConfig(TabConfig),
|
TabConfig(TabConfig),
|
||||||
TabMessage(Option<Entity>, tab::Message),
|
TabMessage(Option<Entity>, tab::Message),
|
||||||
TabNew,
|
TabNew,
|
||||||
TabRescan(Entity, Location, Vec<tab::Item>),
|
TabRescan(Entity, Location, Vec<tab::Item>, Option<PathBuf>),
|
||||||
ToggleContextPage(ContextPage),
|
ToggleContextPage(ContextPage),
|
||||||
Undo(usize),
|
Undo(usize),
|
||||||
UndoTrash(usize, Arc<[PathBuf]>),
|
UndoTrash(usize, Arc<[PathBuf]>),
|
||||||
|
|
@ -380,23 +386,30 @@ pub struct App {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
fn open_tab(&mut self, location: Location) -> Command<Message> {
|
fn open_tab(
|
||||||
|
&mut self,
|
||||||
|
location: Location,
|
||||||
|
activate: bool,
|
||||||
|
selection_path: Option<PathBuf>,
|
||||||
|
) -> Command<Message> {
|
||||||
let tab = Tab::new(location.clone(), self.config.tab);
|
let tab = Tab::new(location.clone(), self.config.tab);
|
||||||
let entity = self
|
let entity = self
|
||||||
.tab_model
|
.tab_model
|
||||||
.insert()
|
.insert()
|
||||||
.text(tab.title())
|
.text(tab.title())
|
||||||
.data(tab)
|
.data(tab)
|
||||||
.closable()
|
.closable();
|
||||||
.activate()
|
|
||||||
.id();
|
|
||||||
|
|
||||||
self.activate_nav_model_location(&location);
|
let entity = if activate {
|
||||||
|
entity.activate().id()
|
||||||
|
} else {
|
||||||
|
entity.id()
|
||||||
|
};
|
||||||
|
|
||||||
Command::batch([
|
Command::batch([
|
||||||
self.update_title(),
|
self.update_title(),
|
||||||
self.update_watcher(),
|
self.update_watcher(),
|
||||||
self.rescan_tab(entity, location),
|
self.rescan_tab(entity, location, selection_path),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -406,13 +419,20 @@ impl App {
|
||||||
self.pending_operations.insert(id, (operation, 0.0));
|
self.pending_operations.insert(id, (operation, 0.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rescan_tab(&mut self, entity: Entity, location: Location) -> Command<Message> {
|
fn rescan_tab(
|
||||||
|
&mut self,
|
||||||
|
entity: Entity,
|
||||||
|
location: Location,
|
||||||
|
selection_path: Option<PathBuf>,
|
||||||
|
) -> Command<Message> {
|
||||||
let icon_sizes = self.config.tab.icon_sizes;
|
let icon_sizes = self.config.tab.icon_sizes;
|
||||||
Command::perform(
|
Command::perform(
|
||||||
async move {
|
async move {
|
||||||
let location2 = location.clone();
|
let location2 = location.clone();
|
||||||
match tokio::task::spawn_blocking(move || location2.scan(icon_sizes)).await {
|
match tokio::task::spawn_blocking(move || location2.scan(icon_sizes)).await {
|
||||||
Ok(items) => message::app(Message::TabRescan(entity, location, items)),
|
Ok(items) => {
|
||||||
|
message::app(Message::TabRescan(entity, location, items, selection_path))
|
||||||
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!("failed to rescan: {}", err);
|
log::warn!("failed to rescan: {}", err);
|
||||||
message::none()
|
message::none()
|
||||||
|
|
@ -435,7 +455,7 @@ impl App {
|
||||||
|
|
||||||
let mut commands = Vec::with_capacity(needs_reload.len());
|
let mut commands = Vec::with_capacity(needs_reload.len());
|
||||||
for (entity, location) in needs_reload {
|
for (entity, location) in needs_reload {
|
||||||
commands.push(self.rescan_tab(entity, location));
|
commands.push(self.rescan_tab(entity, location, None));
|
||||||
}
|
}
|
||||||
Command::batch(commands)
|
Command::batch(commands)
|
||||||
}
|
}
|
||||||
|
|
@ -446,16 +466,12 @@ impl App {
|
||||||
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
|
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
|
||||||
match &tab.location {
|
match &tab.location {
|
||||||
Location::Path(path) | Location::Search(path, ..) => {
|
Location::Path(path) | Location::Search(path, ..) => {
|
||||||
let location = if !self.search_input.is_empty() {
|
let location = if !self.search_input.is_empty() {
|
||||||
Location::Search(path.clone(), self.search_input.clone())
|
Location::Search(path.clone(), self.search_input.clone())
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
Location::Path(path.clone())
|
Location::Path(path.clone())
|
||||||
};
|
};
|
||||||
tab.change_location(
|
tab.change_location(&location, None);
|
||||||
&location,
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
title_location_opt = Some((tab.title(), tab.location.clone()));
|
title_location_opt = Some((tab.title(), tab.location.clone()));
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|
@ -466,7 +482,7 @@ impl App {
|
||||||
return Command::batch([
|
return Command::batch([
|
||||||
self.update_title(),
|
self.update_title(),
|
||||||
self.update_watcher(),
|
self.update_watcher(),
|
||||||
self.rescan_tab(entity, location),
|
self.rescan_tab(entity, location, None),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
Command::none()
|
Command::none()
|
||||||
|
|
@ -1082,14 +1098,14 @@ impl Application for App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
commands.push(app.open_tab(location));
|
commands.push(app.open_tab(location, true, None));
|
||||||
}
|
}
|
||||||
|
|
||||||
if app.tab_model.iter().next().is_none() {
|
if app.tab_model.iter().next().is_none() {
|
||||||
if let Ok(current_dir) = env::current_dir() {
|
if let Ok(current_dir) = env::current_dir() {
|
||||||
commands.push(app.open_tab(Location::Path(current_dir)));
|
commands.push(app.open_tab(Location::Path(current_dir), true, None));
|
||||||
} else {
|
} else {
|
||||||
commands.push(app.open_tab(Location::Path(home_dir())));
|
commands.push(app.open_tab(Location::Path(home_dir()), true, None));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1330,6 +1346,18 @@ impl Application for App {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Message::ExtractHere(entity_opt) => {
|
||||||
|
let paths = self.selected_paths(entity_opt);
|
||||||
|
if let Some(current_path) = paths.get(0) {
|
||||||
|
if let Some(destination) = current_path.parent().zip(current_path.file_stem()) {
|
||||||
|
let destination_path = destination.0.to_path_buf();
|
||||||
|
self.operation(Operation::Extract {
|
||||||
|
paths,
|
||||||
|
to: destination_path,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Message::Key(modifiers, key) => {
|
Message::Key(modifiers, key) => {
|
||||||
let entity = self.tab_model.active();
|
let entity = self.tab_model.active();
|
||||||
for (key_bind, action) in self.key_binds.iter() {
|
for (key_bind, action) in self.key_binds.iter() {
|
||||||
|
|
@ -1404,7 +1432,7 @@ impl Application for App {
|
||||||
};
|
};
|
||||||
if let Some(title) = title_opt {
|
if let Some(title) = title_opt {
|
||||||
self.tab_model.text_set(entity, title);
|
self.tab_model.text_set(entity, title);
|
||||||
commands.push(self.rescan_tab(entity, home_location.clone()));
|
commands.push(self.rescan_tab(entity, home_location.clone(), None));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !commands.is_empty() {
|
if !commands.is_empty() {
|
||||||
|
|
@ -1502,7 +1530,7 @@ impl Application for App {
|
||||||
|
|
||||||
let mut commands = Vec::with_capacity(needs_reload.len());
|
let mut commands = Vec::with_capacity(needs_reload.len());
|
||||||
for (entity, location) in needs_reload {
|
for (entity, location) in needs_reload {
|
||||||
commands.push(self.rescan_tab(entity, location));
|
commands.push(self.rescan_tab(entity, location, None));
|
||||||
}
|
}
|
||||||
return Command::batch(commands);
|
return Command::batch(commands);
|
||||||
}
|
}
|
||||||
|
|
@ -1575,7 +1603,7 @@ impl Application for App {
|
||||||
return Command::batch(self.selected_paths(entity_opt).into_iter().filter_map(
|
return Command::batch(self.selected_paths(entity_opt).into_iter().filter_map(
|
||||||
|path| {
|
|path| {
|
||||||
if path.is_dir() {
|
if path.is_dir() {
|
||||||
Some(self.open_tab(Location::Path(path)))
|
Some(self.open_tab(Location::Path(path), false, None))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
@ -1597,6 +1625,21 @@ impl Application for App {
|
||||||
log::error!("failed to get current executable path: {}", err);
|
log::error!("failed to get current executable path: {}", err);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
Message::OpenItemLocation(entity_opt) => {
|
||||||
|
return Command::batch(self.selected_paths(entity_opt).into_iter().filter_map(
|
||||||
|
|path| {
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
Some(self.open_tab(
|
||||||
|
Location::Path(parent.to_path_buf()),
|
||||||
|
true,
|
||||||
|
Some(path),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
Message::Paste(entity_opt) => {
|
Message::Paste(entity_opt) => {
|
||||||
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
|
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
|
||||||
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
|
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
|
||||||
|
|
@ -1909,14 +1952,14 @@ impl Application for App {
|
||||||
tab::Command::Action(action) => {
|
tab::Command::Action(action) => {
|
||||||
commands.push(self.update(action.message(Some(entity))));
|
commands.push(self.update(action.message(Some(entity))));
|
||||||
}
|
}
|
||||||
tab::Command::ChangeLocation(tab_title, tab_path) => {
|
tab::Command::ChangeLocation(tab_title, tab_path, selection_path) => {
|
||||||
self.activate_nav_model_location(&tab_path);
|
self.activate_nav_model_location(&tab_path);
|
||||||
|
|
||||||
self.tab_model.text_set(entity, tab_title);
|
self.tab_model.text_set(entity, tab_title);
|
||||||
commands.push(Command::batch([
|
commands.push(Command::batch([
|
||||||
self.update_title(),
|
self.update_title(),
|
||||||
self.update_watcher(),
|
self.update_watcher(),
|
||||||
self.rescan_tab(entity, tab_path),
|
self.rescan_tab(entity, tab_path, selection_path),
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
tab::Command::EmptyTrash => {
|
tab::Command::EmptyTrash => {
|
||||||
|
|
@ -1937,7 +1980,7 @@ impl Application for App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tab::Command::OpenInNewTab(path) => {
|
tab::Command::OpenInNewTab(path) => {
|
||||||
commands.push(self.open_tab(Location::Path(path.clone())));
|
commands.push(self.open_tab(Location::Path(path.clone()), false, None));
|
||||||
}
|
}
|
||||||
tab::Command::OpenInNewWindow(path) => match env::current_exe() {
|
tab::Command::OpenInNewWindow(path) => match env::current_exe() {
|
||||||
Ok(exe) => match process::Command::new(&exe).arg(path).spawn() {
|
Ok(exe) => match process::Command::new(&exe).arg(path).spawn() {
|
||||||
|
|
@ -1989,13 +2032,16 @@ impl Application for App {
|
||||||
Some(tab) => tab.location.clone(),
|
Some(tab) => tab.location.clone(),
|
||||||
None => Location::Path(home_dir()),
|
None => Location::Path(home_dir()),
|
||||||
};
|
};
|
||||||
return self.open_tab(location);
|
return self.open_tab(location, true, None);
|
||||||
}
|
}
|
||||||
Message::TabRescan(entity, location, items) => {
|
Message::TabRescan(entity, location, items, selection_path) => {
|
||||||
match self.tab_model.data_mut::<Tab>(entity) {
|
match self.tab_model.data_mut::<Tab>(entity) {
|
||||||
Some(tab) => {
|
Some(tab) => {
|
||||||
if location == tab.location {
|
if location == tab.location {
|
||||||
tab.set_items(items);
|
tab.set_items(items);
|
||||||
|
if let Some(selection_path) = selection_path {
|
||||||
|
tab.select_path(selection_path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
|
|
@ -2125,7 +2171,7 @@ impl Application for App {
|
||||||
return Command::batch([
|
return Command::batch([
|
||||||
self.update_title(),
|
self.update_title(),
|
||||||
self.update_watcher(),
|
self.update_watcher(),
|
||||||
self.rescan_tab(entity, location),
|
self.rescan_tab(entity, location, None),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2195,10 +2241,10 @@ impl Application for App {
|
||||||
NavMenuAction::OpenInNewTab(entity) => {
|
NavMenuAction::OpenInNewTab(entity) => {
|
||||||
match self.nav_model.data::<Location>(entity) {
|
match self.nav_model.data::<Location>(entity) {
|
||||||
Some(Location::Path(ref path)) => {
|
Some(Location::Path(ref path)) => {
|
||||||
return self.open_tab(Location::Path(path.clone()));
|
return self.open_tab(Location::Path(path.clone()), false, None);
|
||||||
}
|
}
|
||||||
Some(Location::Trash) => {
|
Some(Location::Trash) => {
|
||||||
return self.open_tab(Location::Trash);
|
return self.open_tab(Location::Trash, false, None);
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
@ -2244,7 +2290,7 @@ impl Application for App {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Message::Recents => {
|
Message::Recents => {
|
||||||
return self.open_tab(Location::Recents);
|
return self.open_tab(Location::Recents, false, None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -754,7 +754,7 @@ impl Application for App {
|
||||||
tab::Command::Action(action) => {
|
tab::Command::Action(action) => {
|
||||||
log::warn!("Action {:?} not supported in dialog", action);
|
log::warn!("Action {:?} not supported in dialog", action);
|
||||||
}
|
}
|
||||||
tab::Command::ChangeLocation(_tab_title, _tab_path) => {
|
tab::Command::ChangeLocation(_tab_title, _tab_path, _selection_path) => {
|
||||||
commands
|
commands
|
||||||
.push(Command::batch([self.update_watcher(), self.rescan_tab()]));
|
.push(Command::batch([self.update_watcher(), self.rescan_tab()]));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
20
src/menu.rs
20
src/menu.rs
|
|
@ -9,6 +9,7 @@ use cosmic::{
|
||||||
widget::menu::{self, key_bind::KeyBind, ItemHeight, ItemWidth, MenuBar},
|
widget::menu::{self, key_bind::KeyBind, ItemHeight, ItemWidth, MenuBar},
|
||||||
Element,
|
Element,
|
||||||
};
|
};
|
||||||
|
use mime_guess::Mime;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -79,6 +80,7 @@ pub fn context_menu<'a>(
|
||||||
|
|
||||||
let mut selected_dir = 0;
|
let mut selected_dir = 0;
|
||||||
let mut selected = 0;
|
let mut selected = 0;
|
||||||
|
let mut selected_types: Vec<Mime> = vec![];
|
||||||
tab.items_opt().map(|items| {
|
tab.items_opt().map(|items| {
|
||||||
for item in items.iter() {
|
for item in items.iter() {
|
||||||
if item.selected {
|
if item.selected {
|
||||||
|
|
@ -86,9 +88,12 @@ pub fn context_menu<'a>(
|
||||||
if item.metadata.is_dir() {
|
if item.metadata.is_dir() {
|
||||||
selected_dir += 1;
|
selected_dir += 1;
|
||||||
}
|
}
|
||||||
|
selected_types.push(item.mime.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
selected_types.sort_unstable();
|
||||||
|
selected_types.dedup();
|
||||||
|
|
||||||
let mut children: Vec<Element<_>> = Vec::new();
|
let mut children: Vec<Element<_>> = Vec::new();
|
||||||
match tab.location {
|
match tab.location {
|
||||||
|
|
@ -104,6 +109,11 @@ pub fn context_menu<'a>(
|
||||||
.push(menu_item(fl!("open-in-terminal"), Action::OpenTerminal).into());
|
.push(menu_item(fl!("open-in-terminal"), Action::OpenTerminal).into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if matches!(tab.location, Location::Search(_, _)) {
|
||||||
|
children.push(
|
||||||
|
menu_item(fl!("open-item-location"), Action::OpenItemLocation).into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
// All selected items are directories
|
// All selected items are directories
|
||||||
if selected == selected_dir {
|
if selected == selected_dir {
|
||||||
children.push(menu_item(fl!("open-in-new-tab"), Action::OpenInNewTab).into());
|
children.push(menu_item(fl!("open-in-new-tab"), Action::OpenInNewTab).into());
|
||||||
|
|
@ -114,6 +124,16 @@ pub fn context_menu<'a>(
|
||||||
children.push(menu_item(fl!("rename"), Action::Rename).into());
|
children.push(menu_item(fl!("rename"), Action::Rename).into());
|
||||||
children.push(menu_item(fl!("cut"), Action::Cut).into());
|
children.push(menu_item(fl!("cut"), Action::Cut).into());
|
||||||
children.push(menu_item(fl!("copy"), Action::Copy).into());
|
children.push(menu_item(fl!("copy"), Action::Copy).into());
|
||||||
|
|
||||||
|
let supported_archive_types = ["application/x-tar", "application/zip"]
|
||||||
|
.iter()
|
||||||
|
.filter_map(|mime_type| mime_type.parse::<Mime>().ok())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
selected_types.retain(|t| !supported_archive_types.contains(t));
|
||||||
|
if selected_types.is_empty() {
|
||||||
|
children.push(menu_item(fl!("extract-here"), Action::ExtractHere).into());
|
||||||
|
}
|
||||||
|
|
||||||
//TODO: Print?
|
//TODO: Print?
|
||||||
children.push(container(horizontal_rule(1)).padding([0, 8]).into());
|
children.push(container(horizontal_rule(1)).padding([0, 8]).into());
|
||||||
children.push(menu_item(fl!("show-details"), Action::Properties).into());
|
children.push(menu_item(fl!("show-details"), Action::Properties).into());
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
use cosmic::iced::futures::{channel::mpsc::Sender, executor, SinkExt};
|
use cosmic::iced::futures::{channel::mpsc::Sender, executor, SinkExt};
|
||||||
|
use mime_guess::MimeGuess;
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
fs,
|
fs,
|
||||||
|
io::{self, Error},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
|
process,
|
||||||
sync::{
|
sync::{
|
||||||
atomic::{self, AtomicU64},
|
atomic::{self, AtomicU64},
|
||||||
Arc,
|
Arc,
|
||||||
|
|
@ -138,6 +141,11 @@ pub enum Operation {
|
||||||
},
|
},
|
||||||
/// Empty the trash
|
/// Empty the trash
|
||||||
EmptyTrash,
|
EmptyTrash,
|
||||||
|
/// Uncompress files
|
||||||
|
Extract {
|
||||||
|
paths: Vec<PathBuf>,
|
||||||
|
to: PathBuf,
|
||||||
|
},
|
||||||
/// Move items
|
/// Move items
|
||||||
Move {
|
Move {
|
||||||
paths: Vec<PathBuf>,
|
paths: Vec<PathBuf>,
|
||||||
|
|
@ -246,6 +254,12 @@ impl Operation {
|
||||||
to = fl!("trash")
|
to = fl!("trash")
|
||||||
),
|
),
|
||||||
Self::EmptyTrash => fl!("emptying-trash"),
|
Self::EmptyTrash => fl!("emptying-trash"),
|
||||||
|
Self::Extract { paths, to } => fl!(
|
||||||
|
"extracting",
|
||||||
|
items = paths.len(),
|
||||||
|
from = paths_parent_name(paths),
|
||||||
|
to = file_name(to)
|
||||||
|
),
|
||||||
Self::Move { paths, to } => fl!(
|
Self::Move { paths, to } => fl!(
|
||||||
"moving",
|
"moving",
|
||||||
items = paths.len(),
|
items = paths.len(),
|
||||||
|
|
@ -284,6 +298,12 @@ impl Operation {
|
||||||
to = fl!("trash")
|
to = fl!("trash")
|
||||||
),
|
),
|
||||||
Self::EmptyTrash => fl!("emptied-trash"),
|
Self::EmptyTrash => fl!("emptied-trash"),
|
||||||
|
Self::Extract { paths, to } => fl!(
|
||||||
|
"extracted",
|
||||||
|
items = paths.len(),
|
||||||
|
from = paths_parent_name(paths),
|
||||||
|
to = file_name(to)
|
||||||
|
),
|
||||||
Self::Move { paths, to } => fl!(
|
Self::Move { paths, to } => fl!(
|
||||||
"moved",
|
"moved",
|
||||||
items = paths.len(),
|
items = paths.len(),
|
||||||
|
|
@ -473,6 +493,50 @@ impl Operation {
|
||||||
.send(Message::PendingProgress(id, 100.0))
|
.send(Message::PendingProgress(id, 100.0))
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
Self::Extract { paths, to } => {
|
||||||
|
for path in paths {
|
||||||
|
let to = to.to_owned();
|
||||||
|
|
||||||
|
tokio::task::spawn_blocking(move || -> Result<(), String> {
|
||||||
|
if let Some(file_stem) = path.file_stem() {
|
||||||
|
let mut new_dir = to.join(file_stem);
|
||||||
|
if new_dir.exists() {
|
||||||
|
let mut extensionless_path = path.to_owned();
|
||||||
|
extensionless_path.set_extension("");
|
||||||
|
if let Some(new_dir_parent) = new_dir.parent() {
|
||||||
|
new_dir = copy_unique_path(&extensionless_path, new_dir_parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(mime) = mime_guess::from_path(&path).first() {
|
||||||
|
match mime.essence_str() {
|
||||||
|
"application/x-tar" => {
|
||||||
|
return fs::File::open(path)
|
||||||
|
.map(io::BufReader::new)
|
||||||
|
.map(tar::Archive::new)
|
||||||
|
.and_then(|mut archive| archive.unpack(new_dir))
|
||||||
|
.map_err(err_str)
|
||||||
|
}
|
||||||
|
"application/zip" => {
|
||||||
|
return fs::File::open(path)
|
||||||
|
.map(io::BufReader::new)
|
||||||
|
.map(zip::ZipArchive::new)
|
||||||
|
.map_err(err_str)?
|
||||||
|
.and_then(|mut archive| archive.extract(new_dir))
|
||||||
|
.map_err(err_str)
|
||||||
|
}
|
||||||
|
_ => Err(format!("unsupported mime type {:?}", mime))?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(err_str)?
|
||||||
|
.map_err(err_str)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
Self::Move { paths, to } => {
|
Self::Move { paths, to } => {
|
||||||
let msg_tx = msg_tx.clone();
|
let msg_tx = msg_tx.clone();
|
||||||
tokio::task::spawn_blocking(move || {
|
tokio::task::spawn_blocking(move || {
|
||||||
|
|
|
||||||
92
src/tab.rs
92
src/tab.rs
|
|
@ -336,7 +336,7 @@ pub fn scan_path(tab_path: &PathBuf, sizes: IconSizes) -> Vec<Item> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let metadata = match entry.metadata() {
|
let metadata = match fs::metadata(&path) {
|
||||||
Ok(ok) => ok,
|
Ok(ok) => ok,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!("failed to read metadata for entry at {:?}: {}", path, err);
|
log::warn!("failed to read metadata for entry at {:?}: {}", path, err);
|
||||||
|
|
@ -397,7 +397,7 @@ pub fn scan_search(tab_path: &PathBuf, term: &str, sizes: IconSizes) -> Vec<Item
|
||||||
if regex.is_match(file_name) {
|
if regex.is_match(file_name) {
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
|
|
||||||
let metadata = match entry.metadata() {
|
let metadata = match fs::metadata(&path) {
|
||||||
Ok(ok) => ok,
|
Ok(ok) => ok,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!("failed to read metadata for entry at {:?}: {}", path, err);
|
log::warn!("failed to read metadata for entry at {:?}: {}", path, err);
|
||||||
|
|
@ -652,7 +652,7 @@ impl Location {
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum Command {
|
pub enum Command {
|
||||||
Action(Action),
|
Action(Action),
|
||||||
ChangeLocation(String, Location),
|
ChangeLocation(String, Location, Option<PathBuf>),
|
||||||
EmptyTrash,
|
EmptyTrash,
|
||||||
FocusButton(widget::Id),
|
FocusButton(widget::Id),
|
||||||
FocusTextInput(widget::Id),
|
FocusTextInput(widget::Id),
|
||||||
|
|
@ -1003,6 +1003,7 @@ pub struct Tab {
|
||||||
pub dialog: Option<DialogKind>,
|
pub dialog: Option<DialogKind>,
|
||||||
pub scroll_opt: Option<AbsoluteOffset>,
|
pub scroll_opt: Option<AbsoluteOffset>,
|
||||||
pub size_opt: Cell<Option<Size>>,
|
pub size_opt: Cell<Option<Size>>,
|
||||||
|
pub item_view_size_opt: Cell<Option<Size>>,
|
||||||
pub edit_location: Option<Location>,
|
pub edit_location: Option<Location>,
|
||||||
pub edit_location_id: widget::Id,
|
pub edit_location_id: widget::Id,
|
||||||
pub history_i: usize,
|
pub history_i: usize,
|
||||||
|
|
@ -1029,6 +1030,7 @@ impl Tab {
|
||||||
dialog: None,
|
dialog: None,
|
||||||
scroll_opt: None,
|
scroll_opt: None,
|
||||||
size_opt: Cell::new(None),
|
size_opt: Cell::new(None),
|
||||||
|
item_view_size_opt: Cell::new(None),
|
||||||
edit_location: None,
|
edit_location: None,
|
||||||
edit_location_id: widget::Id::unique(),
|
edit_location_id: widget::Id::unique(),
|
||||||
history_i: 0,
|
history_i: 0,
|
||||||
|
|
@ -1104,11 +1106,16 @@ impl Tab {
|
||||||
*self.cached_selected.borrow_mut() = None;
|
*self.cached_selected.borrow_mut() = None;
|
||||||
if let Some(ref mut items) = self.items_opt {
|
if let Some(ref mut items) = self.items_opt {
|
||||||
for item in items.iter_mut() {
|
for item in items.iter_mut() {
|
||||||
if item.name == name {
|
item.selected = item.name == name;
|
||||||
item.selected = true;
|
}
|
||||||
} else {
|
}
|
||||||
item.selected = false;
|
}
|
||||||
}
|
|
||||||
|
pub fn select_path(&mut self, path: PathBuf) {
|
||||||
|
*self.cached_selected.borrow_mut() = None;
|
||||||
|
if let Some(ref mut items) = self.items_opt {
|
||||||
|
for item in items.iter_mut() {
|
||||||
|
item.selected = item.path_opt.as_ref() == Some(&path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1211,7 +1218,10 @@ impl Tab {
|
||||||
Some(offset) => Point::new(0.0, offset.y),
|
Some(offset) => Point::new(0.0, offset.y),
|
||||||
None => Point::new(0.0, 0.0),
|
None => Point::new(0.0, 0.0),
|
||||||
};
|
};
|
||||||
let size = self.size_opt.get().unwrap_or_else(|| Size::new(0.0, 0.0));
|
let size = self
|
||||||
|
.item_view_size_opt
|
||||||
|
.get()
|
||||||
|
.unwrap_or_else(|| Size::new(0.0, 0.0));
|
||||||
Rectangle::new(point, size)
|
Rectangle::new(point, size)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1949,8 +1959,13 @@ impl Tab {
|
||||||
Location::Trash => true,
|
Location::Trash => true,
|
||||||
Location::Recents => true,
|
Location::Recents => true,
|
||||||
} {
|
} {
|
||||||
|
let prev_path = if let Location::Path(path) = &self.location {
|
||||||
|
Some(path.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
self.change_location(&location, history_i_opt);
|
self.change_location(&location, history_i_opt);
|
||||||
commands.push(Command::ChangeLocation(self.title(), location));
|
commands.push(Command::ChangeLocation(self.title(), location, prev_path));
|
||||||
} else {
|
} else {
|
||||||
log::warn!("tried to cd to {:?} which is not a directory", location);
|
log::warn!("tried to cd to {:?} which is not a directory", location);
|
||||||
}
|
}
|
||||||
|
|
@ -2187,9 +2202,9 @@ impl Tab {
|
||||||
crate::mouse_area::MouseArea::new(
|
crate::mouse_area::MouseArea::new(
|
||||||
widget::button(widget::icon::from_name("edit-symbolic").size(16))
|
widget::button(widget::icon::from_name("edit-symbolic").size(16))
|
||||||
.padding(space_xxs)
|
.padding(space_xxs)
|
||||||
.style(theme::Button::Icon),
|
.style(theme::Button::Icon)
|
||||||
|
.on_press(Message::EditLocation(Some(self.location.clone()))),
|
||||||
)
|
)
|
||||||
.on_press(move |_| Message::EditLocation(Some(self.location.clone())))
|
|
||||||
.on_middle_press(move |_| Message::OpenInNewTab(path.clone())),
|
.on_middle_press(move |_| Message::OpenInNewTab(path.clone())),
|
||||||
);
|
);
|
||||||
w += 16.0 + 2.0 * space_xxs as f32;
|
w += 16.0 + 2.0 * space_xxs as f32;
|
||||||
|
|
@ -2289,24 +2304,22 @@ impl Tab {
|
||||||
let mut mouse_area = crate::mouse_area::MouseArea::new(
|
let mut mouse_area = crate::mouse_area::MouseArea::new(
|
||||||
widget::button(row)
|
widget::button(row)
|
||||||
.padding(space_xxxs)
|
.padding(space_xxxs)
|
||||||
.style(theme::Button::Link),
|
.style(theme::Button::Link)
|
||||||
)
|
.on_press(Message::Location(match &self.location {
|
||||||
.on_press(move |_| {
|
Location::Path(_) => Location::Path(ancestor.to_path_buf()),
|
||||||
Message::Location(match &self.location {
|
Location::Search(_, term) => {
|
||||||
Location::Path(_) => Location::Path(ancestor.to_path_buf()),
|
Location::Search(ancestor.to_path_buf(), term.clone())
|
||||||
Location::Search(_, term) => {
|
}
|
||||||
Location::Search(ancestor.to_path_buf(), term.clone())
|
other => other.clone(),
|
||||||
}
|
})),
|
||||||
other => other.clone(),
|
);
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if self.location_context_menu_index.is_some() {
|
if self.location_context_menu_index.is_some() {
|
||||||
mouse_area = mouse_area.on_right_press(move |_point_opt| {
|
mouse_area = mouse_area.on_right_press(move |_point_opt| {
|
||||||
Message::LocationContextMenuIndex(None)
|
Message::LocationContextMenuIndex(None)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
mouse_area = mouse_area.on_right_press_no_capture(move |point_opt| {
|
mouse_area = mouse_area.on_right_press_no_capture(move |_point_opt| {
|
||||||
Message::LocationContextMenuIndex(Some(index))
|
Message::LocationContextMenuIndex(Some(index))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -2619,9 +2632,16 @@ impl Tab {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let spacer_height = height
|
|
||||||
.checked_sub(max_bottom + 7 * (space_xxs as usize))
|
let top_deduct = 7 * (space_xxs as usize);
|
||||||
.unwrap_or(0);
|
|
||||||
|
self.item_view_size_opt
|
||||||
|
.set(self.size_opt.get().map(|s| Size {
|
||||||
|
width: s.width,
|
||||||
|
height: s.height - top_deduct as f32,
|
||||||
|
}));
|
||||||
|
|
||||||
|
let spacer_height = height.checked_sub(max_bottom + top_deduct).unwrap_or(0);
|
||||||
if spacer_height > 0 {
|
if spacer_height > 0 {
|
||||||
children.push(
|
children.push(
|
||||||
widget::container(vertical_space(Length::Fixed(spacer_height as f32)))
|
widget::container(vertical_space(Length::Fixed(spacer_height as f32)))
|
||||||
|
|
@ -2973,12 +2993,18 @@ impl Tab {
|
||||||
}
|
}
|
||||||
//TODO: HACK If we don't reach the bottom of the view, go ahead and add a spacer to do that
|
//TODO: HACK If we don't reach the bottom of the view, go ahead and add a spacer to do that
|
||||||
{
|
{
|
||||||
let spacer_height =
|
let top_deduct = (if condensed { 6 } else { 9 }) * space_xxs;
|
||||||
size.height as i32 - y as i32 - (if condensed { 6 } else { 9 }) * space_xxs as i32;
|
|
||||||
if spacer_height > 0 {
|
self.item_view_size_opt
|
||||||
children.push(
|
.set(self.size_opt.get().map(|s| Size {
|
||||||
widget::container(vertical_space(Length::Fixed(spacer_height as f32))).into(),
|
width: s.width,
|
||||||
);
|
height: s.height - top_deduct as f32,
|
||||||
|
}));
|
||||||
|
|
||||||
|
let spacer_height = size.height - y as f32 - top_deduct as f32;
|
||||||
|
if spacer_height > 0. {
|
||||||
|
children
|
||||||
|
.push(widget::container(vertical_space(Length::Fixed(spacer_height))).into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let drag_col = (!drag_items.is_empty()).then(|| {
|
let drag_col = (!drag_items.is_empty()).then(|| {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue