Initial public release of Warp.

Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
David Stern
2026-04-28 08:43:33 -05:00
commit 0dbd3d567a
4982 changed files with 1431549 additions and 0 deletions
+402
View File
@@ -0,0 +1,402 @@
pub mod util;
#[cfg_attr(not(target_family = "wasm"), path = "native.rs")]
#[cfg_attr(target_family = "wasm", path = "wasm.rs")]
mod imp;
use crate::tab_configs::{TabConfig, TabConfigError};
use crate::themes::theme::WarpThemeConfig;
use crate::{
launch_configs::launch_config::LaunchConfig, themes::theme::ThemeKind,
workflows::workflow::Workflow,
};
use lazy_static::lazy_static;
#[cfg(feature = "local_fs")]
use std::path::Path;
use std::path::PathBuf;
use warp_core::ui::theme::WarpTheme;
use warpui::{Entity, ModelContext, SingletonEntity};
#[cfg(feature = "local_fs")]
pub use imp::load_workflows;
pub use imp::{load_launch_configs, load_theme_configs};
lazy_static! {
pub static ref LAUNCH_CONFIG_COMMENT: String = format!(
"# Warp Launch Configuration
#
#
# Use this to start a certain configuration of windows, tabs, and panes.
# Open the launch configuration palette to access and open any launch configuration.
#
# This file defines your launch configuration.
# More on how to do so here:
# https://docs.warp.dev/terminal/sessions/launch-configurations
#
# All launch configurations are stored under {}.
# Edit them anytime!
#
# You can also add commands that run on-start for your launch configurations like so:
# ---
# name: Example with Command
# windows:
# - tabs:
# - layout:
# cwd: /Users/warp-user/project
# commands:
# - exec: code .
",
warp_core::paths::home_relative_path(&crate::user_config::launch_configs_dir())
);
}
#[derive(Clone)]
pub enum WarpConfigUpdateEvent {
Themes,
#[cfg_attr(not(feature = "local_fs"), expect(dead_code))]
LocalUserWorkflows,
LaunchConfigs,
#[cfg_attr(not(feature = "local_fs"), allow(dead_code))]
TabConfigs,
/// Emitted when one or more tab config files failed to parse.
#[cfg_attr(target_family = "wasm", allow(dead_code))]
TabConfigErrors(Vec<TabConfigError>),
/// The settings file (`settings.toml`) was created, modified, or deleted.
#[cfg_attr(not(feature = "local_fs"), expect(dead_code))]
Settings,
/// One or more settings in `settings.toml` could not be loaded.
#[cfg_attr(not(feature = "local_fs"), expect(dead_code))]
SettingsErrors(crate::settings::SettingsFileError),
/// A previously-errored settings reload succeeded with no errors.
#[cfg_attr(not(feature = "local_fs"), expect(dead_code))]
SettingsErrorsCleared,
}
/// Singleton model containing user configurable file entities like themes, launch configs, and
/// workflows.
///
/// Emits events when entities are changed, which are detected via filesystem
/// watchers on the user's `data_dir()` (themes, workflows, launch configs,
/// tab configs, etc.) and, on platforms where it differs, `config_local_dir()`
/// (`settings.toml`, `keybindings.yaml`, `user_preferences.json`).
#[derive(Default)]
pub struct WarpConfig {
launch_configs: Vec<LaunchConfig>,
tab_configs: Vec<TabConfig>,
#[cfg_attr(target_family = "wasm", allow(dead_code))]
tab_config_errors: Vec<TabConfigError>,
theme_config: WarpThemeConfig,
local_user_workflows: Vec<Workflow>,
}
/// Platform-independent parts of WarpConfig.
///
/// Additional platform-dependent functionality can be found in impl blocks
/// in native.rs and wasm.rs.
impl WarpConfig {
#[cfg(test)]
pub fn mock(_ctx: &mut ModelContext<Self>) -> Self {
Self {
theme_config: WarpThemeConfig::new(),
..Default::default()
}
}
pub fn launch_configs(&self) -> &Vec<LaunchConfig> {
&self.launch_configs
}
pub fn tab_configs(&self) -> &Vec<TabConfig> {
&self.tab_configs
}
pub fn theme_config(&self) -> &WarpThemeConfig {
&self.theme_config
}
pub fn local_user_workflows(&self) -> &Vec<Workflow> {
&self.local_user_workflows
}
/// Saving the newly created launch configuration to the WarpConfig that we currently
/// have.
pub fn append_launch_config(
&mut self,
launch_config: &LaunchConfig,
ctx: &mut ModelContext<Self>,
) {
if !self.launch_configs.contains(launch_config) {
self.launch_configs.push(launch_config.to_owned());
ctx.emit(WarpConfigUpdateEvent::LaunchConfigs);
}
}
pub fn update_theme_config(
&mut self,
theme_config: WarpThemeConfig,
ctx: &mut ModelContext<Self>,
) {
self.theme_config = theme_config;
ctx.emit(WarpConfigUpdateEvent::Themes);
}
pub fn add_new_theme_to_config(
&mut self,
theme_name: ThemeKind,
theme: WarpTheme,
ctx: &mut ModelContext<Self>,
) {
self.theme_config.add_new_theme(theme_name, theme);
ctx.emit(WarpConfigUpdateEvent::Themes);
}
/// Eagerly removes a tab config by its source path and emits a `TabConfigs` event.
/// (Used after deleting the file on disk so the menu updates immediately
/// rather than waiting for the filesystem watcher.)
#[cfg(feature = "local_fs")]
pub fn remove_tab_config_by_path(&mut self, path: &Path, ctx: &mut ModelContext<Self>) {
let before = self.tab_configs.len();
self.tab_configs
.retain(|c| c.source_path.as_deref() != Some(path));
if self.tab_configs.len() != before {
ctx.emit(WarpConfigUpdateEvent::TabConfigs);
}
}
}
/// Returns the base directory in which all of the user's data is stored.
fn base_dir() -> PathBuf {
warp_core::paths::data_dir()
}
/// Returns the path to the directory containing the user's custom themes.
pub fn themes_dir() -> PathBuf {
warp_core::paths::themes_dir()
}
/// Returns the path to the directory containing the user's custom workflows.
#[cfg_attr(target_family = "wasm", expect(dead_code))]
pub fn workflows_dir() -> PathBuf {
crate::workflows::local_workflows::workflows_dir(base_dir())
}
/// Returns the path to the directory containing the user's launch
/// configurations.
pub fn launch_configs_dir() -> PathBuf {
base_dir().join("launch_configurations")
}
/// Returns the path to the directory containing the user's tab configs.
#[cfg_attr(target_family = "wasm", expect(dead_code))]
pub fn tab_configs_dir() -> PathBuf {
base_dir().join("tab_configs")
}
/// Returns the path to the directory containing the built-in default tab configs.
/// These are shipped with Warp and user-editable (Warp does not overwrite modifications).
#[cfg_attr(target_family = "wasm", expect(dead_code))]
pub fn default_tab_configs_dir() -> PathBuf {
base_dir().join("default_tab_configs")
}
/// Returns whether the path points to a tab config TOML file under one of Warp's
/// tab config directories.
#[cfg(feature = "local_fs")]
pub fn is_tab_config_toml(path: &Path) -> bool {
let is_toml = path
.extension()
.and_then(|extension| extension.to_str())
.is_some_and(|extension| extension == "toml");
if !is_toml {
return false;
}
[tab_configs_dir(), default_tab_configs_dir()]
.into_iter()
.any(|dir| path.starts_with(dir))
}
/// Ensures `~/.warp/default_tab_configs/worktree.toml` exists, creating it
/// from the embedded template if missing. Returns the path to the file.
#[cfg(feature = "local_fs")]
pub(crate) fn ensure_default_worktree_config() -> PathBuf {
let dir = default_tab_configs_dir();
let path = dir.join("worktree.toml");
if !path.exists() {
log::info!("Default worktree config missing; creating at {path:?}");
if let Err(e) = std::fs::create_dir_all(&dir) {
log::warn!("Failed to create default_tab_configs dir at {dir:?}: {e:?}");
return path;
}
const TEMPLATE: &str = include_str!("../../resources/tab_configs/default_worktree.toml");
if let Err(e) = std::fs::write(&path, TEMPLATE) {
log::warn!("Failed to write default worktree config at {path:?}: {e:?}");
} else {
log::info!("Default worktree config created at {path:?}");
}
} else {
log::info!("Default worktree config already exists at {path:?}");
}
path
}
#[cfg(feature = "local_fs")]
pub(crate) fn materialize_default_worktree_config(
template_toml: &str,
config_name: &str,
repo_path: &str,
pane_type: &str,
) -> Result<(String, TabConfig), String> {
let worktree_path = crate::tab_configs::tab_config::generated_worktree_path_string(
Path::new(repo_path),
"{{autogenerated_branch_name}}",
);
let mut toml_value = toml::from_str::<toml::Value>(template_toml)
.map_err(|e| format!("failed to parse default worktree template: {e:?}"))?;
if let Some(doc) = toml_value.as_table_mut() {
doc.insert(
"name".to_string(),
toml::Value::String(config_name.to_string()),
);
}
replace_default_worktree_placeholders(&mut toml_value, repo_path, pane_type, &worktree_path);
if let Some(doc) = toml_value.as_table_mut() {
if let Some(params) = doc.get_mut("params").and_then(toml::Value::as_table_mut) {
params.remove("repo");
params.remove("pane_type");
if params.is_empty() {
doc.remove("params");
}
}
}
let toml_content = toml::to_string_pretty(&toml_value)
.map_err(|e| format!("failed to serialize default worktree config: {e:?}"))?;
let tab_config = toml::from_str::<TabConfig>(&toml_content)
.map_err(|e| format!("failed to parse materialized worktree config: {e:?}"))?;
Ok((toml_content, tab_config))
}
#[cfg(feature = "local_fs")]
fn replace_default_worktree_placeholders(
value: &mut toml::Value,
repo_path: &str,
pane_type: &str,
worktree_path: &str,
) {
match value {
toml::Value::String(string) => {
*string = string
.replace(
"{{worktree_path_prefix}}{{autogenerated_branch_name}}",
worktree_path,
)
.replace("{{repo}}", repo_path)
.replace("{{pane_type}}", pane_type)
.replace("{{worktree_path_prefix}}", "");
}
toml::Value::Array(array) => {
for value in array {
replace_default_worktree_placeholders(value, repo_path, pane_type, worktree_path);
}
}
toml::Value::Table(table) => {
for (_, value) in table.iter_mut() {
replace_default_worktree_placeholders(value, repo_path, pane_type, worktree_path);
}
}
toml::Value::Boolean(_)
| toml::Value::Datetime(_)
| toml::Value::Float(_)
| toml::Value::Integer(_) => {}
}
}
/// Returns a path for a new tab config file that does not yet exist in `dir`.
/// Tries `my_tab_config.toml`, then `my_tab_config_1.toml`, `my_tab_config_2.toml`, etc.
#[cfg(feature = "local_fs")]
pub(crate) fn find_unused_tab_config_path(dir: &Path) -> PathBuf {
find_unused_toml_path(dir, "my_tab_config")
}
/// Returns a `.toml` path in `dir` that does not yet exist.
///
/// Tries `{base_name}.toml`, then `{base_name}_1.toml`, `{base_name}_2.toml`, etc.
#[cfg(feature = "local_fs")]
pub(crate) fn find_unused_toml_path(dir: &Path, base_name: &str) -> PathBuf {
let base = dir.join(format!("{base_name}.toml"));
if !base.exists() {
return base;
}
let mut n = 1u32;
loop {
let candidate = dir.join(format!("{base_name}_{n}.toml"));
if !candidate.exists() {
return candidate;
}
n = n.saturating_add(1);
}
}
/// Sanitizes a suggested TOML filename base into a lowercase ASCII-ish stem.
///
/// Preserves ASCII letters, digits, hyphens, and underscores. All other
/// characters are replaced with underscores, repeated underscores are collapsed,
/// and leading/trailing underscores are removed.
#[cfg(feature = "local_fs")]
pub(crate) fn sanitize_toml_base_name(base_name: &str) -> String {
let mut sanitized = String::with_capacity(base_name.len());
let mut last_was_underscore = false;
for c in base_name.chars().flat_map(char::to_lowercase) {
if c.is_ascii_alphanumeric() || c == '-' || c == '_' {
sanitized.push(c);
last_was_underscore = c == '_';
} else if !last_was_underscore && !sanitized.is_empty() {
sanitized.push('_');
last_was_underscore = true;
}
}
sanitized = sanitized.trim_matches('_').to_string();
if sanitized.is_empty() {
"worktree".to_string()
} else {
sanitized
}
}
/// Returns a path for a new worktree tab config that does not yet exist in `dir`.
/// Uses the branch name to create a descriptive filename like `worktree_my-branch.toml`.
///
/// The caller is expected to pass a branch name that has already been validated
/// (alphanumeric, hyphens, underscores only) so no sanitization is performed here.
#[cfg(feature = "local_fs")]
pub(crate) fn find_unused_worktree_config_path(dir: &Path, branch_name: &str) -> PathBuf {
let base = dir.join(format!("worktree_{branch_name}.toml"));
if !base.exists() {
return base;
}
let mut n = 1u32;
loop {
let candidate = dir.join(format!("worktree_{branch_name}_{n}.toml"));
if !candidate.exists() {
return candidate;
}
n = n.saturating_add(1);
}
}
impl Entity for WarpConfig {
type Event = WarpConfigUpdateEvent;
}
impl SingletonEntity for WarpConfig {}
#[cfg(test)]
#[path = "mod_test.rs"]
mod tests;
+114
View File
@@ -0,0 +1,114 @@
use super::*;
use std::collections::HashMap;
use std::path::Path;
use crate::launch_configs::launch_config::PaneTemplateType;
use crate::tab_configs::render_tab_config;
use crate::tab_configs::tab_config::{generated_worktree_repo_dir, TabConfigPaneType};
#[cfg(feature = "local_fs")]
#[test]
fn test_default_tab_configs_dir_uses_underscores() {
assert!(default_tab_configs_dir().ends_with("default_tab_configs"));
}
#[cfg(feature = "local_fs")]
#[test]
fn test_is_tab_config_toml_matches_user_tab_configs() {
let path = tab_configs_dir().join("my_tab_config.toml");
assert!(is_tab_config_toml(&path));
}
#[cfg(feature = "local_fs")]
#[test]
fn test_is_tab_config_toml_matches_default_tab_configs() {
let path = default_tab_configs_dir().join("worktree.toml");
assert!(is_tab_config_toml(&path));
}
#[cfg(feature = "local_fs")]
#[test]
fn test_is_tab_config_toml_rejects_non_toml_paths() {
let path = tab_configs_dir().join("my_tab_config.yaml");
assert!(!is_tab_config_toml(&path));
}
#[cfg(feature = "local_fs")]
#[test]
fn test_is_tab_config_toml_rejects_tomls_outside_tab_config_dirs() {
let path = launch_configs_dir().join("workspace.toml");
assert!(!is_tab_config_toml(&path));
}
#[cfg(feature = "local_fs")]
#[test]
fn test_materialize_default_worktree_config_bakes_repo_and_pane_type_only() {
let template = include_str!("../../resources/tab_configs/default_worktree.toml");
let repo_path = "/tmp/example-repo";
let (toml_content, tab_config) =
materialize_default_worktree_config(template, "Worktree: example-repo", repo_path, "agent")
.expect("expected template materialization to succeed");
assert!(toml_content.contains("name = \"Worktree: example-repo\""));
assert!(toml_content.contains(repo_path));
assert!(toml_content.contains("type = \"agent\""));
assert!(toml_content.contains("{{autogenerated_branch_name}}"));
assert!(toml_content.contains(
&generated_worktree_repo_dir(Path::new(repo_path))
.display()
.to_string()
));
assert!(!toml_content.contains("{{repo}}"));
assert!(!toml_content.contains("{{pane_type}}"));
assert!(!toml_content.contains("{{worktree_path_prefix}}"));
assert!(!toml_content.contains("[params.repo]"));
assert!(!toml_content.contains("[params.pane_type]"));
assert!(tab_config.params.is_empty());
assert_eq!(tab_config.name, "Worktree: example-repo");
assert_eq!(tab_config.panes.len(), 1);
assert_eq!(tab_config.panes[0].directory.as_deref(), Some(repo_path));
assert_eq!(
tab_config.panes[0].pane_type,
Some(TabConfigPaneType::Agent)
);
}
#[cfg(feature = "local_fs")]
#[test]
fn test_materialized_default_worktree_config_renders_full_worktree_path() {
let template = include_str!("../../resources/tab_configs/default_worktree.toml");
let repo_path = "/tmp/example-repo";
let (_, tab_config) =
materialize_default_worktree_config(template, "Worktree: example-repo", repo_path, "agent")
.expect("expected template materialization to succeed");
let (_, pane_template) = render_tab_config(&tab_config, &HashMap::new(), Some("my-feature"));
if let PaneTemplateType::PaneTemplate { commands, .. } = pane_template {
let expected_worktree_path = generated_worktree_repo_dir(Path::new(repo_path))
.join("my-feature")
.display()
.to_string();
assert_eq!(commands.len(), 2);
assert_eq!(
commands[0].exec,
format!("git worktree add -b my-feature {expected_worktree_path}")
);
assert_eq!(commands[1].exec, format!("cd {expected_worktree_path}"));
} else {
panic!("expected terminal pane template");
}
}
#[cfg(feature = "local_fs")]
#[test]
fn test_sanitize_toml_base_name_replaces_spaces_and_dots() {
assert_eq!(sanitize_toml_base_name("My Project.v2"), "my_project_v2");
}
#[cfg(feature = "local_fs")]
#[test]
fn test_sanitize_toml_base_name_falls_back_for_empty_result() {
assert_eq!(sanitize_toml_base_name("..."), "worktree");
}
+220
View File
@@ -0,0 +1,220 @@
use std::fs;
use std::io;
use std::io::Write;
use std::path::Path;
use anyhow::{anyhow, Result};
use itertools::Itertools;
use repo_metadata::RepositoryUpdate;
use warpui::{ModelContext, SingletonEntity};
use crate::features::FeatureFlag;
use crate::launch_configs::launch_config::LaunchConfig;
use crate::tab_configs::{TabConfig, TabConfigError};
use crate::themes::theme::WarpThemeConfig;
use crate::warp_managed_paths_watcher::{
repository_update_touches_path, repository_update_touches_prefix, WarpManagedPathsWatcher,
WarpManagedPathsWatcherEvent,
};
use crate::workflows::workflow::Workflow;
use super::util::{
for_each_dir_entry, has_name, is_config_file, parse_multi_launch_config_dir_entry,
parse_multi_workflow_dir_entry, parse_single_theme_dir_entry, parse_tab_config_dir_entry,
};
use super::{
launch_configs_dir, tab_configs_dir, themes_dir, workflows_dir, WarpConfigUpdateEvent,
LAUNCH_CONFIG_COMMENT,
};
impl super::WarpConfig {
pub fn new(ctx: &mut ModelContext<Self>) -> Self {
// Load launch configs, and workflows from disk asynchronously on a background
// thread.
//
// Themes are required during initialization by `Settings`, so we load this synchronously
// on startup. We should investigate the possibility of offloading theme loading to a
// background thread in the future.
let _ = ctx.spawn(
async move { load_launch_configs(&launch_configs_dir()) },
|me, launch_configs, ctx| {
me.launch_configs = launch_configs;
ctx.emit(WarpConfigUpdateEvent::LaunchConfigs);
},
);
if FeatureFlag::TabConfigs.is_enabled() {
let _ = ctx.spawn(
async move { load_tab_configs(&tab_configs_dir()) },
|me, (tab_configs, tab_config_errors), ctx| {
me.tab_configs = tab_configs;
me.tab_config_errors = tab_config_errors;
ctx.emit(WarpConfigUpdateEvent::TabConfigs);
// Don't emit TabConfigErrors on startup — the error toast
// should only appear when the user saves a config file,
// not on app restart.
},
);
}
let _ = ctx.spawn(
async move { load_workflows(&workflows_dir()) },
|me, user_workflows, ctx| {
me.local_user_workflows = user_workflows;
ctx.emit(WarpConfigUpdateEvent::LocalUserWorkflows);
},
);
ctx.subscribe_to_model(
&WarpManagedPathsWatcher::handle(ctx),
Self::handle_warp_managed_paths_event,
);
Self {
theme_config: load_theme_configs(&themes_dir()),
..Default::default()
}
}
fn handle_warp_managed_paths_event(
&mut self,
event: &WarpManagedPathsWatcherEvent,
ctx: &mut ModelContext<Self>,
) {
let WarpManagedPathsWatcherEvent::FilesChanged(update) = event;
if update_touches_dir(update, &themes_dir()) {
let theme_dir = themes_dir();
let _ = ctx.spawn(
async move { load_theme_configs(&theme_dir) },
|me, theme_config, ctx| {
me.theme_config = theme_config;
ctx.emit(WarpConfigUpdateEvent::Themes);
},
);
}
if update_touches_dir(update, &workflows_dir()) {
let workflow_dir = workflows_dir();
let _ = ctx.spawn(
async move { load_workflows(&workflow_dir) },
|me, workflows, ctx| {
me.local_user_workflows = workflows;
ctx.emit(WarpConfigUpdateEvent::LocalUserWorkflows);
},
);
}
if update_touches_dir(update, &launch_configs_dir()) {
let launch_config_dir = launch_configs_dir();
let _ = ctx.spawn(
async move { load_launch_configs(&launch_config_dir) },
|me, launch_configs, ctx| {
me.launch_configs = launch_configs;
ctx.emit(WarpConfigUpdateEvent::LaunchConfigs);
},
);
}
if FeatureFlag::TabConfigs.is_enabled() && update_touches_dir(update, &tab_configs_dir()) {
let tab_config_dir = tab_configs_dir();
let _ = ctx.spawn(
async move { load_tab_configs(&tab_config_dir) },
|me, (configs, errors), ctx| {
me.tab_configs = configs;
me.tab_config_errors = errors.clone();
ctx.emit(WarpConfigUpdateEvent::TabConfigs);
if !errors.is_empty() {
ctx.emit(WarpConfigUpdateEvent::TabConfigErrors(errors));
}
},
);
}
if FeatureFlag::SettingsFile.is_enabled()
&& update_touches_path(update, &crate::settings::user_preferences_toml_file_path())
{
ctx.emit(WarpConfigUpdateEvent::Settings);
}
}
/// This method takes a file name candidate (appends .yaml if missing) and a LaunchConfig as
/// arguments. It saves the file and returns the filename used if successful.
#[cfg(feature = "local_fs")]
pub fn save_new_launch_config(
file_name: String,
launch_config: LaunchConfig,
) -> Result<String> {
let file_name = if is_config_file(&file_name) {
file_name.trim().into()
} else {
format!("{file_name}.yaml")
};
if !has_name(file_name.trim()) {
return Err(anyhow!("File name is empty"));
};
let path = crate::user_config::launch_configs_dir().join(&file_name);
if path.exists() {
return Err(anyhow!("File already exists"));
};
let file = crate::util::file::create_file(path)?;
let mut writer = io::BufWriter::new(file);
writer.write_all(LAUNCH_CONFIG_COMMENT.as_bytes())?;
serde_yaml::to_writer(writer, &launch_config)?;
Ok(file_name)
}
}
pub fn load_theme_configs(theme_path: &Path) -> WarpThemeConfig {
let mut theme_configs = WarpThemeConfig::new();
for_each_dir_entry(theme_path, parse_single_theme_dir_entry)
.into_iter()
.for_each(|(theme_name, theme)| theme_configs.add_new_theme(theme_name, theme));
theme_configs
}
/// Loads all workflows relative to the `workflow_path`. A YAML file might
/// contain multiple workflows.
pub fn load_workflows(workflow_path: &Path) -> Vec<Workflow> {
for_each_dir_entry(workflow_path, parse_multi_workflow_dir_entry)
.into_iter()
.flatten()
.collect_vec()
}
/// Loads all launch configs relative to the `launch_config_path`. Each workflow is assumed to be in an
/// individual YAML file.
pub fn load_launch_configs(launch_config_path: &Path) -> Vec<LaunchConfig> {
for_each_dir_entry(launch_config_path, parse_multi_launch_config_dir_entry)
.into_iter()
.flatten()
.collect_vec()
}
/// Loads all tab configs from `tab_config_path`. Each tab config is an individual TOML file.
///
/// Returns successfully parsed configs and any errors for files that failed to parse.
pub fn load_tab_configs(tab_config_path: &Path) -> (Vec<TabConfig>, Vec<TabConfigError>) {
let results = for_each_dir_entry(tab_config_path, parse_tab_config_dir_entry);
let mut configs = Vec::new();
let mut errors = Vec::new();
for result in results {
match result {
Ok(config) => configs.push(config),
Err(error) => errors.push(error),
}
}
(configs, errors)
}
fn update_touches_dir(update: &RepositoryUpdate, path: &Path) -> bool {
let canonical_path = fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
repository_update_touches_prefix(update, path)
|| repository_update_touches_prefix(update, &canonical_path)
}
fn update_touches_path(update: &RepositoryUpdate, path: &Path) -> bool {
let canonical_path = fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
repository_update_touches_path(update, path)
|| repository_update_touches_path(update, &canonical_path)
}
+228
View File
@@ -0,0 +1,228 @@
// Allowing dead code when targeting wasm as most of the functions in this
// module are only used on native.
#![cfg_attr(target_family = "wasm", allow(dead_code))]
use std::fs;
use std::path::{Path, PathBuf};
use itertools::Itertools;
use serde::de::DeserializeOwned;
use walkdir::{DirEntry, WalkDir};
use crate::launch_configs::launch_config::LaunchConfig;
use crate::tab_configs::{TabConfig, TabConfigError};
use crate::themes::theme::{ThemeKind, WarpTheme, WarpThemeConfig};
use crate::workflows::workflow::Workflow;
const CONFIG_FILE_SUFFIXES: &[&str] = &[".yaml", ".yml"];
const TOML_CONFIG_FILE_SUFFIX: &str = ".toml";
fn get_file_name(item: &DirEntry) -> Option<String> {
match item.metadata() {
Ok(metadata) if metadata.is_file() => item.file_name().to_str().map(|s| s.to_string()),
// The item was something else, like a directory.
Ok(_) => None,
// The file was deleted between when we generated the DirEntry and now.
Err(_) => None,
}
}
pub fn from_yaml<R>(path: PathBuf) -> anyhow::Result<R>
where
R: DeserializeOwned,
{
let file = fs::File::open(path.as_path())?;
let reader = std::io::BufReader::new(file);
let u: R = serde_yaml::from_reader(reader)?;
Ok(u)
}
pub fn from_toml<R>(path: PathBuf) -> anyhow::Result<R>
where
R: DeserializeOwned,
{
let contents = fs::read_to_string(path.as_path())?;
let u: R = toml::from_str(&contents)?;
Ok(u)
}
/// Deserializes a `DirEntry` into an object of type `G` if the file is a valid
/// config file containing a single item.
fn parse_single_item_file<T, F, G>(item: &DirEntry, post_deserialize_fn: F) -> Option<G>
where
F: Fn(String, T) -> G,
T: DeserializeOwned,
{
if let Some(file_name) = get_file_name(item) {
if is_config_file(&file_name) {
let parsed = from_yaml::<T>(item.path().into());
match parsed {
Ok(parsed) => return Some(post_deserialize_fn(file_name, parsed)),
Err(e) => {
log::warn!("Failed to parse config file at {file_name:?} with error: {e:?}")
}
}
}
}
None
}
fn from_multi_doc_yaml<R>(path: PathBuf) -> anyhow::Result<Vec<R>>
where
R: DeserializeOwned,
{
let file = fs::File::open(path.as_path())?;
let reader = std::io::BufReader::new(file);
serde_yaml::Deserializer::from_reader(reader)
.map(|document| R::deserialize(document).map_err(Into::into))
.collect()
}
/// Deserializes a `DirEntry` into an object of type `Vec<G>` if the file is a
/// valid config file containing one or more items.
fn parse_multi_item_file<T, F, G>(item: &DirEntry, post_deserialize_fn: F) -> Option<Vec<G>>
where
F: Fn(String, T) -> G,
T: DeserializeOwned,
{
if let Some(file_name) = get_file_name(item) {
if is_config_file(&file_name) {
let parsed = from_multi_doc_yaml::<T>(item.path().into());
match parsed {
Ok(parsed) => {
return Some(
parsed
.into_iter()
.map(|val| post_deserialize_fn(file_name.clone(), val))
.collect_vec(),
);
}
Err(e) => {
log::warn!("Failed to parse config file at {file_name:?} with error: {e:?}")
}
}
}
}
None
}
fn title_case(s: &str) -> String {
let lowercase = s.to_lowercase();
let mut chars = lowercase.chars();
chars
.next()
.map(|first_letter| first_letter.to_uppercase())
.into_iter()
.flatten()
.chain(chars)
.collect()
}
pub fn file_name_to_human_readable_name(file_name: &str) -> String {
let name = if let Some(suffix) = CONFIG_FILE_SUFFIXES
.iter()
.find(|&suffix| file_name.ends_with(suffix))
{
file_name.strip_suffix(suffix).unwrap_or(file_name)
} else {
file_name
};
name_to_camel_case(name)
}
fn name_to_camel_case(name: &str) -> String {
// Camel Case conversion (with spaces) treating each '_' as word separator.
// solarized_dark.yaml => Solarized Dark
// SOLARIZED_DARK.yaml => Solarized Dark
// SolarizedDark.yaml => Solarizeddark (note: no '_', so treating as single word)
name.split('_').map(title_case).join(" ")
}
pub(super) fn parse_single_theme_dir_entry(item: &DirEntry) -> Option<(ThemeKind, WarpTheme)> {
parse_single_item_file(item, |file_name, mut theme: WarpTheme| {
// If the name exists in the .yaml, we use it. Otherwise we treat a "human readable" version of the filename as the theme name.
let theme_kind = if let Some(name) = theme.name() {
WarpThemeConfig::file_to_theme(name, item.path().into())
} else {
let name = file_name_to_human_readable_name(file_name.as_str());
theme.set_name(name.clone());
WarpThemeConfig::file_to_theme(name, item.path().into())
};
(theme_kind, theme)
})
}
pub(super) fn parse_multi_workflow_dir_entry(item: &DirEntry) -> Option<Vec<Workflow>> {
parse_multi_item_file(item, |_, workflow| workflow)
}
pub(super) fn parse_multi_launch_config_dir_entry(item: &DirEntry) -> Option<Vec<LaunchConfig>> {
parse_multi_item_file(item, |_file_name, config| config)
}
pub(super) fn parse_tab_config_dir_entry(
item: &DirEntry,
) -> Option<Result<TabConfig, TabConfigError>> {
let file_name = get_file_name(item)?;
if !is_toml_file(&file_name) {
return None;
}
let parsed = from_toml::<TabConfig>(item.path().into());
Some(
parsed
.map(|mut config| {
config.source_path = Some(item.path().into());
config
})
.map_err(|e| TabConfigError {
file_name,
file_path: item.path().into(),
error_message: e.to_string(),
}),
)
}
/// Runs the given function on each `DirEntry` within the `Path`. If the path is not a directory,
/// an empty `Vector` is returned. It works recursively, covering directories within a given path.
pub(super) fn for_each_dir_entry<F, T>(path: &Path, dir_entry_fn: F) -> Vec<T>
where
F: Fn(&DirEntry) -> Option<T>,
{
if path.is_dir() {
WalkDir::new(path)
.follow_links(true)
.into_iter()
.filter_map(Result::ok)
.filter_map(|item| dir_entry_fn(&item))
.collect()
} else {
vec![]
}
}
/// Must end with config file suffix
pub(super) fn is_config_file(file_name: &str) -> bool {
CONFIG_FILE_SUFFIXES
.iter()
.any(|&suffix| file_name.ends_with(suffix))
}
/// Must end with `.toml`
pub(super) fn is_toml_file(file_name: &str) -> bool {
file_name.ends_with(TOML_CONFIG_FILE_SUFFIX)
}
/// Must have a name beyond the suffix
pub(super) fn has_name(file_name: &str) -> bool {
CONFIG_FILE_SUFFIXES
.iter()
.all(|&suffix| file_name != suffix && !file_name.is_empty())
}
#[cfg(test)]
#[path = "util_tests.rs"]
mod tests;
+30
View File
@@ -0,0 +1,30 @@
use super::*;
#[test]
fn title_case_test() {
assert_eq!("Test", title_case("test"));
assert_eq!("Test", title_case("TEST"));
assert_eq!("Test", title_case("Test"));
assert_eq!("Zażółć", title_case("Zażółć"));
assert_eq!("Zażółć", title_case("ZAŻÓŁĆ"));
}
#[test]
fn file_name_to_human_readable_name_test() {
assert_eq!(
"Solarized Dark",
file_name_to_human_readable_name("solarized_dark")
);
assert_eq!(
"Solarized Dark",
file_name_to_human_readable_name("solarized_dark.yaml")
);
assert_eq!(
"Solarized Dark",
file_name_to_human_readable_name("SOLARIZED_DARK.yaml")
);
assert_eq!(
"Solarizeddark",
file_name_to_human_readable_name("solarizeddark.yaml")
);
}
+41
View File
@@ -0,0 +1,41 @@
use std::path::Path;
use warpui::ModelContext;
use crate::launch_configs::launch_config::LaunchConfig;
use crate::themes::theme::WarpThemeConfig;
use crate::workflows::workflow::Workflow;
impl super::WarpConfig {
pub fn new(_ctx: &mut ModelContext<Self>) -> Self {
Self {
launch_configs: Default::default(),
tab_configs: Default::default(),
tab_config_errors: Default::default(),
theme_config: WarpThemeConfig::new(),
local_user_workflows: Default::default(),
}
}
}
/// Loads all themes relative to the `workflow_path`.
pub fn load_theme_configs(_theme_path: &Path) -> WarpThemeConfig {
// There's no local filesystem for wasm, so we'll never be able to retrieve
// themes from any path.
Default::default()
}
/// Loads all workflows relative to the `workflow_path`.
#[cfg_attr(target_family = "wasm", allow(dead_code))]
pub fn load_workflows(_workflow_path: &Path) -> Vec<Workflow> {
// There's no local filesystem for wasm, so we'll never be able to retrieve
// workflows from any path.
Default::default()
}
/// Loads all launch configs relative to the `launch_config_path`.
pub fn load_launch_configs(_launch_config_path: &Path) -> Vec<LaunchConfig> {
// There's no local filesystem for wasm, so we'll never be able to retrieve
// launch configs from any path.
Default::default()
}