Skip to content

Commit 08264ca

Browse files
committed
Add custom actions management and remove shell auto-start
- Add full CRUD for custom actions (fixes #6) - Actions page with list, create, edit, delete functionality - Copy button for upstream actions to create user versions - Remove shell auto-start since user now chooses image
1 parent a138338 commit 08264ca

7 files changed

Lines changed: 1048 additions & 4 deletions

File tree

src-tauri/src/commands/action.rs

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
use serde::{Deserialize, Serialize};
2+
use std::collections::HashMap;
3+
use std::fs;
4+
use std::path::PathBuf;
5+
6+
#[derive(Debug, Clone, Serialize, Deserialize)]
7+
pub struct CustomAction {
8+
pub name: String,
9+
pub description: String,
10+
pub before_mount: String,
11+
pub after_mount: String,
12+
pub before_unmount: String,
13+
pub environment: Vec<String>,
14+
pub capture_environment: Vec<String>,
15+
pub override_nfs_export: String,
16+
pub required_os: String,
17+
pub is_upstream: bool,
18+
}
19+
20+
#[derive(Debug, Deserialize)]
21+
struct ActionConfig {
22+
#[serde(default)]
23+
description: String,
24+
#[serde(default)]
25+
before_mount: String,
26+
#[serde(default)]
27+
after_mount: String,
28+
#[serde(default)]
29+
before_unmount: String,
30+
#[serde(default)]
31+
environment: Vec<String>,
32+
#[serde(default)]
33+
capture_environment: Vec<String>,
34+
#[serde(default)]
35+
override_nfs_export: String,
36+
#[serde(default)]
37+
required_os: String,
38+
}
39+
40+
#[derive(Debug, Deserialize)]
41+
struct ConfigFile {
42+
#[serde(default)]
43+
custom_actions: HashMap<String, ActionConfig>,
44+
}
45+
46+
fn get_user_config_path() -> PathBuf {
47+
dirs::home_dir()
48+
.unwrap_or_else(|| PathBuf::from("/tmp"))
49+
.join(".anylinuxfs/config.toml")
50+
}
51+
52+
fn get_upstream_config_path() -> PathBuf {
53+
PathBuf::from("/opt/homebrew/etc/anylinuxfs.toml")
54+
}
55+
56+
fn parse_actions_from_file(path: &PathBuf, is_upstream: bool) -> Vec<CustomAction> {
57+
let mut actions = Vec::new();
58+
59+
if let Ok(content) = fs::read_to_string(path) {
60+
if let Ok(config) = toml::from_str::<ConfigFile>(&content) {
61+
for (name, action_config) in config.custom_actions {
62+
actions.push(CustomAction {
63+
name,
64+
description: action_config.description,
65+
before_mount: action_config.before_mount,
66+
after_mount: action_config.after_mount,
67+
before_unmount: action_config.before_unmount,
68+
environment: action_config.environment,
69+
capture_environment: action_config.capture_environment,
70+
override_nfs_export: action_config.override_nfs_export,
71+
required_os: action_config.required_os,
72+
is_upstream,
73+
});
74+
}
75+
}
76+
}
77+
78+
actions
79+
}
80+
81+
#[tauri::command]
82+
pub fn list_custom_actions() -> Result<Vec<CustomAction>, String> {
83+
let mut all_actions = Vec::new();
84+
85+
// Load upstream actions (read-only)
86+
let upstream_path = get_upstream_config_path();
87+
all_actions.extend(parse_actions_from_file(&upstream_path, true));
88+
89+
// Load user actions
90+
let user_path = get_user_config_path();
91+
all_actions.extend(parse_actions_from_file(&user_path, false));
92+
93+
// Sort by name
94+
all_actions.sort_by(|a, b| a.name.cmp(&b.name));
95+
96+
Ok(all_actions)
97+
}
98+
99+
#[tauri::command]
100+
pub fn create_custom_action(
101+
name: String,
102+
description: String,
103+
before_mount: String,
104+
after_mount: String,
105+
before_unmount: String,
106+
environment: Vec<String>,
107+
capture_environment: Vec<String>,
108+
override_nfs_export: String,
109+
required_os: String,
110+
) -> Result<(), String> {
111+
let config_path = get_user_config_path();
112+
113+
// Ensure config directory exists
114+
if let Some(parent) = config_path.parent() {
115+
fs::create_dir_all(parent)
116+
.map_err(|e| format!("Failed to create config directory: {}", e))?;
117+
}
118+
119+
// Read existing config
120+
let content = fs::read_to_string(&config_path).unwrap_or_default();
121+
122+
// Parse as raw TOML value to preserve other sections
123+
let mut doc: toml::Table = toml::from_str(&content).unwrap_or_default();
124+
125+
// Get or create custom_actions section
126+
let custom_actions = doc
127+
.entry("custom_actions")
128+
.or_insert_with(|| toml::Value::Table(toml::Table::new()))
129+
.as_table_mut()
130+
.ok_or("Invalid config format")?;
131+
132+
// Check if action already exists
133+
if custom_actions.contains_key(&name) {
134+
return Err(format!("Action '{}' already exists", name));
135+
}
136+
137+
// Create action table
138+
let mut action_table = toml::Table::new();
139+
action_table.insert("description".to_string(), toml::Value::String(description));
140+
action_table.insert("before_mount".to_string(), toml::Value::String(before_mount));
141+
action_table.insert("after_mount".to_string(), toml::Value::String(after_mount));
142+
action_table.insert("before_unmount".to_string(), toml::Value::String(before_unmount));
143+
action_table.insert(
144+
"environment".to_string(),
145+
toml::Value::Array(environment.into_iter().map(toml::Value::String).collect()),
146+
);
147+
action_table.insert(
148+
"capture_environment".to_string(),
149+
toml::Value::Array(capture_environment.into_iter().map(toml::Value::String).collect()),
150+
);
151+
action_table.insert("override_nfs_export".to_string(), toml::Value::String(override_nfs_export));
152+
action_table.insert("required_os".to_string(), toml::Value::String(required_os));
153+
154+
custom_actions.insert(name, toml::Value::Table(action_table));
155+
156+
// Write back
157+
let new_content = toml::to_string_pretty(&doc)
158+
.map_err(|e| format!("Failed to serialize config: {}", e))?;
159+
160+
fs::write(&config_path, new_content)
161+
.map_err(|e| format!("Failed to write config: {}", e))?;
162+
163+
Ok(())
164+
}
165+
166+
#[tauri::command]
167+
pub fn update_custom_action(
168+
name: String,
169+
description: String,
170+
before_mount: String,
171+
after_mount: String,
172+
before_unmount: String,
173+
environment: Vec<String>,
174+
capture_environment: Vec<String>,
175+
override_nfs_export: String,
176+
required_os: String,
177+
) -> Result<(), String> {
178+
let config_path = get_user_config_path();
179+
180+
// Read existing config
181+
let content = fs::read_to_string(&config_path)
182+
.map_err(|e| format!("Failed to read config: {}", e))?;
183+
184+
// Parse as raw TOML value
185+
let mut doc: toml::Table = toml::from_str(&content)
186+
.map_err(|e| format!("Failed to parse config: {}", e))?;
187+
188+
// Get custom_actions section
189+
let custom_actions = doc
190+
.get_mut("custom_actions")
191+
.and_then(|v| v.as_table_mut())
192+
.ok_or("No custom_actions section found")?;
193+
194+
// Check if action exists
195+
if !custom_actions.contains_key(&name) {
196+
return Err(format!("Action '{}' not found", name));
197+
}
198+
199+
// Update action table
200+
let mut action_table = toml::Table::new();
201+
action_table.insert("description".to_string(), toml::Value::String(description));
202+
action_table.insert("before_mount".to_string(), toml::Value::String(before_mount));
203+
action_table.insert("after_mount".to_string(), toml::Value::String(after_mount));
204+
action_table.insert("before_unmount".to_string(), toml::Value::String(before_unmount));
205+
action_table.insert(
206+
"environment".to_string(),
207+
toml::Value::Array(environment.into_iter().map(toml::Value::String).collect()),
208+
);
209+
action_table.insert(
210+
"capture_environment".to_string(),
211+
toml::Value::Array(capture_environment.into_iter().map(toml::Value::String).collect()),
212+
);
213+
action_table.insert("override_nfs_export".to_string(), toml::Value::String(override_nfs_export));
214+
action_table.insert("required_os".to_string(), toml::Value::String(required_os));
215+
216+
custom_actions.insert(name, toml::Value::Table(action_table));
217+
218+
// Write back
219+
let new_content = toml::to_string_pretty(&doc)
220+
.map_err(|e| format!("Failed to serialize config: {}", e))?;
221+
222+
fs::write(&config_path, new_content)
223+
.map_err(|e| format!("Failed to write config: {}", e))?;
224+
225+
Ok(())
226+
}
227+
228+
#[tauri::command]
229+
pub fn delete_custom_action(name: String) -> Result<(), String> {
230+
let config_path = get_user_config_path();
231+
232+
// Check if config file exists
233+
if !config_path.exists() {
234+
return Err(format!("Action '{}' not found", name));
235+
}
236+
237+
// Read existing config
238+
let content = fs::read_to_string(&config_path)
239+
.map_err(|e| format!("Failed to read config: {}", e))?;
240+
241+
// Parse as raw TOML value
242+
let mut doc: toml::Table = toml::from_str(&content)
243+
.map_err(|e| format!("Failed to parse config: {}", e))?;
244+
245+
// Get custom_actions section
246+
let custom_actions = match doc.get_mut("custom_actions").and_then(|v| v.as_table_mut()) {
247+
Some(actions) => actions,
248+
None => return Err(format!("Action '{}' not found", name)),
249+
};
250+
251+
// Remove action
252+
if custom_actions.remove(&name).is_none() {
253+
return Err(format!("Action '{}' not found", name));
254+
}
255+
256+
// Write back
257+
let new_content = toml::to_string_pretty(&doc)
258+
.map_err(|e| format!("Failed to serialize config: {}", e))?;
259+
260+
fs::write(&config_path, new_content)
261+
.map_err(|e| format!("Failed to write config: {}", e))?;
262+
263+
Ok(())
264+
}

src-tauri/src/commands/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pub mod config;
55
pub mod shell;
66
pub mod image;
77
pub mod apk;
8+
pub mod action;
89

910
pub use disk::*;
1011
pub use status::*;
@@ -13,3 +14,4 @@ pub use config::*;
1314
pub use shell::{start_shell, write_shell, resize_shell, stop_shell, PtyState};
1415
pub use image::*;
1516
pub use apk::*;
17+
pub use action::*;

src-tauri/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use commands::{
1010
start_shell, write_shell, resize_shell, stop_shell,
1111
list_images, install_image, uninstall_image,
1212
list_packages, add_packages, remove_packages,
13+
list_custom_actions, create_custom_action, update_custom_action, delete_custom_action,
1314
WatcherState, PtyState,
1415
};
1516

@@ -43,6 +44,10 @@ pub fn run() {
4344
list_packages,
4445
add_packages,
4546
remove_packages,
47+
list_custom_actions,
48+
create_custom_action,
49+
update_custom_action,
50+
delete_custom_action,
4651
])
4752
.run(tauri::generate_context!())
4853
.expect("error while running tauri application");

src/components/Sidebar.svelte

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
{ path: '/shell', label: 'Shell', icon: 'shell' },
77
{ path: '/images', label: 'Images', icon: 'image' },
88
{ path: '/packages', label: 'Packages', icon: 'package' },
9+
{ path: '/actions', label: 'Actions', icon: 'action' },
910
{ path: '/logs', label: 'Logs', icon: 'log' },
1011
{ path: '/settings', label: 'Settings', icon: 'settings' }
1112
];
@@ -143,4 +144,8 @@
143144
.nav-icon[data-icon='package']::before {
144145
content: '\1F4E6';
145146
}
147+
148+
.nav-icon[data-icon='action']::before {
149+
content: '\26A1';
150+
}
146151
</style>

src/lib/api.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,52 @@ export async function addPackages(packages: string[]): Promise<void> {
101101
export async function removePackages(packages: string[]): Promise<void> {
102102
return await invoke<void>('remove_packages', { packages });
103103
}
104+
105+
export interface CustomAction {
106+
name: string;
107+
description: string;
108+
before_mount: string;
109+
after_mount: string;
110+
before_unmount: string;
111+
environment: string[];
112+
capture_environment: string[];
113+
override_nfs_export: string;
114+
required_os: string;
115+
is_upstream: boolean;
116+
}
117+
118+
export async function listCustomActions(): Promise<CustomAction[]> {
119+
return await invoke<CustomAction[]>('list_custom_actions');
120+
}
121+
122+
export async function createCustomAction(action: Omit<CustomAction, 'is_upstream'>): Promise<void> {
123+
return await invoke<void>('create_custom_action', {
124+
name: action.name,
125+
description: action.description,
126+
beforeMount: action.before_mount,
127+
afterMount: action.after_mount,
128+
beforeUnmount: action.before_unmount,
129+
environment: action.environment,
130+
captureEnvironment: action.capture_environment,
131+
overrideNfsExport: action.override_nfs_export,
132+
requiredOs: action.required_os
133+
});
134+
}
135+
136+
export async function updateCustomAction(action: Omit<CustomAction, 'is_upstream'>): Promise<void> {
137+
return await invoke<void>('update_custom_action', {
138+
name: action.name,
139+
description: action.description,
140+
beforeMount: action.before_mount,
141+
afterMount: action.after_mount,
142+
beforeUnmount: action.before_unmount,
143+
environment: action.environment,
144+
captureEnvironment: action.capture_environment,
145+
overrideNfsExport: action.override_nfs_export,
146+
requiredOs: action.required_os
147+
});
148+
}
149+
150+
export async function deleteCustomAction(name: string): Promise<void> {
151+
return await invoke<void>('delete_custom_action', { name });
152+
}

0 commit comments

Comments
 (0)