Files
owlry/src/providers/uuctl.rs
vikingowl 7ca8a1f443 feat: add tags, configurable tabs, and tag-based filtering
- Add `tags` field to LaunchItem for categorization
- Extract .desktop Categories as tags for applications
- Add semantic tags to providers (systemd, ssh, script, etc.)
- Display tag badges in result rows (max 3 tags)
- Add `tabs` config option for customizable header tabs
- Dynamic Ctrl+1-9 shortcuts based on tab config
- Add `:tag:XXX` prefix for tag-based filtering
- Include tags in fuzzy search with lower weight
- Update config.example.toml with tabs documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 17:30:47 +01:00

277 lines
9.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use super::{LaunchItem, Provider, ProviderType};
use log::{debug, warn};
use std::process::Command;
/// Provider for systemd user services
/// Uses systemctl --user to list and control user-level services
pub struct UuctlProvider {
items: Vec<LaunchItem>,
}
/// Represents the state of a systemd service
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct ServiceState {
pub unit_name: String,
pub display_name: String,
pub description: String,
pub active: bool,
pub sub_state: String,
}
impl UuctlProvider {
pub fn new() -> Self {
Self { items: Vec::new() }
}
fn systemctl_available() -> bool {
Command::new("systemctl")
.arg("--user")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
/// Generate submenu actions for a given service
pub fn actions_for_service(unit_name: &str, display_name: &str, is_active: bool) -> Vec<LaunchItem> {
let mut actions = Vec::new();
if is_active {
actions.push(LaunchItem {
id: format!("systemd:restart:{}", unit_name),
name: "↻ Restart".to_string(),
description: Some(format!("Restart {}", display_name)),
icon: Some("view-refresh".to_string()),
provider: ProviderType::Uuctl,
command: format!("systemctl --user restart {}", unit_name),
terminal: false,
tags: vec!["systemd".to_string(), "service".to_string()],
});
actions.push(LaunchItem {
id: format!("systemd:stop:{}", unit_name),
name: "■ Stop".to_string(),
description: Some(format!("Stop {}", display_name)),
icon: Some("process-stop".to_string()),
provider: ProviderType::Uuctl,
command: format!("systemctl --user stop {}", unit_name),
terminal: false,
tags: vec!["systemd".to_string(), "service".to_string()],
});
actions.push(LaunchItem {
id: format!("systemd:reload:{}", unit_name),
name: "⟳ Reload".to_string(),
description: Some(format!("Reload {} configuration", display_name)),
icon: Some("view-refresh".to_string()),
provider: ProviderType::Uuctl,
command: format!("systemctl --user reload {}", unit_name),
terminal: false,
tags: vec!["systemd".to_string(), "service".to_string()],
});
actions.push(LaunchItem {
id: format!("systemd:kill:{}", unit_name),
name: "✗ Kill".to_string(),
description: Some(format!("Force kill {}", display_name)),
icon: Some("edit-delete".to_string()),
provider: ProviderType::Uuctl,
command: format!("systemctl --user kill {}", unit_name),
terminal: false,
tags: vec!["systemd".to_string(), "service".to_string()],
});
} else {
actions.push(LaunchItem {
id: format!("systemd:start:{}", unit_name),
name: "▶ Start".to_string(),
description: Some(format!("Start {}", display_name)),
icon: Some("media-playback-start".to_string()),
provider: ProviderType::Uuctl,
command: format!("systemctl --user start {}", unit_name),
terminal: false,
tags: vec!["systemd".to_string(), "service".to_string()],
});
}
// Always available actions
actions.push(LaunchItem {
id: format!("systemd:status:{}", unit_name),
name: " Status".to_string(),
description: Some(format!("Show {} status", display_name)),
icon: Some("dialog-information".to_string()),
provider: ProviderType::Uuctl,
command: format!("systemctl --user status {}", unit_name),
terminal: true,
tags: vec!["systemd".to_string(), "service".to_string()],
});
actions.push(LaunchItem {
id: format!("systemd:journal:{}", unit_name),
name: "📋 Journal".to_string(),
description: Some(format!("Show {} logs", display_name)),
icon: Some("utilities-system-monitor".to_string()),
provider: ProviderType::Uuctl,
command: format!("journalctl --user -u {} -f", unit_name),
terminal: true,
tags: vec!["systemd".to_string(), "service".to_string()],
});
actions.push(LaunchItem {
id: format!("systemd:enable:{}", unit_name),
name: "⊕ Enable".to_string(),
description: Some(format!("Enable {} on startup", display_name)),
icon: Some("emblem-default".to_string()),
provider: ProviderType::Uuctl,
command: format!("systemctl --user enable {}", unit_name),
terminal: false,
tags: vec!["systemd".to_string(), "service".to_string()],
});
actions.push(LaunchItem {
id: format!("systemd:disable:{}", unit_name),
name: "⊖ Disable".to_string(),
description: Some(format!("Disable {} on startup", display_name)),
icon: Some("emblem-unreadable".to_string()),
provider: ProviderType::Uuctl,
command: format!("systemctl --user disable {}", unit_name),
terminal: false,
tags: vec!["systemd".to_string(), "service".to_string()],
});
actions
}
fn parse_systemctl_output(output: &str) -> Vec<LaunchItem> {
let mut items = Vec::new();
for line in output.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
// Parse systemctl output - handle variable whitespace
// Format: UNIT LOAD ACTIVE SUB DESCRIPTION...
let mut parts = line.split_whitespace();
let unit_name = match parts.next() {
Some(u) => u,
None => continue,
};
// Skip if not a proper service name
if !unit_name.ends_with(".service") {
continue;
}
let _load_state = parts.next().unwrap_or("");
let active_state = parts.next().unwrap_or("");
let sub_state = parts.next().unwrap_or("");
let description: String = parts.collect::<Vec<_>>().join(" ");
// Create a clean display name
let display_name = unit_name
.trim_end_matches(".service")
.replace("app-", "")
.replace("@autostart", "")
.replace("\\x2d", "-");
let is_active = active_state == "active";
let status_icon = if is_active { "" } else { "" };
let status_desc = if description.is_empty() {
format!("{} {} ({})", status_icon, sub_state, active_state)
} else {
format!("{} {} ({})", status_icon, description, sub_state)
};
// Store service info in the command field as encoded data
// Format: SUBMENU:unit_name:is_active
let submenu_data = format!("SUBMENU:{}:{}", unit_name, is_active);
items.push(LaunchItem {
id: format!("systemd:service:{}", unit_name),
name: display_name,
description: Some(status_desc),
icon: Some(if is_active { "emblem-ok-symbolic" } else { "emblem-pause-symbolic" }.to_string()),
provider: ProviderType::Uuctl,
command: submenu_data, // Special marker for submenu
terminal: false,
tags: vec!["systemd".to_string(), "service".to_string()],
});
}
items
}
/// Check if an item is a submenu trigger (service, not action)
pub fn is_submenu_item(item: &LaunchItem) -> bool {
item.provider == ProviderType::Uuctl && item.command.starts_with("SUBMENU:")
}
/// Parse submenu data from item command
pub fn parse_submenu_data(item: &LaunchItem) -> Option<(String, String, bool)> {
if !Self::is_submenu_item(item) {
return None;
}
let parts: Vec<&str> = item.command.splitn(3, ':').collect();
if parts.len() >= 3 {
let unit_name = parts[1].to_string();
let is_active = parts[2] == "true";
Some((unit_name, item.name.clone(), is_active))
} else {
None
}
}
}
impl Provider for UuctlProvider {
fn name(&self) -> &str {
"systemd-user"
}
fn provider_type(&self) -> ProviderType {
ProviderType::Uuctl
}
fn refresh(&mut self) {
self.items.clear();
if !Self::systemctl_available() {
debug!("systemctl --user not available, skipping");
return;
}
// List all user services (both running and available)
let output = match Command::new("systemctl")
.args(["--user", "list-units", "--type=service", "--all", "--no-legend", "--no-pager"])
.output()
{
Ok(o) => o,
Err(e) => {
warn!("Failed to run systemctl --user: {}", e);
return;
}
};
if !output.status.success() {
warn!("systemctl --user failed with status: {}", output.status);
return;
}
let stdout = String::from_utf8_lossy(&output.stdout);
self.items = Self::parse_systemctl_output(&stdout);
// Sort by name
self.items.sort_by(|a, b| a.name.cmp(&b.name));
debug!("Found {} systemd user services", self.items.len());
}
fn items(&self) -> &[LaunchItem] {
&self.items
}
}