Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 67 additions & 39 deletions crates/xtop-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,26 +66,65 @@ fn key_event_to_str(key: &KeyEvent) -> String {
// Embedded default asset files (shipped with the binary)
const DEFAULT_THEMES: &[(&str, &str)] = &[
("x", include_str!("../../../assets/themes/x.jsonc")),
("madrid", include_str!("../../../assets/themes/madrid.jsonc")),
("lahabana", include_str!("../../../assets/themes/lahabana.jsonc")),
(
"madrid",
include_str!("../../../assets/themes/madrid.jsonc"),
),
(
"lahabana",
include_str!("../../../assets/themes/lahabana.jsonc"),
),
("paris", include_str!("../../../assets/themes/paris.jsonc")),
("tokio", include_str!("../../../assets/themes/tokio.jsonc")),
("oslo", include_str!("../../../assets/themes/oslo.jsonc")),
("helsinki", include_str!("../../../assets/themes/helsinki.jsonc")),
("berlin", include_str!("../../../assets/themes/berlin.jsonc")),
("london", include_str!("../../../assets/themes/london.jsonc")),
(
"helsinki",
include_str!("../../../assets/themes/helsinki.jsonc"),
),
(
"berlin",
include_str!("../../../assets/themes/berlin.jsonc"),
),
(
"london",
include_str!("../../../assets/themes/london.jsonc"),
),
("praha", include_str!("../../../assets/themes/praha.jsonc")),
("bogota", include_str!("../../../assets/themes/bogota.jsonc")),
(
"bogota",
include_str!("../../../assets/themes/bogota.jsonc"),
),
];

const DEFAULT_LAYOUTS: &[(&str, &str)] = &[
("dashboard", include_str!("../../../assets/layouts/dashboard.jsonc")),
("vertical", include_str!("../../../assets/layouts/vertical.jsonc")),
("horizontal", include_str!("../../../assets/layouts/horizontal.jsonc")),
("cpu_focus", include_str!("../../../assets/layouts/cpu_focus.jsonc")),
("memory_focus", include_str!("../../../assets/layouts/memory_focus.jsonc")),
("network_focus", include_str!("../../../assets/layouts/network_focus.jsonc")),
("process_focus", include_str!("../../../assets/layouts/process_focus.jsonc")),
(
"dashboard",
include_str!("../../../assets/layouts/dashboard.jsonc"),
),
(
"vertical",
include_str!("../../../assets/layouts/vertical.jsonc"),
),
(
"horizontal",
include_str!("../../../assets/layouts/horizontal.jsonc"),
),
(
"cpu_focus",
include_str!("../../../assets/layouts/cpu_focus.jsonc"),
),
(
"memory_focus",
include_str!("../../../assets/layouts/memory_focus.jsonc"),
),
(
"network_focus",
include_str!("../../../assets/layouts/network_focus.jsonc"),
),
(
"process_focus",
include_str!("../../../assets/layouts/process_focus.jsonc"),
),
];

fn ensure_default_assets() {
Expand Down Expand Up @@ -160,7 +199,9 @@ fn print_usage() {
eprintln!(" xtop Start the TUI system monitor");
eprintln!(" xtop mcp Start MCP server (stdio transport) for AI agents");
eprintln!(" xtop plugin list List installed plugins");
eprintln!(" xtop plugin install <name> Install a plugin from github.com/xscriptor/xtop/plugins/");
eprintln!(
" xtop plugin install <name> Install a plugin from github.com/xscriptor/xtop/plugins/"
);
eprintln!(" xtop plugin install <url> Install a plugin from a git URL");
eprintln!(" xtop plugin scaffold <name> Create a new plugin crate");
}
Expand Down Expand Up @@ -315,7 +356,10 @@ fn cmd_plugin_install(name_or_url: &str) -> anyhow::Result<()> {
// --- Copy into local plugins/ directory ---
let target_dir = plugins_dir.join(&plugin_dir_name);
if target_dir.exists() {
anyhow::bail!("Plugin '{}' already exists at plugins/{plugin_dir_name}", pkg_name);
anyhow::bail!(
"Plugin '{}' already exists at plugins/{plugin_dir_name}",
pkg_name
);
}
fs::create_dir_all(&plugins_dir)?;
cp_recursive(&plugin_src, &target_dir)?;
Expand All @@ -331,17 +375,11 @@ fn cmd_plugin_install(name_or_url: &str) -> anyhow::Result<()> {
let new_ws = if ws_content.contains(sentinel_entry) {
ws_content.replace(
sentinel_entry,
&format!(
"{sentinel_entry}\n{member_entry},"
),
&format!("{sentinel_entry}\n{member_entry},"),
)
} else {
// Fallback: insert before the closing ] of members
ws_content.replacen(
"]",
&format!(" {member_entry},\n]"),
1,
)
ws_content.replacen("]", &format!(" {member_entry},\n]"), 1)
};
fs::write(&workspace_toml, &new_ws)?;

Expand All @@ -350,17 +388,12 @@ fn cmd_plugin_install(name_or_url: &str) -> anyhow::Result<()> {

// Build dependency path relative to crates/xtop-cli/
let dep_path = format!("../../plugins/{plugin_dir_name}");
let dep_line = format!(
"{pkg_name} = {{ path = \"{dep_path}\", optional = true }}"
);
let dep_line = format!("{pkg_name} = {{ path = \"{dep_path}\", optional = true }}");

if !cli_content.contains(&dep_line) {
// Find the last optional plugin dependency and insert after it
let marker = "# Optional plugins (behind feature flags)";
let new_cli = cli_content.replace(
marker,
&format!("{marker}\n{dep_line}"),
);
let new_cli = cli_content.replace(marker, &format!("{marker}\n{dep_line}"));
fs::write(&cli_toml, &new_cli)?;
}

Expand All @@ -375,11 +408,7 @@ fn cmd_plugin_install(name_or_url: &str) -> anyhow::Result<()> {
&format!("{sentinel_feature}\n{feature_line}"),
)
} else {
cli_content2.replacen(
"[features]",
&format!("[features]\n{feature_line}"),
1,
)
cli_content2.replacen("[features]", &format!("[features]\n{feature_line}"), 1)
};
fs::write(&cli_toml, &new_cli2)?;
}
Expand Down Expand Up @@ -601,9 +630,8 @@ fn main() -> anyhow::Result<()> {

// Give plugins first chance to consume the key
let key_str_clone = key_str.clone();
let key_consumed = state.with_plugin_manager_mut(|mgr, this| {
mgr.handle_key(this, &key_str_clone)
});
let key_consumed =
state.with_plugin_manager_mut(|mgr, this| mgr.handle_key(this, &key_str_clone));
if key_consumed {
continue;
}
Expand Down Expand Up @@ -689,7 +717,7 @@ fn main() -> anyhow::Result<()> {
}
_ => {}
}
},
}
}
}
}
Expand Down
11 changes: 6 additions & 5 deletions crates/xtop-core/src/application/plugin_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@
) -> Result<(), PluginError> {
let id = plugin.manifest().id.clone();
let data_dir = self.plugin_data_base.join(&id);
std::fs::create_dir_all(&data_dir)
.map_err(|e| PluginError::Recoverable(format!("failed to create plugin data dir for {id}: {e}")))?;
std::fs::create_dir_all(&data_dir).map_err(|e| {
PluginError::Recoverable(format!("failed to create plugin data dir for {id}: {e}"))
})?;

let capabilities = plugin.manifest().capabilities.clone();
let mut ctx = PluginContext {
Expand All @@ -66,13 +67,13 @@
Ok(())
}

fn plugin_has_capability(plugin: &Box<dyn Plugin>, cap: &PluginCapability) -> bool {

Check failure on line 70 in crates/xtop-core/src/application/plugin_manager.rs

View workflow job for this annotation

GitHub Actions / Check

you seem to be trying to use `&Box<T>`. Consider using just `&T`
plugin.manifest().capabilities.contains(cap)
}

fn build_context<'a>(
base: &std::path::Path,
plugin: &Box<dyn Plugin>,

Check failure on line 76 in crates/xtop-core/src/application/plugin_manager.rs

View workflow job for this annotation

GitHub Actions / Check

you seem to be trying to use `&Box<T>`. Consider using just `&T`
state: &'a mut AppState,
) -> PluginContext<'a> {
let id = plugin.manifest().id.clone();
Expand Down Expand Up @@ -159,7 +160,9 @@
let mut ctx = Self::build_context(&base, plugin, state);
return plugin.execute(&mut ctx, action, params);
}
Err(PluginError::Recoverable(format!("plugin not found: {plugin_id}")))
Err(PluginError::Recoverable(format!(
"plugin not found: {plugin_id}"
)))
}

/// List all loaded plugin manifests (for display / status).
Expand All @@ -184,5 +187,3 @@
}
}
}


2 changes: 1 addition & 1 deletion crates/xtop-core/src/application/state.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use crate::application::history::MetricsHistory;
use crate::application::plugin_manager::PluginManager;
use crate::domain::keybinding::{Action, Keybindings};
use crate::domain::metrics::SystemInfo;
use crate::domain::layout::LayoutDef;
use crate::domain::metrics::SystemInfo;
use crate::domain::metrics::SystemSnapshot;
use crate::domain::plugin::WidgetRegistration;
use crate::domain::system_info::SystemDataProvider;
Expand Down
56 changes: 42 additions & 14 deletions crates/xtop-core/src/domain/keybinding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,48 @@ pub struct Keybindings {
pub cycle_sort: Vec<String>,
}

fn vec_one_q() -> Vec<String> { vec!["q".into()] }
fn vec_one_question() -> Vec<String> { vec!["?".into()] }
fn vec_one_t() -> Vec<String> { vec!["t".into()] }
fn vec_one_shift_t() -> Vec<String> { vec!["T".into()] }
fn vec_one_l() -> Vec<String> { vec!["l".into()] }
fn vec_one_f() -> Vec<String> { vec!["f".into()] }
fn vec_one_shift_f() -> Vec<String> { vec!["F".into()] }
fn vec_one_slash() -> Vec<String> { vec!["/".into()] }
fn vec_one_ctrl_p() -> Vec<String> { vec!["ctrl+p".into(), "ctrl+P".into()] }
fn vec_one_escape() -> Vec<String> { vec!["escape".into()] }
fn vec_one_k() -> Vec<String> { vec!["k".into()] }
fn vec_one_up() -> Vec<String> { vec!["up".into()] }
fn vec_one_down() -> Vec<String> { vec!["down".into()] }
fn vec_one_s() -> Vec<String> { vec!["s".into()] }
fn vec_one_q() -> Vec<String> {
vec!["q".into()]
}
fn vec_one_question() -> Vec<String> {
vec!["?".into()]
}
fn vec_one_t() -> Vec<String> {
vec!["t".into()]
}
fn vec_one_shift_t() -> Vec<String> {
vec!["T".into()]
}
fn vec_one_l() -> Vec<String> {
vec!["l".into()]
}
fn vec_one_f() -> Vec<String> {
vec!["f".into()]
}
fn vec_one_shift_f() -> Vec<String> {
vec!["F".into()]
}
fn vec_one_slash() -> Vec<String> {
vec!["/".into()]
}
fn vec_one_ctrl_p() -> Vec<String> {
vec!["ctrl+p".into(), "ctrl+P".into()]
}
fn vec_one_escape() -> Vec<String> {
vec!["escape".into()]
}
fn vec_one_k() -> Vec<String> {
vec!["k".into()]
}
fn vec_one_up() -> Vec<String> {
vec!["up".into()]
}
fn vec_one_down() -> Vec<String> {
vec!["down".into()]
}
fn vec_one_s() -> Vec<String> {
vec!["s".into()]
}

impl Default for Keybindings {
fn default() -> Self {
Expand Down
7 changes: 4 additions & 3 deletions crates/xtop-core/src/domain/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,10 @@ impl TryFrom<LayoutAreaRaw> for LayoutArea {
Some(SizeRaw::Num(n)) => LayoutConstraint::Length(n),
Some(SizeRaw::Str(s)) if s == "*" => LayoutConstraint::Fill,
Some(SizeRaw::Str(s)) if s.ends_with('%') => {
let pct = s.trim_end_matches('%').parse::<u16>().map_err(|_| {
format!("invalid percentage: {s}")
})?;
let pct = s
.trim_end_matches('%')
.parse::<u16>()
.map_err(|_| format!("invalid percentage: {s}"))?;
LayoutConstraint::Percentage(pct)
}
Some(SizeRaw::Str(s)) => {
Expand Down
17 changes: 10 additions & 7 deletions crates/xtop-core/src/domain/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@
/// A widget that a plugin registers for rendering in the TUI.
pub struct WidgetRegistration {
pub name: String,
pub render: std::sync::Arc<dyn Fn(&mut ratatui::Frame, &AppState, ratatui::prelude::Rect) + Send + Sync>,
pub render: std::sync::Arc<

Check failure on line 61 in crates/xtop-core/src/domain/plugin.rs

View workflow job for this annotation

GitHub Actions / Check

very complex type used. Consider factoring parts into `type` definitions
dyn Fn(&mut ratatui::Frame, &AppState, ratatui::prelude::Rect) + Send + Sync,
>,
}

impl Debug for WidgetRegistration {
Expand Down Expand Up @@ -111,7 +113,12 @@

/// Set alert thresholds for CPU, memory, and disk.
/// Requires `ModifyConfig` capability.
pub fn set_alert_thresholds(&mut self, cpu: f64, mem: f64, disk: f64) -> Result<(), PluginError> {
pub fn set_alert_thresholds(
&mut self,
cpu: f64,
mem: f64,
disk: f64,
) -> Result<(), PluginError> {
self.check_capability(&PluginCapability::ModifyConfig)?;
self.state.set_alert_thresholds(cpu, mem, disk);
Ok(())
Expand Down Expand Up @@ -182,11 +189,7 @@

/// Called when a key is pressed.
/// Return `Ok(true)` if the plugin consumed the key event.
fn on_key(
&mut self,
_ctx: &mut PluginContext,
_key: &str,
) -> Result<bool, PluginError> {
fn on_key(&mut self, _ctx: &mut PluginContext, _key: &str) -> Result<bool, PluginError> {
Ok(false)
}

Expand Down
3 changes: 1 addition & 2 deletions crates/xtop-core/src/domain/theme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,7 @@ impl<'de> Deserialize<'de> for Theme {
}

let name = name.ok_or_else(|| de::Error::missing_field("name"))?;
let palette_str =
palette.ok_or_else(|| de::Error::missing_field("palette"))?;
let palette_str = palette.ok_or_else(|| de::Error::missing_field("palette"))?;

let mut palette = [[0u8; 3]; 16];
for (i, hex) in palette_str.iter().enumerate() {
Expand Down
20 changes: 4 additions & 16 deletions crates/xtop-core/src/infrastructure/composite_provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,31 +55,19 @@ impl SystemDataProvider for CompositeProvider {
}

fn disk_io(&self) -> Vec<DiskIOInfo> {
self.first_non_empty(
|| self.primary.disk_io(),
|e| e.disk_io(),
)
self.first_non_empty(|| self.primary.disk_io(), |e| e.disk_io())
}

fn batteries(&self) -> Vec<BatteryInfo> {
self.first_non_empty(
|| self.primary.batteries(),
|e| e.batteries(),
)
self.first_non_empty(|| self.primary.batteries(), |e| e.batteries())
}

fn gpu_info(&self) -> Vec<GpuInfo> {
self.first_non_empty(
|| self.primary.gpu_info(),
|e| e.gpu_info(),
)
self.first_non_empty(|| self.primary.gpu_info(), |e| e.gpu_info())
}

fn docker_info(&self) -> Vec<DockerInfo> {
self.first_non_empty(
|| self.primary.docker_info(),
|e| e.docker_info(),
)
self.first_non_empty(|| self.primary.docker_info(), |e| e.docker_info())
}

fn system_info(&self) -> SystemInfo {
Expand Down
Loading
Loading