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
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
65 changes: 65 additions & 0 deletions lua/lualine/components/ninetyfive.lua
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions lua/ninetyfive/communication.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lua/ninetyfive/http.lua
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ if ok_ffi then
"curl",
}

ffi.cdef([[
pcall(ffi.cdef, [[
typedef void CURL;
typedef void CURLM;
typedef void CURLSH;
Expand Down
32 changes: 32 additions & 0 deletions lua/ninetyfive/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
39 changes: 27 additions & 12 deletions lua/ninetyfive/websocket.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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
Loading