1982 lines
57 KiB
Lua
1982 lines
57 KiB
Lua
-- mod-version:3 --priority:0
|
|
local core = require "core"
|
|
local config = require "core.config"
|
|
local common = require "core.common"
|
|
local command = require "core.command"
|
|
local keymap = require "core.keymap"
|
|
local style = require "core.style"
|
|
local View = require "core.view"
|
|
local DocView = require "core.docview"
|
|
|
|
-- check if widget is installed before proceeding
|
|
local widget_found, Widget = pcall(require, "libraries.widget")
|
|
if not widget_found then
|
|
core.error("Widget library not found: https://github.com/lite-xl/lite-xl-widgets")
|
|
return false
|
|
end
|
|
|
|
local Label = require "libraries.widget.label"
|
|
local Line = require "libraries.widget.line"
|
|
local NoteBook = require "libraries.widget.notebook"
|
|
local Button = require "libraries.widget.button"
|
|
local TextBox = require "libraries.widget.textbox"
|
|
local SelectBox = require "libraries.widget.selectbox"
|
|
local NumberBox = require "libraries.widget.numberbox"
|
|
local Toggle = require "libraries.widget.toggle"
|
|
local ListBox = require "libraries.widget.listbox"
|
|
local FoldingBook = require "libraries.widget.foldingbook"
|
|
local FontsList = require "libraries.widget.fontslist"
|
|
local ItemsList = require "libraries.widget.itemslist"
|
|
local KeybindingDialog = require "libraries.widget.keybinddialog"
|
|
local Fonts = require "libraries.widget.fonts"
|
|
local FilePicker = require "libraries.widget.filepicker"
|
|
local ColorPicker = require "libraries.widget.colorpicker"
|
|
local MessageBox = require "libraries.widget.messagebox"
|
|
|
|
---@class plugins.settings
|
|
local settings = {}
|
|
|
|
settings.core = {}
|
|
settings.plugins = {}
|
|
settings.sections = {}
|
|
settings.plugin_sections = {}
|
|
settings.config = {}
|
|
settings.default_keybindings = {}
|
|
|
|
---Enumeration for the different types of settings.
|
|
---@type table<string, integer>
|
|
settings.type = {
|
|
STRING = 1,
|
|
NUMBER = 2,
|
|
TOGGLE = 3,
|
|
SELECTION = 4,
|
|
LIST_STRINGS = 5,
|
|
BUTTON = 6,
|
|
FONT = 7,
|
|
FILE = 8,
|
|
DIRECTORY = 9,
|
|
COLOR = 10
|
|
}
|
|
|
|
---@alias settings.types
|
|
---| `settings.type.STRING`
|
|
---| `settings.type.NUMBER`
|
|
---| `settings.type.TOGGLE`
|
|
---| `settings.type.SELECTION`
|
|
---| `settings.type.LIST_STRINGS`
|
|
---| `settings.type.BUTTON`
|
|
---| `settings.type.FONT`
|
|
---| `settings.type.FILE`
|
|
---| `settings.type.DIRECTORY`
|
|
---| `settings.type.COLOR`
|
|
|
|
---Represents a setting to render on a settings pane.
|
|
---@class settings.option
|
|
---Title displayed to the user eg: "My Option"
|
|
---@field public label string
|
|
---Description of the option eg: "Modifies the document indentation"
|
|
---@field public description string
|
|
---Config path in the config table, eg: section.myoption, myoption, etc...
|
|
---@field public path string
|
|
---Type of option that will be used to render an appropriate control
|
|
---@field public type settings.types | integer
|
|
---Default value of the option
|
|
---@field public default string | number | boolean | table<integer, string> | table<integer, integer>
|
|
---Used for NUMBER to indicate the minimum number allowed
|
|
---@field public min number
|
|
---Used for NUMBER to indiciate the maximum number allowed
|
|
---@field public max number
|
|
---Used for NUMBER to indiciate the increment/decrement amount
|
|
---@field public step number
|
|
---Used in a SELECTION to provide the list of valid options
|
|
---@field public values table
|
|
---Optionally used for FONT to store the generated font group.
|
|
---@field public fonts_list table<string, renderer.font>
|
|
---Flag set to true when loading user defined fonts fail
|
|
---@field public font_error boolean
|
|
---Optional function that is used to manipulate the current value on retrieval.
|
|
---@field public get_value nil | fun(value:any):any
|
|
---Optional function that is used to manipulate the saved value on save.
|
|
---@field public set_value nil | fun(value:any):any
|
|
---The icon set for a BUTTON
|
|
---@field public icon string
|
|
---Command or function executed when a BUTTON is clicked
|
|
---@field public on_click nil | string | fun(button:string, x:integer, y:integer)
|
|
---Optional function executed when the option value is applied.
|
|
---@field public on_apply nil | fun(value:any)
|
|
---When FILE or DIRECTORY this flag tells the path should exist.
|
|
---@field public exists boolean
|
|
---Lua patterns used on FILE or DIRECTORY to filter browser results and
|
|
---also force the selection to match one of the filters.
|
|
---@field public filters table<integer,string>
|
|
|
|
---Add a new settings section to the settings UI
|
|
---@param section string
|
|
---@param options settings.option[]
|
|
---@param plugin_name? string Optional name of plugin
|
|
---@param overwrite? boolean Overwrite previous section options
|
|
function settings.add(section, options, plugin_name, overwrite)
|
|
local category = ""
|
|
if plugin_name ~= nil then
|
|
category = "plugins"
|
|
else
|
|
category = "core"
|
|
end
|
|
|
|
if overwrite and settings[category][section] then
|
|
settings[category][section] = {}
|
|
end
|
|
|
|
if not settings[category][section] then
|
|
settings[category][section] = {}
|
|
if category ~= "plugins" then
|
|
table.insert(settings.sections, section)
|
|
else
|
|
table.insert(settings.plugin_sections, section)
|
|
end
|
|
end
|
|
|
|
if plugin_name ~= nil then
|
|
if not settings[category][section][plugin_name] then
|
|
settings[category][section][plugin_name] = {}
|
|
end
|
|
for _, option in ipairs(options) do
|
|
table.insert(settings[category][section][plugin_name], option)
|
|
end
|
|
else
|
|
for _, option in ipairs(options) do
|
|
table.insert(settings[category][section], option)
|
|
end
|
|
end
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Add Core Settings
|
|
--------------------------------------------------------------------------------
|
|
|
|
settings.add("General",
|
|
{
|
|
{
|
|
label = "User Module",
|
|
description = "Open your init.lua for customizations.",
|
|
type = settings.type.BUTTON,
|
|
icon = "P",
|
|
on_click = "core:open-user-module"
|
|
},
|
|
{
|
|
label = "Clear Fonts Cache",
|
|
description = "Delete current font cache and regenerate a fresh one.",
|
|
type = settings.type.BUTTON,
|
|
icon = "C",
|
|
on_click = function()
|
|
if Fonts.cache_is_building() then
|
|
MessageBox.warning(
|
|
"Clear Fonts Cache",
|
|
{ "The font cache is already been built,\n"
|
|
.. "status will be logged on the core log."
|
|
}
|
|
)
|
|
else
|
|
MessageBox.info(
|
|
"Clear Fonts Cache",
|
|
{ "Re-building the font cache can take some time,\n"
|
|
.. "it is needed when you have installed new fonts\n"
|
|
.. "which are not listed on the font picker tool.\n\n"
|
|
.. "Do you want to continue?"
|
|
},
|
|
function(_, button_id, _)
|
|
if button_id == 1 then
|
|
Fonts.clean_cache()
|
|
end
|
|
end,
|
|
MessageBox.BUTTONS_YES_NO
|
|
)
|
|
end
|
|
end
|
|
},
|
|
{
|
|
label = "Maximum Project Files",
|
|
description = "The maximum amount of project files to register.",
|
|
path = "max_project_files",
|
|
type = settings.type.NUMBER,
|
|
default = 2000,
|
|
min = 1,
|
|
max = 100000,
|
|
on_apply = function()
|
|
core.rescan_project_directories()
|
|
end
|
|
},
|
|
{
|
|
label = "File Size Limit",
|
|
description = "The maximum file size in megabytes allowed for editing.",
|
|
path = "file_size_limit",
|
|
type = settings.type.NUMBER,
|
|
default = 10,
|
|
min = 1,
|
|
max = 50
|
|
},
|
|
{
|
|
label = "Ignore Files",
|
|
description = "List of lua patterns matching files to be ignored by the editor.",
|
|
path = "ignore_files",
|
|
type = settings.type.LIST_STRINGS,
|
|
default = {
|
|
-- folders
|
|
"^%.svn/", "^%.git/", "^%.hg/", "^CVS/", "^%.Trash/", "^%.Trash%-.*/",
|
|
"^node_modules/", "^%.cache/", "^__pycache__/",
|
|
-- files
|
|
"%.pyc$", "%.pyo$", "%.exe$", "%.dll$", "%.obj$", "%.o$",
|
|
"%.a$", "%.lib$", "%.so$", "%.dylib$", "%.ncb$", "%.sdf$",
|
|
"%.suo$", "%.pdb$", "%.idb$", "%.class$", "%.psd$", "%.db$",
|
|
"^desktop%.ini$", "^%.DS_Store$", "^%.directory$",
|
|
},
|
|
on_apply = function()
|
|
core.rescan_project_directories()
|
|
end
|
|
},
|
|
{
|
|
label = "Maximum Clicks",
|
|
description = "The maximum amount of consecutive clicks that are registered by the editor.",
|
|
path = "max_clicks",
|
|
type = settings.type.NUMBER,
|
|
default = 3,
|
|
min = 1,
|
|
max = 10
|
|
},
|
|
}
|
|
)
|
|
|
|
settings.add("Graphics",
|
|
{
|
|
{
|
|
label = "Frames Per Second",
|
|
description = "Lower value for low end machines and higher for a smoother experience.",
|
|
path = "fps",
|
|
type = settings.type.NUMBER,
|
|
default = 60,
|
|
min = 10,
|
|
max = 300
|
|
},
|
|
{
|
|
label = "Transitions",
|
|
description = "If disabled turns off all transitions but improves rendering performance.",
|
|
path = "transitions",
|
|
type = settings.type.TOGGLE,
|
|
default = true
|
|
},
|
|
{
|
|
label = "Animation Rate",
|
|
description = "The amount of time it takes for a transition to finish.",
|
|
path = "animation_rate",
|
|
type = settings.type.NUMBER,
|
|
default = 1.0,
|
|
min = 0.5,
|
|
max = 3.0,
|
|
step = 0.1
|
|
},
|
|
{
|
|
label = "Animate Mouse Drag Scroll",
|
|
description = "Causes higher cpu usage but smoother scroll transition.",
|
|
path = "animate_drag_scroll",
|
|
type = settings.type.TOGGLE,
|
|
default = false
|
|
},
|
|
{
|
|
label = "Disable Scrolling Transitions",
|
|
path = "disabled_transitions.scroll",
|
|
type = settings.type.TOGGLE,
|
|
default = false
|
|
},
|
|
{
|
|
label = "Disable Command View Transitions",
|
|
path = "disabled_transitions.commandview",
|
|
type = settings.type.TOGGLE,
|
|
default = false
|
|
},
|
|
{
|
|
label = "Disable Context Menu Transitions",
|
|
path = "disabled_transitions.contextmenu",
|
|
type = settings.type.TOGGLE,
|
|
default = false
|
|
},
|
|
{
|
|
label = "Disable Log View Transitions",
|
|
path = "disabled_transitions.logview",
|
|
type = settings.type.TOGGLE,
|
|
default = false
|
|
},
|
|
{
|
|
label = "Disable Nag Bar Transitions",
|
|
path = "disabled_transitions.nagbar",
|
|
type = settings.type.TOGGLE,
|
|
default = false
|
|
},
|
|
{
|
|
label = "Disable Tab Transitions",
|
|
path = "disabled_transitions.tabs",
|
|
type = settings.type.TOGGLE,
|
|
default = false
|
|
},
|
|
{
|
|
label = "Disable Tab Drag Transitions",
|
|
path = "disabled_transitions.tab_drag",
|
|
type = settings.type.TOGGLE,
|
|
default = false
|
|
},
|
|
{
|
|
label = "Disable Status Bar Transitions",
|
|
path = "disabled_transitions.statusbar",
|
|
type = settings.type.TOGGLE,
|
|
default = false
|
|
},
|
|
}
|
|
)
|
|
|
|
settings.add("User Interface",
|
|
{
|
|
{
|
|
label = "Font",
|
|
description = "The font and fallbacks used on non code text.",
|
|
path = "font",
|
|
type = settings.type.FONT,
|
|
fonts_list = style,
|
|
default = {
|
|
fonts = {
|
|
{
|
|
name = "Fira Sans Regular",
|
|
path = DATADIR .. "/fonts/FiraSans-Regular.ttf"
|
|
}
|
|
},
|
|
options = {
|
|
size = 15,
|
|
antialiasing = "subpixel",
|
|
hinting = "slight"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
label = "Borderless",
|
|
description = "Use built-in window decorations.",
|
|
path = "borderless",
|
|
type = settings.type.TOGGLE,
|
|
default = false,
|
|
on_apply = function()
|
|
core.configure_borderless_window()
|
|
end
|
|
},
|
|
{
|
|
label = "Always Show Tabs",
|
|
description = "Shows tabs even if a single document is opened.",
|
|
path = "always_show_tabs",
|
|
type = settings.type.TOGGLE,
|
|
default = true
|
|
},
|
|
{
|
|
label = "Maximum Tabs",
|
|
description = "The maximum amount of visible document tabs.",
|
|
path = "max_tabs",
|
|
type = settings.type.NUMBER,
|
|
default = 8,
|
|
min = 1,
|
|
max = 100
|
|
},
|
|
{
|
|
label = "Close Button on Tabs",
|
|
description = "Display the close button on tabs.",
|
|
path = "tab_close_button",
|
|
type = settings.type.TOGGLE,
|
|
default = true
|
|
},
|
|
{
|
|
label = "Mouse wheel scroll rate",
|
|
description = "The amount to scroll when using the mouse wheel.",
|
|
path = "mouse_wheel_scroll",
|
|
type = settings.type.NUMBER,
|
|
default = 50,
|
|
min = 10,
|
|
max = 200,
|
|
get_value = function(value)
|
|
return value / SCALE
|
|
end,
|
|
set_value = function(value)
|
|
return value * SCALE
|
|
end
|
|
},
|
|
{
|
|
label = "Force Scrollbar Status",
|
|
description = "Choose a fixed scrollbar state instead of resizing it on mouse hover.",
|
|
path = "force_scrollbar_status",
|
|
type = settings.type.SELECTION,
|
|
default = false,
|
|
values = {
|
|
{"Disabled", false},
|
|
{"Expanded", "expanded"},
|
|
{"Contracted", "contracted"}
|
|
},
|
|
on_apply = function(value)
|
|
local mode = config.force_scrollbar_status_mode or "global"
|
|
local globally = mode == "global"
|
|
local views = core.root_view.root_node:get_children()
|
|
for _, view in ipairs(views) do
|
|
if globally or view:extends(DocView) then
|
|
view.h_scrollbar:set_forced_status(value)
|
|
view.v_scrollbar:set_forced_status(value)
|
|
else
|
|
view.h_scrollbar:set_forced_status(false)
|
|
view.v_scrollbar:set_forced_status(false)
|
|
end
|
|
end
|
|
end
|
|
},
|
|
{
|
|
label = "Force Scrollbar Status Mode",
|
|
description = "Choose between applying globally or document views only.",
|
|
path = "force_scrollbar_status_mode",
|
|
type = settings.type.SELECTION,
|
|
default = "global",
|
|
values = {
|
|
{"Documents", "docview"},
|
|
{"Globally", "global"}
|
|
},
|
|
on_apply = function(value)
|
|
local globally = value == "global"
|
|
local views = core.root_view.root_node:get_children()
|
|
for _, view in ipairs(views) do
|
|
if globally or view:extends(DocView) then
|
|
view.h_scrollbar:set_forced_status(config.force_scrollbar_status)
|
|
view.v_scrollbar:set_forced_status(config.force_scrollbar_status)
|
|
else
|
|
view.h_scrollbar:set_forced_status(false)
|
|
view.v_scrollbar:set_forced_status(false)
|
|
end
|
|
end
|
|
end
|
|
},
|
|
{
|
|
label = "Disable Cursor Blinking",
|
|
description = "Disables cursor blinking on text input elements.",
|
|
path = "disable_blink",
|
|
type = settings.type.TOGGLE,
|
|
default = false
|
|
},
|
|
{
|
|
label = "Cursor Blinking Period",
|
|
description = "Interval in seconds in which the cursor blinks.",
|
|
path = "blink_period",
|
|
type = settings.type.NUMBER,
|
|
default = 0.8,
|
|
min = 0.3,
|
|
max = 2.0,
|
|
step = 0.1
|
|
}
|
|
}
|
|
)
|
|
|
|
settings.add("Editor",
|
|
{
|
|
{
|
|
label = "Code Font",
|
|
description = "The font and fallbacks used on the code editor.",
|
|
path = "code_font",
|
|
type = settings.type.FONT,
|
|
fonts_list = style,
|
|
default = {
|
|
fonts = {
|
|
{
|
|
name = "JetBrains Mono Regular",
|
|
path = DATADIR .. "/fonts/JetBrainsMono-Regular.ttf"
|
|
}
|
|
},
|
|
options = {
|
|
size = 15,
|
|
antialiasing = "subpixel",
|
|
hinting = "slight"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
label = "Indentation Type",
|
|
description = "The character inserted when pressing the tab key.",
|
|
path = "tab_type",
|
|
type = settings.type.SELECTION,
|
|
default = "soft",
|
|
values = {
|
|
{"Space", "soft"},
|
|
{"Tab", "hard"}
|
|
}
|
|
},
|
|
{
|
|
label = "Indentation Size",
|
|
description = "Amount of spaces shown per indentation.",
|
|
path = "indent_size",
|
|
type = settings.type.NUMBER,
|
|
default = 2,
|
|
min = 1,
|
|
max = 10
|
|
},
|
|
{
|
|
label = "Keep Newline Whitespace",
|
|
description = "Do not remove whitespace when pressing enter.",
|
|
path = "keep_newline_whitespace",
|
|
type = settings.type.TOGGLE,
|
|
default = false
|
|
},
|
|
{
|
|
label = "Line Limit",
|
|
description = "Amount of characters at which the line breaking column will be drawn.",
|
|
path = "line_limit",
|
|
type = settings.type.NUMBER,
|
|
default = 80,
|
|
min = 1
|
|
},
|
|
{
|
|
label = "Line Height",
|
|
description = "The amount of spacing between lines.",
|
|
path = "line_height",
|
|
type = settings.type.NUMBER,
|
|
default = 1.2,
|
|
min = 1.0,
|
|
max = 3.0,
|
|
step = 0.1
|
|
},
|
|
{
|
|
label = "Highlight Line",
|
|
description = "Highlight the current line.",
|
|
path = "highlight_current_line",
|
|
type = settings.type.SELECTION,
|
|
default = true,
|
|
values = {
|
|
{"Yes", true},
|
|
{"No", false},
|
|
{"No Selection", "no_selection"}
|
|
},
|
|
set_value = function(value)
|
|
if type(value) == "nil" then return false end
|
|
return value
|
|
end
|
|
},
|
|
{
|
|
label = "Maximum Undo History",
|
|
description = "The amount of undo elements to keep.",
|
|
path = "max_undos",
|
|
type = settings.type.NUMBER,
|
|
default = 10000,
|
|
min = 100,
|
|
max = 100000
|
|
},
|
|
{
|
|
label = "Undo Merge Timeout",
|
|
description = "Time in seconds before applying an undo action.",
|
|
path = "undo_merge_timeout",
|
|
type = settings.type.NUMBER,
|
|
default = 0.3,
|
|
min = 0.1,
|
|
max = 1.0,
|
|
step = 0.1
|
|
},
|
|
{
|
|
label = "Symbol Pattern",
|
|
description = "A lua pattern used to match symbols in the document.",
|
|
path = "symbol_pattern",
|
|
type = settings.type.STRING,
|
|
default = "[%a_][%w_]*"
|
|
},
|
|
{
|
|
label = "Non Word Characters",
|
|
description = "A string of characters that do not belong to a word.",
|
|
path = "non_word_chars",
|
|
type = settings.type.STRING,
|
|
default = " \\t\\n/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-",
|
|
get_value = function(value)
|
|
return value:gsub("\n", "\\n"):gsub("\t", "\\t")
|
|
end,
|
|
set_value = function(value)
|
|
return value:gsub("\\n", "\n"):gsub("\\t", "\t")
|
|
end
|
|
},
|
|
{
|
|
label = "Scroll Past the End",
|
|
description = "Allow scrolling beyond the document ending.",
|
|
path = "scroll_past_end",
|
|
type = settings.type.TOGGLE,
|
|
default = true
|
|
}
|
|
}
|
|
)
|
|
|
|
settings.add("Development",
|
|
{
|
|
{
|
|
label = "Core Log",
|
|
description = "Open the list of logged messages.",
|
|
type = settings.type.BUTTON,
|
|
icon = "f",
|
|
on_click = "core:open-log"
|
|
},
|
|
{
|
|
label = "Log Items",
|
|
description = "The maximum amount of entries to keep on the log UI.",
|
|
path = "max_log_items",
|
|
type = settings.type.NUMBER,
|
|
default = 800,
|
|
min = 150,
|
|
max = 2000
|
|
},
|
|
{
|
|
label = "Skip Plugins Version",
|
|
description = "Do not verify the plugins required versions at startup.",
|
|
path = "skip_plugins_version",
|
|
type = settings.type.TOGGLE,
|
|
default = false
|
|
}
|
|
}
|
|
)
|
|
|
|
settings.add("Status Bar",
|
|
{
|
|
{
|
|
label = "Enabled",
|
|
description = "Toggle the default visibility of the status bar.",
|
|
path = "statusbar.enabled",
|
|
type = settings.type.TOGGLE,
|
|
default = true,
|
|
on_apply = function(enabled)
|
|
if enabled then
|
|
core.status_view:show()
|
|
else
|
|
core.status_view:hide()
|
|
end
|
|
end
|
|
},
|
|
{
|
|
label = "Show Notifications",
|
|
description = "Toggle the visibility of status messages.",
|
|
path = "statusbar.messages",
|
|
type = settings.type.TOGGLE,
|
|
default = true,
|
|
on_apply = function(enabled)
|
|
core.status_view:display_messages(enabled)
|
|
end
|
|
},
|
|
{
|
|
label = "Messages Timeout",
|
|
description = "The amount in seconds before a notification dissapears.",
|
|
path = "message_timeout",
|
|
type = settings.type.NUMBER,
|
|
default = 5,
|
|
min = 1,
|
|
max = 30
|
|
}
|
|
}
|
|
)
|
|
|
|
---Retrieve from given config the associated value using the given path.
|
|
---@param conf table
|
|
---@param path string
|
|
---@param default any
|
|
---@return any | nil
|
|
local function get_config_value(conf, path, default)
|
|
local sections = {};
|
|
for match in (path.."."):gmatch("(.-)%.") do
|
|
table.insert(sections, match);
|
|
end
|
|
|
|
local element = conf
|
|
for _, section in ipairs(sections) do
|
|
if type(element[section]) ~= "nil" then
|
|
element = element[section]
|
|
else
|
|
return default
|
|
end
|
|
end
|
|
|
|
if type(element) == "nil" then
|
|
return default
|
|
end
|
|
|
|
return element
|
|
end
|
|
|
|
---Loops the given config table using the given path and store the value.
|
|
---@param conf table
|
|
---@param path string
|
|
---@param value any
|
|
local function set_config_value(conf, path, value)
|
|
local sections = {};
|
|
for match in (path.."."):gmatch("(.-)%.") do
|
|
table.insert(sections, match);
|
|
end
|
|
|
|
local sections_count = #sections
|
|
|
|
if sections_count == 1 then
|
|
conf[sections[1]] = value
|
|
return
|
|
elseif type(conf[sections[1]]) ~= "table" then
|
|
conf[sections[1]] = {}
|
|
end
|
|
|
|
local element = conf
|
|
for idx, section in ipairs(sections) do
|
|
if type(element[section]) ~= "table" then
|
|
element[section] = {}
|
|
element = element[section]
|
|
else
|
|
element = element[section]
|
|
end
|
|
if idx + 1 == sections_count then break end
|
|
end
|
|
|
|
element[sections[sections_count]] = value
|
|
end
|
|
|
|
---Get a list of system and user installed plugins.
|
|
---@return table<integer, string>
|
|
local function get_installed_plugins()
|
|
local files, ordered = {}, {}
|
|
|
|
for _, root_dir in ipairs {DATADIR, USERDIR} do
|
|
local plugin_dir = root_dir .. "/plugins"
|
|
for _, filename in ipairs(system.list_dir(plugin_dir) or {}) do
|
|
local valid = false
|
|
local file_info = system.get_file_info(plugin_dir .. "/" .. filename)
|
|
if file_info then
|
|
if
|
|
file_info.type == "file"
|
|
and
|
|
filename:match("%.lua$")
|
|
and
|
|
not filename:match("^language_")
|
|
then
|
|
valid = true
|
|
filename = filename:gsub("%.lua$", "")
|
|
elseif file_info.type == "dir" then
|
|
if system.get_file_info(plugin_dir .. "/" .. filename .. "/init.lua") then
|
|
valid = true
|
|
end
|
|
end
|
|
end
|
|
if valid then
|
|
if not files[filename] then table.insert(ordered, filename) end
|
|
files[filename] = true
|
|
end
|
|
end
|
|
end
|
|
|
|
table.sort(ordered)
|
|
|
|
return ordered
|
|
end
|
|
|
|
---Get a list of system and user installed colors.
|
|
---@return table<integer, table>
|
|
local function get_installed_colors()
|
|
local files, ordered = {}, {}
|
|
|
|
for _, root_dir in ipairs {DATADIR, USERDIR} do
|
|
local dir = root_dir .. "/colors"
|
|
for _, filename in ipairs(system.list_dir(dir) or {}) do
|
|
local file_info = system.get_file_info(dir .. "/" .. filename)
|
|
if
|
|
file_info and file_info.type == "file"
|
|
and
|
|
filename:match("%.lua$")
|
|
then
|
|
-- read colors
|
|
local contents = io.open(dir .. "/" .. filename):read("*a")
|
|
local colors = {}
|
|
for r, g, b in contents:gmatch("#(%x%x)(%x%x)(%x%x)") do
|
|
r = tonumber(r, 16)
|
|
g = tonumber(g, 16)
|
|
b = tonumber(b, 16)
|
|
table.insert(colors, { r, g, b, 0xff })
|
|
end
|
|
-- sort colors from darker to lighter
|
|
table.sort(colors, function(a, b)
|
|
return a[1] + a[2] + a[3] < b[1] + b[2] + b[3]
|
|
end)
|
|
-- remove duplicate colors
|
|
local b = {}
|
|
for i = #colors, 1, -1 do
|
|
local a = colors[i]
|
|
if a[1] == b[1] and a[2] == b[2] and a[3] == b[3] then
|
|
table.remove(colors, i)
|
|
else
|
|
b = colors[i]
|
|
end
|
|
end
|
|
-- insert color to ordered table if not duplicate
|
|
filename = filename:gsub("%.lua$", "")
|
|
if not files[filename] then
|
|
table.insert(ordered, {name = filename, colors = colors})
|
|
end
|
|
files[filename] = true
|
|
end
|
|
end
|
|
end
|
|
|
|
table.sort(ordered, function(a, b) return a.name < b.name end)
|
|
|
|
return ordered
|
|
end
|
|
|
|
---Capitalize first letter of every word.
|
|
---Taken from core.command.
|
|
---@param words string
|
|
---@return string
|
|
local function capitalize_first(words)
|
|
return words:sub(1, 1):upper() .. words:sub(2)
|
|
end
|
|
|
|
---Similar to command prettify_name but also takes care of underscores.
|
|
---@param name string
|
|
---@return string
|
|
local function prettify_name(name)
|
|
name = name:gsub("[%-_]", " "):gsub("%S+", capitalize_first)
|
|
return name
|
|
end
|
|
|
|
---Load config options from the USERDIR user_settings.lua and store them on
|
|
---settings.config for later usage.
|
|
local function load_settings()
|
|
local ok, t = pcall(dofile, USERDIR .. "/user_settings.lua")
|
|
settings.config = ok and t.config or {}
|
|
end
|
|
|
|
---Save current config options into the USERDIR user_settings.lua
|
|
local function save_settings()
|
|
local fp = io.open(USERDIR .. "/user_settings.lua", "w")
|
|
if fp then
|
|
local output = "{\n [\"config\"] = "
|
|
.. common.serialize(
|
|
settings.config,
|
|
{ pretty = true, escape = true, sort = true, initial_indent = 1 }
|
|
):gsub("^%s+", "")
|
|
.. "\n}\n"
|
|
fp:write("return ", output)
|
|
fp:close()
|
|
end
|
|
end
|
|
|
|
---Apply a keybinding and optionally save it.
|
|
---@param cmd string
|
|
---@param bindings table<integer, string>
|
|
---@param skip_save? boolean
|
|
---@return table | nil
|
|
local function apply_keybinding(cmd, bindings, skip_save)
|
|
local row_value = nil
|
|
local changed = false
|
|
|
|
local original_bindings = { keymap.get_binding(cmd) }
|
|
for _, binding in ipairs(original_bindings) do
|
|
keymap.unbind(binding, cmd)
|
|
end
|
|
|
|
if #bindings > 0 then
|
|
if
|
|
not skip_save
|
|
and
|
|
settings.config.custom_keybindings
|
|
and
|
|
settings.config.custom_keybindings[cmd]
|
|
then
|
|
settings.config.custom_keybindings[cmd] = {}
|
|
end
|
|
local shortcuts = ""
|
|
for _, binding in ipairs(bindings) do
|
|
if not binding:match("%+$") and binding ~= "" and binding ~= "none" then
|
|
keymap.add({[binding] = cmd})
|
|
shortcuts = shortcuts .. binding .. "\n"
|
|
if not skip_save then
|
|
if not settings.config.custom_keybindings then
|
|
settings.config.custom_keybindings = {}
|
|
settings.config.custom_keybindings[cmd] = {}
|
|
elseif not settings.config.custom_keybindings[cmd] then
|
|
settings.config.custom_keybindings[cmd] = {}
|
|
end
|
|
table.insert(settings.config.custom_keybindings[cmd], binding)
|
|
changed = true
|
|
end
|
|
end
|
|
end
|
|
if shortcuts ~= "" then
|
|
local bindings_list = shortcuts:gsub("\n$", "")
|
|
row_value = {
|
|
style.text, cmd, ListBox.COLEND, style.dim, bindings_list
|
|
}
|
|
end
|
|
elseif
|
|
not skip_save
|
|
and
|
|
settings.config.custom_keybindings
|
|
and
|
|
settings.config.custom_keybindings[cmd]
|
|
then
|
|
settings.config.custom_keybindings[cmd] = nil
|
|
changed = true
|
|
end
|
|
|
|
if changed then
|
|
save_settings()
|
|
end
|
|
|
|
if not row_value then
|
|
row_value = {
|
|
style.text, cmd, ListBox.COLEND, style.dim, "none"
|
|
}
|
|
end
|
|
|
|
return row_value
|
|
end
|
|
|
|
---Load the saved fonts into the config path or fonts_list table.
|
|
---@param option settings.option
|
|
---@param path string
|
|
---@param saved_value any
|
|
local function merge_font_settings(option, path, saved_value)
|
|
local font_options = saved_value.options or {
|
|
size = 15,
|
|
antialiasing = "supixel",
|
|
hinting = "slight"
|
|
}
|
|
font_options.size = font_options.size or 15
|
|
font_options.antialiasing = font_options.antialiasing or "subpixel"
|
|
font_options.hinting = font_options.hinting or "slight"
|
|
|
|
local fonts = {}
|
|
local font_loaded = true
|
|
for _, font in ipairs(saved_value.fonts) do
|
|
local font_data = nil
|
|
font_loaded = core.try(function()
|
|
font_data = renderer.font.load(
|
|
font.path, font_options.size * SCALE, font_options
|
|
)
|
|
end)
|
|
if font_loaded then
|
|
table.insert(fonts, font_data)
|
|
else
|
|
option.font_error = true
|
|
core.error("Settings: could not load %s\n'%s - %s'", path, font.name, font.path)
|
|
break
|
|
end
|
|
end
|
|
|
|
if font_loaded then
|
|
if option.fonts_list then
|
|
set_config_value(option.fonts_list, option.path, renderer.font.group(fonts))
|
|
else
|
|
set_config_value(config, path, renderer.font.group(fonts))
|
|
end
|
|
end
|
|
end
|
|
|
|
---Load the user_settings.lua stored options for a plugin into global config.
|
|
---@param plugin_name string
|
|
---@param options settings.option[]
|
|
local function merge_plugin_settings(plugin_name, options)
|
|
for _, option in pairs(options) do
|
|
if type(option.path) == "string" then
|
|
local path = "plugins." .. plugin_name .. "." .. option.path
|
|
local saved_value = get_config_value(settings.config, path)
|
|
if type(saved_value) ~= "nil" then
|
|
if option.type == settings.type.FONT or option.type == "font" then
|
|
merge_font_settings(option, path, saved_value)
|
|
else
|
|
set_config_value(config, path, saved_value)
|
|
end
|
|
if option.on_apply then
|
|
option.on_apply(saved_value)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
---Merge previously saved settings without destroying the config table.
|
|
local function merge_settings()
|
|
if type(settings.config) ~= "table" then return end
|
|
|
|
-- merge core settings
|
|
for _, section in ipairs(settings.sections) do
|
|
local options = settings.core[section]
|
|
for _, option in ipairs(options) do
|
|
if type(option.path) == "string" then
|
|
local saved_value = get_config_value(settings.config, option.path)
|
|
if type(saved_value) ~= "nil" then
|
|
if option.type == settings.type.FONT or option.type == "font" then
|
|
merge_font_settings(option, option.path, saved_value)
|
|
else
|
|
set_config_value(config, option.path, saved_value)
|
|
end
|
|
if option.on_apply then
|
|
option.on_apply(saved_value)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- merge plugin settings
|
|
table.sort(settings.plugin_sections)
|
|
for _, section in ipairs(settings.plugin_sections) do
|
|
local plugins = settings.plugins[section]
|
|
for plugin_name, options in pairs(plugins) do
|
|
merge_plugin_settings(plugin_name, options)
|
|
end
|
|
end
|
|
|
|
-- apply custom keybindings
|
|
if settings.config.custom_keybindings then
|
|
for cmd, bindings in pairs(settings.config.custom_keybindings) do
|
|
apply_keybinding(cmd, bindings, true)
|
|
end
|
|
end
|
|
end
|
|
|
|
---Scan all plugins to check if they define a config_spec and load it.
|
|
local function scan_plugins_spec()
|
|
for plugin, conf in pairs(config.plugins) do
|
|
if type(conf) == "table" and conf.config_spec then
|
|
settings.add(
|
|
conf.config_spec.name,
|
|
conf.config_spec,
|
|
plugin
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
---Called at core first run to store the default keybindings.
|
|
local function store_default_keybindings()
|
|
for name, _ in pairs(command.map) do
|
|
local keys = { keymap.get_binding(name) }
|
|
if #keys > 0 then
|
|
settings.default_keybindings[name] = keys
|
|
end
|
|
end
|
|
end
|
|
|
|
---@class settings.ui : widget
|
|
---@field private notebook widget.notebook
|
|
---@field private core widget
|
|
---@field private colors widget
|
|
---@field private plugins widget
|
|
---@field private keybinds widget
|
|
---@field private about widget
|
|
---@field private core_sections widget.foldingbook
|
|
---@field private plugin_sections widget.foldingbook
|
|
local Settings = Widget:extend()
|
|
|
|
---Constructor
|
|
function Settings:new()
|
|
Settings.super.new(self, nil, false)
|
|
|
|
self.name = "Settings"
|
|
self.defer_draw = false
|
|
self.border.width = 0
|
|
self.draggable = false
|
|
self.scrollable = false
|
|
|
|
---@type widget.notebook
|
|
self.notebook = NoteBook(self)
|
|
self.notebook.size.x = 250
|
|
self.notebook.size.y = 300
|
|
self.notebook.border.width = 0
|
|
|
|
self.core = self.notebook:add_pane("core", "Core")
|
|
self.colors = self.notebook:add_pane("colors", "Colors")
|
|
self.plugins = self.notebook:add_pane("plugins", "Plugins")
|
|
self.keybinds = self.notebook:add_pane("keybindings", "Keybindings")
|
|
self.about = self.notebook:add_pane("about", "About")
|
|
|
|
self.notebook:set_pane_icon("core", "P")
|
|
self.notebook:set_pane_icon("colors", "W")
|
|
self.notebook:set_pane_icon("plugins", "B")
|
|
self.notebook:set_pane_icon("keybindings", "M")
|
|
self.notebook:set_pane_icon("about", "i")
|
|
|
|
self.core_sections = FoldingBook(self.core)
|
|
self.core_sections.border.width = 0
|
|
self.core_sections.scrollable = false
|
|
|
|
self.plugin_sections = FoldingBook(self.plugins)
|
|
self.plugin_sections.border.width = 0
|
|
self.plugin_sections.scrollable = false
|
|
|
|
self:load_core_settings()
|
|
self:load_color_settings()
|
|
self:load_plugin_settings()
|
|
self:load_keymap_settings()
|
|
|
|
self:setup_about()
|
|
end
|
|
|
|
---Helper function to add control for both core and plugin settings.
|
|
---@oaram pane widget
|
|
---@param option settings.option
|
|
---@param plugin_name? string | nil
|
|
local function add_control(pane, option, plugin_name)
|
|
local found = false
|
|
local path = type(plugin_name) ~= "nil" and
|
|
"plugins." .. plugin_name .. "." .. option.path or option.path
|
|
local option_value = nil
|
|
if type(path) ~= "nil" then
|
|
option_value = get_config_value(config, path, option.default)
|
|
end
|
|
|
|
if option.get_value then
|
|
option_value = option.get_value(option_value)
|
|
end
|
|
|
|
---@type widget
|
|
local widget = nil
|
|
|
|
if type(option.type) == "string" then
|
|
option.type = settings.type[option.type:upper()]
|
|
end
|
|
|
|
if option.type == settings.type.NUMBER then
|
|
---@type widget.label
|
|
Label(pane, option.label .. ":")
|
|
---@type widget.numberbox
|
|
local number = NumberBox(pane, option_value, option.min, option.max, option.step)
|
|
widget = number
|
|
found = true
|
|
|
|
elseif option.type == settings.type.TOGGLE then
|
|
---@type widget.toggle
|
|
local toggle = Toggle(pane, option.label, option_value)
|
|
widget = toggle
|
|
found = true
|
|
|
|
elseif option.type == settings.type.STRING then
|
|
---@type widget.label
|
|
Label(pane, option.label .. ":")
|
|
---@type widget.textbox
|
|
local string = TextBox(pane, option_value or "")
|
|
widget = string
|
|
found = true
|
|
|
|
elseif option.type == settings.type.SELECTION then
|
|
---@type widget.label
|
|
Label(pane, option.label .. ":")
|
|
---@type widget.selectbox
|
|
local select = SelectBox(pane)
|
|
for _, data in pairs(option.values) do
|
|
select:add_option(data[1], data[2])
|
|
end
|
|
for idx, _ in ipairs(select.list.rows) do
|
|
if select.list:get_row_data(idx) == option_value then
|
|
select:set_selected(idx-1)
|
|
break
|
|
end
|
|
end
|
|
widget = select
|
|
found = true
|
|
|
|
elseif option.type == settings.type.BUTTON then
|
|
---@type widget.button
|
|
local button = Button(pane, option.label)
|
|
if option.icon then
|
|
button:set_icon(option.icon)
|
|
end
|
|
if option.on_click then
|
|
local command_type = type(option.on_click)
|
|
if command_type == "string" then
|
|
function button:on_click()
|
|
command.perform(option.on_click)
|
|
end
|
|
elseif command_type == "function" then
|
|
button.on_click = option.on_click
|
|
end
|
|
end
|
|
widget = button
|
|
found = true
|
|
|
|
elseif option.type == settings.type.LIST_STRINGS then
|
|
---@type widget.label
|
|
Label(pane, option.label .. ":")
|
|
---@type widget.itemslist
|
|
local list = ItemsList(pane)
|
|
if type(option_value) == "table" then
|
|
for _, value in ipairs(option_value) do
|
|
list:add_item(value)
|
|
end
|
|
end
|
|
widget = list
|
|
found = true
|
|
|
|
elseif option.type == settings.type.FONT then
|
|
--get fonts without conversion to renderer.font
|
|
if type(path) ~= "nil" then
|
|
if not option.font_error then
|
|
option_value = get_config_value(settings.config, path, option.default)
|
|
else
|
|
--fallback to default fonts if error loading user defined ones
|
|
option_value = option.default
|
|
end
|
|
end
|
|
---@type widget.label
|
|
Label(pane, option.label .. ":")
|
|
---@type widget.fontslist
|
|
local fonts = FontsList(pane)
|
|
if type(option_value) == "table" then
|
|
for _, font in ipairs(option_value.fonts) do
|
|
fonts:add_font(font)
|
|
end
|
|
|
|
local font_options = option_value.options or {
|
|
size = 15,
|
|
antialiasing = "supixel",
|
|
hinting = "slight"
|
|
}
|
|
font_options.size = font_options.size or 15
|
|
font_options.antialiasing = font_options.antialiasing or "subpixel"
|
|
font_options.hinting = font_options.hinting or "slight"
|
|
fonts:set_options(font_options)
|
|
end
|
|
widget = fonts
|
|
found = true
|
|
|
|
elseif option.type == settings.type.FILE then
|
|
---@type widget.label
|
|
Label(pane, option.label .. ":")
|
|
---@type widget.filepicker
|
|
local file = FilePicker(pane, option_value or "")
|
|
if option.exists then
|
|
file:set_mode(FilePicker.mode.FILE_EXISTS)
|
|
else
|
|
file:set_mode(FilePicker.mode.FILE)
|
|
end
|
|
file.filters = option.filters or {}
|
|
widget = file
|
|
found = true
|
|
|
|
elseif option.type == settings.type.DIRECTORY then
|
|
---@type widget.label
|
|
Label(pane, option.label .. ":")
|
|
---@type widget.filepicker
|
|
local file = FilePicker(pane, option_value or "")
|
|
if option.exists then
|
|
file:set_mode(FilePicker.mode.DIRECTORY_EXISTS)
|
|
else
|
|
file:set_mode(FilePicker.mode.DIRECTORY)
|
|
end
|
|
file.filters = option.filters or {}
|
|
widget = file
|
|
found = true
|
|
|
|
elseif option.type == settings.type.COLOR then
|
|
---@type widget.label
|
|
Label(pane, option.label .. ":")
|
|
---@type widget.colorpicker
|
|
local color = ColorPicker(pane, option_value)
|
|
widget = color
|
|
found = true
|
|
end
|
|
|
|
if widget and type(path) ~= "nil" then
|
|
function widget:on_change(value)
|
|
if self:is(SelectBox) then
|
|
value = self:get_selected_data()
|
|
elseif self:is(ItemsList) then
|
|
value = self:get_items()
|
|
elseif self:is(FontsList) then
|
|
value = {
|
|
fonts = self:get_fonts(),
|
|
options = self:get_options()
|
|
}
|
|
end
|
|
|
|
if option.set_value then
|
|
value = option.set_value(value)
|
|
end
|
|
|
|
if self:is(FontsList) then
|
|
local fonts = {}
|
|
for _, font in ipairs(value.fonts) do
|
|
table.insert(fonts, renderer.font.load(
|
|
font.path, value.options.size * SCALE, value.options
|
|
))
|
|
end
|
|
if option.fonts_list then
|
|
set_config_value(option.fonts_list, path, renderer.font.group(fonts))
|
|
else
|
|
set_config_value(config, path, renderer.font.group(fonts))
|
|
end
|
|
else
|
|
set_config_value(config, path, value)
|
|
end
|
|
|
|
set_config_value(settings.config, path, value)
|
|
save_settings()
|
|
if option.on_apply then
|
|
option.on_apply(value)
|
|
end
|
|
end
|
|
end
|
|
|
|
if (option.description or option.default) and found then
|
|
local text = option.description or ""
|
|
local default = ""
|
|
local default_type = type(option.default)
|
|
if default_type ~= "table" and default_type ~= "nil" then
|
|
if text ~= "" then
|
|
text = text .. " "
|
|
end
|
|
default = string.format("(default: %s)", option.default)
|
|
end
|
|
---@type widget.label
|
|
local description = Label(pane, text .. default)
|
|
description.desc = true
|
|
end
|
|
end
|
|
|
|
---Generate all the widgets for core settings.
|
|
function Settings:load_core_settings()
|
|
for _, section in ipairs(settings.sections) do
|
|
local options = settings.core[section]
|
|
|
|
---@type widget|widget.foldingbook.pane|nil
|
|
local pane = self.core_sections:get_pane(section)
|
|
if not pane then
|
|
pane = self.core_sections:add_pane(section, section)
|
|
else
|
|
pane = pane.container
|
|
end
|
|
|
|
for _, opt in ipairs(options) do
|
|
---@type settings.option
|
|
local option = opt
|
|
add_control(pane, option)
|
|
end
|
|
end
|
|
end
|
|
|
|
---Function in charge of rendering the colors column of the color pane.
|
|
---@param self widget.listbox
|
|
---@oaram row integer
|
|
---@param x integer
|
|
---@param y integer
|
|
---@param font renderer.font
|
|
---@param color renderer.color
|
|
---@param only_calc boolean
|
|
---@return number width
|
|
---@return number height
|
|
local function on_color_draw(self, row, x, y, font, color, only_calc)
|
|
local w = self:get_width() - (x - self.position.x) - style.padding.x
|
|
local h = font:get_height()
|
|
|
|
if not only_calc then
|
|
local row_data = self:get_row_data(row)
|
|
local width = w/#row_data.colors
|
|
|
|
for i = 1, #row_data.colors do
|
|
renderer.draw_rect(x + ((i - 1) * width), y, width, h, row_data.colors[i])
|
|
end
|
|
end
|
|
|
|
return w, h
|
|
end
|
|
|
|
---Generate the list of all available colors with preview
|
|
function Settings:load_color_settings()
|
|
self.colors.scrollable = false
|
|
|
|
local colors = get_installed_colors()
|
|
|
|
---@type widget.listbox
|
|
local listbox = ListBox(self.colors)
|
|
|
|
listbox.border.width = 0
|
|
listbox:enable_expand(true)
|
|
|
|
listbox:add_column("Theme")
|
|
listbox:add_column("Colors")
|
|
|
|
for idx, details in ipairs(colors) do
|
|
local name = details.name
|
|
if settings.config.theme and settings.config.theme == name then
|
|
listbox:set_selected(idx)
|
|
end
|
|
listbox:add_row({
|
|
style.text, name, ListBox.COLEND, on_color_draw
|
|
}, {name = name, colors = details.colors})
|
|
end
|
|
|
|
function listbox:on_row_click(idx, data)
|
|
core.reload_module("colors." .. data.name)
|
|
settings.config.theme = data.name
|
|
save_settings()
|
|
end
|
|
end
|
|
|
|
---Unload a plugin settings from plugins section.
|
|
---@param plugin string
|
|
function Settings:disable_plugin(plugin)
|
|
for _, section in ipairs(settings.plugin_sections) do
|
|
local plugins = settings.plugins[section]
|
|
|
|
for plugin_name, options in pairs(plugins) do
|
|
if plugin_name == plugin then
|
|
self.plugin_sections:delete_pane(section)
|
|
end
|
|
end
|
|
end
|
|
|
|
if
|
|
type(settings.config.enabled_plugins) == "table"
|
|
and
|
|
settings.config.enabled_plugins[plugin]
|
|
then
|
|
settings.config.enabled_plugins[plugin] = nil
|
|
end
|
|
if type(settings.config.disabled_plugins) ~= "table" then
|
|
settings.config.disabled_plugins = {}
|
|
end
|
|
|
|
settings.config.disabled_plugins[plugin] = true
|
|
save_settings()
|
|
end
|
|
|
|
---Load plugin and append its settings to the plugins section.
|
|
---@param plugin string
|
|
function Settings:enable_plugin(plugin)
|
|
local loaded = false
|
|
local config_type = type(config.plugins[plugin])
|
|
if config_type == "boolean" or config_type == "nil" then
|
|
config.plugins[plugin] = {}
|
|
loaded = true
|
|
end
|
|
|
|
require("plugins." .. plugin)
|
|
|
|
if config.plugins[plugin] and config.plugins[plugin].config_spec then
|
|
local conf = config.plugins[plugin].config_spec
|
|
settings.add(conf.name, conf, plugin, true)
|
|
end
|
|
|
|
for _, section in ipairs(settings.plugin_sections) do
|
|
local plugins = settings.plugins[section]
|
|
|
|
for plugin_name, options in pairs(plugins) do
|
|
if plugin_name == plugin then
|
|
---@type widget|widget.foldingbook.pane|nil
|
|
local pane = self.plugin_sections:get_pane(section)
|
|
if not pane then
|
|
pane = self.plugin_sections:add_pane(section, section)
|
|
else
|
|
pane = pane.container
|
|
end
|
|
|
|
merge_plugin_settings(plugin, options)
|
|
|
|
for _, opt in ipairs(options) do
|
|
---@type settings.option
|
|
local option = opt
|
|
add_control(pane, option, plugin_name)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if
|
|
type(settings.config.disabled_plugins) == "table"
|
|
and
|
|
settings.config.disabled_plugins[plugin]
|
|
then
|
|
settings.config.disabled_plugins[plugin] = nil
|
|
end
|
|
if type(settings.config.enabled_plugins) ~= "table" then
|
|
settings.config.enabled_plugins = {}
|
|
end
|
|
|
|
settings.config.enabled_plugins[plugin] = true
|
|
save_settings()
|
|
|
|
if loaded then
|
|
core.log("Loaded '%s' plugin", plugin)
|
|
end
|
|
end
|
|
|
|
---Generate all the widgets for plugin settings.
|
|
function Settings:load_plugin_settings()
|
|
---@type widget|widget.foldingbook.pane|nil
|
|
local pane = self.plugin_sections:get_pane("enable_disable")
|
|
if not pane then
|
|
pane = self.plugin_sections:add_pane("enable_disable", "Installed")
|
|
else
|
|
pane = pane.container
|
|
end
|
|
|
|
-- requires earlier access to startup process
|
|
Label(
|
|
pane,
|
|
"Notice: disabling plugins will not take effect until next restart"
|
|
)
|
|
|
|
Line(pane, 2, 10)
|
|
|
|
local plugins = get_installed_plugins()
|
|
for _, plugin in ipairs(plugins) do
|
|
if plugin ~= "settings" then
|
|
local enabled = false
|
|
|
|
if
|
|
(
|
|
type(config.plugins[plugin]) ~= "nil"
|
|
and
|
|
config.plugins[plugin] ~= false
|
|
)
|
|
or
|
|
(
|
|
settings.config.enabled_plugins
|
|
and
|
|
settings.config.enabled_plugins[plugin]
|
|
)
|
|
then
|
|
enabled = true
|
|
end
|
|
|
|
local this = self
|
|
|
|
---@type widget.toggle
|
|
local toggle = Toggle(pane, prettify_name(plugin), enabled)
|
|
function toggle:on_change(value)
|
|
if value then
|
|
this:enable_plugin(plugin)
|
|
else
|
|
this:disable_plugin(plugin)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
table.sort(settings.plugin_sections)
|
|
|
|
for _, section in ipairs(settings.plugin_sections) do
|
|
local plugins = settings.plugins[section]
|
|
|
|
for plugin_name, options in pairs(plugins) do
|
|
---@type widget|widget.foldingbook.pane|nil
|
|
local pane = self.plugin_sections:get_pane(section)
|
|
if not pane then
|
|
pane = self.plugin_sections:add_pane(section, section)
|
|
else
|
|
pane = pane.container
|
|
end
|
|
|
|
for _, opt in ipairs(options) do
|
|
---@type settings.option
|
|
local option = opt
|
|
add_control(pane, option, plugin_name)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
---@type widget.keybinddialog
|
|
local keymap_dialog = KeybindingDialog()
|
|
|
|
function keymap_dialog:on_save(bindings)
|
|
local row_value = apply_keybinding(self.command, bindings)
|
|
if row_value then
|
|
self.listbox:set_row(self.row_id, row_value)
|
|
end
|
|
end
|
|
|
|
function keymap_dialog:on_reset()
|
|
local default_keys = settings.default_keybindings[self.command]
|
|
local current_keys = { keymap.get_binding(self.command) }
|
|
|
|
for _, binding in ipairs(current_keys) do
|
|
keymap.unbind(binding, self.command)
|
|
end
|
|
|
|
if default_keys and #default_keys > 0 then
|
|
local cmd = self.command
|
|
if not settings.config.custom_keybindings then
|
|
settings.config.custom_keybindings = {}
|
|
settings.config.custom_keybindings[cmd] = {}
|
|
elseif not settings.config.custom_keybindings[cmd] then
|
|
settings.config.custom_keybindings[cmd] = {}
|
|
end
|
|
local shortcuts = ""
|
|
for _, binding in ipairs(default_keys) do
|
|
keymap.add({[binding] = cmd})
|
|
shortcuts = shortcuts .. binding .. "\n"
|
|
table.insert(settings.config.custom_keybindings[cmd], binding)
|
|
end
|
|
local bindings_list = shortcuts:gsub("\n$", "")
|
|
self.listbox:set_row(self.row_id, {
|
|
style.text, cmd, ListBox.COLEND, style.dim, bindings_list
|
|
})
|
|
else
|
|
self.listbox:set_row(self.row_id, {
|
|
style.text, self.command, ListBox.COLEND, style.dim, "none"
|
|
})
|
|
end
|
|
if
|
|
settings.config.custom_keybindings
|
|
and
|
|
settings.config.custom_keybindings[self.command]
|
|
then
|
|
settings.config.custom_keybindings[self.command] = nil
|
|
save_settings()
|
|
end
|
|
end
|
|
|
|
---Generate the list of all available commands and allow editing their keymaps.
|
|
function Settings:load_keymap_settings()
|
|
self.keybinds.scrollable = false
|
|
|
|
local ordered = {}
|
|
for name, _ in pairs(command.map) do
|
|
table.insert(ordered, name)
|
|
end
|
|
table.sort(ordered)
|
|
|
|
---@type widget.textbox
|
|
local textbox = TextBox(self.keybinds, "", "filter bindings...")
|
|
|
|
---@type widget.listbox
|
|
local listbox = ListBox(self.keybinds)
|
|
|
|
listbox.border.width = 0
|
|
|
|
listbox:add_column("Command")
|
|
listbox:add_column("Bindings")
|
|
|
|
for _, name in ipairs(ordered) do
|
|
local keys = { keymap.get_binding(name) }
|
|
local binding = ""
|
|
if #keys == 1 then
|
|
binding = keys[1]
|
|
elseif #keys > 1 then
|
|
binding = keys[1]
|
|
for idx, key in ipairs(keys) do
|
|
if idx ~= 1 then
|
|
binding = binding .. "\n" .. key
|
|
end
|
|
end
|
|
elseif #keys < 1 then
|
|
binding = "none"
|
|
end
|
|
listbox:add_row({
|
|
style.text, name, ListBox.COLEND, style.dim, binding
|
|
}, name)
|
|
end
|
|
|
|
function textbox:on_change(value)
|
|
listbox:filter(value)
|
|
end
|
|
|
|
function listbox:on_mouse_pressed(button, x, y, clicks)
|
|
listbox.super.on_mouse_pressed(self, button, x, y, clicks)
|
|
local idx = listbox:get_selected()
|
|
local data = listbox:get_row_data(idx)
|
|
if clicks == 2 and not keymap_dialog:is_visible() then
|
|
local bindings = { keymap.get_binding(data) }
|
|
keymap_dialog:set_bindings(bindings)
|
|
keymap_dialog.row_id = idx
|
|
keymap_dialog.command = data
|
|
keymap_dialog.listbox = self
|
|
keymap_dialog:show()
|
|
end
|
|
end
|
|
|
|
---@param self widget
|
|
function self.keybinds:update_positions()
|
|
textbox:set_position(0, 0)
|
|
textbox:set_size(self:get_width() - self.border.width * 2)
|
|
listbox:set_position(0, textbox:get_bottom())
|
|
listbox:set_size(self:get_width() - self.border.width * 2, self:get_height() - textbox:get_height())
|
|
end
|
|
end
|
|
|
|
function Settings:setup_about()
|
|
---@type widget.label
|
|
local title = Label(self.about, "Lite XL")
|
|
title.font = "big_font"
|
|
---@type widget.label
|
|
local version = Label(self.about, "version " .. VERSION)
|
|
---@type widget.label
|
|
local description = Label(
|
|
self.about,
|
|
"A lightweight text editor written in Lua, adapted from lite."
|
|
)
|
|
|
|
local function open_link(link)
|
|
local platform_filelauncher
|
|
if PLATFORM == "Windows" then
|
|
platform_filelauncher = "start"
|
|
elseif PLATFORM == "Mac OS X" then
|
|
platform_filelauncher = "open"
|
|
else
|
|
platform_filelauncher = "xdg-open"
|
|
end
|
|
system.exec(platform_filelauncher .. " " .. link)
|
|
end
|
|
|
|
---@type widget.button
|
|
local button = Button(self.about, "Visit Website")
|
|
button:set_tooltip("Open https://lite-xl.com/")
|
|
function button:on_click() open_link("https://lite-xl.com/") end
|
|
|
|
---@type widget.listbox
|
|
local contributors = ListBox(self.about)
|
|
contributors.scrollable = true
|
|
contributors:add_column("Contributors")
|
|
contributors:add_column("")
|
|
contributors:add_column("Website")
|
|
function contributors:on_row_click(_, data) open_link(data) end
|
|
|
|
local contributors_list = {
|
|
{ "Rxi", "Lite Founder", "https://github.com/rxi" },
|
|
{ "Francesco Abbate", "Lite XL Founder", "https://github.com/franko" },
|
|
{ "Adam Harrison", "Core", "https://github.com/adamharrison" },
|
|
{ "Andrea Zanellato", "CI, Website", "https://github.com/redtide" },
|
|
{ "Björn Buckwalter", "MacOS Support", "https://github.com/bjornbm" },
|
|
{ "boppyt", "Contributor", "https://github.com/boppyt" },
|
|
{ "Cukmekerb", "Contributor", "https://github.com/vincens2005" },
|
|
{ "Daniel Rocha", "Contributor", "https://github.com/dannRocha" },
|
|
{ "daubaris", "Contributor", "https://github.com/daubaris" },
|
|
{ "Dheisom Gomes", "Contributor", "https://github.com/dheisom" },
|
|
{ "Evgeny Petrovskiy", "Contributor", "https://github.com/eugenpt" },
|
|
{ "Ferdinand Prantl", "Contributor", "https://github.com/prantlf" },
|
|
{ "Jan", "Build System", "https://github.com/Jan200101" },
|
|
{ "Janis-Leuenberger", "MacOS Support", "https://github.com/Janis-Leuenberger" },
|
|
{ "Jefferson", "Contributor", "https://github.com/jgmdev" },
|
|
{ "Jipok", "Contributor", "https://github.com/Jipok" },
|
|
{ "Joshua Minor", "Contributor", "https://github.com/jminor" },
|
|
{ "George Linkovsky", "Contributor", "https://github.com/Timofffee" },
|
|
{ "Guldoman", "Core", "https://github.com/Guldoman" },
|
|
{ "liquidev", "Contributor", "https://github.com/liquidev" },
|
|
{ "Mat Mariani", "MacOS Support", "https://github.com/mathewmariani" },
|
|
{ "Nightwing", "Contributor", "https://github.com/Nightwing13" },
|
|
{ "Nils Kvist", "Contributor", "https://github.com/budRich" },
|
|
{ "Not-a-web-Developer", "Contributor", "https://github.com/Not-a-web-Developer" },
|
|
{ "Robert Štojs", "CI", "https://github.com/netrobert" },
|
|
{ "sammyette", "Plugins", "https://github.com/TorchedSammy" },
|
|
{ "Takase", "Core", "https://github.com/takase1121" },
|
|
{ "xwii", "Contributor", "https://github.com/xcb-xwii" }
|
|
}
|
|
|
|
for _, c in ipairs(contributors_list) do
|
|
contributors:add_row({
|
|
c[1], ListBox.COLEND, c[2], ListBox.COLEND, c[3]
|
|
}, c[3])
|
|
end
|
|
|
|
---@param self widget
|
|
function self.about:update_positions()
|
|
local center = self:get_width() / 2
|
|
|
|
title:set_label("Lite XL")
|
|
title:set_position(
|
|
center - (title:get_width() / 2),
|
|
style.padding.y
|
|
)
|
|
|
|
version:set_position(
|
|
center - (version:get_width() / 2),
|
|
title:get_bottom() + (style.padding.y / 2)
|
|
)
|
|
|
|
description:set_position(
|
|
center - (description:get_width() / 2),
|
|
version:get_bottom() + (style.padding.y / 2)
|
|
)
|
|
|
|
button:set_position(
|
|
center - (button:get_width() / 2),
|
|
description:get_bottom() + style.padding.y
|
|
)
|
|
|
|
contributors:set_position(
|
|
style.padding.x,
|
|
button:get_bottom() + style.padding.y
|
|
)
|
|
|
|
contributors:set_size(
|
|
self:get_width() - (style.padding.x * 2),
|
|
self:get_height() - (button:get_bottom() + (style.padding.y * 2))
|
|
)
|
|
|
|
contributors:set_visible_rows()
|
|
end
|
|
end
|
|
|
|
---Reposition and resize core and plugin widgets.
|
|
function Settings:update()
|
|
if not Settings.super.update(self) then return end
|
|
|
|
self.notebook:set_size(self.size.x, self.size.y)
|
|
|
|
for _, section in ipairs({self.core_sections, self.plugin_sections}) do
|
|
if section.parent:is_visible() then
|
|
section:set_size(
|
|
section.parent.size.x - (style.padding.x),
|
|
section:get_real_height()
|
|
)
|
|
section:set_position(style.padding.x / 2, 0)
|
|
for _, pane in ipairs(section.panes) do
|
|
local prev_child = nil
|
|
for pos=#pane.container.childs, 1, -1 do
|
|
local child = pane.container.childs[pos]
|
|
local x, y = 10, (10 * SCALE)
|
|
if prev_child then
|
|
if
|
|
(prev_child:is(Label) and not prev_child.desc)
|
|
or
|
|
(child:is(Label) and child.desc)
|
|
then
|
|
y = prev_child:get_bottom() + (10 * SCALE)
|
|
else
|
|
y = prev_child:get_bottom() + (30 * SCALE)
|
|
end
|
|
end
|
|
if child:is(Line) then
|
|
x = 0
|
|
elseif child:is(ItemsList) or child:is(FilePicker) or child:is(TextBox) then
|
|
child:set_size(pane.container:get_width() - 20, child.size.y)
|
|
end
|
|
child:set_position(x, y)
|
|
prev_child = child
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if self.keybinds:is_visible() then
|
|
self.keybinds:update_positions()
|
|
end
|
|
|
|
if self.about:is_visible() then
|
|
self.about:update_positions()
|
|
end
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- overwrite core run to inject previously saved settings
|
|
--------------------------------------------------------------------------------
|
|
local core_run = core.run
|
|
function core.run()
|
|
store_default_keybindings()
|
|
|
|
-- load plugins disabled by default and enabled by user
|
|
if settings.config.enabled_plugins then
|
|
for name, _ in pairs(settings.config.enabled_plugins) do
|
|
if not config.plugins[name] then
|
|
require("plugins." .. name)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- append all settings defined in the plugins spec
|
|
scan_plugins_spec()
|
|
|
|
-- merge custom settings into config
|
|
merge_settings()
|
|
|
|
---@type settings.ui
|
|
settings.ui = Settings()
|
|
|
|
-- apply user chosen color theme
|
|
if settings.config.theme and settings.config.theme ~= "default" then
|
|
core.try(function()
|
|
core.reload_module("colors." .. settings.config.theme)
|
|
end)
|
|
end
|
|
|
|
-- re-apply user settings
|
|
core.load_user_directory()
|
|
core.load_project_module()
|
|
|
|
core_run()
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Disable plugins at startup, only works if this file is the first
|
|
-- required on user module, or priority tag is obeyed by lite-xl.
|
|
--------------------------------------------------------------------------------
|
|
-- load custom user settings that include list of disabled plugins
|
|
load_settings()
|
|
|
|
-- only disable non already loaded plugins
|
|
if settings.config.disabled_plugins then
|
|
for name, _ in pairs(settings.config.disabled_plugins) do
|
|
if not package.loaded[name] then
|
|
config.plugins[name] = false
|
|
end
|
|
end
|
|
end
|
|
|
|
-- properly apply skip_plugins_version before other plugins are loaded
|
|
if settings.config.skip_plugins_version then
|
|
config.skip_plugins_version = true
|
|
else
|
|
config.skip_plugins_version = false
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Add command and keymap to load settings view
|
|
--------------------------------------------------------------------------------
|
|
command.add(nil, {
|
|
["ui:settings"] = function()
|
|
settings.ui:show()
|
|
local node = core.root_view:get_active_node_default()
|
|
local found = false
|
|
for _, view in ipairs(node.views) do
|
|
if view == settings.ui then
|
|
found = true
|
|
node:set_active_view(view)
|
|
break
|
|
end
|
|
end
|
|
if not found then
|
|
node:add_view(settings.ui)
|
|
end
|
|
end,
|
|
})
|
|
|
|
keymap.add {
|
|
["ctrl+alt+p"] = "ui:settings"
|
|
}
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Overwrite toolbar preferences command to open the settings gui
|
|
--------------------------------------------------------------------------------
|
|
if config.plugins.toolbarview ~= false then
|
|
local ToolbarView = require "plugins.toolbarview"
|
|
local toolbarview_on_mouse_moved = ToolbarView.on_mouse_moved
|
|
function ToolbarView:on_mouse_moved(px, py, ...)
|
|
toolbarview_on_mouse_moved(self, px, py, ...)
|
|
if
|
|
self.hovered_item
|
|
and
|
|
self.hovered_item.command == "core:open-user-module"
|
|
then
|
|
self.hovered_item.command = "ui:settings"
|
|
end
|
|
end
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Overwrite View:new to allow setting force scrollbar status globally
|
|
--------------------------------------------------------------------------------
|
|
local view_new = View.new
|
|
function View:new()
|
|
view_new(self)
|
|
local mode = config.force_scrollbar_status_mode or "global"
|
|
local globally = mode == "global"
|
|
if globally then
|
|
--This is delayed to allow widgets to also apply it to child views/widgets
|
|
core.add_thread(function()
|
|
self.v_scrollbar:set_forced_status(config.force_scrollbar_status)
|
|
self.h_scrollbar:set_forced_status(config.force_scrollbar_status)
|
|
end)
|
|
end
|
|
end
|
|
|
|
return settings;
|