use crate::config;
use gtk::prelude::*;
use gtk::{gio, glib};
use serde::Deserialize;
use std::cell::{Cell, RefCell};
use std::collections::BTreeMap;
use std::env;
use std::ffi::OsString;
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::rc::Rc;
const OFFICIAL_PLUGINS: &[PluginSpec] = &[PluginSpec {
key: "lios_bar",
name: "LiosBar",
crate_name: "lios-bar",
binary_name: "lios-bar",
version: "0.1.1",
description: "Sidebar workspace for navigating several live terminal sessions.",
}];
#[derive(Debug, Clone, Copy)]
struct PluginSpec {
key: &'static str,
name: &'static str,
crate_name: &'static str,
binary_name: &'static str,
version: &'static str,
description: &'static str,
}
struct PluginState {
spec: &'static PluginSpec,
config_path: Option<PathBuf>,
toggle: glib::WeakRef<gtk::ToggleButton>,
install: glib::WeakRef<gtk::Button>,
launch: glib::WeakRef<gtk::Button>,
state_label: glib::WeakRef<gtk::Label>,
global_status: glib::WeakRef<gtk::Label>,
syncing: Cell<bool>,
busy: Cell<bool>,
installed: RefCell<PluginInstallation>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum PluginInstallation {
Missing,
Unverified(PathBuf),
Verified(PathBuf),
}
impl PluginInstallation {
fn is_verified(&self) -> bool {
matches!(self, Self::Verified(_))
}
fn launch_path(&self) -> Option<PathBuf> {
match self {
Self::Verified(path) => Some(path.clone()),
Self::Missing | Self::Unverified(_) => None,
}
}
}
#[derive(Debug, Deserialize)]
struct CargoInstallMetadata {
installs: BTreeMap<String, CargoInstallEntry>,
}
#[derive(Debug, Deserialize)]
struct CargoInstallEntry {
bins: Vec<String>,
}
pub(crate) fn build_card(status: >k::Label, config_path: Option<PathBuf>) -> gtk::Box {
let card = gtk::Box::new(gtk::Orientation::Vertical, 6);
card.add_css_class("lios-card");
card.set_hexpand(true);
card.set_vexpand(false);
card.set_size_request(230, -1);
let title = gtk::Label::new(Some("Plugins"));
title.add_css_class("lios-row-label");
title.set_xalign(0.0);
let subtitle = gtk::Label::new(Some(
"Curated, version-pinned helpers. Disable keeps the binary installed.",
));
subtitle.add_css_class("lios-muted");
subtitle.set_xalign(0.0);
subtitle.set_wrap(true);
card.append(&title);
card.append(&subtitle);
for spec in OFFICIAL_PLUGINS {
card.append(&build_plugin_row(spec, status, config_path.clone()));
}
let boundary = gtk::Label::new(Some(
"Installs run only after your click. No arbitrary package names, automatic updates, or background downloads.",
));
boundary.add_css_class("lios-hint");
boundary.set_xalign(0.0);
boundary.set_wrap(true);
card.append(&boundary);
card
}
fn build_plugin_row(
spec: &'static PluginSpec,
global_status: >k::Label,
config_path: Option<PathBuf>,
) -> gtk::Box {
let row = gtk::Box::new(gtk::Orientation::Vertical, 5);
row.add_css_class("lios-plugin-row");
let heading = gtk::Box::new(gtk::Orientation::Horizontal, 6);
let identity = gtk::Box::new(gtk::Orientation::Vertical, 1);
identity.set_hexpand(true);
let name = gtk::Label::new(Some(spec.name));
name.add_css_class("lios-row-label");
name.set_xalign(0.0);
let description = gtk::Label::new(Some(spec.description));
description.add_css_class("lios-muted");
description.set_xalign(0.0);
description.set_wrap(true);
identity.append(&name);
identity.append(&description);
let toggle = gtk::ToggleButton::with_label("Disabled");
toggle.add_css_class("lios-choice");
toggle.set_tooltip_text(Some(
"Enable Lios integration; disabling does not uninstall the plugin",
));
heading.append(&identity);
heading.append(&toggle);
row.append(&heading);
let state_label = gtk::Label::new(None);
state_label.add_css_class("lios-hint");
state_label.set_xalign(0.0);
state_label.set_ellipsize(gtk::pango::EllipsizeMode::Middle);
row.append(&state_label);
let actions = gtk::Box::new(gtk::Orientation::Horizontal, 6);
let install = gtk::Button::with_label("Install");
install.add_css_class("lios-choice");
install.set_hexpand(true);
install.set_tooltip_text(Some(
"Install or repair the exact reviewed crates.io release with Cargo",
));
let launch = gtk::Button::with_label("Launch");
launch.add_css_class("lios-choice");
launch.set_hexpand(true);
launch.set_tooltip_text(Some("Launch the enabled installed plugin"));
actions.append(&install);
actions.append(&launch);
row.append(&actions);
let installed = detect_installation(spec);
let enabled = match config::plugin_enabled(config_path.as_deref(), spec.key) {
Ok(enabled) => enabled,
Err(error) => {
global_status.set_text(&error);
false
}
};
let state = Rc::new(PluginState {
spec,
config_path,
toggle: toggle.downgrade(),
install: install.downgrade(),
launch: launch.downgrade(),
state_label: state_label.downgrade(),
global_status: global_status.downgrade(),
syncing: Cell::new(true),
busy: Cell::new(false),
installed: RefCell::new(installed),
});
toggle.set_active(enabled);
state.syncing.set(false);
refresh(&state);
toggle.connect_toggled({
let state = state.clone();
move |toggle| {
if state.syncing.get() {
return;
}
let enabled = toggle.is_active();
if !enabled {
match save_enabled(&state, false) {
Ok(()) => set_global_status(
&state,
&format!(
"{} disabled. Its installed binary was preserved.",
state.spec.name
),
),
Err(error) => {
set_global_status(&state, &error);
set_toggle(&state, true);
}
}
refresh(&state);
return;
}
if !state.installed.borrow().is_verified() {
begin_install(&state, true);
return;
}
match save_enabled(&state, true) {
Ok(()) => set_global_status(
&state,
&format!("{} enabled and ready to launch.", state.spec.name),
),
Err(error) => {
set_global_status(&state, &error);
set_toggle(&state, false);
}
}
refresh(&state);
}
});
install.connect_clicked({
let state = state.clone();
move |_| begin_install(&state, false)
});
launch.connect_clicked({
let state = state.clone();
move |_| launch_plugin(&state)
});
row
}
fn begin_install(state: &Rc<PluginState>, enabled_by_toggle: bool) {
if state.busy.replace(true) {
return;
}
refresh(state);
set_global_status(
state,
&format!(
"Installing pinned {} {} with Cargo…",
state.spec.name, state.spec.version
),
);
let Some(cargo) = executable_path("cargo") else {
finish_install_error(
state,
enabled_by_toggle,
"Cargo was not found in PATH, CARGO_HOME, or ~/.cargo/bin.",
);
return;
};
let arguments = install_arguments(&cargo, state.spec);
let argument_refs = arguments
.iter()
.map(OsString::as_os_str)
.collect::<Vec<_>>();
let flags = gio::SubprocessFlags::STDOUT_SILENCE | gio::SubprocessFlags::STDERR_SILENCE;
let process = match gio::Subprocess::newv(&argument_refs, flags) {
Ok(process) => process,
Err(error) => {
finish_install_error(state, enabled_by_toggle, &error.to_string());
return;
}
};
let state = state.clone();
process.wait_check_async(gio::Cancellable::NONE, move |result| match result {
Ok(()) => finish_install_success(&state),
Err(error) => finish_install_error(&state, enabled_by_toggle, &error.to_string()),
});
}
fn finish_install_success(state: &Rc<PluginState>) {
let installed = detect_installation(state.spec);
*state.installed.borrow_mut() = installed;
state.busy.set(false);
if !state.installed.borrow().is_verified() {
set_toggle(state, false);
set_global_status(
state,
&format!(
"{} installation completed, but '{}' is not discoverable. Add Cargo's bin directory to PATH.",
state.spec.name, state.spec.binary_name
),
);
refresh(state);
return;
}
let enabled = state
.toggle
.upgrade()
.is_some_and(|toggle| toggle.is_active());
if let Err(error) = save_enabled(state, enabled) {
set_toggle(state, false);
set_global_status(state, &error);
} else if enabled {
set_global_status(
state,
&format!("{} installed, enabled, and ready.", state.spec.name),
);
} else {
set_global_status(
state,
&format!(
"{} installed. Enable it when you want Lios to use it.",
state.spec.name
),
);
}
refresh(state);
}
fn finish_install_error(state: &Rc<PluginState>, enabled_by_toggle: bool, error: &str) {
state.busy.set(false);
if enabled_by_toggle {
set_toggle(state, false);
}
set_global_status(
state,
&format!(
"Could not install {} {}: {}",
state.spec.name,
state.spec.version,
bounded_message(error)
),
);
refresh(state);
}
fn launch_plugin(state: &Rc<PluginState>) {
let enabled = state
.toggle
.upgrade()
.is_some_and(|toggle| toggle.is_active());
if !enabled {
set_global_status(
state,
&format!("Enable {} before launching it.", state.spec.name),
);
return;
}
*state.installed.borrow_mut() = detect_installation(state.spec);
let Some(binary) = state.installed.borrow().launch_path() else {
set_global_status(
state,
&format!(
"{} is missing or not the pinned registry release. Install or repair it first.",
state.spec.name
),
);
refresh(state);
return;
};
let launcher = gio::SubprocessLauncher::new(gio::SubprocessFlags::NONE);
launcher.unsetenv("GSK_RENDERER");
match launcher.spawn(&[binary.as_os_str()]) {
Ok(_) => set_global_status(state, &format!("Launched {}.", state.spec.name)),
Err(error) => set_global_status(
state,
&format!("Could not launch {}: {error}", state.spec.name),
),
}
}
fn save_enabled(state: &PluginState, enabled: bool) -> Result<(), String> {
match &state.config_path {
Some(path) => config::set_plugin_enabled(path, state.spec.key, enabled),
None => Ok(()),
}
}
fn set_toggle(state: &PluginState, enabled: bool) {
let Some(toggle) = state.toggle.upgrade() else {
return;
};
state.syncing.set(true);
toggle.set_active(enabled);
state.syncing.set(false);
}
fn refresh(state: &PluginState) {
let enabled = state
.toggle
.upgrade()
.is_some_and(|toggle| toggle.is_active());
let installation = state.installed.borrow().clone();
let installed = installation.is_verified();
let unverified = matches!(installation, PluginInstallation::Unverified(_));
let busy = state.busy.get();
if let Some(toggle) = state.toggle.upgrade() {
toggle.set_sensitive(!busy);
toggle.set_label(if enabled { "Enabled" } else { "Disabled" });
if enabled {
toggle.add_css_class("lios-selected");
} else {
toggle.remove_css_class("lios-selected");
}
}
if let Some(install) = state.install.upgrade() {
install.set_sensitive(!busy);
install.set_label(if busy {
"Installing…"
} else if installed || unverified {
"Repair"
} else {
"Install"
});
}
if let Some(launch) = state.launch.upgrade() {
launch.set_sensitive(installed && enabled && !busy);
}
if let Some(label) = state.state_label.upgrade() {
let text = match (installed, enabled, busy) {
(_, _, true) => format!(
"INSTALLING {} {} · EXACT VERSION",
state.spec.crate_name, state.spec.version
),
(true, true, false) => {
format!("INSTALLED {} · ENABLED", state.spec.version)
}
(true, false, false) => {
format!("INSTALLED {} · DISABLED", state.spec.version)
}
(false, _, false) if unverified => "UNVERIFIED BINARY · REPAIR REQUIRED".to_string(),
(false, true, false) => "ENABLED · INSTALL REQUIRED".to_string(),
(false, false, false) => "NOT INSTALLED · DISABLED".to_string(),
};
label.set_text(&text);
}
}
fn set_global_status(state: &PluginState, message: &str) {
if let Some(status) = state.global_status.upgrade() {
status.set_text(message);
}
}
fn install_arguments(cargo: &Path, spec: &PluginSpec) -> Vec<OsString> {
vec![
cargo.as_os_str().to_os_string(),
OsString::from("install"),
OsString::from("--locked"),
OsString::from("--force"),
OsString::from("--registry"),
OsString::from("crates-io"),
OsString::from("--version"),
OsString::from(format!("={}", spec.version)),
OsString::from(spec.crate_name),
]
}
fn executable_path(binary: &str) -> Option<PathBuf> {
let mut directories = env::var_os("PATH")
.map(|path| env::split_paths(&path).collect::<Vec<_>>())
.unwrap_or_default();
if let Ok(current) = env::current_exe() {
if let Some(parent) = current.parent() {
directories.push(parent.to_path_buf());
}
}
if let Some(cargo_home) = env::var_os("CARGO_HOME").filter(|value| !value.is_empty()) {
directories.push(PathBuf::from(cargo_home).join("bin"));
}
if let Some(home) = env::var_os("HOME").filter(|value| !value.is_empty()) {
directories.push(PathBuf::from(home).join(".cargo/bin"));
}
find_executable_in(binary, &directories)
}
fn detect_installation(spec: &PluginSpec) -> PluginInstallation {
let Some(binary) = executable_path(spec.binary_name) else {
return PluginInstallation::Missing;
};
if cargo_metadata_matches(&binary, spec) {
PluginInstallation::Verified(binary)
} else {
PluginInstallation::Unverified(binary)
}
}
fn cargo_metadata_matches(binary: &Path, spec: &PluginSpec) -> bool {
let Some(root) = binary.parent().and_then(Path::parent) else {
return false;
};
let Ok(contents) = fs::read(root.join(".crates2.json")) else {
return false;
};
let Ok(metadata) = serde_json::from_slice::<CargoInstallMetadata>(&contents) else {
return false;
};
let expected_identity = format!(
"{} {} (registry+https://github.com/rust-lang/crates.io-index)",
spec.crate_name, spec.version
);
metadata.installs.iter().any(|(identity, entry)| {
identity == &expected_identity && entry.bins.iter().any(|binary| binary == spec.binary_name)
})
}
fn find_executable_in(binary: &str, directories: &[PathBuf]) -> Option<PathBuf> {
if binary.is_empty() || binary.as_bytes().contains(&b'/') {
return None;
}
directories.iter().find_map(|directory| {
let candidate = directory.join(binary);
is_executable(&candidate).then_some(candidate)
})
}
fn is_executable(path: &Path) -> bool {
fs::metadata(path)
.is_ok_and(|metadata| metadata.is_file() && metadata.permissions().mode() & 0o111 != 0)
}
fn bounded_message(message: &str) -> String {
const MAX_CHARS: usize = 180;
let normalized = message.split_whitespace().collect::<Vec<_>>().join(" ");
let mut bounded = normalized.chars().take(MAX_CHARS).collect::<String>();
if normalized.chars().count() > MAX_CHARS {
bounded.push('…');
}
bounded
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::OpenOptions;
use std::os::unix::fs::OpenOptionsExt;
use std::time::{SystemTime, UNIX_EPOCH};
#[test]
fn official_install_is_exact_locked_and_never_uses_a_shell() {
let spec = &OFFICIAL_PLUGINS[0];
let arguments = install_arguments(Path::new("/usr/bin/cargo"), spec);
let actual = arguments
.iter()
.map(|argument| argument.to_string_lossy().into_owned())
.collect::<Vec<_>>();
assert_eq!(
actual,
[
"/usr/bin/cargo",
"install",
"--locked",
"--force",
"--registry",
"crates-io",
"--version",
"=0.1.1",
"lios-bar"
]
);
assert!(!actual.iter().any(|argument| {
matches!(
argument.as_str(),
"sh" | "/bin/sh" | "bash" | "/bin/bash" | "-c"
)
}));
}
#[test]
fn executable_discovery_rejects_paths_and_requires_execute_permission() {
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let directory =
env::temp_dir().join(format!("lios-plugin-path-{}-{nonce}", std::process::id()));
fs::create_dir_all(&directory).unwrap();
let binary = directory.join("lios-bar");
OpenOptions::new()
.write(true)
.create_new(true)
.mode(0o600)
.open(&binary)
.unwrap();
assert_eq!(
find_executable_in("lios-bar", std::slice::from_ref(&directory)),
None
);
fs::set_permissions(&binary, fs::Permissions::from_mode(0o700)).unwrap();
assert_eq!(
find_executable_in("lios-bar", std::slice::from_ref(&directory)),
Some(binary.clone())
);
assert_eq!(
find_executable_in("../lios-bar", std::slice::from_ref(&directory)),
None
);
fs::remove_dir_all(directory).unwrap();
}
#[test]
fn error_messages_are_single_line_and_bounded() {
let message = format!("first line\n{}", "x".repeat(300));
let bounded = bounded_message(&message);
assert!(!bounded.contains('\n'));
assert!(bounded.chars().count() <= 181);
}
#[test]
fn cargo_metadata_requires_exact_registry_version_and_binary() {
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let root = env::temp_dir().join(format!("lios-plugin-meta-{}-{nonce}", std::process::id()));
let bin = root.join("bin");
fs::create_dir_all(&bin).unwrap();
let binary = bin.join("lios-bar");
OpenOptions::new()
.write(true)
.create_new(true)
.mode(0o700)
.open(&binary)
.unwrap();
let spec = &OFFICIAL_PLUGINS[0];
fs::write(
root.join(".crates2.json"),
r#"{"installs":{"lios-bar 0.0.9 (registry+https://github.com/rust-lang/crates.io-index)":{"bins":["lios-bar"]}}}"#,
)
.unwrap();
assert!(!cargo_metadata_matches(&binary, spec));
fs::write(
root.join(".crates2.json"),
r#"{"installs":{"lios-bar 0.1.1 (path+file:///tmp/LiosBar)":{"bins":["lios-bar"]}}}"#,
)
.unwrap();
assert!(!cargo_metadata_matches(&binary, spec));
fs::write(
root.join(".crates2.json"),
r#"{"installs":{"lios-bar 0.1.1 (registry+https://packages.example.invalid/index)":{"bins":["lios-bar"]}}}"#,
)
.unwrap();
assert!(!cargo_metadata_matches(&binary, spec));
fs::write(
root.join(".crates2.json"),
r#"{"installs":{"lios-bar 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)-lookalike":{"bins":["lios-bar"]}}}"#,
)
.unwrap();
assert!(!cargo_metadata_matches(&binary, spec));
fs::write(
root.join(".crates2.json"),
r#"{"installs":{"lios-bar 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)":{"bins":["lios-bar"]}}}"#,
)
.unwrap();
assert!(cargo_metadata_matches(&binary, spec));
fs::remove_dir_all(root).unwrap();
}
}