diff --git a/README.md b/README.md index 0192588..1364f83 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,393 @@ -# volt +# โšก Volt -Volt is a Neovim plugin to create interactive UIs within Neovim! +**Create blazing fast & beautiful reactive UIs in Neovim** +Volt is a powerful framework for building interactive, clickable, and hoverable user interfaces directly within Neovim using virtual text and extmarks. Built by [siduck](https://github.com/siduck) for the [NvChad](https://nvchad.com) ecosystem. -# Showcase +![License](https://img.shields.io/badge/license-GPL--3.0-blue.svg) +![Neovim](https://img.shields.io/badge/Neovim-0.9+-green.svg) -Plugins made using volt :zap: +## ๐Ÿ“‘ Table of Contents -## Minty +- [Features](#-features) +- [Showcase](#-showcase) +- [Installation](#-installation) +- [Quick Start](#-quick-start) +- [Documentation](#-documentation) +- [Core Components](#-core-components) +- [Color Utilities](#-color-utilities) +- [Building Your UI](#-building-your-ui) +- [Advanced Features](#-advanced-features) +- [Key Mappings](#-key-mappings) +- [Contributing](#-contributing) +- [License](#-license) +- [Credits](#-credits) +- [Support](#-support) -Beautifully crafted color tools for Neovim. [repo](https://github.com/NvChad/minty) +## โœจ Features -![shades](https://github.com/user-attachments/assets/d499748b-d9c8-4a92-89ba-bfce1814c275) -![huefy](https://github.com/user-attachments/assets/21f2c23d-94c6-4ccf-a0d0-ddf91f6bb5c1) +- **๐ŸŽจ Rich UI Components**: Sliders, checkboxes, tables, tabs, progress bars, and graphs +- **๐Ÿ–ฑ๏ธ Interactive Elements**: Full mouse and keyboard support for clickable/hoverable UI +- **โšก Blazing Fast**: Leverages Neovim's native virtual text (extmarks) for rendering +- **๐ŸŽฏ Event-Driven**: Simple callback-based system for handling user interactions +- **๐Ÿ“Š Data Visualization**: Built-in bar and dot graph components +- **๐ŸŽจ Themeable**: Automatically adapts to your Neovim colorscheme +- **๐Ÿ”ง Modular**: Composable components for building complex UIs -## Menu +## ๐Ÿ“ธ Showcase -Menu creator plugin for Neovim. [repo](https://github.com/NvChad/menu) +### Plugins Built with Volt -![image](https://github.com/user-attachments/assets/c8402279-b86d-432f-ad11-14a76c887ab1) +#### [Minty](https://github.com/NvChad/minty) - Color Tools +Beautiful color picker, shade generator, and huefy tools. -## Typr +![Minty Shades](https://github.com/user-attachments/assets/d499748b-d9c8-4a92-89ba-bfce1814c275) +![Minty Huefy](https://github.com/user-attachments/assets/21f2c23d-94c6-4ccf-a0d0-ddf91f6bb5c1) -A Neovim plugin for practice typing with a very beautiful dashboard. [repo](https://github.com/NvChad/typr) +#### [Menu](https://github.com/NvChad/menu) - Menu Creator +Extensible menu and submenu system. -![typr](https://github.com/user-attachments/assets/4426d1c4-c4d3-4da7-987a-3b4c4395a4b5) -![typrstats](https://github.com/user-attachments/assets/b1653de3-05f3-4b90-b35e-9341eed8bf3e) +![Menu](https://github.com/user-attachments/assets/c8402279-b86d-432f-ad11-14a76c887ab1) -## NvChad's Theme Picker +#### [Typr](https://github.com/NvChad/typr) - Typing Practice +Beautiful typing practice with stats dashboard. -Base46 Theme picker with 3 different styles. [repo](https://github.com/NvChad/base46) +![Typr](https://github.com/user-attachments/assets/4426d1c4-c4d3-4da7-987a-3b4c4395a4b5) +![Typr Stats](https://github.com/user-attachments/assets/b1653de3-05f3-4b90-b35e-9341eed8bf3e) -![image](https://github.com/user-attachments/assets/897e46f1-9ae2-4cc2-8fa2-64eff40a90dd) +#### [Base46 Theme Picker](https://github.com/NvChad/base46) +Interactive theme picker with multiple styles. +![Theme Picker](https://github.com/user-attachments/assets/897e46f1-9ae2-4cc2-8fa2-64eff40a90dd) -# Docs to be added soon before 2026 +## ๐Ÿ“ฆ Installation + +### Using [lazy.nvim](https://github.com/folke/lazy.nvim) + +```lua +{ + "NvChad/volt", + lazy = true, +} +``` + +### Using [packer.nvim](https://github.com/wbthomason/packer.nvim) + +```lua +use { + "NvChad/volt", + opt = true, +} +``` + +### Using Neovim's built-in `vim.pack` + +```lua +vim.pack.add({ + { src = "https://github.com/NvChad/volt" }, +}) +``` + +## ๐Ÿš€ Quick Start + +Here's a minimal example to create a simple interactive UI: + +```lua +local volt = require("volt") + +-- Create a buffer for your UI +local buf = vim.api.nvim_create_buf(false, true) + +-- Define your UI layout +local layout = { + { + name = "header", + lines = function() + return { + { { "Hello from Volt! โšก", "Title" } } + } + end + } +} + +-- Generate the UI data +volt.gen_data({ + { + buf = buf, + layout = layout, + ns = vim.api.nvim_create_namespace("my_volt_ui"), + xpad = 2, -- horizontal padding + } +}) + +-- Create a window and show the UI +local win = vim.api.nvim_open_win(buf, true, { + relative = "editor", + width = 60, + height = 10, + row = 5, + col = 5, + style = "minimal", + border = "rounded", +}) + +-- Render the UI +volt.run(buf, { h = 10, w = 60 }) +``` + +## ๐Ÿ“š Documentation + +- **[User Guide](docs/USER_GUIDE.md)**: Learn how to use Volt components +- **[Developer Guide](docs/DEVELOPER_GUIDE.md)**: Build your own UI with Volt +- **[API Reference](docs/API.md)**: Complete API documentation +- **`:help volt`**: Vim help documentation (after installation) + +## ๐Ÿงฉ Core Components + +Volt provides a rich set of UI components out of the box: + +### Basic Components +- **Checkbox**: Toggle states with custom icons +- **Slider**: Interactive value selector with mouse/keyboard support +- **Progress Bar**: Visual progress indicators +- **Separator**: Horizontal dividers +- **Tabs**: Tabbed interface navigation + +### Layout Components +- **Grid Column**: Multi-column layouts +- **Grid Row**: Row-based layouts +- **Border**: Add borders around content +- **Horizontal Padding**: Dynamic padding system + +### Data Visualization +- **Bar Graph**: Vertical bar charts with customization +- **Dot Graph**: Scatter-style data visualization +- **Table**: Formatted tables with borders and alignment + +### Utilities +- **Color Utilities**: HSL/RGB/HEX conversions, color mixing, gradients +- **Event System**: Mouse and keyboard event handling +- **State Management**: Buffer-specific state tracking + +## ๐ŸŽจ Color Utilities + +Volt includes comprehensive color manipulation functions: + +```lua +local color = require("volt.color") + +-- Convert between formats +local r, g, b = color.hex2rgb("#ff5555") +local hex = color.rgb2hex(255, 85, 85) +local h, s, l = color.hex2hsl("#ff5555") + +-- Modify colors +local lighter = color.change_hex_lightness("#ff5555", 20) -- lighten by 20% +local saturated = color.change_hex_saturation("#ff5555", -30) -- desaturate +local shifted = color.change_hex_hue("#ff5555", 45) -- shift hue + +-- Mix colors +local mixed = color.mix("#ff5555", "#5555ff", 50) -- 50/50 mix + +-- Generate gradients +local gradient = color.compute_gradient("#ff5555", "#5555ff", 10) + +-- Complementary colors +local complements = color.hex2complementary("#ff5555", 3) +``` + +## ๐Ÿ› ๏ธ Building Your UI + +### 1. Define Your Layout + +```lua +local layout = { + { + name = "section1", + lines = function(buf) + return { + { + { "Text", "Highlight" }, + { " More text", "Normal" }, + { + " Clickable", + "Special", + function() print("Clicked!") end -- Click handler + } + } + } + end + } +} +``` + +### 2. Create Interactive Elements + +```lua +local ui = require("volt.ui") + +-- Slider +local slider = ui.slider.config({ + txt = "Volume: ", + val = 50, + w = 40, + hlon = "String", + hloff = "Comment", + thumb = true, + ratio_txt = true, + actions = function() + local new_val = ui.slider.val(40, "Volume: ", 2) + print("New value:", new_val) + end +}) + +-- Checkbox +local checkbox = ui.checkbox({ + active = true, + txt = "Enable feature", + check = "โœ“", + uncheck = "โœ—", + hlon = "String", + hloff = "Comment", + actions = function() + -- Toggle logic + end +}) + +-- Table +local table_lines = ui.table({ + { "Name", "Age", "City" }, + { "Alice", "25", "NYC" }, + { "Bob", "30", "LA" } +}, 60, "Title") +``` + +### 3. Enable Events + +```lua +-- Enable mouse and keyboard events +require("volt.events").enable() + +-- Register your buffer(s) +require("volt.events").add(buf) +``` + +## ๐Ÿ”ง Advanced Features + +### Hover Effects + +```lua +{ + "Hoverable text", + "Normal", + { + click = function() print("Clicked") end, + hover = { + id = "my_hover", + redraw = { "section_to_redraw" }, + callback = function() + vim.g.nvmark_hovered = "my_hover" + end + } + } +} +``` + +### Dynamic Redraws + +```lua +-- Redraw specific sections +volt.redraw(buf, "section_name") + +-- Redraw multiple sections +volt.redraw(buf, { "section1", "section2" }) + +-- Redraw everything +volt.redraw(buf, "all") +``` + +### Graphs + +```lua +local graphs = require("volt.ui.graphs") + +-- Bar graph +local bar_graph = graphs.bar({ + val = { 5, 8, 3, 10, 7 }, + baropts = { + w = 3, + gap = 1, + icon = "โ–ˆ", + hl = "String", + dual_hl = { "Comment", "String" }, -- inactive/active colors + }, + format_labels = function(val) + return tostring(val) .. "%" + end, + footer_label = { "Performance", "Title" } +}) + +-- Dot graph +local dot_graph = graphs.dot({ + val = { 5, 8, 3, 10, 7 }, + baropts = { + icons = { on = " ๓ฐ„ฐ", off = " ยท" }, + hl = { on = "String", off = "Comment" }, + sidelabels = true, + } +}) +``` + +## ๐ŸŽฏ Key Mappings + +When using Volt UIs, the following keys are automatically mapped: + +- **``**: Activate the element under cursor +- **``**: Cycle to next clickable element +- **``**: Cycle to previous clickable element +- **`q` / ``**: Close the UI +- **``**: Cycle between multiple buffers (if configured) + +## ๐Ÿค Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change. + +1. Fork the repository +2. Create your feature branch (`git checkout -b feature/amazing-feature`) +3. Commit your changes (`git commit -m 'Add some amazing feature'`) +4. Push to the branch (`git push origin feature/amazing-feature`) +5. Open a Pull Request + +## ๐Ÿ“ License + +This project is licensed under the GNU General Public License v3.0 - see the [LICENSE](LICENSE) file for details. + +## ๐Ÿ™ Credits + +- **Creator**: [Siduck](https://github.com/siduck) +- **Color utilities**: Based on work by [Leon Heidelbach](https://github.com/LeonHeidelbach) +- **Part of**: [NvChad](https://nvchad.com) ecosystem + +## ๐Ÿ”— Related Projects + +- [NvChad](https://github.com/NvChad/NvChad) - Blazing fast Neovim framework +- [Minty](https://github.com/NvChad/minty) - Color tools built with Volt +- [Menu](https://github.com/NvChad/menu) - Menu system built with Volt +- [Typr](https://github.com/NvChad/typr) - Typing practice built with Volt + +## ๐Ÿ“ž Support + +- ๐Ÿ› [Report Bugs](https://github.com/NvChad/volt/issues) +- ๐Ÿ’ก [Feature Requests](https://github.com/NvChad/volt/issues) +- ๐Ÿ’ฌ [Discord](https://discord.gg/gADmkJb9Fb) - Join the NvChad community +- ๐Ÿ“– [Documentation](https://nvchad.com) + +--- + +
+ +**[โญ Star this repository](https://github.com/NvChad/volt) if you find it useful!** + +Made with โšก by the NvChad team + +
diff --git a/doc/volt.txt b/doc/volt.txt new file mode 100644 index 0000000..a9e40a0 --- /dev/null +++ b/doc/volt.txt @@ -0,0 +1,785 @@ +*volt.txt* Create blazing fast & beautiful reactive UIs in Neovim + +============================================================================== +CONTENTS *volt-contents* + + 1. Introduction ............................ |volt-introduction| + 2. Installation ............................ |volt-installation| + 3. Quick Start ............................. |volt-quickstart| + 4. Core Functions .......................... |volt-functions| + 5. UI Components ........................... |volt-components| + 6. Event System ............................ |volt-events| + 7. Color Utilities ......................... |volt-color| + 8. Configuration ........................... |volt-config| + 9. Examples ................................ |volt-examples| + 10. Troubleshooting ......................... |volt-troubleshooting| + 11. About ................................... |volt-about| + +============================================================================== +INTRODUCTION *volt-introduction* + +Volt is a framework for creating interactive, clickable, and hoverable user +interfaces directly within Neovim using virtual text and extmarks. + +Features:~ + โ€ข Rich UI components (sliders, checkboxes, tables, graphs) + โ€ข Full mouse and keyboard support + โ€ข Event-driven architecture + โ€ข Blazing fast rendering with extmarks + โ€ข Automatic theme adaptation + โ€ข Modular and composable + +============================================================================== +INSTALLATION *volt-installation* + +Using lazy.nvim:~ +>lua + { + "NvChad/volt", + lazy = true, + } +< + +Using packer.nvim:~ +>lua + use { + "NvChad/volt", + opt = true, + } +< + +Using Neovim built-in `vim.pack`:~ +>lua + vim.pack.add({ + { src = "https://github.com/nvzone/volt" }, + }) +< + +============================================================================== +QUICK START *volt-quickstart* + +Minimal example:~ +>lua + local volt = require("volt") + + -- Create buffer + local buf = vim.api.nvim_create_buf(false, true) + + -- Define layout + local layout = { + { + name = "greeting", + lines = function() + return { + { { "Hello, Volt! โšก", "Title" } } + } + end + } + } + + -- Setup + volt.gen_data({{ + buf = buf, + layout = layout, + ns = vim.api.nvim_create_namespace("my_ui"), + xpad = 2, + }}) + + -- Create window + local win = vim.api.nvim_open_win(buf, true, { + relative = "editor", + width = 40, + height = 5, + row = 10, + col = 10, + style = "minimal", + border = "rounded", + }) + + -- Render + volt.run(buf, { h = 5, w = 40 }) +< + +============================================================================== +CORE FUNCTIONS *volt-functions* + +volt.gen_data({config}) *volt.gen_data()* + Generate state and calculate layout for UI buffers. + + Parameters:~ + {config} List of buffer configurations + + Config structure:~ + { + buf = buffer_id, + layout = layout_table, + ns = namespace_id, + xpad = horizontal_padding + } + + Example:~ +>lua + volt.gen_data({ + { + buf = buf, + layout = my_layout, + ns = vim.api.nvim_create_namespace("myui"), + xpad = 2 + } + }) +< + +volt.run({buf}, {opts}) *volt.run()* + Render UI to buffer and setup event handlers. + + Parameters:~ + {buf} Buffer ID + {opts} Options table + + Options:~ + {h} Height in lines + {w} Width in columns + {custom_empty_lines} Optional function to setup empty lines + {winclosed_event} Auto-close on window close (boolean) + + Example:~ +>lua + volt.run(buf, { + h = 30, + w = 80, + winclosed_event = true + }) +< + +volt.redraw({buf}, {sections}) *volt.redraw()* + Redraw specific sections or entire UI. + + Parameters:~ + {buf} Buffer ID + {sections} Section name (string), list of names, or "all" + + Examples:~ +>lua + volt.redraw(buf, "header") + volt.redraw(buf, { "header", "content" }) + volt.redraw(buf, "all") +< + +volt.set_empty_lines({buf}, {n}, {w}) *volt.set_empty_lines()* + Setup empty lines in buffer for rendering. + + Parameters:~ + {buf} Buffer ID + {n} Number of lines + {w} Width of each line + +volt.mappings({config}) *volt.mappings()* + Setup default keymaps for buffers. + + Config:~ + {bufs} List of buffer IDs + {winclosed_event} Enable WinClosed autocmd (boolean) + + Default mappings:~ + Activate element under cursor + Cycle to next clickable + Cycle to previous clickable + q Close UI + Close UI + Cycle buffers (if multiple) + +volt.toggle_func({open_fn}, {state}) *volt.toggle_func()* + Helper for toggling UI visibility. + + Parameters:~ + {open_fn} Function to open UI + {state} Current state (boolean) + +volt.close({buf}) *volt.close()* + Close UI buffer. + + Parameters:~ + {buf} Buffer ID (optional, defaults to current) + +============================================================================== +UI COMPONENTS *volt-components* + +volt.ui *volt.ui* + +The UI module provides pre-built components. + +------------------------------------------------------------------------------ +CHECKBOX *volt.ui.checkbox* + +volt.ui.checkbox({opts}) + Create a toggleable checkbox. + + Options:~ + {active} Current state (boolean) + {txt} Label text (string) + {check} Custom checked icon (string, default: "") + {uncheck} Custom unchecked icon (string, default: "") + {hlon} Active highlight (string) + {hloff} Inactive highlight (string) + {actions} Click handler (function) + + Returns:~ + Virtual text table for use in layout + + Example:~ +>lua + local ui = require("volt.ui") + + local checkbox = ui.checkbox({ + active = true, + txt = "Dark mode", + check = "โœ“", + uncheck = "โœ—", + hlon = "String", + hloff = "Comment", + actions = function() + -- Toggle logic + end + }) +< + +------------------------------------------------------------------------------ +SLIDER *volt.ui.slider* + +volt.ui.slider.config({opts}) + Create an interactive slider. + + Options:~ + {txt} Label text (string) + {val} Current value 0-100 (number) + {w} Width (number) + {hlon} Active highlight (string) + {hloff} Inactive highlight (string) + {thumb} Show thumb indicator (boolean) + {thumb_icon} Custom thumb icon (string) + {ratio_txt} Show percentage (boolean) + {actions} Value change handler (function) + + Example:~ +>lua + local slider = ui.slider.config({ + txt = "Volume: ", + val = 50, + w = 40, + hlon = "String", + hloff = "Comment", + thumb = true, + ratio_txt = true, + actions = function() + local new_val = ui.slider.val(40, "Volume: ", 2) + print("New value:", new_val) + end + }) +< + +volt.ui.slider.val({w}, {left_txt}, {xpad}, {opts}) + Get slider value based on cursor position. + + Parameters:~ + {w} Slider width + {left_txt} Label text + {xpad} Horizontal padding + {opts} Options table + + Returns:~ + Value between 0-100 + +------------------------------------------------------------------------------ +PROGRESSBAR *volt.ui.progressbar* + +volt.ui.progressbar({opts}) + Create a progress bar. + + Options:~ + {w} Width (number) + {val} Progress 0-100 (number) + {icon} Icon table: { on = "โ”", off = "โ”€" } + {hl} Highlight table: { on = "String", off = "Comment" } + + Example:~ +>lua + local progress = ui.progressbar({ + w = 30, + val = 65, + icon = { on = "โ”", off = "โ”€" }, + hl = { on = "String", off = "Comment" } + }) +< + +------------------------------------------------------------------------------ +SEPARATOR *volt.ui.separator* + +volt.ui.separator({char}, {w}, {hl}) + Create a horizontal separator. + + Parameters:~ + {char} Character to use (default: "โ”€") + {w} Width + {hl} Highlight group (default: "linenr") + +------------------------------------------------------------------------------ +TABLE *volt.ui.table* + +volt.ui.table({data}, {w}, {header_hl}, {title}) + Create a formatted table with borders. + + Parameters:~ + {data} 2D array of table data + {w} Total width (or "fit" for auto-width) + {header_hl} Highlight for header row + {title} Optional title virtual text + + Example:~ +>lua + local tbl = ui.table( + { + { "Name", "Age", "City" }, + { "Alice", "25", "NYC" }, + { "Bob", "30", "LA" } + }, + 60, + "Title" + ) +< + +------------------------------------------------------------------------------ +TABS *volt.ui.tabs* + +volt.ui.tabs({data}, {w}, {opts}) + Create tabbed interface. + + Parameters:~ + {data} Array of tab names + {w} Total width + {opts} Options table + + Options:~ + {active} Currently active tab + {hlon} Active highlight + {hloff} Inactive highlight + + Example:~ +>lua + local tabs = ui.tabs( + { "Home", "Settings", "About" }, + 60, + { active = "Settings", hlon = "Title" } + ) +< + +------------------------------------------------------------------------------ +GRAPHS *volt.ui.graphs* + +volt.ui.graphs.bar({data}) *volt.ui.graphs.bar* + Create a bar graph. + + Data structure:~ + { + val = { 5, 8, 3, 10 }, -- Values (0-10 scale) + baropts = { + w = 3, -- Bar width + gap = 1, -- Gap between bars + icon = "โ–ˆ", -- Bar character + hl = "String", -- Highlight + dual_hl = { "Comment", "String" }, -- Or dual colors + format_hl = function(val) end -- Or dynamic hl + }, + format_labels = function(val) end, + footer_label = { "Label", "HighlightGroup" } + } + +volt.ui.graphs.dot({data}) *volt.ui.graphs.dot* + Create a dot graph. + + Data structure:~ + { + val = { 5, 8, 3, 10 }, + baropts = { + icons = { on = " ๓ฐ„ฐ", off = " ยท" }, + hl = { on = "String", off = "Comment" }, + sidelabels = true, + format_icon = function(val) end + } + } + +------------------------------------------------------------------------------ +LAYOUT COMPONENTS *volt.ui.layout* + +volt.ui.grid_col({columns}) *volt.ui.grid_col* + Arrange components in columns. + + Parameters:~ + {columns} Array of column configs + + Column config:~ + { + lines = line_table, + w = width, + pad = right_padding + } + +volt.ui.grid_row({parts}) *volt.ui.grid_row* + Concatenate multiple line groups horizontally. + +volt.ui.border({lines}, {hl}) *volt.ui.border* + Add border around content. Modifies lines in-place. + +volt.ui.hpad({line}, {w}) *volt.ui.hpad* + Add horizontal padding to reach width. Replace "_pad_" markers. + +volt.ui.line_w({line}) *volt.ui.line_w* + Calculate line width from virtual text. + +============================================================================== +EVENT SYSTEM *volt-events* + +volt.events *volt.events* + +The event system handles mouse and keyboard interactions. + +volt.events.enable() *volt.events.enable()* + Enable global event system. Call once in your config. +>lua + require("volt.events").enable() +< + +volt.events.add({buf}) *volt.events.add()* + Register buffer(s) for event handling. + + Parameters:~ + {buf} Buffer ID or array of IDs + + Example:~ +>lua + local events = require("volt.events") + events.add(buf) + events.add({ buf1, buf2, buf3 }) +< + +------------------------------------------------------------------------------ +VIRTUAL TEXT ACTIONS *volt-text-actions* + +Virtual text can have click and hover handlers: + +Click only:~ +>lua + { + "Click me", + "Highlight", + function() + print("Clicked!") + end + } +< + +Click and hover:~ +>lua + { + "Hover me", + "Normal", + { + click = function() + print("Clicked!") + end, + hover = { + id = "unique_id", + callback = function() + vim.g.nvmark_hovered = "unique_id" + end, + redraw = { "section_to_update" } + } + } + } +< + +Check hover state:~ +>lua + if vim.g.nvmark_hovered == "unique_id" then + -- Element is hovered + end +< + +============================================================================== +COLOR UTILITIES *volt-color* + +volt.color *volt.color* + +Comprehensive color manipulation utilities. + +volt.color.hex2rgb({hex}) *volt.color.hex2rgb* + Convert hex color to RGB. + + Returns:~ + r, g, b (0-255) + +volt.color.rgb2hex({r}, {g}, {b}) *volt.color.rgb2hex* + Convert RGB to hex color. + +volt.color.hex2hsl({hex}) *volt.color.hex2hsl* + Convert hex to HSL. + + Returns:~ + h (0-360), s (0-1), l (0-1) + +volt.color.hsl2hex({h}, {s}, {l}) *volt.color.hsl2hex* + Convert HSL to hex color. + +volt.color.change_hex_lightness({hex}, {percent}) + *volt.color.change_hex_lightness* + Lighten or darken color. + + Parameters:~ + {hex} Hex color + {percent} Amount (-100 to 100) + +volt.color.change_hex_saturation({hex}, {percent}) + *volt.color.change_hex_saturation* + Change color saturation. + +volt.color.change_hex_hue({hex}, {percent}) *volt.color.change_hex_hue* + Shift color hue. + +volt.color.mix({first}, {second}, {strength}) *volt.color.mix* + Mix two colors. + + Parameters:~ + {first} Primary color + {second} Color to mix in + {strength} Amount of second color (0-100) + +volt.color.compute_gradient({hex1}, {hex2}, {steps}) + *volt.color.compute_gradient* + Generate color gradient. + + Returns:~ + Array of hex colors + +volt.color.hex2complementary({hex}, {count}) + *volt.color.hex2complementary* + Generate complementary colors. + + Returns:~ + Array of hex colors + +Example:~ +>lua + local color = require("volt.color") + + local lighter = color.change_hex_lightness("#ff5555", 20) + local mixed = color.mix("#ff5555", "#5555ff", 50) + local gradient = color.compute_gradient("#ff5555", "#5555ff", 10) +< + +============================================================================== +CONFIGURATION *volt-config* + +Volt works out of the box but can be customized. + +Highlight Groups:~ + ExDarkBg, ExDarkBorder + ExBlack2Bg, ExBlack2Border + ExBlack3Bg, ExBlack3Border + ExRed, ExYellow, ExBlue, ExGreen + ExLightGrey, CommentFg + +These automatically adapt to your colorscheme or NvChad's base46. + +Custom highlights:~ +>lua + vim.api.nvim_set_hl(0, "MyCustomHL", { + fg = "#ff5555", + bg = "#282a36" + }) +< + +State Access:~ +>lua + local state = require("volt.state") + + -- Access buffer state + local buf_state = state[buf] + + -- Available fields: + -- buf_state.clickables + -- buf_state.hoverables + -- buf_state.layout + -- buf_state.ns + -- buf_state.xpad + -- buf_state.h +< + +============================================================================== +EXAMPLES *volt-examples* + +Simple Dialog:~ +>lua + local function show_dialog() + local volt = require("volt") + local ui = require("volt.ui") + local buf = vim.api.nvim_create_buf(false, true) + + local layout = { + { + name = "message", + lines = function() + return { + { { "Are you sure?", "Title" } }, + { { "", "Normal" } }, + { + { " Yes ", "String", function() + print("Confirmed") + volt.close(buf) + end }, + { " ", "Normal" }, + { " No ", "Error", function() + volt.close(buf) + end } + } + } + end + } + } + + volt.gen_data({{ + buf = buf, + layout = layout, + ns = vim.api.nvim_create_namespace("dialog"), + xpad = 2 + }}) + + vim.api.nvim_open_win(buf, true, { + relative = "editor", + width = 40, + height = 5, + row = 10, + col = 10, + style = "minimal", + border = "rounded" + }) + + volt.run(buf, { h = 5, w = 40 }) + require("volt.events").add(buf) + end +< + +Interactive Settings:~ +>lua + local function show_settings() + local volt = require("volt") + local ui = require("volt.ui") + local buf = vim.api.nvim_create_buf(false, true) + + local settings = { volume = 50, notifications = true } + + local layout = { + { + name = "title", + lines = function() + return { { { "Settings", "Title" } } } + end + }, + { + name = "volume", + lines = function() + return { + ui.slider.config({ + txt = "Volume: ", + val = settings.volume, + w = 40, + hlon = "String", + thumb = true, + ratio_txt = true, + actions = function() + settings.volume = ui.slider.val(40, "Volume: ", 2) + volt.redraw(buf, "volume") + end + }) + } + end + }, + { + name = "notifications", + lines = function() + return { + ui.checkbox({ + active = settings.notifications, + txt = "Notifications", + actions = function() + settings.notifications = not settings.notifications + volt.redraw(buf, "notifications") + end + }) + } + end + } + } + + volt.gen_data({{ buf = buf, layout = layout, + ns = vim.api.nvim_create_namespace("settings"), xpad = 2 }}) + + vim.api.nvim_open_win(buf, true, { + relative = "editor", width = 50, height = 10, + row = 5, col = 10, style = "minimal", border = "rounded" + }) + + volt.run(buf, { h = 10, w = 50 }) + require("volt.events").add(buf) + end +< + +============================================================================== +TROUBLESHOOTING *volt-troubleshooting* + +Clicks not working?~ + โ€ข Ensure events are enabled: `require("volt.events").enable()` + โ€ข Register your buffer: `require("volt.events").add(buf)` + โ€ข Check mouse: `:set mouse=a` + +UI not rendering?~ + โ€ข Verify buffer is valid: `vim.api.nvim_buf_is_valid(buf)` + โ€ข Ensure window is visible + โ€ข Confirm `volt.run(buf, opts)` was called + โ€ข Check layout functions return proper format + +Colors look wrong?~ + โ€ข Volt highlights load automatically + โ€ข Ensure colorscheme is set before loading Volt + โ€ข Check available highlights: `:so $VIMRUNTIME/syntax/hitest.vim` + +Layout issues?~ + โ€ข Verify layout structure matches documentation + โ€ข Ensure lines functions return arrays of arrays + โ€ข Check virtual text format: `{ text, hl, action }` + +Performance issues?~ + โ€ข Minimize redraws - redraw only changed sections + โ€ข Use specific section names instead of "all" + โ€ข Consider lazy rendering for large lists + +============================================================================== +ABOUT *volt-about* + +Volt is created by Siduck (https://github.com/siduck) as part of the NvChad +ecosystem (https://nvchad.com). + +Color utilities based on work by Leon Heidelbach. + +License: GNU General Public License v3.0 + +Links:~ + GitHub: https://github.com/NvChad/volt + Discord: https://discord.gg/gADmkJb9Fb + Website: https://nvchad.com + +Plugins built with Volt:~ + โ€ข Minty - Color tools (https://github.com/NvChad/minty) + โ€ข Menu - Menu system (https://github.com/NvChad/menu) + โ€ข Typr - Typing practice (https://github.com/NvChad/typr) + โ€ข Base46 - Theme picker (https://github.com/NvChad/base46) + + vim:tw=78:ts=8:noet:ft=help:norl: diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..7c9c93d --- /dev/null +++ b/docs/API.md @@ -0,0 +1,1228 @@ +# Volt API Reference + +Complete technical reference for the Volt framework. + +## Table of Contents + +- [Core API](#core-api) +- [UI Components](#ui-components) +- [Event System](#event-system) +- [Color Utilities](#color-utilities) +- [State Management](#state-management) +- [Utilities](#utilities) +- [Type Definitions](#type-definitions) + +## Core API + +### `volt` Module + +Main entry point for Volt functionality. + +```lua +local volt = require("volt") +``` + +--- + +#### `volt.gen_data(config)` + +Generate UI state and calculate layout positions for buffers. + +**Parameters:** +- `config` (table[]): Array of buffer configurations + +**Config Structure:** +```lua +{ + buf = number, -- Buffer ID + layout = table, -- Layout definition (see Layout Structure) + ns = number, -- Namespace ID from nvim_create_namespace() + xpad = number -- Horizontal padding (optional, default: 0) +} +``` + +**Layout Structure:** +```lua +{ + { + name = string, -- Section identifier + row = number, -- Starting row (auto-calculated) + col_start = number, -- Column offset (optional) + lines = function(buf) -> table -- Function returning line data + }, + -- ... more sections +} +``` + +**Line Data Format:** +```lua +{ + { + { "text", "HighlightGroup" }, + { "more text", "AnotherHL" }, + { "clickable", "HL", callback_function } + }, + -- ... more lines +} +``` + +**Example:** +```lua +volt.gen_data({ + { + buf = my_buf, + layout = { + { + name = "header", + lines = function(buf) + return { + { { "Title", "Title" } } + } + end + } + }, + ns = vim.api.nvim_create_namespace("my_ns"), + xpad = 2 + } +}) +``` + +--- + +#### `volt.run(buf, opts)` + +Render UI to buffer and initialize event system. + +**Parameters:** +- `buf` (number): Buffer ID +- `opts` (table): Configuration options + +**Options:** +```lua +{ + h = number, -- Height in lines + w = number, -- Width in columns + custom_empty_lines = function, -- Optional: custom line setup function + winclosed_event = boolean -- Auto-close on WinClosed (default: false) +} +``` + +**Example:** +```lua +volt.run(buf, { + h = 30, + w = 80, + winclosed_event = true +}) +``` + +--- + +#### `volt.redraw(buf, sections)` + +Update specific sections of the UI. + +**Parameters:** +- `buf` (number): Buffer ID +- `sections` (string|string[]|"all"): Section(s) to redraw + +**Examples:** +```lua +-- Single section +volt.redraw(buf, "header") + +-- Multiple sections +volt.redraw(buf, { "header", "content", "footer" }) + +-- All sections +volt.redraw(buf, "all") +``` + +--- + +#### `volt.set_empty_lines(buf, n, w)` + +Create empty lines in buffer for rendering. + +**Parameters:** +- `buf` (number): Buffer ID +- `n` (number): Number of lines +- `w` (number): Width of each line + +**Example:** +```lua +volt.set_empty_lines(buf, 30, 80) +``` + +--- + +#### `volt.mappings(config)` + +Setup default keymaps for Volt buffers. + +**Parameters:** +- `config` (table): Mapping configuration + +**Config:** +```lua +{ + bufs = number[], -- Array of buffer IDs + winclosed_event = boolean -- Enable WinClosed autocmd (optional) +} +``` + +**Default Mappings:** +- ``: Activate element under cursor +- ``: Next clickable element +- ``: Previous clickable element +- `q`: Close UI +- ``: Close UI +- ``: Cycle buffers (if multiple) + +**Example:** +```lua +volt.mappings({ + bufs = { buf1, buf2 }, + winclosed_event = true +}) +``` + +--- + +#### `volt.toggle_func(open_fn, state)` + +Helper for UI toggle functionality. + +**Parameters:** +- `open_fn` (function): Function to open UI +- `state` (boolean): Current UI state + +**Example:** +```lua +local is_open = false + +function toggle_ui() + volt.toggle_func(open_ui, is_open) + is_open = not is_open +end +``` + +--- + +#### `volt.close(buf?)` + +Close UI buffer and cleanup. + +**Parameters:** +- `buf` (number|nil): Buffer ID (optional, uses current buffer if nil) + +**Example:** +```lua +volt.close(buf) +-- or +volt.close() -- closes current buffer +``` + +--- + +## UI Components + +### `volt.ui` Module + +Pre-built UI components. + +```lua +local ui = require("volt.ui") +``` + +--- + +### `ui.checkbox(opts)` + +Create a checkbox component. + +**Parameters:** +- `opts` (CheckboxOptions) + +**CheckboxOptions:** +```lua +{ + active = boolean, -- Current state + txt = string, -- Label text + check = string, -- Checked icon (default: "") + uncheck = string, -- Unchecked icon (default: "") + hlon = string, -- Active highlight (default: "String") + hloff = string, -- Inactive highlight (default: "ExInactive") + actions = function -- Click handler (optional) +} +``` + +**Returns:** `table` - Virtual text line + +**Example:** +```lua +local checkbox = ui.checkbox({ + active = true, + txt = "Enable feature", + check = "โœ“", + uncheck = "โœ—", + hlon = "String", + hloff = "Comment", + actions = function() + print("Toggled!") + end +}) +``` + +--- + +### `ui.slider.config(opts)` + +Create a slider component. + +**Parameters:** +- `opts` (SliderOptions) + +**SliderOptions:** +```lua +{ + txt = string, -- Label text (optional) + val = number, -- Current value (0-100) + w = number, -- Width + hlon = string, -- Active highlight + hloff = string, -- Inactive highlight (optional) + thumb = boolean, -- Show thumb indicator (optional) + thumb_icon = string, -- Custom thumb icon (optional, default: "") + ratio_txt = boolean, -- Show percentage (optional) + actions = function -- Value change handler +} +``` + +**Returns:** `table` - Virtual text line + +**Example:** +```lua +local slider = ui.slider.config({ + txt = "Volume: ", + val = 50, + w = 40, + hlon = "String", + hloff = "Comment", + thumb = true, + ratio_txt = true, + actions = function() + local new_val = ui.slider.val(40, "Volume: ", 2) + print("New value:", new_val) + end +}) +``` + +--- + +### `ui.slider.val(w, left_txt, xpad, opts?)` + +Get slider value from cursor position. + +**Parameters:** +- `w` (number): Slider width +- `left_txt` (string): Label text +- `xpad` (number): Horizontal padding +- `opts` (table|nil): Options `{ thumb = boolean, ratio = boolean }` + +**Returns:** `number` - Value (0-100) + +**Example:** +```lua +local value = ui.slider.val(40, "Volume: ", 2, { thumb = true }) +``` + +--- + +### `ui.progressbar(opts)` + +Create a progress bar. + +**Parameters:** +- `opts` (ProgressOptions) + +**ProgressOptions:** +```lua +{ + w = number, -- Width + val = number, -- Progress (0-100) + icon = { -- Icon configuration (optional) + on = string, -- Active icon (default: "-") + off = string -- Inactive icon (default: "-") + }, + hl = { -- Highlight configuration (optional) + on = string, -- Active hl (default: "exred") + off = string -- Inactive hl (default: "linenr") + } +} +``` + +**Returns:** `table[]` - Two-element array: `{ active_part, inactive_part }` + +**Example:** +```lua +local progress = ui.progressbar({ + w = 30, + val = 65, + icon = { on = "โ”", off = "โ”€" }, + hl = { on = "String", off = "Comment" } +}) +``` + +--- + +### `ui.separator(char?, w, hl?)` + +Create a horizontal separator line. + +**Parameters:** +- `char` (string|nil): Character to use (default: "โ”€") +- `w` (number): Width +- `hl` (string|nil): Highlight group (default: "linenr") + +**Returns:** `table[]` - Single-element line array + +**Example:** +```lua +local sep = ui.separator("โ”€", 50, "Comment") +``` + +--- + +### `ui.table(data, w, header_hl?, title?)` + +Create a formatted table with borders. + +**Parameters:** +- `data` (table[][]): 2D array of table data +- `w` (number|"fit"): Total width or "fit" for auto-width +- `header_hl` (string|nil): Header row highlight (optional) +- `title` (table|nil): Title virtual text (optional) + +**Returns:** `table[]` - Array of table lines + +**Example:** +```lua +local tbl = ui.table( + { + { "Name", "Age", "City" }, + { "Alice", "25", "NYC" }, + { "Bob", "30", "LA" } + }, + 60, + "Title", + { "Users", "Title" } +) +``` + +**Complex Cells:** +```lua +local tbl = ui.table({ + { "Name", "Status" }, + { + "Alice", + { { "โ—", "String" }, { " Active", "Normal" } } + } +}, 60) +``` + +--- + +### `ui.tabs(data, w, opts)` + +Create tabbed interface. + +**Parameters:** +- `data` (string[]): Array of tab names (use `"_pad_"` for dynamic spacing) +- `w` (number): Total width +- `opts` (TabOptions) + +**TabOptions:** +```lua +{ + active = string, -- Currently active tab name + hlon = string, -- Active highlight (optional, default: "normal") + hloff = string -- Inactive highlight (optional, default: "commentfg") +} +``` + +**Returns:** `table[]` - Three lines (top border, text, bottom border) + +**Example:** +```lua +local tabs = ui.tabs( + { "Home", "_pad_", "Settings", "About" }, + 60, + { active = "Settings", hlon = "Title" } +) +``` + +--- + +### `ui.graphs.bar(data)` + +Create a bar graph. + +**Parameters:** +- `data` (BarGraphData) + +**BarGraphData:** +```lua +{ + val = number[], -- Data values (0-10 scale) + baropts = { + w = number, -- Bar width + gap = number, -- Gap between bars + icon = string, -- Bar character (optional, default: "โ–ˆ") + hl = string, -- Single color OR + dual_hl = string[], -- [inactive, active] colors OR + format_hl = function(val) -> string -- Dynamic highlight + }, + format_labels = function(val) -> string, -- Y-axis labels (optional) + footer_label = table -- Footer virtual text (optional) +} +``` + +**Returns:** `table[]` - Array of graph lines + +**Example:** +```lua +local graphs = require("volt.ui.graphs") + +local bar = graphs.bar({ + val = { 5, 8, 3, 10, 7 }, + baropts = { + w = 3, + gap = 1, + icon = "โ–ˆ", + format_hl = function(val) + if val >= 80 then return "Error" end + return "String" + end + }, + format_labels = function(val) + return tostring(val) .. "%" + end, + footer_label = { "Week", "Comment" } +}) +``` + +--- + +### `ui.graphs.dot(data)` + +Create a dot graph. + +**Parameters:** +- `data` (DotGraphData) + +**DotGraphData:** +```lua +{ + val = number[], -- Data values (0-10 scale) + baropts = { + icons = { + on = string, -- Active icon (default: " ๓ฐ„ฐ") + off = string -- Inactive icon (default: " ยท") + }, + hl = { + on = string, -- Active hl (default: "exblue") + off = string -- Inactive hl (default: "commentfg") + }, + sidelabels = boolean, -- Show Y-axis (default: true) + format_icon = function(val) -> string, -- Dynamic icon (optional) + format_hl = function(val) -> string -- Dynamic hl (optional) + }, + format_labels = function(val) -> string, -- Y-axis labels (optional) + footer_label = table -- Footer virtual text (optional) +} +``` + +**Returns:** `table[]` - Array of graph lines + +**Example:** +```lua +local dot = graphs.dot({ + val = { 5, 8, 3, 10, 7 }, + baropts = { + icons = { on = " ๓ฐ„ฐ", off = " ยท" }, + hl = { on = "String", off = "Comment" }, + sidelabels = true + } +}) +``` + +--- + +### `ui.grid_col(columns)` + +Arrange components in columns. + +**Parameters:** +- `columns` (ColumnConfig[]) + +**ColumnConfig:** +```lua +{ + lines = table[], -- Line data + w = number, -- Column width + pad = number -- Right padding (optional, default: 0) +} +``` + +**Returns:** `table[]` - Combined grid lines + +**Example:** +```lua +local grid = ui.grid_col({ + { lines = col1_lines, w = 30, pad = 2 }, + { lines = col2_lines, w = 30, pad = 0 } +}) +``` + +--- + +### `ui.grid_row(parts)` + +Concatenate line groups horizontally. + +**Parameters:** +- `parts` (table[][]): Array of line groups + +**Returns:** `table` - Combined line + +**Example:** +```lua +local row = ui.grid_row({ + { { "Left", "Normal" } }, + { { " | ", "Comment" } }, + { { "Right", "String" } } +}) +``` + +--- + +### `ui.border(lines, hl?)` + +Add border around content. **Modifies `lines` in-place.** + +**Parameters:** +- `lines` (table[]): Line array to wrap +- `hl` (string|nil): Border highlight (default: "linenr") + +**Example:** +```lua +local lines = { + { { "Content 1", "Normal" } }, + { { "Content 2", "Normal" } } +} + +ui.border(lines, "Comment") + +-- Now lines includes border: +-- โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +-- โ”‚ Content 1โ”‚ +-- โ”‚ Content 2โ”‚ +-- โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +--- + +### `ui.hpad(line, w)` + +Fill horizontal padding to reach width. + +**Parameters:** +- `line` (table): Line with `"_pad_"` markers +- `w` (number): Total desired width + +**Returns:** `table` - Line with padding filled + +**Example:** +```lua +local line = { + { "Left", "Normal" }, + { "_pad_" }, + { "Right", "Normal" } +} + +ui.hpad(line, 50) +-- Padding is calculated and filled +``` + +--- + +### `ui.line_w(line)` + +Calculate total width of virtual text line. + +**Parameters:** +- `line` (table): Virtual text line + +**Returns:** `number` - Total width + +**Example:** +```lua +local width = ui.line_w({ + { "Hello", "Normal" }, + { " ", "Normal" }, + { "World", "String" } +}) +-- Returns: 11 +``` + +--- + +## Event System + +### `volt.events` Module + +Handles user interactions. + +```lua +local events = require("volt.events") +``` + +--- + +#### `events.enable()` + +Enable global event system. **Call once** in your configuration. + +Sets up `vim.on_key()` handlers for mouse events. + +**Example:** +```lua +require("volt.events").enable() +``` + +--- + +#### `events.add(buffers)` + +Register buffer(s) for event handling. + +**Parameters:** +- `buffers` (number|number[]): Buffer ID or array of IDs + +**Example:** +```lua +events.add(buf) +events.add({ buf1, buf2, buf3 }) +``` + +--- + +#### `events.bufs` + +Array of currently registered buffer IDs. + +**Type:** `number[]` + +--- + +### Virtual Text Actions + +Define interactive regions in virtual text. + +#### Click-Only Action + +```lua +{ + "Click me", + "HighlightGroup", + function() + -- Click handler + end +} +``` + +#### Click and Hover Actions + +```lua +{ + "Hover me", + "Normal", + { + ui_type = "slider", -- Optional: "slider" for special handling + click = function() + -- Click handler + end, + hover = { + id = "unique_id", -- Hover state identifier + callback = function() -- Hover callback (optional) + vim.g.nvmark_hovered = "unique_id" + end, + redraw = string[] -- Sections to redraw on hover + } + } +} +``` + +#### Check Hover State + +```lua +if vim.g.nvmark_hovered == "unique_id" then + -- Element is being hovered +end +``` + +--- + +## Color Utilities + +### `volt.color` Module + +Color manipulation and conversion utilities. + +```lua +local color = require("volt.color") +``` + +--- + +#### `color.hex2rgb(hex)` + +Convert hex color to RGB. + +**Parameters:** +- `hex` (string): Hex color (with or without #) + +**Returns:** `number, number, number` - R, G, B (0-255) + +**Example:** +```lua +local r, g, b = color.hex2rgb("#ff5555") +-- r=255, g=85, b=85 +``` + +--- + +#### `color.hex2rgb_ratio(hex)` + +Convert hex to RGB ratios. + +**Parameters:** +- `hex` (string): Hex color + +**Returns:** `number, number, number` - R, G, B (0-100) + +--- + +#### `color.rgb2hex(r, g, b)` + +Convert RGB to hex color. + +**Parameters:** +- `r` (number): Red (0-255) +- `g` (number): Green (0-255) +- `b` (number): Blue (0-255) + +**Returns:** `string` - Hex color + +**Example:** +```lua +local hex = color.rgb2hex(255, 85, 85) +-- hex="#ff5555" +``` + +--- + +#### `color.hex2hsl(hex)` + +Convert hex to HSL. + +**Parameters:** +- `hex` (string): Hex color + +**Returns:** `number, number, number` - H (0-360), S (0-1), L (0-1) + +--- + +#### `color.hsl2hex(h, s, l)` + +Convert HSL to hex. + +**Parameters:** +- `h` (number): Hue (0-360) +- `s` (number): Saturation (0-1) +- `l` (number): Lightness (0-1) + +**Returns:** `string` - Hex color + +--- + +#### `color.rgb2hsl(r, g, b)` + +Convert RGB to HSL. + +**Parameters:** +- `r` (number): Red (0-255) +- `g` (number): Green (0-255) +- `b` (number): Blue (0-255) + +**Returns:** `number, number, number` - H (0-360), S (0-1), L (0-1) + +--- + +#### `color.hsl2rgb(h, s, l)` + +Convert HSL to RGB. + +**Parameters:** +- `h` (number): Hue (0-360) +- `s` (number): Saturation (0-1) +- `l` (number): Lightness (0-1) + +**Returns:** `number, number, number` - R, G, B (0-255) + +--- + +#### `color.change_hex_lightness(hex, percent)` + +Lighten or darken a color. + +**Parameters:** +- `hex` (string): Hex color +- `percent` (number): Amount to change (-100 to 100) + - Positive: lighter + - Negative: darker + +**Returns:** `string` - Modified hex color + +**Example:** +```lua +local lighter = color.change_hex_lightness("#ff5555", 20) +local darker = color.change_hex_lightness("#ff5555", -20) +``` + +--- + +#### `color.change_hex_saturation(hex, percent)` + +Adjust color saturation. + +**Parameters:** +- `hex` (string): Hex color +- `percent` (number): Amount to change (-100 to 100) + +**Returns:** `string` - Modified hex color + +--- + +#### `color.change_hex_hue(hex, percent)` + +Shift color hue. + +**Parameters:** +- `hex` (string): Hex color +- `percent` (number): Hue shift as percentage of 360ยฐ + +**Returns:** `string` - Modified hex color + +**Example:** +```lua +local shifted = color.change_hex_hue("#ff5555", 50) +``` + +--- + +#### `color.mix(first, second, strength)` + +Mix two colors. + +**Parameters:** +- `first` (string): Primary hex color +- `second` (string): Color to mix in +- `strength` (number): Amount of second color (0-100) + +**Returns:** `string` - Mixed hex color + +**Example:** +```lua +local mixed = color.mix("#ff5555", "#5555ff", 50) +-- 50/50 mix of red and blue +``` + +--- + +#### `color.compute_gradient(hex1, hex2, steps)` + +Generate color gradient. + +**Parameters:** +- `hex1` (string): Start color +- `hex2` (string): End color +- `steps` (number): Number of steps + +**Returns:** `string[]` - Array of hex colors + +**Example:** +```lua +local gradient = color.compute_gradient("#ff5555", "#5555ff", 10) +-- Returns 10 colors from red to blue +``` + +--- + +#### `color.hex2complementary(hex, count)` + +Generate complementary colors. + +**Parameters:** +- `hex` (string): Base color +- `count` (number): Number of complementary colors + +**Returns:** `string[]` - Array of hex colors + +**Example:** +```lua +local complements = color.hex2complementary("#ff5555", 3) +``` + +--- + +## State Management + +### `volt.state` Module + +Buffer-specific state storage. + +```lua +local state = require("volt.state") +``` + +### State Structure + +```lua +state[buf] = { + clickables = table, -- Click regions per row + hoverables = table, -- Hover regions per row + layout = table, -- Layout definition + ns = number, -- Namespace ID + xpad = number, -- Horizontal padding + h = number, -- Total height + buf = number, -- Buffer ID + hovered_extmarks = any -- Currently hovered sections +} +``` + +### Click/Hover Region Structure + +```lua +state[buf].clickables[row_number] = { + { + col_start = number, + col_end = number, + ui_type = string, -- Optional: "slider" + actions = function|table, + hover = table -- Hover config (optional) + }, + -- ... more regions in this row +} +``` + +--- + +## Utilities + +### `volt.utils` Module + +Helper utilities. + +```lua +local utils = require("volt.utils") +``` + +--- + +#### `utils.cycle_bufs(bufs)` + +Cycle through array of buffers. + +**Parameters:** +- `bufs` (number[]): Array of buffer IDs + +--- + +#### `utils.cycle_clickables(buf, step)` + +Navigate through clickable elements. + +**Parameters:** +- `buf` (number): Buffer ID +- `step` (number): Direction (1 or -1) + +--- + +#### `utils.close(config)` + +Close UI and cleanup. + +**Parameters:** +- `config` (CloseConfig) + +**CloseConfig:** +```lua +{ + bufs = number[], -- Buffers to close + close_func = function(buf), -- Per-buffer cleanup (optional) + after_close = function() -- Final cleanup (optional) +} +``` + +**Example:** +```lua +utils.close({ + bufs = { buf1, buf2 }, + close_func = function(buf) + print("Closing", buf) + end, + after_close = function() + print("All closed") + end +}) +``` + +--- + +#### `utils.get_hl(name)` + +Get highlight group colors. + +**Parameters:** +- `name` (string): Highlight group name + +**Returns:** `table` - `{ fg = "#rrggbb", bg = "#rrggbb" }` + +**Example:** +```lua +local hl = utils.get_hl("Normal") +-- { fg = "#ffffff", bg = "#000000" } +``` + +--- + +## Type Definitions + +### Layout Definition + +```lua +---@class VoltSection +---@field name string Section identifier +---@field row? number Starting row (auto-calculated) +---@field col_start? number Column offset +---@field lines fun(buf: number): table[] Function returning line data + +---@class VoltLayout +---@field [number] VoltSection +``` + +### Line Data + +```lua +---@class VoltVirtualText +---@field [1] string Text content +---@field [2] string Highlight group +---@field [3]? function|table Click handler or action config + +---@class VoltLine +---@field [number] VoltVirtualText +``` + +### Action Config + +```lua +---@class VoltAction +---@field ui_type? string Special UI type ("slider") +---@field click? function Click handler +---@field hover? VoltHover Hover configuration + +---@class VoltHover +---@field id string Unique identifier +---@field callback? function Hover callback +---@field redraw string[] Sections to redraw +``` + +### Component Options + +```lua +---@class CheckboxOptions +---@field active boolean Current state +---@field txt string Label text +---@field check? string Checked icon +---@field uncheck? string Unchecked icon +---@field hlon? string Active highlight +---@field hloff? string Inactive highlight +---@field actions? function Click handler + +---@class SliderOptions +---@field txt? string Label text +---@field val number Current value (0-100) +---@field w number Width +---@field hlon string Active highlight +---@field hloff? string Inactive highlight +---@field thumb? boolean Show thumb +---@field thumb_icon? string Thumb icon +---@field ratio_txt? boolean Show percentage +---@field actions function Value change handler + +---@class ProgressOptions +---@field w number Width +---@field val number Progress (0-100) +---@field icon? table Icon config +---@field hl? table Highlight config +``` + +--- + +## Highlight Groups + +Volt automatically creates these highlight groups: + +| Group | Description | +|-------|-------------| +| `ExDarkBg` | Darkest background | +| `ExDarkBorder` | Dark border | +| `ExBlack2Bg` | Medium background | +| `ExBlack2Border` | Medium border | +| `ExBlack3Bg` | Light background | +| `ExBlack3Border` | Light border | +| `ExRed` | Red foreground | +| `ExYellow` | Yellow foreground | +| `ExBlue` | Blue foreground | +| `ExGreen` | Green foreground | +| `ExLightGrey` | Light grey foreground | +| `CommentFg` | Comment-like foreground | + +These adapt to: +- NvChad's base46 theme system +- Your current colorscheme +- Light/dark background setting + +--- + +## Internal Modules + +These modules are used internally but can be accessed if needed: + +### `volt.draw` + +```lua +local draw = require("volt.draw") +draw(buf, section) -- Draw a single section +``` + +### `volt.highlights` + +```lua +require("volt.highlights") -- Loads highlight definitions +``` + +--- + +This API reference covers all public interfaces in Volt. For implementation details, see the [Developer Guide](DEVELOPER_GUIDE.md). diff --git a/docs/DEVELOPER_GUIDE.md b/docs/DEVELOPER_GUIDE.md new file mode 100644 index 0000000..f98fe9f --- /dev/null +++ b/docs/DEVELOPER_GUIDE.md @@ -0,0 +1,839 @@ +# Volt Developer Guide + +This guide is for developers who want to build plugins and applications using the Volt framework. + +## Table of Contents + +- [Architecture Overview](#architecture-overview) +- [Core Modules](#core-modules) +- [Building a Plugin](#building-a-plugin) +- [Advanced Patterns](#advanced-patterns) +- [Performance Optimization](#performance-optimization) +- [Testing](#testing) + +## Architecture Overview + +### How Volt Works + +Volt leverages Neovim's **extmarks** (extended marks) and **virtual text** to render UIs directly in buffers without modifying actual buffer content. Here's the rendering pipeline: + +``` +Layout Definition โ†’ gen_data() โ†’ State Creation โ†’ draw() โ†’ Extmarks โ†’ Visual UI + โ†“ + Event System โ† User Interaction +``` + +### Key Principles + +1. **Declarative UI**: Define what to render, not how to render it +2. **Reactive Updates**: Update specific sections without full redraws +3. **Event-Driven**: Mouse and keyboard events trigger callbacks +4. **State Per Buffer**: Each buffer has isolated state +5. **Virtual Text**: No buffer modification, pure visual overlay + +## Core Modules + +### `volt.init` - Main Entry Point + +```lua +local volt = require("volt") + +-- Generate state and calculate layout +volt.gen_data(config) + +-- Render UI to buffer +volt.run(buf, options) + +-- Redraw specific sections +volt.redraw(buf, section_name) + +-- Setup empty lines for rendering +volt.set_empty_lines(buf, height, width) + +-- Add keymaps to buffers +volt.mappings(config) + +-- Toggle UI visibility +volt.toggle_func(open_function, state_variable) + +-- Close UI +volt.close(buf) +``` + +### `volt.state` - State Management + +The state module maintains buffer-specific data: + +```lua +local state = require("volt.state") + +-- State structure for each buffer +state[buf] = { + clickables = {}, -- Click regions indexed by row + hoverables = {}, -- Hover regions indexed by row + layout = {}, -- Layout definition + ns = namespace_id, -- Namespace for extmarks + xpad = 0, -- Horizontal padding + h = 0, -- Total height + buf = buf, -- Buffer ID + hovered_extmarks = nil -- Currently hovered sections +} +``` + +### `volt.draw` - Rendering Engine + +The draw module converts layout data to extmarks: + +```lua +local draw = require("volt.draw") + +-- Internal use - draws a section +draw(buf, section) +``` + +**Process:** +1. Calculates virtual text positions +2. Registers clickable/hoverable regions +3. Creates extmarks with `nvim_buf_set_extmark` + +### `volt.events` - Event System + +Handles user interactions: + +```lua +local events = require("volt.events") + +-- Enable global event system +events.enable() + +-- Register buffer(s) +events.add(buf) +events.add({ buf1, buf2, buf3 }) + +-- Internally tracked buffers +events.bufs -- Array of registered buffers +``` + +**Event Flow:** +``` +User Input โ†’ vim.on_key() โ†’ MouseMove/LeftMouse โ†’ + โ†’ Get Position โ†’ Find Virtual Element โ†’ + โ†’ Execute Callback โ†’ Redraw if needed +``` + +### `volt.highlights` - Theme System + +Automatically creates highlight groups: + +```lua +-- Highlights are created automatically +-- They adapt to base46 (NvChad) or your colorscheme + +-- Available groups: +ExDarkBg, ExDarkBorder -- Darkest +ExBlack2Bg, ExBlack2Border -- Medium +ExBlack3Bg, ExBlack3Border -- Lighter +ExRed, ExYellow, ExBlue, ExGreen +ExLightGrey, CommentFg +``` + +### `volt.color` - Color Utilities + +Comprehensive color manipulation: + +```lua +local color = require("volt.color") + +-- Conversions +color.hex2rgb(hex) โ†’ r, g, b +color.rgb2hex(r, g, b) โ†’ hex +color.hex2hsl(hex) โ†’ h, s, l +color.hsl2hex(h, s, l) โ†’ hex +color.hex2rgb_ratio(hex) โ†’ r%, g%, b% + +-- Transformations +color.change_hex_hue(hex, percent) +color.change_hex_saturation(hex, percent) +color.change_hex_lightness(hex, percent) + +-- Generation +color.compute_gradient(hex1, hex2, steps) +color.hex2complementary(hex, count) +color.mix(first, second, strength) +``` + +### `volt.utils` - Helper Functions + +```lua +local utils = require("volt.utils") + +-- Cycle through buffers +utils.cycle_bufs(buffer_array) + +-- Cycle clickable elements +utils.cycle_clickables(buf, step) -- step: 1 or -1 + +-- Close UI and cleanup +utils.close({ + bufs = { buf1, buf2 }, + close_func = function(buf) end, + after_close = function() end +}) + +-- Get highlight table +local hl = utils.get_hl("Normal") +-- Returns: { fg = "#rrggbb", bg = "#rrggbb" } +``` + +## Building a Plugin + +### Step 1: Plugin Structure + +```lua +-- lua/myplugin/init.lua +local M = {} +local volt = require("volt") +local ui = require("volt.ui") + +-- Plugin state +local state = { + is_open = false, + buf = nil, + win = nil, + data = {} +} + +-- Your plugin logic +M.setup = function(opts) + -- Configuration +end + +M.toggle = function() + volt.toggle_func(M.open, state.is_open) + state.is_open = not state.is_open +end + +M.open = function() + -- Create UI +end + +M.close = function() + volt.close(state.buf) + state.is_open = false +end + +return M +``` + +### Step 2: Create Buffer and Window + +```lua +M.open = function() + -- Create buffer + state.buf = vim.api.nvim_create_buf(false, true) + vim.bo[state.buf].bufhidden = "wipe" + vim.bo[state.buf].filetype = "MyPlugin" + + -- Calculate dimensions + local width = 80 + local height = 30 + + -- Create window + state.win = vim.api.nvim_open_win(state.buf, true, { + relative = "editor", + width = width, + height = height, + row = math.floor((vim.o.lines - height) / 2), + col = math.floor((vim.o.columns - width) / 2), + style = "minimal", + border = "rounded", + title = " My Plugin ", + title_pos = "center" + }) + + -- Window options + vim.wo[state.win].cursorline = false + vim.wo[state.win].wrap = false + + -- Setup UI + setup_ui() +end +``` + +### Step 3: Define Layout + +```lua +local function setup_ui() + local layout = { + { + name = "header", + lines = function(buf) + return { + { { "My Amazing Plugin", "Title" } }, + { { "", "Normal" } } + } + end + }, + { + name = "controls", + lines = function(buf) + return create_controls() + end + }, + { + name = "content", + lines = function(buf) + return create_content() + end + }, + { + name = "footer", + lines = function(buf) + return { + { { "", "Normal" } }, + ui.separator("โ”€", 76, "Comment"), + { + { " q: quit ", "Comment" }, + { " : next ", "Comment" } + } + } + end + } + } + + -- Generate data + volt.gen_data({ + { + buf = state.buf, + layout = layout, + ns = vim.api.nvim_create_namespace("myplugin"), + xpad = 2 + } + }) + + -- Render + volt.run(state.buf, { + h = 30, + w = 80, + winclosed_event = true -- Auto-close on window close + }) + + -- Enable events + if not vim.g.extmarks_events then + require("volt.events").enable() + end + require("volt.events").add(state.buf) + + -- Add custom mappings + volt.mappings({ + bufs = { state.buf }, + winclosed_event = true + }) +end +``` + +### Step 4: Implement Interactive Elements + +```lua +local function create_controls() + local lines = {} + + -- Slider example + table.insert(lines, + ui.slider.config({ + txt = "Volume: ", + val = state.data.volume or 50, + w = 60, + hlon = "String", + hloff = "Comment", + thumb = true, + ratio_txt = true, + actions = function() + state.data.volume = ui.slider.val(60, "Volume: ", 2, { thumb = true }) + volt.redraw(state.buf, "controls") + -- Do something with new volume + update_volume(state.data.volume) + end + }) + ) + + -- Checkbox example + table.insert(lines, + ui.checkbox({ + active = state.data.enabled or false, + txt = "Enable feature", + actions = function() + state.data.enabled = not state.data.enabled + volt.redraw(state.buf, { "controls", "content" }) + -- React to change + on_feature_toggle(state.data.enabled) + end + }) + ) + + return lines +end +``` + +### Step 5: Dynamic Content + +```lua +local function create_content() + if not state.data.enabled then + return { + { { "Feature is disabled", "Comment" } } + } + end + + -- Generate dynamic content based on state + local lines = {} + + for i, item in ipairs(state.data.items or {}) do + table.insert(lines, { + { tostring(i) .. ". ", "LineNr" }, + { item.name, "Normal" }, + { + " [ร—]", + "Error", + function() + table.remove(state.data.items, i) + volt.redraw(state.buf, "content") + end + } + }) + end + + -- Add button + table.insert(lines, { { "", "Normal" } }) + table.insert(lines, { + { + " + Add Item ", + "String", + function() + table.insert(state.data.items, { name = "New Item" }) + volt.redraw(state.buf, "content") + end + } + }) + + return lines +end +``` + +## Advanced Patterns + +### Multi-Buffer UIs + +Create tabbed interfaces with multiple buffers: + +```lua +local buffers = {} +local current_tab = "main" + +local function create_tab(name) + local buf = vim.api.nvim_create_buf(false, true) + buffers[name] = buf + + -- Setup layout for this buffer + local layout = get_layout_for_tab(name) + + volt.gen_data({ + { + buf = buf, + layout = layout, + ns = vim.api.nvim_create_namespace("tab_" .. name), + xpad = 2 + } + }) + + return buf +end + +local function switch_tab(name) + current_tab = name + local buf = buffers[name] + vim.api.nvim_win_set_buf(state.win, buf) + volt.redraw(buf, "all") +end + +-- Setup with volt.mappings for Ctrl+T cycling +volt.mappings({ + bufs = vim.tbl_values(buffers), + winclosed_event = true +}) +``` + +### Hover Effects + +Implement visual feedback on hover: + +```lua +local function create_hover_button(text, action) + return { + text, + vim.g.nvmark_hovered == "btn_" .. text and "Title" or "Normal", + { + click = action, + hover = { + id = "btn_" .. text, + callback = function() + vim.g.nvmark_hovered = "btn_" .. text + end, + redraw = { "section_name" } + } + } + } +end +``` + +### Async Operations + +Handle async data loading: + +```lua +local function load_data_async() + -- Show loading state + state.loading = true + volt.redraw(state.buf, "content") + + vim.defer_fn(function() + -- Fetch data (could be from file, API, etc.) + state.data = fetch_data() + state.loading = false + + -- Update UI + volt.redraw(state.buf, "content") + end, 100) +end + +local function create_content() + if state.loading then + return { + { { "Loading...", "Comment" } } + } + end + + -- Render actual content + return render_data(state.data) +end +``` + +### Custom Input Handling + +Create custom input fields: + +```lua +local function create_input() + local input_value = state.input or "" + + return { + { "Input: ", "Comment" }, + { + input_value .. "โ–ˆ", -- Cursor + "Normal", + { + click = function() + vim.ui.input({ prompt = "Enter value: ", default = input_value }, + function(value) + if value then + state.input = value + volt.redraw(state.buf, "input_section") + on_input_change(value) + end + end + ) + end + } + } + } +end +``` + +### Keyboard Shortcuts + +Add custom key handlers: + +```lua +vim.api.nvim_buf_set_keymap(state.buf, "n", "a", "", { + callback = function() + -- Your action + add_item() + volt.redraw(state.buf, "list") + end, + noremap = true, + silent = true +}) + +vim.api.nvim_buf_set_keymap(state.buf, "n", "d", "", { + callback = function() + delete_selected() + volt.redraw(state.buf, "list") + end, + noremap = true, + silent = true +}) +``` + +### Stateful Animations + +Create simple animations: + +```lua +local animation_state = { frame = 1, timer = nil } + +local function start_animation() + animation_state.timer = vim.loop.new_timer() + animation_state.timer:start(0, 100, vim.schedule_wrap(function() + animation_state.frame = (animation_state.frame % 8) + 1 + volt.redraw(state.buf, "spinner") + end)) +end + +local function stop_animation() + if animation_state.timer then + animation_state.timer:stop() + animation_state.timer = nil + end +end + +local function create_spinner() + local frames = { "โ ‹", "โ ™", "โ น", "โ ธ", "โ ผ", "โ ด", "โ ฆ", "โ ง" } + return { + { frames[animation_state.frame] .. " Loading", "Comment" } + } +end +``` + +## Performance Optimization + +### Minimize Redraws + +Only redraw what changes: + +```lua +-- Bad: Redraw everything +volt.redraw(buf, "all") + +-- Good: Redraw specific sections +volt.redraw(buf, { "header", "content" }) + +-- Best: Redraw only changed section +volt.redraw(buf, "changed_section") +``` + +### Lazy Rendering + +Generate content only when visible: + +```lua +local function create_large_list() + -- Only render visible portion + local start_idx = state.scroll_pos + local end_idx = math.min(start_idx + 20, #state.items) + + local lines = {} + for i = start_idx, end_idx do + table.insert(lines, render_item(state.items[i])) + end + + return lines +end +``` + +### Memoization + +Cache expensive computations: + +```lua +local cache = {} + +local function get_computed_data(key) + if not cache[key] then + cache[key] = expensive_computation(key) + end + return cache[key] +end + +-- Invalidate when needed +local function invalidate_cache(key) + cache[key] = nil +end +``` + +### Batch Updates + +Group state changes: + +```lua +-- Bad: Multiple redraws +for _, item in ipairs(items) do + process_item(item) + volt.redraw(buf, "list") +end + +-- Good: Single redraw +for _, item in ipairs(items) do + process_item(item) +end +volt.redraw(buf, "list") +``` + +## Testing + +### Unit Testing Example + +```lua +-- tests/myplug_spec.lua +describe("MyPlugin", function() + local plugin + + before_each(function() + plugin = require("myplugin") + plugin.setup() + end) + + it("creates buffer on open", function() + plugin.open() + assert.is_not_nil(plugin.state.buf) + assert.is_true(vim.api.nvim_buf_is_valid(plugin.state.buf)) + end) + + it("handles toggle correctly", function() + assert.is_false(plugin.state.is_open) + plugin.toggle() + assert.is_true(plugin.state.is_open) + plugin.toggle() + assert.is_false(plugin.state.is_open) + end) + + it("cleans up on close", function() + plugin.open() + local buf = plugin.state.buf + plugin.close() + assert.is_false(vim.api.nvim_buf_is_valid(buf)) + end) +end) +``` + +### Integration Testing + +```lua +-- Manual testing helper +M.debug = function() + print("State:", vim.inspect(state)) + print("Buffer:", state.buf) + print("Layout sections:", #state.layout) + + local volt_state = require("volt.state")[state.buf] + print("Clickables:", vim.inspect(volt_state.clickables)) + print("Hoverables:", vim.inspect(volt_state.hoverables)) +end +``` + +## Best Practices + +### Code Organization + +``` +lua/myplugin/ +โ”œโ”€โ”€ init.lua -- Main entry point +โ”œโ”€โ”€ config.lua -- Configuration +โ”œโ”€โ”€ state.lua -- State management +โ”œโ”€โ”€ ui/ +โ”‚ โ”œโ”€โ”€ layout.lua -- Layout definitions +โ”‚ โ”œโ”€โ”€ components.lua -- Custom components +โ”‚ โ””โ”€โ”€ theme.lua -- Theming +โ””โ”€โ”€ utils.lua -- Utilities +``` + +### Error Handling + +```lua +local function safe_action(fn) + return function(...) + local ok, err = pcall(fn, ...) + if not ok then + vim.notify( + "MyPlugin error: " .. tostring(err), + vim.log.levels.ERROR + ) + end + end +end + +-- Use in actions +{ + "Click me", + "Normal", + safe_action(function() + -- Potentially failing code + end) +} +``` + +### User Configuration + +```lua +local default_config = { + width = 80, + height = 30, + theme = "auto", + keymaps = { + toggle = "mp", + close = "q" + } +} + +M.setup = function(opts) + local config = vim.tbl_deep_extend("force", default_config, opts or {}) + + -- Setup keymap + vim.keymap.set("n", config.keymaps.toggle, M.toggle, { + desc = "Toggle MyPlugin" + }) + + return config +end +``` + +### Documentation + +Add vim.doc comments: + +```lua +--- Open the plugin UI +--- @param opts table|nil Optional configuration +--- @field width number Window width (default: 80) +--- @field height number Window height (default: 30) +M.open = function(opts) + -- Implementation +end +``` + +## Real-World Examples + +Study these plugins built with Volt: + +1. **[Minty](https://github.com/NvChad/minty)**: Color tools with sliders and pickers +2. **[Menu](https://github.com/NvChad/menu)**: Nested menu system +3. **[Typr](https://github.com/NvChad/typr)**: Typing practice with statistics +4. **[Base46 Theme Picker](https://github.com/NvChad/base46)**: Theme selection UI + +## Getting Help + +- **Discord**: [NvChad Community](https://discord.gg/gADmkJb9Fb) +- **Issues**: [GitHub Issues](https://github.com/NvChad/volt/issues) +- **Discussions**: Share your Volt plugins! + +## Contributing to Volt + +Interested in improving Volt itself? Check out: + +- Follow the existing code style (see `.stylua.toml`) +- Add tests for new features +- Update documentation +- Submit PRs to the [main repo](https://github.com/NvChad/volt) + +--- + +Happy building with Volt! โšก diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md new file mode 100644 index 0000000..b777306 --- /dev/null +++ b/docs/USER_GUIDE.md @@ -0,0 +1,851 @@ +# Volt User Guide + +Welcome to Volt! This guide will teach you how to use Volt to create beautiful interactive UIs in Neovim. + +## Table of Contents + +- [Getting Started](#getting-started) +- [Basic Concepts](#basic-concepts) +- [UI Components](#ui-components) +- [Layouts](#layouts) +- [Event Handling](#event-handling) +- [Styling](#styling) +- [Examples](#examples) + +## Getting Started + +### Installation + +Install Volt using your preferred plugin manager: + +**lazy.nvim:** +```lua +{ + "NvChad/volt", + lazy = true, +} +``` + +**packer.nvim:** +```lua +use { + "NvChad/volt", + opt = true, +} +``` + +**vim.pack** +```lua +vim.pack.add({ + { src = "https://github.com/nvzone/volt" }, +}) +``` + +### Your First Volt UI + +```lua +local volt = require("volt") + +-- Create a buffer +local buf = vim.api.nvim_create_buf(false, true) +vim.bo[buf].bufhidden = "wipe" + +-- Define layout +local layout = { + { + name = "greeting", + lines = function() + return { + { { "Hello, Volt! โšก", "Title" } } + } + end + } +} + +-- Setup UI data +volt.gen_data({ + { + buf = buf, + layout = layout, + ns = vim.api.nvim_create_namespace("my_ui"), + xpad = 2, + } +}) + +-- Create window +local win = vim.api.nvim_open_win(buf, true, { + relative = "editor", + width = 40, + height = 5, + row = 10, + col = 10, + style = "minimal", + border = "rounded", +}) + +-- Render +volt.run(buf, { h = 5, w = 40 }) +``` + +## Basic Concepts + +### Virtual Text Structure + +Volt uses Neovim's virtual text (extmarks) to render UI. Each line is an array of "cells": + +```lua +{ + { "text", "highlight_group" }, + { " more text", "another_highlight" }, + { " clickable", "special_hl", callback_function } +} +``` + +Structure: +1. **Text**: The string to display +2. **Highlight**: Highlight group name +3. **Action** (optional): Function to execute on click + +### Layout Sections + +Your UI is divided into named sections: + +```lua +local layout = { + { + name = "header", -- Section identifier + row = 0, -- Starting row (auto-calculated) + col_start = 5, -- Optional: column offset + lines = function(buf) -- Function returning line data + return { + { { "Header Text", "Title" } } + } + end + }, + { + name = "content", + lines = function(buf) + return { + { { "Content line 1", "Normal" } }, + { { "Content line 2", "Normal" } } + } + end + } +} +``` + +### State Management + +Volt maintains state per buffer: + +```lua +local state = require("volt.state") + +-- Access buffer state +local buf_state = state[buf] + +-- State contains: +-- buf_state.clickables - Click regions per row +-- buf_state.hoverables - Hover regions per row +-- buf_state.layout - Your layout definition +-- buf_state.ns - Namespace ID +-- buf_state.xpad - Horizontal padding +``` + +## UI Components + +### Checkbox + +Create toggleable checkboxes: + +```lua +local ui = require("volt.ui") + +local checked = false + +local checkbox = ui.checkbox({ + active = checked, + txt = "Enable dark mode", + check = "โœ“", -- Custom check icon (default: ) + uncheck = "โœ—", -- Custom uncheck icon (default: ) + hlon = "String", -- Active highlight + hloff = "Comment", -- Inactive highlight + actions = function() + checked = not checked + volt.redraw(buf, "section_name") + end +}) + +-- Use in your layout +lines = function() + return { checkbox } +end +``` + +### Slider + +Interactive value sliders: + +```lua +local volume = 50 + +local slider_line = ui.slider.config({ + txt = "Volume: ", -- Label text + val = volume, -- Current value (0-100) + w = 40, -- Width + hlon = "String", -- Active color + hloff = "Comment", -- Inactive color + thumb = true, -- Show thumb indicator + thumb_icon = "", -- Custom thumb icon + ratio_txt = true, -- Show percentage + actions = function() + -- Get new value based on cursor position + volume = ui.slider.val(40, "Volume: ", 2) + volt.redraw(buf, "section_name") + end +}) +``` + +**Keyboard Navigation**: Move cursor over slider and press Enter to update value. + +### Progress Bar + +Show progress indicators: + +```lua +local progress = ui.progressbar({ + w = 30, -- Width + val = 65, -- Progress (0-100) + icon = { + on = "โ”", -- Active icon + off = "โ”€" -- Inactive icon + }, + hl = { + on = "String", -- Active highlight + off = "Comment" -- Inactive highlight + } +}) + +-- Returns: { { active_part, hl }, { inactive_part, hl } } +``` + +### Separator + +Horizontal dividers: + +```lua +local separator = ui.separator("โ”€", 50, "Comment") +-- Character, width, highlight +``` + +### Tabs + +Create tabbed interfaces: + +```lua +local active_tab = "Settings" + +local tabs = ui.tabs( + { "Home", "Settings", "About" }, -- Tab names + 60, -- Total width + { + active = active_tab, -- Currently active + hlon = "Title", -- Active highlight + hloff = "Comment" -- Inactive highlight + } +) + +-- Returns 3 lines: top border, text, bottom border +``` + +### Table + +Create formatted tables: + +```lua +local data = { + { "Name", "Age", "City" }, -- Header row + { "Alice", "25", "New York" }, + { "Bob", "30", "Los Angeles" }, + { "Carol", "28", "Chicago" } +} + +local table_lines = ui.table( + data, + 60, -- Total width + "Title", -- Header highlight + { "Users", "Title" } -- Optional title +) +``` + +Tables support complex cells with virtual text: + +```lua +local complex_data = { + { "Name", "Status" }, + { + "Alice", + { { "โ—", "String" }, { " Active", "Normal" } } -- Virtual text cell + } +} +``` + +### Graphs + +#### Bar Graph + +```lua +local graphs = require("volt.ui.graphs") + +local bar_data = graphs.bar({ + val = { 5, 8, 3, 10, 7, 6 }, -- Data points (0-10 scale) + baropts = { + w = 3, -- Bar width + gap = 1, -- Gap between bars + icon = "โ–ˆ", -- Bar character + hl = "String", -- Single color, or: + dual_hl = { "Comment", "String" }, -- [inactive, active] + -- OR + format_hl = function(val) + if val >= 80 then return "Error" end + if val >= 50 then return "Warning" end + return "String" + end + }, + format_labels = function(val) + return tostring(val) .. "%" + end, + footer_label = { "Performance", "Title" } +}) +``` + +#### Dot Graph + +```lua +local dot_data = graphs.dot({ + val = { 5, 8, 3, 10, 7 }, + baropts = { + icons = { + on = " ๓ฐ„ฐ", -- Active icon + off = " ยท" -- Inactive icon + }, + hl = { + on = "String", + off = "Comment" + }, + sidelabels = true, -- Show Y-axis labels + format_icon = function(val) + if val >= 80 then return " " end + return " ๓ฐ„ฐ" + end + }, + footer_label = { "Metrics", "Title" } +}) +``` + +## Layouts + +### Grid Column Layout + +Arrange content in columns: + +```lua +local grid_col = require("volt.ui.grid_col") + +local column1 = { + lines = { { { "Col 1 Line 1", "Normal" } } }, + w = 20, + pad = 2 -- Right padding +} + +local column2 = { + lines = { { { "Col 2 Line 1", "Normal" } } }, + w = 20, + pad = 0 +} + +local grid_lines = grid_col({ column1, column2 }) +``` + +### Grid Row Layout + +Concatenate multiple line groups: + +```lua +local ui = require("volt.ui") + +local row = ui.grid_row({ + { { "Part 1", "Normal" } }, + { { " | ", "Comment" } }, + { { "Part 2", "String" } } +}) +-- Combines all parts into single line +``` + +### Borders + +Add borders around content: + +```lua +local lines = { + { { "Content line 1", "Normal" } }, + { { "Content line 2", "Normal" } } +} + +ui.border(lines, "Comment") -- Adds border with given highlight + +-- Before: +-- Content line 1 +-- Content line 2 + +-- After: +-- โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +-- โ”‚ Content line 1 โ”‚ +-- โ”‚ Content line 2 โ”‚ +-- โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### Horizontal Padding + +Add dynamic padding: + +```lua +local line = { + { "Left text", "Normal" }, + { "_pad_" }, -- Will be replaced with spaces + { "Right text", "Normal" } +} + +ui.hpad(line, 50) -- Total width +-- Calculates and fills padding automatically +``` + +Calculate line width: + +```lua +local width = ui.line_w(line) +``` + +## Event Handling + +### Enable Events + +```lua +-- Enable global event system (call once) +require("volt.events").enable() + +-- Register buffer(s) +require("volt.events").add(buf) + +-- Register multiple buffers +require("volt.events").add({ buf1, buf2, buf3 }) +``` + +### Click Events + +```lua +{ + "Click me!", + "String", + function() + print("Clicked!") + volt.redraw(buf, "my_section") + end +} +``` + +### Hover Events + +```lua +{ + "Hover me!", + "Normal", + { + click = function() + print("Clicked!") + end, + hover = { + id = "hover_state_1", + callback = function() + vim.g.nvmark_hovered = "hover_state_1" + end, + redraw = { "section_to_update" } + } + } +} +``` + +Check hover state: + +```lua +if vim.g.nvmark_hovered == "hover_state_1" then + -- Element is being hovered +end +``` + +### Slider Interaction + +Sliders have special UI type: + +```lua +{ + text = "โ”โ”โ”โ”โ”โ”", + hl = "String", + { + ui_type = "slider", + click = function() + local new_val = ui.slider.val(width, label, xpad, { thumb = true }) + -- Update value + end + } +} +``` + +## Styling + +### Highlight Groups + +Volt provides these highlight groups: + +```lua +-- Dark backgrounds +ExDarkBg +ExDarkBorder + +-- Medium backgrounds +ExBlack2Bg +ExBlack2Border + +-- Light backgrounds +ExBlack3Bg +ExBlack3Border + +-- Colors +ExRed +ExYellow +ExBlue +ExGreen +ExLightGrey +CommentFg +``` + +These automatically adapt to: +- Your colorscheme +- NvChad's base46 theme system +- Light/dark background + +### Custom Highlights + +Override or create your own: + +```lua +vim.api.nvim_set_hl(0, "MyCustomHL", { + fg = "#ff5555", + bg = "#282a36", + bold = true +}) +``` + +## Examples + +### Complete Modal Dialog + +```lua +local volt = require("volt") +local ui = require("volt.ui") + +local function create_dialog() + local buf = vim.api.nvim_create_buf(false, true) + vim.bo[buf].bufhidden = "wipe" + + local confirmed = false + + local layout = { + { + name = "title", + lines = function() + return { + { { "โš  Confirmation Required", "Title" } } + } + end + }, + { + name = "separator", + lines = function() + return { ui.separator("โ”€", 50, "Comment") } + end + }, + { + name = "message", + lines = function() + return { + { { "Are you sure you want to continue?", "Normal" } } + } + end + }, + { + name = "buttons", + lines = function() + return { + ui.grid_row({ + { { " ", "Normal" } }, + { + { " โœ“ Confirm ", "String" }, + { + click = function() + confirmed = true + volt.close(buf) + end + } + }, + { { " ", "Normal" } }, + { + { " โœ— Cancel ", "Error" }, + { + click = function() + volt.close(buf) + end + } + } + }) + } + end + } + } + + volt.gen_data({{ + buf = buf, + layout = layout, + ns = vim.api.nvim_create_namespace("dialog"), + xpad = 2 + }}) + + local win = vim.api.nvim_open_win(buf, true, { + relative = "editor", + width = 50, + height = 8, + row = math.floor(vim.o.lines / 2) - 4, + col = math.floor(vim.o.columns / 2) - 25, + style = "minimal", + border = "rounded" + }) + + volt.run(buf, { h = 8, w = 50 }) + require("volt.events").add(buf) + + return confirmed +end +``` + +### Settings Panel + +```lua +local function create_settings() + local buf = vim.api.nvim_create_buf(false, true) + + local settings = { + dark_mode = true, + auto_save = false, + font_size = 14 + } + + local layout = { + { + name = "header", + lines = function() + return { { { "โš™ Settings", "Title" } } } + end + }, + { + name = "separator1", + lines = function() + return { ui.separator("โ”€", 60, "Comment") } + end + }, + { + name = "dark_mode", + lines = function() + return { + ui.checkbox({ + active = settings.dark_mode, + txt = "Dark mode", + actions = function() + settings.dark_mode = not settings.dark_mode + volt.redraw(buf, "dark_mode") + end + }) + } + end + }, + { + name = "auto_save", + lines = function() + return { + ui.checkbox({ + active = settings.auto_save, + txt = "Auto-save", + actions = function() + settings.auto_save = not settings.auto_save + volt.redraw(buf, "auto_save") + end + }) + } + end + }, + { + name = "font_size", + lines = function() + return { + ui.slider.config({ + txt = "Font size: ", + val = math.floor((settings.font_size - 8) / 24 * 100), + w = 45, + hlon = "String", + thumb = true, + ratio_txt = false, + actions = function() + local percent = ui.slider.val(45, "Font size: ", 2, { thumb = true }) + settings.font_size = math.floor(8 + (percent / 100) * 24) + volt.redraw(buf, "font_size") + end + }) + } + end + } + } + + volt.gen_data({{ + buf = buf, + layout = layout, + ns = vim.api.nvim_create_namespace("settings"), + xpad = 2 + }}) + + local win = vim.api.nvim_open_win(buf, true, { + relative = "editor", + width = 60, + height = 12, + row = 5, + col = 10, + style = "minimal", + border = "rounded" + }) + + volt.run(buf, { h = 12, w = 60 }) + require("volt.events").add(buf) +end +``` + +### Statistics Dashboard + +```lua +local function create_dashboard() + local buf = vim.api.nvim_create_buf(false, true) + local graphs = require("volt.ui.graphs") + + local data = { 7, 8, 6, 9, 10, 8, 9 } + + local layout = { + { + name = "title", + lines = function() + return { { { "๐Ÿ“Š Weekly Statistics", "Title" } } } + end + }, + { + name = "separator", + lines = function() + return { ui.separator("โ”€", 70, "Comment") } + end + }, + { + name = "graph", + lines = function() + return graphs.bar({ + val = data, + baropts = { + w = 4, + gap = 2, + icon = "โ–ˆ", + format_hl = function(val) + if val >= 80 then return "String" end + if val >= 50 then return "Function" end + return "Comment" + end + }, + format_labels = function(val) + return tostring(val * 10) + end, + footer_label = { "Day of Week", "Comment" } + }) + end + }, + { + name = "stats", + lines = function() + local avg = 0 + for _, v in ipairs(data) do avg = avg + v end + avg = math.floor(avg / #data * 10) + + return { + { { "", "Normal" } }, + { { "Average: " .. avg .. "%", "Comment" } }, + { { "Peak: " .. math.max(unpack(data)) * 10 .. "%", "Comment" } } + } + end + } + } + + volt.gen_data({{ + buf = buf, + layout = layout, + ns = vim.api.nvim_create_namespace("dashboard"), + xpad = 5 + }}) + + local win = vim.api.nvim_open_win(buf, true, { + relative = "editor", + width = 70, + height = 20, + row = 2, + col = 10, + style = "minimal", + border = "rounded" + }) + + volt.run(buf, { h = 20, w = 70 }) + require("volt.events").add(buf) +end +``` + +## Tips & Best Practices + +1. **Always enable events**: Call `require("volt.events").enable()` once in your config +2. **Register buffers**: Use `require("volt.events").add(buf)` for interactive elements +3. **Use sections**: Name your layout sections for easy redraws +4. **Leverage closures**: Capture state in your `lines` functions +5. **Clean up**: Volt handles buffer deletion, but you can add custom cleanup +6. **Test interactivity**: Ensure click targets are appropriately sized +7. **Consider themes**: Use Volt's highlight groups for theme compatibility + +## Troubleshooting + +**Clicks not working?** +- Ensure `require("volt.events").enable()` was called +- Verify buffer is registered with `require("volt.events").add(buf)` +- Check if mouse is enabled: `:set mouse=a` + +**UI not rendering?** +- Confirm buffer is valid and window is visible +- Check if `volt.run(buf, opts)` was called +- Verify layout functions return proper format + +**Colors look wrong?** +- Load highlights: Volt automatically loads them, but ensure your colorscheme is set first +- Use `:so $VIMRUNTIME/syntax/hitest.vim` to see available highlights + +## Next Steps + +- Read the [Developer Guide](DEVELOPER_GUIDE.md) for advanced topics +- Check out the [source code](https://github.com/NvChad/volt) of Minty, Menu, and Typr +- Join the [NvChad Discord](https://discord.gg/gADmkJb9Fb) for help