#![doc = include_str!("../README.md")]
use std::collections::HashSet;
use std::fs;
use std::path::{Path, PathBuf};
pub mod app;
pub mod base_plugin;
pub mod commands;
pub mod layout;
mod plugin_tabs;
pub mod plugins;
pub mod product;
pub mod shell;
pub mod spec;
pub mod theme;
use gtk::prelude::*;
use gtk::Application;
pub use app::{
LauncherSpec, MaruzzellaHandle, ModeSwitchError, ShellChrome, ShellMode, WindowPolicy,
WorkspaceSession,
};
pub use maruzzella_sdk::attach_text_tooltip;
pub use plugins::{
diagnostic_for_load_error, diagnostic_for_runtime_error, load_plugin, load_static_plugin,
resolve_load_order, LoadedPlugin, PluginDependencySpec, PluginDescriptor, PluginDiagnostic,
PluginDiagnosticLevel, PluginHost, PluginLoadError, PluginLogEntry, PluginResolveError,
PluginRuntime, PluginRuntimeError, RegisteredCommand, RegisteredMenuItem, RegisteredService,
RegisteredSurfaceContribution, RegisteredViewFactory, Version as PluginVersion,
};
pub use product::{default_product_spec, BrandingSpec, LayoutContribution, ProductSpec};
pub use spec::{
plugin_tab, plugin_tab_with_instance, text_tab, BottomPanelLayout, CommandSpec, MenuItemSpec,
MenuRootSpec, PanelContentKind, ShellSpec, SplitAxis, TabGroupSpec, TabSpec,
PanelResizePolicy, ToolbarDisplayMode, ToolbarItemSpec,
WorkbenchNodeSpec,
};
pub use theme::{
ButtonAppearance, ButtonStyle, InputAppearance, SurfaceAppearance, SurfaceLevel,
TabStripAppearance, TabStripStyle, TextAppearance, TextRole, ThemeAppearances, ThemeDensity,
ThemePalette, ThemeSpec, ThemeStylesheet, ThemeTypography, Tone,
};
#[derive(Clone, Debug)]
pub struct MaruzzellaConfig {
pub application_id: String,
pub persistence_id: String,
pub product: ProductSpec,
pub startup_mode: ShellMode,
pub launcher: Option<LauncherSpec>,
pub workspace_window_policy: Option<WindowPolicy>,
pub launcher_window_policy: Option<WindowPolicy>,
pub theme: ThemeSpec,
pub plugin_paths: Vec<PathBuf>,
pub plugin_dirs: Vec<PathBuf>,
pub enable_default_plugin_discovery: bool,
pub builtin_plugins: Vec<fn() -> Result<plugins::LoadedPlugin, plugins::PluginLoadError>>,
}
impl Default for MaruzzellaConfig {
fn default() -> Self {
Self::new("com.lelloman.maruzzella")
}
}
impl MaruzzellaConfig {
pub fn new(application_id: &str) -> Self {
Self {
application_id: application_id.to_string(),
persistence_id: "maruzzella".to_string(),
product: default_product_spec(),
startup_mode: ShellMode::Workspace,
launcher: None,
workspace_window_policy: None,
launcher_window_policy: None,
theme: ThemeSpec::default(),
plugin_paths: Vec::new(),
plugin_dirs: Vec::new(),
enable_default_plugin_discovery: true,
builtin_plugins: Vec::new(),
}
}
pub fn with_persistence_id(mut self, persistence_id: &str) -> Self {
self.persistence_id = persistence_id.to_string();
self
}
pub fn with_product(mut self, product: ProductSpec) -> Self {
self.product = product;
self
}
pub fn with_startup_mode(mut self, startup_mode: ShellMode) -> Self {
self.startup_mode = startup_mode;
self
}
pub fn with_launcher(mut self, launcher: LauncherSpec) -> Self {
self.launcher = Some(launcher);
self
}
pub fn with_workspace_window_policy(mut self, policy: WindowPolicy) -> Self {
self.workspace_window_policy = Some(policy);
self
}
pub fn with_launcher_window_policy(mut self, policy: WindowPolicy) -> Self {
self.launcher_window_policy = Some(policy);
self
}
pub fn with_theme(mut self, theme: ThemeSpec) -> Self {
self.theme = theme;
self
}
pub fn with_plugin_path(mut self, path: impl AsRef<Path>) -> Self {
self.plugin_paths.push(path.as_ref().to_path_buf());
self
}
pub fn with_plugin_paths<I, P>(mut self, paths: I) -> Self
where
I: IntoIterator<Item = P>,
P: AsRef<Path>,
{
self.plugin_paths = paths
.into_iter()
.map(|path| path.as_ref().to_path_buf())
.collect();
self
}
pub fn with_plugin_dir(mut self, path: impl AsRef<Path>) -> Self {
self.plugin_dirs.push(path.as_ref().to_path_buf());
self
}
pub fn with_plugin_dirs<I, P>(mut self, paths: I) -> Self
where
I: IntoIterator<Item = P>,
P: AsRef<Path>,
{
self.plugin_dirs = paths
.into_iter()
.map(|path| path.as_ref().to_path_buf())
.collect();
self
}
pub fn with_default_plugin_discovery(mut self) -> Self {
self.enable_default_plugin_discovery = true;
self
}
pub fn without_default_plugin_discovery(mut self) -> Self {
self.enable_default_plugin_discovery = false;
self
}
pub fn with_builtin_plugin(
mut self,
loader: fn() -> Result<plugins::LoadedPlugin, plugins::PluginLoadError>,
) -> Self {
self.builtin_plugins.push(loader);
self
}
pub fn with_builtin_plugins<I>(mut self, loaders: I) -> Self
where
I: IntoIterator<Item = fn() -> Result<plugins::LoadedPlugin, plugins::PluginLoadError>>,
{
self.builtin_plugins = loaders.into_iter().collect();
self
}
}
pub fn build_application(config: MaruzzellaConfig) -> Application {
build_application_with_handle(config).0
}
pub fn build_application_with_handle(config: MaruzzellaConfig) -> (Application, MaruzzellaHandle) {
let config_for_activate = config.clone();
let handle = MaruzzellaHandle::default();
let handle_for_activate = handle.clone();
let application = build_application_with_activate(&config.application_id, move |application| {
app::build(application, &config_for_activate, &handle_for_activate);
});
(application, handle)
}
pub fn build_application_with_activate<F>(application_id: &str, activate: F) -> Application
where
F: Fn(&Application) + 'static,
{
let application = Application::builder()
.application_id(application_id)
.build();
application.connect_activate(activate);
application
}
pub fn run_default() {
run(MaruzzellaConfig::default());
}
pub fn run(config: MaruzzellaConfig) {
build_application(config).run();
}
pub fn default_plugin_discovery_dirs(persistence_id: &str) -> Vec<PathBuf> {
let mut dirs = vec![plugin_config_root(persistence_id).join("plugins"), PathBuf::from("plugins")];
dirs.retain(|dir| !dir.as_os_str().is_empty());
let mut seen = HashSet::new();
dirs.into_iter()
.filter(|dir| seen.insert(dir.clone()))
.collect()
}
pub fn discover_plugin_paths_in_dir(dir: impl AsRef<Path>) -> Vec<PathBuf> {
let Ok(entries) = fs::read_dir(dir) else {
return Vec::new();
};
let mut paths = entries
.filter_map(|entry| entry.ok().map(|entry| entry.path()))
.filter(|path| path.is_file() && is_dynamic_plugin_library(path))
.collect::<Vec<_>>();
paths.sort();
paths
}
pub fn is_dynamic_plugin_library(path: impl AsRef<Path>) -> bool {
let Some(extension) = path.as_ref().extension().and_then(|ext| ext.to_str()) else {
return false;
};
#[cfg(target_os = "windows")]
{
extension.eq_ignore_ascii_case("dll")
}
#[cfg(target_os = "macos")]
{
extension.eq_ignore_ascii_case("dylib")
}
#[cfg(all(unix, not(target_os = "macos")))]
{
extension.eq_ignore_ascii_case("so")
}
}
fn plugin_config_root(persistence_id: &str) -> PathBuf {
let mut root = if let Ok(dir) = std::env::var("XDG_CONFIG_HOME") {
PathBuf::from(dir)
} else if let Ok(home) = std::env::var("HOME") {
PathBuf::from(home).join(".config")
} else {
PathBuf::from(".")
};
root.push(persistence_id);
root
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_plugin_discovery_includes_config_and_local_dirs() {
let dirs = default_plugin_discovery_dirs("maruzzella-test");
assert!(dirs.iter().any(|dir| dir.ends_with("maruzzella-test/plugins")));
assert!(dirs.iter().any(|dir| dir == Path::new("plugins")));
}
#[test]
fn discovery_filters_for_platform_plugin_libraries() {
let temp = std::env::temp_dir().join(format!(
"maruzzella-discovery-{}",
std::process::id()
));
let _ = fs::create_dir_all(&temp);
let plugin_name = if cfg!(target_os = "windows") {
"example_plugin.dll"
} else if cfg!(target_os = "macos") {
"libexample_plugin.dylib"
} else {
"libexample_plugin.so"
};
let plugin_path = temp.join(plugin_name);
let non_plugin_path = temp.join("README.md");
let _ = fs::write(&plugin_path, []);
let _ = fs::write(&non_plugin_path, []);
let discovered = discover_plugin_paths_in_dir(&temp);
assert_eq!(discovered, vec![plugin_path.clone()]);
assert!(is_dynamic_plugin_library(&plugin_path));
assert!(!is_dynamic_plugin_library(&non_plugin_path));
let _ = fs::remove_file(plugin_path);
let _ = fs::remove_file(non_plugin_path);
let _ = fs::remove_dir(temp);
}
}