diff options
author | Jefferson González <jgmdev@gmail.com> | 2022-05-23 00:48:47 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-05-23 00:48:47 -0400 |
commit | 21025951770df69c0fedd67b33f1f7725c22980d (patch) | |
tree | 6c6d64ce67d959d09a849670799306e9b83913e2 | |
parent | 8d1421f31b98738fb9e3112083153434d92f3786 (diff) | |
download | lite-xl-plugins-21025951770df69c0fedd67b33f1f7725c22980d.tar.gz lite-xl-plugins-21025951770df69c0fedd67b33f1f7725c22980d.zip |
Add settings gui plugin (#65)
* Appropriately merged config hashes.
* indentguide: use indent size from doc instead of config
* added settings gui plugin
* added new animation/transition settings
* overwrite ToolbarView settings command
* Introduced new button type
* Added button to open user module on General section
* settings: partially implemented keybindings
* initial support for settings.type.LIST_STRINGS
* added on_apply, prettify settings file and other fixes
* added ability to save keybinds and apply at startup
* dropped custom prettify in favor of new common.serialize support
* handle plugins enable/disable
* added priority:0
* added color theme chooser pane
* do not remove duplicated colors
* properly list all shortcuts assigned to a command
* handle multiple bindings per command
* custom drawing of colors preview
* added video preview to readme
* removed unused settings.type.LIST_NUMBERS
-rw-r--r-- | README.md | 1 | ||||
-rw-r--r-- | plugins/indentguide.lua | 2 | ||||
-rw-r--r-- | plugins/openselected.lua | 1 | ||||
-rw-r--r-- | plugins/settings.lua | 1460 |
4 files changed, 1463 insertions, 1 deletions
@@ -153,6 +153,7 @@ to something other than a raw file it should be marked with an asterisk.* | [`scalestatus`](plugins/scalestatus.lua?raw=1) | Displays current scale (zoom) in status view (depends on scale plugin) | | [`select_colorscheme`](plugins/select_colorscheme.lua?raw=1) | Select a color theme, like VScode, Sublime Text.(plugin saves changes) | | [`selectionhighlight`](plugins/selectionhighlight.lua?raw=1) | Highlights regions of code that match the current selection *([screenshot](https://user-images.githubusercontent.com/3920290/80710883-5f597c80-8ae7-11ea-97f0-76dfacc08439.png))* | +| [`settings`](plugins/settings.lua?raw=1) | Provides a GUI to manage core and plugin settings, bindings and select color theme *([video](https://user-images.githubusercontent.com/1702572/169743674-ececae24-f6b7-4ff2-bfa2-c4762cd327d9.mp4))*. (depends on [`widget`](https://github.com/lite-xl/lite-xl-widgets)) | | [`smallclock`](plugins/smallclock.lua?raw=1) | Displays the current time in the corner of the status view | | [`smoothcaret`](plugins/smoothcaret.lua?raw=1) | Smooth caret animation *([gif](https://user-images.githubusercontent.com/20792268/139006049-a0ba6559-88cb-49a7-8077-4822445b4a1f.gif))* | | [`sort`](plugins/sort.lua?raw=1) | Sorts selected lines alphabetically | diff --git a/plugins/indentguide.lua b/plugins/indentguide.lua index c66d825..8d78ee3 100644 --- a/plugins/indentguide.lua +++ b/plugins/indentguide.lua @@ -1,4 +1,4 @@ --- mod-version:3 --lite-xl 2.1 +--- mod-version:3 --lite-xl 2.1 local style = require "core.style" local config = require "core.config" local DocView = require "core.docview" diff --git a/plugins/openselected.lua b/plugins/openselected.lua index 4522fe9..c2cb3a4 100644 --- a/plugins/openselected.lua +++ b/plugins/openselected.lua @@ -2,6 +2,7 @@ local core = require "core" local command = require "core.command" local keymap = require "core.keymap" +local common = require "core.common" local config = require "core.config" local contextmenu = require "plugins.contextmenu" diff --git a/plugins/settings.lua b/plugins/settings.lua new file mode 100644 index 0000000..f304f9a --- /dev/null +++ b/plugins/settings.lua @@ -0,0 +1,1460 @@ +-- mod-version:3 --lite-xl 2.1 --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 Widget = require "widget" +local Label = require "widget.label" +local Line = require "widget.line" +local NoteBook = require "widget.notebook" +local Button = require "widget.button" +local TextBox = require "widget.textbox" +local SelectBox = require "widget.selectbox" +local NumberBox = require "widget.numberbox" +local Toggle = require "widget.toggle" +local ListBox = require "widget.listbox" +local FoldingBook = require "widget.foldingbook" +local ItemsList = require "widget.itemslist" +local ToolbarView = require "plugins.toolbarview" +local KeybindingDialog = require "widget.keybinddialog" + +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 +} + +---@alias settings.types +---|>'settings.type.STRING' +---| 'settings.type.NUMBER' +---| 'settings.type.TOGGLE' +---| 'settings.type.SELECTION' +---| 'settings.type.LIST_STRINGS' +---| 'settings.type.BUTTON' + +---Represents a setting to render on a settings pane. +---@class settings.option +---@field public label string +---@field public description string +---@field public path string +---@field public type settings.types +---@field public default string | number | table<integer, string> | table<integer, integer> +---@field public min number +---@field public max number +---@field public step number +---@field public values table +---@field public get_value nil | fun(value:any):any +---@field public set_value nil | fun(value:any):any +---@field public icon string +---@field public on_click nil | string | fun(button:string, x:integer, y:integer) +---@field public on_apply nil | fun(value:any) +settings.option = { + ---Title displayed to the user eg: "My Option" + label = "", + ---Description of the option eg: "Modifies the document indentation" + description = "", + ---Config path in the config table, eg: section.myoption, myoption, etc... + path = "", + ---Type of option that will be used to render an appropriate control + type = "", + ---Default value of the option + default = "", + ---Used for NUMBER to indiciate the minimum number allowed + min = 0, + ---Used for NUMBER to indiciate the maximum number allowed + max = 0, + ---Used for NUMBER to indiciate the increment/decrement amount + step = 0, + ---Used in a SELECTION to provide the list of valid options + values = {}, + ---Optional function that is used to manipulate the current value on retrieval. + get_value = nil, + ---Optional function that is used to manipulate the saved value on save. + set_value = nil, + ---The icon set for a BUTTON + icon = "", + ---Command or function executed when a BUTTON is clicked + on_click = nil, + ---Optional function executed when the option value is applied. + on_apply = nil +} + +---Add a new settings section to the settings UI +---@param section string +---@param options settings.option[] +---@param plugin_name? string Optional name of plugin +function settings.add(section, options, plugin_name) + local category = "" + if plugin_name ~= nil then + category = "plugins" + else + category = "core" + 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 = "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 = { "^%." }, + 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 = "Borderless", + description = "Use built-in window decorations.", + path = "borderless", + type = settings.type.TOGGLE, + default = false, + on_apply = function() + core.configure_borderless_window() + 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 + }, + { + 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 = "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 = "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 = "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 = "Show Spaces", + description = "Draw another character in place of invisble spaces.", + path = "draw_whitespace", + type = settings.type.TOGGLE, + default = false + }, + { + 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 = "Log Items", + description = "The maximum amount of entries to keep on the log UI.", + path = "max_log_items", + type = settings.type.NUMBER, + default = 80, + min = 50, + 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 + } + } +) + +---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.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 + 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.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) + return name:gsub("[%-_]", " "):gsub("%S+", capitalize_first) +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 + 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 + +---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 + set_config_value(config, option.path, saved_value) + 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 + 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 + set_config_value(config, path, saved_value) + if option.on_apply then + option.on_apply(saved_value) + end + end + end + end + 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 + +---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 plugins widget +---@field private keybinds 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, 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.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.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() +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 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) + 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 + 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() + end + if option.set_value then + value = option.set_value(value) + end + set_config_value(config, path, value) + set_config_value(settings.config, path, value) + save_settings() + if option.on_apply then + option.on_apply(value) + end + end + end + + if option.description and found then + local default = "" + local default_type = type(option.default) + if default_type ~= "table" and default_type ~= "nil" then + default = string.format(" (default: %s)", option.default) + end + ---@type widget.label + local description = Label(pane, option.description .. 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 + 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 + 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) + + 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 + 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 + + 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 + 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 + 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.listbox + local listbox = ListBox(self.keybinds) + + listbox.border.width = 0 + listbox:enable_expand(true) + + 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 listbox:on_row_click(idx, data) + if 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 +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) + + self.core:set_size( + self.size.x, + self.size.y - self.notebook.active_pane.tab:get_height() - 8 + ) + + self.plugins:set_size( + self.size.x, + self.size.y - self.notebook.active_pane.tab:get_height() - 8 + ) + + self.core_sections:set_size( + self.core.size.x - (style.padding.x), + self.core_sections:get_real_height() + ) + + self.plugin_sections:set_size( + self.plugins.size.x - (style.padding.x), + self.plugin_sections:get_real_height() + ) + + self.core_sections:set_position( + style.padding.x / 2, + 0 + ) + + self.plugin_sections:set_position( + style.padding.x / 2, + 0 + ) + + for _, section in ipairs({self.core_sections, self.plugin_sections}) do + 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 + 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 + else + y = prev_child:get_bottom() + 40 + end + end + if child:is(Line) then + x = 0 + elseif child:is(ItemsList) 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 + +-------------------------------------------------------------------------------- +-- overwrite core run to inject previously saved settings and save at exit +-------------------------------------------------------------------------------- +local core_run = core.run +function core.run() + store_default_keybindings() + + -- merge custom settings into config + merge_settings() + + ---@type settings.ui + settings.ui = Settings() + + -- 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 + type(config.plugins[name]) == "boolean" + and + not config.plugins[name] + then + settings.ui:enable_plugin(name) + end + end + end + + -- apply user chosen color theme + if settings.config.theme then + core.reload_module("colors." .. settings.config.theme) + end + + core_run() + + -- save on a normal exit + save_settings() +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 type(rawget(config.plugins, name)) == "nil" then + config.plugins[name] = false + end + end +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 +-------------------------------------------------------------------------------- +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 + + +return settings; |