Skip to content

Commit 90a6221

Browse files
authored
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
1 parent 81c5a2a commit 90a6221

File tree

9 files changed

+110
-5
lines changed

9 files changed

+110
-5
lines changed

src/commands.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,7 @@ pub(crate) fn start_client(opts: CliArgs) {
444444
layout_dir.clone(),
445445
config_without_layout.clone(),
446446
),
447+
LayoutInfo::Url(url) => Layout::from_url(&url, config_without_layout.clone()),
447448
};
448449
match new_session_layout {
449450
Ok(new_session_layout) => {

zellij-utils/src/data.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -797,19 +797,22 @@ pub struct SessionInfo {
797797
pub enum LayoutInfo {
798798
BuiltIn(String),
799799
File(String),
800+
Url(String),
800801
}
801802

802803
impl LayoutInfo {
803804
pub fn name(&self) -> &str {
804805
match self {
805806
LayoutInfo::BuiltIn(name) => &name,
806807
LayoutInfo::File(name) => &name,
808+
LayoutInfo::Url(url) => &url,
807809
}
808810
}
809811
pub fn is_builtin(&self) -> bool {
810812
match self {
811813
LayoutInfo::BuiltIn(_name) => true,
812814
LayoutInfo::File(_name) => false,
815+
LayoutInfo::Url(_url) => false,
813816
}
814817
}
815818
}

zellij-utils/src/downloader.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ pub enum DownloaderError {
1616
Io(#[source] std::io::Error),
1717
#[error("File name cannot be found in URL: {0}")]
1818
NotFoundFileName(String),
19+
#[error("Failed to parse URL body: {0}")]
20+
InvalidUrlBody(String),
1921
}
2022

2123
#[derive(Debug)]
@@ -110,6 +112,29 @@ impl Downloader {
110112

111113
Ok(())
112114
}
115+
pub async fn download_without_cache(url: &str) -> Result<String, DownloaderError> {
116+
// result is the stringified body
117+
let client = surf::client().with(surf::middleware::Redirect::default());
118+
119+
let res = client
120+
.get(url)
121+
.header("Content-Type", "application/octet-stream")
122+
.await
123+
.map_err(|e| DownloaderError::Request(e))?;
124+
125+
let mut downloaded_bytes: Vec<u8> = vec![];
126+
let mut stream = res.bytes();
127+
while let Some(byte) = stream.next().await {
128+
let byte = byte.map_err(|e| DownloaderError::Io(e))?;
129+
downloaded_bytes.push(byte);
130+
}
131+
132+
log::debug!("Download complete");
133+
let stringified = String::from_utf8(downloaded_bytes)
134+
.map_err(|e| DownloaderError::InvalidUrlBody(format!("{}", e)))?;
135+
136+
Ok(stringified)
137+
}
113138

114139
fn parse_name(&self, url: &str) -> Result<String, DownloaderError> {
115140
Url::parse(url)

zellij-utils/src/input/actions.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -529,9 +529,25 @@ impl Action {
529529
let layout_dir = layout_dir
530530
.or_else(|| config.and_then(|c| c.options.layout_dir))
531531
.or_else(|| get_layout_dir(find_default_config_dir()));
532-
let (path_to_raw_layout, raw_layout, swap_layouts) =
532+
533+
let (path_to_raw_layout, raw_layout, swap_layouts) = if let Some(layout_url) =
534+
layout_path.to_str().and_then(|l| {
535+
if l.starts_with("http://") || l.starts_with("https://") {
536+
Some(l)
537+
} else {
538+
None
539+
}
540+
}) {
541+
(
542+
layout_url.to_owned(),
543+
Layout::stringified_from_url(layout_url)
544+
.map_err(|e| format!("Failed to load layout: {}", e))?,
545+
None,
546+
)
547+
} else {
533548
Layout::stringified_from_path_or_default(Some(&layout_path), layout_dir)
534-
.map_err(|e| format!("Failed to load layout: {}", e))?;
549+
.map_err(|e| format!("Failed to load layout: {}", e))?
550+
};
535551
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| {
536552
let stringified_error = match e {
537553
ConfigError::KdlError(kdl_error) => {

zellij-utils/src/input/config.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ pub enum ConfigError {
9696
PluginsError(#[from] PluginsConfigError),
9797
#[error("{0}")]
9898
ConversionError(#[from] ConversionError),
99+
#[error("{0}")]
100+
DownloadError(String),
99101
}
100102

101103
impl ConfigError {

zellij-utils/src/input/layout.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
// place.
99
// If plugins should be able to depend on the layout system
1010
// then [`zellij-utils`] could be a proper place.
11+
#[cfg(not(target_family = "wasm"))]
12+
use crate::downloader::Downloader;
1113
use crate::{
1214
data::{Direction, LayoutInfo},
1315
home::{default_layout_dir, find_default_config_dir},
@@ -18,6 +20,8 @@ use crate::{
1820
pane_size::{Constraint, Dimension, PaneGeom},
1921
setup::{self},
2022
};
23+
#[cfg(not(target_family = "wasm"))]
24+
use async_std::task;
2125

2226
use std::cmp::Ordering;
2327
use std::fmt::{Display, Formatter};
@@ -1145,6 +1149,7 @@ impl Layout {
11451149
LayoutInfo::BuiltIn(layout_name) => {
11461150
Self::stringified_from_default_assets(&PathBuf::from(layout_name))?
11471151
},
1152+
LayoutInfo::Url(url) => (url.clone(), Self::stringified_from_url(&url)?, None),
11481153
};
11491154
Layout::from_kdl(
11501155
&raw_layout,
@@ -1179,6 +1184,20 @@ impl Layout {
11791184
),
11801185
}
11811186
}
1187+
pub fn stringified_from_url(url: &str) -> Result<String, ConfigError> {
1188+
#[cfg(not(target_family = "wasm"))]
1189+
let raw_layout = task::block_on(async move {
1190+
let download = Downloader::download_without_cache(url).await;
1191+
match download {
1192+
Ok(stringified) => Ok(stringified),
1193+
Err(e) => Err(ConfigError::DownloadError(format!("{}", e))),
1194+
}
1195+
})?;
1196+
// silently fail - this should not happen in plugins and legacy architecture is hard
1197+
#[cfg(target_family = "wasm")]
1198+
let raw_layout = String::new();
1199+
Ok(raw_layout)
1200+
}
11821201
pub fn from_path_or_default(
11831202
layout_path: Option<&PathBuf>,
11841203
layout_dir: Option<PathBuf>,
@@ -1197,6 +1216,25 @@ impl Layout {
11971216
let config = Config::from_kdl(&raw_layout, Some(config))?; // this merges the two config, with
11981217
Ok((layout, config))
11991218
}
1219+
#[cfg(not(target_family = "wasm"))]
1220+
pub fn from_url(url: &str, config: Config) -> Result<(Layout, Config), ConfigError> {
1221+
let raw_layout = task::block_on(async move {
1222+
let download = Downloader::download_without_cache(url).await;
1223+
match download {
1224+
Ok(stringified) => Ok(stringified),
1225+
Err(e) => Err(ConfigError::DownloadError(format!("{}", e))),
1226+
}
1227+
})?;
1228+
let layout = Layout::from_kdl(&raw_layout, url.into(), None, None)?;
1229+
let config = Config::from_kdl(&raw_layout, Some(config))?; // this merges the two config, with
1230+
Ok((layout, config))
1231+
}
1232+
#[cfg(target_family = "wasm")]
1233+
pub fn from_url(url: &str, config: Config) -> Result<(Layout, Config), ConfigError> {
1234+
Err(ConfigError::DownloadError(format!(
1235+
"Unsupported platform, cannot download layout from the web"
1236+
)))
1237+
}
12001238
pub fn from_path_or_default_without_config(
12011239
layout_path: Option<&PathBuf>,
12021240
layout_dir: Option<PathBuf>,

zellij-utils/src/kdl/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2166,6 +2166,7 @@ impl SessionInfo {
21662166
let (layout_name, layout_source) = match layout_info {
21672167
LayoutInfo::File(name) => (name.clone(), "file"),
21682168
LayoutInfo::BuiltIn(name) => (name.clone(), "built-in"),
2169+
LayoutInfo::Url(url) => (url.clone(), "url"),
21692170
};
21702171
let mut layout_node = KdlNode::new(format!("{}", layout_name));
21712172
let layout_source = KdlEntry::new_prop("source", layout_source);

zellij-utils/src/plugin_api/event.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,10 @@ impl TryFrom<LayoutInfo> for ProtobufLayoutInfo {
545545
source: "built-in".to_owned(),
546546
name,
547547
}),
548+
LayoutInfo::Url(name) => Ok(ProtobufLayoutInfo {
549+
source: "url".to_owned(),
550+
name,
551+
}),
548552
}
549553
}
550554
}
@@ -555,6 +559,7 @@ impl TryFrom<ProtobufLayoutInfo> for LayoutInfo {
555559
match protobuf_layout_info.source.as_str() {
556560
"file" => Ok(LayoutInfo::File(protobuf_layout_info.name)),
557561
"built-in" => Ok(LayoutInfo::BuiltIn(protobuf_layout_info.name)),
562+
"url" => Ok(LayoutInfo::Url(protobuf_layout_info.name)),
558563
_ => Err("Unknown source for layout"),
559564
}
560565
}

zellij-utils/src/setup.rs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -671,9 +671,23 @@ impl Setup {
671671
.and_then(|cli_options| cli_options.default_layout.clone())
672672
})
673673
.or_else(|| config.options.default_layout.clone());
674-
// we merge-override the config here because the layout might contain configuration
675-
// that needs to take precedence
676-
Layout::from_path_or_default(chosen_layout.as_ref(), layout_dir.clone(), config)
674+
if let Some(layout_url) = chosen_layout
675+
.as_ref()
676+
.and_then(|l| l.to_str())
677+
.and_then(|l| {
678+
if l.starts_with("http://") || l.starts_with("https://") {
679+
Some(l)
680+
} else {
681+
None
682+
}
683+
})
684+
{
685+
Layout::from_url(layout_url, config)
686+
} else {
687+
// we merge-override the config here because the layout might contain configuration
688+
// that needs to take precedence
689+
Layout::from_path_or_default(chosen_layout.as_ref(), layout_dir.clone(), config)
690+
}
677691
}
678692
fn handle_setup_commands(cli_args: &CliArgs) {
679693
if let Some(Command::Setup(ref setup)) = &cli_args.command {

0 commit comments

Comments
 (0)