diff --git a/lua/cord.lua b/lua/cord.lua index a9a3e300..c1efda3e 100644 --- a/lua/cord.lua +++ b/lua/cord.lua @@ -55,10 +55,10 @@ local timer = vim.loop.new_timer() local enabled = false local is_focused = true local force_idle = false -local is_blacklisted = false local problem_count = -1 local last_updated = os.clock() local last_presence +local is_blacklisted local function connect(config) discord.init( @@ -72,6 +72,14 @@ local function connect(config) config.text.file_browser, config.text.plugin_manager, config.text.workspace, + vim.fn.getcwd(), + ffi.new( + 'Buttons', + (config.buttons[1] and config.buttons[1].label) or '', + (config.buttons[1] and config.buttons[1].url) or '', + (config.buttons[2] and config.buttons[2].label) or '', + (config.buttons[2] and config.buttons[2].url) or '' + ), config.display.swap_fields ) end @@ -114,7 +122,7 @@ local function update_idle_presence(config) return false end -local function update_presence(config) +local function update_presence(config, initial) if is_blacklisted then return end @@ -140,6 +148,13 @@ local function update_presence(config) local success = discord.update_presence(current_presence.name, current_presence.type, current_presence.readonly, cursor_pos, problem_count) if success then last_presence = current_presence + if is_blacklisted == nil then + is_blacklisted = utils.array_contains(config.display.workspace_blacklist, ffi.string(discord.update_workspace(vim.fn.getcwd()))) + end + if initial then + timer:stop() + timer:start(0, config.timer.interval, vim.schedule_wrap(function() update_presence(config, false) end)) + end end elseif not update_idle_presence(config) then return @@ -148,17 +163,13 @@ end local function start_timer(config) timer:stop() - if vim.g.cord_started == nil then - vim.g.cord_started = true - if not utils.validate_severity(config) then return end - is_blacklisted = utils.array_contains(config.display.workspace_blacklist, utils.update_cwd(config, discord)) - cord.setup_autocmds(config) - if config.display.show_time then - discord.update_time() - end + + if not utils.validate_severity(config) then return end + if config.display.show_time then + discord.update_time() end - update_presence(config) - timer:start(0, config.timer.interval, vim.schedule_wrap(function() update_presence(config) end)) + + timer:start(0, 1000, vim.schedule_wrap(function() update_presence(config, true) end)) end function cord.setup(userConfig) @@ -166,23 +177,22 @@ function cord.setup(userConfig) local config = vim.tbl_deep_extend('force', cord.config, userConfig or {}) config.timer.interval = math.max(config.timer.interval, 500) - local work = vim.loop.new_async(vim.schedule_wrap(function() - discord = utils.init_discord(ffi) - connect(config) - if config.timer.enable then - start_timer(config) - end + discord = utils.init_discord(ffi) + connect(config) + if config.timer.enable then + cord.setup_autocmds(config) + start_timer(config) + end + + vim.api.nvim_create_autocmd('ExitPre', { callback = function() discord.disconnect() end }) + if config.usercmds then cord.setup_usercmds(config) end - vim.api.nvim_create_autocmd('ExitPre', { callback = function() discord.disconnect() end }) - if config.usercmds then cord.setup_usercmds(config) end - end)) - work:send() vim.g.cord_initialized = true end end function cord.setup_autocmds(config) - vim.api.nvim_create_autocmd('DirChanged', { callback = function() is_blacklisted = utils.array_contains(config.display.workspace_blacklist, utils.update_cwd(config, discord)) end }) + vim.api.nvim_create_autocmd('DirChanged', { callback = function() is_blacklisted = utils.array_contains(config.display.workspace_blacklist, ffi.string(discord.update_workspace(vim.fn.getcwd()))) end }) vim.api.nvim_create_autocmd('FocusGained', { callback = function() is_focused = true; last_presence = nil end }) vim.api.nvim_create_autocmd('FocusLost', { callback = function() is_focused = false end }) end diff --git a/lua/cord/utils.lua b/lua/cord/utils.lua index f4326a58..41c32078 100644 --- a/lua/cord/utils.lua +++ b/lua/cord/utils.lua @@ -32,6 +32,12 @@ local function init_discord(ffi) end ffi.cdef[[ + typedef struct { + const char* first_label; + const char* first_url; + const char* second_label; + const char* second_url; + } Buttons; void init( const char* client, const char* image, @@ -43,9 +49,11 @@ local function init_discord(ffi) const char* fileBrowserText, const char* pluginManagerText, const char* workspaceText, - bool swap + const char* initialPath, + const Buttons* buttons, + const bool swap ); - bool update_presence( + const bool update_presence( const char* filename, const char* filetype, bool isReadOnly, @@ -54,49 +62,14 @@ local function init_discord(ffi) ); void clear_presence(); void disconnect(); - void set_cwd(const char* directory); - void set_buttons( - const char* first_label, - const char* first_url, - const char* second_label, - const char* second_url - ); + const char* update_workspace(const char* workspace); void update_time(); + const char* get_workspace(); ]] return ffi.load(new_path) end -local function fetch_repository() - local handle = io.popen('git config --get remote.origin.url') - if not handle then - vim.notify('[cord.nvim] Could not fetch Git repository URL', vim.log.levels.WARN) - return - end - local git_url = handle:read('*a') - handle:close() - - return git_url:match('^%s*(.-)%s*$') -end - -local function find_workspace() - local curr_dir = vim.fn.expand('%:p:h') - local vcs_markers = {'.git', '.svn', '.hg'} - - while curr_dir ~= '' do - for _, dir in ipairs(vcs_markers) do - if vim.fn.isdirectory(curr_dir .. '/' .. dir) == 1 then - return vim.fn.fnamemodify(curr_dir, ':t') - end - end - - curr_dir = vim.fn.fnamemodify(curr_dir, ':h') - if curr_dir == vim.fn.fnamemodify(curr_dir, ':h') then break end -- reached root - end - - return vim.fn.fnamemodify(vim.fn.getcwd(), ':t') -- fallback to cwd -end - local function validate_severity(config) config.lsp.severity = tonumber(config.lsp.severity) if config.lsp.severity == nil or config.lsp.severity < 1 or config.lsp.severity > 4 then @@ -106,46 +79,6 @@ local function validate_severity(config) return true end -local function validate_buttons(config) - if config.display.show_repository then - local buttons = {} - local repo - for i, button in ipairs(config.buttons) do - if i > 2 then - vim.notify('[cord.nvim] Detected more than two buttons in the config. Only the first two will be displayed', vim.log.levels.WARN) - return buttons - end - if button.url == 'git' then - if not repo then - repo = fetch_repository() - end - if repo and repo ~= '' then - table.insert(buttons, { label = button.label, url = repo }) - end - else - table.insert(buttons, button) - end - end - return buttons - end -end - -local function update_cwd(config, discord) - local workspace = find_workspace() - discord.set_cwd(workspace) - - local buttons = validate_buttons(config) - if not buttons then return workspace end - - if #buttons == 1 then - discord.set_buttons(buttons[1].label, buttons[1].url, nil, nil) - elseif #buttons >= 2 then - discord.set_buttons(buttons[1].label, buttons[1].url, buttons[2].label, buttons[2].url) - end - - return workspace -end - local function get_problem_count(config) if config.lsp.show_problem_count then local bufnr = config.lsp.scope == 'buffer' and vim.api.nvim_get_current_buf() or nil @@ -172,10 +105,7 @@ end return { init_discord = init_discord, - fetch_repository = fetch_repository, - find_workspace = find_workspace, validate_severity = validate_severity, - update_cwd = update_cwd, get_problem_count = get_problem_count, array_contains = array_contains } diff --git a/src/lib.rs b/src/lib.rs index 4521df2c..08c2afc9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,21 +5,23 @@ mod rpc; mod utils; use rpc::activity::{ActivityAssets, ActivityButton}; -use std::{ffi::c_char, time::UNIX_EPOCH}; -use utils::{build_presence, get_presence_state, ptr_to_string}; +use std::{ + ffi::{c_char, CString}, + ptr::null, + time::UNIX_EPOCH, +}; +use utils::{ + build_presence, find_workspace, get_presence_state, ptr_to_string, + validate_buttons, GITHUB_ASSETS_URL, +}; use crate::{ ipc::client::{Connection, RichClient}, rpc::packet::{Activity, Packet}, }; -const GITHUB_ASSETS_URL: &str = - "http://raw.githubusercontent.com/vyfor/cord.nvim/master/assets"; - static mut INITIALIZED: bool = false; -static mut CWD: String = String::new(); static mut START_TIME: Option = None; -static mut BUTTONS: Vec = Vec::new(); static mut CONFIG: Option = None; struct Config { @@ -33,9 +35,25 @@ struct Config { file_browser_text: String, plugin_manager_text: String, workspace_text: String, + workspace: String, + buttons: Vec, swap_fields: bool, } +#[repr(C)] +pub struct Buttons { + pub first_label: *const c_char, + pub first_url: *const c_char, + pub second_label: *const c_char, + pub second_url: *const c_char, +} + +#[repr(C)] +pub struct StringArray { + pub array: *const *const c_char, + pub length: usize, +} + #[no_mangle] pub extern "C" fn init( client: *const c_char, @@ -48,6 +66,8 @@ pub extern "C" fn init( file_browser_text: *const c_char, plugin_manager_text: *const c_char, workspace_text: *const c_char, + initial_path: *const c_char, + buttons: *const Buttons, swap_fields: bool, ) { unsafe { @@ -90,6 +110,16 @@ pub extern "C" fn init( let file_browser_text = ptr_to_string(file_browser_text); let plugin_manager_text = ptr_to_string(plugin_manager_text); let workspace_text = ptr_to_string(workspace_text); + let workspace = find_workspace(&ptr_to_string(initial_path)); + + let buttons = &*buttons; + let buttons = validate_buttons( + ptr_to_string(buttons.first_label), + ptr_to_string(buttons.first_url), + ptr_to_string(buttons.second_label), + ptr_to_string(buttons.second_url), + workspace.to_str().unwrap(), + ); std::thread::spawn(move || { if let Ok(mut client) = RichClient::connect(client_id) { @@ -109,6 +139,12 @@ pub extern "C" fn init( file_browser_text: file_browser_text, plugin_manager_text: plugin_manager_text, workspace_text: workspace_text, + workspace: workspace + .file_name() + .unwrap() + .to_string_lossy() + .to_string(), + buttons: buttons, swap_fields: swap_fields, }); INITIALIZED = true; @@ -147,7 +183,7 @@ pub extern "C" fn update_presence( ( config.idle_text.clone(), - format!("{}/editor/idle.png?v=1", GITHUB_ASSETS_URL), + format!("{}/editor/idle.png?v=5", GITHUB_ASSETS_URL), config.idle_tooltip.clone(), ) } else { @@ -164,11 +200,17 @@ pub extern "C" fn update_presence( if config.swap_fields { activity.state = Some(presence_details); - activity.details = - get_presence_state(&config, CWD.as_ref(), problem_count); + activity.details = get_presence_state( + &config, + &config.workspace, + problem_count, + ); } else { - activity.state = - get_presence_state(&config, CWD.as_ref(), problem_count); + activity.state = get_presence_state( + &config, + &config.workspace, + problem_count, + ); activity.details = Some(presence_details); } activity.assets = Some(ActivityAssets { @@ -188,8 +230,8 @@ pub extern "C" fn update_presence( START_TIME.as_ref().map(|start_time| { activity.timestamp = Some(start_time.clone()); }); - if !BUTTONS.is_empty() { - activity.buttons = Some(BUTTONS.clone()); + if !config.buttons.is_empty() { + activity.buttons = Some(config.buttons.clone()); } match config @@ -237,48 +279,43 @@ pub extern "C" fn disconnect() { } #[no_mangle] -pub extern "C" fn set_cwd(value: *const c_char) { +pub extern "C" fn update_time() { unsafe { - CWD = ptr_to_string(value); + START_TIME = Some( + std::time::SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis(), + ); } } #[no_mangle] -pub extern "C" fn set_buttons( - first_label: *const c_char, - first_url: *const c_char, - second_label: *const c_char, - second_url: *const c_char, -) { +pub extern "C" fn update_workspace(value: *const c_char) -> *const c_char { unsafe { - BUTTONS.clear(); - let first_label = ptr_to_string(first_label); - let first_url = ptr_to_string(first_url); - if !first_label.is_empty() && !first_url.is_empty() { - BUTTONS.push(ActivityButton { - label: first_label, - url: first_url, - }); - } - let second_label = ptr_to_string(second_label); - let second_url = ptr_to_string(second_url); - if !second_label.is_empty() && !second_url.is_empty() { - BUTTONS.push(ActivityButton { - label: second_label, - url: second_url, - }) + let mut ws = String::new(); + if let Some(config) = CONFIG.as_mut() { + if let Some(workspace) = + find_workspace(&ptr_to_string(value)).file_name() + { + let workspace = workspace.to_string_lossy().to_string(); + ws = workspace.clone(); + config.workspace = workspace; + } } + + CString::new(ws).unwrap().into_raw() as *const c_char } } #[no_mangle] -pub extern "C" fn update_time() { +pub extern "C" fn get_workspace() -> *const c_char { unsafe { - START_TIME = Some( - std::time::SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis(), - ); + if let Some(config) = CONFIG.as_ref() { + CString::new(config.workspace.clone()).unwrap().into_raw() + as *const c_char + } else { + null() + } } } diff --git a/src/utils.rs b/src/utils.rs index 854ff0b3..12dc9f22 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,10 +1,20 @@ -use std::ffi::{c_char, CStr}; +use std::{ + ffi::{c_char, CStr}, + fs::File, + io::{BufRead, BufReader}, + path::PathBuf, +}; use crate::{ mappings::{get_by_filetype, Filetype}, - Config, GITHUB_ASSETS_URL, + rpc::activity::ActivityButton, + Config, }; +pub const GITHUB_ASSETS_URL: &str = + "http://raw.githubusercontent.com/vyfor/cord.nvim/master/assets"; +const VCS_MARKERS: [&str; 3] = [".git", ".svn", ".hg"]; + #[inline(always)] pub fn ptr_to_string(ptr: *const c_char) -> String { if ptr.is_null() { @@ -15,6 +25,74 @@ pub fn ptr_to_string(ptr: *const c_char) -> String { c_str.to_string_lossy().into_owned() } +#[inline(always)] +pub fn find_workspace(initial_path: &str) -> PathBuf { + let mut curr_dir = PathBuf::from(initial_path); + + while !curr_dir.as_os_str().is_empty() { + for dir in VCS_MARKERS { + let marker_path = curr_dir.join(dir); + if marker_path.is_dir() { + return curr_dir; + } + } + + curr_dir = match curr_dir.parent() { + Some(parent) => parent.to_path_buf(), + None => break, + }; + if curr_dir.parent() == Some(&curr_dir) { + break; + } + } + + PathBuf::from(initial_path) +} + +#[inline(always)] +pub fn validate_buttons( + first_label: String, + mut first_url: String, + second_label: String, + mut second_url: String, + workspace: &str, +) -> Vec { + let mut buttons = Vec::with_capacity(2); + + if first_url == "git" || second_url == "git" { + if let Some(repository) = find_repository(workspace) { + if first_url == "git" { + first_url = repository.clone(); + } + if second_url == "git" { + second_url = repository; + } + } + } + + if !first_label.is_empty() + && !first_url.is_empty() + && first_url.starts_with("http") + { + buttons.push(ActivityButton { + label: first_label, + url: first_url, + }); + } + + if !second_label.is_empty() + && !second_url.is_empty() + && second_url.starts_with("http") + { + buttons.push(ActivityButton { + label: second_label, + url: second_url, + }); + } + + buttons +} + #[inline(always)] pub fn build_presence( config: &Config, @@ -125,3 +203,33 @@ fn plugin_manager_presence( (presence_details, presence_large_image, presence_large_text) } + +#[inline(always)] +fn find_repository(workspace_path: &str) -> Option { + let config_path = format!("{}/{}", workspace_path, ".git/config"); + + let file = match File::open(config_path) { + Ok(file) => file, + Err(_) => return None, + }; + let reader = BufReader::new(file); + + let mut repo_url = String::new(); + + for line in reader.lines() { + let line = match line { + Ok(line) => line, + Err(_) => continue, + }; + if line.trim_start().starts_with("url = ") { + repo_url = line[7..].trim().to_string(); + break; + } + } + + if repo_url.is_empty() { + None + } else { + Some(repo_url) + } +}