Skip to main content

strop_engine/
files.rs

1//! Application open targets (0042). `FileTarget` is the *unresolved* input an
2//! open/browse command was given — a local path or a remote URI spelling — as
3//! opposed to `strop_workspace::ResourceLocation`, the resolved identity of a
4//! resource once it is known. Remote URIs and native local paths stay distinct.
5use serde::{Deserialize, Serialize};
6use std::path::{Path, PathBuf};
7use strop_workspace::{AddressError, RemoteLocation};
8
9/// Local paths keep their existing native-byte wire representation; remote targets
10/// have an explicit remote envelope, never an ambiguous legacy local-path string.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum FileTarget {
13    Local(PathBuf),
14    Remote(RemoteLocation),
15    /// A path inside a running container (0037 DC1b) — the path names
16    /// container bytes; there is never a local interpretation.
17    Container {
18        container: strop_workspace::ContainerId,
19        path: PathBuf,
20    },
21}
22
23impl Serialize for FileTarget {
24    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
25        #[derive(Serialize)]
26        struct RemoteRecord<'a> {
27            remote: &'a RemoteLocation,
28        }
29        #[derive(Serialize)]
30        struct ContainerRecord<'a> {
31            container: &'a strop_workspace::ContainerId,
32            #[serde(with = "strop_core::path_serde")]
33            path: &'a PathBuf,
34        }
35        match self {
36            Self::Local(path) => strop_core::path_serde::serialize(path, serializer),
37            Self::Remote(remote) => RemoteRecord { remote }.serialize(serializer),
38            Self::Container { container, path } => {
39                ContainerRecord { container, path }.serialize(serializer)
40            }
41        }
42    }
43}
44impl<'de> Deserialize<'de> for FileTarget {
45    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
46        #[derive(Deserialize)]
47        #[serde(untagged)]
48        enum Record {
49            Remote {
50                remote: RemoteLocation,
51            },
52            Container {
53                container: strop_workspace::ContainerId,
54                #[serde(with = "strop_core::path_serde")]
55                path: PathBuf,
56            },
57            Local(#[serde(with = "strop_core::path_serde")] PathBuf),
58        }
59        Ok(match Record::deserialize(deserializer)? {
60            Record::Remote { remote } => Self::Remote(remote),
61            Record::Container { container, path } => Self::Container { container, path },
62            Record::Local(path) => Self::Local(path),
63        })
64    }
65}
66impl FileTarget {
67    /// Only textual user-entry boundaries interpret the scheme. Filesystem/LSP
68    /// callers construct Local directly, including filenames containing `ssh:`.
69    pub fn parse(value: PathBuf) -> Result<Self, AddressError> {
70        match value.to_str().filter(|text| text.starts_with("ssh://")) {
71            Some(uri) => RemoteLocation::parse(uri).map(Self::Remote),
72            None => Ok(Self::Local(value)),
73        }
74    }
75    pub fn local_path(&self) -> Option<&Path> {
76        match self {
77            Self::Local(path) => Some(path),
78            _ => None,
79        }
80    }
81}
82impl std::fmt::Display for FileTarget {
83    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84        match self {
85            Self::Local(path) => path.display().fmt(formatter),
86            Self::Remote(file) => file.fmt(formatter),
87            Self::Container { container, path } => write!(
88                formatter,
89                "container:{}{}",
90                &container.as_str()[..12],
91                path.display()
92            ),
93        }
94    }
95}