fix(owlry-core,owlry): preserve config on save, fix tab filtering, clean provider fields
- Config::save() now merges into existing file via toml_edit to preserve user comments and unknown keys instead of overwriting - Remove 16 dead ProvidersConfig plugin-toggle fields (uuctl, ssh, clipboard, bookmarks, emoji, scripts, files, media, weather_*, pomodoro_*) that became unreachable after accept_all=true was introduced - Wire general.tabs to ProviderFilter::new() to drive UI tab display - Fix set_single_mode() and toggle() not clearing accept_all, making tab filtering a no-op in default launch mode - Add restore_all_mode() for cycle-back-to-All path in tab cycling - Reduce PROVIDER_TOGGLES to built-in providers only
This commit is contained in:
@@ -2,6 +2,7 @@ use log::{debug, info, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use toml_edit::{DocumentMut, Item};
|
||||
|
||||
use crate::paths;
|
||||
|
||||
@@ -157,82 +158,26 @@ pub struct ProvidersConfig {
|
||||
pub applications: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub commands: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub uuctl: bool,
|
||||
/// Enable calculator provider (= expression or calc expression)
|
||||
/// Enable built-in calculator provider (= or calc trigger)
|
||||
#[serde(default = "default_true")]
|
||||
pub calculator: bool,
|
||||
/// Enable converter provider (> expression or auto-detect)
|
||||
/// Enable built-in unit/currency converter (> trigger)
|
||||
#[serde(default = "default_true")]
|
||||
pub converter: bool,
|
||||
/// Enable built-in system actions (shutdown, reboot, lock, etc.)
|
||||
#[serde(default = "default_true")]
|
||||
pub system: bool,
|
||||
/// Enable frecency-based result ranking
|
||||
#[serde(default = "default_true")]
|
||||
pub frecency: bool,
|
||||
/// Weight for frecency boost (0.0 = disabled, 1.0 = strong boost)
|
||||
#[serde(default = "default_frecency_weight")]
|
||||
pub frecency_weight: f64,
|
||||
/// Enable web search provider (? query or web query)
|
||||
#[serde(default = "default_true")]
|
||||
pub websearch: bool,
|
||||
/// Search engine for web search
|
||||
/// Search engine for web search (used by owlry-plugin-websearch)
|
||||
/// Options: google, duckduckgo, bing, startpage, searxng, brave, ecosia
|
||||
/// Or custom URL with {query} placeholder
|
||||
/// Or a custom URL with a {query} placeholder
|
||||
#[serde(default = "default_search_engine")]
|
||||
pub search_engine: String,
|
||||
/// Enable system commands (shutdown, reboot, etc.)
|
||||
#[serde(default = "default_true")]
|
||||
pub system: bool,
|
||||
/// Enable SSH connections from ~/.ssh/config
|
||||
#[serde(default = "default_true")]
|
||||
pub ssh: bool,
|
||||
/// Enable clipboard history (requires cliphist)
|
||||
#[serde(default = "default_true")]
|
||||
pub clipboard: bool,
|
||||
/// Enable browser bookmarks
|
||||
#[serde(default = "default_true")]
|
||||
pub bookmarks: bool,
|
||||
/// Enable emoji picker
|
||||
#[serde(default = "default_true")]
|
||||
pub emoji: bool,
|
||||
/// Enable custom scripts from ~/.config/owlry/scripts/
|
||||
#[serde(default = "default_true")]
|
||||
pub scripts: bool,
|
||||
/// Enable file search (requires fd or locate)
|
||||
#[serde(default = "default_true")]
|
||||
pub files: bool,
|
||||
|
||||
// ─── Widget Providers ───────────────────────────────────────────────
|
||||
/// Enable MPRIS media player widget
|
||||
#[serde(default = "default_true")]
|
||||
pub media: bool,
|
||||
|
||||
/// Enable weather widget
|
||||
#[serde(default)]
|
||||
pub weather: bool,
|
||||
|
||||
/// Weather provider: wttr.in (default), openweathermap, open-meteo
|
||||
#[serde(default = "default_weather_provider")]
|
||||
pub weather_provider: String,
|
||||
|
||||
/// API key for weather services that require it (e.g., OpenWeatherMap)
|
||||
#[serde(default)]
|
||||
pub weather_api_key: Option<String>,
|
||||
|
||||
/// Location for weather (city name or coordinates)
|
||||
#[serde(default)]
|
||||
pub weather_location: Option<String>,
|
||||
|
||||
/// Enable pomodoro timer widget
|
||||
#[serde(default)]
|
||||
pub pomodoro: bool,
|
||||
|
||||
/// Pomodoro work duration in minutes
|
||||
#[serde(default = "default_pomodoro_work")]
|
||||
pub pomodoro_work_mins: u32,
|
||||
|
||||
/// Pomodoro break duration in minutes
|
||||
#[serde(default = "default_pomodoro_break")]
|
||||
pub pomodoro_break_mins: u32,
|
||||
}
|
||||
|
||||
impl Default for ProvidersConfig {
|
||||
@@ -240,28 +185,12 @@ impl Default for ProvidersConfig {
|
||||
Self {
|
||||
applications: true,
|
||||
commands: true,
|
||||
uuctl: true,
|
||||
calculator: true,
|
||||
converter: true,
|
||||
system: true,
|
||||
frecency: true,
|
||||
frecency_weight: 0.3,
|
||||
websearch: true,
|
||||
search_engine: "duckduckgo".to_string(),
|
||||
system: true,
|
||||
ssh: true,
|
||||
clipboard: true,
|
||||
bookmarks: true,
|
||||
emoji: true,
|
||||
scripts: true,
|
||||
files: true,
|
||||
media: true,
|
||||
weather: false,
|
||||
weather_provider: "wttr.in".to_string(),
|
||||
weather_api_key: None,
|
||||
weather_location: Some("Berlin".to_string()),
|
||||
pomodoro: false,
|
||||
pomodoro_work_mins: 25,
|
||||
pomodoro_break_mins: 5,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -399,18 +328,6 @@ fn default_frecency_weight() -> f64 {
|
||||
0.3
|
||||
}
|
||||
|
||||
fn default_weather_provider() -> String {
|
||||
"wttr.in".to_string()
|
||||
}
|
||||
|
||||
fn default_pomodoro_work() -> u32 {
|
||||
25
|
||||
}
|
||||
|
||||
fn default_pomodoro_break() -> u32 {
|
||||
5
|
||||
}
|
||||
|
||||
/// Detect the best available terminal emulator
|
||||
/// Fallback chain:
|
||||
/// 1. $TERMINAL env var (user's explicit preference)
|
||||
@@ -539,6 +456,38 @@ fn command_exists(cmd: &str) -> bool {
|
||||
|
||||
// Note: Config derives Default via #[derive(Default)] - all sub-structs have impl Default
|
||||
|
||||
/// Merge `new` into `existing`, updating values while preserving comments and unknown keys.
|
||||
///
|
||||
/// Tables are recursed into so that section-level comments survive. For leaf values
|
||||
/// (scalars, arrays) the item is replaced but the surrounding table structure — and
|
||||
/// any keys in `existing` that are absent from `new` — are left untouched.
|
||||
fn merge_toml_doc(existing: &mut DocumentMut, new: &DocumentMut) {
|
||||
for (key, new_item) in new.iter() {
|
||||
match existing.get_mut(key) {
|
||||
Some(existing_item) => merge_item(existing_item, new_item),
|
||||
None => {
|
||||
existing.insert(key, new_item.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_item(existing: &mut Item, new: &Item) {
|
||||
match (existing.as_table_mut(), new.as_table()) {
|
||||
(Some(e), Some(n)) => {
|
||||
for (key, new_child) in n.iter() {
|
||||
match e.get_mut(key) {
|
||||
Some(existing_child) => merge_item(existing_child, new_child),
|
||||
None => {
|
||||
e.insert(key, new_child.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => *existing = new.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn config_path() -> Option<PathBuf> {
|
||||
paths::config_file()
|
||||
@@ -585,13 +534,34 @@ impl Config {
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn save(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let path = Self::config_path().ok_or("Could not determine config path")?;
|
||||
|
||||
paths::ensure_parent_dir(&path)?;
|
||||
|
||||
let content = toml::to_string_pretty(self)?;
|
||||
let new_content = toml::to_string_pretty(self)?;
|
||||
|
||||
// If a config file already exists, merge into it to preserve comments and
|
||||
// any keys the user has added that are not part of the Config struct.
|
||||
let content = if path.exists() {
|
||||
let existing = std::fs::read_to_string(&path)?;
|
||||
match existing.parse::<toml_edit::DocumentMut>() {
|
||||
Ok(mut doc) => {
|
||||
if let Ok(new_doc) = new_content.parse::<toml_edit::DocumentMut>() {
|
||||
merge_toml_doc(&mut doc, &new_doc);
|
||||
}
|
||||
doc.to_string()
|
||||
}
|
||||
Err(_) => {
|
||||
// Existing file is malformed — fall back to full rewrite.
|
||||
warn!("Existing config is malformed; overwriting with current settings");
|
||||
new_content
|
||||
}
|
||||
}
|
||||
} else {
|
||||
new_content
|
||||
};
|
||||
|
||||
std::fs::write(&path, content)?;
|
||||
info!("Saved config to {:?}", path);
|
||||
Ok(())
|
||||
|
||||
@@ -26,11 +26,17 @@ pub struct ParsedQuery {
|
||||
}
|
||||
|
||||
impl ProviderFilter {
|
||||
/// Create filter from CLI args and config
|
||||
/// Create filter from CLI args and config.
|
||||
///
|
||||
/// `tabs` is `general.tabs` from config and drives which provider tabs are
|
||||
/// shown in the UI when no explicit CLI mode is active. It has no effect on
|
||||
/// query routing: when no CLI mode is set, `accept_all=true` causes
|
||||
/// `is_active()` to return `true` for every provider regardless.
|
||||
pub fn new(
|
||||
cli_mode: Option<ProviderType>,
|
||||
cli_providers: Option<Vec<ProviderType>>,
|
||||
config_providers: &ProvidersConfig,
|
||||
tabs: &[String],
|
||||
) -> Self {
|
||||
let accept_all = cli_mode.is_none() && cli_providers.is_none();
|
||||
|
||||
@@ -41,50 +47,23 @@ impl ProviderFilter {
|
||||
// --providers overrides config
|
||||
providers.into_iter().collect()
|
||||
} else {
|
||||
// Use config file settings, default to apps only
|
||||
let mut set = HashSet::new();
|
||||
// Core providers
|
||||
if config_providers.applications {
|
||||
set.insert(ProviderType::Application);
|
||||
}
|
||||
if config_providers.commands {
|
||||
set.insert(ProviderType::Command);
|
||||
}
|
||||
// Plugin providers - use Plugin(type_id) for all
|
||||
if config_providers.uuctl {
|
||||
set.insert(ProviderType::Plugin("uuctl".to_string()));
|
||||
}
|
||||
if config_providers.system {
|
||||
set.insert(ProviderType::Plugin("system".to_string()));
|
||||
}
|
||||
if config_providers.ssh {
|
||||
set.insert(ProviderType::Plugin("ssh".to_string()));
|
||||
}
|
||||
if config_providers.clipboard {
|
||||
set.insert(ProviderType::Plugin("clipboard".to_string()));
|
||||
}
|
||||
if config_providers.bookmarks {
|
||||
set.insert(ProviderType::Plugin("bookmarks".to_string()));
|
||||
}
|
||||
if config_providers.emoji {
|
||||
set.insert(ProviderType::Plugin("emoji".to_string()));
|
||||
}
|
||||
if config_providers.scripts {
|
||||
set.insert(ProviderType::Plugin("scripts".to_string()));
|
||||
}
|
||||
// Dynamic providers
|
||||
if config_providers.files {
|
||||
set.insert(ProviderType::Plugin("filesearch".to_string()));
|
||||
}
|
||||
if config_providers.calculator {
|
||||
set.insert(ProviderType::Plugin("calc".to_string()));
|
||||
}
|
||||
if config_providers.websearch {
|
||||
set.insert(ProviderType::Plugin("websearch".to_string()));
|
||||
}
|
||||
// Default to apps if nothing enabled
|
||||
// No CLI restriction: accept_all=true, so is_active() returns true for
|
||||
// everything. Build the enabled set only for UI tab display, driven by
|
||||
// general.tabs. Falls back to Application + Command if tabs is empty.
|
||||
let mut set: HashSet<ProviderType> = tabs
|
||||
.iter()
|
||||
.map(|s| Self::mode_string_to_provider_type(s))
|
||||
.collect();
|
||||
if set.is_empty() {
|
||||
set.insert(ProviderType::Application);
|
||||
if config_providers.applications {
|
||||
set.insert(ProviderType::Application);
|
||||
}
|
||||
if config_providers.commands {
|
||||
set.insert(ProviderType::Command);
|
||||
}
|
||||
if set.is_empty() {
|
||||
set.insert(ProviderType::Application);
|
||||
}
|
||||
}
|
||||
set
|
||||
};
|
||||
@@ -114,7 +93,8 @@ impl ProviderFilter {
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle a provider on/off
|
||||
/// Toggle a provider on/off. Clears accept_all so the enabled set is
|
||||
/// actually used for routing — use restore_all_mode() to go back to All.
|
||||
pub fn toggle(&mut self, provider: ProviderType) {
|
||||
if self.enabled.contains(&provider) {
|
||||
self.enabled.remove(&provider);
|
||||
@@ -137,6 +117,7 @@ impl ProviderFilter {
|
||||
provider_debug, self.enabled
|
||||
);
|
||||
}
|
||||
self.accept_all = false;
|
||||
}
|
||||
|
||||
/// Enable a specific provider
|
||||
@@ -156,6 +137,12 @@ impl ProviderFilter {
|
||||
pub fn set_single_mode(&mut self, provider: ProviderType) {
|
||||
self.enabled.clear();
|
||||
self.enabled.insert(provider);
|
||||
self.accept_all = false;
|
||||
}
|
||||
|
||||
/// Restore accept-all mode (used when cycling back to the "All" tab).
|
||||
pub fn restore_all_mode(&mut self) {
|
||||
self.accept_all = true;
|
||||
}
|
||||
|
||||
/// Set prefix mode (from :app, :cmd, etc.)
|
||||
|
||||
@@ -23,24 +23,15 @@ const SEARCH_ENGINES: &[&str] = &[
|
||||
const BUILTIN_THEMES: &[&str] = &["owl"];
|
||||
|
||||
/// Boolean provider fields that can be toggled via CONFIG:toggle:providers.*.
|
||||
/// Only built-in providers are listed here; plugins are enabled/disabled via
|
||||
/// [plugins] disabled_plugins in config.toml or `owlry plugin enable/disable`.
|
||||
const PROVIDER_TOGGLES: &[(&str, &str)] = &[
|
||||
("applications", "Applications"),
|
||||
("commands", "Commands"),
|
||||
("uuctl", "Systemd Units"),
|
||||
("calculator", "Calculator"),
|
||||
("converter", "Unit Converter"),
|
||||
("frecency", "Frecency Ranking"),
|
||||
("websearch", "Web Search"),
|
||||
("system", "System Actions"),
|
||||
("ssh", "SSH Connections"),
|
||||
("clipboard", "Clipboard History"),
|
||||
("bookmarks", "Bookmarks"),
|
||||
("emoji", "Emoji Picker"),
|
||||
("scripts", "Scripts"),
|
||||
("files", "File Search"),
|
||||
("media", "Media Widget"),
|
||||
("weather", "Weather Widget"),
|
||||
("pomodoro", "Pomodoro Widget"),
|
||||
("frecency", "Frecency Ranking"),
|
||||
];
|
||||
|
||||
/// Built-in config editor provider. Interprets query text as a navigation path
|
||||
@@ -70,12 +61,8 @@ impl ConfigProvider {
|
||||
false
|
||||
};
|
||||
|
||||
if result {
|
||||
if let Ok(cfg) = self.config.read() {
|
||||
if let Err(e) = cfg.save() {
|
||||
warn!("Failed to save config: {}", e);
|
||||
}
|
||||
}
|
||||
if result && let Ok(cfg) = self.config.read() && let Err(e) = cfg.save() {
|
||||
warn!("Failed to save config: {}", e);
|
||||
}
|
||||
|
||||
result
|
||||
@@ -98,10 +85,6 @@ impl ConfigProvider {
|
||||
cfg.providers.commands = !cfg.providers.commands;
|
||||
true
|
||||
}
|
||||
"providers.uuctl" => {
|
||||
cfg.providers.uuctl = !cfg.providers.uuctl;
|
||||
true
|
||||
}
|
||||
"providers.calculator" => {
|
||||
cfg.providers.calculator = !cfg.providers.calculator;
|
||||
true
|
||||
@@ -110,52 +93,12 @@ impl ConfigProvider {
|
||||
cfg.providers.converter = !cfg.providers.converter;
|
||||
true
|
||||
}
|
||||
"providers.frecency" => {
|
||||
cfg.providers.frecency = !cfg.providers.frecency;
|
||||
true
|
||||
}
|
||||
"providers.websearch" => {
|
||||
cfg.providers.websearch = !cfg.providers.websearch;
|
||||
true
|
||||
}
|
||||
"providers.system" => {
|
||||
cfg.providers.system = !cfg.providers.system;
|
||||
true
|
||||
}
|
||||
"providers.ssh" => {
|
||||
cfg.providers.ssh = !cfg.providers.ssh;
|
||||
true
|
||||
}
|
||||
"providers.clipboard" => {
|
||||
cfg.providers.clipboard = !cfg.providers.clipboard;
|
||||
true
|
||||
}
|
||||
"providers.bookmarks" => {
|
||||
cfg.providers.bookmarks = !cfg.providers.bookmarks;
|
||||
true
|
||||
}
|
||||
"providers.emoji" => {
|
||||
cfg.providers.emoji = !cfg.providers.emoji;
|
||||
true
|
||||
}
|
||||
"providers.scripts" => {
|
||||
cfg.providers.scripts = !cfg.providers.scripts;
|
||||
true
|
||||
}
|
||||
"providers.files" => {
|
||||
cfg.providers.files = !cfg.providers.files;
|
||||
true
|
||||
}
|
||||
"providers.media" => {
|
||||
cfg.providers.media = !cfg.providers.media;
|
||||
true
|
||||
}
|
||||
"providers.weather" => {
|
||||
cfg.providers.weather = !cfg.providers.weather;
|
||||
true
|
||||
}
|
||||
"providers.pomodoro" => {
|
||||
cfg.providers.pomodoro = !cfg.providers.pomodoro;
|
||||
"providers.frecency" => {
|
||||
cfg.providers.frecency = !cfg.providers.frecency;
|
||||
true
|
||||
}
|
||||
"general.show_icons" => {
|
||||
@@ -762,21 +705,10 @@ fn get_provider_bool(cfg: &Config, field: &str) -> bool {
|
||||
match field {
|
||||
"applications" => cfg.providers.applications,
|
||||
"commands" => cfg.providers.commands,
|
||||
"uuctl" => cfg.providers.uuctl,
|
||||
"calculator" => cfg.providers.calculator,
|
||||
"converter" => cfg.providers.converter,
|
||||
"frecency" => cfg.providers.frecency,
|
||||
"websearch" => cfg.providers.websearch,
|
||||
"system" => cfg.providers.system,
|
||||
"ssh" => cfg.providers.ssh,
|
||||
"clipboard" => cfg.providers.clipboard,
|
||||
"bookmarks" => cfg.providers.bookmarks,
|
||||
"emoji" => cfg.providers.emoji,
|
||||
"scripts" => cfg.providers.scripts,
|
||||
"files" => cfg.providers.files,
|
||||
"media" => cfg.providers.media,
|
||||
"weather" => cfg.providers.weather,
|
||||
"pomodoro" => cfg.providers.pomodoro,
|
||||
"frecency" => cfg.providers.frecency,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,17 +91,20 @@ impl OwlryApp {
|
||||
.iter()
|
||||
.map(|s| ProviderFilter::mode_string_to_provider_type(s))
|
||||
.collect();
|
||||
let tabs = &config.borrow().general.tabs.clone();
|
||||
if provider_types.len() == 1 {
|
||||
ProviderFilter::new(
|
||||
Some(provider_types[0].clone()),
|
||||
None,
|
||||
&config.borrow().providers,
|
||||
tabs,
|
||||
)
|
||||
} else {
|
||||
ProviderFilter::new(None, Some(provider_types), &config.borrow().providers)
|
||||
ProviderFilter::new(None, Some(provider_types), &config.borrow().providers, tabs)
|
||||
}
|
||||
} else {
|
||||
ProviderFilter::new(None, None, &config.borrow().providers)
|
||||
let tabs = config.borrow().general.tabs.clone();
|
||||
ProviderFilter::new(None, None, &config.borrow().providers, &tabs)
|
||||
};
|
||||
let filter = Rc::new(RefCell::new(filter));
|
||||
|
||||
|
||||
@@ -394,7 +394,9 @@ impl MainWindow {
|
||||
format!("Search {}...", active.join(", "))
|
||||
}
|
||||
|
||||
/// Build dynamic hints based on enabled providers
|
||||
/// Build hints string for the status bar based on enabled built-in providers.
|
||||
/// Plugin trigger hints (? web, / files, etc.) are not included here since
|
||||
/// plugin availability is not tracked in ProvidersConfig.
|
||||
fn build_hints(config: &owlry_core::config::ProvidersConfig) -> String {
|
||||
let mut parts: Vec<String> = vec![
|
||||
"Tab: cycle".to_string(),
|
||||
@@ -403,38 +405,14 @@ impl MainWindow {
|
||||
"Esc: close".to_string(),
|
||||
];
|
||||
|
||||
// Add trigger hints for enabled dynamic providers
|
||||
if config.calculator {
|
||||
parts.push("= calc".to_string());
|
||||
}
|
||||
if config.websearch {
|
||||
parts.push("? web".to_string());
|
||||
if config.converter {
|
||||
parts.push("> conv".to_string());
|
||||
}
|
||||
if config.files {
|
||||
parts.push("/ files".to_string());
|
||||
}
|
||||
|
||||
// Add prefix hints for static providers
|
||||
let mut prefixes = Vec::new();
|
||||
if config.system {
|
||||
prefixes.push(":sys");
|
||||
}
|
||||
if config.emoji {
|
||||
prefixes.push(":emoji");
|
||||
}
|
||||
if config.ssh {
|
||||
prefixes.push(":ssh");
|
||||
}
|
||||
if config.clipboard {
|
||||
prefixes.push(":clip");
|
||||
}
|
||||
if config.bookmarks {
|
||||
prefixes.push(":bm");
|
||||
}
|
||||
|
||||
// Only show first few prefixes to avoid overflow
|
||||
if !prefixes.is_empty() {
|
||||
parts.push(prefixes[..prefixes.len().min(4)].join(" "));
|
||||
parts.push(":sys".to_string());
|
||||
}
|
||||
|
||||
parts.join(" ")
|
||||
@@ -1159,6 +1137,7 @@ impl MainWindow {
|
||||
for provider in tab_order {
|
||||
f.enable(provider.clone());
|
||||
}
|
||||
f.restore_all_mode();
|
||||
}
|
||||
for (_, button) in buttons.borrow().iter() {
|
||||
button.set_active(true);
|
||||
|
||||
Reference in New Issue
Block a user