Merge pull request #22 from aarmea/u/aarmea/18/flatpak-type

Implement first-class Flatpak support
This commit is contained in:
Albert Armea 2026-01-09 22:32:04 -05:00 committed by GitHub
commit b7f2294a81
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 239 additions and 74 deletions

View file

@ -104,16 +104,16 @@ All behavior shown above is driven entirely by declarative configuration.
For the Minecraft example shown above: For the Minecraft example shown above:
```toml ```toml
# Minecraft via mc-installer snap # Prism Launcher - Minecraft launcher (Flatpak)
# Ubuntu: sudo snap install mc-installer # Install: flatpak install flathub org.prismlauncher.PrismLauncher
[[entries]] [[entries]]
id = "minecraft" id = "prism-launcher"
label = "Minecraft" label = "Prism Launcher"
icon = "minecraft" icon = "org.prismlauncher.PrismLauncher"
[entries.kind] [entries.kind]
type = "snap" type = "flatpak"
snap_name = "mc-installer" app_id = "org.prismlauncher.PrismLauncher"
[entries.availability] [entries.availability]
[[entries.availability.windows]] [[entries.availability.windows]]
@ -160,7 +160,7 @@ compatibility infrastructure:
* Wayland and Sway * Wayland and Sway
* Rust * Rust
* Snap * Flatpak and Snap
* Proton and WINE * Proton and WINE
This project was written with the assistance of generative AI-based coding This project was written with the assistance of generative AI-based coding

View file

@ -165,52 +165,6 @@ max_run_seconds = 0 # Unlimited
daily_quota_seconds = 0 # Unlimited daily_quota_seconds = 0 # Unlimited
cooldown_seconds = 0 # No cooldown cooldown_seconds = 0 # No cooldown
# Minecraft via mc-installer snap
# Ubuntu: sudo snap install mc-installer
[[entries]]
id = "minecraft"
label = "Minecraft"
icon = "~/.minecraft/launcher/icons/minecraft256.png"
[entries.kind]
type = "snap"
snap_name = "mc-installer"
[entries.availability]
[[entries.availability.windows]]
days = "weekdays"
start = "15:00"
end = "18:00"
[[entries.availability.windows]]
days = "weekends"
start = "10:00"
end = "20:00"
[entries.limits]
max_run_seconds = 1800 # 30 minutes (roughly 3 in-game days)
daily_quota_seconds = 3600 # 1 hour per day
cooldown_seconds = 600 # 10 minute cooldown
[[entries.warnings]]
seconds_before = 600
severity = "info"
message = "10 minutes left - start wrapping up!"
[[entries.warnings]]
seconds_before = 120
severity = "warn"
message = "2 minutes remaining - save your game!"
[[entries.warnings]]
seconds_before = 30
severity = "critical"
message = "30 seconds! Save NOW!"
# Entry-specific volume restrictions (overrides global)
[entries.volume]
max_volume = 60 # Limit volume during gaming sessions
## === Steam games === ## === Steam games ===
# Steam can be used via Canonical's Steam snap package: # Steam can be used via Canonical's Steam snap package:
# https://snapcraft.io/steam # https://snapcraft.io/steam
@ -267,6 +221,76 @@ days = "weekends"
start = "10:00" start = "10:00"
end = "20:00" end = "20:00"
## === Flatpak-based applications ===
# Flatpak entries use the "flatpak" type for proper process management.
# Similar to Snap, Flatpak apps run in sandboxed environments and use
# systemd scopes for process management.
# Prism Launcher - Minecraft launcher (Flatpak)
# Install: flatpak install flathub org.prismlauncher.PrismLauncher
[[entries]]
id = "prism-launcher"
label = "Prism Launcher"
icon = "org.prismlauncher.PrismLauncher"
[entries.kind]
type = "flatpak"
app_id = "org.prismlauncher.PrismLauncher"
[entries.availability]
[[entries.availability.windows]]
days = "weekdays"
start = "15:00"
end = "18:00"
[[entries.availability.windows]]
days = "weekends"
start = "10:00"
end = "20:00"
[entries.limits]
max_run_seconds = 1800 # 30 minutes (roughly 3 in-game days)
daily_quota_seconds = 3600 # 1 hour per day
cooldown_seconds = 600 # 10 minute cooldown
[[entries.warnings]]
seconds_before = 600
severity = "info"
message = "10 minutes left - start wrapping up!"
[[entries.warnings]]
seconds_before = 120
severity = "warn"
message = "2 minutes remaining - save your game!"
[[entries.warnings]]
seconds_before = 30
severity = "critical"
message = "30 seconds! Save NOW!"
# Entry-specific volume restrictions (overrides global)
[entries.volume]
max_volume = 60 # Limit volume during gaming sessions
# Krita - digital painting (Flatpak)
# Install: flatpak install flathub org.kde.krita
[[entries]]
id = "krita"
label = "Krita"
icon = "org.kde.krita"
[entries.kind]
type = "flatpak"
app_id = "org.kde.krita"
[entries.availability]
always = true
[entries.limits]
max_run_seconds = 0 # Unlimited
daily_quota_seconds = 0 # Unlimited
cooldown_seconds = 0 # No cooldown
## === Media === ## === Media ===
# Just use `mpv` to play media (for now). # Just use `mpv` to play media (for now).
# Files can be local on your system or URLs (YouTube, etc). # Files can be local on your system or URLs (YouTube, etc).

View file

@ -13,6 +13,7 @@ use std::time::Duration;
pub enum EntryKindTag { pub enum EntryKindTag {
Process, Process,
Snap, Snap,
Flatpak,
Vm, Vm,
Media, Media,
Custom, Custom,
@ -46,6 +47,17 @@ pub enum EntryKind {
#[serde(default)] #[serde(default)]
env: HashMap<String, String>, env: HashMap<String, String>,
}, },
/// Flatpak application - uses systemd scope-based process management
Flatpak {
/// The Flatpak application ID (e.g., "org.prismlauncher.PrismLauncher")
app_id: String,
/// Additional command-line arguments
#[serde(default)]
args: Vec<String>,
/// Additional environment variables
#[serde(default)]
env: HashMap<String, String>,
},
Vm { Vm {
driver: String, driver: String,
#[serde(default)] #[serde(default)]
@ -67,6 +79,7 @@ impl EntryKind {
match self { match self {
EntryKind::Process { .. } => EntryKindTag::Process, EntryKind::Process { .. } => EntryKindTag::Process,
EntryKind::Snap { .. } => EntryKindTag::Snap, EntryKind::Snap { .. } => EntryKindTag::Snap,
EntryKind::Flatpak { .. } => EntryKindTag::Flatpak,
EntryKind::Vm { .. } => EntryKindTag::Vm, EntryKind::Vm { .. } => EntryKindTag::Vm,
EntryKind::Media { .. } => EntryKindTag::Media, EntryKind::Media { .. } => EntryKindTag::Media,
EntryKind::Custom { .. } => EntryKindTag::Custom, EntryKind::Custom { .. } => EntryKindTag::Custom,

View file

@ -54,6 +54,9 @@ fn main() -> ExitCode {
EntryKind::Snap { snap_name, .. } => { EntryKind::Snap { snap_name, .. } => {
format!("snap ({})", snap_name) format!("snap ({})", snap_name)
} }
EntryKind::Flatpak { app_id, .. } => {
format!("flatpak ({})", app_id)
}
EntryKind::Vm { driver, .. } => { EntryKind::Vm { driver, .. } => {
format!("vm ({})", driver) format!("vm ({})", driver)
} }

View file

@ -256,6 +256,7 @@ fn convert_entry_kind(raw: RawEntryKind) -> EntryKind {
match raw { match raw {
RawEntryKind::Process { command, args, env, cwd } => EntryKind::Process { command, args, env, cwd }, RawEntryKind::Process { command, args, env, cwd } => EntryKind::Process { command, args, env, cwd },
RawEntryKind::Snap { snap_name, command, args, env } => EntryKind::Snap { snap_name, command, args, env }, RawEntryKind::Snap { snap_name, command, args, env } => EntryKind::Snap { snap_name, command, args, env },
RawEntryKind::Flatpak { app_id, args, env } => EntryKind::Flatpak { app_id, args, env },
RawEntryKind::Vm { driver, args } => EntryKind::Vm { driver, args }, RawEntryKind::Vm { driver, args } => EntryKind::Vm { driver, args },
RawEntryKind::Media { library_id, args } => EntryKind::Media { library_id, args }, RawEntryKind::Media { library_id, args } => EntryKind::Media { library_id, args },
RawEntryKind::Custom { type_name, payload } => EntryKind::Custom { RawEntryKind::Custom { type_name, payload } => EntryKind::Custom {

View file

@ -116,6 +116,17 @@ pub enum RawEntryKind {
#[serde(default)] #[serde(default)]
env: HashMap<String, String>, env: HashMap<String, String>,
}, },
/// Flatpak application - uses systemd scope-based process management
Flatpak {
/// The Flatpak application ID (e.g., "org.prismlauncher.PrismLauncher")
app_id: String,
/// Additional command-line arguments
#[serde(default)]
args: Vec<String>,
/// Additional environment variables
#[serde(default)]
env: HashMap<String, String>,
},
Vm { Vm {
driver: String, driver: String,
#[serde(default)] #[serde(default)]

View file

@ -71,6 +71,14 @@ fn validate_entry(entry: &RawEntry, config: &RawConfig) -> Vec<ValidationError>
}); });
} }
} }
RawEntryKind::Flatpak { app_id, .. } => {
if app_id.is_empty() {
errors.push(ValidationError::EntryError {
entry_id: entry.id.clone(),
message: "app_id cannot be empty".into(),
});
}
}
RawEntryKind::Vm { driver, .. } => { RawEntryKind::Vm { driver, .. } => {
if driver.is_empty() { if driver.is_empty() {
errors.push(ValidationError::EntryError { errors.push(ValidationError::EntryError {

View file

@ -59,6 +59,7 @@ impl HostCapabilities {
let mut spawn_kinds = HashSet::new(); let mut spawn_kinds = HashSet::new();
spawn_kinds.insert(EntryKindTag::Process); spawn_kinds.insert(EntryKindTag::Process);
spawn_kinds.insert(EntryKindTag::Snap); spawn_kinds.insert(EntryKindTag::Snap);
spawn_kinds.insert(EntryKindTag::Flatpak);
spawn_kinds.insert(EntryKindTag::Vm); spawn_kinds.insert(EntryKindTag::Vm);
spawn_kinds.insert(EntryKindTag::Media); spawn_kinds.insert(EntryKindTag::Media);

View file

@ -13,7 +13,7 @@ use std::time::Duration;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tracing::{info, warn}; use tracing::{info, warn};
use crate::process::{init, kill_by_command, kill_snap_cgroup, ManagedProcess}; use crate::process::{init, kill_by_command, kill_flatpak_cgroup, kill_snap_cgroup, ManagedProcess};
/// Expand `~` at the beginning of a path to the user's home directory /// Expand `~` at the beginning of a path to the user's home directory
fn expand_tilde(path: &str) -> String { fn expand_tilde(path: &str) -> String {
@ -39,6 +39,7 @@ fn expand_args(args: &[String]) -> Vec<String> {
struct SessionInfo { struct SessionInfo {
command_name: String, command_name: String,
snap_name: Option<String>, snap_name: Option<String>,
flatpak_app_id: Option<String>,
} }
/// Linux host adapter /// Linux host adapter
@ -132,15 +133,15 @@ impl HostAdapter for LinuxHost {
entry_kind: &EntryKind, entry_kind: &EntryKind,
options: SpawnOptions, options: SpawnOptions,
) -> HostResult<HostSessionHandle> { ) -> HostResult<HostSessionHandle> {
// Extract argv, env, cwd, and snap_name based on entry kind // Extract argv, env, cwd, snap_name, and flatpak_app_id based on entry kind
let (argv, env, cwd, snap_name) = match entry_kind { let (argv, env, cwd, snap_name, flatpak_app_id) = match entry_kind {
EntryKind::Process { command, args, env, cwd } => { EntryKind::Process { command, args, env, cwd } => {
let mut argv = vec![expand_tilde(command)]; let mut argv = vec![expand_tilde(command)];
argv.extend(expand_args(args)); argv.extend(expand_args(args));
let expanded_cwd = cwd.as_ref().map(|c| { let expanded_cwd = cwd.as_ref().map(|c| {
std::path::PathBuf::from(expand_tilde(&c.to_string_lossy())) std::path::PathBuf::from(expand_tilde(&c.to_string_lossy()))
}); });
(argv, env.clone(), expanded_cwd, None) (argv, env.clone(), expanded_cwd, None, None)
} }
EntryKind::Snap { snap_name, command, args, env } => { EntryKind::Snap { snap_name, command, args, env } => {
// For snap apps, we need to use 'snap run <snap_name>' to launch them. // For snap apps, we need to use 'snap run <snap_name>' to launch them.
@ -153,7 +154,13 @@ impl HostAdapter for LinuxHost {
argv.push(cmd.clone()); argv.push(cmd.clone());
} }
argv.extend(expand_args(args)); argv.extend(expand_args(args));
(argv, env.clone(), None, Some(snap_name.clone())) (argv, env.clone(), None, Some(snap_name.clone()), None)
}
EntryKind::Flatpak { app_id, args, env } => {
// For Flatpak apps, we use 'flatpak run <app_id>' to launch them.
let mut argv = vec!["flatpak".to_string(), "run".to_string(), app_id.clone()];
argv.extend(expand_args(args));
(argv, env.clone(), None, None, Some(app_id.clone()))
} }
EntryKind::Vm { driver, args } => { EntryKind::Vm { driver, args } => {
// Construct command line from VM driver // Construct command line from VM driver
@ -166,13 +173,13 @@ impl HostAdapter for LinuxHost {
argv.push(value.to_string()); argv.push(value.to_string());
} }
} }
(argv, HashMap::new(), None, None) (argv, HashMap::new(), None, None, None)
} }
EntryKind::Media { library_id, args: _ } => { EntryKind::Media { library_id, args: _ } => {
// For media, we'd typically launch a media player // For media, we'd typically launch a media player
// This is a placeholder - real implementation would integrate with a player // This is a placeholder - real implementation would integrate with a player
let argv = vec!["xdg-open".to_string(), expand_tilde(library_id)]; let argv = vec!["xdg-open".to_string(), expand_tilde(library_id)];
(argv, HashMap::new(), None, None) (argv, HashMap::new(), None, None, None)
} }
EntryKind::Custom { type_name: _, payload: _ } => { EntryKind::Custom { type_name: _, payload: _ } => {
return Err(HostError::UnsupportedKind); return Err(HostError::UnsupportedKind);
@ -180,19 +187,24 @@ impl HostAdapter for LinuxHost {
}; };
// Get the command name for fallback killing // Get the command name for fallback killing
// For snap apps, use the snap_name (not "snap") to avoid killing unrelated processes // For snap/flatpak apps, use the app name (not "snap"/"flatpak") to avoid killing unrelated processes
let command_name = if let Some(ref snap) = snap_name { let command_name = if let Some(ref snap) = snap_name {
snap.clone() snap.clone()
} else if let Some(ref app_id) = flatpak_app_id {
app_id.clone()
} else { } else {
argv.first().cloned().unwrap_or_default() argv.first().cloned().unwrap_or_default()
}; };
// Determine if this is a sandboxed app (snap or flatpak)
let sandboxed_app_name = snap_name.clone().or_else(|| flatpak_app_id.clone());
let proc = ManagedProcess::spawn( let proc = ManagedProcess::spawn(
&argv, &argv,
&env, &env,
cwd.as_ref(), cwd.as_ref(),
options.log_path.clone(), options.log_path.clone(),
snap_name.clone(), sandboxed_app_name,
)?; )?;
let pid = proc.pid; let pid = proc.pid;
@ -202,9 +214,10 @@ impl HostAdapter for LinuxHost {
let session_info_entry = SessionInfo { let session_info_entry = SessionInfo {
command_name: command_name.clone(), command_name: command_name.clone(),
snap_name: snap_name.clone(), snap_name: snap_name.clone(),
flatpak_app_id: flatpak_app_id.clone(),
}; };
self.session_info.lock().unwrap().insert(session_id.clone(), session_info_entry); self.session_info.lock().unwrap().insert(session_id.clone(), session_info_entry);
info!(session_id = %session_id, command = %command_name, snap = ?snap_name, "Tracking session info"); info!(session_id = %session_id, command = %command_name, snap = ?snap_name, flatpak = ?flatpak_app_id, "Tracking session info");
let handle = HostSessionHandle::new( let handle = HostSessionHandle::new(
session_id, session_id,
@ -238,13 +251,16 @@ impl HostAdapter for LinuxHost {
match mode { match mode {
StopMode::Graceful { timeout } => { StopMode::Graceful { timeout } => {
// If this is a snap app, use cgroup-based killing (most reliable) // If this is a snap or flatpak app, use cgroup-based killing (most reliable)
if let Some(ref info) = session_info { if let Some(ref info) = session_info {
if let Some(ref snap) = info.snap_name { if let Some(ref snap) = info.snap_name {
kill_snap_cgroup(snap, nix::sys::signal::Signal::SIGTERM); kill_snap_cgroup(snap, nix::sys::signal::Signal::SIGTERM);
info!(snap = %snap, "Sent SIGTERM via snap cgroup"); info!(snap = %snap, "Sent SIGTERM via snap cgroup");
} else if let Some(ref app_id) = info.flatpak_app_id {
kill_flatpak_cgroup(app_id, nix::sys::signal::Signal::SIGTERM);
info!(flatpak = %app_id, "Sent SIGTERM via flatpak cgroup");
} else { } else {
// Fall back to command name for non-snap apps // Fall back to command name for non-sandboxed apps
kill_by_command(&info.command_name, nix::sys::signal::Signal::SIGTERM); kill_by_command(&info.command_name, nix::sys::signal::Signal::SIGTERM);
info!(command = %info.command_name, "Sent SIGTERM via command name"); info!(command = %info.command_name, "Sent SIGTERM via command name");
} }
@ -262,11 +278,14 @@ impl HostAdapter for LinuxHost {
let start = std::time::Instant::now(); let start = std::time::Instant::now();
loop { loop {
if start.elapsed() >= timeout { if start.elapsed() >= timeout {
// Force kill after timeout using snap cgroup or command name // Force kill after timeout using snap/flatpak cgroup or command name
if let Some(ref info) = session_info { if let Some(ref info) = session_info {
if let Some(ref snap) = info.snap_name { if let Some(ref snap) = info.snap_name {
kill_snap_cgroup(snap, nix::sys::signal::Signal::SIGKILL); kill_snap_cgroup(snap, nix::sys::signal::Signal::SIGKILL);
info!(snap = %snap, "Sent SIGKILL via snap cgroup (timeout)"); info!(snap = %snap, "Sent SIGKILL via snap cgroup (timeout)");
} else if let Some(ref app_id) = info.flatpak_app_id {
kill_flatpak_cgroup(app_id, nix::sys::signal::Signal::SIGKILL);
info!(flatpak = %app_id, "Sent SIGKILL via flatpak cgroup (timeout)");
} else { } else {
kill_by_command(&info.command_name, nix::sys::signal::Signal::SIGKILL); kill_by_command(&info.command_name, nix::sys::signal::Signal::SIGKILL);
info!(command = %info.command_name, "Sent SIGKILL via command name (timeout)"); info!(command = %info.command_name, "Sent SIGKILL via command name (timeout)");
@ -292,11 +311,14 @@ impl HostAdapter for LinuxHost {
} }
} }
StopMode::Force => { StopMode::Force => {
// Force kill via snap cgroup or command name // Force kill via snap/flatpak cgroup or command name
if let Some(ref info) = session_info { if let Some(ref info) = session_info {
if let Some(ref snap) = info.snap_name { if let Some(ref snap) = info.snap_name {
kill_snap_cgroup(snap, nix::sys::signal::Signal::SIGKILL); kill_snap_cgroup(snap, nix::sys::signal::Signal::SIGKILL);
info!(snap = %snap, "Sent SIGKILL via snap cgroup"); info!(snap = %snap, "Sent SIGKILL via snap cgroup");
} else if let Some(ref app_id) = info.flatpak_app_id {
kill_flatpak_cgroup(app_id, nix::sys::signal::Signal::SIGKILL);
info!(flatpak = %app_id, "Sent SIGKILL via flatpak cgroup");
} else { } else {
kill_by_command(&info.command_name, nix::sys::signal::Signal::SIGKILL); kill_by_command(&info.command_name, nix::sys::signal::Signal::SIGKILL);
info!(command = %info.command_name, "Sent SIGKILL via command name"); info!(command = %info.command_name, "Sent SIGKILL via command name");

View file

@ -91,6 +91,70 @@ pub fn kill_snap_cgroup(snap_name: &str, _signal: Signal) -> bool {
stopped_any stopped_any
} }
/// Kill all processes in a Flatpak app's cgroup using systemd
/// Flatpak apps create scopes at: app-flatpak-<app_id>-<number>.scope
/// For example: app-flatpak-org.prismlauncher.PrismLauncher-12345.scope
/// Similar to snap apps, we use systemctl --user to manage the scopes.
pub fn kill_flatpak_cgroup(app_id: &str, _signal: Signal) -> bool {
let uid = nix::unistd::getuid().as_raw();
let base_path = format!(
"/sys/fs/cgroup/user.slice/user-{}.slice/user@{}.service/app.slice",
uid, uid
);
// Flatpak uses a different naming pattern than snap
// The app_id dots are preserved: app-flatpak-org.example.App-<number>.scope
let pattern = format!("app-flatpak-{}-", app_id);
let base = std::path::Path::new(&base_path);
if !base.exists() {
debug!(path = %base_path, "Flatpak cgroup base path doesn't exist");
return false;
}
let mut stopped_any = false;
if let Ok(entries) = std::fs::read_dir(base) {
for entry in entries.flatten() {
let name = entry.file_name();
let name_str = name.to_string_lossy();
if name_str.starts_with(&pattern) && name_str.ends_with(".scope") {
let scope_name = name_str.to_string();
// Always use SIGKILL for flatpak apps to prevent self-restart behavior
// Using systemctl kill --signal=KILL sends SIGKILL to all processes in scope
let result = Command::new("systemctl")
.args(["--user", "kill", "--signal=KILL", &scope_name])
.output();
match result {
Ok(output) => {
if output.status.success() {
info!(scope = %scope_name, "Killed flatpak scope via systemctl SIGKILL");
stopped_any = true;
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
warn!(scope = %scope_name, stderr = %stderr, "systemctl kill command failed");
}
}
Err(e) => {
warn!(scope = %scope_name, error = %e, "Failed to run systemctl");
}
}
}
}
}
if stopped_any {
info!(app_id = app_id, "Killed flatpak scope(s) via systemctl SIGKILL");
} else {
debug!(app_id = app_id, "No flatpak scope found to kill");
}
stopped_any
}
/// Kill processes by command name using pkill /// Kill processes by command name using pkill
pub fn kill_by_command(command_name: &str, signal: Signal) -> bool { pub fn kill_by_command(command_name: &str, signal: Signal) -> bool {
let signal_name = match signal { let signal_name = match signal {

View file

@ -79,6 +79,7 @@ impl LauncherTile {
let fallback_icon = match entry.kind_tag { let fallback_icon = match entry.kind_tag {
shepherd_api::EntryKindTag::Process => "application-x-executable", shepherd_api::EntryKindTag::Process => "application-x-executable",
shepherd_api::EntryKindTag::Snap => "application-x-executable", shepherd_api::EntryKindTag::Snap => "application-x-executable",
shepherd_api::EntryKindTag::Flatpak => "application-x-executable",
shepherd_api::EntryKindTag::Vm => "computer", shepherd_api::EntryKindTag::Vm => "computer",
shepherd_api::EntryKindTag::Media => "video-x-generic", shepherd_api::EntryKindTag::Media => "video-x-generic",
shepherd_api::EntryKindTag::Custom => "applications-other", shepherd_api::EntryKindTag::Custom => "applications-other",

View file

@ -127,9 +127,10 @@ EOF
install_config() { install_config() {
local user="${1:-}" local user="${1:-}"
local source_config="${2:-}" local source_config="${2:-}"
local force="${3:-false}"
if [[ -z "$user" ]]; then if [[ -z "$user" ]]; then
die "Usage: shepherd install config --user USER [--source CONFIG]" die "Usage: shepherd install config --user USER [--source CONFIG] [--force]"
fi fi
validate_user "$user" validate_user "$user"
@ -161,7 +162,15 @@ install_config() {
# Check if config already exists # Check if config already exists
if maybe_sudo test -f "$dst_config"; then if maybe_sudo test -f "$dst_config"; then
warn "Config file already exists at $dst_config, skipping" if [[ "$force" == "true" ]]; then
warn "Overwriting existing config at $dst_config"
maybe_sudo cp "$source_config" "$dst_config"
maybe_sudo chown "$user:$user" "$dst_config"
maybe_sudo chmod 0644 "$dst_config"
success "Overwrote user configuration for $user"
else
warn "Config file already exists at $dst_config, skipping (use --force to overwrite)"
fi
else else
# Copy config file # Copy config file
maybe_sudo cp "$source_config" "$dst_config" maybe_sudo cp "$source_config" "$dst_config"
@ -175,9 +184,10 @@ install_config() {
install_all() { install_all() {
local user="${1:-}" local user="${1:-}"
local prefix="${2:-$DEFAULT_PREFIX}" local prefix="${2:-$DEFAULT_PREFIX}"
local force="${3:-false}"
if [[ -z "$user" ]]; then if [[ -z "$user" ]]; then
die "Usage: shepherd install all --user USER [--prefix PREFIX]" die "Usage: shepherd install all --user USER [--prefix PREFIX] [--force]"
fi fi
require_root require_root
@ -188,7 +198,7 @@ install_all() {
install_bins "$prefix" install_bins "$prefix"
install_sway_config "$prefix" install_sway_config "$prefix"
install_desktop_entry "$prefix" install_desktop_entry "$prefix"
install_config "$user" install_config "$user" "" "$force"
success "Installation complete!" success "Installation complete!"
info "" info ""
@ -206,6 +216,7 @@ install_main() {
local user="" local user=""
local prefix="$DEFAULT_PREFIX" local prefix="$DEFAULT_PREFIX"
local source_config="" local source_config=""
local force="false"
# Parse remaining arguments # Parse remaining arguments
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
@ -222,6 +233,10 @@ install_main() {
source_config="$2" source_config="$2"
shift 2 shift 2
;; ;;
--force|-f)
force="true"
shift
;;
*) *)
die "Unknown option: $1" die "Unknown option: $1"
;; ;;
@ -233,7 +248,7 @@ install_main() {
install_bins "$prefix" install_bins "$prefix"
;; ;;
config) config)
install_config "$user" "$source_config" install_config "$user" "$source_config" "$force"
;; ;;
sway-config) sway-config)
install_sway_config "$prefix" install_sway_config "$prefix"
@ -242,7 +257,7 @@ install_main() {
install_desktop_entry "$prefix" install_desktop_entry "$prefix"
;; ;;
all) all)
install_all "$user" "$prefix" install_all "$user" "$prefix" "$force"
;; ;;
""|help|-h|--help) ""|help|-h|--help)
cat <<EOF cat <<EOF
@ -259,6 +274,7 @@ Options:
--user USER Target user for config deployment (required for config/all) --user USER Target user for config deployment (required for config/all)
--prefix PREFIX Installation prefix (default: $DEFAULT_PREFIX) --prefix PREFIX Installation prefix (default: $DEFAULT_PREFIX)
--source CONFIG Source config file (default: config.example.toml) --source CONFIG Source config file (default: config.example.toml)
--force, -f Overwrite existing configuration files
Environment: Environment:
DESTDIR Installation root for packaging (default: empty) DESTDIR Installation root for packaging (default: empty)
@ -266,6 +282,7 @@ Environment:
Examples: Examples:
shepherd install bins --prefix /usr/local shepherd install bins --prefix /usr/local
shepherd install config --user kiosk shepherd install config --user kiosk
shepherd install config --user kiosk --force
shepherd install all --user kiosk --prefix /usr shepherd install all --user kiosk --prefix /usr
EOF EOF
;; ;;