generated from nhcarrigan/template
feat: add discord rich presence (#105)
### Explanation _No response_ ### Issue _No response_ ### Attestations - [ ] I have read and agree to the [Code of Conduct](https://docs.nhcarrigan.com/community/coc/) - [ ] I have read and agree to the [Community Guidelines](https://docs.nhcarrigan.com/community/guide/). - [ ] My contribution complies with the [Contributor Covenant](https://docs.nhcarrigan.com/dev/covenant/). ### Dependencies - [ ] I have pinned the dependencies to a specific patch version. ### Style - [ ] I have run the linter and resolved any errors. - [ ] My pull request uses an appropriate title, matching the conventional commit standards. - [ ] My scope of feat/fix/chore/etc. correctly matches the nature of changes in my pull request. ### Tests - [ ] My contribution adds new code, and I have added tests to cover it. - [ ] My contribution modifies existing code, and I have updated the tests to reflect these changes. - [ ] All new and existing tests pass locally with my changes. - [ ] Code coverage remains at or above the configured threshold. ### Documentation _No response_ ### Versioning _No response_ Reviewed-on: #105 Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com> Co-committed-by: Naomi Carrigan <commits@nhcarrigan.com>
This commit was merged in pull request #105.
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
use discord_rich_presence::activity::{Activity, Assets, Timestamps};
|
||||
use discord_rich_presence::{DiscordIpc, DiscordIpcClient};
|
||||
use parking_lot::RwLock;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
pub struct DiscordRpcManager {
|
||||
client: Arc<RwLock<Option<DiscordIpcClient>>>,
|
||||
session_name: Arc<RwLock<String>>,
|
||||
model: Arc<RwLock<String>>,
|
||||
started_at: Arc<RwLock<i64>>,
|
||||
log_path: Arc<RwLock<Option<PathBuf>>>,
|
||||
}
|
||||
|
||||
impl DiscordRpcManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
client: Arc::new(RwLock::new(None)),
|
||||
session_name: Arc::new(RwLock::new(String::new())),
|
||||
model: Arc::new(RwLock::new(String::new())),
|
||||
started_at: Arc::new(RwLock::new(0)),
|
||||
log_path: Arc::new(RwLock::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_app_handle(&self, app_handle: &AppHandle) {
|
||||
if let Ok(app_data_dir) = app_handle.path().app_data_dir() {
|
||||
// Ensure the directory exists
|
||||
if let Err(e) = std::fs::create_dir_all(&app_data_dir) {
|
||||
eprintln!("Failed to create app data directory: {}", e);
|
||||
return;
|
||||
}
|
||||
let log_path = app_data_dir.join("hikari_discord_rpc.log");
|
||||
*self.log_path.write() = Some(log_path.clone());
|
||||
self.log(&format!(
|
||||
"Log file initialised at: {}",
|
||||
log_path.display()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn log(&self, message: &str) {
|
||||
let log_path_guard = self.log_path.read();
|
||||
let path = match log_path_guard.as_ref() {
|
||||
Some(p) => p.clone(),
|
||||
None => PathBuf::from("hikari_discord_rpc.log"),
|
||||
};
|
||||
drop(log_path_guard);
|
||||
|
||||
if let Ok(mut file) = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&path)
|
||||
{
|
||||
let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
|
||||
let _ = writeln!(file, "[{}] {}", timestamp, message);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(&self, initial_session_name: String, initial_model: String, started_at: i64) -> Result<(), String> {
|
||||
self.log("Attempting to initialize Discord RPC...");
|
||||
self.log("DEBUG: Application ID: 1391117878182281316");
|
||||
self.log(&format!("DEBUG: Initial session: '{}', model: '{}', timestamp: {}",
|
||||
initial_session_name, initial_model, started_at));
|
||||
|
||||
let mut client = DiscordIpcClient::new("1391117878182281316")
|
||||
.map_err(|e| {
|
||||
let error_msg = format!("Failed to create Discord RPC client: {} (is Discord running?)", e);
|
||||
self.log(&format!("ERROR: {}", error_msg));
|
||||
error_msg
|
||||
})?;
|
||||
|
||||
self.log("DEBUG: DiscordIpcClient created successfully");
|
||||
|
||||
client
|
||||
.connect()
|
||||
.map_err(|e| {
|
||||
let error_msg = format!("Failed to connect to Discord RPC: {} (ensure Discord is running)", e);
|
||||
self.log(&format!("ERROR: {}", error_msg));
|
||||
error_msg
|
||||
})?;
|
||||
|
||||
self.log("DEBUG: Connected to Discord IPC socket");
|
||||
|
||||
// Set initial activity immediately after connecting
|
||||
self.log("DEBUG: Building initial activity...");
|
||||
let state_text = format!("Model: {}", initial_model);
|
||||
let assets = Assets::new()
|
||||
.large_image("hikari")
|
||||
.large_text("Hikari - Claude Code Assistant");
|
||||
|
||||
self.log("DEBUG: Assets created - large_image: 'hikari', large_text: 'Hikari - Claude Code Assistant'");
|
||||
|
||||
let timestamps = Timestamps::new()
|
||||
.start(started_at);
|
||||
|
||||
self.log(&format!("DEBUG: Timestamps created - start: {}", started_at));
|
||||
|
||||
let activity = Activity::new()
|
||||
.details(initial_session_name.as_str())
|
||||
.state(state_text.as_str())
|
||||
.assets(assets)
|
||||
.timestamps(timestamps);
|
||||
|
||||
self.log(&format!("DEBUG: Activity created - details: '{}', state: '{}'",
|
||||
initial_session_name, state_text));
|
||||
|
||||
self.log("DEBUG: Attempting to set initial activity...");
|
||||
client
|
||||
.set_activity(activity)
|
||||
.map_err(|e| {
|
||||
let error_msg = format!("Failed to set initial Discord RPC activity: {}", e);
|
||||
self.log(&format!("ERROR: {}", error_msg));
|
||||
error_msg
|
||||
})?;
|
||||
|
||||
self.log("DEBUG: Initial activity set successfully!");
|
||||
|
||||
// Store the client and initial state
|
||||
*self.client.write() = Some(client);
|
||||
*self.session_name.write() = initial_session_name.clone();
|
||||
*self.model.write() = initial_model.clone();
|
||||
*self.started_at.write() = started_at;
|
||||
|
||||
self.log(&format!("Discord RPC connected successfully with initial activity: session='{}', model='{}'",
|
||||
initial_session_name, initial_model));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update(
|
||||
&self,
|
||||
session_name: String,
|
||||
model: String,
|
||||
started_at: i64,
|
||||
) -> Result<(), String> {
|
||||
self.log(&format!("DEBUG: update() called with session='{}', model='{}', timestamp={}",
|
||||
session_name, model, started_at));
|
||||
|
||||
*self.session_name.write() = session_name.clone();
|
||||
*self.model.write() = model.clone();
|
||||
*self.started_at.write() = started_at;
|
||||
|
||||
self.log("DEBUG: State variables updated");
|
||||
|
||||
let mut client_guard = self.client.write();
|
||||
let client = client_guard
|
||||
.as_mut()
|
||||
.ok_or_else(|| {
|
||||
let error_msg = "Discord RPC client not initialized".to_string();
|
||||
self.log(&format!("ERROR: {}", error_msg));
|
||||
error_msg
|
||||
})?;
|
||||
|
||||
self.log("DEBUG: Client lock acquired");
|
||||
|
||||
let state_text = format!("Model: {}", model);
|
||||
let assets = Assets::new()
|
||||
.large_image("hikari")
|
||||
.large_text("Hikari - Claude Code Assistant");
|
||||
|
||||
self.log("DEBUG: Assets created - large_image: 'hikari', large_text: 'Hikari - Claude Code Assistant'");
|
||||
|
||||
let timestamps = Timestamps::new()
|
||||
.start(started_at);
|
||||
|
||||
self.log(&format!("DEBUG: Timestamps created - start: {}", started_at));
|
||||
|
||||
let activity = Activity::new()
|
||||
.details(session_name.as_str())
|
||||
.state(state_text.as_str())
|
||||
.assets(assets)
|
||||
.timestamps(timestamps);
|
||||
|
||||
self.log(&format!("DEBUG: Activity created - details: '{}', state: '{}'",
|
||||
session_name, state_text));
|
||||
|
||||
self.log("DEBUG: Attempting to set activity...");
|
||||
client
|
||||
.set_activity(activity)
|
||||
.map_err(|e| {
|
||||
let error_msg = format!("Failed to update Discord RPC: {}", e);
|
||||
self.log(&format!("ERROR: {}", error_msg));
|
||||
error_msg
|
||||
})?;
|
||||
|
||||
self.log(&format!("Updated Discord RPC: session='{}', model='{}'", session_name, model));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn stop(&self) -> Result<(), String> {
|
||||
self.log("DEBUG: stop() called");
|
||||
|
||||
let mut client_guard = self.client.write();
|
||||
if let Some(mut client) = client_guard.take() {
|
||||
self.log("DEBUG: Client found, attempting to close...");
|
||||
client
|
||||
.close()
|
||||
.map_err(|e| {
|
||||
let error_msg = format!("Failed to close Discord RPC: {}", e);
|
||||
self.log(&format!("ERROR: {}", error_msg));
|
||||
error_msg
|
||||
})?;
|
||||
self.log("Discord RPC stopped successfully");
|
||||
} else {
|
||||
self.log("DEBUG: No client to stop (already stopped or never initialized)");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DiscordRpcManager {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user