Merge pull request #1390 from Cheong-Lau/enter-network-path

feat(tab): allow entering of network uri as path
This commit is contained in:
Jeremy Soller 2025-12-01 14:38:10 -07:00 committed by GitHub
commit 3baaf4b452
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 165 additions and 95 deletions

View file

@ -3004,7 +3004,12 @@ impl Application for App {
.as_ref()
.map_or_else(|| &tab.location, |x| &x.location);
// Try to add text to end of location
if let Some(path) = location.path_opt() {
if let Location::Network(uri, ..) = location {
let mut uri_string = uri.clone();
uri_string.push_str(&text);
tab.edit_location =
Some(location.with_uri(uri_string).into());
} else if let Some(path) = location.path_opt() {
let mut path_string =
path.to_string_lossy().into_owned();
path_string.push_str(&text);

View file

@ -16,6 +16,24 @@ use crate::{
const TARGET_URI_ATTRIBUTE: &str = "standard::target-uri";
fn resolve_uri(uri: &str) -> (String, gio::File) {
let file = gio::File::for_uri(uri);
// Resolve the target-uri if it exists
if let Ok(file_info) = file.query_info(
TARGET_URI_ATTRIBUTE,
gio::FileQueryInfoFlags::NONE,
gio::Cancellable::NONE,
) {
if let Some(resolved_uri) = file_info.attribute_as_string(TARGET_URI_ATTRIBUTE) {
let resolved_uri = String::from(resolved_uri);
let file = gio::File::for_uri(&resolved_uri);
return (resolved_uri, file);
}
}
(uri.to_string(), file)
}
fn gio_icon_to_path(icon: &gio::Icon, size: u16) -> Option<PathBuf> {
if let Some(themed_icon) = icon.downcast_ref::<gio::ThemedIcon>() {
for name in themed_icon.names() {
@ -83,19 +101,8 @@ fn items(monitor: &gio::VolumeMonitor, sizes: IconSizes) -> MounterItems {
}
fn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, String> {
let mut file = gio::File::for_uri(uri);
let force_dir = uri.starts_with("network:///");
// Resolve the target-uri if it exists
if let Ok(file_info) = file.query_info(
TARGET_URI_ATTRIBUTE,
gio::FileQueryInfoFlags::NONE,
gio::Cancellable::NONE,
) {
if let Some(resolved_uri) = file_info.attribute_as_string(TARGET_URI_ATTRIBUTE) {
file = gio::File::for_uri(resolved_uri.as_str());
}
}
let (_, file) = resolve_uri(uri);
// Read .hidden file if present
let hidden_files: Box<[String]> = if let Some(path) = file.path() {
@ -210,6 +217,17 @@ fn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, String> {
Ok(items)
}
fn dir_info(uri: &str) -> Result<(String, String), glib::Error> {
let (resolved_uri, file) = resolve_uri(uri);
let info = file.query_info(
gio::FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
gio::FileQueryInfoFlags::NONE,
gio::Cancellable::NONE,
)?;
Ok((resolved_uri, info.display_name().into()))
}
fn mount_op(uri: String, event_tx: mpsc::UnboundedSender<Event>) -> gio::MountOperation {
let mount_op = gio::MountOperation::new();
mount_op.connect_ask_password(
@ -270,6 +288,7 @@ enum Cmd {
IconSizes,
mpsc::Sender<Result<Vec<tab::Item>, String>>,
),
DirInfo(String, mpsc::Sender<Result<(String, String), glib::Error>>),
Unmount(MounterItem),
}
@ -494,38 +513,27 @@ impl Gvfs {
}
);
}
Cmd::NetworkScan(mut uri, sizes, items_tx) => {
let original_uri = uri.clone();
let mut file = gio::File::for_uri(&uri);
if let Ok(file_info) = file.query_info(
TARGET_URI_ATTRIBUTE,
gio::FileQueryInfoFlags::NONE,
gio::Cancellable::NONE,
) {
if let Some(resolved_uri) = file_info.attribute_as_string(TARGET_URI_ATTRIBUTE) {
uri = resolved_uri.into();
file = gio::File::for_uri(&uri);
}
}
Cmd::NetworkScan(uri, sizes, items_tx) => {
let (resolved_uri, file) = resolve_uri(&uri);
let needs_mount = uri != "network:///" && match file.find_enclosing_mount(gio::Cancellable::NONE) {
let needs_mount = resolved_uri != "network:///" && match file.find_enclosing_mount(gio::Cancellable::NONE) {
Ok(_) => false,
Err(err) => matches!(err.kind::<gio::IOErrorEnum>(), Some(gio::IOErrorEnum::NotMounted))
};
if needs_mount {
let mount_op = mount_op(uri.clone(), event_tx.clone());
let mount_op = mount_op(resolved_uri.clone(), event_tx.clone());
let event_tx = event_tx.clone();
file.mount_enclosing_volume(
gio::MountMountFlags::empty(),
Some(&mount_op),
gio::Cancellable::NONE,
move |res| {
log::info!("network scan mounted {uri}: result {res:?}");
log::info!("network scan mounted {resolved_uri}: result {res:?}");
// FIXME sometimes a uri can be mounted and then not recognized as mounted...
// seems to be related to uri with a path
items_tx.blocking_send(network_scan(&original_uri, sizes)).unwrap();
event_tx.send(Event::NetworkResult(uri, match res {
items_tx.blocking_send(network_scan(&uri, sizes)).unwrap();
event_tx.send(Event::NetworkResult(resolved_uri, match res {
Ok(()) => {
Ok(true)
},
@ -537,9 +545,12 @@ impl Gvfs {
}
);
} else {
items_tx.send(network_scan(&original_uri, sizes)).await.unwrap();
items_tx.send(network_scan(&uri, sizes)).await.unwrap();
}
}
Cmd::DirInfo(uri, result_tx) => {
result_tx.send(dir_info(&uri)).await.unwrap();
}
Cmd::Unmount(mounter_item) => {
let MounterItem::Gvfs(item) = mounter_item else { continue };
let ItemKind::Mount = item.kind else { continue };
@ -640,6 +651,14 @@ impl Mounter for Gvfs {
items_rx.blocking_recv()
}
fn dir_info(&self, uri: &str) -> Option<(String, String)> {
let (result_tx, mut result_rx) = mpsc::channel(1);
self.command_tx
.send(Cmd::DirInfo(uri.to_string(), result_tx))
.unwrap();
result_rx.blocking_recv().and_then(|res| res.ok())
}
fn unmount(&self, item: MounterItem) -> Task<()> {
let command_tx = self.command_tx.clone();
Task::future(async move {

View file

@ -116,6 +116,7 @@ pub trait Mounter: Send + Sync {
fn mount(&self, item: MounterItem) -> Task<()>;
fn network_drive(&self, uri: String) -> Task<()>;
fn network_scan(&self, uri: &str, sizes: IconSizes) -> Option<Result<Vec<tab::Item>, String>>;
fn dir_info(&self, uri: &str) -> Option<(String, String)>;
fn unmount(&self, item: MounterItem) -> Task<()>;
fn subscription(&self) -> Subscription<MounterMessage>;
}

View file

@ -1362,12 +1362,19 @@ pub struct EditLocation {
impl EditLocation {
pub fn resolve(&self) -> Option<Location> {
let Some(selected) = self.selected else {
return Some(self.location.clone());
};
let completions = self.completions.as_ref()?;
let completion = completions.get(selected)?;
Some(self.location.with_path(completion.1.clone()))
if let Location::Network(uri, _, path) = &self.location {
MOUNTERS
.values()
.find_map(|mounter| mounter.dir_info(uri))
.map(|(uri, display_name)| Location::Network(uri, display_name, path.clone()))
} else {
let Some(selected) = self.selected else {
return Some(self.location.clone());
};
let completions = self.completions.as_ref()?;
let completion = completions.get(selected)?;
Some(self.location.with_path(completion.1.clone()))
}
}
pub fn select(&mut self, forwards: bool) {
@ -1430,7 +1437,15 @@ impl std::fmt::Display for Location {
impl Location {
pub fn normalize(&self) -> Self {
if let Some(mut path) = self.path_opt().cloned() {
if let Location::Network(uri, ..) = self {
if !uri.ends_with('/') {
let mut uri = uri.clone();
uri.push('/');
self.with_uri(uri)
} else {
self.clone()
}
} else if let Some(mut path) = self.path_opt().cloned() {
// Add trailing slash if location is a path
path.push("");
self.with_path(path)
@ -1482,12 +1497,19 @@ impl Location {
Self::Search(_, term, show_hidden, time) => {
Self::Search(path, term.clone(), *show_hidden, *time)
}
Self::Network(id, name, path) => Self::Network(id.clone(), name.clone(), path.clone()),
other => other.clone(),
}
}
pub fn with_uri(&self, uri: String) -> Self {
if let Self::Network(_, name, path) = self {
Self::Network(uri, name.clone(), path.clone())
} else {
self.clone()
}
}
pub fn scan(&self, sizes: IconSizes) -> (Option<Item>, Vec<Item>) {
let items = match self {
Self::Desktop(path, display, desktop_config) => {
@ -3064,10 +3086,14 @@ impl Tab {
{
let mut remove = false;
if let Some(last_location) = self.history.last() {
if let Some(last_path) = last_location.path_opt() {
if let Some(path) = location.path_opt() {
remove = last_path == path;
}
if let Location::Network(last_uri, ..) = last_location
&& let Location::Network(uri, ..) = location
{
remove = last_uri == uri;
} else if let Some(last_path) = last_location.path_opt()
&& let Some(path) = location.path_opt()
{
remove = last_path == path;
}
}
if remove {
@ -3409,8 +3435,10 @@ impl Tab {
}
Message::EditLocationComplete(selected) => {
if let Some(mut edit_location) = self.edit_location.take() {
edit_location.selected = Some(selected);
cd = edit_location.resolve();
if !matches!(edit_location.location, Location::Network(..)) {
edit_location.selected = Some(selected);
cd = edit_location.resolve();
}
}
}
Message::EditLocationEnable => {
@ -4634,65 +4662,82 @@ impl Tab {
.padding([0, theme::active().cosmic().corner_radii.radius_xs[0] as u16]);
if let Some(edit_location) = &self.edit_location {
if let Some(location) = edit_location.resolve() {
//TODO: allow editing other locations
if let Some(path) = location.path_opt() {
row = row.push(
widget::button::custom(
widget::icon::from_name("window-close-symbolic").size(16),
)
.on_press(Message::EditLocation(None))
.padding(space_xxs)
.class(theme::Button::Icon),
);
let text_input = widget::text_input("", path.to_string_lossy().into_owned())
let mut text_input = None;
//TODO: allow editing other locations
if let Location::Network(ref uri, ..) = edit_location.location {
let location = edit_location.location.clone();
text_input = Some(
widget::text_input("", uri.clone())
.id(self.edit_location_id.clone())
.on_input(move |input| {
Message::EditLocation(Some(location.with_uri(input).into()))
})
.on_submit(|_| Message::EditLocationSubmit)
.line_height(1.0),
);
} else if let Some(resolved_location) = edit_location.resolve()
&& let Some(path) = resolved_location.path_opt().cloned()
{
text_input = Some(
widget::text_input("", path.to_string_lossy().into_owned())
.id(self.edit_location_id.clone())
.on_input(move |input| {
Message::EditLocation(Some(
location.with_path(PathBuf::from(input)).into(),
resolved_location.with_path(PathBuf::from(input)).into(),
))
})
.on_submit(|_| Message::EditLocationSubmit)
.line_height(1.0);
let mut popover =
widget::popover(text_input).position(widget::popover::Position::Bottom);
if let Some(completions) = &edit_location.completions {
if !completions.is_empty() {
let mut column =
widget::column::with_capacity(completions.len()).padding(space_xxs);
for (i, (name, _path)) in completions.iter().enumerate() {
let selected = edit_location.selected == Some(i);
column = column.push(
widget::button::custom(widget::text::body(name))
//TODO: match to design
.class(if selected {
theme::Button::Standard
} else {
theme::Button::HeaderBar
})
.on_press(Message::EditLocationComplete(i))
.padding(space_xxs)
.width(Length::Fill),
);
}
popover = popover.popup(
widget::container(column)
.class(theme::Container::Dropdown)
//TODO: This is a hack to get the popover to be the right width
.max_width(size.width - 140.0),
.line_height(1.0),
);
}
if let Some(text_input) = text_input {
row = row.push(
widget::button::custom(
widget::icon::from_name("window-close-symbolic").size(16),
)
.on_press(Message::EditLocation(None))
.padding(space_xxs)
.class(theme::Button::Icon),
);
let mut popover =
widget::popover(text_input).position(widget::popover::Position::Bottom);
if let Some(completions) = &edit_location.completions {
if !completions.is_empty() {
let mut column =
widget::column::with_capacity(completions.len()).padding(space_xxs);
for (i, (name, _path)) in completions.iter().enumerate() {
let selected = edit_location.selected == Some(i);
column = column.push(
widget::button::custom(widget::text::body(name))
//TODO: match to design
.class(if selected {
theme::Button::Standard
} else {
theme::Button::HeaderBar
})
.on_press(Message::EditLocationComplete(i))
.padding(space_xxs)
.width(Length::Fill),
);
}
popover = popover.popup(
widget::container(column)
.class(theme::Container::Dropdown)
//TODO: This is a hack to get the popover to be the right width
.max_width(size.width - 140.0),
);
}
row = row.push(popover);
let mut column = widget::column::with_capacity(4).padding([0, space_s]);
column = column.push(row);
column = column.push(accent_rule);
if self.config.view == View::List && !condensed {
column = column.push(heading_row);
column = column.push(heading_rule);
}
return column.into();
}
row = row.push(popover);
let mut column = widget::column::with_capacity(4).padding([0, space_s]);
column = column.push(row);
column = column.push(accent_rule);
if self.config.view == View::List && !condensed {
column = column.push(heading_row);
column = column.push(heading_rule);
}
return column.into();
}
} else if let Some(path) = self.location.path_opt() {
row = row.push(