Quick Start
Plugin Basics
A plugin is a Lua file placed in your user plugins directory that extends the editor's functionality.
- User plugins:
USERDIR/plugins/- Place your custom plugins here - Built-in plugins:
DATADIR/plugins/- Plugins that ship with the editor
USERDIR and DATADIR are global Lua variables available in all plugins. See Configuration → Location for where Pragtical looks for the user directory.
Common user plugin directory locations:
- Portable install:
<pragtical_executable_dir>/user/plugins/ - Linux/macOS:
~/.config/pragtical/plugins/ - Windows:
%USERPROFILE%/.config/pragtical/plugins/
Minimal Plugin Structure
-- mod-version:3
local core = require "core"
local command = require "core.command"
-- your plugin code here
command.add(nil, {
["myplugin:do-something"] = function()
core.log("Hello from my plugin!")
-- do plugin specific stuff
end
})
Key points:
- First line must be
-- mod-version:3 - Require the modules you need
- Register commands with
command.add() - Plugin file names must be valid Lua identifiers in lowercase (e.g.,
myplugin.lua,auto_save.lua) to work with config paths likeconfig.plugins.plugin_name
Example 1: Simple Command Plugin
A plugin that adds a command to insert the current date.
File: user/plugins/insertdate.lua
-- mod-version:3
local core = require "core"
local command = require "core.command"
local DocView = require "core.docview"
command.add("core.docview", {
["insertdate:insert-date"] = function()
local date = os.date("%Y-%m-%d")
core.active_view.doc:text_input(date)
end,
["insertdate:insert-datetime"] = function()
local datetime = os.date("%Y-%m-%d %H:%M:%S")
core.active_view.doc:text_input(datetime)
end
})
Usage: Open the command palette (Ctrl+Shift+P) and search for "insert date"
Explanation:
command.add("core.docview", ...)- Commands only work in document viewscore.active_view.doc:text_input()- Insert text at cursor- Command names use format:
category:action-name
Example 2: Running External Programs
A plugin that launches external programs and adds keyboard shortcuts.
File: user/plugins/runner.lua
-- mod-version:3
local core = require "core"
local command = require "core.command"
local keymap = require "core.keymap"
command.add(nil, {
["run:calculator"] = function()
local cmd = {"calc"} -- or any other OS program
process.start(cmd)
end,
["run:build-project"] = function()
local cwd = core.root_project().path
local cmd = {"build.bat"} -- build project in the background
process.start(cmd, {cwd = cwd})
end,
["run:build-project-show-window"] = function()
local cwd = core.root_project().path
local cmd = {"cmd", "/c", "start", "build.bat"} -- show cmd window
process.start(cmd, {cwd = cwd})
end,
})
-- Add keyboard shortcuts
keymap.add({
['ctrl+alt+c'] = "run:calculator",
['ctrl+b'] = "run:build-project-show-window",
})
Explanation:
process- Global API for running external programs (no require needed)process.start(cmd)- Launch external programs with command as a tableprocess.start(cmd, {cwd = cwd})- Specify working directory with options tablecore.root_project().path- Get current project's root directorykeymap.add()- Register keyboard shortcuts for commands- Commands run in the background without blocking the editor
Example 3: Creating a Custom View
A simple view that displays a message.
File: user/plugins/messageview.lua
-- mod-version:3
local core = require "core"
local command = require "core.command"
local View = require "core.view"
local style = require "core.style"
local MessageView = View:extend()
function MessageView:new()
MessageView.super.new(self)
self.message = "Hello, World!"
end
function MessageView:get_name()
return "Message View"
end
function MessageView:draw()
self:draw_background(style.background)
local font = style.font
local text = self.message
local w = font:get_width(text)
local h = font:get_height()
-- Center the text
local x = self.position.x + (self.size.x - w) / 2
local y = self.position.y + (self.size.y - h) / 2
renderer.draw_text(font, text, x, y, style.text)
end
-- Command to open the view
command.add(nil, {
["messageview:open"] = function()
local node = core.root_view:get_active_node()
node:add_view(MessageView())
end
})
Explanation:
- Extend
Viewwith:extend() - Call
super.new(self)in constructor - Override
get_name()for tab title - Override
draw()to render - Add to layout with
node:add_view()
Example 4: Interactive Custom View
A counter view with mouse interaction.
File: user/plugins/counterview.lua
-- mod-version:3
local core = require "core"
local command = require "core.command"
local View = require "core.view"
local style = require "core.style"
local CounterView = View:extend()
function CounterView:new()
CounterView.super.new(self)
self.count = 0
self.button_rect = { x = 0, y = 0, w = 100, h = 30 }
end
function CounterView:get_name()
return "Counter"
end
function CounterView:update_button_position()
-- Center the button
self.button_rect.x = self.position.x + (self.size.x - self.button_rect.w) / 2
self.button_rect.y = self.position.y + self.size.y / 2
end
function CounterView:update()
CounterView.super.update(self)
self:update_button_position()
end
function CounterView:draw()
self:draw_background(style.background)
local font = style.font
local big_font = style.big_font or font
-- Draw count at top
local count_text = tostring(self.count)
local tw = big_font:get_width(count_text)
local x = self.position.x + (self.size.x - tw) / 2
local y = self.position.y + 50
renderer.draw_text(big_font, count_text, x, y, style.text)
-- Draw button
local btn = self.button_rect
renderer.draw_rect(btn.x, btn.y, btn.w, btn.h, style.accent)
local btn_text = "Click Me"
local btw = font:get_width(btn_text)
local bth = font:get_height()
local btx = btn.x + (btn.w - btw) / 2
local bty = btn.y + (btn.h - bth) / 2
renderer.draw_text(font, btn_text, btx, bty, style.background)
end
function CounterView:on_mouse_pressed(button, x, y, clicks)
if button == "left" then
local btn = self.button_rect
-- Check if click is inside button
if x >= btn.x and x <= btn.x + btn.w and
y >= btn.y and y <= btn.y + btn.h then
self.count = self.count + 1
return true
end
end
return CounterView.super.on_mouse_pressed(self, button, x, y, clicks)
end
command.add(nil, {
["counterview:open"] = function()
local node = core.root_view:get_active_node()
node:add_view(CounterView())
end
})
-- Commands that only work when CounterView is active
command.add(CounterView, {
["counterview:increment"] = function(cv)
cv.count = cv.count + 1
end,
["counterview:reset"] = function(cv)
cv.count = 0
end
})
Explanation:
on_mouse_pressed()handles clicks- Return
trueto consume the event - Use rectangles for hit testing
command.add(CounterView, ...)- command only works in CounterView- Predicate function automatically passes the view as first parameter
Example 5: Plugin with Configuration
A plugin that shows how to add configuration.
File: user/plugins/myconfig.lua
-- mod-version:3
local core = require "core"
local config = require "core.config"
local command = require "core.command"
local common = require "core.common"
local style = require "core.style"
-- Initialize config with defaults
config.plugins.myconfig = common.merge({
enabled = true,
max_count = 100,
custom_color = { 100, 150, 200, 255 },
-- Config spec for settings GUI
config_spec = {
name = "My Config Plugin",
{
label = "Enabled",
description = "Enable or disable this plugin",
path = "enabled",
type = "toggle",
default = true
},
{
label = "Max Count",
description = "Maximum counter value",
path = "max_count",
type = "number",
default = 100,
min = 1,
max = 1000
},
{
label = "Custom Color",
description = "Color for highlighting",
path = "custom_color",
type = "color",
default = { 100, 150, 200, 255 }
}
}
}, config.plugins.myconfig)
-- Use the config
command.add(nil, {
["myconfig:show-settings"] = function()
core.log("Enabled: %s, Max: %d",
tostring(config.plugins.myconfig.enabled),
config.plugins.myconfig.max_count)
end
})
Explanation:
- Use
common.merge()to merge defaults with user settings config_specenables GUI configuration- Types:
"toggle","number","string","color","list_strings"
Example 6: Using CommandView for User Input
A plugin that presents a list of choices to the user.
File: user/plugins/choices.lua
-- mod-version:3
local core = require "core"
local command = require "core.command"
command.add(nil, {
["choices:pick-color"] = function()
-- List of color choices
local colors = {
"Red",
"Green",
"Blue",
"Yellow",
"Purple",
"Orange"
}
core.command_view:enter("Pick a Color", {
submit = function(text, item)
-- item is the selected suggestion
local choice = item and item.text or text
core.log("You selected: %s", choice)
end,
suggest = function(text)
-- Filter colors based on user input
local suggestions = {}
for _, color in ipairs(colors) do
if color:lower():find(text:lower(), 1, true) then
table.insert(suggestions, { text = color })
end
end
return suggestions
end
})
end
})
Explanation:
core.command_view:enter()- Opens the command palette with custom inputsubmit- Called when user presses Enter (receives text and selected item)suggest- Returns list of suggestions based on user input- Suggestions are tables with
textfield - User can type to filter the list or use arrow keys to select
Command System
Command Predicates
Commands can have predicates that determine when they're available:
-- Always available
command.add(nil, { ... })
-- Only in DocView
command.add("core.docview", { ... })
-- Only in specific view (inheritance check)
command.add(MyView, { ... })
-- Strict type check (exact match)
command.add("core.docview!", { ... })
-- Custom predicate function
command.add(function()
return core.active_view:is(DocView)
and core.active_view.doc:has_selection()
end, {
["mycmd:do-something"] = function()
-- Only runs if predicate returns true
end
})
Predicate with Extra Parameters
command.add(function()
local view = core.active_view
if view:is(DocView) and view.doc:has_selection() then
return true, view, view.doc -- Pass view and doc to command
end
return false
end, {
["mycmd:process"] = function(view, doc)
-- Receives view and doc from predicate
local text = doc:get_text(doc:get_selection())
core.log("Selected: %s", text)
end
})
Useful APIs
Core APIs
core.log("Message: %s", value) -- Log to console
core.error("Error: %s", message) -- Log error
core.active_view -- Current view
core.root_view -- Root view
core.root_view:get_active_node() -- Active node
Document APIs
doc:text_input(text) -- Insert text at cursor
doc:get_selection() -- Get selection range (line1, col1, line2, col2)
doc:get_text(line1, col1, line2, col2) -- Get text in range
doc:insert(line, col, text) -- Insert text at position
doc:remove(line1, col1, line2, col2) -- Remove text range
doc:set_selection(line1, col1, line2, col2) -- Set selection range
doc:has_selection() -- Check if text is selected
Renderer APIs
renderer.draw_rect(x, y, w, h, color) -- Draw filled rectangle
renderer.draw_text(font, text, x, y, color) -- Draw text
renderer.set_clip_rect(x, y, w, h) -- Set clipping region
Style
style.font -- Default font
style.big_font -- Larger font
style.code_font -- Monospace font
style.background -- Background color
style.text -- Text color
style.accent -- Accent color
style.selection -- Selection color
Plugin Loading
- User plugins in
user/plugins/ - Built-in plugins in
data/plugins/ - You can disable any plugin via
config.plugins.pluginname = falsein your init.lua
Tips
- Always save original functions when overriding
- Call super methods in constructors and overrides
- Use config.plugins.yourname for configuration
- Test predicates to ensure commands appear correctly
- Keep draw() fast - it's called every frame
- Return true from event handlers to consume events
Next Steps
- Read the other sections for a deeper understanding of the internals
- Look at built-in plugins in
data/plugins/for examples - Check
data/core/commands/for built-in command implementations