feat(layouts): allow consuming a layout from a url (#3351)

* feat(cli): allow loading layouts directly from a url

* feat(plugins): allow loading layouts directly from a url

* style(fmt): rustfmt
This commit is contained in:
Aram Drevekenin 2024-05-15 11:20:36 +02:00 committed by GitHub
parent 81c5a2a9df
commit 90a62217fd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 110 additions and 5 deletions

View file

@ -444,6 +444,7 @@ pub(crate) fn start_client(opts: CliArgs) {
layout_dir.clone(), layout_dir.clone(),
config_without_layout.clone(), config_without_layout.clone(),
), ),
LayoutInfo::Url(url) => Layout::from_url(&url, config_without_layout.clone()),
}; };
match new_session_layout { match new_session_layout {
Ok(new_session_layout) => { Ok(new_session_layout) => {

View file

@ -797,6 +797,7 @@ pub struct SessionInfo {
pub enum LayoutInfo { pub enum LayoutInfo {
BuiltIn(String), BuiltIn(String),
File(String), File(String),
Url(String),
} }
impl LayoutInfo { impl LayoutInfo {
@ -804,12 +805,14 @@ impl LayoutInfo {
match self { match self {
LayoutInfo::BuiltIn(name) => &name, LayoutInfo::BuiltIn(name) => &name,
LayoutInfo::File(name) => &name, LayoutInfo::File(name) => &name,
LayoutInfo::Url(url) => &url,
} }
} }
pub fn is_builtin(&self) -> bool { pub fn is_builtin(&self) -> bool {
match self { match self {
LayoutInfo::BuiltIn(_name) => true, LayoutInfo::BuiltIn(_name) => true,
LayoutInfo::File(_name) => false, LayoutInfo::File(_name) => false,
LayoutInfo::Url(_url) => false,
} }
} }
} }

View file

@ -16,6 +16,8 @@ pub enum DownloaderError {
Io(#[source] std::io::Error), Io(#[source] std::io::Error),
#[error("File name cannot be found in URL: {0}")] #[error("File name cannot be found in URL: {0}")]
NotFoundFileName(String), NotFoundFileName(String),
#[error("Failed to parse URL body: {0}")]
InvalidUrlBody(String),
} }
#[derive(Debug)] #[derive(Debug)]
@ -110,6 +112,29 @@ impl Downloader {
Ok(()) Ok(())
} }
pub async fn download_without_cache(url: &str) -> Result<String, DownloaderError> {
// result is the stringified body
let client = surf::client().with(surf::middleware::Redirect::default());
let res = client
.get(url)
.header("Content-Type", "application/octet-stream")
.await
.map_err(|e| DownloaderError::Request(e))?;
let mut downloaded_bytes: Vec<u8> = vec![];
let mut stream = res.bytes();
while let Some(byte) = stream.next().await {
let byte = byte.map_err(|e| DownloaderError::Io(e))?;
downloaded_bytes.push(byte);
}
log::debug!("Download complete");
let stringified = String::from_utf8(downloaded_bytes)
.map_err(|e| DownloaderError::InvalidUrlBody(format!("{}", e)))?;
Ok(stringified)
}
fn parse_name(&self, url: &str) -> Result<String, DownloaderError> { fn parse_name(&self, url: &str) -> Result<String, DownloaderError> {
Url::parse(url) Url::parse(url)

View file

@ -529,9 +529,25 @@ impl Action {
let layout_dir = layout_dir let layout_dir = layout_dir
.or_else(|| config.and_then(|c| c.options.layout_dir)) .or_else(|| config.and_then(|c| c.options.layout_dir))
.or_else(|| get_layout_dir(find_default_config_dir())); .or_else(|| get_layout_dir(find_default_config_dir()));
let (path_to_raw_layout, raw_layout, swap_layouts) =
let (path_to_raw_layout, raw_layout, swap_layouts) = if let Some(layout_url) =
layout_path.to_str().and_then(|l| {
if l.starts_with("http://") || l.starts_with("https://") {
Some(l)
} else {
None
}
}) {
(
layout_url.to_owned(),
Layout::stringified_from_url(layout_url)
.map_err(|e| format!("Failed to load layout: {}", e))?,
None,
)
} else {
Layout::stringified_from_path_or_default(Some(&layout_path), layout_dir) Layout::stringified_from_path_or_default(Some(&layout_path), layout_dir)
.map_err(|e| format!("Failed to load layout: {}", e))?; .map_err(|e| format!("Failed to load layout: {}", e))?
};
let layout = Layout::from_str(&raw_layout, path_to_raw_layout, swap_layouts.as_ref().map(|(f, p)| (f.as_str(), p.as_str())), cwd).map_err(|e| { let layout = Layout::from_str(&raw_layout, path_to_raw_layout, swap_layouts.as_ref().map(|(f, p)| (f.as_str(), p.as_str())), cwd).map_err(|e| {
let stringified_error = match e { let stringified_error = match e {
ConfigError::KdlError(kdl_error) => { ConfigError::KdlError(kdl_error) => {

View file

@ -96,6 +96,8 @@ pub enum ConfigError {
PluginsError(#[from] PluginsConfigError), PluginsError(#[from] PluginsConfigError),
#[error("{0}")] #[error("{0}")]
ConversionError(#[from] ConversionError), ConversionError(#[from] ConversionError),
#[error("{0}")]
DownloadError(String),
} }
impl ConfigError { impl ConfigError {

View file

@ -8,6 +8,8 @@
// place. // place.
// If plugins should be able to depend on the layout system // If plugins should be able to depend on the layout system
// then [`zellij-utils`] could be a proper place. // then [`zellij-utils`] could be a proper place.
#[cfg(not(target_family = "wasm"))]
use crate::downloader::Downloader;
use crate::{ use crate::{
data::{Direction, LayoutInfo}, data::{Direction, LayoutInfo},
home::{default_layout_dir, find_default_config_dir}, home::{default_layout_dir, find_default_config_dir},
@ -18,6 +20,8 @@ use crate::{
pane_size::{Constraint, Dimension, PaneGeom}, pane_size::{Constraint, Dimension, PaneGeom},
setup::{self}, setup::{self},
}; };
#[cfg(not(target_family = "wasm"))]
use async_std::task;
use std::cmp::Ordering; use std::cmp::Ordering;
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
@ -1145,6 +1149,7 @@ impl Layout {
LayoutInfo::BuiltIn(layout_name) => { LayoutInfo::BuiltIn(layout_name) => {
Self::stringified_from_default_assets(&PathBuf::from(layout_name))? Self::stringified_from_default_assets(&PathBuf::from(layout_name))?
}, },
LayoutInfo::Url(url) => (url.clone(), Self::stringified_from_url(&url)?, None),
}; };
Layout::from_kdl( Layout::from_kdl(
&raw_layout, &raw_layout,
@ -1179,6 +1184,20 @@ impl Layout {
), ),
} }
} }
pub fn stringified_from_url(url: &str) -> Result<String, ConfigError> {
#[cfg(not(target_family = "wasm"))]
let raw_layout = task::block_on(async move {
let download = Downloader::download_without_cache(url).await;
match download {
Ok(stringified) => Ok(stringified),
Err(e) => Err(ConfigError::DownloadError(format!("{}", e))),
}
})?;
// silently fail - this should not happen in plugins and legacy architecture is hard
#[cfg(target_family = "wasm")]
let raw_layout = String::new();
Ok(raw_layout)
}
pub fn from_path_or_default( pub fn from_path_or_default(
layout_path: Option<&PathBuf>, layout_path: Option<&PathBuf>,
layout_dir: Option<PathBuf>, layout_dir: Option<PathBuf>,
@ -1197,6 +1216,25 @@ impl Layout {
let config = Config::from_kdl(&raw_layout, Some(config))?; // this merges the two config, with let config = Config::from_kdl(&raw_layout, Some(config))?; // this merges the two config, with
Ok((layout, config)) Ok((layout, config))
} }
#[cfg(not(target_family = "wasm"))]
pub fn from_url(url: &str, config: Config) -> Result<(Layout, Config), ConfigError> {
let raw_layout = task::block_on(async move {
let download = Downloader::download_without_cache(url).await;
match download {
Ok(stringified) => Ok(stringified),
Err(e) => Err(ConfigError::DownloadError(format!("{}", e))),
}
})?;
let layout = Layout::from_kdl(&raw_layout, url.into(), None, None)?;
let config = Config::from_kdl(&raw_layout, Some(config))?; // this merges the two config, with
Ok((layout, config))
}
#[cfg(target_family = "wasm")]
pub fn from_url(url: &str, config: Config) -> Result<(Layout, Config), ConfigError> {
Err(ConfigError::DownloadError(format!(
"Unsupported platform, cannot download layout from the web"
)))
}
pub fn from_path_or_default_without_config( pub fn from_path_or_default_without_config(
layout_path: Option<&PathBuf>, layout_path: Option<&PathBuf>,
layout_dir: Option<PathBuf>, layout_dir: Option<PathBuf>,

View file

@ -2166,6 +2166,7 @@ impl SessionInfo {
let (layout_name, layout_source) = match layout_info { let (layout_name, layout_source) = match layout_info {
LayoutInfo::File(name) => (name.clone(), "file"), LayoutInfo::File(name) => (name.clone(), "file"),
LayoutInfo::BuiltIn(name) => (name.clone(), "built-in"), LayoutInfo::BuiltIn(name) => (name.clone(), "built-in"),
LayoutInfo::Url(url) => (url.clone(), "url"),
}; };
let mut layout_node = KdlNode::new(format!("{}", layout_name)); let mut layout_node = KdlNode::new(format!("{}", layout_name));
let layout_source = KdlEntry::new_prop("source", layout_source); let layout_source = KdlEntry::new_prop("source", layout_source);

View file

@ -545,6 +545,10 @@ impl TryFrom<LayoutInfo> for ProtobufLayoutInfo {
source: "built-in".to_owned(), source: "built-in".to_owned(),
name, name,
}), }),
LayoutInfo::Url(name) => Ok(ProtobufLayoutInfo {
source: "url".to_owned(),
name,
}),
} }
} }
} }
@ -555,6 +559,7 @@ impl TryFrom<ProtobufLayoutInfo> for LayoutInfo {
match protobuf_layout_info.source.as_str() { match protobuf_layout_info.source.as_str() {
"file" => Ok(LayoutInfo::File(protobuf_layout_info.name)), "file" => Ok(LayoutInfo::File(protobuf_layout_info.name)),
"built-in" => Ok(LayoutInfo::BuiltIn(protobuf_layout_info.name)), "built-in" => Ok(LayoutInfo::BuiltIn(protobuf_layout_info.name)),
"url" => Ok(LayoutInfo::Url(protobuf_layout_info.name)),
_ => Err("Unknown source for layout"), _ => Err("Unknown source for layout"),
} }
} }

View file

@ -671,10 +671,24 @@ impl Setup {
.and_then(|cli_options| cli_options.default_layout.clone()) .and_then(|cli_options| cli_options.default_layout.clone())
}) })
.or_else(|| config.options.default_layout.clone()); .or_else(|| config.options.default_layout.clone());
if let Some(layout_url) = chosen_layout
.as_ref()
.and_then(|l| l.to_str())
.and_then(|l| {
if l.starts_with("http://") || l.starts_with("https://") {
Some(l)
} else {
None
}
})
{
Layout::from_url(layout_url, config)
} else {
// we merge-override the config here because the layout might contain configuration // we merge-override the config here because the layout might contain configuration
// that needs to take precedence // that needs to take precedence
Layout::from_path_or_default(chosen_layout.as_ref(), layout_dir.clone(), config) Layout::from_path_or_default(chosen_layout.as_ref(), layout_dir.clone(), config)
} }
}
fn handle_setup_commands(cli_args: &CliArgs) { fn handle_setup_commands(cli_args: &CliArgs) {
if let Some(Command::Setup(ref setup)) = &cli_args.command { if let Some(Command::Setup(ref setup)) = &cli_args.command {
setup.from_cli().map_or_else( setup.from_cli().map_or_else(