1use std::collections::HashSet;
4use std::path::PathBuf;
5
6use crate::config::{ResolvedConfig, RuntimeLoadOptions};
7use crate::plugin::PluginManager;
8use crate::plugin::config::{PluginConfigEntry, PluginConfigEnv, PluginConfigEnvCache};
9use crate::ui::RenderSettings;
10use crate::ui::messages::MessageLevel;
11use crate::ui::theme_loader::ThemeCatalog;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum TerminalKind {
15 Cli,
16 Repl,
17}
18
19impl TerminalKind {
20 pub fn as_config_terminal(self) -> &'static str {
21 match self {
22 TerminalKind::Cli => "cli",
23 TerminalKind::Repl => "repl",
24 }
25 }
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct RuntimeContext {
30 profile_override: Option<String>,
31 terminal_kind: TerminalKind,
32 terminal_env: Option<String>,
33}
34
35impl RuntimeContext {
36 pub fn new(
37 profile_override: Option<String>,
38 terminal_kind: TerminalKind,
39 terminal_env: Option<String>,
40 ) -> Self {
41 Self {
42 profile_override: profile_override
43 .map(|value| value.trim().to_ascii_lowercase())
44 .filter(|value| !value.is_empty()),
45 terminal_kind,
46 terminal_env,
47 }
48 }
49
50 pub fn profile_override(&self) -> Option<&str> {
51 self.profile_override.as_deref()
52 }
53
54 pub fn terminal_kind(&self) -> TerminalKind {
55 self.terminal_kind
56 }
57
58 pub fn terminal_env(&self) -> Option<&str> {
59 self.terminal_env.as_deref()
60 }
61}
62
63pub struct ConfigState {
64 resolved: ResolvedConfig,
65 revision: u64,
66}
67
68impl ConfigState {
69 pub fn new(resolved: ResolvedConfig) -> Self {
70 Self {
71 resolved,
72 revision: 1,
73 }
74 }
75
76 pub fn resolved(&self) -> &ResolvedConfig {
77 &self.resolved
78 }
79
80 pub fn revision(&self) -> u64 {
81 self.revision
82 }
83
84 pub fn replace_resolved(&mut self, next: ResolvedConfig) -> bool {
85 if self.resolved == next {
86 return false;
87 }
88
89 self.resolved = next;
90 self.revision += 1;
91 true
92 }
93
94 pub fn transaction<F, E>(&mut self, mutator: F) -> Result<bool, E>
95 where
96 F: FnOnce(&ResolvedConfig) -> Result<ResolvedConfig, E>,
97 {
98 let current = self.resolved.clone();
99 let candidate = mutator(¤t)?;
100 Ok(self.replace_resolved(candidate))
101 }
102}
103
104#[derive(Debug, Clone)]
105pub struct UiState {
106 pub render_settings: RenderSettings,
107 pub message_verbosity: MessageLevel,
108 pub debug_verbosity: u8,
109}
110
111#[derive(Debug, Clone, Default)]
112pub struct LaunchContext {
113 pub plugin_dirs: Vec<PathBuf>,
114 pub config_root: Option<PathBuf>,
115 pub cache_root: Option<PathBuf>,
116 pub runtime_load: RuntimeLoadOptions,
117}
118
119pub struct AppClients {
120 pub plugins: PluginManager,
121 plugin_config_env: PluginConfigEnvCache,
122}
123
124impl AppClients {
125 pub fn new(plugins: PluginManager) -> Self {
126 Self {
127 plugins,
128 plugin_config_env: PluginConfigEnvCache::default(),
129 }
130 }
131
132 pub(crate) fn plugin_config_env(&self, config: &ConfigState) -> PluginConfigEnv {
133 self.plugin_config_env.collect(config)
134 }
135
136 pub(crate) fn plugin_config_entries(
137 &self,
138 config: &ConfigState,
139 plugin_id: &str,
140 ) -> Vec<PluginConfigEntry> {
141 let config_env = self.plugin_config_env(config);
142 let mut merged = std::collections::BTreeMap::new();
143 for entry in config_env.shared {
144 merged.insert(entry.env_key.clone(), entry);
145 }
146 if let Some(entries) = config_env.by_plugin_id.get(plugin_id) {
147 for entry in entries {
148 merged.insert(entry.env_key.clone(), entry.clone());
149 }
150 }
151 merged.into_values().collect()
152 }
153}
154
155pub struct AppRuntime {
156 pub context: RuntimeContext,
157 pub config: ConfigState,
158 pub ui: UiState,
159 pub auth: AuthState,
160 pub(crate) themes: ThemeCatalog,
161 pub launch: LaunchContext,
162}
163
164pub struct AuthState {
165 builtins_allowlist: Option<HashSet<String>>,
166 plugins_allowlist: Option<HashSet<String>>,
167}
168
169impl AuthState {
170 pub fn from_resolved(config: &ResolvedConfig) -> Self {
171 Self {
172 builtins_allowlist: parse_allowlist(config.get_string("auth.visible.builtins")),
173 plugins_allowlist: parse_allowlist(config.get_string("auth.visible.plugins")),
174 }
175 }
176
177 pub fn is_builtin_visible(&self, command: &str) -> bool {
178 is_visible_in_allowlist(&self.builtins_allowlist, command)
179 }
180
181 pub fn is_plugin_command_visible(&self, command: &str) -> bool {
182 is_visible_in_allowlist(&self.plugins_allowlist, command)
183 }
184}
185
186fn parse_allowlist(raw: Option<&str>) -> Option<HashSet<String>> {
187 let raw = raw.map(str::trim).filter(|value| !value.is_empty())?;
188
189 if raw == "*" {
190 return None;
191 }
192
193 let values = raw
194 .split([',', ' '])
195 .map(str::trim)
196 .filter(|value| !value.is_empty())
197 .map(|value| value.to_ascii_lowercase())
198 .collect::<HashSet<String>>();
199 if values.is_empty() {
200 None
201 } else {
202 Some(values)
203 }
204}
205
206fn is_visible_in_allowlist(allowlist: &Option<HashSet<String>>, command: &str) -> bool {
207 match allowlist {
208 None => true,
209 Some(values) => values.contains(&command.to_ascii_lowercase()),
210 }
211}