1use crate::config::{ConfigValue, ResolvedConfig};
2use anyhow::Result;
3use std::collections::BTreeMap;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub(crate) enum PluginCommandState {
7 Enabled,
8 Disabled,
9}
10
11impl PluginCommandState {
12 pub(crate) fn as_str(self) -> &'static str {
13 match self {
14 Self::Enabled => "enabled",
15 Self::Disabled => "disabled",
16 }
17 }
18
19 fn from_config_value(value: &ConfigValue) -> Option<Self> {
20 match value.reveal() {
21 ConfigValue::String(raw) if raw.eq_ignore_ascii_case("enabled") => Some(Self::Enabled),
22 ConfigValue::String(raw) if raw.eq_ignore_ascii_case("disabled") => {
23 Some(Self::Disabled)
24 }
25 _ => None,
26 }
27 }
28}
29
30#[derive(Debug, Clone, Default)]
31pub(crate) struct PluginCommandPreferences {
32 pub(crate) command_states: BTreeMap<String, PluginCommandState>,
33 pub(crate) preferred_providers: BTreeMap<String, String>,
34}
35
36impl PluginCommandPreferences {
37 pub(crate) fn from_resolved(config: &ResolvedConfig) -> Self {
38 let mut preferences = Self::default();
39 for (key, entry) in config.values() {
40 let Some((command, field)) = plugin_command_config_field(key) else {
41 continue;
42 };
43 match field {
44 PluginCommandConfigField::State => {
45 if let Some(state) = PluginCommandState::from_config_value(&entry.value) {
46 preferences.command_states.insert(command, state);
47 }
48 }
49 PluginCommandConfigField::Provider => {
50 if let ConfigValue::String(provider) = entry.value.reveal() {
51 let provider = provider.trim();
52 if !provider.is_empty() {
53 preferences
54 .preferred_providers
55 .insert(command, provider.to_string());
56 }
57 }
58 }
59 }
60 }
61 preferences
62 }
63
64 pub(crate) fn state_for(&self, command: &str) -> Option<PluginCommandState> {
65 self.command_states.get(command).copied()
66 }
67
68 pub(crate) fn preferred_provider_for(&self, command: &str) -> Option<&str> {
69 self.preferred_providers.get(command).map(String::as_str)
70 }
71
72 #[cfg(test)]
73 pub(crate) fn set_state(&mut self, command: &str, state: PluginCommandState) {
74 self.command_states.insert(command.to_string(), state);
75 }
76
77 pub(crate) fn set_provider(&mut self, command: &str, plugin_id: &str) {
78 self.preferred_providers
79 .insert(command.to_string(), plugin_id.to_string());
80 }
81
82 pub(crate) fn clear_provider(&mut self, command: &str) -> bool {
83 self.preferred_providers.remove(command).is_some()
84 }
85}
86
87enum PluginCommandConfigField {
88 State,
89 Provider,
90}
91
92fn plugin_command_config_field(key: &str) -> Option<(String, PluginCommandConfigField)> {
93 let normalized = key.trim().to_ascii_lowercase();
94 let remainder = normalized.strip_prefix("plugins.")?;
95 let (command, field) = remainder.rsplit_once('.')?;
96 if command.trim().is_empty() {
97 return None;
98 }
99 let field = match field {
100 "state" => PluginCommandConfigField::State,
101 "provider" => PluginCommandConfigField::Provider,
102 _ => return None,
103 };
104 Some((command.to_string(), field))
105}
106
107pub(super) fn write_text_atomic(path: &std::path::Path, payload: &str) -> Result<()> {
108 crate::config::write_text_atomic(path, payload.as_bytes(), false).map_err(Into::into)
109}
110
111pub(super) fn merge_issue(target: &mut Option<String>, message: String) {
112 if message.trim().is_empty() {
113 return;
114 }
115
116 match target {
117 Some(existing) => {
118 existing.push_str("; ");
119 existing.push_str(&message);
120 }
121 None => *target = Some(message),
122 }
123}