Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 19 additions & 10 deletions src/actions/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@
//!
//! Flags are comma-separated (`,`) and values use `key=value`, for example:
//! `layout:load:Work|dry_run,only_active_monitor,filter=chrome`.
use crate::common::config_files::{ensure_config_file, ConfigFileResult};
use crate::plugins::layouts_storage::{
self, list_layouts as list_saved_layouts, remove_layout as remove_saved_layout, Layout,
LayoutCoordMode, LayoutOptions, LayoutWindowLaunch, LAYOUTS_FILE,
self, layouts_config_path, list_layouts as list_saved_layouts,
remove_layout as remove_saved_layout, Layout, LayoutCoordMode, LayoutOptions,
LayoutWindowLaunch, LAYOUTS_CONFIG,
};
use crate::settings;
use crate::windows_layout::{
apply_layout_restore_plan, collect_layout_windows, plan_layout_restore, LayoutMatchResult,
LayoutRestoreSummary, LayoutWindowOptions,
Expand Down Expand Up @@ -556,7 +559,7 @@ pub fn save_layout(name: &str, flags: Option<&str>) -> anyhow::Result<()> {
return Ok(());
}

let mut store = layouts_storage::load_layouts(LAYOUTS_FILE)?;
let mut store = layouts_storage::load_layouts(layouts_config_path())?;
let windows = collect_layout_windows(LayoutWindowOptions {
only_active_monitor: flags.only_active_monitor,
include_minimized: flags.include_minimized,
Expand All @@ -572,14 +575,14 @@ pub fn save_layout(name: &str, flags: Option<&str>) -> anyhow::Result<()> {
ignore: Vec::new(),
};
layouts_storage::upsert_layout(&mut store, layout);
layouts_storage::save_layouts(LAYOUTS_FILE, &store)?;
layouts_storage::save_layouts(layouts_config_path(), &store)?;
Ok(())
}

pub fn load_layout(name: &str, flags: Option<&str>) -> anyhow::Result<()> {
ensure_layout_name(name)?;
let flags = parse_flags(flags);
let store = layouts_storage::load_layouts(LAYOUTS_FILE)?;
let store = layouts_storage::load_layouts(layouts_config_path())?;
let layout = layouts_storage::get_layout(&store, name)
.ok_or_else(|| anyhow::anyhow!("layout '{name}' not found"))?;
let layout = filter_layout_by_groups(layout, &flags.only_groups);
Expand Down Expand Up @@ -665,7 +668,7 @@ pub fn load_layout(name: &str, flags: Option<&str>) -> anyhow::Result<()> {
pub fn show_layout(name: &str, flags: Option<&str>) -> anyhow::Result<()> {
ensure_layout_name(name)?;
let flags = parse_flags(flags);
let store = layouts_storage::load_layouts(LAYOUTS_FILE)?;
let store = layouts_storage::load_layouts(layouts_config_path())?;
let layout = layouts_storage::get_layout(&store, name)
.ok_or_else(|| anyhow::anyhow!("layout '{name}' not found"))?;
let layout = filter_layout_by_groups(layout, &flags.only_groups);
Expand Down Expand Up @@ -709,30 +712,36 @@ pub fn show_layout(name: &str, flags: Option<&str>) -> anyhow::Result<()> {
}

pub fn edit_layouts() -> anyhow::Result<()> {
open::that(LAYOUTS_FILE)?;
let settings_path = settings::settings_path();
let result = ensure_config_file(&settings_path, &LAYOUTS_CONFIG)?;
match &result {
ConfigFileResult::Opened { path } | ConfigFileResult::Created { path } => {
open::that(path)?;
}
}
Ok(())
}

pub fn remove_layout(name: &str, flags: Option<&str>) -> anyhow::Result<()> {
ensure_layout_name(name)?;
let flags = parse_flags(flags);
let mut store = layouts_storage::load_layouts(LAYOUTS_FILE)?;
let mut store = layouts_storage::load_layouts(layouts_config_path())?;
if layouts_storage::get_layout(&store, name).is_none() {
anyhow::bail!("layout '{name}' not found");
}
if flags.dry_run {
return Ok(());
}
let _ = remove_saved_layout(&mut store, name);
layouts_storage::save_layouts(LAYOUTS_FILE, &store)?;
layouts_storage::save_layouts(layouts_config_path(), &store)?;
Ok(())
}

pub fn list_layouts(flags: Option<&str>) -> anyhow::Result<()> {
use std::fmt::Write as _;

let flags = parse_flags(flags);
let store = layouts_storage::load_layouts(LAYOUTS_FILE)?;
let store = layouts_storage::load_layouts(layouts_config_path())?;
let mut list = list_saved_layouts(&store);
if let Some(filter) = flags.filter {
let needle = filter.to_lowercase();
Expand Down
83 changes: 83 additions & 0 deletions src/common/config_files.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
use std::path::{Path, PathBuf};

pub enum ConfigFileResult {
Opened { path: PathBuf },
Created { path: PathBuf },
}

pub struct ConfigFileSpec<'a> {
pub label: &'a str,
pub relative_path: &'a str,
pub default_contents: &'a str,
}

impl<'a> ConfigFileSpec<'a> {
pub const fn new(label: &'a str, relative_path: &'a str, default_contents: &'a str) -> Self {
Self {
label,
relative_path,
default_contents,
}
}
}

pub fn resolve_config_path(settings_path: &Path, spec: &ConfigFileSpec<'_>) -> PathBuf {
let base_dir = settings_path.parent().unwrap_or_else(|| Path::new("."));
base_dir.join(spec.relative_path)
}

pub fn ensure_config_file(
settings_path: &Path,
spec: &ConfigFileSpec<'_>,
) -> anyhow::Result<ConfigFileResult> {
let path = resolve_config_path(settings_path, spec);
if path.exists() {
return Ok(ConfigFileResult::Opened { path });
}
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&path, spec.default_contents)?;
Ok(ConfigFileResult::Created { path })
}

#[cfg(test)]
mod tests {
use super::{ensure_config_file, resolve_config_path, ConfigFileResult, ConfigFileSpec};
use std::path::Path;

#[test]
fn resolves_path_relative_to_settings_dir() {
let dir = tempfile::tempdir().expect("tempdir");
let settings_path = dir.path().join("settings.json");
let spec = ConfigFileSpec::new("test", "configs/test.json", "{}");
let resolved = resolve_config_path(&settings_path, &spec);
assert_eq!(
resolved,
dir.path().join(Path::new("configs").join("test.json"))
);
}

#[test]
fn creates_and_reuses_config_file() {
let dir = tempfile::tempdir().expect("tempdir");
let settings_path = dir.path().join("settings.json");
let spec = ConfigFileSpec::new("test", "test.json", "default");

let created = ensure_config_file(&settings_path, &spec).expect("create file");
let path = match created {
ConfigFileResult::Created { path } => path,
ConfigFileResult::Opened { .. } => panic!("expected create"),
};
let contents = std::fs::read_to_string(&path).expect("read file");
assert_eq!(contents, "default");

let opened = ensure_config_file(&settings_path, &spec).expect("open file");
match opened {
ConfigFileResult::Opened { path: opened_path } => {
assert_eq!(opened_path, path);
}
ConfigFileResult::Created { .. } => panic!("expected open"),
}
}
}
1 change: 1 addition & 0 deletions src/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub fn strip_prefix_ci<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
}

pub mod command;
pub mod config_files;
pub mod json_watch;
pub mod slug;
pub mod lru;
Expand Down
39 changes: 25 additions & 14 deletions src/dashboard/widgets/layouts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use super::{
};
use crate::actions::Action;
use crate::dashboard::dashboard::{DashboardContext, WidgetActivation};
use crate::plugins::layouts_storage::{self, Layout, LayoutMatch, LayoutStore, LAYOUTS_FILE};
use crate::plugins::layouts_storage::{self, layouts_config_path, Layout, LayoutMatch, LayoutStore};
use crate::windows_layout::{collect_layout_windows, LayoutWindowOptions};
use chrono::Utc;
use eframe::egui;
Expand Down Expand Up @@ -174,7 +174,7 @@ impl LayoutsWidget {
}

fn load_active_layout(&mut self, name: &str) -> Result<(), String> {
let store = layouts_storage::load_layouts(LAYOUTS_FILE)
let store = layouts_storage::load_layouts(layouts_config_path())
.map_err(|err| format!("Failed to load layouts: {err}"))?;
let layout = layouts_storage::get_layout(&store, name)
.ok_or_else(|| "Layout not found.".to_string())?
Expand All @@ -187,10 +187,10 @@ impl LayoutsWidget {
}

fn save_layout(&mut self, layout: Layout) -> Result<(), String> {
let mut store = layouts_storage::load_layouts(LAYOUTS_FILE)
let mut store = layouts_storage::load_layouts(layouts_config_path())
.map_err(|err| format!("Failed to load layouts: {err}"))?;
layouts_storage::upsert_layout(&mut store, layout.clone());
layouts_storage::save_layouts(LAYOUTS_FILE, &store)
layouts_storage::save_layouts(layouts_config_path(), &store)
.map_err(|err| format!("Failed to save layouts: {err}"))?;
self.active_layout_name = Some(layout.name.clone());
self.active_layout = Some(layout.clone());
Expand Down Expand Up @@ -234,14 +234,14 @@ impl LayoutsWidget {
let Some(layout) = self.active_layout.clone() else {
return Err("No active layout selected.".to_string());
};
let mut store = layouts_storage::load_layouts(LAYOUTS_FILE)
let mut store = layouts_storage::load_layouts(layouts_config_path())
.map_err(|err| format!("Failed to load layouts: {err}"))?;
let new_name = Self::unique_layout_name(&layout.name, &store, " (copy)");
let mut cloned = layout.clone();
cloned.name = new_name.clone();
cloned.created_at = Some(Utc::now().to_rfc3339());
layouts_storage::upsert_layout(&mut store, cloned.clone());
layouts_storage::save_layouts(LAYOUTS_FILE, &store)
layouts_storage::save_layouts(layouts_config_path(), &store)
.map_err(|err| format!("Failed to save layouts: {err}"))?;
self.active_layout_name = Some(new_name.clone());
self.active_layout = Some(cloned.clone());
Expand Down Expand Up @@ -321,7 +321,7 @@ impl LayoutsWidget {
}

fn load_layouts(cfg: &LayoutsConfig) -> (LayoutsData, Option<String>) {
let store = match layouts_storage::load_layouts(LAYOUTS_FILE) {
let store = match layouts_storage::load_layouts(layouts_config_path()) {
Ok(store) => store,
Err(err) => {
return (
Expand Down Expand Up @@ -407,7 +407,7 @@ impl LayoutsWidget {
if new_name == from {
return Ok(());
}
let mut store = layouts_storage::load_layouts(LAYOUTS_FILE)
let mut store = layouts_storage::load_layouts(layouts_config_path())
.map_err(|err| format!("Failed to load layouts: {err}"))?;
if store.layouts.iter().any(|layout| layout.name == new_name) {
return Err("A layout with that name already exists.".to_string());
Expand All @@ -416,7 +416,7 @@ impl LayoutsWidget {
return Err("Layout not found.".to_string());
};
layout.name = new_name.to_string();
layouts_storage::save_layouts(LAYOUTS_FILE, &store)
layouts_storage::save_layouts(layouts_config_path(), &store)
.map_err(|err| format!("Failed to save layouts: {err}"))?;
if self.active_layout_name.as_deref() == Some(from) {
if let Some(active) = self.active_layout.as_mut() {
Expand Down Expand Up @@ -602,7 +602,7 @@ impl Widget for LayoutsWidget {
self.pending_import = None;
}
if ui.button("Import as new").clicked() {
let mut store = match layouts_storage::load_layouts(LAYOUTS_FILE) {
let mut store = match layouts_storage::load_layouts(layouts_config_path()) {
Ok(store) => store,
Err(err) => {
self.set_status(
Expand All @@ -618,7 +618,7 @@ impl Widget for LayoutsWidget {
Self::unique_layout_name(&imported.name, &store, " (imported)");
imported.name = new_name.clone();
layouts_storage::upsert_layout(&mut store, imported.clone());
if let Err(err) = layouts_storage::save_layouts(LAYOUTS_FILE, &store) {
if let Err(err) = layouts_storage::save_layouts(layouts_config_path(), &store) {
self.set_status(
format!("Failed to save layouts: {err}"),
egui::Color32::YELLOW,
Expand Down Expand Up @@ -670,10 +670,21 @@ impl Widget for LayoutsWidget {
));
ui.close_menu();
}
if ui.button("Edit JSON").clicked() {
let layouts_path = layouts_config_path();
let layouts_exists = layouts_path.exists();
let layouts_label = if layouts_exists {
"Edit layouts.json"
} else {
"Create layouts.json"
};
if ui.button(layouts_label).clicked() {
selected = Some(Self::action(
"Open layouts.json".to_string(),
LAYOUTS_FILE.to_string(),
format!(
"{} ({})",
layouts_label,
layouts_path.to_string_lossy()
),
"layout:edit".to_string(),
));
ui.close_menu();
}
Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ fn spawn_gui(

fn main() -> anyhow::Result<()> {
let mut settings = Settings::load("settings.json").unwrap_or_default();
multi_launcher::settings::set_settings_path("settings.json");
logging::init(settings.debug_logging, settings.log_file_path());
tracing::debug!(?settings, "settings loaded");
let mut actions_vec = load_actions("actions.json").unwrap_or_default();
Expand Down
3 changes: 3 additions & 0 deletions src/plugins/clipboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@ impl ClipboardPlugin {
Some(h) => h,
None => return VecDeque::new(),
};
if std::env::var("ML_SKIP_CLIPBOARD_SYNC").is_ok() {
return history.clone();
}
let mut cb_lock = match self.clipboard.lock().ok() {
Some(c) => c,
None => return history.clone(),
Expand Down
24 changes: 17 additions & 7 deletions src/plugins/layout.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use crate::actions::Action;
use crate::plugin::Plugin;
use crate::plugins::layouts_storage::{get_layout, load_layouts, LayoutMatch, LAYOUTS_FILE};
use crate::plugins::layouts_storage::{
get_layout, layouts_config_path, load_layouts, LayoutMatch,
};

#[derive(Default, Debug, Clone)]
struct LayoutFlags {
Expand Down Expand Up @@ -151,6 +153,14 @@ pub struct LayoutPlugin;

impl Plugin for LayoutPlugin {
fn search(&self, query: &str) -> Vec<Action> {
let config_path = layouts_config_path();
let config_exists = config_path.exists();
let config_label = if config_exists {
"Edit layouts.json"
} else {
"Create layouts.json"
};
let config_desc = config_path.to_string_lossy();
let trimmed = query.trim();
let rest = match crate::common::strip_prefix_ci(trimmed, "layout") {
Some(rest) => rest.trim(),
Expand Down Expand Up @@ -184,8 +194,8 @@ impl Plugin for LayoutPlugin {
args: None,
},
Action {
label: "layout edit".into(),
desc: "Layout".into(),
label: format!("{config_label} ({config_desc})"),
desc: "Layout config".into(),
action: "layout:edit".into(),
args: None,
},
Expand All @@ -200,7 +210,7 @@ impl Plugin for LayoutPlugin {

if let Some(rest) = crate::common::strip_prefix_ci(rest, "list") {
let filter = rest.trim().to_lowercase();
if let Ok(store) = load_layouts(LAYOUTS_FILE) {
if let Ok(store) = load_layouts(layouts_config_path()) {
return store
.layouts
.iter()
Expand Down Expand Up @@ -249,7 +259,7 @@ impl Plugin for LayoutPlugin {

if let Some(rest) = crate::common::strip_prefix_ci(rest, "show") {
let (name, flags) = parse_name_and_flags(rest.trim());
if let Ok(store) = load_layouts(LAYOUTS_FILE) {
if let Ok(store) = load_layouts(layouts_config_path()) {
if !name.is_empty() {
if let Some(layout) = get_layout(&store, &name) {
let action = build_action(format!("layout:show:{}", layout.name), &flags);
Expand Down Expand Up @@ -314,8 +324,8 @@ impl Plugin for LayoutPlugin {
if let Some(rest) = crate::common::strip_prefix_ci(rest, "edit") {
if rest.trim().is_empty() {
return vec![Action {
label: "Edit layouts.json".into(),
desc: "Layout".into(),
label: format!("{config_label} ({config_desc})"),
desc: "Layout config".into(),
action: "layout:edit".into(),
args: None,
}];
Expand Down
Loading