Merge pull request #1329 from Cheong-Lau/clippy
General code cleanup and performance optimisations
This commit is contained in:
commit
6ab15d23a1
19 changed files with 1956 additions and 2029 deletions
1575
src/app.rs
1575
src/app.rs
File diff suppressed because it is too large
Load diff
|
|
@ -47,7 +47,7 @@ pub fn extract(
|
||||||
controller: &Controller,
|
controller: &Controller,
|
||||||
) -> Result<(), OperationError> {
|
) -> Result<(), OperationError> {
|
||||||
let mime = mime_for_path(path, None, false);
|
let mime = mime_for_path(path, None, false);
|
||||||
let password = password.clone();
|
let password = password.as_deref();
|
||||||
match mime.essence_str() {
|
match mime.essence_str() {
|
||||||
"application/gzip" | "application/x-compressed-tar" => {
|
"application/gzip" | "application/x-compressed-tar" => {
|
||||||
OpReader::new(path, controller.clone())
|
OpReader::new(path, controller.clone())
|
||||||
|
|
@ -55,7 +55,7 @@ pub fn extract(
|
||||||
.map(flate2::read::GzDecoder::new)
|
.map(flate2::read::GzDecoder::new)
|
||||||
.map(tar::Archive::new)
|
.map(tar::Archive::new)
|
||||||
.and_then(|mut archive| archive.unpack(new_dir))
|
.and_then(|mut archive| archive.unpack(new_dir))
|
||||||
.map_err(|e| OperationError::from_err(e, controller))?
|
.map_err(|e| OperationError::from_err(e, controller))?;
|
||||||
}
|
}
|
||||||
"application/x-tar" => OpReader::new(path, controller.clone())
|
"application/x-tar" => OpReader::new(path, controller.clone())
|
||||||
.map(io::BufReader::new)
|
.map(io::BufReader::new)
|
||||||
|
|
@ -93,10 +93,10 @@ pub fn extract(
|
||||||
.map(|reader| lzma_rust2::XzReader::new(reader, true))
|
.map(|reader| lzma_rust2::XzReader::new(reader, true))
|
||||||
.map(tar::Archive::new)
|
.map(tar::Archive::new)
|
||||||
.and_then(|mut archive| archive.unpack(new_dir))
|
.and_then(|mut archive| archive.unpack(new_dir))
|
||||||
.map_err(|e| OperationError::from_err(e, controller))?
|
.map_err(|e| OperationError::from_err(e, controller))?;
|
||||||
}
|
}
|
||||||
_ => Err(OperationError::from_err(
|
_ => Err(OperationError::from_err(
|
||||||
format!("unsupported mime type {:?}", mime),
|
format!("unsupported mime type {mime:?}"),
|
||||||
controller,
|
controller,
|
||||||
))?,
|
))?,
|
||||||
}
|
}
|
||||||
|
|
@ -107,7 +107,7 @@ pub fn extract(
|
||||||
fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
|
fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
|
||||||
archive: &mut zip::ZipArchive<R>,
|
archive: &mut zip::ZipArchive<R>,
|
||||||
directory: P,
|
directory: P,
|
||||||
password: Option<String>,
|
password: Option<&str>,
|
||||||
controller: Controller,
|
controller: Controller,
|
||||||
) -> zip::result::ZipResult<()> {
|
) -> zip::result::ZipResult<()> {
|
||||||
use std::{ffi::OsString, fs};
|
use std::{ffi::OsString, fs};
|
||||||
|
|
@ -145,7 +145,7 @@ fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
|
||||||
|
|
||||||
controller.set_progress((i as f32) / total_files as f32);
|
controller.set_progress((i as f32) / total_files as f32);
|
||||||
|
|
||||||
let mut file = match &password {
|
let mut file = match password {
|
||||||
None => archive.by_index(i),
|
None => archive.by_index(i),
|
||||||
Some(pwd) => archive.by_index_decrypt(i, pwd.as_bytes()),
|
Some(pwd) => archive.by_index_decrypt(i, pwd.as_bytes()),
|
||||||
}?;
|
}?;
|
||||||
|
|
@ -207,7 +207,7 @@ fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let mut file = match &password {
|
let mut file = match password {
|
||||||
None => archive.by_index(i),
|
None => archive.by_index(i),
|
||||||
Some(pwd) => archive.by_index_decrypt(i, pwd.as_bytes()),
|
Some(pwd) => archive.by_index_decrypt(i, pwd.as_bytes()),
|
||||||
}?;
|
}?;
|
||||||
|
|
@ -262,7 +262,7 @@ fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
|
||||||
// Ensure we update children's permissions before making a parent unwritable
|
// Ensure we update children's permissions before making a parent unwritable
|
||||||
files_by_unix_mode.sort_by_key(|(path, _)| Reverse(path.clone()));
|
files_by_unix_mode.sort_by_key(|(path, _)| Reverse(path.clone()));
|
||||||
}
|
}
|
||||||
for (path, mode) in files_by_unix_mode.into_iter() {
|
for (path, mode) in files_by_unix_mode {
|
||||||
fs::set_permissions(&path, fs::Permissions::from_mode(mode))?;
|
fs::set_permissions(&path, fs::Permissions::from_mode(mode))?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ pub struct ClipboardCopy {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClipboardCopy {
|
impl ClipboardCopy {
|
||||||
pub fn new<P: AsRef<Path>>(kind: ClipboardKind, paths: &[P]) -> Self {
|
pub fn new<P: AsRef<Path>>(kind: ClipboardKind, paths: impl IntoIterator<Item = P>) -> Self {
|
||||||
let available = vec![
|
let available = vec![
|
||||||
"text/plain".to_string(),
|
"text/plain".to_string(),
|
||||||
"text/plain;charset=utf-8".to_string(),
|
"text/plain;charset=utf-8".to_string(),
|
||||||
|
|
@ -42,7 +42,7 @@ impl ClipboardCopy {
|
||||||
.to_string();
|
.to_string();
|
||||||
//TODO: do we have to use \r\n?
|
//TODO: do we have to use \r\n?
|
||||||
let cr_nl = "\r\n";
|
let cr_nl = "\r\n";
|
||||||
for path in paths.iter() {
|
for path in paths {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
|
|
||||||
match path.to_str() {
|
match path.to_str() {
|
||||||
|
|
@ -56,8 +56,8 @@ impl ClipboardCopy {
|
||||||
None => {
|
None => {
|
||||||
//TODO: allow non-UTF-8?
|
//TODO: allow non-UTF-8?
|
||||||
log::warn!(
|
log::warn!(
|
||||||
"{:?} is not valid UTF-8, not adding to text/plain clipboard",
|
"{} is not valid UTF-8, not adding to text/plain clipboard",
|
||||||
path
|
path.display()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -74,8 +74,8 @@ impl ClipboardCopy {
|
||||||
}
|
}
|
||||||
Err(()) => {
|
Err(()) => {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
"{:?} cannot be turned into a URL, not adding to text/uri-list clipboard",
|
"{} cannot be turned into a URL, not adding to text/uri-list clipboard",
|
||||||
path
|
path.display()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -135,7 +135,7 @@ impl TryFrom<(Vec<u8>, String)> for ClipboardPaste {
|
||||||
let url = Url::parse(line)?;
|
let url = Url::parse(line)?;
|
||||||
match url.to_file_path() {
|
match url.to_file_path() {
|
||||||
Ok(path) => paths.push(path),
|
Ok(path) => paths.push(path),
|
||||||
Err(()) => Err(format!("invalid file URL {:?}", url))?,
|
Err(()) => Err(format!("invalid file URL {url:?}"))?,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -146,18 +146,18 @@ impl TryFrom<(Vec<u8>, String)> for ClipboardPaste {
|
||||||
kind = match line {
|
kind = match line {
|
||||||
"copy" => ClipboardKind::Copy,
|
"copy" => ClipboardKind::Copy,
|
||||||
"cut" => ClipboardKind::Cut { is_dnd: false },
|
"cut" => ClipboardKind::Cut { is_dnd: false },
|
||||||
_ => Err(format!("unsupported clipboard operation {:?}", line))?,
|
_ => Err(format!("unsupported clipboard operation {line:?}"))?,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
let url = Url::parse(line)?;
|
let url = Url::parse(line)?;
|
||||||
match url.to_file_path() {
|
match url.to_file_path() {
|
||||||
Ok(path) => paths.push(path),
|
Ok(path) => paths.push(path),
|
||||||
Err(()) => Err(format!("invalid file URL {:?}", url))?,
|
Err(()) => Err(format!("invalid file URL {url:?}"))?,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => Err(format!("unsupported mime type {:?}", mime))?,
|
_ => Err(format!("unsupported mime type {mime:?}"))?,
|
||||||
}
|
}
|
||||||
Ok(Self { kind, paths })
|
Ok(Self { kind, paths })
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -75,21 +75,17 @@ pub enum Favorite {
|
||||||
impl Favorite {
|
impl Favorite {
|
||||||
pub fn from_path(path: PathBuf) -> Self {
|
pub fn from_path(path: PathBuf) -> Self {
|
||||||
// Ensure that special folders are handled properly
|
// Ensure that special folders are handled properly
|
||||||
for favorite in &[
|
[
|
||||||
Self::Home,
|
Self::Home,
|
||||||
Self::Documents,
|
Self::Documents,
|
||||||
Self::Downloads,
|
Self::Downloads,
|
||||||
Self::Music,
|
Self::Music,
|
||||||
Self::Pictures,
|
Self::Pictures,
|
||||||
Self::Videos,
|
Self::Videos,
|
||||||
] {
|
]
|
||||||
if let Some(favorite_path) = favorite.path_opt() {
|
.into_iter()
|
||||||
if favorite_path == path {
|
.find(|fav| fav.path_opt().as_ref() == Some(&path))
|
||||||
return favorite.clone();
|
.unwrap_or(Self::Path(path))
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Self::Path(path)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn path_opt(&self) -> Option<PathBuf> {
|
pub fn path_opt(&self) -> Option<PathBuf> {
|
||||||
|
|
@ -135,18 +131,18 @@ impl State {
|
||||||
pub fn load() -> (Option<cosmic_config::Config>, Self) {
|
pub fn load() -> (Option<cosmic_config::Config>, Self) {
|
||||||
match cosmic_config::Config::new_state(App::APP_ID, CONFIG_VERSION) {
|
match cosmic_config::Config::new_state(App::APP_ID, CONFIG_VERSION) {
|
||||||
Ok(config_handler) => {
|
Ok(config_handler) => {
|
||||||
let config = match State::get_entry(&config_handler) {
|
let config = match Self::get_entry(&config_handler) {
|
||||||
Ok(ok) => ok,
|
Ok(ok) => ok,
|
||||||
Err((errs, config)) => {
|
Err((errs, config)) => {
|
||||||
log::info!("errors loading config: {:?}", errs);
|
log::info!("errors loading config: {errs:?}");
|
||||||
config
|
config
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
(Some(config_handler), config)
|
(Some(config_handler), config)
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::error!("failed to create config handler: {}", err);
|
log::error!("failed to create config handler: {err}");
|
||||||
(None, State::default())
|
(None, Self::default())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -178,18 +174,18 @@ impl Config {
|
||||||
pub fn load() -> (Option<cosmic_config::Config>, Self) {
|
pub fn load() -> (Option<cosmic_config::Config>, Self) {
|
||||||
match cosmic_config::Config::new(App::APP_ID, CONFIG_VERSION) {
|
match cosmic_config::Config::new(App::APP_ID, CONFIG_VERSION) {
|
||||||
Ok(config_handler) => {
|
Ok(config_handler) => {
|
||||||
let config = match Config::get_entry(&config_handler) {
|
let config = match Self::get_entry(&config_handler) {
|
||||||
Ok(ok) => ok,
|
Ok(ok) => ok,
|
||||||
Err((errs, config)) => {
|
Err((errs, config)) => {
|
||||||
log::info!("errors loading config: {:?}", errs);
|
log::info!("errors loading config: {errs:?}");
|
||||||
config
|
config
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
(Some(config_handler), config)
|
(Some(config_handler), config)
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::error!("failed to create config handler: {}", err);
|
log::error!("failed to create config handler: {err}");
|
||||||
(None, Config::default())
|
(None, Self::default())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -204,7 +200,7 @@ impl Config {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Construct tab config for dialog
|
/// Construct tab config for dialog
|
||||||
pub fn dialog_tab(&self) -> TabConfig {
|
pub const fn dialog_tab(&self) -> TabConfig {
|
||||||
TabConfig {
|
TabConfig {
|
||||||
folders_first: self.dialog.folders_first,
|
folders_first: self.dialog.folders_first,
|
||||||
icon_sizes: self.dialog.icon_sizes,
|
icon_sizes: self.dialog.icon_sizes,
|
||||||
|
|
|
||||||
451
src/dialog.rs
451
src/dialog.rs
|
|
@ -31,9 +31,7 @@ use std::{
|
||||||
any::TypeId,
|
any::TypeId,
|
||||||
collections::{HashMap, VecDeque},
|
collections::{HashMap, VecDeque},
|
||||||
env, fmt, fs,
|
env, fmt, fs,
|
||||||
num::NonZeroU16,
|
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
str::FromStr,
|
|
||||||
time::{self, Instant},
|
time::{self, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -48,6 +46,7 @@ use crate::{
|
||||||
menu,
|
menu,
|
||||||
mounter::{MOUNTERS, MounterItem, MounterItems, MounterKey, MounterMessage},
|
mounter::{MOUNTERS, MounterItem, MounterItems, MounterKey, MounterMessage},
|
||||||
tab::{self, ItemMetadata, Location, Tab},
|
tab::{self, ItemMetadata, Location, Tab},
|
||||||
|
zoom::{zoom_in_view, zoom_out_view, zoom_to_default},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
|
@ -86,15 +85,15 @@ impl DialogKind {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_dir(&self) -> bool {
|
pub const fn is_dir(&self) -> bool {
|
||||||
matches!(self, Self::OpenFolder | Self::OpenMultipleFolders)
|
matches!(self, Self::OpenFolder | Self::OpenMultipleFolders)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn multiple(&self) -> bool {
|
pub const fn multiple(&self) -> bool {
|
||||||
matches!(self, Self::OpenMultipleFiles | Self::OpenMultipleFolders)
|
matches!(self, Self::OpenMultipleFiles | Self::OpenMultipleFolders)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save(&self) -> bool {
|
pub const fn save(&self) -> bool {
|
||||||
matches!(self, Self::SaveFile { .. })
|
matches!(self, Self::SaveFile { .. })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -200,7 +199,7 @@ impl<T: AsRef<str>> From<T> for DialogLabel {
|
||||||
impl<'a, M: Clone + 'static> From<&'a DialogLabel> for Element<'a, M> {
|
impl<'a, M: Clone + 'static> From<&'a DialogLabel> for Element<'a, M> {
|
||||||
fn from(label: &'a DialogLabel) -> Self {
|
fn from(label: &'a DialogLabel) -> Self {
|
||||||
let mut iced_spans = Vec::with_capacity(label.spans.len());
|
let mut iced_spans = Vec::with_capacity(label.spans.len());
|
||||||
for span in label.spans.iter() {
|
for span in &label.spans {
|
||||||
iced_spans.push(cosmic::iced::widget::span(&span.text).underline(span.underline));
|
iced_spans.push(cosmic::iced::widget::span(&span.text).underline(span.underline));
|
||||||
}
|
}
|
||||||
cosmic::iced::widget::rich_text(iced_spans).into()
|
cosmic::iced::widget::rich_text(iced_spans).into()
|
||||||
|
|
@ -276,7 +275,7 @@ impl<M: Send + 'static> Dialog<M> {
|
||||||
settings.platform_specific.application_id = dialog_settings.app_id;
|
settings.platform_specific.application_id = dialog_settings.app_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
let (window_id, window_command) = window::open(settings.clone());
|
let (window_id, window_command) = window::open(settings);
|
||||||
|
|
||||||
let mut core = Core::default();
|
let mut core = Core::default();
|
||||||
core.set_main_window_id(Some(window_id));
|
core.set_main_window_id(Some(window_id));
|
||||||
|
|
@ -286,7 +285,7 @@ impl<M: Send + 'static> Dialog<M> {
|
||||||
match fs::canonicalize(path) {
|
match fs::canonicalize(path) {
|
||||||
Ok(ok) => Some(ok),
|
Ok(ok) => Some(ok),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!("failed to canonicalize {:?}: {}", path, err);
|
log::warn!("failed to canonicalize {}: {}", path.display(), err);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -372,7 +371,7 @@ impl<M: Send + 'static> Dialog<M> {
|
||||||
let on_result_message = (self.on_result)(result);
|
let on_result_message = (self.on_result)(result);
|
||||||
Task::batch([
|
Task::batch([
|
||||||
command,
|
command,
|
||||||
Task::perform(async move { cosmic::action::app(on_result_message) }, |x| x),
|
Task::future(async move { cosmic::action::app(on_result_message) }),
|
||||||
])
|
])
|
||||||
} else {
|
} else {
|
||||||
command
|
command
|
||||||
|
|
@ -386,7 +385,7 @@ impl<M: Send + 'static> Dialog<M> {
|
||||||
.map(self.mapper)
|
.map(self.mapper)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn window_id(&self) -> window::Id {
|
pub const fn window_id(&self) -> window::Id {
|
||||||
self.cosmic.app.flags.window_id
|
self.cosmic.app.flags.window_id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -452,24 +451,24 @@ enum Message {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<AppMessage> for Message {
|
impl From<AppMessage> for Message {
|
||||||
fn from(app_message: AppMessage) -> Message {
|
fn from(app_message: AppMessage) -> Self {
|
||||||
match app_message {
|
match app_message {
|
||||||
AppMessage::None => Message::None,
|
AppMessage::None => Self::None,
|
||||||
AppMessage::Preview(_entity_opt) => Message::Preview,
|
AppMessage::Preview(_entity_opt) => Self::Preview,
|
||||||
AppMessage::SearchActivate => Message::SearchActivate,
|
AppMessage::SearchActivate => Self::SearchActivate,
|
||||||
AppMessage::ScrollTab(scroll_speed) => Message::ScrollTab(scroll_speed),
|
AppMessage::ScrollTab(scroll_speed) => Self::ScrollTab(scroll_speed),
|
||||||
AppMessage::TabMessage(_entity_opt, tab_message) => Message::TabMessage(tab_message),
|
AppMessage::TabMessage(_entity_opt, tab_message) => Self::TabMessage(tab_message),
|
||||||
AppMessage::TabView(_entity_opt, view) => Message::TabView(view),
|
AppMessage::TabView(_entity_opt, view) => Self::TabView(view),
|
||||||
AppMessage::ToggleFoldersFirst => Message::ToggleFoldersFirst,
|
AppMessage::ToggleFoldersFirst => Self::ToggleFoldersFirst,
|
||||||
AppMessage::ToggleShowHidden => Message::ToggleShowHidden,
|
AppMessage::ToggleShowHidden => Self::ToggleShowHidden,
|
||||||
AppMessage::ZoomDefault(_entity_opt) => Message::ZoomDefault,
|
AppMessage::ZoomDefault(_entity_opt) => Self::ZoomDefault,
|
||||||
AppMessage::ZoomIn(_entity_opt) => Message::ZoomIn,
|
AppMessage::ZoomIn(_entity_opt) => Self::ZoomIn,
|
||||||
AppMessage::ZoomOut(_entity_opt) => Message::ZoomOut,
|
AppMessage::ZoomOut(_entity_opt) => Self::ZoomOut,
|
||||||
AppMessage::NewItem(_entity_opt, true) => Message::NewFolder,
|
AppMessage::NewItem(_entity_opt, true) => Self::NewFolder,
|
||||||
AppMessage::Surface(action) => Message::Surface(action),
|
AppMessage::Surface(action) => Self::Surface(action),
|
||||||
unsupported => {
|
unsupported => {
|
||||||
log::warn!("{unsupported:?} not supported in dialog mode");
|
log::warn!("{unsupported:?} not supported in dialog mode");
|
||||||
Message::None
|
Self::None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -550,7 +549,7 @@ impl App {
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut row = widget::row::with_capacity(
|
let mut row = widget::row::with_capacity(
|
||||||
if !self.filters.is_empty() { 1 } else { 0 }
|
usize::from(!self.filters.is_empty())
|
||||||
+ self.choices.len() * 2
|
+ self.choices.len() * 2
|
||||||
+ if is_condensed { 0 } else { 3 },
|
+ if is_condensed { 0 } else { 3 },
|
||||||
)
|
)
|
||||||
|
|
@ -566,9 +565,10 @@ impl App {
|
||||||
for (choice_i, choice) in self.choices.iter().enumerate() {
|
for (choice_i, choice) in self.choices.iter().enumerate() {
|
||||||
match choice {
|
match choice {
|
||||||
DialogChoice::CheckBox { label, value, .. } => {
|
DialogChoice::CheckBox { label, value, .. } => {
|
||||||
row = row.push(widget::checkbox(label, *value).on_toggle(move |checked| {
|
row =
|
||||||
Message::Choice(choice_i, if checked { 1 } else { 0 })
|
row.push(widget::checkbox(label, *value).on_toggle(move |checked| {
|
||||||
}));
|
Message::Choice(choice_i, usize::from(checked))
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
DialogChoice::ComboBox {
|
DialogChoice::ComboBox {
|
||||||
label,
|
label,
|
||||||
|
|
@ -595,7 +595,7 @@ impl App {
|
||||||
|
|
||||||
let mut has_selected = false;
|
let mut has_selected = false;
|
||||||
if let Some(items) = self.tab.items_opt() {
|
if let Some(items) = self.tab.items_opt() {
|
||||||
for item in items.iter() {
|
for item in items {
|
||||||
if item.selected {
|
if item.selected {
|
||||||
has_selected = true;
|
has_selected = true;
|
||||||
break;
|
break;
|
||||||
|
|
@ -605,7 +605,7 @@ impl App {
|
||||||
row = row.push(
|
row = row.push(
|
||||||
//TODO: easier way to create buttons with rich text
|
//TODO: easier way to create buttons with rich text
|
||||||
widget::button::custom(
|
widget::button::custom(
|
||||||
widget::row::with_children(vec![Element::from(&self.accept_label)])
|
widget::row::with_children([Element::from(&self.accept_label)])
|
||||||
.padding([0, space_s])
|
.padding([0, space_s])
|
||||||
.width(Length::Shrink)
|
.width(Length::Shrink)
|
||||||
.height(space_l)
|
.height(space_l)
|
||||||
|
|
@ -641,7 +641,7 @@ impl App {
|
||||||
}
|
}
|
||||||
PreviewKind::Location(location) => {
|
PreviewKind::Location(location) => {
|
||||||
if let Some(items) = self.tab.items_opt() {
|
if let Some(items) = self.tab.items_opt() {
|
||||||
for item in items.iter() {
|
for item in items {
|
||||||
if item.location_opt.as_ref() == Some(location) {
|
if item.location_opt.as_ref() == Some(location) {
|
||||||
children.push(item.preview_view(None, military_time));
|
children.push(item.preview_view(None, military_time));
|
||||||
// Only show one property view to avoid issues like hangs when generating
|
// Only show one property view to avoid issues like hangs when generating
|
||||||
|
|
@ -653,7 +653,7 @@ impl App {
|
||||||
}
|
}
|
||||||
PreviewKind::Selected => {
|
PreviewKind::Selected => {
|
||||||
if let Some(items) = self.tab.items_opt() {
|
if let Some(items) = self.tab.items_opt() {
|
||||||
for item in items.iter() {
|
for item in items {
|
||||||
if item.selected {
|
if item.selected {
|
||||||
children.push(item.preview_view(None, military_time));
|
children.push(item.preview_view(None, military_time));
|
||||||
// Only show one property view to avoid issues like hangs when generating
|
// Only show one property view to avoid issues like hangs when generating
|
||||||
|
|
@ -676,40 +676,37 @@ impl App {
|
||||||
let location = self.tab.location.clone();
|
let location = self.tab.location.clone();
|
||||||
let icon_sizes = self.tab.config.icon_sizes;
|
let icon_sizes = self.tab.config.icon_sizes;
|
||||||
let mounter_items = self.mounter_items.clone();
|
let mounter_items = self.mounter_items.clone();
|
||||||
Task::perform(
|
Task::future(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((parent_item_opt, mut items)) => {
|
||||||
Ok((parent_item_opt, mut items)) => {
|
#[cfg(feature = "gvfs")]
|
||||||
#[cfg(feature = "gvfs")]
|
{
|
||||||
{
|
let mounter_paths: Box<[_]> = mounter_items
|
||||||
let mounter_paths: Vec<_> = mounter_items
|
.values()
|
||||||
.iter()
|
.flatten()
|
||||||
.flat_map(|item| item.1.iter())
|
.filter_map(MounterItem::path)
|
||||||
.filter_map(|item| item.path())
|
.collect();
|
||||||
.collect();
|
if !mounter_paths.is_empty() {
|
||||||
if !mounter_paths.is_empty() {
|
for item in &mut items {
|
||||||
for item in &mut items {
|
item.is_mount_point =
|
||||||
item.is_mount_point =
|
item.path_opt().is_some_and(|p| mounter_paths.contains(p));
|
||||||
item.path_opt().is_some_and(|p| mounter_paths.contains(p));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cosmic::action::app(Message::TabRescan(
|
|
||||||
location,
|
|
||||||
parent_item_opt,
|
|
||||||
items,
|
|
||||||
selection_paths,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
log::warn!("failed to rescan: {}", err);
|
|
||||||
cosmic::action::none()
|
|
||||||
}
|
}
|
||||||
|
cosmic::action::app(Message::TabRescan(
|
||||||
|
location,
|
||||||
|
parent_item_opt,
|
||||||
|
items,
|
||||||
|
selection_paths,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
},
|
Err(err) => {
|
||||||
|x| x,
|
log::warn!("failed to rescan: {err}");
|
||||||
)
|
cosmic::action::none()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn search_get(&self) -> Option<&str> {
|
fn search_get(&self) -> Option<&str> {
|
||||||
|
|
@ -724,7 +721,7 @@ impl App {
|
||||||
Some(term) => self.tab.location.path_opt().map(|path| {
|
Some(term) => self.tab.location.path_opt().map(|path| {
|
||||||
(
|
(
|
||||||
Location::Search(
|
Location::Search(
|
||||||
path.to_path_buf(),
|
path.clone(),
|
||||||
term,
|
term,
|
||||||
self.tab.config.show_hidden,
|
self.tab.config.show_hidden,
|
||||||
Instant::now(),
|
Instant::now(),
|
||||||
|
|
@ -733,7 +730,7 @@ impl App {
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
None => match &self.tab.location {
|
None => match &self.tab.location {
|
||||||
Location::Search(path, ..) => Some((Location::Path(path.to_path_buf()), false)),
|
Location::Search(path, ..) => Some((Location::Path(path.clone()), false)),
|
||||||
_ => None,
|
_ => None,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -763,24 +760,21 @@ impl App {
|
||||||
fn with_dialog_config<F: Fn(&mut DialogConfig)>(&mut self, f: F) -> Task<Message> {
|
fn with_dialog_config<F: Fn(&mut DialogConfig)>(&mut self, f: F) -> Task<Message> {
|
||||||
let mut dialog = self.flags.config.dialog;
|
let mut dialog = self.flags.config.dialog;
|
||||||
f(&mut dialog);
|
f(&mut dialog);
|
||||||
if dialog != self.flags.config.dialog {
|
if dialog == self.flags.config.dialog {
|
||||||
match &self.flags.config_handler {
|
Task::none()
|
||||||
Some(config_handler) => {
|
} else {
|
||||||
match self.flags.config.set_dialog(config_handler, dialog) {
|
if let Some(config_handler) = &self.flags.config_handler {
|
||||||
Ok(_) => {}
|
match self.flags.config.set_dialog(config_handler, dialog) {
|
||||||
Err(err) => {
|
Ok(_) => {}
|
||||||
log::warn!("failed to save config \"dialog\": {}", err);
|
Err(err) => {
|
||||||
}
|
log::warn!("failed to save config \"dialog\": {err}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
} else {
|
||||||
self.flags.config.dialog = dialog;
|
self.flags.config.dialog = dialog;
|
||||||
log::warn!("failed to save config \"dialog\": no config handler",);
|
log::warn!("failed to save config \"dialog\": no config handler",);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
self.update_config()
|
self.update_config()
|
||||||
} else {
|
|
||||||
Task::none()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -788,8 +782,7 @@ impl App {
|
||||||
let nav_bar_id = self.nav_model.iter().find(|&id| {
|
let nav_bar_id = self.nav_model.iter().find(|&id| {
|
||||||
self.nav_model
|
self.nav_model
|
||||||
.data::<Location>(id)
|
.data::<Location>(id)
|
||||||
.map(|l| l == location)
|
.is_some_and(|l| l == location)
|
||||||
.unwrap_or_default()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(id) = nav_bar_id {
|
if let Some(id) = nav_bar_id {
|
||||||
|
|
@ -809,7 +802,7 @@ impl App {
|
||||||
.data(Location::Recents)
|
.data(Location::Recents)
|
||||||
});
|
});
|
||||||
|
|
||||||
for favorite in self.flags.config.favorites.iter() {
|
for favorite in &self.flags.config.favorites {
|
||||||
if let Some(path) = favorite.path_opt() {
|
if let Some(path) = favorite.path_opt() {
|
||||||
let name = if matches!(favorite, Favorite::Home) {
|
let name = if matches!(favorite, Favorite::Home) {
|
||||||
fl!("home")
|
fl!("home")
|
||||||
|
|
@ -837,19 +830,17 @@ impl App {
|
||||||
|
|
||||||
// Collect all mounter items
|
// Collect all mounter items
|
||||||
let mut nav_items = Vec::new();
|
let mut nav_items = Vec::new();
|
||||||
for (key, items) in self.mounter_items.iter() {
|
for (key, items) in &self.mounter_items {
|
||||||
for item in items.iter() {
|
nav_items.extend(items.iter().map(|item| (*key, item)));
|
||||||
nav_items.push((*key, item));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Sort by name lexically
|
// Sort by name lexically
|
||||||
nav_items.sort_by(|a, b| LANGUAGE_SORTER.compare(&a.1.name(), &b.1.name()));
|
nav_items.sort_unstable_by(|a, b| LANGUAGE_SORTER.compare(&a.1.name(), &b.1.name()));
|
||||||
// Add items to nav model
|
// Add items to nav model
|
||||||
for (i, (key, item)) in nav_items.into_iter().enumerate() {
|
for (i, (key, item)) in nav_items.into_iter().enumerate() {
|
||||||
nav_model = nav_model.insert(|mut b| {
|
nav_model = nav_model.insert(|mut b| {
|
||||||
b = b.text(item.name()).data(MounterData(key, item.clone()));
|
b = b.text(item.name()).data(MounterData(key, item.clone()));
|
||||||
if let Some(path) = item.path() {
|
if let Some(path) = item.path() {
|
||||||
b = b.data(Location::Path(path.clone()));
|
b = b.data(Location::Path(path));
|
||||||
}
|
}
|
||||||
if let Some(icon) = item.icon(true) {
|
if let Some(icon) = item.icon(true) {
|
||||||
b = b.icon(widget::icon::icon(icon).size(16));
|
b = b.icon(widget::icon::icon(icon).size(16));
|
||||||
|
|
@ -878,33 +869,33 @@ impl App {
|
||||||
if let Some((mut watcher, old_paths)) = self.watcher_opt.take() {
|
if let Some((mut watcher, old_paths)) = self.watcher_opt.take() {
|
||||||
let mut new_paths = FxHashSet::default();
|
let mut new_paths = FxHashSet::default();
|
||||||
if let Some(path) = &self.tab.location.path_opt() {
|
if let Some(path) = &self.tab.location.path_opt() {
|
||||||
new_paths.insert(path.to_path_buf());
|
new_paths.insert((*path).clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unwatch paths no longer used
|
// Unwatch paths no longer used
|
||||||
for path in old_paths.iter() {
|
for path in &old_paths {
|
||||||
if !new_paths.contains(path) {
|
if !new_paths.contains(path) {
|
||||||
match watcher.unwatch(path) {
|
match watcher.unwatch(path) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
log::debug!("unwatching {:?}", path);
|
log::debug!("unwatching {}", path.display());
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::debug!("failed to unwatch {:?}: {}", path, err);
|
log::debug!("failed to unwatch {}: {}", path.display(), err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watch new paths
|
// Watch new paths
|
||||||
for path in new_paths.iter() {
|
for path in &new_paths {
|
||||||
if !old_paths.contains(path) {
|
if !old_paths.contains(path) {
|
||||||
//TODO: should this be recursive?
|
//TODO: should this be recursive?
|
||||||
match watcher.watch(path, notify::RecursiveMode::NonRecursive) {
|
match watcher.watch(path, notify::RecursiveMode::NonRecursive) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
log::debug!("watching {:?}", path);
|
log::debug!("watching {}", path.display());
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::debug!("failed to watch {:?}: {}", path, err);
|
log::debug!("failed to watch {}: {}", path.display(), err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -951,7 +942,7 @@ impl Application for App {
|
||||||
let accept_label = flags.kind.accept_label();
|
let accept_label = flags.kind.accept_label();
|
||||||
|
|
||||||
let location = Location::Path(match &flags.path_opt {
|
let location = Location::Path(match &flags.path_opt {
|
||||||
Some(path) => path.to_path_buf(),
|
Some(path) => path.clone(),
|
||||||
None => match env::current_dir() {
|
None => match env::current_dir() {
|
||||||
Ok(path) => path,
|
Ok(path) => path,
|
||||||
Err(_) => home_dir(),
|
Err(_) => home_dir(),
|
||||||
|
|
@ -972,7 +963,7 @@ impl Application for App {
|
||||||
|
|
||||||
let key_binds = key_binds(&tab.mode);
|
let key_binds = key_binds(&tab.mode);
|
||||||
|
|
||||||
let mut app = App {
|
let mut app = Self {
|
||||||
core,
|
core,
|
||||||
flags,
|
flags,
|
||||||
title,
|
title,
|
||||||
|
|
@ -1015,16 +1006,16 @@ impl Application for App {
|
||||||
ContextPage::Preview(_, kind) => {
|
ContextPage::Preview(_, kind) => {
|
||||||
let mut actions = Vec::with_capacity(3);
|
let mut actions = Vec::with_capacity(3);
|
||||||
if let Some(items) = self.tab.items_opt() {
|
if let Some(items) = self.tab.items_opt() {
|
||||||
for item in items.iter() {
|
for item in items {
|
||||||
if item.selected {
|
if item.selected {
|
||||||
actions.extend(
|
actions.extend(
|
||||||
item.preview_header()
|
item.preview_header()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|element| element.map(Message::TabMessage)),
|
.map(|element| element.map(Message::TabMessage)),
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
Some(
|
Some(
|
||||||
context_drawer::context_drawer(
|
context_drawer::context_drawer(
|
||||||
self.preview(kind).map(Message::TabMessage),
|
self.preview(kind).map(Message::TabMessage),
|
||||||
|
|
@ -1043,7 +1034,7 @@ impl Application for App {
|
||||||
//TODO: should gallery view just be a dialog?
|
//TODO: should gallery view just be a dialog?
|
||||||
if self.tab.gallery {
|
if self.tab.gallery {
|
||||||
return Some(
|
return Some(
|
||||||
widget::column::with_children(vec![
|
widget::column::with_children([
|
||||||
self.tab.gallery_view().map(Message::TabMessage),
|
self.tab.gallery_view().map(Message::TabMessage),
|
||||||
// Draw button row as part of the overlay
|
// Draw button row as part of the overlay
|
||||||
widget::container(self.button_view())
|
widget::container(self.button_view())
|
||||||
|
|
@ -1101,7 +1092,7 @@ impl Application for App {
|
||||||
widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel),
|
widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel),
|
||||||
)
|
)
|
||||||
.control(
|
.control(
|
||||||
widget::column::with_children(vec![
|
widget::column::with_children([
|
||||||
widget::text::body(fl!("folder-name")).into(),
|
widget::text::body(fl!("folder-name")).into(),
|
||||||
widget::text_input("", name.as_str())
|
widget::text_input("", name.as_str())
|
||||||
.id(self.dialog_text_input.clone())
|
.id(self.dialog_text_input.clone())
|
||||||
|
|
@ -1111,9 +1102,7 @@ impl Application for App {
|
||||||
name,
|
name,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.on_submit_maybe(
|
.on_submit_maybe(complete_maybe.map(|maybe| move |_| maybe.clone()))
|
||||||
complete_maybe.clone().map(|maybe| move |_| maybe.clone()),
|
|
||||||
)
|
|
||||||
.into(),
|
.into(),
|
||||||
])
|
])
|
||||||
.spacing(space_xxs),
|
.spacing(space_xxs),
|
||||||
|
|
@ -1237,7 +1226,7 @@ impl Application for App {
|
||||||
if let Some(mounter) = MOUNTERS.get(&data.0) {
|
if let Some(mounter) = MOUNTERS.get(&data.0) {
|
||||||
return mounter
|
return mounter
|
||||||
.mount(data.1.clone())
|
.mount(data.1.clone())
|
||||||
.map(|_| cosmic::action::none());
|
.map(|()| cosmic::action::none());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
|
|
@ -1328,12 +1317,12 @@ impl Application for App {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
// cd to directory
|
// cd to directory
|
||||||
let message = Message::TabMessage(tab::Message::Location(
|
let message = Message::TabMessage(tab::Message::Location(
|
||||||
Location::Path(path.clone()),
|
Location::Path(path),
|
||||||
));
|
));
|
||||||
return self.update(message);
|
return self.update(message);
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!("failed to create {:?}: {}", path, err);
|
log::warn!("failed to create {}: {}", path.display(), err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1366,7 +1355,7 @@ impl Application for App {
|
||||||
return self.rescan_tab(None);
|
return self.rescan_tab(None);
|
||||||
}
|
}
|
||||||
Message::Key(modifiers, key, text) => {
|
Message::Key(modifiers, key, text) => {
|
||||||
for (key_bind, action) in self.key_binds.iter() {
|
for (key_bind, action) in &self.key_binds {
|
||||||
if key_bind.matches(modifiers, &key) {
|
if key_bind.matches(modifiers, &key) {
|
||||||
return self.update(Message::from(action.message()));
|
return self.update(Message::from(action.message()));
|
||||||
}
|
}
|
||||||
|
|
@ -1397,10 +1386,9 @@ impl Application for App {
|
||||||
return self.search_set(Some(term));
|
return self.search_set(Some(term));
|
||||||
}
|
}
|
||||||
TypeToSearch::EnterPath => {
|
TypeToSearch::EnterPath => {
|
||||||
let location = self.tab.edit_location.as_ref().map_or_else(
|
let location = (self.tab.edit_location)
|
||||||
|| self.tab.location.clone(),
|
.as_ref()
|
||||||
|x| x.location.clone(),
|
.map_or_else(|| &self.tab.location, |x| &x.location);
|
||||||
);
|
|
||||||
// Try to add text to end of location
|
// Try to add text to end of location
|
||||||
if let Some(path) = location.path_opt() {
|
if let Some(path) = location.path_opt() {
|
||||||
let mut path_string = path.to_string_lossy().to_string();
|
let mut path_string = path.to_string_lossy().to_string();
|
||||||
|
|
@ -1423,11 +1411,11 @@ impl Application for App {
|
||||||
// Check for unmounted folders
|
// Check for unmounted folders
|
||||||
let mut unmounted = Vec::new();
|
let mut unmounted = Vec::new();
|
||||||
if let Some(old_items) = self.mounter_items.get(&mounter_key) {
|
if let Some(old_items) = self.mounter_items.get(&mounter_key) {
|
||||||
for old_item in old_items.iter() {
|
for old_item in old_items {
|
||||||
if let Some(old_path) = old_item.path() {
|
if let Some(old_path) = old_item.path() {
|
||||||
if old_item.is_mounted() {
|
if old_item.is_mounted() {
|
||||||
let mut still_mounted = false;
|
let mut still_mounted = false;
|
||||||
for item in mounter_items.iter() {
|
for item in &mounter_items {
|
||||||
if let Some(path) = item.path() {
|
if let Some(path) = item.path() {
|
||||||
if path == old_path && item.is_mounted() {
|
if path == old_path && item.is_mounted() {
|
||||||
still_mounted = true;
|
still_mounted = true;
|
||||||
|
|
@ -1466,61 +1454,56 @@ impl Application for App {
|
||||||
Message::NewFolder => {
|
Message::NewFolder => {
|
||||||
if let Some(path) = self.tab.location.path_opt() {
|
if let Some(path) = self.tab.location.path_opt() {
|
||||||
self.dialog_pages.push_back(DialogPage::NewFolder {
|
self.dialog_pages.push_back(DialogPage::NewFolder {
|
||||||
parent: path.to_path_buf(),
|
parent: path.clone(),
|
||||||
name: String::new(),
|
name: String::new(),
|
||||||
});
|
});
|
||||||
return widget::text_input::focus(self.dialog_text_input.clone());
|
return widget::text_input::focus(self.dialog_text_input.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::NotifyEvents(events) => {
|
Message::NotifyEvents(events) => {
|
||||||
log::debug!("{:?}", events);
|
log::debug!("{events:?}");
|
||||||
|
|
||||||
if let Some(path) = self.tab.location.path_opt() {
|
if let Some(path) = self.tab.location.path_opt() {
|
||||||
let mut contains_change = false;
|
let mut contains_change = false;
|
||||||
for event in events.iter() {
|
for event in &events {
|
||||||
for event_path in event.paths.iter() {
|
for event_path in &event.paths {
|
||||||
if event_path.starts_with(path) {
|
if event_path.starts_with(path) {
|
||||||
match event.kind {
|
if let notify::EventKind::Modify(
|
||||||
notify::EventKind::Modify(
|
notify::event::ModifyKind::Metadata(_)
|
||||||
notify::event::ModifyKind::Metadata(_),
|
| notify::event::ModifyKind::Data(_),
|
||||||
)
|
) = event.kind
|
||||||
| notify::EventKind::Modify(notify::event::ModifyKind::Data(
|
{
|
||||||
_,
|
// If metadata or data changed, find the matching item and reload it
|
||||||
)) => {
|
//TODO: this could be further optimized by looking at what exactly changed
|
||||||
// If metadata or data changed, find the matching item and reload it
|
if let Some(items) = &mut self.tab.items_opt {
|
||||||
//TODO: this could be further optimized by looking at what exactly changed
|
for item in items.iter_mut() {
|
||||||
if let Some(items) = &mut self.tab.items_opt {
|
if item.path_opt() == Some(event_path) {
|
||||||
for item in items.iter_mut() {
|
//TODO: reload more, like mime types?
|
||||||
if item.path_opt() == Some(event_path) {
|
match fs::metadata(event_path) {
|
||||||
//TODO: reload more, like mime types?
|
Ok(new_metadata) => {
|
||||||
match fs::metadata(event_path) {
|
if let ItemMetadata::Path {
|
||||||
Ok(new_metadata) => {
|
metadata, ..
|
||||||
if let ItemMetadata::Path {
|
} = &mut item.metadata
|
||||||
metadata,
|
{
|
||||||
..
|
*metadata = new_metadata;
|
||||||
} = &mut item.metadata
|
|
||||||
{
|
|
||||||
*metadata = new_metadata;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
log::warn!(
|
|
||||||
"failed to reload metadata for {:?}: {}",
|
|
||||||
path,
|
|
||||||
err
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//TODO item.thumbnail_opt =
|
Err(err) => {
|
||||||
|
log::warn!(
|
||||||
|
"failed to reload metadata for {}: {}",
|
||||||
|
path.display(),
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
//TODO item.thumbnail_opt =
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
} else {
|
||||||
// Any other events reload the whole tab
|
// Any other events reload the whole tab
|
||||||
contains_change = true;
|
contains_change = true;
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1543,13 +1526,13 @@ impl Application for App {
|
||||||
Message::Open => {
|
Message::Open => {
|
||||||
let mut paths = Vec::new();
|
let mut paths = Vec::new();
|
||||||
if let Some(items) = self.tab.items_opt() {
|
if let Some(items) = self.tab.items_opt() {
|
||||||
for item in items.iter() {
|
for item in items {
|
||||||
if item.selected {
|
if item.selected {
|
||||||
if let Some(path) = item.path_opt() {
|
if let Some(path) = item.path_opt() {
|
||||||
paths.push(path.clone());
|
paths.push(path.clone());
|
||||||
let _ = update_recently_used(
|
let _ = update_recently_used(
|
||||||
&path.clone(),
|
path,
|
||||||
App::APP_ID.to_string(),
|
Self::APP_ID.to_string(),
|
||||||
"cosmic-files".to_string(),
|
"cosmic-files".to_string(),
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
|
|
@ -1560,7 +1543,7 @@ impl Application for App {
|
||||||
|
|
||||||
// Ensure selection is allowed
|
// Ensure selection is allowed
|
||||||
//TODO: improve tab logic so this doesn't block the open button so often
|
//TODO: improve tab logic so this doesn't block the open button so often
|
||||||
for path in paths.iter() {
|
for path in &paths {
|
||||||
let path_is_dir = path.is_dir();
|
let path_is_dir = path.is_dir();
|
||||||
if path_is_dir != self.flags.kind.is_dir() {
|
if path_is_dir != self.flags.kind.is_dir() {
|
||||||
if path_is_dir && paths.len() == 1 {
|
if path_is_dir && paths.len() == 1 {
|
||||||
|
|
@ -1569,10 +1552,10 @@ impl Application for App {
|
||||||
Location::Path(path.clone()),
|
Location::Path(path.clone()),
|
||||||
));
|
));
|
||||||
return self.update(message);
|
return self.update(message);
|
||||||
} else {
|
|
||||||
// Otherwise, this is not a legal selection
|
|
||||||
return Task::none();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Otherwise, this is not a legal selection
|
||||||
|
return Task::none();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1604,7 +1587,7 @@ impl Application for App {
|
||||||
if path.is_dir() {
|
if path.is_dir() {
|
||||||
// cd to directory
|
// cd to directory
|
||||||
let message = Message::TabMessage(tab::Message::Location(
|
let message = Message::TabMessage(tab::Message::Location(
|
||||||
Location::Path(path.clone()),
|
Location::Path(path),
|
||||||
));
|
));
|
||||||
return self.update(message);
|
return self.update(message);
|
||||||
} else if !replace && path.exists() {
|
} else if !replace && path.exists() {
|
||||||
|
|
@ -1612,17 +1595,16 @@ impl Application for App {
|
||||||
filename: filename.clone(),
|
filename: filename.clone(),
|
||||||
});
|
});
|
||||||
return widget::button::focus(REPLACE_BUTTON_ID.clone());
|
return widget::button::focus(REPLACE_BUTTON_ID.clone());
|
||||||
} else {
|
|
||||||
self.result_opt = Some(DialogResult::Open(vec![path]));
|
|
||||||
return window::close(self.flags.window_id);
|
|
||||||
}
|
}
|
||||||
|
self.result_opt = Some(DialogResult::Open(vec![path]));
|
||||||
|
return window::close(self.flags.window_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::ScrollTab(scroll_speed) => {
|
Message::ScrollTab(scroll_speed) => {
|
||||||
return self.update(Message::TabMessage(tab::Message::ScrollTab(
|
return self.update(Message::TabMessage(tab::Message::ScrollTab(
|
||||||
(scroll_speed as f32) / 10.0,
|
f32::from(scroll_speed) / 10.0,
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
Message::SearchActivate => {
|
Message::SearchActivate => {
|
||||||
|
|
@ -1652,7 +1634,7 @@ impl Application for App {
|
||||||
if let Some(items) = self.tab.items_opt() {
|
if let Some(items) = self.tab.items_opt() {
|
||||||
if let Some(item) = items.get(click_i) {
|
if let Some(item) = items.get(click_i) {
|
||||||
if item.selected && !item.metadata.is_dir() {
|
if item.selected && !item.metadata.is_dir() {
|
||||||
*filename = item.name.clone();
|
filename.clone_from(&item.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1689,7 +1671,7 @@ impl Application for App {
|
||||||
let autosize_id = widget::Id::unique();
|
let autosize_id = widget::Id::unique();
|
||||||
commands.push(self.update(Message::Surface(
|
commands.push(self.update(Message::Surface(
|
||||||
cosmic::surface::action::app_popup(
|
cosmic::surface::action::app_popup(
|
||||||
move |app: &mut App| -> SctkPopupSettings {
|
move |app: &mut Self| -> SctkPopupSettings {
|
||||||
let anchor_rect = Rectangle {
|
let anchor_rect = Rectangle {
|
||||||
x: point.x as i32,
|
x: point.x as i32,
|
||||||
y: point.y as i32,
|
y: point.y as i32,
|
||||||
|
|
@ -1715,7 +1697,7 @@ impl Application for App {
|
||||||
input_zone: None,
|
input_zone: None,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Some(Box::new(move |app: &App| {
|
Some(Box::new(move |app: &Self| {
|
||||||
widget::autosize::autosize(
|
widget::autosize::autosize(
|
||||||
menu::context_menu(
|
menu::context_menu(
|
||||||
&app.tab,
|
&app.tab,
|
||||||
|
|
@ -1786,67 +1768,37 @@ impl Application for App {
|
||||||
// Filter
|
// Filter
|
||||||
if let Some(filter_i) = self.filter_selected {
|
if let Some(filter_i) = self.filter_selected {
|
||||||
if let Some(filter) = self.filters.get(filter_i) {
|
if let Some(filter) = self.filters.get(filter_i) {
|
||||||
// Parse filters
|
// Parse globs (Mime implements PartialEq with &str, so no need to parse)
|
||||||
let mut parsed_globs = Vec::new();
|
let mut parsed_globs = Vec::new();
|
||||||
let mut parsed_mimes = Vec::new();
|
let mut mimes = Vec::new();
|
||||||
for pattern in filter.patterns.iter() {
|
for pattern in &filter.patterns {
|
||||||
match pattern {
|
match pattern {
|
||||||
DialogFilterPattern::Glob(value) => {
|
DialogFilterPattern::Glob(value) => {
|
||||||
match glob::Pattern::new(value) {
|
match glob::Pattern::new(value) {
|
||||||
Ok(glob) => parsed_globs.push(glob),
|
Ok(glob) => parsed_globs.push(glob),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!(
|
log::warn!("failed to parse glob {value:?}: {err}");
|
||||||
"failed to parse glob {:?}: {}",
|
|
||||||
value,
|
|
||||||
err
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DialogFilterPattern::Mime(value) => {
|
|
||||||
match mime_guess::Mime::from_str(value) {
|
|
||||||
Ok(mime) => parsed_mimes.push(mime),
|
|
||||||
Err(err) => {
|
|
||||||
log::warn!(
|
|
||||||
"failed to parse mime {:?}: {}",
|
|
||||||
value,
|
|
||||||
err
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
DialogFilterPattern::Mime(value) => mimes.push(value.as_str()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
items.retain(|item| {
|
items.retain(|item| {
|
||||||
if item.metadata.is_dir() {
|
// Directories are always shown
|
||||||
// Directories are always shown
|
item.metadata.is_dir()
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for mime type match (first because it is faster)
|
// Check for mime type match (first because it is faster)
|
||||||
for mime in parsed_mimes.iter() {
|
|| mimes.iter().copied().any(|mime| mime == item.mime)
|
||||||
if mime == &item.mime {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for glob match (last because it is slower)
|
// Check for glob match (last because it is slower)
|
||||||
for glob in parsed_globs.iter() {
|
|| parsed_globs.iter().any(|glob| glob.matches(&item.name))
|
||||||
if glob.matches(&item.name) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// No filters matched
|
|
||||||
false
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Select based on filename
|
// Select based on filename
|
||||||
if let DialogKind::SaveFile { filename } = &self.flags.kind {
|
if let DialogKind::SaveFile { filename } = &self.flags.kind {
|
||||||
for item in items.iter_mut() {
|
for item in &mut items {
|
||||||
item.selected = &item.name == filename;
|
item.selected = &item.name == filename;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1864,9 +1816,8 @@ impl Application for App {
|
||||||
// Reset focus on location change
|
// Reset focus on location change
|
||||||
if self.search_get().is_some() {
|
if self.search_get().is_some() {
|
||||||
return widget::text_input::focus(self.search_id.clone());
|
return widget::text_input::focus(self.search_id.clone());
|
||||||
} else {
|
|
||||||
return widget::text_input::focus(self.filename_id.clone());
|
|
||||||
}
|
}
|
||||||
|
return widget::text_input::focus(self.filename_id.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::TabView(view) => {
|
Message::TabView(view) => {
|
||||||
|
|
@ -1889,47 +1840,18 @@ impl Application for App {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Message::ZoomDefault => {
|
Message::ZoomDefault => {
|
||||||
return self.with_dialog_config(|config| match config.view {
|
return self.with_dialog_config(|config| {
|
||||||
tab::View::List => config.icon_sizes.list = 100.try_into().unwrap(),
|
zoom_to_default(config.view, &mut config.icon_sizes);
|
||||||
tab::View::Grid => config.icon_sizes.grid = 100.try_into().unwrap(),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Message::ZoomIn => {
|
Message::ZoomIn => {
|
||||||
let zoom_in = |size: &mut NonZeroU16, min: u16, max: u16| {
|
return self.with_dialog_config(|config| {
|
||||||
let mut step = min;
|
zoom_in_view(config.view, &mut config.icon_sizes);
|
||||||
while step <= max {
|
|
||||||
if size.get() < step {
|
|
||||||
*size = step.try_into().unwrap();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
step += 25;
|
|
||||||
}
|
|
||||||
if size.get() > step {
|
|
||||||
*size = step.try_into().unwrap();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return self.with_dialog_config(|config| match config.view {
|
|
||||||
tab::View::List => zoom_in(&mut config.icon_sizes.list, 50, 500),
|
|
||||||
tab::View::Grid => zoom_in(&mut config.icon_sizes.grid, 50, 500),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Message::ZoomOut => {
|
Message::ZoomOut => {
|
||||||
let zoom_out = |size: &mut NonZeroU16, min: u16, max: u16| {
|
return self.with_dialog_config(|config| {
|
||||||
let mut step = max;
|
zoom_out_view(config.view, &mut config.icon_sizes);
|
||||||
while step >= min {
|
|
||||||
if size.get() > step {
|
|
||||||
*size = step.try_into().unwrap();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
step -= 25;
|
|
||||||
}
|
|
||||||
if size.get() < step {
|
|
||||||
*size = step.try_into().unwrap();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return self.with_dialog_config(|config| match config.view {
|
|
||||||
tab::View::List => zoom_out(&mut config.icon_sizes.list, 50, 500),
|
|
||||||
tab::View::Grid => zoom_out(&mut config.icon_sizes.grid, 50, 500),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Message::Surface(action) => {
|
Message::Surface(action) => {
|
||||||
|
|
@ -1959,7 +1881,7 @@ impl Application for App {
|
||||||
.on_input(Message::SearchInput),
|
.on_input(Message::SearchInput),
|
||||||
)
|
)
|
||||||
.padding(space_xxs),
|
.padding(space_xxs),
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2055,15 +1977,14 @@ impl Application for App {
|
||||||
Ok(()) => {}
|
Ok(()) => {}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
"failed to send notify events: {:?}",
|
"failed to send notify events: {err:?}"
|
||||||
err
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!("failed to watch files: {:?}", err);
|
log::warn!("failed to watch files: {err:?}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -2080,12 +2001,12 @@ impl Application for App {
|
||||||
{
|
{
|
||||||
Ok(()) => {}
|
Ok(()) => {}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!("failed to send notify watcher: {:?}", err);
|
log::warn!("failed to send notify watcher: {err:?}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!("failed to create file watcher: {:?}", err);
|
log::warn!("failed to create file watcher: {err:?}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2111,19 +2032,19 @@ impl Application for App {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (key, mounter) in MOUNTERS.iter() {
|
subscriptions.extend(MOUNTERS.iter().map(|(key, mounter)| {
|
||||||
subscriptions.push(
|
mounter
|
||||||
mounter.subscription().with(*key).map(
|
.subscription()
|
||||||
|(key, mounter_message)| match mounter_message {
|
.with(*key)
|
||||||
MounterMessage::Items(items) => Message::MounterItems(key, items),
|
.map(|(key, mounter_message)| {
|
||||||
_ => {
|
if let MounterMessage::Items(items) = mounter_message {
|
||||||
log::warn!("{:?} not supported in dialog mode", mounter_message);
|
Message::MounterItems(key, items)
|
||||||
Message::None
|
} else {
|
||||||
}
|
log::warn!("{mounter_message:?} not supported in dialog mode");
|
||||||
},
|
Message::None
|
||||||
),
|
}
|
||||||
);
|
})
|
||||||
}
|
}));
|
||||||
|
|
||||||
Subscription::batch(subscriptions)
|
Subscription::batch(subscriptions)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
46
src/lib.rs
46
src/lib.rs
|
|
@ -21,6 +21,7 @@ mod mouse_area;
|
||||||
pub mod operation;
|
pub mod operation;
|
||||||
mod spawn_detached;
|
mod spawn_detached;
|
||||||
use tab::Location;
|
use tab::Location;
|
||||||
|
mod zoom;
|
||||||
|
|
||||||
use crate::config::State;
|
use crate::config::State;
|
||||||
pub mod tab;
|
pub mod tab;
|
||||||
|
|
@ -34,24 +35,28 @@ pub(crate) fn err_str<T: ToString>(err: T) -> String {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn desktop_dir() -> PathBuf {
|
pub fn desktop_dir() -> PathBuf {
|
||||||
match dirs::desktop_dir() {
|
if let Some(path) = dirs::desktop_dir() {
|
||||||
Some(path) => path,
|
path
|
||||||
None => {
|
} else {
|
||||||
let path = home_dir().join("Desktop");
|
let path = home_dir().join("Desktop");
|
||||||
log::warn!("failed to locate desktop directory, falling back to {path:?}");
|
log::warn!(
|
||||||
path
|
"failed to locate desktop directory, falling back to {}",
|
||||||
}
|
path.display()
|
||||||
|
);
|
||||||
|
path
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn home_dir() -> PathBuf {
|
pub fn home_dir() -> PathBuf {
|
||||||
match dirs::home_dir() {
|
if let Some(home) = dirs::home_dir() {
|
||||||
Some(home) => home,
|
home
|
||||||
None => {
|
} else {
|
||||||
let path = PathBuf::from("/");
|
let path = PathBuf::from("/");
|
||||||
log::warn!("failed to locate home directory, falling back to {path:?}");
|
log::warn!(
|
||||||
path
|
"failed to locate home directory, falling back to {}",
|
||||||
}
|
path.display()
|
||||||
|
);
|
||||||
|
path
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -123,12 +128,9 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
} else {
|
} else {
|
||||||
//TODO: support more URLs
|
//TODO: support more URLs
|
||||||
let path = match url::Url::parse(&arg) {
|
let path = match url::Url::parse(&arg) {
|
||||||
Ok(url) if url.scheme() == "file" => match url.to_file_path() {
|
Ok(url) if url.scheme() == "file" => if let Ok(path) = url.to_file_path() { path } else {
|
||||||
Ok(path) => path,
|
log::warn!("invalid argument {arg:?}");
|
||||||
Err(()) => {
|
continue;
|
||||||
log::warn!("invalid argument {:?}", arg);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
Ok(url) => {
|
Ok(url) => {
|
||||||
uris.push(url);
|
uris.push(url);
|
||||||
|
|
@ -139,7 +141,7 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
match fs::canonicalize(&path) {
|
match fs::canonicalize(&path) {
|
||||||
Ok(absolute) => Location::Path(absolute),
|
Ok(absolute) => Location::Path(absolute),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!("failed to canonicalize {:?}: {}", path, err);
|
log::warn!("failed to canonicalize {}: {}", path.display(), err);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -153,7 +155,7 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
Ok(fork::Fork::Child) => (),
|
Ok(fork::Fork::Child) => (),
|
||||||
Ok(fork::Fork::Parent(_child_pid)) => process::exit(0),
|
Ok(fork::Fork::Parent(_child_pid)) => process::exit(0),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("failed to daemonize: {:?}", err);
|
eprintln!("failed to daemonize: {err:?}");
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,6 @@ pub fn localize() {
|
||||||
let requested_languages = i18n_embed::DesktopLanguageRequester::requested_languages();
|
let requested_languages = i18n_embed::DesktopLanguageRequester::requested_languages();
|
||||||
|
|
||||||
if let Err(error) = localizer.select(&requested_languages) {
|
if let Err(error) = localizer.select(&requested_languages) {
|
||||||
eprintln!("Error while loading language for COSMIC Files {}", error);
|
eprintln!("Error while loading language for COSMIC Files {error}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
37
src/menu.rs
37
src/menu.rs
|
|
@ -32,7 +32,7 @@ macro_rules! menu_button {
|
||||||
($($x:expr),+ $(,)?) => (
|
($($x:expr),+ $(,)?) => (
|
||||||
button::custom(
|
button::custom(
|
||||||
Row::with_children(
|
Row::with_children(
|
||||||
vec![$(Element::from($x)),+]
|
[$(Element::from($x)),+]
|
||||||
)
|
)
|
||||||
.height(Length::Fixed(24.0))
|
.height(Length::Fixed(24.0))
|
||||||
.align_y(Alignment::Center)
|
.align_y(Alignment::Center)
|
||||||
|
|
@ -43,7 +43,7 @@ macro_rules! menu_button {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn menu_button_optional(
|
const fn menu_button_optional(
|
||||||
label: String,
|
label: String,
|
||||||
action: Action,
|
action: Action,
|
||||||
enabled: bool,
|
enabled: bool,
|
||||||
|
|
@ -61,7 +61,7 @@ pub fn context_menu<'a>(
|
||||||
modifiers: &Modifiers,
|
modifiers: &Modifiers,
|
||||||
) -> Element<'a, tab::Message> {
|
) -> Element<'a, tab::Message> {
|
||||||
let find_key = |action: &Action| -> String {
|
let find_key = |action: &Action| -> String {
|
||||||
for (key_bind, key_action) in key_binds.iter() {
|
for (key_bind, key_action) in key_binds {
|
||||||
if action == key_action {
|
if action == key_action {
|
||||||
return key_bind.to_string();
|
return key_bind.to_string();
|
||||||
}
|
}
|
||||||
|
|
@ -110,11 +110,11 @@ pub fn context_menu<'a>(
|
||||||
let mut selected_types: Vec<Mime> = vec![];
|
let mut selected_types: Vec<Mime> = vec![];
|
||||||
let mut selected_mount_point = 0;
|
let mut selected_mount_point = 0;
|
||||||
if let Some(items) = tab.items_opt() {
|
if let Some(items) = tab.items_opt() {
|
||||||
for item in items.iter() {
|
for item in items {
|
||||||
if item.selected {
|
if item.selected {
|
||||||
selected += 1;
|
selected += 1;
|
||||||
if item.metadata.is_dir() {
|
if item.metadata.is_dir() {
|
||||||
selected_mount_point += item.is_mount_point as i32;
|
selected_mount_point += i32::from(item.is_mount_point);
|
||||||
selected_dir += 1;
|
selected_dir += 1;
|
||||||
}
|
}
|
||||||
match &item.location_opt {
|
match &item.location_opt {
|
||||||
|
|
@ -131,7 +131,7 @@ pub fn context_menu<'a>(
|
||||||
selected_types.push(item.mime.clone());
|
selected_types.push(item.mime.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
selected_types.sort_unstable();
|
selected_types.sort_unstable();
|
||||||
selected_types.dedup();
|
selected_types.dedup();
|
||||||
selected_trash_only = selected_trash_only && selected == 1;
|
selected_trash_only = selected_trash_only && selected == 1;
|
||||||
|
|
@ -167,9 +167,9 @@ pub fn context_menu<'a>(
|
||||||
children.push(menu_item(fl!("open"), Action::Open).into());
|
children.push(menu_item(fl!("open"), Action::Open).into());
|
||||||
#[cfg(feature = "desktop")]
|
#[cfg(feature = "desktop")]
|
||||||
{
|
{
|
||||||
for (i, action) in entry.desktop_actions.into_iter().enumerate() {
|
children.extend(entry.desktop_actions.into_iter().enumerate().map(
|
||||||
children.push(menu_item(action.name, Action::ExecEntryAction(i)).into())
|
|(i, action)| menu_item(action.name, Action::ExecEntryAction(i)).into(),
|
||||||
}
|
));
|
||||||
}
|
}
|
||||||
children.push(divider::horizontal::light().into());
|
children.push(divider::horizontal::light().into());
|
||||||
children.push(menu_item(fl!("rename"), Action::Rename).into());
|
children.push(menu_item(fl!("rename"), Action::Rename).into());
|
||||||
|
|
@ -207,11 +207,8 @@ pub fn context_menu<'a>(
|
||||||
children.push(menu_item(fl!("copy"), Action::Copy).into());
|
children.push(menu_item(fl!("copy"), Action::Copy).into());
|
||||||
|
|
||||||
children.push(divider::horizontal::light().into());
|
children.push(divider::horizontal::light().into());
|
||||||
let supported_archive_types = crate::archive::SUPPORTED_ARCHIVE_TYPES
|
let supported_archive_types = crate::archive::SUPPORTED_ARCHIVE_TYPES;
|
||||||
.iter()
|
selected_types.retain(|t| supported_archive_types.iter().copied().all(|m| *t != m));
|
||||||
.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() {
|
if selected_types.is_empty() {
|
||||||
children.push(menu_item(fl!("extract-here"), Action::ExtractHere).into());
|
children.push(menu_item(fl!("extract-here"), Action::ExtractHere).into());
|
||||||
children.push(menu_item(fl!("extract-to"), Action::ExtractTo).into());
|
children.push(menu_item(fl!("extract-to"), Action::ExtractTo).into());
|
||||||
|
|
@ -396,12 +393,12 @@ pub fn dialog_menu(
|
||||||
|
|
||||||
let mut selected_gallery = 0;
|
let mut selected_gallery = 0;
|
||||||
if let Some(items) = tab.items_opt() {
|
if let Some(items) = tab.items_opt() {
|
||||||
for item in items.iter() {
|
for item in items {
|
||||||
if item.selected && item.can_gallery() {
|
if item.selected && item.can_gallery() {
|
||||||
selected_gallery += 1;
|
selected_gallery += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
MenuBar::new(vec![
|
MenuBar::new(vec![
|
||||||
menu::Tree::with_children(
|
menu::Tree::with_children(
|
||||||
|
|
@ -530,7 +527,7 @@ pub fn menu_bar<'a>(
|
||||||
modifiers: &Modifiers,
|
modifiers: &Modifiers,
|
||||||
key_binds: &HashMap<KeyBind, Action>,
|
key_binds: &HashMap<KeyBind, Action>,
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
let sort_options = tab_opt.map(|tab| tab.sort_options());
|
let sort_options = tab_opt.map(Tab::sort_options);
|
||||||
let sort_item = |label, sort, dir| {
|
let sort_item = |label, sort, dir| {
|
||||||
menu::Item::CheckBox(
|
menu::Item::CheckBox(
|
||||||
label,
|
label,
|
||||||
|
|
@ -547,7 +544,7 @@ pub fn menu_bar<'a>(
|
||||||
let mut selected = 0;
|
let mut selected = 0;
|
||||||
let mut selected_gallery = 0;
|
let mut selected_gallery = 0;
|
||||||
if let Some(items) = tab_opt.and_then(|tab| tab.items_opt()) {
|
if let Some(items) = tab_opt.and_then(|tab| tab.items_opt()) {
|
||||||
for item in items.iter() {
|
for item in items {
|
||||||
if item.selected {
|
if item.selected {
|
||||||
selected += 1;
|
selected += 1;
|
||||||
if item.metadata.is_dir() {
|
if item.metadata.is_dir() {
|
||||||
|
|
@ -558,7 +555,7 @@ pub fn menu_bar<'a>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
let (delete_item, delete_item_action) = if in_trash || modifiers.shift() {
|
let (delete_item, delete_item_action) = if in_trash || modifiers.shift() {
|
||||||
(fl!("delete-permanently"), Action::Delete)
|
(fl!("delete-permanently"), Action::Delete)
|
||||||
|
|
@ -719,7 +716,7 @@ pub fn menu_bar<'a>(
|
||||||
|
|
||||||
pub fn location_context_menu<'a>(ancestor_index: usize) -> Element<'a, tab::Message> {
|
pub fn location_context_menu<'a>(ancestor_index: usize) -> Element<'a, tab::Message> {
|
||||||
//TODO: only add some of these when in App mode
|
//TODO: only add some of these when in App mode
|
||||||
let children = vec![
|
let children = [
|
||||||
menu_button!(text::body(fl!("open-in-new-tab")))
|
menu_button!(text::body(fl!("open-in-new-tab")))
|
||||||
.on_press(tab::Message::LocationMenuAction(
|
.on_press(tab::Message::LocationMenuAction(
|
||||||
LocationMenuAction::OpenInNewTab(ancestor_index),
|
LocationMenuAction::OpenInNewTab(ancestor_index),
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ pub fn exec_to_command(
|
||||||
// Number of args before the field code.
|
// Number of args before the field code.
|
||||||
// This won't be an off by one err below because take is not zero indexed.
|
// This won't be an off by one err below because take is not zero indexed.
|
||||||
let field_code_pos = field_code_pos.unwrap_or_default();
|
let field_code_pos = field_code_pos.unwrap_or_default();
|
||||||
let mut processes = match args_handler.map(|s| s.as_str()) {
|
let mut processes = match args_handler.map(String::as_str) {
|
||||||
Some("%f") => {
|
Some("%f") => {
|
||||||
let mut processes = Vec::with_capacity(path_opt.len());
|
let mut processes = Vec::with_capacity(path_opt.len());
|
||||||
|
|
||||||
|
|
@ -83,7 +83,7 @@ pub fn exec_to_command(
|
||||||
for invalid in path_opt
|
for invalid in path_opt
|
||||||
.iter()
|
.iter()
|
||||||
.map(AsRef::as_ref)
|
.map(AsRef::as_ref)
|
||||||
.filter(|path| from_file_or_dir(path).is_none())
|
.filter(|&path| from_file_or_dir(path).is_none())
|
||||||
{
|
{
|
||||||
log::warn!("Desktop file expects a file path instead of a URL: {invalid:?}");
|
log::warn!("Desktop file expects a file path instead of a URL: {invalid:?}");
|
||||||
}
|
}
|
||||||
|
|
@ -137,7 +137,7 @@ pub fn exec_to_command(
|
||||||
if !EXEC_HANDLERS.contains(&field_code)
|
if !EXEC_HANDLERS.contains(&field_code)
|
||||||
&& !DEPRECATED_HANDLERS.contains(&field_code)
|
&& !DEPRECATED_HANDLERS.contains(&field_code)
|
||||||
{
|
{
|
||||||
log::warn!("unsupported Exec code {:?} in {:?}", field_code, exec);
|
log::warn!("unsupported Exec code {field_code:?} in {exec:?}");
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -215,14 +215,13 @@ fn filename_eq(path_opt: &Option<PathBuf>, filename: &str) -> bool {
|
||||||
path_opt
|
path_opt
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|path| path.file_name())
|
.and_then(|path| path.file_name())
|
||||||
.map(|x| x == filename)
|
.is_some_and(|x| x == filename)
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct MimeAppCache {
|
pub struct MimeAppCache {
|
||||||
apps: Vec<MimeApp>,
|
apps: Vec<MimeApp>,
|
||||||
cache: FxHashMap<Mime, Vec<MimeApp>>,
|
cache: FxHashMap<Mime, Vec<MimeApp>>,
|
||||||
icons: FxHashMap<Mime, Vec<widget::icon::Handle>>,
|
icons: FxHashMap<Mime, Box<[widget::icon::Handle]>>,
|
||||||
terminals: Vec<MimeApp>,
|
terminals: Vec<MimeApp>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -258,13 +257,13 @@ impl MimeAppCache {
|
||||||
|
|
||||||
// Load desktop applications by supported mime types
|
// Load desktop applications by supported mime types
|
||||||
//TODO: hashmap for all apps by id?
|
//TODO: hashmap for all apps by id?
|
||||||
let all_apps: Vec<_> = desktop::load_applications(locale, false, None).collect();
|
let all_apps: Box<[_]> = desktop::load_applications(locale, false, None).collect();
|
||||||
for app in all_apps.iter() {
|
for app in &all_apps {
|
||||||
//TODO: just collect apps that can be executed with a file argument?
|
//TODO: just collect apps that can be executed with a file argument?
|
||||||
if !app.mime_types.is_empty() {
|
if !app.mime_types.is_empty() {
|
||||||
self.apps.push(MimeApp::from(app));
|
self.apps.push(MimeApp::from(app));
|
||||||
}
|
}
|
||||||
for mime in app.mime_types.iter() {
|
for mime in &app.mime_types {
|
||||||
let apps = self
|
let apps = self
|
||||||
.cache
|
.cache
|
||||||
.entry(mime.clone())
|
.entry(mime.clone())
|
||||||
|
|
@ -273,7 +272,7 @@ impl MimeAppCache {
|
||||||
apps.push(MimeApp::from(app));
|
apps.push(MimeApp::from(app));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for category in app.categories.iter() {
|
for category in &app.categories {
|
||||||
if category == "TerminalEmulator" {
|
if category == "TerminalEmulator" {
|
||||||
self.terminals.push(MimeApp::from(app));
|
self.terminals.push(MimeApp::from(app));
|
||||||
break;
|
break;
|
||||||
|
|
@ -284,7 +283,7 @@ impl MimeAppCache {
|
||||||
let desktops: Vec<String> = env::var("XDG_CURRENT_DESKTOP")
|
let desktops: Vec<String> = env::var("XDG_CURRENT_DESKTOP")
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.split(':')
|
.split(':')
|
||||||
.map(|x| x.to_ascii_lowercase())
|
.map(str::to_ascii_lowercase)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Load mimeapps.list files
|
// Load mimeapps.list files
|
||||||
|
|
@ -293,21 +292,17 @@ impl MimeAppCache {
|
||||||
let mut mimeapps_paths = Vec::new();
|
let mut mimeapps_paths = Vec::new();
|
||||||
let xdg_dirs = xdg::BaseDirectories::new();
|
let xdg_dirs = xdg::BaseDirectories::new();
|
||||||
|
|
||||||
for path in xdg_dirs.find_data_files("applications/mimeapps.list") {
|
mimeapps_paths.extend(xdg_dirs.find_data_files("applications/mimeapps.list"));
|
||||||
mimeapps_paths.push(path);
|
|
||||||
}
|
|
||||||
for desktop in desktops.iter().rev() {
|
for desktop in desktops.iter().rev() {
|
||||||
for path in xdg_dirs.find_data_files(format!("applications/{desktop}-mimeapps.list")) {
|
mimeapps_paths
|
||||||
mimeapps_paths.push(path);
|
.extend(xdg_dirs.find_data_files(format!("applications/{desktop}-mimeapps.list")));
|
||||||
}
|
|
||||||
}
|
|
||||||
for path in xdg_dirs.find_config_files("mimeapps.list") {
|
|
||||||
mimeapps_paths.push(path);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mimeapps_paths.extend(xdg_dirs.find_config_files("mimeapps.list"));
|
||||||
|
|
||||||
for desktop in desktops.iter().rev() {
|
for desktop in desktops.iter().rev() {
|
||||||
for path in xdg_dirs.find_config_files(format!("{desktop}-mimeapps.list")) {
|
mimeapps_paths.extend(xdg_dirs.find_config_files(format!("{desktop}-mimeapps.list")));
|
||||||
mimeapps_paths.push(path);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: handle directory specific behavior
|
//TODO: handle directory specific behavior
|
||||||
|
|
@ -315,7 +310,7 @@ impl MimeAppCache {
|
||||||
let entry = match freedesktop_entry_parser::parse_entry(&path) {
|
let entry = match freedesktop_entry_parser::parse_entry(&path) {
|
||||||
Ok(ok) => ok,
|
Ok(ok) => ok,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!("failed to parse {:?}: {}", path, err);
|
log::warn!("failed to parse {}: {}", path.display(), err);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -328,21 +323,19 @@ impl MimeAppCache {
|
||||||
if let Ok(mime) = attr.name.parse::<Mime>() {
|
if let Ok(mime) = attr.name.parse::<Mime>() {
|
||||||
if let Some(filenames) = attr.value {
|
if let Some(filenames) = attr.value {
|
||||||
for filename in filenames.split_terminator(';') {
|
for filename in filenames.split_terminator(';') {
|
||||||
log::trace!("add {}={}", mime, filename);
|
log::trace!("add {mime}={filename}");
|
||||||
let apps = self
|
let apps = self
|
||||||
.cache
|
.cache
|
||||||
.entry(mime.clone())
|
.entry(mime.clone())
|
||||||
.or_insert_with(|| Vec::with_capacity(1));
|
.or_insert_with(|| Vec::with_capacity(1));
|
||||||
if !apps.iter().any(|x| filename_eq(&x.path, filename)) {
|
if !apps.iter().any(|x| filename_eq(&x.path, filename)) {
|
||||||
if let Some(app) =
|
if let Some(app) =
|
||||||
all_apps.iter().find(|x| filename_eq(&x.path, filename))
|
all_apps.iter().find(|&x| filename_eq(&x.path, filename))
|
||||||
{
|
{
|
||||||
apps.push(MimeApp::from(app));
|
apps.push(MimeApp::from(app));
|
||||||
} else {
|
} else {
|
||||||
log::info!(
|
log::info!(
|
||||||
"failed to add association for {:?}: application {:?} not found",
|
"failed to add association for {mime:?}: application {filename:?} not found"
|
||||||
mime,
|
|
||||||
filename
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -355,7 +348,7 @@ impl MimeAppCache {
|
||||||
if let Ok(mime) = attr.name.parse::<Mime>() {
|
if let Ok(mime) = attr.name.parse::<Mime>() {
|
||||||
if let Some(filenames) = attr.value {
|
if let Some(filenames) = attr.value {
|
||||||
for filename in filenames.split_terminator(';') {
|
for filename in filenames.split_terminator(';') {
|
||||||
log::trace!("remove {}={}", mime, filename);
|
log::trace!("remove {mime}={filename}");
|
||||||
if let Some(apps) = self.cache.get_mut(&mime) {
|
if let Some(apps) = self.cache.get_mut(&mime) {
|
||||||
apps.retain(|x| !filename_eq(&x.path, filename));
|
apps.retain(|x| !filename_eq(&x.path, filename));
|
||||||
}
|
}
|
||||||
|
|
@ -368,7 +361,7 @@ impl MimeAppCache {
|
||||||
if let Ok(mime) = attr.name.parse::<Mime>() {
|
if let Ok(mime) = attr.name.parse::<Mime>() {
|
||||||
if let Some(filenames) = attr.value {
|
if let Some(filenames) = attr.value {
|
||||||
for filename in filenames.split_terminator(';') {
|
for filename in filenames.split_terminator(';') {
|
||||||
log::trace!("default {}={}", mime, filename);
|
log::trace!("default {mime}={filename}");
|
||||||
if let Some(apps) = self.cache.get_mut(&mime) {
|
if let Some(apps) = self.cache.get_mut(&mime) {
|
||||||
let mut found = false;
|
let mut found = false;
|
||||||
for app in apps.iter_mut() {
|
for app in apps.iter_mut() {
|
||||||
|
|
@ -381,13 +374,10 @@ impl MimeAppCache {
|
||||||
}
|
}
|
||||||
if found {
|
if found {
|
||||||
break;
|
break;
|
||||||
} else {
|
|
||||||
log::debug!(
|
|
||||||
"failed to set default for {:?}: application {:?} not found",
|
|
||||||
mime,
|
|
||||||
filename
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
log::debug!(
|
||||||
|
"failed to set default for {mime:?}: application {filename:?} not found"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -412,15 +402,15 @@ impl MimeAppCache {
|
||||||
|
|
||||||
// Copy icons to special cache
|
// Copy icons to special cache
|
||||||
//TODO: adjust dropdown API so this is no longer needed
|
//TODO: adjust dropdown API so this is no longer needed
|
||||||
for (mime, apps) in self.cache.iter() {
|
self.icons.extend(self.cache.iter().map(|(mime, apps)| {
|
||||||
self.icons.insert(
|
(
|
||||||
mime.clone(),
|
mime.clone(),
|
||||||
apps.iter().map(|app| app.icon.clone()).collect(),
|
apps.iter().map(|app| app.icon.clone()).collect(),
|
||||||
);
|
)
|
||||||
}
|
}));
|
||||||
|
|
||||||
let elapsed = start.elapsed();
|
let elapsed = start.elapsed();
|
||||||
log::info!("loaded mime app cache in {:?}", elapsed);
|
log::info!("loaded mime app cache in {elapsed:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn apps(&self) -> &[MimeApp] {
|
pub fn apps(&self) -> &[MimeApp] {
|
||||||
|
|
@ -428,13 +418,11 @@ impl MimeAppCache {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get(&self, key: &Mime) -> &[MimeApp] {
|
pub fn get(&self, key: &Mime) -> &[MimeApp] {
|
||||||
static EMPTY: Vec<MimeApp> = Vec::new();
|
self.cache.get(key).map_or(&[], Vec::as_slice)
|
||||||
self.cache.get(key).unwrap_or(&EMPTY)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn icons(&self, key: &Mime) -> &[widget::icon::Handle] {
|
pub fn icons(&self, key: &Mime) -> &[widget::icon::Handle] {
|
||||||
static EMPTY: Vec<widget::icon::Handle> = Vec::new();
|
self.icons.get(key).map_or(&[], Box::as_ref)
|
||||||
self.icons.get(key).unwrap_or(&EMPTY)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_default_terminal(&self) -> Option<String> {
|
fn get_default_terminal(&self) -> Option<String> {
|
||||||
|
|
@ -466,7 +454,7 @@ impl MimeAppCache {
|
||||||
}
|
}
|
||||||
|
|
||||||
for id in &preference_order {
|
for id in &preference_order {
|
||||||
for terminal in self.terminals.iter() {
|
for terminal in &self.terminals {
|
||||||
if &terminal.id == id {
|
if &terminal.id == id {
|
||||||
return Some(terminal);
|
return Some(terminal);
|
||||||
}
|
}
|
||||||
|
|
@ -498,7 +486,7 @@ impl MimeAppCache {
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
if err.kind() != io::ErrorKind::NotFound {
|
if err.kind() != io::ErrorKind::NotFound {
|
||||||
log::warn!("failed to read {path:?}: {err}");
|
log::warn!("failed to read {}: {}", path.display(), err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -517,7 +505,7 @@ impl MimeAppCache {
|
||||||
self.reload();
|
self.reload();
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!("failed to write {path:?}: {err}");
|
log::warn!("failed to write {}: {}", path.display(), err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,10 +41,8 @@ impl MimeIconCache {
|
||||||
let icon_name = icon_names.remove(0);
|
let icon_name = icon_names.remove(0);
|
||||||
let mut named = icon::from_name(icon_name).size(key.size);
|
let mut named = icon::from_name(icon_name).size(key.size);
|
||||||
if !icon_names.is_empty() {
|
if !icon_names.is_empty() {
|
||||||
let mut fallback_names = Vec::with_capacity(icon_names.len());
|
let fallback_names =
|
||||||
for fallback_name in icon_names {
|
icon_names.into_iter().map(std::borrow::Cow::from).collect();
|
||||||
fallback_names.push(fallback_name.into());
|
|
||||||
}
|
|
||||||
named = named.fallback(Some(icon::IconFallback::Names(fallback_names)));
|
named = named.fallback(Some(icon::IconFallback::Names(fallback_names)));
|
||||||
}
|
}
|
||||||
Some(named.handle())
|
Some(named.handle())
|
||||||
|
|
@ -55,8 +53,8 @@ impl MimeIconCache {
|
||||||
static MIME_ICON_CACHE: LazyLock<Mutex<MimeIconCache>> =
|
static MIME_ICON_CACHE: LazyLock<Mutex<MimeIconCache>> =
|
||||||
LazyLock::new(|| Mutex::new(MimeIconCache::new()));
|
LazyLock::new(|| Mutex::new(MimeIconCache::new()));
|
||||||
|
|
||||||
pub fn mime_for_path<P: AsRef<Path>>(
|
pub fn mime_for_path(
|
||||||
path: P,
|
path: impl AsRef<Path>,
|
||||||
metadata_opt: Option<&fs::Metadata>,
|
metadata_opt: Option<&fs::Metadata>,
|
||||||
remote: bool,
|
remote: bool,
|
||||||
) -> Mime {
|
) -> Mime {
|
||||||
|
|
@ -65,7 +63,7 @@ pub fn mime_for_path<P: AsRef<Path>>(
|
||||||
// Try the shared mime info cache first
|
// Try the shared mime info cache first
|
||||||
let mut gb = mime_icon_cache.shared_mime_info.guess_mime_type();
|
let mut gb = mime_icon_cache.shared_mime_info.guess_mime_type();
|
||||||
if remote {
|
if remote {
|
||||||
if let Some(file_name) = path.file_name().and_then(|x| x.to_str()) {
|
if let Some(file_name) = path.file_name().and_then(std::ffi::OsStr::to_str) {
|
||||||
gb.file_name(file_name);
|
gb.file_name(file_name);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -75,11 +73,22 @@ pub fn mime_for_path<P: AsRef<Path>>(
|
||||||
gb.metadata(metadata.clone());
|
gb.metadata(metadata.clone());
|
||||||
}
|
}
|
||||||
let guess = gb.guess();
|
let guess = gb.guess();
|
||||||
if guess.uncertain() {
|
let guessed_mime = guess.mime_type();
|
||||||
|
|
||||||
|
/// Checks if the `Mime` is a special variant returned by `xdg-mime`.
|
||||||
|
/// This includes directories, symlinks and zerosize files, which are returned as uncertain.
|
||||||
|
fn is_special_mime(mime: &Mime) -> bool {
|
||||||
|
*mime == "inode/directory" || *mime == "inode/symlink" || *mime == "application/x-zerosize"
|
||||||
|
}
|
||||||
|
|
||||||
|
// `xdg-mime-rs` sets the guess to uncertain if it returns special mime types.
|
||||||
|
// The guess could also be uncertain on platforms without shared-mime-info.
|
||||||
|
// Try mime_guess, but only if it is not one of the special mime types.
|
||||||
|
if guess.uncertain() && (remote || !is_special_mime(guessed_mime)) {
|
||||||
// If uncertain, try mime_guess. This could happen on platforms without shared-mime-info
|
// If uncertain, try mime_guess. This could happen on platforms without shared-mime-info
|
||||||
mime_guess::from_path(path).first_or_octet_stream()
|
mime_guess::from_path(path).first_or_octet_stream()
|
||||||
} else {
|
} else {
|
||||||
guess.mime_type().clone()
|
guessed_mime.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,45 +30,48 @@ fn gio_icon_to_path(icon: &gio::Icon, size: u16) -> Option<PathBuf> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn items(monitor: &gio::VolumeMonitor, sizes: IconSizes) -> MounterItems {
|
fn items(monitor: &gio::VolumeMonitor, sizes: IconSizes) -> MounterItems {
|
||||||
let mut items = MounterItems::new();
|
let mut items: MounterItems = (monitor.mounts().into_iter())
|
||||||
for (i, mount) in monitor.mounts().into_iter().enumerate() {
|
.enumerate()
|
||||||
items.push(MounterItem::Gvfs(Item {
|
.map(|(i, mount)| {
|
||||||
uri: MountExt::root(&mount).uri().to_string(),
|
MounterItem::Gvfs(Item {
|
||||||
kind: ItemKind::Mount,
|
uri: mount.root().uri().into(),
|
||||||
index: i,
|
kind: ItemKind::Mount,
|
||||||
name: MountExt::name(&mount).to_string(),
|
index: i,
|
||||||
is_mounted: true,
|
name: mount.name().into(),
|
||||||
icon_opt: gio_icon_to_path(&MountExt::icon(&mount), sizes.grid()),
|
is_mounted: true,
|
||||||
icon_symbolic_opt: gio_icon_to_path(&MountExt::symbolic_icon(&mount), 16),
|
icon_opt: gio_icon_to_path(&MountExt::icon(&mount), sizes.grid()),
|
||||||
path_opt: MountExt::root(&mount).path(),
|
icon_symbolic_opt: gio_icon_to_path(&MountExt::symbolic_icon(&mount), 16),
|
||||||
}));
|
path_opt: MountExt::root(&mount).path(),
|
||||||
}
|
})
|
||||||
for (i, volume) in monitor.volumes().into_iter().enumerate() {
|
})
|
||||||
if volume.get_mount().is_some() {
|
.collect();
|
||||||
|
items.extend(
|
||||||
|
(monitor.volumes().into_iter())
|
||||||
|
.enumerate()
|
||||||
// Volumes with mounts are already listed by mount
|
// Volumes with mounts are already listed by mount
|
||||||
continue;
|
.filter(|(_, volume)| volume.get_mount().is_none())
|
||||||
}
|
.map(|(i, volume)| {
|
||||||
let uri = VolumeExt::activation_root(&volume)
|
let uri = VolumeExt::activation_root(&volume)
|
||||||
.map(|f| f.uri().to_string())
|
.map(|f| f.uri().into())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
items.push(MounterItem::Gvfs(Item {
|
MounterItem::Gvfs(Item {
|
||||||
// TODO can we get URI for volumes with no mount?
|
// TODO can we get URI for volumes with no mount?
|
||||||
uri,
|
uri,
|
||||||
kind: ItemKind::Volume,
|
kind: ItemKind::Volume,
|
||||||
index: i,
|
index: i,
|
||||||
name: VolumeExt::name(&volume).to_string(),
|
name: volume.name().into(),
|
||||||
is_mounted: false,
|
is_mounted: false,
|
||||||
icon_opt: gio_icon_to_path(&VolumeExt::icon(&volume), sizes.grid()),
|
icon_opt: gio_icon_to_path(&VolumeExt::icon(&volume), sizes.grid()),
|
||||||
icon_symbolic_opt: gio_icon_to_path(&VolumeExt::symbolic_icon(&volume), 16),
|
icon_symbolic_opt: gio_icon_to_path(&VolumeExt::symbolic_icon(&volume), 16),
|
||||||
path_opt: None,
|
path_opt: None,
|
||||||
}));
|
})
|
||||||
}
|
}),
|
||||||
|
);
|
||||||
items
|
items
|
||||||
}
|
}
|
||||||
|
|
||||||
fn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, String> {
|
fn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, String> {
|
||||||
let mut uri = uri.to_string();
|
let mut file = gio::File::for_uri(uri);
|
||||||
let mut file = gio::File::for_uri(&uri);
|
|
||||||
let force_dir = uri.starts_with("network:///");
|
let force_dir = uri.starts_with("network:///");
|
||||||
|
|
||||||
// Resolve the target-uri if it exists
|
// Resolve the target-uri if it exists
|
||||||
|
|
@ -78,8 +81,7 @@ fn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, String> {
|
||||||
gio::Cancellable::NONE,
|
gio::Cancellable::NONE,
|
||||||
) {
|
) {
|
||||||
if let Some(resolved_uri) = file_info.attribute_as_string(TARGET_URI_ATTRIBUTE) {
|
if let Some(resolved_uri) = file_info.attribute_as_string(TARGET_URI_ATTRIBUTE) {
|
||||||
uri = resolved_uri.to_string();
|
file = gio::File::for_uri(resolved_uri.as_str());
|
||||||
file = gio::File::for_uri(&uri);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -89,10 +91,10 @@ fn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, String> {
|
||||||
.map_err(err_str)?
|
.map_err(err_str)?
|
||||||
{
|
{
|
||||||
let info = info_res.map_err(err_str)?;
|
let info = info_res.map_err(err_str)?;
|
||||||
let name = info.name().to_string_lossy().to_string();
|
let name = info.name().to_string_lossy().into_owned();
|
||||||
let display_name = info.display_name().to_string();
|
let display_name = String::from(info.display_name());
|
||||||
|
|
||||||
let uri = file.child(info.name()).uri().to_string();
|
let uri = String::from(file.child(info.name()).uri());
|
||||||
|
|
||||||
//TODO: what is the best way to resolve shortcuts?
|
//TODO: what is the best way to resolve shortcuts?
|
||||||
let location = Location::Network(uri, display_name.clone(), file.child(&name).path());
|
let location = Location::Network(uri, display_name.clone(), file.child(&name).path());
|
||||||
|
|
@ -100,10 +102,7 @@ fn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, String> {
|
||||||
let metadata = if !force_dir && !info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE) {
|
let metadata = if !force_dir && !info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE) {
|
||||||
let mtime = info.attribute_uint64(gio::FILE_ATTRIBUTE_TIME_MODIFIED);
|
let mtime = info.attribute_uint64(gio::FILE_ATTRIBUTE_TIME_MODIFIED);
|
||||||
let is_dir = matches!(info.file_type(), gio::FileType::Directory);
|
let is_dir = matches!(info.file_type(), gio::FileType::Directory);
|
||||||
let size_opt = match is_dir {
|
let size_opt = (!is_dir).then_some(info.size() as u64);
|
||||||
true => None,
|
|
||||||
false => Some(info.size() as u64),
|
|
||||||
};
|
|
||||||
let mut children_opt = None;
|
let mut children_opt = None;
|
||||||
|
|
||||||
if is_dir {
|
if is_dir {
|
||||||
|
|
@ -114,7 +113,7 @@ fn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, String> {
|
||||||
children_opt = Some(entries.count());
|
children_opt = Some(entries.count());
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!("failed to read directory {:?}: {}", path, err);
|
log::warn!("failed to read directory {}: {}", path.display(), err);
|
||||||
children_opt = Some(0);
|
children_opt = Some(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -188,31 +187,21 @@ fn mount_op(uri: String, event_tx: mpsc::UnboundedSender<Event>) -> gio::MountOp
|
||||||
move |mount_op, message, default_user, default_domain, flags| {
|
move |mount_op, message, default_user, default_domain, flags| {
|
||||||
let auth = MounterAuth {
|
let auth = MounterAuth {
|
||||||
message: message.to_string(),
|
message: message.to_string(),
|
||||||
username_opt: if flags.contains(gio::AskPasswordFlags::NEED_USERNAME) {
|
username_opt: flags
|
||||||
Some(default_user.to_string())
|
.contains(gio::AskPasswordFlags::NEED_USERNAME)
|
||||||
} else {
|
.then(|| default_user.to_string()),
|
||||||
None
|
domain_opt: flags
|
||||||
},
|
.contains(gio::AskPasswordFlags::NEED_DOMAIN)
|
||||||
domain_opt: if flags.contains(gio::AskPasswordFlags::NEED_DOMAIN) {
|
.then(|| default_domain.to_string()),
|
||||||
Some(default_domain.to_string())
|
password_opt: flags
|
||||||
} else {
|
.contains(gio::AskPasswordFlags::NEED_PASSWORD)
|
||||||
None
|
.then(String::new),
|
||||||
},
|
remember_opt: flags
|
||||||
password_opt: if flags.contains(gio::AskPasswordFlags::NEED_PASSWORD) {
|
.contains(gio::AskPasswordFlags::SAVING_SUPPORTED)
|
||||||
Some(String::new())
|
.then_some(false),
|
||||||
} else {
|
anonymous_opt: flags
|
||||||
None
|
.contains(gio::AskPasswordFlags::ANONYMOUS_SUPPORTED)
|
||||||
},
|
.then_some(false),
|
||||||
remember_opt: if flags.contains(gio::AskPasswordFlags::SAVING_SUPPORTED) {
|
|
||||||
Some(false)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
anonymous_opt: if flags.contains(gio::AskPasswordFlags::ANONYMOUS_SUPPORTED) {
|
|
||||||
Some(false)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
let (auth_tx, mut auth_rx) = mpsc::channel(1);
|
let (auth_tx, mut auth_rx) = mpsc::channel(1);
|
||||||
event_tx
|
event_tx
|
||||||
|
|
@ -287,7 +276,7 @@ impl Item {
|
||||||
self.name.clone()
|
self.name.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_mounted(&self) -> bool {
|
pub const fn is_mounted(&self) -> bool {
|
||||||
self.is_mounted
|
self.is_mounted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -395,7 +384,7 @@ impl Gvfs {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
log::info!("mount {}", name);
|
log::info!("mount {name}");
|
||||||
//TODO: do not use name as a URI for mount_op
|
//TODO: do not use name as a URI for mount_op
|
||||||
let mount_op = mount_op(name.to_string(), event_tx.clone());
|
let mount_op = mount_op(name.to_string(), event_tx.clone());
|
||||||
let event_tx = event_tx.clone();
|
let event_tx = event_tx.clone();
|
||||||
|
|
@ -406,7 +395,7 @@ impl Gvfs {
|
||||||
Some(&mount_op),
|
Some(&mount_op),
|
||||||
gio::Cancellable::NONE,
|
gio::Cancellable::NONE,
|
||||||
move |res| {
|
move |res| {
|
||||||
log::info!("mount {}: result {:?}", name, res);
|
log::info!("mount {name}: result {res:?}");
|
||||||
event_tx.send(Event::MountResult(mounter_item, match res {
|
event_tx.send(Event::MountResult(mounter_item, match res {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
_ = complete_tx.send(Ok(()));
|
_ = complete_tx.send(Ok(()));
|
||||||
|
|
@ -416,7 +405,7 @@ impl Gvfs {
|
||||||
_ = complete_tx.send(Err(anyhow::anyhow!("{err:?}")));
|
_ = complete_tx.send(Err(anyhow::anyhow!("{err:?}")));
|
||||||
match err.kind::<gio::IOErrorEnum>() {
|
match err.kind::<gio::IOErrorEnum>() {
|
||||||
Some(gio::IOErrorEnum::FailedHandled) => Ok(false),
|
Some(gio::IOErrorEnum::FailedHandled) => Ok(false),
|
||||||
_ => Err(format!("{}", err))
|
_ => Err(format!("{err}"))
|
||||||
}}
|
}}
|
||||||
})).unwrap();
|
})).unwrap();
|
||||||
},
|
},
|
||||||
|
|
@ -433,7 +422,7 @@ impl Gvfs {
|
||||||
Some(&mount_op),
|
Some(&mount_op),
|
||||||
gio::Cancellable::NONE,
|
gio::Cancellable::NONE,
|
||||||
move |res| {
|
move |res| {
|
||||||
log::info!("network drive {}: result {:?}", uri, res);
|
log::info!("network drive {uri}: result {res:?}");
|
||||||
event_tx.send(Event::NetworkResult(uri, match res {
|
event_tx.send(Event::NetworkResult(uri, match res {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
_ = result_tx.send(Ok(()));
|
_ = result_tx.send(Ok(()));
|
||||||
|
|
@ -442,7 +431,7 @@ impl Gvfs {
|
||||||
_ = result_tx.send(Err(anyhow::anyhow!("{err:?}")));
|
_ = result_tx.send(Err(anyhow::anyhow!("{err:?}")));
|
||||||
match err.kind::<gio::IOErrorEnum>() {
|
match err.kind::<gio::IOErrorEnum>() {
|
||||||
Some(gio::IOErrorEnum::FailedHandled) => Ok(false),
|
Some(gio::IOErrorEnum::FailedHandled) => Ok(false),
|
||||||
_ => Err(format!("{}", err))
|
_ => Err(format!("{err}"))
|
||||||
}}
|
}}
|
||||||
})).unwrap();
|
})).unwrap();
|
||||||
}
|
}
|
||||||
|
|
@ -457,7 +446,7 @@ impl Gvfs {
|
||||||
gio::Cancellable::NONE,
|
gio::Cancellable::NONE,
|
||||||
) {
|
) {
|
||||||
if let Some(resolved_uri) = file_info.attribute_as_string(TARGET_URI_ATTRIBUTE) {
|
if let Some(resolved_uri) = file_info.attribute_as_string(TARGET_URI_ATTRIBUTE) {
|
||||||
uri = resolved_uri.to_string();
|
uri = resolved_uri.into();
|
||||||
file = gio::File::for_uri(&uri);
|
file = gio::File::for_uri(&uri);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -475,7 +464,7 @@ impl Gvfs {
|
||||||
Some(&mount_op),
|
Some(&mount_op),
|
||||||
gio::Cancellable::NONE,
|
gio::Cancellable::NONE,
|
||||||
move |res| {
|
move |res| {
|
||||||
log::info!("network scan mounted {}: result {:?}", uri, res);
|
log::info!("network scan mounted {uri}: result {res:?}");
|
||||||
// FIXME sometimes a uri can be mounted and then not recognized as mounted...
|
// FIXME sometimes a uri can be mounted and then not recognized as mounted...
|
||||||
// seems to be related to uri with a path
|
// seems to be related to uri with a path
|
||||||
items_tx.blocking_send(network_scan(&original_uri, sizes)).unwrap();
|
items_tx.blocking_send(network_scan(&original_uri, sizes)).unwrap();
|
||||||
|
|
@ -485,7 +474,7 @@ impl Gvfs {
|
||||||
},
|
},
|
||||||
Err(err) => match err.kind::<gio::IOErrorEnum>() {
|
Err(err) => match err.kind::<gio::IOErrorEnum>() {
|
||||||
Some(gio::IOErrorEnum::FailedHandled) => Ok(false),
|
Some(gio::IOErrorEnum::FailedHandled) => Ok(false),
|
||||||
_ => Err(format!("{}", err))
|
_ => Err(format!("{err}"))
|
||||||
}
|
}
|
||||||
})).unwrap();
|
})).unwrap();
|
||||||
}
|
}
|
||||||
|
|
@ -509,25 +498,25 @@ impl Gvfs {
|
||||||
}
|
}
|
||||||
|
|
||||||
if MountExt::can_eject(&mount) {
|
if MountExt::can_eject(&mount) {
|
||||||
log::info!("eject {}", name);
|
log::info!("eject {name}");
|
||||||
MountExt::eject_with_operation(
|
MountExt::eject_with_operation(
|
||||||
&mount,
|
&mount,
|
||||||
gio::MountUnmountFlags::NONE,
|
gio::MountUnmountFlags::NONE,
|
||||||
gio::MountOperation::NONE,
|
gio::MountOperation::NONE,
|
||||||
gio::Cancellable::NONE,
|
gio::Cancellable::NONE,
|
||||||
move |result| {
|
move |result| {
|
||||||
log::info!("eject {}: result {:?}", name, result);
|
log::info!("eject {name}: result {result:?}");
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
log::info!("unmount {}", name);
|
log::info!("unmount {name}");
|
||||||
MountExt::unmount_with_operation(
|
MountExt::unmount_with_operation(
|
||||||
&mount,
|
&mount,
|
||||||
gio::MountUnmountFlags::NONE,
|
gio::MountUnmountFlags::NONE,
|
||||||
gio::MountOperation::NONE,
|
gio::MountOperation::NONE,
|
||||||
gio::Cancellable::NONE,
|
gio::Cancellable::NONE,
|
||||||
move |result| {
|
move |result| {
|
||||||
log::info!("unmount {}: result {:?}", name, result);
|
log::info!("unmount {name}: result {result:?}");
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -536,7 +525,7 @@ impl Gvfs {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
main_loop.run()
|
main_loop.run();
|
||||||
});
|
});
|
||||||
Self {
|
Self {
|
||||||
command_tx,
|
command_tx,
|
||||||
|
|
@ -596,12 +585,9 @@ impl Mounter for Gvfs {
|
||||||
|
|
||||||
fn unmount(&self, item: MounterItem) -> Task<()> {
|
fn unmount(&self, item: MounterItem) -> Task<()> {
|
||||||
let command_tx = self.command_tx.clone();
|
let command_tx = self.command_tx.clone();
|
||||||
Task::perform(
|
Task::future(async move {
|
||||||
async move {
|
command_tx.send(Cmd::Unmount(item)).unwrap();
|
||||||
command_tx.send(Cmd::Unmount(item)).unwrap();
|
})
|
||||||
},
|
|
||||||
|x| x,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn subscription(&self) -> Subscription<MounterMessage> {
|
fn subscription(&self) -> Subscription<MounterMessage> {
|
||||||
|
|
@ -615,7 +601,7 @@ impl Mounter for Gvfs {
|
||||||
match event {
|
match event {
|
||||||
Event::Changed => command_tx.send(Cmd::Rescan).unwrap(),
|
Event::Changed => command_tx.send(Cmd::Rescan).unwrap(),
|
||||||
Event::Items(items) => {
|
Event::Items(items) => {
|
||||||
output.send(MounterMessage::Items(items)).await.unwrap()
|
output.send(MounterMessage::Items(items)).await.unwrap();
|
||||||
}
|
}
|
||||||
Event::MountResult(item, res) => output
|
Event::MountResult(item, res) => output
|
||||||
.send(MounterMessage::MountResult(item, res))
|
.send(MounterMessage::MountResult(item, res))
|
||||||
|
|
|
||||||
|
|
@ -199,7 +199,7 @@ impl<'a, Message> MouseArea<'a, Message> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn show_drag_rect(mut self, show_drag_rect: bool) -> Self {
|
pub const fn show_drag_rect(mut self, show_drag_rect: bool) -> Self {
|
||||||
self.show_drag_rect = show_drag_rect;
|
self.show_drag_rect = show_drag_rect;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
@ -379,7 +379,7 @@ where
|
||||||
shell: &mut Shell<'_, Message>,
|
shell: &mut Shell<'_, Message>,
|
||||||
viewport: &Rectangle,
|
viewport: &Rectangle,
|
||||||
) -> event::Status {
|
) -> event::Status {
|
||||||
if let event::Status::Captured = self.content.as_widget_mut().on_event(
|
if self.content.as_widget_mut().on_event(
|
||||||
&mut tree.children[0],
|
&mut tree.children[0],
|
||||||
event.clone(),
|
event.clone(),
|
||||||
layout,
|
layout,
|
||||||
|
|
@ -388,7 +388,8 @@ where
|
||||||
clipboard,
|
clipboard,
|
||||||
shell,
|
shell,
|
||||||
viewport,
|
viewport,
|
||||||
) {
|
) == event::Status::Captured
|
||||||
|
{
|
||||||
return event::Status::Captured;
|
return event::Status::Captured;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -507,7 +508,7 @@ where
|
||||||
Renderer: 'a + renderer::Renderer,
|
Renderer: 'a + renderer::Renderer,
|
||||||
Theme: 'a,
|
Theme: 'a,
|
||||||
{
|
{
|
||||||
fn from(area: MouseArea<'a, Message>) -> Element<'a, Message> {
|
fn from(area: MouseArea<'a, Message>) -> Self {
|
||||||
Element::new(area)
|
Element::new(area)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -538,12 +539,12 @@ fn update<Message: Clone>(
|
||||||
match (position_in, state.last_position) {
|
match (position_in, state.last_position) {
|
||||||
(None, Some(_)) => {
|
(None, Some(_)) => {
|
||||||
if let Some(message) = widget.on_exit.as_ref() {
|
if let Some(message) = widget.on_exit.as_ref() {
|
||||||
shell.publish(message())
|
shell.publish(message());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(Some(_), None) => {
|
(Some(_), None) => {
|
||||||
if let Some(message) = widget.on_enter.as_ref() {
|
if let Some(message) = widget.on_enter.as_ref() {
|
||||||
shell.publish(message())
|
shell.publish(message());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|
@ -632,8 +633,7 @@ fn update<Message: Clone>(
|
||||||
let recent_click = state
|
let recent_click = state
|
||||||
.prev_click
|
.prev_click
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|(_, i)| Instant::now().duration_since(*i) <= DOUBLE_CLICK_DURATION)
|
.is_some_and(|(_, i)| Instant::now().duration_since(*i) <= DOUBLE_CLICK_DURATION);
|
||||||
.unwrap_or_default();
|
|
||||||
if matches!(
|
if matches!(
|
||||||
event,
|
event,
|
||||||
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
|
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
|
||||||
|
|
@ -653,7 +653,10 @@ fn update<Message: Clone>(
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(message) = widget.on_right_press.as_ref() {
|
if let Some(message) = widget.on_right_press.as_ref() {
|
||||||
if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) = event {
|
if matches!(
|
||||||
|
event,
|
||||||
|
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right))
|
||||||
|
) {
|
||||||
let point_opt = if widget.on_right_press_window_position {
|
let point_opt = if widget.on_right_press_window_position {
|
||||||
cursor.position_over(layout_bounds).map(|mut p| {
|
cursor.position_over(layout_bounds).map(|mut p| {
|
||||||
p.x -= offset.x;
|
p.x -= offset.x;
|
||||||
|
|
@ -667,14 +670,16 @@ fn update<Message: Clone>(
|
||||||
|
|
||||||
if widget.on_right_press_no_capture {
|
if widget.on_right_press_no_capture {
|
||||||
return event::Status::Ignored;
|
return event::Status::Ignored;
|
||||||
} else {
|
|
||||||
return event::Status::Captured;
|
|
||||||
}
|
}
|
||||||
|
return event::Status::Captured;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(message) = widget.on_right_release.as_ref() {
|
if let Some(message) = widget.on_right_release.as_ref() {
|
||||||
if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Right)) = event {
|
if matches!(
|
||||||
|
event,
|
||||||
|
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Right))
|
||||||
|
) {
|
||||||
shell.publish(message(cursor.position_in(layout_bounds)));
|
shell.publish(message(cursor.position_in(layout_bounds)));
|
||||||
|
|
||||||
return event::Status::Captured;
|
return event::Status::Captured;
|
||||||
|
|
@ -682,7 +687,10 @@ fn update<Message: Clone>(
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(message) = widget.on_middle_press.as_ref() {
|
if let Some(message) = widget.on_middle_press.as_ref() {
|
||||||
if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)) = event {
|
if matches!(
|
||||||
|
event,
|
||||||
|
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle))
|
||||||
|
) {
|
||||||
shell.publish(message(cursor.position_in(layout_bounds)));
|
shell.publish(message(cursor.position_in(layout_bounds)));
|
||||||
|
|
||||||
return event::Status::Captured;
|
return event::Status::Captured;
|
||||||
|
|
@ -690,7 +698,10 @@ fn update<Message: Clone>(
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(message) = widget.on_middle_release.as_ref() {
|
if let Some(message) = widget.on_middle_release.as_ref() {
|
||||||
if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Middle)) = event {
|
if matches!(
|
||||||
|
event,
|
||||||
|
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Middle))
|
||||||
|
) {
|
||||||
shell.publish(message(cursor.position_in(layout_bounds)));
|
shell.publish(message(cursor.position_in(layout_bounds)));
|
||||||
|
|
||||||
return event::Status::Captured;
|
return event::Status::Captured;
|
||||||
|
|
@ -698,7 +709,10 @@ fn update<Message: Clone>(
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(message) = widget.on_back_press.as_ref() {
|
if let Some(message) = widget.on_back_press.as_ref() {
|
||||||
if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Back)) = event {
|
if matches!(
|
||||||
|
event,
|
||||||
|
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Back))
|
||||||
|
) {
|
||||||
shell.publish(message(cursor.position_in(layout_bounds)));
|
shell.publish(message(cursor.position_in(layout_bounds)));
|
||||||
|
|
||||||
return event::Status::Captured;
|
return event::Status::Captured;
|
||||||
|
|
@ -706,7 +720,10 @@ fn update<Message: Clone>(
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(message) = widget.on_back_release.as_ref() {
|
if let Some(message) = widget.on_back_release.as_ref() {
|
||||||
if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Back)) = event {
|
if matches!(
|
||||||
|
event,
|
||||||
|
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Back))
|
||||||
|
) {
|
||||||
shell.publish(message(cursor.position_in(layout_bounds)));
|
shell.publish(message(cursor.position_in(layout_bounds)));
|
||||||
|
|
||||||
return event::Status::Captured;
|
return event::Status::Captured;
|
||||||
|
|
@ -714,7 +731,10 @@ fn update<Message: Clone>(
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(message) = widget.on_forward_press.as_ref() {
|
if let Some(message) = widget.on_forward_press.as_ref() {
|
||||||
if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Forward)) = event {
|
if matches!(
|
||||||
|
event,
|
||||||
|
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Forward))
|
||||||
|
) {
|
||||||
shell.publish(message(cursor.position_in(layout_bounds)));
|
shell.publish(message(cursor.position_in(layout_bounds)));
|
||||||
|
|
||||||
return event::Status::Captured;
|
return event::Status::Captured;
|
||||||
|
|
@ -722,7 +742,10 @@ fn update<Message: Clone>(
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(message) = widget.on_forward_release.as_ref() {
|
if let Some(message) = widget.on_forward_release.as_ref() {
|
||||||
if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Forward)) = event {
|
if matches!(
|
||||||
|
event,
|
||||||
|
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Forward))
|
||||||
|
) {
|
||||||
shell.publish(message(cursor.position_in(layout_bounds)));
|
shell.publish(message(cursor.position_in(layout_bounds)));
|
||||||
|
|
||||||
return event::Status::Captured;
|
return event::Status::Captured;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use tokio::sync::Notify;
|
use tokio::sync::Notify;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub enum ControllerState {
|
pub enum ControllerState {
|
||||||
Cancelled,
|
Cancelled,
|
||||||
Failed,
|
Failed,
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ async fn handle_replace(
|
||||||
let item_from = match tab::item_from_path(file_from, IconSizes::default()) {
|
let item_from = match tab::item_from_path(file_from, IconSizes::default()) {
|
||||||
Ok(ok) => ok,
|
Ok(ok) => ok,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!("{}", err);
|
log::warn!("{err}");
|
||||||
return ReplaceResult::Cancel;
|
return ReplaceResult::Cancel;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -44,7 +44,7 @@ async fn handle_replace(
|
||||||
let item_to = match tab::item_from_path(file_to, IconSizes::default()) {
|
let item_to = match tab::item_from_path(file_to, IconSizes::default()) {
|
||||||
Ok(ok) => ok,
|
Ok(ok) => ok,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!("{}", err);
|
log::warn!("{err}");
|
||||||
return ReplaceResult::Cancel;
|
return ReplaceResult::Cancel;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -98,13 +98,13 @@ async fn copy_or_move(
|
||||||
compio::runtime::spawn(async move {
|
compio::runtime::spawn(async move {
|
||||||
let controller = controller_c;
|
let controller = controller_c;
|
||||||
log::info!(
|
log::info!(
|
||||||
"{} {:?} to {:?}",
|
"{} {:?} to {}",
|
||||||
match method {
|
match method {
|
||||||
Method::Copy => "Copy",
|
Method::Copy => "Copy",
|
||||||
Method::Move { .. } => "Move",
|
Method::Move { .. } => "Move",
|
||||||
},
|
},
|
||||||
paths,
|
paths,
|
||||||
to
|
to.display()
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handle duplicate file names by renaming paths
|
// Handle duplicate file names by renaming paths
|
||||||
|
|
@ -141,12 +141,15 @@ async fn copy_or_move(
|
||||||
//TODO: use compio::fs::rename?
|
//TODO: use compio::fs::rename?
|
||||||
match fs::rename(from, to) {
|
match fs::rename(from, to) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
log::info!("renamed {from:?} to {to:?}");
|
log::info!("renamed {} to {}", from.display(), to.display());
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::info!(
|
log::info!(
|
||||||
"failed to rename {from:?} to {to:?}, fallback to recursive move: {err}"
|
"failed to rename {} to {}, fallback to recursive move: {}",
|
||||||
|
from.display(),
|
||||||
|
to.display(),
|
||||||
|
err
|
||||||
);
|
);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
@ -217,8 +220,9 @@ fn copy_unique_path(from: &Path, to: &Path) -> PathBuf {
|
||||||
let file_name = file_name.to_string();
|
let file_name = file_name.to_string();
|
||||||
COMPOUND_EXTENSIONS
|
COMPOUND_EXTENSIONS
|
||||||
.iter()
|
.iter()
|
||||||
.find(|&&ext| file_name.ends_with(ext))
|
.copied()
|
||||||
.map(|&ext| {
|
.find(|&ext| file_name.ends_with(ext))
|
||||||
|
.map(|ext| {
|
||||||
(
|
(
|
||||||
file_name.strip_suffix(ext).unwrap().to_string(),
|
file_name.strip_suffix(ext).unwrap().to_string(),
|
||||||
Some(ext[1..].to_string()),
|
Some(ext[1..].to_string()),
|
||||||
|
|
@ -227,15 +231,14 @@ fn copy_unique_path(from: &Path, to: &Path) -> PathBuf {
|
||||||
.unwrap_or_else(|| {
|
.unwrap_or_else(|| {
|
||||||
from.file_stem()
|
from.file_stem()
|
||||||
.and_then(|s| s.to_str())
|
.and_then(|s| s.to_str())
|
||||||
.map(|stem| {
|
.map_or((file_name, None), |stem| {
|
||||||
(
|
(
|
||||||
stem.to_string(),
|
stem.to_string(),
|
||||||
from.extension()
|
from.extension()
|
||||||
.and_then(|e| e.to_str())
|
.and_then(|e| e.to_str())
|
||||||
.map(|e| e.to_string()),
|
.map(str::to_string),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.unwrap_or((file_name, None))
|
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -249,7 +252,7 @@ fn copy_unique_path(from: &Path, to: &Path) -> PathBuf {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
to = to.join(&new_name);
|
to.push(&new_name);
|
||||||
|
|
||||||
if !matches!(to.try_exists(), Ok(true)) {
|
if !matches!(to.try_exists(), Ok(true)) {
|
||||||
break;
|
break;
|
||||||
|
|
@ -283,7 +286,7 @@ fn paths_parent_name(paths: &[PathBuf]) -> Cow<'_, str> {
|
||||||
return fl!("unknown-folder").into();
|
return fl!("unknown-folder").into();
|
||||||
};
|
};
|
||||||
|
|
||||||
for path in paths.iter() {
|
for path in paths {
|
||||||
//TODO: is it possible to have different parents, and what should be returned?
|
//TODO: is it possible to have different parents, and what should be returned?
|
||||||
if path.parent() != Some(parent) {
|
if path.parent() != Some(parent) {
|
||||||
return fl!("unknown-folder").into();
|
return fl!("unknown-folder").into();
|
||||||
|
|
@ -327,7 +330,7 @@ pub enum Operation {
|
||||||
EmptyTrash,
|
EmptyTrash,
|
||||||
/// Uncompress files
|
/// Uncompress files
|
||||||
Extract {
|
Extract {
|
||||||
paths: Vec<PathBuf>,
|
paths: Box<[PathBuf]>,
|
||||||
to: PathBuf,
|
to: PathBuf,
|
||||||
password: Option<String>,
|
password: Option<String>,
|
||||||
},
|
},
|
||||||
|
|
@ -345,10 +348,10 @@ pub enum Operation {
|
||||||
},
|
},
|
||||||
/// Permanently delete items, skipping the trash
|
/// Permanently delete items, skipping the trash
|
||||||
PermanentlyDelete {
|
PermanentlyDelete {
|
||||||
paths: Vec<PathBuf>,
|
paths: Box<[PathBuf]>,
|
||||||
},
|
},
|
||||||
RemoveFromRecents {
|
RemoveFromRecents {
|
||||||
paths: Vec<PathBuf>,
|
paths: Box<[PathBuf]>,
|
||||||
},
|
},
|
||||||
Rename {
|
Rename {
|
||||||
from: PathBuf,
|
from: PathBuf,
|
||||||
|
|
@ -397,18 +400,18 @@ impl OperationError {
|
||||||
pub fn from_err<T: ToString>(err: T, controller: &Controller) -> Self {
|
pub fn from_err<T: ToString>(err: T, controller: &Controller) -> Self {
|
||||||
controller.set_state(ControllerState::Failed);
|
controller.set_state(ControllerState::Failed);
|
||||||
|
|
||||||
OperationError {
|
Self {
|
||||||
kind: OperationErrorType::Generic(err.to_string()),
|
kind: OperationErrorType::Generic(err.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_kind(kind: OperationErrorType, controller: &Controller) -> Self {
|
pub fn from_kind(kind: OperationErrorType, controller: &Controller) -> Self {
|
||||||
controller.set_state(ControllerState::Failed);
|
controller.set_state(ControllerState::Failed);
|
||||||
OperationError { kind }
|
Self { kind }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_msg(m: impl Into<String>) -> Self {
|
pub fn from_msg(m: impl Into<String>) -> Self {
|
||||||
OperationError {
|
Self {
|
||||||
kind: OperationErrorType::Generic(m.into()),
|
kind: OperationErrorType::Generic(m.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -572,7 +575,7 @@ impl Operation {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn show_progress_notification(&self) -> bool {
|
pub const fn show_progress_notification(&self) -> bool {
|
||||||
// Long running operations show a progress notification
|
// Long running operations show a progress notification
|
||||||
match self {
|
match self {
|
||||||
Self::Compress { .. }
|
Self::Compress { .. }
|
||||||
|
|
@ -625,7 +628,7 @@ impl Operation {
|
||||||
let controller = controller_c;
|
let controller = controller_c;
|
||||||
let Some(relative_root) = to.parent() else {
|
let Some(relative_root) = to.parent() else {
|
||||||
return Err(OperationError::from_err(
|
return Err(OperationError::from_err(
|
||||||
format!("path {:?} has no parent directory", to),
|
format!("path {} has no parent directory", to.display()),
|
||||||
&controller,
|
&controller,
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
|
@ -636,7 +639,7 @@ impl Operation {
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut paths = paths;
|
let mut paths = paths;
|
||||||
for path in paths.clone().iter() {
|
for path in &paths.clone() {
|
||||||
if path.is_dir() {
|
if path.is_dir() {
|
||||||
let new_paths_it = WalkDir::new(path).into_iter();
|
let new_paths_it = WalkDir::new(path).into_iter();
|
||||||
for entry in new_paths_it.skip(1) {
|
for entry in new_paths_it.skip(1) {
|
||||||
|
|
@ -1011,7 +1014,7 @@ impl Operation {
|
||||||
}
|
}
|
||||||
Self::RemoveFromRecents { paths } => {
|
Self::RemoveFromRecents { paths } => {
|
||||||
tokio::task::spawn_blocking(move || {
|
tokio::task::spawn_blocking(move || {
|
||||||
let path_refs = paths.iter().map(|p| p.as_ref()).collect::<Vec<&Path>>();
|
let path_refs = paths.iter().map(PathBuf::as_path).collect::<Box<[_]>>();
|
||||||
recently_used_xbel::remove_recently_used(&path_refs)
|
recently_used_xbel::remove_recently_used(&path_refs)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
|
|
@ -1205,7 +1208,7 @@ mod tests {
|
||||||
debug!("[{id}] Replace request");
|
debug!("[{id}] Replace request");
|
||||||
tx.send(ReplaceResult::Cancel)
|
tx.send(ReplaceResult::Cancel)
|
||||||
.await
|
.await
|
||||||
.expect("Sending a response to a replace request should succeed")
|
.expect("Sending a response to a replace request should succeed");
|
||||||
}
|
}
|
||||||
_ => unreachable!(
|
_ => unreachable!(
|
||||||
"Only [ `Message::PendingProgress`, `Message::DialogPush(DialogPage::Replace)` ] are sent from operation"
|
"Only [ `Message::PendingProgress`, `Message::DialogPush(DialogPage::Replace)` ] are sent from operation"
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ impl Context {
|
||||||
|
|
||||||
pub async fn recursive_copy_or_move(
|
pub async fn recursive_copy_or_move(
|
||||||
&mut self,
|
&mut self,
|
||||||
from_to_pairs: Vec<(PathBuf, PathBuf)>,
|
from_to_pairs: impl IntoIterator<Item = (PathBuf, PathBuf)>,
|
||||||
method: Method,
|
method: Method,
|
||||||
) -> Result<bool, OperationError> {
|
) -> Result<bool, OperationError> {
|
||||||
let mut ops = Vec::new();
|
let mut ops = Vec::new();
|
||||||
|
|
@ -68,7 +68,7 @@ impl Context {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
for entry in WalkDir::new(&from_parent).into_iter() {
|
for entry in WalkDir::new(&from_parent) {
|
||||||
self.controller
|
self.controller
|
||||||
.check()
|
.check()
|
||||||
.await
|
.await
|
||||||
|
|
@ -76,7 +76,11 @@ impl Context {
|
||||||
|
|
||||||
let entry = entry.map_err(|err| {
|
let entry = entry.map_err(|err| {
|
||||||
OperationError::from_err(
|
OperationError::from_err(
|
||||||
format!("failed to walk directory {:?}: {}", from_parent, err),
|
format!(
|
||||||
|
"failed to walk directory {}: {}",
|
||||||
|
from_parent.display(),
|
||||||
|
err
|
||||||
|
),
|
||||||
&self.controller,
|
&self.controller,
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
@ -92,7 +96,7 @@ impl Context {
|
||||||
} else if file_type.is_symlink() {
|
} else if file_type.is_symlink() {
|
||||||
let target = fs::read_link(&from).map_err(|err| {
|
let target = fs::read_link(&from).map_err(|err| {
|
||||||
OperationError::from_err(
|
OperationError::from_err(
|
||||||
format!("failed to read link {:?}: {}", from, err),
|
format!("failed to read link {}: {}", from_parent.display(), err),
|
||||||
&self.controller,
|
&self.controller,
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
@ -111,8 +115,10 @@ impl Context {
|
||||||
let relative = from.strip_prefix(&from_parent).map_err(|err| {
|
let relative = from.strip_prefix(&from_parent).map_err(|err| {
|
||||||
OperationError::from_err(
|
OperationError::from_err(
|
||||||
format!(
|
format!(
|
||||||
"failed to remove prefix {:?} from {:?}: {}",
|
"failed to remove prefix {} from {}: {}",
|
||||||
from_parent, from, err
|
from_parent.display(),
|
||||||
|
from.display(),
|
||||||
|
err
|
||||||
),
|
),
|
||||||
&self.controller,
|
&self.controller,
|
||||||
)
|
)
|
||||||
|
|
@ -142,9 +148,8 @@ impl Context {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add cleanup ops after standard ops, in reverse
|
// Add cleanup ops after standard ops, in reverse
|
||||||
for cleanup_op in cleanup_ops.into_iter().rev() {
|
cleanup_ops.reverse();
|
||||||
ops.push(cleanup_op);
|
ops.append(&mut cleanup_ops);
|
||||||
}
|
|
||||||
|
|
||||||
let total_ops = ops.len();
|
let total_ops = ops.len();
|
||||||
for (current_ops, mut op) in ops.into_iter().enumerate() {
|
for (current_ops, mut op) in ops.into_iter().enumerate() {
|
||||||
|
|
@ -163,8 +168,11 @@ impl Context {
|
||||||
if op.run(self, progress).await.map_err(|err| {
|
if op.run(self, progress).await.map_err(|err| {
|
||||||
OperationError::from_err(
|
OperationError::from_err(
|
||||||
format!(
|
format!(
|
||||||
"failed to {:?} {:?} to {:?}: {}",
|
"failed to {:?} {} to {}: {}",
|
||||||
op.kind, op.from, op.to, err
|
op.kind,
|
||||||
|
op.from.display(),
|
||||||
|
op.to.display(),
|
||||||
|
err
|
||||||
),
|
),
|
||||||
&self.controller,
|
&self.controller,
|
||||||
)
|
)
|
||||||
|
|
@ -209,7 +217,7 @@ impl Context {
|
||||||
}
|
}
|
||||||
ReplaceResult::KeepBoth => match op.to.parent() {
|
ReplaceResult::KeepBoth => match op.to.parent() {
|
||||||
Some(to_parent) => Ok(ControlFlow::Continue(copy_unique_path(&op.from, to_parent))),
|
Some(to_parent) => Ok(ControlFlow::Continue(copy_unique_path(&op.from, to_parent))),
|
||||||
None => Err(format!("failed to get parent of {:?}", op.to).into()),
|
None => Err(format!("failed to get parent of {}", op.to.display()).into()),
|
||||||
},
|
},
|
||||||
ReplaceResult::Skip(apply_to_all) => {
|
ReplaceResult::Skip(apply_to_all) => {
|
||||||
if apply_to_all {
|
if apply_to_all {
|
||||||
|
|
@ -319,7 +327,11 @@ impl Op {
|
||||||
(ctx.on_progress)(self, &progress);
|
(ctx.on_progress)(self, &progress);
|
||||||
if let Err(err) = to_file.set_permissions(metadata.permissions()).await {
|
if let Err(err) = to_file.set_permissions(metadata.permissions()).await {
|
||||||
// This error is not propagated upwards as some filesystems do not support setting permissions
|
// This error is not propagated upwards as some filesystems do not support setting permissions
|
||||||
log::warn!("failed to set permissions for {:?}: {}", self.to, err);
|
log::warn!(
|
||||||
|
"failed to set permissions for {}: {}",
|
||||||
|
self.to.display(),
|
||||||
|
err
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent spamming the progress callbacks.
|
// Prevent spamming the progress callbacks.
|
||||||
|
|
@ -402,7 +414,7 @@ impl Op {
|
||||||
self.skipped.cleanup.set(true);
|
self.skipped.cleanup.set(true);
|
||||||
}
|
}
|
||||||
// Try standard copy if hard link fails with cross device error
|
// Try standard copy if hard link fails with cross device error
|
||||||
let mut copy_op = Op {
|
let mut copy_op = Self {
|
||||||
kind: OpKind::Copy,
|
kind: OpKind::Copy,
|
||||||
from: self.from.clone(),
|
from: self.from.clone(),
|
||||||
to: self.to.clone(),
|
to: self.to.clone(),
|
||||||
|
|
@ -410,9 +422,8 @@ impl Op {
|
||||||
is_cleanup: self.is_cleanup,
|
is_cleanup: self.is_cleanup,
|
||||||
};
|
};
|
||||||
return Box::pin(copy_op.run(ctx, progress)).await;
|
return Box::pin(copy_op.run(ctx, progress)).await;
|
||||||
} else {
|
|
||||||
return Err(err.into());
|
|
||||||
}
|
}
|
||||||
|
return Err(err.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1207
src/tab.rs
1207
src/tab.rs
File diff suppressed because it is too large
Load diff
|
|
@ -14,7 +14,7 @@ use tempfile::NamedTempFile;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
/// Implements thumbnail caching based on the freedesktop.org Thumbnail Managing Standard.
|
/// Implements thumbnail caching based on the freedesktop.org Thumbnail Managing Standard.
|
||||||
/// https://specifications.freedesktop.org/thumbnail-spec/latest/
|
/// <https://specifications.freedesktop.org/thumbnail-spec/latest>/
|
||||||
pub struct ThumbnailCacher {
|
pub struct ThumbnailCacher {
|
||||||
file_path: PathBuf,
|
file_path: PathBuf,
|
||||||
file_uri: String,
|
file_uri: String,
|
||||||
|
|
@ -27,18 +27,22 @@ pub struct ThumbnailCacher {
|
||||||
impl ThumbnailCacher {
|
impl ThumbnailCacher {
|
||||||
pub fn new(file_path: &Path, thumbnail_size: ThumbnailSize) -> Result<Self, String> {
|
pub fn new(file_path: &Path, thumbnail_size: ThumbnailSize) -> Result<Self, String> {
|
||||||
let file_uri = thumbnail_uri(file_path)
|
let file_uri = thumbnail_uri(file_path)
|
||||||
.map_err(|err| format!("failed to create URI for {file_path:?}: {err}"))?;
|
.map_err(|err| format!("failed to create URI for {}: {}", file_path.display(), err))?;
|
||||||
let cache_base_dir = THUMBNAIL_CACHE_BASE_DIR
|
let cache_base_dir = THUMBNAIL_CACHE_BASE_DIR
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or("failed to get thumbnail cache directory".to_string())?;
|
.ok_or("failed to get thumbnail cache directory".to_string())?;
|
||||||
let thumbnail_filename = thumbnail_cache_filename(&file_uri);
|
let thumbnail_filename = thumbnail_cache_filename(&file_uri);
|
||||||
let thumbnail_dir = cache_base_dir.join(thumbnail_size.subdirectory_name());
|
let thumbnail_dir = cache_base_dir.join(thumbnail_size.subdirectory_name());
|
||||||
if !thumbnail_dir.is_dir() {
|
if !thumbnail_dir.is_dir() {
|
||||||
log::warn!("{:?} is not a directory, creating one now", &thumbnail_dir);
|
log::warn!(
|
||||||
fs::create_dir_all(&thumbnail_dir).unwrap_or(log::error!(
|
"{} is not a directory, creating one now",
|
||||||
"{:?} failed to create directory, this error can be expected on first run",
|
thumbnail_dir.display()
|
||||||
&thumbnail_dir
|
);
|
||||||
));
|
let _: () = log::error!(
|
||||||
|
"{} failed to create directory, this error can be expected on first run",
|
||||||
|
thumbnail_dir.display()
|
||||||
|
);
|
||||||
|
fs::create_dir_all(&thumbnail_dir).unwrap_or(());
|
||||||
}
|
}
|
||||||
let thumbnail_path = thumbnail_dir.join(&thumbnail_filename);
|
let thumbnail_path = thumbnail_dir.join(&thumbnail_filename);
|
||||||
let thumbnail_fail_marker_path = cache_base_dir
|
let thumbnail_fail_marker_path = cache_base_dir
|
||||||
|
|
@ -64,7 +68,7 @@ impl ThumbnailCacher {
|
||||||
std::fs::metadata(&self.file_path),
|
std::fs::metadata(&self.file_path),
|
||||||
) {
|
) {
|
||||||
if metadata.is_file() && self.file_path.starts_with(cache_base_dir) {
|
if metadata.is_file() && self.file_path.starts_with(cache_base_dir) {
|
||||||
return CachedThumbnail::Valid((self.file_path.to_path_buf(), None));
|
return CachedThumbnail::Valid((self.file_path.clone(), None));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -145,9 +149,8 @@ impl ThumbnailCacher {
|
||||||
let info = reader.info();
|
let info = reader.info();
|
||||||
let text_chunks: FxHashMap<String, String> = info
|
let text_chunks: FxHashMap<String, String> = info
|
||||||
.uncompressed_latin1_text
|
.uncompressed_latin1_text
|
||||||
.clone()
|
.iter()
|
||||||
.into_iter()
|
.map(|chunk| (chunk.keyword.clone(), chunk.text.clone()))
|
||||||
.map(|chunk| (chunk.keyword, chunk.text))
|
|
||||||
.collect();
|
.collect();
|
||||||
(
|
(
|
||||||
info.width,
|
info.width,
|
||||||
|
|
@ -204,7 +207,11 @@ impl ThumbnailCacher {
|
||||||
let reader = match decoder.read_info() {
|
let reader = match decoder.read_info() {
|
||||||
Ok(reader) => reader,
|
Ok(reader) => reader,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!("failed to decode {thumbnail_path:?} as PNG: {err}");
|
log::warn!(
|
||||||
|
"failed to decode {} as PNG: {}",
|
||||||
|
thumbnail_path.display(),
|
||||||
|
err
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -214,7 +221,7 @@ impl ThumbnailCacher {
|
||||||
// Thumb::URI is required and must match.
|
// Thumb::URI is required and must match.
|
||||||
let thumb_uri = texts
|
let thumb_uri = texts
|
||||||
.iter()
|
.iter()
|
||||||
.find(|text| text.keyword == "Thumb::URI")
|
.find(|&text| text.keyword == "Thumb::URI")
|
||||||
.map(|t| &t.text);
|
.map(|t| &t.text);
|
||||||
if let Some(thumb_uri) = thumb_uri {
|
if let Some(thumb_uri) = thumb_uri {
|
||||||
if *thumb_uri != self.file_uri {
|
if *thumb_uri != self.file_uri {
|
||||||
|
|
@ -227,7 +234,11 @@ impl ThumbnailCacher {
|
||||||
let metadata = match std::fs::metadata(&self.file_path) {
|
let metadata = match std::fs::metadata(&self.file_path) {
|
||||||
Ok(m) => m,
|
Ok(m) => m,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!("failed to get metatdata of {:?}: {}", self.file_path, err);
|
log::warn!(
|
||||||
|
"failed to get metatdata of {}: {}",
|
||||||
|
self.file_path.display(),
|
||||||
|
err
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -235,15 +246,15 @@ impl ThumbnailCacher {
|
||||||
// Thumb::MTime is required and must match.
|
// Thumb::MTime is required and must match.
|
||||||
let thumb_mtime = texts
|
let thumb_mtime = texts
|
||||||
.iter()
|
.iter()
|
||||||
.find(|text| text.keyword == "Thumb::MTime")
|
.find(|&text| text.keyword == "Thumb::MTime")
|
||||||
.map(|t| &t.text);
|
.map(|t| &t.text);
|
||||||
if let Some(thumb_mtime) = thumb_mtime {
|
if let Some(thumb_mtime) = thumb_mtime {
|
||||||
let modified = match metadata.modified() {
|
let modified = match metadata.modified() {
|
||||||
Ok(m) => m,
|
Ok(m) => m,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
"failed to get modified from metatdata of {:?}, {}",
|
"failed to get modified from metatdata of {}, {}",
|
||||||
self.file_path,
|
self.file_path.display(),
|
||||||
err
|
err
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -264,7 +275,7 @@ impl ThumbnailCacher {
|
||||||
// Thumb::Size isn't required, but it should be verified if present.
|
// Thumb::Size isn't required, but it should be verified if present.
|
||||||
let thumb_size = texts
|
let thumb_size = texts
|
||||||
.iter()
|
.iter()
|
||||||
.find(|text| text.keyword == "Thumb::Size")
|
.find(|&text| text.keyword == "Thumb::Size")
|
||||||
.map(|t| &t.text);
|
.map(|t| &t.text);
|
||||||
if let Some(thumb_size) = thumb_size {
|
if let Some(thumb_size) = thumb_size {
|
||||||
let size = metadata.len();
|
let size = metadata.len();
|
||||||
|
|
@ -279,16 +290,17 @@ impl ThumbnailCacher {
|
||||||
|
|
||||||
fn thumbnail_uri(path: &Path) -> io::Result<String> {
|
fn thumbnail_uri(path: &Path) -> io::Result<String> {
|
||||||
let absolute_path = fs::canonicalize(path)?;
|
let absolute_path = fs::canonicalize(path)?;
|
||||||
let url = Url::from_file_path(&absolute_path).map_err(|_| {
|
let url = Url::from_file_path(&absolute_path).map_err(|()| {
|
||||||
io::Error::other(format!(
|
io::Error::other(format!(
|
||||||
"failed to create URI for thumbnail_file: {absolute_path:?}"
|
"failed to create URI for thumbnail_file: {}",
|
||||||
|
absolute_path.display()
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
// Technically square brackets don't need to be percent encoded,
|
// Technically square brackets don't need to be percent encoded,
|
||||||
// and they aren't by the url crate, but the thumbnailer used by
|
// and they aren't by the url crate, but the thumbnailer used by
|
||||||
// Gnome Files does. In order to share thumbnails and not get duplicates
|
// Gnome Files does. In order to share thumbnails and not get duplicates
|
||||||
// we should do the same.
|
// we should do the same.
|
||||||
let url = url.to_string().replace("[", "%5B").replace("]", "%5D");
|
let url = url.as_str().replace('[', "%5B").replace(']', "%5D");
|
||||||
Ok(url)
|
Ok(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -319,11 +331,11 @@ impl ThumbnailSize {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pixel_size(self) -> u32 {
|
pub const fn pixel_size(self) -> u32 {
|
||||||
self as u32
|
self as u32
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn subdirectory_name(self) -> &'static str {
|
pub const fn subdirectory_name(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Normal => "normal",
|
Self::Normal => "normal",
|
||||||
Self::Large => "large",
|
Self::Large => "large",
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ impl Thumbnailer {
|
||||||
command.arg(output);
|
command.arg(output);
|
||||||
}
|
}
|
||||||
"%s" => {
|
"%s" => {
|
||||||
command.arg(format!("{}", thumbnail_size));
|
command.arg(format!("{thumbnail_size}"));
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
|
|
@ -80,29 +80,35 @@ impl ThumbnailerCache {
|
||||||
let mut search_dirs = Vec::new();
|
let mut search_dirs = Vec::new();
|
||||||
let xdg_dirs = xdg::BaseDirectories::new();
|
let xdg_dirs = xdg::BaseDirectories::new();
|
||||||
|
|
||||||
if let Some(data_home) = xdg_dirs.get_data_home() {
|
if let Some(mut data_home) = xdg_dirs.get_data_home() {
|
||||||
search_dirs.push(data_home.join("thumbnailers"));
|
data_home.push("thumbnailers");
|
||||||
}
|
search_dirs.push(data_home);
|
||||||
for data_dir in xdg_dirs.get_data_dirs() {
|
|
||||||
search_dirs.push(data_dir.join("thumbnailers"));
|
|
||||||
}
|
}
|
||||||
|
search_dirs.extend(xdg_dirs.get_data_dirs().into_iter().map(|mut data_dir| {
|
||||||
|
data_dir.push("thumbnailers");
|
||||||
|
data_dir
|
||||||
|
}));
|
||||||
|
|
||||||
let mut thumbnailer_paths = Vec::new();
|
let mut thumbnailer_paths = Vec::new();
|
||||||
for dir in search_dirs {
|
for dir in search_dirs {
|
||||||
log::trace!("looking for thumbnailers in {:?}", dir);
|
log::trace!("looking for thumbnailers in {}", dir.display());
|
||||||
match fs::read_dir(&dir) {
|
match fs::read_dir(&dir) {
|
||||||
Ok(entries) => {
|
Ok(entries) => {
|
||||||
for entry_res in entries {
|
thumbnailer_paths.extend(entries.filter_map(|entry_res| {
|
||||||
match entry_res {
|
entry_res
|
||||||
Ok(entry) => thumbnailer_paths.push(entry.path()),
|
.inspect_err(|err| {
|
||||||
Err(err) => {
|
log::warn!(
|
||||||
log::warn!("failed to read entry in directory {:?}: {}", dir, err);
|
"failed to read entry in directory {}: {}",
|
||||||
}
|
dir.display(),
|
||||||
}
|
err
|
||||||
}
|
)
|
||||||
|
})
|
||||||
|
.ok()
|
||||||
|
.map(|entry| entry.path())
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!("failed to read directory {:?}: {}", dir, err);
|
log::warn!("failed to read directory {}: {}", dir.display(), err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -112,7 +118,7 @@ impl ThumbnailerCache {
|
||||||
let entry = match freedesktop_entry_parser::parse_entry(&path) {
|
let entry = match freedesktop_entry_parser::parse_entry(&path) {
|
||||||
Ok(ok) => ok,
|
Ok(ok) => ok,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!("failed to parse {:?}: {}", path, err);
|
log::warn!("failed to parse {}: {}", path.display(), err);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -120,20 +126,23 @@ impl ThumbnailerCache {
|
||||||
//TODO: use TryExec?
|
//TODO: use TryExec?
|
||||||
let section = entry.section("Thumbnailer Entry");
|
let section = entry.section("Thumbnailer Entry");
|
||||||
let Some(exec) = section.attr("Exec") else {
|
let Some(exec) = section.attr("Exec") else {
|
||||||
log::warn!("missing Exec attribute for thumbnailer {:?}", path);
|
log::warn!("missing Exec attribute for thumbnailer {}", path.display());
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let Some(mime_types) = section.attr("MimeType") else {
|
let Some(mime_types) = section.attr("MimeType") else {
|
||||||
log::warn!("missing MimeType attribute for thumbnailer {:?}", path);
|
log::warn!(
|
||||||
|
"missing MimeType attribute for thumbnailer {}",
|
||||||
|
path.display()
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
for mime_type in mime_types.split_terminator(';') {
|
for mime_type in mime_types.split_terminator(';') {
|
||||||
if let Ok(mime) = mime_type.parse::<Mime>() {
|
if let Ok(mime) = mime_type.parse::<Mime>() {
|
||||||
log::trace!("thumbnailer {}={:?}", mime, path);
|
log::trace!("thumbnailer {}={}", mime, path.display());
|
||||||
let apps = self
|
let apps = self
|
||||||
.cache
|
.cache
|
||||||
.entry(mime.clone())
|
.entry(mime)
|
||||||
.or_insert_with(|| Vec::with_capacity(1));
|
.or_insert_with(|| Vec::with_capacity(1));
|
||||||
apps.push(Thumbnailer {
|
apps.push(Thumbnailer {
|
||||||
exec: exec.to_string(),
|
exec: exec.to_string(),
|
||||||
|
|
@ -143,11 +152,11 @@ impl ThumbnailerCache {
|
||||||
}
|
}
|
||||||
|
|
||||||
let elapsed = start.elapsed();
|
let elapsed = start.elapsed();
|
||||||
log::info!("loaded thumbnailer cache in {:?}", elapsed);
|
log::info!("loaded thumbnailer cache in {elapsed:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get(&self, key: &Mime) -> Vec<Thumbnailer> {
|
pub fn get(&self, key: &Mime) -> Vec<Thumbnailer> {
|
||||||
self.cache.get(key).map_or_else(Vec::new, |x| x.clone())
|
self.cache.get(key).map_or_else(Vec::new, Vec::clone)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
52
src/zoom.rs
Normal file
52
src/zoom.rs
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
use std::num::NonZeroU16;
|
||||||
|
|
||||||
|
use crate::{config::IconSizes, tab::View};
|
||||||
|
|
||||||
|
static DEFAULT_ZOOM: NonZeroU16 = NonZeroU16::new(100).unwrap();
|
||||||
|
static MIN_ZOOM: NonZeroU16 = NonZeroU16::new(50).unwrap();
|
||||||
|
static MAX_ZOOM: NonZeroU16 = NonZeroU16::new(500).unwrap();
|
||||||
|
const ZOOM_STEP: u16 = 25;
|
||||||
|
|
||||||
|
pub(crate) const fn zoom_to_default(view: View, icon_sizes: &mut IconSizes) {
|
||||||
|
let icon_size = select_resized_icon(view, icon_sizes);
|
||||||
|
*icon_size = DEFAULT_ZOOM;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn zoom_in_view(view: View, icon_sizes: &mut IconSizes) {
|
||||||
|
let icon_size = select_resized_icon(view, icon_sizes);
|
||||||
|
|
||||||
|
let mut step = MIN_ZOOM;
|
||||||
|
while step <= MAX_ZOOM {
|
||||||
|
if *icon_size < step {
|
||||||
|
*icon_size = step;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
step = step.saturating_add(ZOOM_STEP);
|
||||||
|
}
|
||||||
|
if *icon_size > step {
|
||||||
|
*icon_size = step;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn zoom_out_view(view: View, icon_sizes: &mut IconSizes) {
|
||||||
|
let icon_size = select_resized_icon(view, icon_sizes);
|
||||||
|
|
||||||
|
let mut step = MAX_ZOOM;
|
||||||
|
while step >= MIN_ZOOM {
|
||||||
|
if *icon_size > step {
|
||||||
|
*icon_size = step;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
step = NonZeroU16::new(step.get().saturating_sub(ZOOM_STEP)).unwrap();
|
||||||
|
}
|
||||||
|
if *icon_size < step {
|
||||||
|
*icon_size = step;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn select_resized_icon(view: View, icon_sizes: &mut IconSizes) -> &mut NonZeroU16 {
|
||||||
|
match view {
|
||||||
|
View::Grid => &mut icon_sizes.grid,
|
||||||
|
View::List => &mut icon_sizes.list,
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue