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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ iced = { version = "0.14.0", default-features = false, features = [
iced_aw = "0.14.1"
strum = { version = "0.28", features = ["derive"] }
ksni = "0.3.6"
chrono = "0.4.45"

[profile.dev]
overflow-checks = true
Expand Down
1 change: 1 addition & 0 deletions crates/cardwire-gui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ serde_json.workspace = true
ksni.workspace = true
toml.workspace = true
xdg.workspace = true
chrono.workspace = true

[[bin]]
name = "cardwire-gui"
Expand Down
16 changes: 14 additions & 2 deletions crates/cardwire-gui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use log::error;
use std::collections::BTreeMap;

use crate::{
gui_config::{GuiConfig, PrimaryClickAction}, helpers::{CardwireDbus, GpuDevice}, message::Message, models::{DaemonSettings, MainState, Mode, Page, PciDevice, SettingState}, tray::{self, TrayAction, TrayHandle}, ui::{self, daemon_setting_page, error_bar, info_bar, pci_page}
gui_config::{GuiConfig, PrimaryClickAction}, helpers::{CardwireDbus, GpuDevice}, message::Message, models::{DaemonSettings, LogState, MainState, Mode, Page, PciDevice, SettingState}, tray::{self, TrayAction, TrayHandle}, ui::{self, daemon_setting_page, error_bar, info_bar, pci_page}
};

#[derive(Debug)]
Expand All @@ -18,6 +18,7 @@ pub struct AppState {
pub pci_list: BTreeMap<String, PciDevice>,
pub main_state: MainState,
pub setting_state: SettingState,
pub log_state: LogState,
window_id: Option<window::Id>,
tray_handle: Option<TrayHandle>,
tray_available: bool,
Expand Down Expand Up @@ -50,6 +51,7 @@ impl AppState {
gui_config,
..SettingState::default()
},
log_state: LogState::default(),
window_id,
tray_handle: None,
tray_available: true,
Expand Down Expand Up @@ -361,6 +363,16 @@ impl AppState {
Ok(()) => self.info = Some("GPU list refreshed".to_string()),
Err(err) => self.error = Some(format!("Refresh error: {}", err)),
},
// Fetch the initial blocked process logs from dbus
Message::FetchedLogs(res) => match res {
Ok(logs) => {
self.log_state.replace(logs);
self.error = None;
}
Err(err) => self.error = Some(format!("Error fetching logs: {}", err)),
},
// Append a new blocked process log received from dbus
Message::NewLog(log) => self.log_state.push(log),
Message::OpenUrl(url) => {
let _ = std::process::Command::new("xdg-open").arg(url).spawn();
}
Expand Down Expand Up @@ -429,7 +441,7 @@ impl AppState {
Page::Pci => pci_page(&self.pci_list),
Page::SmartMode => text("Smart Mode TODO").into(),
Page::CardwireSettings => daemon_setting_page(&self.setting_state),
Page::AccessLogs => text!("TODO").into(),
Page::Logs => ui::logs_page(&self.log_state, &self.gpu_list),
Page::Advanced => ui::advanced_page(),
Page::About => ui::about_page(),
}));
Expand Down
6 changes: 4 additions & 2 deletions crates/cardwire-gui/src/message.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::{
gui_config::GuiConfig, helpers::GpuDevice, models::{DaemonSettings, LsofData, Mode, Page, PciDevice}, tray::{TrayAction, TrayHandle}
gui_config::GuiConfig, helpers::GpuDevice, models::{DaemonSettings, LogEntry, LsofData, Mode, Page, PciDevice}, tray::{TrayAction, TrayHandle}
};
use std::collections::BTreeMap;
use std::collections::{BTreeMap, VecDeque};

#[derive(Debug, Clone)]
pub enum Message {
Expand Down Expand Up @@ -32,6 +32,8 @@ pub enum Message {
CloseLsofWindow,
RefreshGpu,
RefreshGpuResult(Result<(), String>),
FetchedLogs(Result<VecDeque<LogEntry>, String>),
NewLog(LogEntry),
OpenUrl(String),
ClearError,
ClearInfo,
Expand Down
40 changes: 37 additions & 3 deletions crates/cardwire-gui/src/models.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::{
collections::HashMap, fmt::{self, Display}
collections::{HashMap, VecDeque}, fmt::{self, Display}, time::SystemTime
};
use strum::{EnumIter, FromRepr, VariantArray};

Expand Down Expand Up @@ -50,7 +50,7 @@ pub enum Page {
Main,
Pci,
SmartMode,
AccessLogs,
Logs,
CardwireSettings,
Advanced,
About,
Expand All @@ -62,7 +62,7 @@ impl Display for Page {
Page::Pci => write!(f, "PCI"),
Page::SmartMode => write!(f, "Smart Mode"),
Page::CardwireSettings => write!(f, "Cardwire Settings"),
Page::AccessLogs => write!(f, "Access Logs"),
Page::Logs => write!(f, "Logs"),
Page::Advanced => write!(f, "Advanced"),
Page::About => write!(f, "About"),
}
Expand All @@ -76,6 +76,40 @@ pub struct MainState {
pub lsof_window: Option<LsofData>,
}

#[derive(serde::Deserialize, zbus::zvariant::Type, Debug, Clone)]
pub struct LogEntry {
pub timestamp: SystemTime,
pub pid: u32,
pub comm: String,
pub gpu_id: u32,
pub wayland_app_id: String,
}

// Maximum number of log entries kept in the GUI
const MAX_GUI_LOG_ENTRIES: usize = 500;

#[derive(Default, Clone, Debug)]
pub struct LogState {
pub logs: VecDeque<LogEntry>,
}

impl LogState {
pub fn replace(&mut self, logs: VecDeque<LogEntry>) {
let mut logs = logs;
while logs.len() > MAX_GUI_LOG_ENTRIES {
logs.pop_front();
}
self.logs = logs;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

pub fn push(&mut self, log: LogEntry) {
self.logs.push_back(log);
while self.logs.len() > MAX_GUI_LOG_ENTRIES {
self.logs.pop_front();
}
}
}

#[derive(Clone, Debug)]
pub struct LsofData {
pub gpu_id: usize,
Expand Down
78 changes: 75 additions & 3 deletions crates/cardwire-gui/src/subscription.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::collections::{BTreeMap, HashMap};
use std::collections::{BTreeMap, HashMap, VecDeque};

use iced::{
Subscription, futures::{SinkExt, StreamExt, channel::mpsc::Sender}, stream
Expand All @@ -9,7 +9,7 @@ use tokio::select;
use tokio_stream::StreamMap;

use crate::{
helpers::CardwireDbus, message::Message, models::{DaemonSettings, Mode, PciDevice}, tray
helpers::CardwireDbus, message::Message, models::{DaemonSettings, LogEntry, Mode, PciDevice}, tray
};
use zbus::{
Connection, Proxy, names::OwnedInterfaceName, proxy, zvariant::{OwnedObjectPath, OwnedValue}
Expand Down Expand Up @@ -525,6 +525,78 @@ fn pci_sub() -> Subscription<Message> {
})
}

// CardwireLogger is used to listen to log signals

#[proxy(
default_service = "org.opengamingcollective.cardwire",
default_path = "/org/opengamingcollective/cardwire",
interface = "org.opengamingcollective.cardwire.Logger"
)]
// org.freedesktop.DBus.Properties
trait CardwireLogger {
fn process_blocked(&self) -> zbus::Result<VecDeque<LogEntry>>;
#[zbus(signal)]
fn process_blocked_changed(&self, log: LogEntry) -> zbus::Result<()>;
}

fn logger_sub() -> Subscription<Message> {
Subscription::run_with("cardwire_logger_subscription", |_id| {
stream::channel(100, |mut output: Sender<Message>| async move {
let connection = match Connection::system().await {
Ok(conn) => conn,
Err(e) => {
warn!("Failed to connect to D-Bus: {}", e);
let _ = output.send(Message::FetchedLogs(Err(e.to_string()))).await;
return;
}
};

let proxy = match CardwireLoggerProxy::new(&connection).await {
Ok(p) => p,
Err(e) => {
warn!("Failed to create D-Bus proxy: {}", e);
let _ = output.send(Message::FetchedLogs(Err(e.to_string()))).await;
return;
}
};
// for startup, get current blocked apps logs
match proxy.process_blocked().await {
Ok(initial_logs) => {
if !initial_logs.is_empty() {
let _ = output.send(Message::FetchedLogs(Ok(initial_logs))).await;
}
}
Err(error) => {
let _ = output
.send(Message::FetchedLogs(Err(error.to_string())))
.await;
}
}
let mut logs_stream = match proxy.receive_process_blocked_changed().await {
Comment thread
luytan marked this conversation as resolved.
Ok(stream) => stream,
Err(err) => {
warn!("Failed to subscribe to D-Bus logs signal: {}", err);
let _ = output
.send(Message::FetchedLogs(Err(err.to_string())))
.await;
return;
}
};
while let Some(log_signal) = logs_stream.next().await {
if let Ok(log_arg) = log_signal.args() {
let log: LogEntry = log_arg.log().clone();
let _ = output.send(Message::NewLog(log)).await;
}
}
let _ = output
.send(Message::FetchedLogs(Err(
"Cardwire daemon disconnected".to_string()
)))
.await;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
})
})
}

pub fn dbus_sub() -> Subscription<Message> {
Subscription::batch([config_sub(), mode_sub(), gpu_sub(), pci_sub()])
Subscription::batch([config_sub(), mode_sub(), gpu_sub(), pci_sub(), logger_sub()])
}
101 changes: 95 additions & 6 deletions crates/cardwire-gui/src/ui.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use chrono::{DateTime, Local};
use iced::{
Alignment, Border, Color, Element, Font, Length::{Fill, FillPortion, Fixed}, widget::{
button, column, container, pick_list, row, scrollable, space::horizontal, text, toggler
Expand All @@ -8,7 +9,7 @@ use std::collections::BTreeMap;
use strum::{IntoEnumIterator, VariantArray};

use crate::{
gui_config::{GuiConfig, PrimaryClickAction}, helpers::GpuDevice, message::Message, models::{LsofData, MainState, Mode, Page, PciDevice, SettingState}
gui_config::{GuiConfig, PrimaryClickAction}, helpers::GpuDevice, message::Message, models::{LogEntry, LogState, LsofData, MainState, Mode, Page, PciDevice, SettingState}
};

// Custom macro for box theming, used by cards
Expand Down Expand Up @@ -140,7 +141,7 @@ pub fn main_page<'a>(
) -> Element<'a, Message> {
column![
mode_element(main_state.current_mode),
gpu_cards(gpu_list, main_state.open_gpu_menu)
gpu_cards(gpu_list, main_state.open_gpu_menu, main_state.current_mode)
]
.spacing(20)
.into()
Expand All @@ -160,6 +161,7 @@ fn mode_element(current_mode: Option<Mode>) -> Element<'static, Message> {
fn gpu_cards(
gpu_list: &BTreeMap<usize, GpuDevice>,
open_dropdown: Option<usize>,
current_mode: Option<Mode>,
) -> Element<'_, Message> {
let cards = gpu_list
.iter()
Expand All @@ -180,14 +182,13 @@ fn gpu_cards(
let gpu_id = *id;
let is_blocked = gpu.blocked;

let is_offload_dgpu = !gpu.default && gpu.discrete;

let is_available = gpu.available;

// Build dropdown menu items
let mut dropdown_col = column![];
// Block/Unblock (only for offload dGPU)
if is_offload_dgpu && is_available {
// Block/Unblock (only in manual mode and if not default)
if current_mode.is_some_and(|mode| mode == Mode::Manual) && !gpu.default && is_available
{
if is_blocked {
dropdown_col = dropdown_col.push(
button("Unblock")
Expand Down Expand Up @@ -437,6 +438,94 @@ pub fn lsof_overlay<'a>(
.into()
}

// A dark terminal-like page showing the blocked process logs
pub fn logs_page<'a>(
log_state: &'a LogState,
gpu_list: &'a BTreeMap<usize, GpuDevice>,
) -> Element<'a, Message> {
let header = row![
text("Blocked Process Logs")
.size(20)
.color(Color::from_rgb(0.9, 0.9, 0.9)),
horizontal(),
text!("{} entries", log_state.logs.len()).color(Color::from_rgb(0.6, 0.6, 0.6)),
]
.align_y(Alignment::Center);

let content = if log_state.logs.is_empty() {
column![
text("No blocked process logs yet")
.color(Color::from_rgb(0.5, 0.5, 0.5))
.font(Font::MONOSPACE)
]
} else {
log_state
.logs
.iter()
.fold(column![].spacing(4), |col, log| {
col.push(log_line(log, gpu_list))
})
};

let terminal = container(scrollable(content))
.width(Fill)
.height(Fill)
.padding(16)
.style(|_| container::Style {
background: Some(Color::from_rgb(0.07, 0.07, 0.08).into()),
border: Border {
radius: 8.0.into(),
width: 1.0,
color: Color::from_rgb(0.2, 0.2, 0.2),
},
..Default::default()
});

column![header, terminal].spacing(12).into()
}

// One color-coded log line, the gpu id is replaced by the gpu name when known
fn log_line<'a>(
log: &'a LogEntry,
gpu_list: &'a BTreeMap<usize, GpuDevice>,
) -> Element<'a, Message> {
let timestamp = DateTime::<Local>::from(log.timestamp)
.format("%H:%M:%S")
.to_string();
let app_name = if log.wayland_app_id.is_empty() {
log.comm.as_str()
} else {
log.wayland_app_id.as_str()
};
let gpu_name = gpu_list
.get(&(log.gpu_id as usize))
.map(|g| g.name.as_str())
.unwrap_or("Unknown");

row![
text!("[{}] ", timestamp)
.color(Color::from_rgb(0.55, 0.55, 0.55))
.font(Font::MONOSPACE),
text(app_name)
.color(Color::from_rgb(0.35, 0.8, 0.98))
.font(Font::MONOSPACE),
text!("[{}] ", log.pid)
.color(Color::from_rgb(0.98, 0.2, 0.6))
.font(Font::MONOSPACE),
text("tried to access GPU ")
.color(Color::from_rgb(0.75, 0.75, 0.75))
.font(Font::MONOSPACE),
text(gpu_name)
.color(Color::from_rgb(0.4, 0.8, 0.4))
.font(Font::MONOSPACE),
text(" (blocked by cardwire)")
.color(Color::from_rgb(0.5, 0.5, 0.5))
.font(Font::MONOSPACE),
]
.align_y(Alignment::Center)
.into()
}

pub fn about_page() -> Element<'static, Message> {
let version = env!("CARGO_PKG_VERSION");
let content = column![
Expand Down