Skip to content

Commit

Permalink
feat(layouts): allow consuming a layout from a url (#3351)
Browse files Browse the repository at this point in the history
* feat(cli): allow loading layouts directly from a url

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

* style(fmt): rustfmt
  • Loading branch information
imsnif committed May 15, 2024
1 parent 81c5a2a commit 90a6221
Show file tree
Hide file tree
Showing 9 changed files with 110 additions and 5 deletions.
1 change: 1 addition & 0 deletions src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,7 @@ pub(crate) fn start_client(opts: CliArgs) {
layout_dir.clone(),
config_without_layout.clone(),
),
LayoutInfo::Url(url) => Layout::from_url(&url, config_without_layout.clone()),
};
match new_session_layout {
Ok(new_session_layout) => {
Expand Down
3 changes: 3 additions & 0 deletions zellij-utils/src/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -797,19 +797,22 @@ pub struct SessionInfo {
pub enum LayoutInfo {
BuiltIn(String),
File(String),
Url(String),
}

impl LayoutInfo {
pub fn name(&self) -> &str {
match self {
LayoutInfo::BuiltIn(name) => &name,
LayoutInfo::File(name) => &name,
LayoutInfo::Url(url) => &url,
}
}
pub fn is_builtin(&self) -> bool {
match self {
LayoutInfo::BuiltIn(_name) => true,
LayoutInfo::File(_name) => false,
LayoutInfo::Url(_url) => false,
}
}
}
Expand Down
25 changes: 25 additions & 0 deletions zellij-utils/src/downloader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ pub enum DownloaderError {
Io(#[source] std::io::Error),
#[error("File name cannot be found in URL: {0}")]
NotFoundFileName(String),
#[error("Failed to parse URL body: {0}")]
InvalidUrlBody(String),
}

#[derive(Debug)]
Expand Down Expand Up @@ -110,6 +112,29 @@ impl Downloader {

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> {
Url::parse(url)
Expand Down
20 changes: 18 additions & 2 deletions zellij-utils/src/input/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -529,9 +529,25 @@ impl Action {
let layout_dir = layout_dir
.or_else(|| config.and_then(|c| c.options.layout_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)
.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 stringified_error = match e {
ConfigError::KdlError(kdl_error) => {
Expand Down
2 changes: 2 additions & 0 deletions zellij-utils/src/input/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ pub enum ConfigError {
PluginsError(#[from] PluginsConfigError),
#[error("{0}")]
ConversionError(#[from] ConversionError),
#[error("{0}")]
DownloadError(String),
}

impl ConfigError {
Expand Down
38 changes: 38 additions & 0 deletions zellij-utils/src/input/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
// place.
// If plugins should be able to depend on the layout system
// then [`zellij-utils`] could be a proper place.
#[cfg(not(target_family = "wasm"))]
use crate::downloader::Downloader;
use crate::{
data::{Direction, LayoutInfo},
home::{default_layout_dir, find_default_config_dir},
Expand All @@ -18,6 +20,8 @@ use crate::{
pane_size::{Constraint, Dimension, PaneGeom},
setup::{self},
};
#[cfg(not(target_family = "wasm"))]
use async_std::task;

use std::cmp::Ordering;
use std::fmt::{Display, Formatter};
Expand Down Expand Up @@ -1145,6 +1149,7 @@ impl Layout {
LayoutInfo::BuiltIn(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(
&raw_layout,
Expand Down Expand Up @@ -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(
layout_path: Option<&PathBuf>,
layout_dir: Option<PathBuf>,
Expand All @@ -1197,6 +1216,25 @@ impl Layout {
let config = Config::from_kdl(&raw_layout, Some(config))?; // this merges the two config, with
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(
layout_path: Option<&PathBuf>,
layout_dir: Option<PathBuf>,
Expand Down
1 change: 1 addition & 0 deletions zellij-utils/src/kdl/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2166,6 +2166,7 @@ impl SessionInfo {
let (layout_name, layout_source) = match layout_info {
LayoutInfo::File(name) => (name.clone(), "file"),
LayoutInfo::BuiltIn(name) => (name.clone(), "built-in"),
LayoutInfo::Url(url) => (url.clone(), "url"),
};
let mut layout_node = KdlNode::new(format!("{}", layout_name));
let layout_source = KdlEntry::new_prop("source", layout_source);
Expand Down
5 changes: 5 additions & 0 deletions zellij-utils/src/plugin_api/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,10 @@ impl TryFrom<LayoutInfo> for ProtobufLayoutInfo {
source: "built-in".to_owned(),
name,
}),
LayoutInfo::Url(name) => Ok(ProtobufLayoutInfo {
source: "url".to_owned(),
name,
}),
}
}
}
Expand All @@ -555,6 +559,7 @@ impl TryFrom<ProtobufLayoutInfo> for LayoutInfo {
match protobuf_layout_info.source.as_str() {
"file" => Ok(LayoutInfo::File(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"),
}
}
Expand Down
20 changes: 17 additions & 3 deletions zellij-utils/src/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -671,9 +671,23 @@ impl Setup {
.and_then(|cli_options| cli_options.default_layout.clone())
})
.or_else(|| config.options.default_layout.clone());
// we merge-override the config here because the layout might contain configuration
// that needs to take precedence
Layout::from_path_or_default(chosen_layout.as_ref(), layout_dir.clone(), config)
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
// that needs to take precedence
Layout::from_path_or_default(chosen_layout.as_ref(), layout_dir.clone(), config)
}
}
fn handle_setup_commands(cli_args: &CliArgs) {
if let Some(Command::Setup(ref setup)) = &cli_args.command {
Expand Down

0 comments on commit 90a6221

Please sign in to comment.