{"id":49056765,"url":"https://github.com/selimacerbas/live-server.nvim","last_synced_at":"2026-04-19T23:13:21.136Z","repository":{"id":336891412,"uuid":"1074192660","full_name":"selimacerbas/live-server.nvim","owner":"selimacerbas","description":"A tiny, zero-dependency local web server for Neovim — written in pure Lua with `vim.loop`. Start a server on any file or folder, auto-reload the browser on save, and quickly reopen existing ports.","archived":false,"fork":false,"pushed_at":"2026-03-21T08:43:01.000Z","size":57,"stargazers_count":15,"open_issues_count":1,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-03-22T00:39:44.717Z","etag":null,"topics":["http-server","live-server","lua","neovim","neovim-plugin","sse"],"latest_commit_sha":null,"homepage":"","language":"Lua","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/selimacerbas.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-10-11T10:15:16.000Z","updated_at":"2026-03-21T08:42:55.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/selimacerbas/live-server.nvim","commit_stats":null,"previous_names":["selimacerbas/live-server.nvim"],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/selimacerbas/live-server.nvim","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/selimacerbas%2Flive-server.nvim","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/selimacerbas%2Flive-server.nvim/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/selimacerbas%2Flive-server.nvim/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/selimacerbas%2Flive-server.nvim/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/selimacerbas","download_url":"https://codeload.github.com/selimacerbas/live-server.nvim/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/selimacerbas%2Flive-server.nvim/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32025858,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-18T20:23:30.271Z","status":"online","status_checked_at":"2026-04-19T02:00:07.110Z","response_time":55,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["http-server","live-server","lua","neovim","neovim-plugin","sse"],"created_at":"2026-04-19T23:13:20.566Z","updated_at":"2026-04-19T23:13:21.104Z","avatar_url":"https://github.com/selimacerbas.png","language":"Lua","readme":"# live-server.nvim\n\nA tiny, zero-dependency **local web server** for Neovim — written in pure Lua with `vim.loop`.\nStart a server on any file or folder, auto-reload the browser on save, and quickly reopen existing ports.\n\n* **Pure Lua**: no npm, no Python, no binaries.\n* **Local-only**: binds to `127.0.0.1` (loopback).\n* **SSE live-reload**: instant page refresh on file changes (debounced).\n* **CSS hot-inject**: stylesheet changes apply instantly without a full page reload.\n* **Directory listing**: clean index when no `index.html` exists.\n* **Telescope UX**: pick a path (file or directory) and a port from a friendly picker.\n* **Which-key friendly**: group label in `init`, real mappings in `keys`, no conflicts.\n* **Same-port retargeting**: starting on the same port updates the served root/index (reuses the same browser tab/URL).\n* **Auto-start**: optionally start a server when you open an HTML file.\n* **Statusline**: show active servers in your statusline/lualine.\n\n\u003e This plugin serves **only** on `127.0.0.1`. It's meant for local dev previews, not production.\n\n---\n\n## Requirements\n\n* Neovim **0.8+** (tested on 0.9 / 0.10).\n* Linux, macOS, or Windows.\n* [telescope.nvim](https://github.com/nvim-telescope/telescope.nvim) **recommended** for the best picking UX (falls back to `vim.ui.select/input` if missing).\n* [which-key.nvim](https://github.com/folke/which-key.nvim) recommended.\n\n---\n\n## Installation (lazy.nvim)\n\n```lua\n-- lua/plugins/live-server.lua\nreturn {\n  \"selimacerbas/live-server.nvim\",\n  dependencies = {\n    \"folke/which-key.nvim\",\n    \"nvim-telescope/telescope.nvim\", -- recommended for path picker\n  },\n  init = function()\n    -- which-key group label only (best practice)\n    local ok, wk = pcall(require, \"which-key\")\n    if ok then wk.add({ { \"\u003cleader\u003el\", group = \"LiveServer\" } }) end\n  end,\n  opts = {\n    default_port = 8000,\n    live_reload = { enabled = true, inject_script = true, debounce = 120, css_inject = true },\n    directory_listing = { enabled = true, show_hidden = false },\n  },\n  -- map to user commands (robust lazy-loading)\n  keys = {\n    { \"\u003cleader\u003els\", \"\u003ccmd\u003eLiveServerStart\u003ccr\u003e\",      desc = \"Start (pick path \u0026 port)\" },\n    { \"\u003cleader\u003elo\", \"\u003ccmd\u003eLiveServerOpen\u003ccr\u003e\",       desc = \"Open existing port in browser\" },\n    { \"\u003cleader\u003elr\", \"\u003ccmd\u003eLiveServerReload\u003ccr\u003e\",     desc = \"Force reload (pick port)\" },\n    { \"\u003cleader\u003elt\", \"\u003ccmd\u003eLiveServerToggleLive\u003ccr\u003e\", desc = \"Toggle live-reload (pick port)\" },\n    { \"\u003cleader\u003eli\", \"\u003ccmd\u003eLiveServerStatus\u003ccr\u003e\",     desc = \"Show server status\" },\n    { \"\u003cleader\u003elS\", \"\u003ccmd\u003eLiveServerStop\u003ccr\u003e\",       desc = \"Stop one (pick port)\" },\n    { \"\u003cleader\u003elA\", \"\u003ccmd\u003eLiveServerStopAll\u003ccr\u003e\",    desc = \"Stop all\" },\n  },\n  config = function(_, opts)\n    require(\"live_server\").setup(opts)\n  end,\n}\n```\n\n---\n\n## Usage\n\n### Start a server\n\n* Press **`\u003cleader\u003els`** (or run `:LiveServerStart`).\n* Pick a **path** (file or directory), then pick a **port** (default `8000`).\n* Your browser opens `http://127.0.0.1:\u003cport\u003e/`.\n\n\u003e If you pick a **file**, the server serves the file's folder with that file as the default index.\n\u003e If you pick the **same port** again later, the server **retargets** to the new root/index instead of creating a new instance.\n\n### Other commands\n\n| Command | Description |\n| --- | --- |\n| `:LiveServerStart` | Pick a path and port, start serving |\n| `:LiveServerOpen` | Pick a port, open its URL in browser |\n| `:LiveServerReload` | Force reload all connected clients |\n| `:LiveServerToggleLive` | Enable/disable file watching for a port |\n| `:LiveServerStatus` | Show running servers (port, root, uptime, clients) |\n| `:LiveServerStop` | Pick a port to stop |\n| `:LiveServerStopAll` | Stop all servers |\n\n---\n\n## Options\n\nConfigured via `require(\"live_server\").setup({...})` or `opts = { ... }` in your lazy spec.\n\n```lua\n{\n  default_port     = 8000,           -- default suggestion in the port picker\n  open_on_start    = true,           -- open browser after start/retarget\n  notify           = true,           -- use vim.notify for events\n  notify_on_reload = false,          -- notify on every live-reload event\n  headers          = { [\"Cache-Control\"] = \"no-cache\" }, -- extra response headers\n  cors             = false,          -- true/\"*\" or origin string (e.g. \"http://localhost:3000\")\n  index_names      = { \"index.html\", \"index.htm\" }, -- index files to try in order\n\n  auto_start = nil,                  -- set to auto-start on filetype, e.g.:\n  -- auto_start = { filetypes = { \"html\" }, port = 8000 },\n\n  live_reload = {\n    enabled       = true,            -- watch files under the served root\n    inject_script = true,            -- injects \u003cscript src=\"/__live/script.js\"\u003e\n    debounce      = 120,             -- ms debounce for rapid changes\n    css_inject    = true,            -- hot-swap CSS without full page reload\n  },\n\n  directory_listing = {\n    enabled     = true,              -- render an index page if no index.html\n    show_hidden = false,             -- include dotfiles in listing\n  },\n}\n```\n\n---\n\n## Features\n\n### CSS hot-inject\n\nWhen `css_inject` is enabled (default), editing a `.css` file triggers an instant stylesheet swap in the browser — no full page reload, no DOM state lost. All other file changes still trigger a full reload.\n\n### Auto-start\n\nSet `auto_start` to automatically start a server when you open a matching filetype:\n\n```lua\nauto_start = { filetypes = { \"html\" }, port = 8000 }\n```\n\nThe server starts once per directory — opening another HTML file in the same folder won't spawn a duplicate.\n\n### `.liveignore`\n\nCreate a `.liveignore` file in your served root to skip file-watcher noise. One pattern per line, `*` as wildcard, `#` for comments:\n\n```\n# Don't reload on these\nnode_modules\n*.log\n.git\ndist\n```\n\n### CORS\n\nEnable cross-origin headers for all responses:\n\n```lua\ncors = true,                         -- Access-Control-Allow-Origin: *\ncors = \"http://localhost:3000\",      -- specific origin\n```\n\nUseful when your frontend (on the live server) makes API calls to a separate backend.\n\n### Statusline\n\nShow active servers in lualine or any statusline:\n\n```lua\n-- lualine example\nsections = {\n  lualine_x = {\n    { require(\"live_server\").statusline },\n  },\n}\n```\n\nReturns `\"[LS :8000]\"` when a server is running, or `\"\"` when idle.\n\n### Styled error pages\n\n404 and 400 errors display a clean, dark-mode-aware HTML page instead of raw text — easier to spot during development.\n\n---\n\n## Keymaps (default)\n\nAll under the which-key group **`\u003cleader\u003el`**:\n\n| Key          | Action                         |\n| ------------ | ------------------------------ |\n| `\u003cleader\u003els` | Start (pick path \u0026 port)       |\n| `\u003cleader\u003elo` | Open existing port in browser  |\n| `\u003cleader\u003elr` | Force reload (pick port)       |\n| `\u003cleader\u003elt` | Toggle live-reload (pick port) |\n| `\u003cleader\u003eli` | Show server status             |\n| `\u003cleader\u003elS` | Stop one (pick port)           |\n| `\u003cleader\u003elA` | Stop all                       |\n\n\u003e We register only the **group label** in `init`, and return actual mappings in `keys` — the recommended pattern for Folke's ecosystem to avoid conflicts and enable lazy-loading on keypress.\n\n---\n\n## Design notes\n\n* **Local by default**: binds to `127.0.0.1`. If you want LAN, you can change the bind address in `server.lua` (not recommended for security).\n* **Path safety**: requests are realpath-checked to prevent escaping the served root.\n* **Index resolution**: root directory → `default_index` (if starting from a file) → `index_names` in order → directory listing. Subdirectories always use their own index files.\n* **Port 0 (OS-assigned)**: pass `port = 0` to let the OS pick a free port. The actual port is available via `inst.port` after `server.start()`.\n* **Same port, new path**: reusing the same port retargets the server → same URL, so browsers typically reuse the same tab.\n* **Event injection**: `GET /__live/inject?event=\u003ctype\u003e\u0026data=\u003cjson\u003e` lets external processes broadcast SSE events to connected clients.\n* **Graceful exit**: all servers are automatically stopped on `VimLeavePre`.\n\n---\n\n## Troubleshooting\n\n* **\"Port in use or failed to bind\"**\n  Another process is using that port (or a previous server didn't exit cleanly). Pick a different port, or stop the other process.\n  You can stop live-server instances via `:LiveServerStop` or `:LiveServerStopAll`.\n\n* **\"start() bad argument #2 to 'start' (table expected, got number)\"**\n  Some `luv` builds expect `fs_event:start(path, {recursive=true}, cb)` while others accept `start(path, cb)`. The plugin tries both. Make sure you're on the **latest** plugin files.\n\n* **Browser didn't open**\n  We try `vim.ui.open` (NVIM 0.10) and fall back to `xdg-open`/`open`/`start`. If none work, copy the URL from the message and open manually.\n\n* **Live-reload didn't trigger**\n\n  * It only injects into **HTML** pages.\n  * Ensure the served root actually changed (the watcher is per root).\n  * Check `.liveignore` isn't excluding the file.\n  * Try `:LiveServerToggleLive` off/on, or `:LiveServerReload` to force.\n\n---\n\n## API (for lua configs)\n\n```lua\nlocal ls = require(\"live_server\")\n\nls.setup({ ... })                -- configure defaults\nls.start_picker()                -- UI flow: pick path, then port\nls.open_existing()               -- pick a port → open in browser\nls.force_reload()                -- broadcast reload to clients\nls.toggle_livereload()           -- enable/disable live-reload for a port\nls.status()                      -- print running server info\nls.statusline()                  -- returns \"[LS :8000]\" or \"\"\nls.stop_one()                    -- pick a port → stop\nls.stop_all()                    -- stop everything\n```\n\n### Server-level API (for plugin authors)\n\n```lua\nlocal server = require(\"live_server.server\")\n\nlocal inst = server.start({ port = 0, root = \"/path\", ... })  -- port 0 = OS-assigned\nserver.send_event(inst, \"scroll\", '{\"line\":42}')               -- broadcast custom SSE event\nserver.reload(inst, \"file.html\")                                -- broadcast reload event\nserver.update_target(inst, new_root, new_index)                 -- retarget without restart\nserver.connected_client_count(inst)                             -- number of SSE clients\nserver.stop(inst)                                               -- shut down\n```\n\n### HTTP event injection\n\nExternal processes can inject SSE events via HTTP:\n\n```\nGET /__live/inject?event=\u003ctype\u003e\u0026data=\u003curl-encoded-json\u003e\n```\n\nThis broadcasts the event to all connected SSE clients. Used by [markdown-preview.nvim](https://github.com/selimacerbas/markdown-preview.nvim) for cross-instance scroll sync.\n\n---\n\n## Roadmap\n\n* Optional LAN binding with allowlist.\n* Pluggable middlewares (custom headers, rewrites).\n* Directory listing customization (sorting, columns).\n\n---\n\n## Contributing\n\nPRs and issues are welcome!\nPlease include your **OS**, **Neovim version**, and (if relevant) **`vim.loop`/luv** version when reporting bugs. Repro steps make fixes fast.\n\n---\n\n## License\n\nMIT © Selim Acerbaş\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fselimacerbas%2Flive-server.nvim","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fselimacerbas%2Flive-server.nvim","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fselimacerbas%2Flive-server.nvim/lists"}