gardesk/gartop / b9657a9

Browse files

scaffold workspace with gartop, gartopctl, gartop-ipc crates

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
b9657a952fe83d3f16fc89bda1cc08ced71b76e3
Tree
7eb3cd2

14 changed files

StatusFile+-
A .gitignore 15 0
A Cargo.toml 49 0
A gartop-ipc/Cargo.toml 11 0
A gartop-ipc/src/lib.rs 189 0
A gartop/Cargo.toml 32 0
A gartop/src/collector/mod.rs 5 0
A gartop/src/config.rs 98 0
A gartop/src/daemon/mod.rs 10 0
A gartop/src/error.rs 27 0
A gartop/src/gui/mod.rs 10 0
A gartop/src/ipc/mod.rs 6 0
A gartop/src/main.rs 71 0
A gartopctl/Cargo.toml 19 0
A gartopctl/src/main.rs 121 0
.gitignoreadded
@@ -0,0 +1,15 @@
1
+# Build artifacts
2
+/target/
3
+Cargo.lock
4
+
5
+# IDE
6
+.idea/
7
+.vscode/
8
+*.swp
9
+*.swo
10
+
11
+# Documentation and planning
12
+/docs/
13
+
14
+# Claude
15
+.claude/
Cargo.tomladded
@@ -0,0 +1,49 @@
1
+[workspace]
2
+resolver = "2"
3
+members = ["gartop", "gartopctl", "gartop-ipc"]
4
+
5
+[workspace.package]
6
+version = "0.1.0"
7
+edition = "2024"
8
+authors = ["mfwolffe"]
9
+license = "MIT"
10
+
11
+[workspace.dependencies]
12
+# Shared IPC types
13
+gartop-ipc = { path = "gartop-ipc" }
14
+
15
+# gartk toolkit
16
+gartk-core = { path = "../gartk/gartk-core" }
17
+gartk-x11 = { path = "../gartk/gartk-x11" }
18
+gartk-render = { path = "../gartk/gartk-render" }
19
+
20
+# X11
21
+x11rb = { version = "0.13", features = ["allow-unsafe-code", "randr"] }
22
+
23
+# Cairo rendering
24
+cairo-rs = { version = "0.20", features = ["png"] }
25
+
26
+# System data
27
+procfs = "0.18"
28
+
29
+# Async runtime
30
+tokio = { version = "1", features = ["full", "signal"] }
31
+
32
+# Serialization
33
+serde = { version = "1", features = ["derive"] }
34
+serde_json = "1"
35
+toml = "0.8"
36
+
37
+# CLI
38
+clap = { version = "4", features = ["derive"] }
39
+
40
+# Logging/Errors
41
+tracing = "0.1"
42
+tracing-subscriber = { version = "0.3", features = ["env-filter"] }
43
+thiserror = "2"
44
+anyhow = "1"
45
+
46
+# Utilities
47
+dirs = "5"
48
+nix = { version = "0.29", features = ["signal"] }
49
+users = "0.11"
gartop-ipc/Cargo.tomladded
@@ -0,0 +1,11 @@
1
+[package]
2
+name = "gartop-ipc"
3
+version.workspace = true
4
+edition.workspace = true
5
+authors.workspace = true
6
+license.workspace = true
7
+description = "IPC protocol types for gartop system monitor"
8
+
9
+[dependencies]
10
+serde.workspace = true
11
+serde_json.workspace = true
gartop-ipc/src/lib.rsadded
@@ -0,0 +1,189 @@
1
+//! Shared IPC protocol types for gartop.
2
+//!
3
+//! This crate defines the request/response types used for communication
4
+//! between gartop daemon and clients (gartopctl, GUI, garbar).
5
+
6
+use serde::{Deserialize, Serialize};
7
+use std::path::PathBuf;
8
+
9
+/// IPC commands sent to the gartop daemon.
10
+#[derive(Debug, Clone, Serialize, Deserialize)]
11
+#[serde(tag = "command", rename_all = "snake_case")]
12
+pub enum Command {
13
+    /// Get daemon status.
14
+    Status,
15
+    /// Get current CPU stats.
16
+    GetCpu,
17
+    /// Get current memory stats.
18
+    GetMemory,
19
+    /// Get CPU history.
20
+    GetCpuHistory {
21
+        #[serde(default)]
22
+        count: Option<usize>,
23
+    },
24
+    /// Get memory history.
25
+    GetMemoryHistory {
26
+        #[serde(default)]
27
+        count: Option<usize>,
28
+    },
29
+    /// Get running processes.
30
+    GetProcesses {
31
+        #[serde(default)]
32
+        sort_by: Option<SortField>,
33
+        #[serde(default)]
34
+        limit: Option<usize>,
35
+    },
36
+    /// Kill a process by PID.
37
+    KillProcess {
38
+        pid: i32,
39
+        #[serde(default)]
40
+        signal: Option<i32>,
41
+    },
42
+    /// Subscribe to real-time updates.
43
+    Subscribe { events: Vec<String> },
44
+    /// Unsubscribe from events.
45
+    Unsubscribe { events: Vec<String> },
46
+    /// Reload configuration.
47
+    Reload,
48
+    /// Quit the daemon.
49
+    Quit,
50
+}
51
+
52
+/// Sort field for process list.
53
+#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
54
+#[serde(rename_all = "snake_case")]
55
+pub enum SortField {
56
+    #[default]
57
+    Cpu,
58
+    Memory,
59
+    Pid,
60
+    Name,
61
+}
62
+
63
+/// IPC response from gartop daemon.
64
+#[derive(Debug, Clone, Serialize, Deserialize)]
65
+pub struct Response {
66
+    pub success: bool,
67
+    #[serde(skip_serializing_if = "Option::is_none")]
68
+    pub data: Option<serde_json::Value>,
69
+    #[serde(skip_serializing_if = "Option::is_none")]
70
+    pub error: Option<String>,
71
+}
72
+
73
+impl Response {
74
+    /// Create a successful response with no data.
75
+    pub fn ok() -> Self {
76
+        Self {
77
+            success: true,
78
+            data: None,
79
+            error: None,
80
+        }
81
+    }
82
+
83
+    /// Create a successful response with data.
84
+    pub fn ok_with_data(data: impl Serialize) -> Self {
85
+        Self {
86
+            success: true,
87
+            data: serde_json::to_value(data).ok(),
88
+            error: None,
89
+        }
90
+    }
91
+
92
+    /// Create an error response.
93
+    pub fn err(message: impl Into<String>) -> Self {
94
+        Self {
95
+            success: false,
96
+            data: None,
97
+            error: Some(message.into()),
98
+        }
99
+    }
100
+}
101
+
102
+/// CPU usage statistics.
103
+#[derive(Debug, Clone, Serialize, Deserialize)]
104
+pub struct CpuStats {
105
+    /// Overall CPU usage percentage (0-100).
106
+    pub usage_percent: f64,
107
+    /// Per-core usage percentages.
108
+    pub per_core: Vec<f64>,
109
+    /// Number of cores.
110
+    pub core_count: usize,
111
+    /// Timestamp (milliseconds since epoch).
112
+    pub timestamp: u64,
113
+}
114
+
115
+/// Memory usage statistics.
116
+#[derive(Debug, Clone, Serialize, Deserialize)]
117
+pub struct MemoryStats {
118
+    /// Total memory in bytes.
119
+    pub total: u64,
120
+    /// Used memory in bytes (excluding cache/buffers).
121
+    pub used: u64,
122
+    /// Free memory in bytes.
123
+    pub free: u64,
124
+    /// Available memory in bytes.
125
+    pub available: u64,
126
+    /// Swap total in bytes.
127
+    pub swap_total: u64,
128
+    /// Swap used in bytes.
129
+    pub swap_used: u64,
130
+    /// Usage percentage (0-100).
131
+    pub usage_percent: f64,
132
+    /// Timestamp (milliseconds since epoch).
133
+    pub timestamp: u64,
134
+}
135
+
136
+/// Process information.
137
+#[derive(Debug, Clone, Serialize, Deserialize)]
138
+pub struct ProcessInfo {
139
+    /// Process ID.
140
+    pub pid: i32,
141
+    /// Process name.
142
+    pub name: String,
143
+    /// Command line.
144
+    pub cmdline: String,
145
+    /// CPU usage percentage.
146
+    pub cpu_percent: f64,
147
+    /// Memory usage percentage.
148
+    pub memory_percent: f64,
149
+    /// Resident set size in bytes.
150
+    pub rss: u64,
151
+    /// Virtual memory size in bytes.
152
+    pub vsize: u64,
153
+    /// Process state.
154
+    pub state: String,
155
+    /// User owning the process.
156
+    pub user: String,
157
+}
158
+
159
+/// Events broadcast to subscribers.
160
+#[derive(Debug, Clone, Serialize, Deserialize)]
161
+#[serde(tag = "event", rename_all = "snake_case")]
162
+pub enum Event {
163
+    /// CPU stats updated.
164
+    CpuUpdate(CpuStats),
165
+    /// Memory stats updated.
166
+    MemoryUpdate(MemoryStats),
167
+    /// Process list updated.
168
+    ProcessUpdate { processes: Vec<ProcessInfo> },
169
+}
170
+
171
+/// Daemon status information.
172
+#[derive(Debug, Clone, Serialize, Deserialize)]
173
+pub struct StatusInfo {
174
+    /// Daemon version.
175
+    pub version: String,
176
+    /// Uptime in seconds.
177
+    pub uptime_secs: u64,
178
+    /// Sample interval in milliseconds.
179
+    pub sample_interval_ms: u64,
180
+    /// History buffer size.
181
+    pub history_size: usize,
182
+}
183
+
184
+/// Get the default socket path for gartop IPC.
185
+pub fn socket_path() -> PathBuf {
186
+    let runtime_dir =
187
+        std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/tmp".to_string());
188
+    PathBuf::from(runtime_dir).join("gartop.sock")
189
+}
gartop/Cargo.tomladded
@@ -0,0 +1,32 @@
1
+[package]
2
+name = "gartop"
3
+version.workspace = true
4
+edition.workspace = true
5
+authors.workspace = true
6
+license.workspace = true
7
+description = "System monitor daemon for gar desktop"
8
+
9
+[[bin]]
10
+name = "gartop"
11
+path = "src/main.rs"
12
+
13
+[dependencies]
14
+gartop-ipc.workspace = true
15
+gartk-core.workspace = true
16
+gartk-x11.workspace = true
17
+gartk-render.workspace = true
18
+x11rb.workspace = true
19
+cairo-rs.workspace = true
20
+procfs.workspace = true
21
+tokio.workspace = true
22
+serde.workspace = true
23
+serde_json.workspace = true
24
+toml.workspace = true
25
+clap.workspace = true
26
+tracing.workspace = true
27
+tracing-subscriber.workspace = true
28
+thiserror.workspace = true
29
+anyhow.workspace = true
30
+dirs.workspace = true
31
+nix.workspace = true
32
+users.workspace = true
gartop/src/collector/mod.rsadded
@@ -0,0 +1,5 @@
1
+//! System data collectors
2
+//!
3
+//! Collects CPU, memory, and process data from procfs.
4
+
5
+// TODO: Sprint 2 - implement collectors
gartop/src/config.rsadded
@@ -0,0 +1,98 @@
1
+//! Configuration for gartop
2
+
3
+use anyhow::Result;
4
+use serde::{Deserialize, Serialize};
5
+use std::path::PathBuf;
6
+
7
+/// Main configuration.
8
+#[derive(Debug, Clone, Serialize, Deserialize)]
9
+#[serde(default)]
10
+pub struct Config {
11
+    pub daemon: DaemonConfig,
12
+    pub gui: GuiConfig,
13
+}
14
+
15
+/// Daemon configuration.
16
+#[derive(Debug, Clone, Serialize, Deserialize)]
17
+#[serde(default)]
18
+pub struct DaemonConfig {
19
+    /// Sample interval in milliseconds.
20
+    pub sample_interval_ms: u64,
21
+    /// History buffer size (number of samples to keep).
22
+    pub history_size: usize,
23
+    /// Maximum number of processes to track.
24
+    pub max_processes: usize,
25
+}
26
+
27
+/// GUI configuration.
28
+#[derive(Debug, Clone, Serialize, Deserialize)]
29
+#[serde(default)]
30
+pub struct GuiConfig {
31
+    /// Initial window width.
32
+    pub width: u32,
33
+    /// Initial window height.
34
+    pub height: u32,
35
+    /// Refresh rate in FPS.
36
+    pub refresh_rate: u32,
37
+    /// Show legend on graphs.
38
+    pub show_legend: bool,
39
+    /// Font family for UI.
40
+    pub font_family: String,
41
+    /// Font size.
42
+    pub font_size: f64,
43
+}
44
+
45
+impl Default for Config {
46
+    fn default() -> Self {
47
+        Self {
48
+            daemon: DaemonConfig::default(),
49
+            gui: GuiConfig::default(),
50
+        }
51
+    }
52
+}
53
+
54
+impl Default for DaemonConfig {
55
+    fn default() -> Self {
56
+        Self {
57
+            sample_interval_ms: 1000,
58
+            history_size: 300,
59
+            max_processes: 100,
60
+        }
61
+    }
62
+}
63
+
64
+impl Default for GuiConfig {
65
+    fn default() -> Self {
66
+        Self {
67
+            width: 600,
68
+            height: 500,
69
+            refresh_rate: 30,
70
+            show_legend: true,
71
+            font_family: "sans-serif".to_string(),
72
+            font_size: 12.0,
73
+        }
74
+    }
75
+}
76
+
77
+impl Config {
78
+    /// Load configuration from file.
79
+    pub fn load(path: Option<&str>) -> Result<Self> {
80
+        let config_path = path.map(PathBuf::from).or_else(Self::default_path);
81
+
82
+        if let Some(path) = config_path {
83
+            if path.exists() {
84
+                let content = std::fs::read_to_string(&path)?;
85
+                let config: Config = toml::from_str(&content)?;
86
+                tracing::info!("Loaded config from {}", path.display());
87
+                return Ok(config);
88
+            }
89
+        }
90
+
91
+        Ok(Self::default())
92
+    }
93
+
94
+    /// Get default configuration path.
95
+    fn default_path() -> Option<PathBuf> {
96
+        dirs::config_dir().map(|d| d.join("gartop").join("config.toml"))
97
+    }
98
+}
gartop/src/daemon/mod.rsadded
@@ -0,0 +1,10 @@
1
+//! Daemon mode implementation
2
+
3
+use anyhow::Result;
4
+
5
+/// Run the gartop daemon.
6
+pub async fn run(_config_path: Option<String>, _foreground: bool) -> Result<()> {
7
+    // TODO: Sprint 2/3 - implement daemon
8
+    tracing::info!("Daemon not yet implemented");
9
+    Ok(())
10
+}
gartop/src/error.rsadded
@@ -0,0 +1,27 @@
1
+//! Error types for gartop
2
+
3
+use thiserror::Error;
4
+
5
+/// Library errors
6
+#[derive(Debug, Error)]
7
+pub enum Error {
8
+    #[error("procfs error: {0}")]
9
+    Procfs(#[from] procfs::ProcError),
10
+
11
+    #[error("IO error: {0}")]
12
+    Io(#[from] std::io::Error),
13
+
14
+    #[error("IPC error: {0}")]
15
+    Ipc(String),
16
+
17
+    #[error("process not found: {0}")]
18
+    ProcessNotFound(i32),
19
+
20
+    #[error("permission denied: {0}")]
21
+    PermissionDenied(String),
22
+
23
+    #[error("config error: {0}")]
24
+    Config(String),
25
+}
26
+
27
+pub type Result<T> = std::result::Result<T, Error>;
gartop/src/gui/mod.rsadded
@@ -0,0 +1,10 @@
1
+//! GUI implementation using gartk
2
+
3
+use anyhow::Result;
4
+
5
+/// Run the gartop GUI.
6
+pub async fn run() -> Result<()> {
7
+    // TODO: Sprint 4 - implement GUI
8
+    tracing::info!("GUI not yet implemented");
9
+    Ok(())
10
+}
gartop/src/ipc/mod.rsadded
@@ -0,0 +1,6 @@
1
+//! IPC server and client handling
2
+
3
+// Re-export from gartop-ipc
4
+pub use gartop_ipc::{Command, Event, Response};
5
+
6
+// TODO: Sprint 3 - implement server and client
gartop/src/main.rsadded
@@ -0,0 +1,71 @@
1
+//! gartop - System monitor for gar desktop
2
+//!
3
+//! A daemon-based system monitor with real-time CPU, memory, and process
4
+//! monitoring. Uses gartk for GUI rendering.
5
+
6
+use anyhow::Result;
7
+use clap::{Parser, Subcommand};
8
+use tracing_subscriber::EnvFilter;
9
+
10
+mod collector;
11
+mod config;
12
+mod daemon;
13
+mod error;
14
+mod gui;
15
+mod ipc;
16
+
17
+/// gartop - System monitor for gar desktop
18
+#[derive(Parser)]
19
+#[command(name = "gartop")]
20
+#[command(about = "System monitor for gar desktop", long_about = None)]
21
+struct Cli {
22
+    #[command(subcommand)]
23
+    command: Option<Commands>,
24
+
25
+    /// Run in foreground (don't daemonize)
26
+    #[arg(short, long)]
27
+    foreground: bool,
28
+
29
+    /// Configuration file path
30
+    #[arg(short, long)]
31
+    config: Option<String>,
32
+}
33
+
34
+#[derive(Subcommand)]
35
+enum Commands {
36
+    /// Start the daemon (default)
37
+    Daemon {
38
+        /// Run in foreground
39
+        #[arg(short, long)]
40
+        foreground: bool,
41
+    },
42
+    /// Open the GUI window (connects to daemon)
43
+    Gui,
44
+}
45
+
46
+#[tokio::main]
47
+async fn main() -> Result<()> {
48
+    tracing_subscriber::fmt()
49
+        .with_env_filter(
50
+            EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")),
51
+        )
52
+        .init();
53
+
54
+    let cli = Cli::parse();
55
+
56
+    match cli.command {
57
+        Some(Commands::Daemon { foreground }) => {
58
+            tracing::info!("Starting gartop daemon");
59
+            daemon::run(cli.config, foreground || cli.foreground).await
60
+        }
61
+        Some(Commands::Gui) => {
62
+            tracing::info!("Starting gartop GUI");
63
+            gui::run().await
64
+        }
65
+        None => {
66
+            // Default: start daemon
67
+            tracing::info!("Starting gartop daemon");
68
+            daemon::run(cli.config, cli.foreground).await
69
+        }
70
+    }
71
+}
gartopctl/Cargo.tomladded
@@ -0,0 +1,19 @@
1
+[package]
2
+name = "gartopctl"
3
+version.workspace = true
4
+edition.workspace = true
5
+authors.workspace = true
6
+license.workspace = true
7
+description = "Control CLI for gartop daemon"
8
+
9
+[[bin]]
10
+name = "gartopctl"
11
+path = "src/main.rs"
12
+
13
+[dependencies]
14
+gartop-ipc.workspace = true
15
+serde.workspace = true
16
+serde_json.workspace = true
17
+clap.workspace = true
18
+anyhow.workspace = true
19
+dirs.workspace = true
gartopctl/src/main.rsadded
@@ -0,0 +1,121 @@
1
+//! gartopctl - Control CLI for gartop daemon
2
+
3
+use anyhow::{Context, Result};
4
+use clap::{Parser, Subcommand};
5
+use gartop_ipc::{Command, Response, SortField};
6
+use std::io::{BufRead, BufReader, Write};
7
+use std::os::unix::net::UnixStream;
8
+
9
+#[derive(Parser)]
10
+#[command(name = "gartopctl")]
11
+#[command(about = "Control the gartop daemon", long_about = None)]
12
+struct Cli {
13
+    #[command(subcommand)]
14
+    command: Commands,
15
+}
16
+
17
+#[derive(Subcommand)]
18
+enum Commands {
19
+    /// Get daemon status
20
+    Status,
21
+    /// Get current CPU stats
22
+    Cpu,
23
+    /// Get current memory stats
24
+    Memory,
25
+    /// Get CPU usage history
26
+    CpuHistory {
27
+        /// Number of samples
28
+        #[arg(short, long)]
29
+        count: Option<usize>,
30
+    },
31
+    /// Get memory usage history
32
+    MemoryHistory {
33
+        /// Number of samples
34
+        #[arg(short, long)]
35
+        count: Option<usize>,
36
+    },
37
+    /// List running processes
38
+    Processes {
39
+        /// Sort by field (cpu, memory, pid, name)
40
+        #[arg(short, long, default_value = "cpu")]
41
+        sort: String,
42
+        /// Limit results
43
+        #[arg(short, long)]
44
+        limit: Option<usize>,
45
+    },
46
+    /// Kill a process
47
+    Kill {
48
+        /// Process ID
49
+        pid: i32,
50
+        /// Signal number (default: 15 SIGTERM)
51
+        #[arg(short, long)]
52
+        signal: Option<i32>,
53
+    },
54
+    /// Reload configuration
55
+    Reload,
56
+    /// Stop the daemon
57
+    Quit,
58
+}
59
+
60
+fn main() -> Result<()> {
61
+    let cli = Cli::parse();
62
+
63
+    let cmd = match cli.command {
64
+        Commands::Status => Command::Status,
65
+        Commands::Cpu => Command::GetCpu,
66
+        Commands::Memory => Command::GetMemory,
67
+        Commands::CpuHistory { count } => Command::GetCpuHistory { count },
68
+        Commands::MemoryHistory { count } => Command::GetMemoryHistory { count },
69
+        Commands::Processes { sort, limit } => {
70
+            let sort_by = match sort.as_str() {
71
+                "memory" | "mem" => Some(SortField::Memory),
72
+                "pid" => Some(SortField::Pid),
73
+                "name" => Some(SortField::Name),
74
+                _ => Some(SortField::Cpu),
75
+            };
76
+            Command::GetProcesses { sort_by, limit }
77
+        }
78
+        Commands::Kill { pid, signal } => Command::KillProcess { pid, signal },
79
+        Commands::Reload => Command::Reload,
80
+        Commands::Quit => Command::Quit,
81
+    };
82
+
83
+    let response = send_command(&cmd)?;
84
+
85
+    if response.success {
86
+        if let Some(data) = response.data {
87
+            println!("{}", serde_json::to_string_pretty(&data)?);
88
+        } else {
89
+            println!("OK");
90
+        }
91
+    } else {
92
+        eprintln!("Error: {}", response.error.unwrap_or_default());
93
+        std::process::exit(1);
94
+    }
95
+
96
+    Ok(())
97
+}
98
+
99
+fn send_command(cmd: &Command) -> Result<Response> {
100
+    let socket_path = gartop_ipc::socket_path();
101
+
102
+    if !socket_path.exists() {
103
+        anyhow::bail!("gartop daemon not running (socket not found at {})", socket_path.display());
104
+    }
105
+
106
+    let mut stream = UnixStream::connect(&socket_path)
107
+        .with_context(|| "Failed to connect to gartop daemon")?;
108
+
109
+    // Send command as JSON line
110
+    let json = serde_json::to_string(cmd)?;
111
+    writeln!(stream, "{}", json)?;
112
+    stream.flush()?;
113
+
114
+    // Read response
115
+    let mut reader = BufReader::new(stream);
116
+    let mut line = String::new();
117
+    reader.read_line(&mut line)?;
118
+
119
+    let response: Response = serde_json::from_str(&line)?;
120
+    Ok(response)
121
+}