diff --git a/README.md b/README.md index 3b3e353..c587404 100644 --- a/README.md +++ b/README.md @@ -219,6 +219,36 @@ _Note_: all NinetyFive cache is stored at `~/.ninetyfive/` | `:NinetyFivePurchase` | Redirects to the purchase page | | `:NinetyFiveKey` | Provide an API key | +## Lualine Integration + +NinetyFive provides a [lualine](https://github.com/nvim-lualine/lualine.nvim) component: + +```lua +require("lualine").setup({ + sections = { + lualine_x = { "ninetyfive" }, + }, +}) +``` + +The status shows your subscription name when connected, or "NinetyFive Disconnected" when offline. Colors indicate connection state (red = disconnected, yellow = free tier). + +Options: + +```lua +lualine_x = { + { + "ninetyfive", + short = false, -- use "95" instead of full status text + show_colors = true, + colors = { + disconnected = "#e06c75", + unpaid = "#e5c07b", + }, + }, +} +``` + ## Development ```bash diff --git a/lua/lualine/components/ninetyfive.lua b/lua/lualine/components/ninetyfive.lua new file mode 100644 index 0000000..25c13cb --- /dev/null +++ b/lua/lualine/components/ninetyfive.lua @@ -0,0 +1,65 @@ +local lualine_require = require("lualine_require") +local M = lualine_require.require("lualine.component"):extend() +local highlight = require("lualine.highlight") + +local default_options = { + short = false, + show_colors = true, + colors = { + disconnected = "#e06c75", + unpaid = "#e5c07b", + }, +} + +function M:init(options) + M.super.init(self, options) + self.options = vim.tbl_deep_extend("force", default_options, self.options or {}) + + if self.options.show_colors then + self.highlight_groups = { + disconnected = highlight.create_component_highlight_group( + { bg = self.options.colors.disconnected, fg = "#ffffff" }, + "disconnected", + self.options + ), + unpaid = highlight.create_component_highlight_group( + { bg = self.options.colors.unpaid, fg = "#ffffff" }, + "unpaid", + self.options + ), + } + end +end + +function M:update_status() + local ok, websocket = pcall(require, "ninetyfive.websocket") + if not ok then + return "" + end + + local connected = websocket.is_connected() + local sub_info = websocket.get_subscription_info() + + local status_text + local hl_group + + if not connected then + status_text = self.options.short and "95" or "NinetyFive Disconnected" + hl_group = self.highlight_groups and self.highlight_groups.disconnected + elseif sub_info and sub_info.name then + status_text = self.options.short and "95" or sub_info.name + if not sub_info.is_paid then + hl_group = self.highlight_groups and self.highlight_groups.unpaid + end + else + status_text = self.options.short and "95" or "NinetyFive" + hl_group = self.highlight_groups and self.highlight_groups.unpaid + end + + if hl_group then + return highlight.component_format_highlight(hl_group) .. status_text + end + return status_text +end + +return M diff --git a/lua/ninetyfive/communication.lua b/lua/ninetyfive/communication.lua index 1ccbc6a..801c5ca 100644 --- a/lua/ninetyfive/communication.lua +++ b/lua/ninetyfive/communication.lua @@ -171,6 +171,11 @@ function Communication:_send_workspace(payload) end vim.schedule(function() + -- Check connection is still valid before sending + if not websocket.is_connected() then + log.debug("comm", "skipping set-workspace - websocket not connected") + return + end if not websocket.send_message(message) then log.debug("comm", "failed to send set-workspace message") end diff --git a/lua/ninetyfive/http.lua b/lua/ninetyfive/http.lua index dec0d35..b773b45 100644 --- a/lua/ninetyfive/http.lua +++ b/lua/ninetyfive/http.lua @@ -25,7 +25,7 @@ if ok_ffi then "curl", } - ffi.cdef([[ + pcall(ffi.cdef, [[ typedef void CURL; typedef void CURLM; typedef void CURLSH; diff --git a/lua/ninetyfive/init.lua b/lua/ninetyfive/init.lua index 1969381..48938bc 100644 --- a/lua/ninetyfive/init.lua +++ b/lua/ninetyfive/init.lua @@ -231,6 +231,38 @@ function Ninetyfive.reject() suggestion.clear() end +--- Returns the current status text for display (e.g., in lualine) +--- Returns subscription name if connected, "NinetyFive Disconnected" otherwise +---@return string +function Ninetyfive.get_status() + if not websocket.is_connected() then + return "NinetyFive Disconnected" + end + + local sub_info = websocket.get_subscription_info() + if sub_info and sub_info.name then + return sub_info.name + end + + return "NinetyFive" +end + +--- Returns the color for the current status (for lualine) +--- Returns nil for paid users (use default), red for disconnected, yellow for unpaid +---@return table|nil +function Ninetyfive.get_status_color() + if not websocket.is_connected() then + return { fg = "#e06c75" } -- red for disconnected + end + + local sub_info = websocket.get_subscription_info() + if sub_info and sub_info.is_paid then + return nil -- no color override for paid users + end + + return { fg = "#e5c07b" } -- yellow for unpaid/unknown +end + _G.Ninetyfive = Ninetyfive return _G.Ninetyfive diff --git a/lua/ninetyfive/websocket.lua b/lua/ninetyfive/websocket.lua index ba2b031..38be46a 100644 --- a/lua/ninetyfive/websocket.lua +++ b/lua/ninetyfive/websocket.lua @@ -23,6 +23,9 @@ local connection_params = { -- Callbacks to run after successful reconnection local on_reconnect_callbacks = {} +-- Store subscription info from server +local subscription_info = nil + function Websocket.on_reconnect(callback) table.insert(on_reconnect_callbacks, callback) end @@ -49,7 +52,12 @@ function Websocket.send_message(message) log.debug("websocket", "-> sending: %s", message) - local result = vim.fn.chansend(_G.Ninetyfive.websocket_job, message .. "\n") + -- Use pcall to handle race condition where channel closes between check and send + local ok, result = pcall(vim.fn.chansend, _G.Ninetyfive.websocket_job, message .. "\n") + if not ok then + log.debug("websocket", "channel closed while sending: %s", tostring(result)) + return false + end return result > 0 end @@ -276,6 +284,10 @@ function Websocket.setup_connection(server_uri, user_id, api_key) if msg_type == "subscription-info" then log.debug("messages", "<- [subscription-info]", parsed) + subscription_info = { + is_paid = parsed.isPaid, + name = parsed.name, + } elseif msg_type == "get-commit" then local commit = git.get_commit(parsed.commitHash) if commit then @@ -351,16 +363,7 @@ function Websocket.setup_connection(server_uri, user_id, api_key) _G.Ninetyfive.websocket_job = nil -- 143 = SIGTERM (normal shutdown), 0 = normal exit - if exit_code ~= 0 and exit_code ~= 143 then - log.notify( - "websocket", - vim.log.levels.WARN, - true, - "websocket disconnected (code: " .. tostring(exit_code) .. ")" - ) - else - log.debug("websocket", "websocket job exiting with code: " .. tostring(exit_code)) - end + log.debug("websocket", "websocket job exiting with code: " .. tostring(exit_code)) -- Attempt reconnection if not intentional shutdown if not is_intentional_shutdown and connection_params.server_uri then @@ -376,7 +379,7 @@ function Websocket.setup_connection(server_uri, user_id, api_key) connection_params.api_key ) if success then - log.notify("websocket", vim.log.levels.INFO, false, "Reconnected") + log.debug("websocket", "Reconnected") vim.defer_fn(run_reconnect_callbacks, 500) end end, reconnect_delay) @@ -405,4 +408,16 @@ function Websocket.get_completion() return c.completion end +function Websocket.get_subscription_info() + return subscription_info +end + +function Websocket.is_connected() + return _G.Ninetyfive and _G.Ninetyfive.websocket_job and _G.Ninetyfive.websocket_job > 0 +end + +function Websocket.clear_subscription_info() + subscription_info = nil +end + return Websocket