Merge branch 'master' into amiga2.1

This commit is contained in:
George Sokianos 2023-12-26 13:36:13 +00:00
commit 5c983f10b5
66 changed files with 1632 additions and 540 deletions

View File

@ -110,6 +110,12 @@ jobs:
run: | run: |
echo "$HOME/.local/bin" >> "$GITHUB_PATH" echo "$HOME/.local/bin" >> "$GITHUB_PATH"
echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-macos-universal" >> "$GITHUB_ENV" echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-macos-universal" >> "$GITHUB_ENV"
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: Install dmgbuild
run: pip install dmgbuild
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@v3 uses: actions/download-artifact@v3
@ -117,8 +123,6 @@ jobs:
with: with:
name: macOS DMG Images name: macOS DMG Images
path: dmgs-original path: dmgs-original
- name: Install appdmg
run: cd ~; npm i appdmg; cd -
- name: Make universal bundles - name: Make universal bundles
run: | run: |
bash --version bash --version
@ -200,13 +204,14 @@ jobs:
run: | run: |
"INSTALL_NAME=lite-xl-$($env:GITHUB_REF -replace ".*/")-windows-msvc-${{ matrix.arch.name }}" >> $env:GITHUB_ENV "INSTALL_NAME=lite-xl-$($env:GITHUB_REF -replace ".*/")-windows-msvc-${{ matrix.arch.name }}" >> $env:GITHUB_ENV
"INSTALL_REF=$($env:GITHUB_REF -replace ".*/")" >> $env:GITHUB_ENV "INSTALL_REF=$($env:GITHUB_REF -replace ".*/")" >> $env:GITHUB_ENV
"LUA_SUBPROJECT_PATH=subprojects/lua-5.4.4" >> $env:GITHUB_ENV "LUA_SUBPROJECT_PATH=subprojects/$(awk -F ' *= *' '/directory/ { printf $2 }' subprojects/lua.wrap)" >> $env:GITHUB_ENV
- name: Download and patch subprojects
shell: bash
run: |
meson subprojects download
cat resources/windows/001-lua-unicode.diff | patch -Np1 -d "$LUA_SUBPROJECT_PATH"
- name: Configure - name: Configure
run: | run: |
# Download the subprojects first so we can patch it before configuring.
# This avoids reconfiguring the subprojects when compiling.
meson subprojects download
Get-Content -Path resources/windows/001-lua-unicode.diff -Raw | patch -d $env:LUA_SUBPROJECT_PATH -p1 --forward
meson setup --wrap-mode=forcefallback build meson setup --wrap-mode=forcefallback build
- name: Build - name: Build
run: | run: |

View File

@ -185,8 +185,8 @@ jobs:
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: 3.9 python-version: 3.9
- name: Install appdmg - name: Install dmgbuild
run: cd ~; npm i appdmg; cd - run: pip install dmgbuild
- name: Prepare DMG Images - name: Prepare DMG Images
run: | run: |
mkdir -p dmgs-addons dmgs-normal mkdir -p dmgs-addons dmgs-normal

1
.gitignore vendored
View File

@ -28,3 +28,4 @@ lite
!resources/windows/*.diff !resources/windows/*.diff
!resources/windows/*.exe.manifest.in !resources/windows/*.exe.manifest.in
!resources/macos/*.py

View File

@ -1,4 +1,4 @@
Copyright (c) 2020-2021 Lite XL Team Copyright (c) 2020-present Lite XL Team
Permission is hereby granted, free of charge, to any person obtaining a copy of Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in this software and associated documentation files (the "Software"), to deal in

View File

@ -1,7 +1,7 @@
# Lite XL # Lite XL
[![CI]](https://github.com/lite-xl/lite-xl/actions/workflows/build.yml) [![CI]](https://github.com/lite-xl/lite-xl/actions/workflows/build.yml)
[![Discord Badge Image]](https://discord.gg/RWzqC3nx7K) [![Discord Badge Image]](https://discord.gg/UQKnzBhY5H)
![screenshot-dark] ![screenshot-dark]
@ -81,6 +81,39 @@ affects only the place where the application is actually installed.
Head over to [releases](https://github.com/lite-xl/lite-xl/releases) and download the version for your operating system. Head over to [releases](https://github.com/lite-xl/lite-xl/releases) and download the version for your operating system.
### Windows
Lite XL comes with installers on Windows for typical installations.
Alternatively, we provide ZIP archives that you can download and extract anywhere and run directly.
To make Lite XL portable (e.g. running Lite XL from a thumb drive),
simply create a `user` folder where `lite-xl.exe` is located.
Lite XL will load and store all your configurations and plugins in the folder.
### macOS
We provide DMG files for macOS. Simply drag the program into your Applications folder.
> **Important**
> Newer versions of Lite XL are signed with a self-signed certificate,
> so you'll have to follow these steps when running Lite XL for the first time.
>
> 1. Find Lite XL in Finder (do not open it in Launchpad).
> 2. Control-click Lite XL, then choose `Open` from the shortcut menu.
> 3. Click `Open` in the popup menu.
>
> The correct steps may vary between macOS versions, so you should refer to
> the [macOS User Guide](https://support.apple.com/en-my/guide/mac-help/mh40616/mac).
>
> On an older version of Lite XL, you will need to run these commands instead:
>
> ```sh
> # clears attributes from the directory
> xattr -cr /Applications/Lite\ XL.app
> ```
>
> Otherwise, macOS will display a **very misleading error** saying that the application is damaged.
### Linux ### Linux
Unzip the file and `cd` into the `lite-xl` directory: Unzip the file and `cd` into the `lite-xl` directory:
@ -91,6 +124,7 @@ cd lite-xl
``` ```
To run lite-xl without installing: To run lite-xl without installing:
```sh ```sh
./lite-xl ./lite-xl
``` ```
@ -103,21 +137,59 @@ mkdir -p $HOME/.local/bin && cp lite-xl $HOME/.local/bin/
mkdir -p $HOME/.local/share/lite-xl && cp -r data/* $HOME/.local/share/lite-xl/ mkdir -p $HOME/.local/share/lite-xl && cp -r data/* $HOME/.local/share/lite-xl/
``` ```
#### Add Lite XL to PATH
To run Lite XL from the command line, you must add it to PATH.
If `$HOME/.local/bin` is not in PATH: If `$HOME/.local/bin` is not in PATH:
```sh ```sh
echo -e 'export PATH=$PATH:$HOME/.local/bin' >> $HOME/.bashrc echo -e 'export PATH=$PATH:$HOME/.local/bin' >> $HOME/.bashrc
``` ```
To get the icon to show up in app launcher: Alternatively on recent versions of GNOME and KDE Plasma,
you can add `$HOME/.local/bin` to PATH via `~/.config/environment.d/envvars.conf`:
```ini
PATH=$HOME/.local/bin:$PATH
```
> **Note**
> Some systems might not load `.bashrc` when logging in.
> This can cause problems with launching applications from the desktop / menu.
#### Add Lite XL to application launchers
To get the icon to show up in app launcher, you need to create a desktop
entry and put it into `/usr/share/applications` or `~/.local/share/applications`.
Here is an example for a desktop entry in `~/.local/share/applications/com.lite_xl.LiteXL.desktop`,
assuming Lite XL is in PATH:
```ini
[Desktop Entry]
Type=Application
Name=Lite XL
Comment=A lightweight text editor written in Lua
Exec=lite-xl %F
Icon=lite-xl
Terminal=false
StartupWMClass=lite-xl
Categories=Development;IDE;
MimeType=text/plain;inode/directory;
```
To get the icon to show up in app launcher immediately, run:
```sh ```sh
xdg-desktop-menu forceupdate xdg-desktop-menu forceupdate
``` ```
You may need to logout and login again to see icon in app launcher. Alternatively, you may log out and log in again.
To uninstall just run: #### Uninstall
To uninstall Lite XL, run:
```sh ```sh
rm -f $HOME/.local/bin/lite-xl rm -f $HOME/.local/bin/lite-xl
@ -127,7 +199,6 @@ rm -rf $HOME/.local/share/icons/hicolor/scalable/apps/lite-xl.svg \
$HOME/.local/share/lite-xl $HOME/.local/share/lite-xl
``` ```
## Contributing ## Contributing
Any additional functionality that can be added through a plugin should be done Any additional functionality that can be added through a plugin should be done

View File

@ -38,7 +38,7 @@ show_help() {
echo "-v --version VERSION Sets the version on the package name." echo "-v --version VERSION Sets the version on the package name."
echo "-A --appimage Create an AppImage (Linux only)." echo "-A --appimage Create an AppImage (Linux only)."
echo "-D --dmg Create a DMG disk image (macOS only)." echo "-D --dmg Create a DMG disk image (macOS only)."
echo " Requires NPM and AppDMG." echo " Requires dmgbuild."
echo "-I --innosetup Create an InnoSetup installer (Windows only)." echo "-I --innosetup Create an InnoSetup installer (Windows only)."
echo "-r --release Compile in release mode." echo "-r --release Compile in release mode."
echo "-S --source Create a source code package," echo "-S --source Create a source code package,"

View File

@ -43,7 +43,7 @@ local function save(filename)
core.log("Saved \"%s\"", saved_filename) core.log("Saved \"%s\"", saved_filename)
else else
core.error(err) core.error(err)
core.nag_view:show("Saving failed", string.format("Could not save \"%s\" do you want to save to another location?", doc().filename), { core.nag_view:show("Saving failed", string.format("Couldn't save file \"%s\". Do you want to save to another location?", doc().filename), {
{ text = "No", default_no = true }, { text = "No", default_no = true },
{ text = "Yes", default_yes = true } { text = "Yes", default_yes = true }
}, function(item) }, function(item)
@ -340,10 +340,11 @@ local commands = {
local text = dv.doc:get_text(line1, 1, line1, col1) local text = dv.doc:get_text(line1, 1, line1, col1)
if #text >= indent_size and text:find("^ *$") then if #text >= indent_size and text:find("^ *$") then
dv.doc:delete_to_cursor(idx, 0, -indent_size) dv.doc:delete_to_cursor(idx, 0, -indent_size)
return goto continue
end end
end end
dv.doc:delete_to_cursor(idx, translate.previous_char) dv.doc:delete_to_cursor(idx, translate.previous_char)
::continue::
end end
end, end,
@ -544,6 +545,11 @@ local commands = {
dv.doc.crlf = not dv.doc.crlf dv.doc.crlf = not dv.doc.crlf
end, end,
["doc:toggle-overwrite"] = function(dv)
dv.doc.overwrite = not dv.doc.overwrite
core.blink_reset() -- to show the cursor has changed edit modes
end,
["doc:save-as"] = function(dv) ["doc:save-as"] = function(dv)
local last_doc = core.last_active_view and core.last_active_view.doc local last_doc = core.last_active_view and core.last_active_view.doc
local text local text

View File

@ -164,13 +164,16 @@ local function is_in_any_selection(line, col)
end end
local function select_add_next(all) local function select_add_next(all)
local il1, ic1 = doc():get_selection(true) local il1, ic1
for idx, l1, c1, l2, c2 in doc():get_selections(true, true) do for _, l1, c1, l2, c2 in doc():get_selections(true, true) do
if not il1 then
il1, ic1 = l1, c1
end
local text = doc():get_text(l1, c1, l2, c2) local text = doc():get_text(l1, c1, l2, c2)
repeat repeat
l1, c1, l2, c2 = search.find(doc(), l2, c2, text, { wrap = true }) l1, c1, l2, c2 = search.find(doc(), l2, c2, text, { wrap = true })
if l1 == il1 and c1 == ic1 then break end if l1 == il1 and c1 == ic1 then break end
if l2 and (all or not is_in_any_selection(l2, c2)) then if l2 and not is_in_any_selection(l2, c2) then
doc():add_selection(l2, c2, l1, c1) doc():add_selection(l2, c2, l1, c1)
if not all then if not all then
core.active_view:scroll_to_make_visible(l2, c2) core.active_view:scroll_to_make_visible(l2, c2)
@ -266,7 +269,7 @@ command.add(valid_for_finding, {
core.error("No find to continue from") core.error("No find to continue from")
else else
local sl1, sc1, sl2, sc2 = dv.doc:get_selection(true) local sl1, sc1, sl2, sc2 = dv.doc:get_selection(true)
local line1, col1, line2, col2 = last_fn(dv.doc, sl1, sc2, last_text, case_sensitive, find_regex, false) local line1, col1, line2, col2 = last_fn(dv.doc, sl2, sc2, last_text, case_sensitive, find_regex, false)
if line1 then if line1 then
dv.doc:set_selection(line2, col2, line1, col1) dv.doc:set_selection(line2, col2, line1, col1)
dv:scroll_to_line(line2, true) dv:scroll_to_line(line2, true)

View File

@ -226,7 +226,7 @@ function common.path_suggest(text, root)
if root and root:sub(-1) ~= PATHSEP then if root and root:sub(-1) ~= PATHSEP then
root = root .. PATHSEP root = root .. PATHSEP
end end
local path, name = text:match("^(.-)([^:/\\]*)$") local path, name = text:match("^(.-)([^"..PATHSEP.."]*)$")
local clean_dotslash = false local clean_dotslash = false
-- ignore root if path is absolute -- ignore root if path is absolute
local is_absolute = common.is_absolute_path(text) local is_absolute = common.is_absolute_path(text)
@ -279,7 +279,7 @@ end
---@param text string The input path. ---@param text string The input path.
---@return string[] ---@return string[]
function common.dir_path_suggest(text) function common.dir_path_suggest(text)
local path, name = text:match("^(.-)([^:/\\]*)$") local path, name = text:match("^(.-)([^"..PATHSEP.."]*)$")
local files = system.list_dir(path == "" and "." or path) or {} local files = system.list_dir(path == "" and "." or path) or {}
local res = {} local res = {}
for _, file in ipairs(files) do for _, file in ipairs(files) do
@ -298,7 +298,7 @@ end
---@param dir_list string[] A list of paths to filter. ---@param dir_list string[] A list of paths to filter.
---@return string[] ---@return string[]
function common.dir_list_suggest(text, dir_list) function common.dir_list_suggest(text, dir_list)
local path, name = text:match("^(.-)([^:/\\]*)$") local path, name = text:match("^(.-)([^"..PATHSEP.."]*)$")
local res = {} local res = {}
for _, dir_path in ipairs(dir_list) do for _, dir_path in ipairs(dir_list) do
if dir_path:lower():find(text:lower(), nil, true) == 1 then if dir_path:lower():find(text:lower(), nil, true) == 1 then
@ -378,12 +378,15 @@ function common.bench(name, fn, ...)
return res return res
end end
-- From gvx/Ser
local oddvals = {[tostring(1/0)] = "1/0", [tostring(-1/0)] = "-1/0", [tostring(-(0/0))] = "-(0/0)", [tostring(0/0)] = "0/0"}
local function serialize(val, pretty, indent_str, escape, sort, limit, level) local function serialize(val, pretty, indent_str, escape, sort, limit, level)
local space = pretty and " " or "" local space = pretty and " " or ""
local indent = pretty and string.rep(indent_str, level) or "" local indent = pretty and string.rep(indent_str, level) or ""
local newline = pretty and "\n" or "" local newline = pretty and "\n" or ""
if type(val) == "string" then local ty = type(val)
if ty == "string" then
local out = string.format("%q", val) local out = string.format("%q", val)
if escape then if escape then
out = string.gsub(out, "\\\n", "\\n") out = string.gsub(out, "\\\n", "\\n")
@ -395,7 +398,7 @@ local function serialize(val, pretty, indent_str, escape, sort, limit, level)
out = string.gsub(out, "\\13", "\\r") out = string.gsub(out, "\\13", "\\r")
end end
return out return out
elseif type(val) == "table" then elseif ty == "table" then
-- early exit -- early exit
if level >= limit then return tostring(val) end if level >= limit then return tostring(val) end
local next_indent = pretty and (indent .. indent_str) or "" local next_indent = pretty and (indent .. indent_str) or ""
@ -410,6 +413,12 @@ local function serialize(val, pretty, indent_str, escape, sort, limit, level)
if sort then table.sort(t) end if sort then table.sort(t) end
return "{" .. newline .. table.concat(t, "," .. newline) .. newline .. indent .. "}" return "{" .. newline .. table.concat(t, "," .. newline) .. newline .. indent .. "}"
end end
if ty == "number" then
-- tostring is locale-dependent, so we need to replace an eventual `,` with `.`
local res, _ = tostring(val):gsub(",", ".")
-- handle inf/nan
return oddvals[res] or res
end
return tostring(val) return tostring(val)
end end
@ -452,7 +461,7 @@ end
function common.basename(path) function common.basename(path)
-- a path should never end by / or \ except if it is '/' (unix root) or -- a path should never end by / or \ except if it is '/' (unix root) or
-- 'X:\' (windows drive) -- 'X:\' (windows drive)
return path:match("[^\\/]+$") or path return path:match("[^"..PATHSEP.."]+$") or path
end end
@ -461,7 +470,7 @@ end
---@param path string ---@param path string
---@return string|nil ---@return string|nil
function common.dirname(path) function common.dirname(path)
return path:match("(.+)[:\\/][^\\/]+$") return path:match("(.+)["..PATHSEP.."][^"..PATHSEP.."]+$")
end end
@ -507,10 +516,10 @@ end
local function split_on_slash(s, sep_pattern) local function split_on_slash(s, sep_pattern)
local t = {} local t = {}
if s:match("^[/\\]") then if s:match("^["..PATHSEP.."]") then
t[#t + 1] = "" t[#t + 1] = ""
end end
for fragment in string.gmatch(s, "([^/\\]+)") do for fragment in string.gmatch(s, "([^"..PATHSEP.."]+)") do
t[#t + 1] = fragment t[#t + 1] = fragment
end end
return t return t
@ -643,7 +652,7 @@ function common.mkdirp(path)
while path and path ~= "" do while path and path ~= "" do
local success_mkdir = system.mkdir(path) local success_mkdir = system.mkdir(path)
if success_mkdir then break end if success_mkdir then break end
local updir, basedir = path:match("(.*)[/\\](.+)$") local updir, basedir = path:match("(.*)["..PATHSEP.."](.+)$")
table.insert(subdirs, 1, basedir or path) table.insert(subdirs, 1, basedir or path)
path = updir path = updir
end end

View File

@ -2,15 +2,71 @@ local common = require "core.common"
local config = {} local config = {}
---The frame rate of Lite XL.
---Note that setting this value to the screen's refresh rate
---does not eliminate screen tearing.
---
---Defaults to 60.
---@type number
config.fps = 60 config.fps = 60
---Maximum number of log items that will be stored.
---When the number of log items exceed this value, old items will be discarded.
---
---Defaults to 800.
---@type number
config.max_log_items = 800 config.max_log_items = 800
---The timeout, in seconds, before a message dissapears from StatusView.
---
---Defaults to 5.
---@type number
config.message_timeout = 5 config.message_timeout = 5
---The number of pixels scrolled per-step.
---
---Defaults to 50 * SCALE.
---@type number
config.mouse_wheel_scroll = 50 * SCALE config.mouse_wheel_scroll = 50 * SCALE
---Enables/disables transitions when scrolling with the scrollbar.
---When enabled, the scrollbar will have inertia and slowly move towards the cursor.
---Otherwise, the scrollbar will immediately follow the cursor.
---
---Defaults to false.
---@type boolean
config.animate_drag_scroll = false config.animate_drag_scroll = false
---Enables/disables scrolling past the end of a document.
---
---Defaults to true.
---@type boolean
config.scroll_past_end = true config.scroll_past_end = true
---@type "expanded" | "contracted" | false @Force the scrollbar status of the DocView
---@alias config.scrollbartype
---| "expanded" # A thicker scrollbar is shown at all times.
---| "contracted" # A thinner scrollbar is shown at all times.
---| false # The scrollbar expands when the cursor hovers over it.
---Controls whether the DocView scrollbar is always shown or hidden.
---This option does not affect other View's scrollbars.
---
---Defaults to false.
---@type config.scrollbartype
config.force_scrollbar_status = false config.force_scrollbar_status = false
---The file size limit, in megabytes.
---Files larger than this size will not be shown in the file picker.
---
---Defaults to 10.
---@type number
config.file_size_limit = 10 config.file_size_limit = 10
---A list of files and directories to ignore.
---Each element is a Lua pattern, where patterns ending with a forward slash
---are recognized as directories while patterns ending with an anchor ("$") are
---recognized as files.
---@type string[]
config.ignore_files = { config.ignore_files = {
-- folders -- folders
"^%.svn/", "^%.git/", "^%.hg/", "^CVS/", "^%.Trash/", "^%.Trash%-.*/", "^%.svn/", "^%.git/", "^%.hg/", "^CVS/", "^%.Trash/", "^%.Trash%-.*/",
@ -21,46 +77,194 @@ config.ignore_files = {
"%.suo$", "%.pdb$", "%.idb$", "%.class$", "%.psd$", "%.db$", "%.suo$", "%.pdb$", "%.idb$", "%.class$", "%.psd$", "%.db$",
"^desktop%.ini$", "^%.DS_Store$", "^%.directory$", "^desktop%.ini$", "^%.DS_Store$", "^%.directory$",
} }
---Lua pattern used to find symbols when advanced syntax highlighting
---is not available.
---This pattern is also used for navigation, e.g. move to next word.
---
---The default pattern matches all letters, followed by any number
---of letters and digits.
---@type string
config.symbol_pattern = "[%a_][%w_]*" config.symbol_pattern = "[%a_][%w_]*"
---A list of characters that delimits a word.
---
---The default is ``" \t\n/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-"``
---@type string
config.non_word_chars = " \t\n/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-" config.non_word_chars = " \t\n/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-"
---The timeout, in seconds, before several consecutive actions
---are merged as a single undo step.
---
---The default is 0.3 seconds.
---@type number
config.undo_merge_timeout = 0.3 config.undo_merge_timeout = 0.3
---The maximum number of undo steps per-document.
---
---The default is 10000.
---@type number
config.max_undos = 10000 config.max_undos = 10000
---The maximum number of tabs shown at a time.
---
---The default is 8.
---@type number
config.max_tabs = 8 config.max_tabs = 8
---Shows/hides the tab bar when there is only one tab open.
---
---The tab bar is always shown by default.
---@type boolean
config.always_show_tabs = true config.always_show_tabs = true
-- Possible values: false, true, "no_selection"
---@alias config.highlightlinetype
---| true # Always highlight the current line.
---| false # Never highlight the current line.
---| "no_selection" # Highlight the current line if no text is selected.
---Highlights the current line.
---
---The default is true.
---@type config.highlightlinetype
config.highlight_current_line = true config.highlight_current_line = true
---The spacing between each line of text.
---
---The default is 120% of the height of the text (1.2).
---@type number
config.line_height = 1.2 config.line_height = 1.2
---The number of spaces each level of indentation represents.
---
---The default is 2.
---@type number
config.indent_size = 2 config.indent_size = 2
---The type of indentation.
---
---The default is "soft" (spaces).
---@type "soft" | "hard"
config.tab_type = "soft" config.tab_type = "soft"
---Do not remove whitespaces when advancing to the next line.
---
---Defaults to false.
---@type boolean
config.keep_newline_whitespace = false config.keep_newline_whitespace = false
---Default line endings for new files.
---
---Defaults to `crlf` (`\r\n`) on Windows and `lf` (`\n`) on everything else.
---@type "crlf" | "lf"
config.line_endings = PLATFORM == "Windows" and "crlf" or "lf"
---Maximum number of characters per-line for the line guide.
---
---Defaults to 80.
---@type number
config.line_limit = 80 config.line_limit = 80
---Maximum number of project files to keep track of.
---If the number of files in the project exceeds this number,
---Lite XL will not be able to keep track of them.
---They will be not be searched when searching for files or text.
---
---Defaults to 2000.
---@type number
config.max_project_files = 2000 config.max_project_files = 2000
---Enables/disables all transitions.
---
---Defaults to true.
---@type boolean
config.transitions = true config.transitions = true
---Enable/disable individual transitions.
---These values are overriden by `config.transitions`.
config.disabled_transitions = { config.disabled_transitions = {
---Disables scrolling transitions.
scroll = false, scroll = false,
---Disables transitions for CommandView's suggestions list.
commandview = false, commandview = false,
---Disables transitions for showing/hiding the context menu.
contextmenu = false, contextmenu = false,
---Disables transitions when clicking on log items in LogView.
logview = false, logview = false,
---Disables transitions for showing/hiding the Nagbar.
nagbar = false, nagbar = false,
---Disables transitions when scrolling the tab bar.
tabs = false, tabs = false,
---Disables transitions when a tab is being dragged.
tab_drag = false, tab_drag = false,
---Disables transitions when a notification is shown.
statusbar = false, statusbar = false,
} }
---The rate of all transitions.
---
---Defaults to 1.
---@type number
config.animation_rate = 1.0 config.animation_rate = 1.0
---The caret's blinking period, in seconds.
---
---Defaults to 0.8.
---@type number
config.blink_period = 0.8 config.blink_period = 0.8
---Disables caret blinking.
---
---Defaults to false.
---@type boolean
config.disable_blink = false config.disable_blink = false
---Draws whitespaces as dots.
---This option is deprecated.
---Please use the drawwhitespace plugin instead.
---@deprecated
config.draw_whitespace = false config.draw_whitespace = false
---Disables system-drawn window borders.
---
---When set to true, Lite XL draws its own window decorations,
---which can be useful for certain setups.
---
---Defaults to false.
---@type boolean
config.borderless = false config.borderless = false
---Shows/hides the close buttons on tabs.
---When hidden, users can close tabs via keyboard shortcuts or commands.
---
---Defaults to true.
---@type boolean
config.tab_close_button = true config.tab_close_button = true
---Maximum number of clicks recognized by Lite XL.
---
---Defaults to 3.
---@type number
config.max_clicks = 3 config.max_clicks = 3
-- set as true to be able to test non supported plugins ---Disables plugin version checking.
---Do not change this unless you know what you are doing.
---
---Defaults to false.
---@type boolean
config.skip_plugins_version = false config.skip_plugins_version = false
-- holds the plugins real config table -- holds the plugins real config table
local plugins_config = {} local plugins_config = {}
-- virtual representation of plugins config table ---A table containing configuration for all the plugins.
---
---This is a metatable that automaticaly creates a minimal
---configuration when a plugin is initially configured.
---Each plugins will then call `common.merge()` to get the finalized
---plugin config.
---Do not use raw operations on this table.
---@type table
config.plugins = {} config.plugins = {}
-- allows virtual access to the plugins config table -- allows virtual access to the plugins config table

View File

@ -12,11 +12,31 @@ local divider_width = 1
local divider_padding = 5 local divider_padding = 5
local DIVIDER = {} local DIVIDER = {}
---An item in the context menu.
---@class core.contextmenu.item
---@field text string
---@field info string|nil If provided, this text is displayed on the right side of the menu.
---@field command string|fun()
---A list of items with the same predicate.
---@see core.command.predicate
---@class core.contextmenu.itemset
---@field predicate core.command.predicate
---@field items core.contextmenu.item[]
---A context menu.
---@class core.contextmenu : core.object ---@class core.contextmenu : core.object
---@field itemset core.contextmenu.itemset[]
---@field show_context_menu boolean
---@field selected number
---@field position core.view.position
---@field current_scale number
local ContextMenu = Object:extend() local ContextMenu = Object:extend()
---A unique value representing the divider in a context menu.
ContextMenu.DIVIDER = DIVIDER ContextMenu.DIVIDER = DIVIDER
---Creates a new context menu.
function ContextMenu:new() function ContextMenu:new()
self.itemset = {} self.itemset = {}
self.show_context_menu = false self.show_context_menu = false
@ -55,12 +75,19 @@ local function update_items_size(items, update_binding)
items.width, items.height = width, height items.width, items.height = width, height
end end
---Registers a list of items into the context menu with a predicate.
---@param predicate core.command.predicate
---@param items core.contextmenu.item[]
function ContextMenu:register(predicate, items) function ContextMenu:register(predicate, items)
predicate = command.generate_predicate(predicate) predicate = command.generate_predicate(predicate)
update_items_size(items, true) update_items_size(items, true)
table.insert(self.itemset, { predicate = predicate, items = items }) table.insert(self.itemset, { predicate = predicate, items = items })
end end
---Shows the context menu.
---@param x number
---@param y number
---@return boolean # If true, the context menu is shown.
function ContextMenu:show(x, y) function ContextMenu:show(x, y)
self.items = nil self.items = nil
local items_list = { width = 0, height = 0 } local items_list = { width = 0, height = 0 }
@ -94,6 +121,7 @@ function ContextMenu:show(x, y)
return false return false
end end
---Hides the context menu.
function ContextMenu:hide() function ContextMenu:hide()
self.show_context_menu = false self.show_context_menu = false
self.items = nil self.items = nil
@ -102,6 +130,8 @@ function ContextMenu:hide()
core.request_cursor(core.active_view.cursor) core.request_cursor(core.active_view.cursor)
end end
---Returns an iterator that iterates over each context menu item and their dimensions.
---@return fun(): number, core.contextmenu.item, number, number, number, number
function ContextMenu:each_item() function ContextMenu:each_item()
local x, y, w = self.position.x, self.position.y, self.items.width local x, y, w = self.position.x, self.position.y, self.items.width
local oy = y local oy = y
@ -115,8 +145,12 @@ function ContextMenu:each_item()
end) end)
end end
---Event handler for mouse movements.
---@param px any
---@param py any
---@return boolean # true if the event is caught.
function ContextMenu:on_mouse_moved(px, py) function ContextMenu:on_mouse_moved(px, py)
if not self.show_context_menu then return end if not self.show_context_menu then return false end
self.selected = -1 self.selected = -1
for i, item, x, y, w, h in self:each_item() do for i, item, x, y, w, h in self:each_item() do
@ -128,6 +162,8 @@ function ContextMenu:on_mouse_moved(px, py)
return true return true
end end
---Event handler for when the selection is confirmed.
---@param item core.contextmenu.item
function ContextMenu:on_selected(item) function ContextMenu:on_selected(item)
if type(item.command) == "string" then if type(item.command) == "string" then
command.perform(item.command) command.perform(item.command)
@ -140,6 +176,7 @@ local function change_value(value, change)
return value + change return value + change
end end
---Selects the the previous item.
function ContextMenu:focus_previous() function ContextMenu:focus_previous()
self.selected = (self.selected == -1 or self.selected == 1) and #self.items or change_value(self.selected, -1) self.selected = (self.selected == -1 or self.selected == 1) and #self.items or change_value(self.selected, -1)
if self:get_item_selected() == DIVIDER then if self:get_item_selected() == DIVIDER then
@ -147,6 +184,7 @@ function ContextMenu:focus_previous()
end end
end end
---Selects the next item.
function ContextMenu:focus_next() function ContextMenu:focus_next()
self.selected = (self.selected == -1 or self.selected == #self.items) and 1 or change_value(self.selected, 1) self.selected = (self.selected == -1 or self.selected == #self.items) and 1 or change_value(self.selected, 1)
if self:get_item_selected() == DIVIDER then if self:get_item_selected() == DIVIDER then
@ -154,10 +192,13 @@ function ContextMenu:focus_next()
end end
end end
---Gets the currently selected item.
---@return core.contextmenu.item|nil
function ContextMenu:get_item_selected() function ContextMenu:get_item_selected()
return (self.items or {})[self.selected] return (self.items or {})[self.selected]
end end
---Hides the context menu and performs the command if an item is selected.
function ContextMenu:call_selected_item() function ContextMenu:call_selected_item()
local selected = self:get_item_selected() local selected = self:get_item_selected()
self:hide() self:hide()
@ -166,6 +207,12 @@ function ContextMenu:call_selected_item()
end end
end end
---Event handler for mouse press.
---@param button core.view.mousebutton
---@param px number
---@param py number
---@param clicks number
---@return boolean # true if the event is caught.
function ContextMenu:on_mouse_pressed(button, px, py, clicks) function ContextMenu:on_mouse_pressed(button, px, py, clicks)
local caught = false local caught = false
@ -186,14 +233,20 @@ function ContextMenu:on_mouse_pressed(button, px, py, clicks)
return caught return caught
end end
---@type fun(self: table, k: string, dest: number, rate?: number, name?: string)
ContextMenu.move_towards = View.move_towards ContextMenu.move_towards = View.move_towards
---Event handler for content update.
function ContextMenu:update() function ContextMenu:update()
if self.show_context_menu then if self.show_context_menu then
self:move_towards("height", self.items.height, nil, "contextmenu") self:move_towards("height", self.items.height, nil, "contextmenu")
end end
end end
---Draws the context menu.
---
---This wraps `ContextMenu:draw_context_menu()`.
---@see core.contextmenu.draw_context_menu
function ContextMenu:draw() function ContextMenu:draw()
if not self.show_context_menu then return end if not self.show_context_menu then return end
if self.current_scale ~= SCALE then if self.current_scale ~= SCALE then
@ -206,6 +259,7 @@ function ContextMenu:draw()
core.root_view:defer_draw(self.draw_context_menu, self) core.root_view:defer_draw(self.draw_context_menu, self)
end end
---Draws the context menu.
function ContextMenu:draw_context_menu() function ContextMenu:draw_context_menu()
if not self.items then return end if not self.items then return end
local bx, by, bw, bh = self.position.x, self.position.y, self.items.width, self.height local bx, by, bw, bh = self.position.x, self.position.y, self.items.width, self.height

View File

@ -91,6 +91,7 @@ end
-- designed to be run inside a coroutine. -- designed to be run inside a coroutine.
function dirwatch:check(change_callback, scan_time, wait_time) function dirwatch:check(change_callback, scan_time, wait_time)
local had_change = false local had_change = false
local last_error
self.monitor:check(function(id) self.monitor:check(function(id)
had_change = true had_change = true
if self.monitor:mode() == "single" then if self.monitor:mode() == "single" then
@ -102,7 +103,10 @@ function dirwatch:check(change_callback, scan_time, wait_time)
elseif self.reverse_watched[id] then elseif self.reverse_watched[id] then
change_callback(self.reverse_watched[id]) change_callback(self.reverse_watched[id])
end end
end, function(err)
last_error = err
end) end)
if last_error ~= nil then error(last_error) end
local start_time = system.get_time() local start_time = system.get_time()
for directory, old_modified in pairs(self.scanned) do for directory, old_modified in pairs(self.scanned) do
if old_modified then if old_modified then
@ -186,49 +190,47 @@ end
-- "root" will by an absolute path without trailing '/' -- "root" will by an absolute path without trailing '/'
-- "path" will be a path starting without '/' and without trailing '/' -- "path" will be a path starting without '/' and without trailing '/'
-- or the empty string. -- or the empty string.
-- It will identifies a sub-path within "root. -- It identifies a sub-path within "root".
-- The current path location will therefore always be: root .. path. -- The current path location will therefore always be: root .. path.
-- When recursing "root" will always be the same, only "path" will change. -- When recursing, "root" will always be the same, only "path" will change.
-- Returns a list of file "items". In each item the "filename" will be the -- Returns a list of file "items". In each item the "filename" will be the
-- complete file path relative to "root" *without* the trailing '/', and without the starting '/'. -- complete file path relative to "root" *without* the trailing '/', and without the starting '/'.
function dirwatch.get_directory_files(dir, root, path, t, entries_count, recurse_pred) function dirwatch.get_directory_files(dir, root, path, entries_count, recurse_pred)
local t = {}
local t0 = system.get_time() local t0 = system.get_time()
local t_elapsed = system.get_time() - t0
local dirs, files = {}, {}
local ignore_compiled = compile_ignore_files() local ignore_compiled = compile_ignore_files()
local all = system.list_dir(root .. PATHSEP .. path) local all = system.list_dir(root .. PATHSEP .. path)
if not all then return nil end if not all then return nil end
local entries = { }
for _, file in ipairs(all or {}) do for _, file in ipairs(all) do
local info = get_project_file_info(root, (path ~= "" and (path .. PATHSEP) or "") .. file, ignore_compiled) local info = get_project_file_info(root, (path ~= "" and (path .. PATHSEP) or "") .. file, ignore_compiled)
if info then if info then
table.insert(info.type == "dir" and dirs or files, info) table.insert(entries, info)
entries_count = entries_count + 1
end end
end end
table.sort(entries, compare_file)
local recurse_complete = true local recurse_complete = true
table.sort(dirs, compare_file) for _, info in ipairs(entries) do
for _, f in ipairs(dirs) do table.insert(t, info)
table.insert(t, f) entries_count = entries_count + 1
if recurse_pred(dir, f.filename, entries_count, t_elapsed) then if info.type == "dir" then
local _, complete, n = dirwatch.get_directory_files(dir, root, f.filename, t, entries_count, recurse_pred) if recurse_pred(dir, info.filename, entries_count, system.get_time() - t0) then
recurse_complete = recurse_complete and complete local t_rec, complete, n = dirwatch.get_directory_files(dir, root, info.filename, entries_count, recurse_pred)
if n ~= nil then recurse_complete = recurse_complete and complete
entries_count = n if n ~= nil then
entries_count = n
for _, info_rec in ipairs(t_rec) do
table.insert(t, info_rec)
end
end
else
recurse_complete = false
end end
else
recurse_complete = false
end end
end end
table.sort(files, compare_file)
for _, f in ipairs(files) do
table.insert(t, f)
end
return t, recurse_complete, entries_count return t, recurse_complete, entries_count
end end

View File

@ -1,5 +1,6 @@
local Object = require "core.object" local Object = require "core.object"
local Highlighter = require "core.doc.highlighter" local Highlighter = require "core.doc.highlighter"
local translate = require "core.doc.translate"
local core = require "core" local core = require "core"
local syntax = require "core.syntax" local syntax = require "core.syntax"
local config = require "core.config" local config = require "core.config"
@ -27,9 +28,11 @@ function Doc:new(filename, abs_filename, new_file)
self:load(filename) self:load(filename)
end end
end end
if new_file then
self.crlf = config.line_endings == "crlf"
end
end end
function Doc:reset() function Doc:reset()
self.lines = { "\n" } self.lines = { "\n" }
self.selections = { 1, 1, 1, 1 } self.selections = { 1, 1, 1, 1 }
@ -38,10 +41,10 @@ function Doc:reset()
self.redo_stack = { idx = 1 } self.redo_stack = { idx = 1 }
self.clean_change_id = 1 self.clean_change_id = 1
self.highlighter = Highlighter(self) self.highlighter = Highlighter(self)
self.overwrite = false
self:reset_syntax() self:reset_syntax()
end end
function Doc:reset_syntax() function Doc:reset_syntax()
local header = self:get_text(1, 1, self:position_offset(1, 1, 128)) local header = self:get_text(1, 1, self:position_offset(1, 1, 128))
local path = self.abs_filename local path = self.abs_filename
@ -56,16 +59,14 @@ function Doc:reset_syntax()
end end
end end
function Doc:set_filename(filename, abs_filename) function Doc:set_filename(filename, abs_filename)
self.filename = filename self.filename = filename
self.abs_filename = abs_filename self.abs_filename = abs_filename
self:reset_syntax() self:reset_syntax()
end end
function Doc:load(filename) function Doc:load(filename)
local fp = assert( io.open(filename, "rb") ) local fp = assert(io.open(filename, "rb"))
self:reset() self:reset()
self.lines = {} self.lines = {}
local i = 1 local i = 1
@ -85,7 +86,6 @@ function Doc:load(filename)
self:reset_syntax() self:reset_syntax()
end end
function Doc:reload() function Doc:reload()
if self.filename then if self.filename then
local sel = { self:get_selection() } local sel = { self:get_selection() }
@ -95,7 +95,6 @@ function Doc:reload()
end end
end end
function Doc:save(filename, abs_filename) function Doc:save(filename, abs_filename)
if not filename then if not filename then
assert(self.filename, "no filename set to default to") assert(self.filename, "no filename set to default to")
@ -104,7 +103,7 @@ function Doc:save(filename, abs_filename)
else else
assert(self.filename or abs_filename, "calling save on unnamed doc without absolute path") assert(self.filename or abs_filename, "calling save on unnamed doc without absolute path")
end end
local fp = assert( io.open(filename, "wb") ) local fp = assert(io.open(filename, "wb"))
for _, line in ipairs(self.lines) do for _, line in ipairs(self.lines) do
if self.crlf then line = line:gsub("\n", "\r\n") end if self.crlf then line = line:gsub("\n", "\r\n") end
fp:write(line) fp:write(line)
@ -115,34 +114,30 @@ function Doc:save(filename, abs_filename)
self:clean() self:clean()
end end
function Doc:get_name() function Doc:get_name()
return self.filename or "unsaved" return self.filename or "unsaved"
end end
function Doc:is_dirty() function Doc:is_dirty()
if self.new_file then if self.new_file then
if self.filename then return true end
return #self.lines > 1 or #self.lines[1] > 1 return #self.lines > 1 or #self.lines[1] > 1
else else
return self.clean_change_id ~= self:get_change_id() return self.clean_change_id ~= self:get_change_id()
end end
end end
function Doc:clean() function Doc:clean()
self.clean_change_id = self:get_change_id() self.clean_change_id = self:get_change_id()
end end
function Doc:get_indent_info() function Doc:get_indent_info()
if not self.indent_info then return config.tab_type, config.indent_size, false end if not self.indent_info then return config.tab_type, config.indent_size, false end
return self.indent_info.type or config.tab_type, return self.indent_info.type or config.tab_type,
self.indent_info.size or config.indent_size, self.indent_info.size or config.indent_size,
self.indent_info.confirmed self.indent_info.confirmed
end end
function Doc:get_change_id() function Doc:get_change_id()
return self.undo_stack.idx return self.undo_stack.idx
end end
@ -166,13 +161,14 @@ function Doc:get_selection(sort)
return line1, col1, line2, col2, swap return line1, col1, line2, col2, swap
end end
---Get the selection specified by `idx` ---Get the selection specified by `idx`
---@param idx integer @the index of the selection to retrieve ---@param idx integer @the index of the selection to retrieve
---@param sort? boolean @whether to sort the selection returned ---@param sort? boolean @whether to sort the selection returned
---@return integer,integer,integer,integer,boolean? @line1, col1, line2, col2, was the selection sorted ---@return integer,integer,integer,integer,boolean? @line1, col1, line2, col2, was the selection sorted
function Doc:get_selection_idx(idx, sort) function Doc:get_selection_idx(idx, sort)
local line1, col1, line2, col2 = self.selections[idx*4-3], self.selections[idx*4-2], self.selections[idx*4-1], self.selections[idx*4] local line1, col1, line2, col2 = self.selections[idx * 4 - 3], self.selections[idx * 4 - 2],
self.selections[idx * 4 - 1],
self.selections[idx * 4]
if line1 and sort then if line1 and sort then
return sort_positions(line1, col1, line2, col2) return sort_positions(line1, col1, line2, col2)
else else
@ -216,7 +212,7 @@ function Doc:set_selections(idx, line1, col1, line2, col2, swap, rm)
if swap then line1, col1, line2, col2 = line2, col2, line1, col1 end if swap then line1, col1, line2, col2 = line2, col2, line1, col1 end
line1, col1 = self:sanitize_position(line1, col1) line1, col1 = self:sanitize_position(line1, col1)
line2, col2 = self:sanitize_position(line2 or line1, col2 or col1) line2, col2 = self:sanitize_position(line2 or line1, col2 or col1)
common.splice(self.selections, (idx - 1)*4 + 1, rm == nil and 4 or rm, { line1, col1, line2, col2 }) common.splice(self.selections, (idx - 1) * 4 + 1, rm == nil and 4 or rm, { line1, col1, line2, col2 })
end end
function Doc:add_selection(line1, col1, line2, col2, swap) function Doc:add_selection(line1, col1, line2, col2, swap)
@ -232,7 +228,6 @@ function Doc:add_selection(line1, col1, line2, col2, swap)
self.last_selection = target self.last_selection = target
end end
function Doc:remove_selection(idx) function Doc:remove_selection(idx)
if self.last_selection >= idx then if self.last_selection >= idx then
self.last_selection = self.last_selection - 1 self.last_selection = self.last_selection - 1
@ -240,7 +235,6 @@ function Doc:remove_selection(idx)
common.splice(self.selections, (idx - 1) * 4 + 1, 4) common.splice(self.selections, (idx - 1) * 4 + 1, 4)
end end
function Doc:set_selection(line1, col1, line2, col2, swap) function Doc:set_selection(line1, col1, line2, col2, swap)
self.selections = {} self.selections = {}
self:set_selections(1, line1, col1, line2, col2, swap) self:set_selections(1, line1, col1, line2, col2, swap)
@ -251,24 +245,24 @@ function Doc:merge_cursors(idx)
for i = (idx or (#self.selections - 3)), (idx or 5), -4 do for i = (idx or (#self.selections - 3)), (idx or 5), -4 do
for j = 1, i - 4, 4 do for j = 1, i - 4, 4 do
if self.selections[i] == self.selections[j] and if self.selections[i] == self.selections[j] and
self.selections[i+1] == self.selections[j+1] then self.selections[i + 1] == self.selections[j + 1] then
common.splice(self.selections, i, 4) common.splice(self.selections, i, 4)
if self.last_selection >= (i+3)/4 then if self.last_selection >= (i + 3) / 4 then
self.last_selection = self.last_selection - 1 self.last_selection = self.last_selection - 1
end end
break break
end end
end end
end end
end end
local function selection_iterator(invariant, idx) local function selection_iterator(invariant, idx)
local target = invariant[3] and (idx*4 - 7) or (idx*4 + 1) local target = invariant[3] and (idx * 4 - 7) or (idx * 4 + 1)
if target > #invariant[1] or target <= 0 or (type(invariant[3]) == "number" and invariant[3] ~= idx - 1) then return end if target > #invariant[1] or target <= 0 or (type(invariant[3]) == "number" and invariant[3] ~= idx - 1) then return end
if invariant[2] then if invariant[2] then
return idx+(invariant[3] and -1 or 1), sort_positions(table.unpack(invariant[1], target, target+4)) return idx + (invariant[3] and -1 or 1), sort_positions(table.unpack(invariant[1], target, target + 4))
else else
return idx+(invariant[3] and -1 or 1), table.unpack(invariant[1], target, target+4) return idx + (invariant[3] and -1 or 1), table.unpack(invariant[1], target, target + 4)
end end
end end
@ -276,8 +270,9 @@ end
-- If a number, runs for exactly that iteration. -- If a number, runs for exactly that iteration.
function Doc:get_selections(sort_intra, idx_reverse) function Doc:get_selections(sort_intra, idx_reverse)
return selection_iterator, { self.selections, sort_intra, idx_reverse }, return selection_iterator, { self.selections, sort_intra, idx_reverse },
idx_reverse == true and ((#self.selections / 4) + 1) or ((idx_reverse or -1)+1) idx_reverse == true and ((#self.selections / 4) + 1) or ((idx_reverse or -1) + 1)
end end
-- End of cursor seciton. -- End of cursor seciton.
function Doc:sanitize_position(line, col) function Doc:sanitize_position(line, col)
@ -290,7 +285,6 @@ function Doc:sanitize_position(line, col)
return line, common.clamp(col, 1, #self.lines[line]) return line, common.clamp(col, 1, #self.lines[line])
end end
local function position_offset_func(self, line, col, fn, ...) local function position_offset_func(self, line, col, fn, ...)
line, col = self:sanitize_position(line, col) line, col = self:sanitize_position(line, col)
return fn(self, line, col, ...) return fn(self, line, col, ...)
@ -329,7 +323,6 @@ function Doc:position_offset(line, col, ...)
end end
end end
function Doc:get_text(line1, col1, line2, col2) function Doc:get_text(line1, col1, line2, col2)
line1, col1 = self:sanitize_position(line1, col1) line1, col1 = self:sanitize_position(line1, col1)
line2, col2 = self:sanitize_position(line2, col2) line2, col2 = self:sanitize_position(line2, col2)
@ -345,13 +338,11 @@ function Doc:get_text(line1, col1, line2, col2)
return table.concat(lines) return table.concat(lines)
end end
function Doc:get_char(line, col) function Doc:get_char(line, col)
line, col = self:sanitize_position(line, col) line, col = self:sanitize_position(line, col)
return self.lines[line]:sub(col, col) return self.lines[line]:sub(col, col)
end end
local function push_undo(undo_stack, time, type, ...) local function push_undo(undo_stack, time, type, ...)
undo_stack[undo_stack.idx] = { type = type, time = time, ... } undo_stack[undo_stack.idx] = { type = type, time = time, ... }
undo_stack[undo_stack.idx - config.max_undos] = nil undo_stack[undo_stack.idx - config.max_undos] = nil
@ -412,7 +403,8 @@ function Doc:raw_insert(line, col, text, undo_stack, time)
if cline1 < line then break end if cline1 < line then break end
local line_addition = (line < cline1 or col < ccol1) and #lines - 1 or 0 local line_addition = (line < cline1 or col < ccol1) and #lines - 1 or 0
local column_addition = line == cline1 and ccol1 > col and len or 0 local column_addition = line == cline1 and ccol1 > col and len or 0
self:set_selections(idx, cline1 + line_addition, ccol1 + column_addition, cline2 + line_addition, ccol2 + column_addition) self:set_selections(idx, cline1 + line_addition, ccol1 + column_addition, cline2 + line_addition,
ccol2 + column_addition)
end end
-- push undo -- push undo
@ -425,7 +417,6 @@ function Doc:raw_insert(line, col, text, undo_stack, time)
self:sanitize_selection() self:sanitize_selection()
end end
function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time) function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time)
-- push undo -- push undo
local text = self:get_text(line1, col1, line2, col2) local text = self:get_text(line1, col1, line2, col2)
@ -484,15 +475,17 @@ function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time)
self:sanitize_selection() self:sanitize_selection()
end end
function Doc:insert(line, col, text) function Doc:insert(line, col, text)
self.redo_stack = { idx = 1 } self.redo_stack = { idx = 1 }
-- Reset the clean id when we're pushing something new before it
if self:get_change_id() < self.clean_change_id then
self.clean_change_id = -1
end
line, col = self:sanitize_position(line, col) line, col = self:sanitize_position(line, col)
self:raw_insert(line, col, text, self.undo_stack, system.get_time()) self:raw_insert(line, col, text, self.undo_stack, system.get_time())
self:on_text_change("insert") self:on_text_change("insert")
end end
function Doc:remove(line1, col1, line2, col2) function Doc:remove(line1, col1, line2, col2)
self.redo_stack = { idx = 1 } self.redo_stack = { idx = 1 }
line1, col1 = self:sanitize_position(line1, col1) line1, col1 = self:sanitize_position(line1, col1)
@ -502,28 +495,34 @@ function Doc:remove(line1, col1, line2, col2)
self:on_text_change("remove") self:on_text_change("remove")
end end
function Doc:undo() function Doc:undo()
pop_undo(self, self.undo_stack, self.redo_stack, false) pop_undo(self, self.undo_stack, self.redo_stack, false)
end end
function Doc:redo() function Doc:redo()
pop_undo(self, self.redo_stack, self.undo_stack, false) pop_undo(self, self.redo_stack, self.undo_stack, false)
end end
function Doc:text_input(text, idx) function Doc:text_input(text, idx)
for sidx, line1, col1, line2, col2 in self:get_selections(true, idx or true) do for sidx, line1, col1, line2, col2 in self:get_selections(true, idx or true) do
local had_selection = false
if line1 ~= line2 or col1 ~= col2 then if line1 ~= line2 or col1 ~= col2 then
self:delete_to_cursor(sidx) self:delete_to_cursor(sidx)
had_selection = true
end end
if self.overwrite
and not had_selection
and col1 < #self.lines[line1]
and text:ulen() == 1 then
self:remove(line1, col1, translate.next_char(self, line1, col1))
end
self:insert(line1, col1, text) self:insert(line1, col1, text)
self:move_to_cursor(sidx, #text) self:move_to_cursor(sidx, #text)
end end
end end
function Doc:ime_text_editing(text, start, length, idx) function Doc:ime_text_editing(text, start, length, idx)
for sidx, line1, col1, line2, col2 in self:get_selections(true, idx or true) do for sidx, line1, col1, line2, col2 in self:get_selections(true, idx or true) do
if line1 ~= line2 or col1 ~= col2 then if line1 ~= line2 or col1 ~= col2 then
@ -534,7 +533,6 @@ function Doc:ime_text_editing(text, start, length, idx)
end end
end end
function Doc:replace_cursor(idx, line1, col1, line2, col2, fn) function Doc:replace_cursor(idx, line1, col1, line2, col2, fn)
local old_text = self:get_text(line1, col1, line2, col2) local old_text = self:get_text(line1, col1, line2, col2)
local new_text, res = fn(old_text) local new_text, res = fn(old_text)
@ -550,7 +548,7 @@ function Doc:replace_cursor(idx, line1, col1, line2, col2, fn)
end end
function Doc:replace(fn) function Doc:replace(fn)
local has_selection, results = false, { } local has_selection, results = false, {}
for idx, line1, col1, line2, col2 in self:get_selections(true) do for idx, line1, col1, line2, col2 in self:get_selections(true) do
if line1 ~= line2 or col1 ~= col2 then if line1 ~= line2 or col1 ~= col2 then
results[idx] = self:replace_cursor(idx, line1, col1, line2, col2, fn) results[idx] = self:replace_cursor(idx, line1, col1, line2, col2, fn)
@ -564,7 +562,6 @@ function Doc:replace(fn)
return results return results
end end
function Doc:delete_to_cursor(idx, ...) function Doc:delete_to_cursor(idx, ...)
for sidx, line1, col1, line2, col2 in self:get_selections(true, idx) do for sidx, line1, col1, line2, col2 in self:get_selections(true, idx) do
if line1 ~= line2 or col1 ~= col2 then if line1 ~= line2 or col1 ~= col2 then
@ -578,6 +575,7 @@ function Doc:delete_to_cursor(idx, ...)
end end
self:merge_cursors(idx) self:merge_cursors(idx)
end end
function Doc:delete_to(...) return self:delete_to_cursor(nil, ...) end function Doc:delete_to(...) return self:delete_to_cursor(nil, ...) end
function Doc:move_to_cursor(idx, ...) function Doc:move_to_cursor(idx, ...)
@ -586,8 +584,8 @@ function Doc:move_to_cursor(idx, ...)
end end
self:merge_cursors(idx) self:merge_cursors(idx)
end end
function Doc:move_to(...) return self:move_to_cursor(nil, ...) end
function Doc:move_to(...) return self:move_to_cursor(nil, ...) end
function Doc:select_to_cursor(idx, ...) function Doc:select_to_cursor(idx, ...)
for sidx, line, col, line2, col2 in self:get_selections(false, idx) do for sidx, line, col, line2, col2 in self:get_selections(false, idx) do
@ -596,8 +594,8 @@ function Doc:select_to_cursor(idx, ...)
end end
self:merge_cursors(idx) self:merge_cursors(idx)
end end
function Doc:select_to(...) return self:select_to_cursor(nil, ...) end
function Doc:select_to(...) return self:select_to_cursor(nil, ...) end
function Doc:get_indent_string() function Doc:get_indent_string()
local indent_type, indent_size = self:get_indent_info() local indent_type, indent_size = self:get_indent_info()
@ -620,7 +618,7 @@ function Doc:get_line_indent(line, rnd_up)
local indent = e and line:sub(1, e):gsub("\t", soft_tab) or "" local indent = e and line:sub(1, e):gsub("\t", soft_tab) or ""
local number = #indent / #soft_tab local number = #indent / #soft_tab
return e, indent:sub(1, return e, indent:sub(1,
(rnd_up and math.ceil(number) or math.floor(number))*#soft_tab) (rnd_up and math.ceil(number) or math.floor(number)) * #soft_tab)
end end
end end
@ -669,5 +667,4 @@ function Doc:on_close()
core.log_quiet("Closed doc \"%s\"", self:get_name()) core.log_quiet("Closed doc \"%s\"", self:get_name())
end end
return Doc return Doc

View File

@ -66,7 +66,18 @@ function search.find(doc, line, col, text, opt)
s, e = search_func(line_text, pattern, col, plain) s, e = search_func(line_text, pattern, col, plain)
end end
if s then if s then
return line, s, line, e + 1 local line2 = line
-- If we've matched the newline too,
-- return until the initial character of the next line.
if e >= #doc.lines[line] then
line2 = line + 1
e = 0
end
-- Avoid returning matches that go beyond the last line.
-- This is needed to avoid selecting the "last" newline.
if line2 <= #doc.lines then
return line, s, line2, e + 1
end
end end
col = opt.reverse and -1 or 1 col = opt.reverse and -1 or 1
end end

View File

@ -460,9 +460,16 @@ function DocView:draw_line_text(line, x, y)
return self:get_line_height() return self:get_line_height()
end end
function DocView:draw_overwrite_caret(x, y, width)
local lh = self:get_line_height()
renderer.draw_rect(x, y + lh - style.caret_width, width, style.caret_width, style.caret)
end
function DocView:draw_caret(x, y) function DocView:draw_caret(x, y)
local lh = self:get_line_height() local lh = self:get_line_height()
renderer.draw_rect(x, y, style.caret_width, lh, style.caret) renderer.draw_rect(x, y, style.caret_width, lh, style.caret)
end end
function DocView:draw_line_body(line, x, y) function DocView:draw_line_body(line, x, y)
@ -559,7 +566,12 @@ function DocView:draw_overlay()
else else
if config.disable_blink if config.disable_blink
or (core.blink_timer - core.blink_start) % T < T / 2 then or (core.blink_timer - core.blink_start) % T < T / 2 then
self:draw_caret(self:get_line_screen_position(line1, col1)) local x, y = self:get_line_screen_position(line1, col1)
if self.doc.overwrite then
self:draw_overwrite_caret(x, y, self:get_font():get_width(self.doc:get_char(line1, col1)))
else
self:draw_caret(x, y)
end
end end
end end
end end

View File

@ -102,7 +102,7 @@ local function strip_leading_path(filename)
end end
local function strip_trailing_slash(filename) local function strip_trailing_slash(filename)
if filename:match("[^:][/\\]$") then if filename:match("[^:]["..PATHSEP.."]$") then
return filename:sub(1, -2) return filename:sub(1, -2)
end end
return filename return filename
@ -120,9 +120,7 @@ local function show_max_files_warning(dir)
"Too many files in project directory: stopped reading at ".. "Too many files in project directory: stopped reading at "..
config.max_project_files.." files. For more information see ".. config.max_project_files.." files. For more information see "..
"usage.md at https://github.com/lite-xl/lite-xl." "usage.md at https://github.com/lite-xl/lite-xl."
if core.status_view then core.warn(message)
core.status_view:show_message("!", style.accent, message)
end
end end
@ -184,7 +182,7 @@ local function refresh_directory(topdir, target)
directory_start_idx = directory_start_idx + 1 directory_start_idx = directory_start_idx + 1
end end
local files = dirwatch.get_directory_files(topdir, topdir.name, (target or ""), {}, 0, function() return false end) local files = dirwatch.get_directory_files(topdir, topdir.name, (target or ""), 0, function() return false end)
local change = false local change = false
-- If this file doesn't exist, we should be calling this on our parent directory, assume we'll do that. -- If this file doesn't exist, we should be calling this on our parent directory, assume we'll do that.
@ -265,7 +263,7 @@ function core.add_project_directory(path)
local fstype = PLATFORM == "Linux" and system.get_fs_type(topdir.name) or "unknown" local fstype = PLATFORM == "Linux" and system.get_fs_type(topdir.name) or "unknown"
topdir.force_scans = (fstype == "nfs" or fstype == "fuse") topdir.force_scans = (fstype == "nfs" or fstype == "fuse")
local t, complete, entries_count = dirwatch.get_directory_files(topdir, topdir.name, "", {}, 0, timed_max_files_pred) local t, complete, entries_count = dirwatch.get_directory_files(topdir, topdir.name, "", 0, timed_max_files_pred)
topdir.files = t topdir.files = t
if not complete then if not complete then
topdir.slow_filesystem = not complete and (entries_count <= config.max_project_files) topdir.slow_filesystem = not complete and (entries_count <= config.max_project_files)
@ -810,7 +808,11 @@ function core.init()
end end
if not plugins_success or got_user_error or got_project_error then if not plugins_success or got_user_error or got_project_error then
command.perform("core:open-log") -- defer LogView to after everything is initialized,
-- so that EmptyView won't be added after LogView.
core.add_thread(function()
command.perform("core:open-log")
end)
end end
core.configure_borderless_window() core.configure_borderless_window()
@ -1274,6 +1276,9 @@ function core.on_event(type, ...)
elseif type == "textediting" then elseif type == "textediting" then
ime.on_text_editing(...) ime.on_text_editing(...)
elseif type == "keypressed" then elseif type == "keypressed" then
-- In some cases during IME composition input is still sent to us
-- so we just ignore it.
if ime.editing then return false end
did_keymap = keymap.on_key_pressed(...) did_keymap = keymap.on_key_pressed(...)
elseif type == "keyreleased" then elseif type == "keyreleased" then
keymap.on_key_released(...) keymap.on_key_released(...)
@ -1418,11 +1423,11 @@ local run_threads = coroutine.wrap(function()
-- stop running threads if we're about to hit the end of frame -- stop running threads if we're about to hit the end of frame
if system.get_time() - core.frame_start > max_time then if system.get_time() - core.frame_start > max_time then
coroutine.yield(0) coroutine.yield(0, false)
end end
end end
coroutine.yield(minimal_time_to_wake) coroutine.yield(minimal_time_to_wake, true)
end end
end) end)
@ -1430,10 +1435,15 @@ end)
function core.run() function core.run()
local next_step local next_step
local last_frame_time local last_frame_time
local run_threads_full = 0
while true do while true do
core.frame_start = system.get_time() core.frame_start = system.get_time()
local time_to_wake = run_threads() local time_to_wake, threads_done = run_threads()
if threads_done then
run_threads_full = run_threads_full + 1
end
local did_redraw = false local did_redraw = false
local did_step = false
local force_draw = core.redraw and last_frame_time and core.frame_start - last_frame_time > (1 / config.fps) local force_draw = core.redraw and last_frame_time and core.frame_start - last_frame_time > (1 / config.fps)
if force_draw or not next_step or system.get_time() >= next_step then if force_draw or not next_step or system.get_time() >= next_step then
if core.step() then if core.step() then
@ -1441,11 +1451,12 @@ function core.run()
last_frame_time = core.frame_start last_frame_time = core.frame_start
end end
next_step = nil next_step = nil
did_step = true
end end
if core.restart_request or core.quit_request then break end if core.restart_request or core.quit_request then break end
if not did_redraw then if not did_redraw then
if system.window_has_focus() then if system.window_has_focus() or not did_step or run_threads_full < 2 then
local now = system.get_time() local now = system.get_time()
if not next_step then -- compute the time until the next blink if not next_step then -- compute the time until the next blink
local t = now - core.blink_start local t = now - core.blink_start
@ -1454,7 +1465,7 @@ function core.run()
local cursor_time_to_wake = dt + 1 / config.fps local cursor_time_to_wake = dt + 1 / config.fps
next_step = now + cursor_time_to_wake next_step = now + cursor_time_to_wake
end end
if time_to_wake > 0 and system.wait_event(math.min(next_step - now, time_to_wake)) then if system.wait_event(math.min(next_step - now, time_to_wake)) then
next_step = nil -- if we've recevied an event, perform a step next_step = nil -- if we've recevied an event, perform a step
end end
else else
@ -1462,6 +1473,7 @@ function core.run()
next_step = nil -- perform a step when we're not in focus if get we an event next_step = nil -- perform a step when we're not in focus if get we an event
end end
else -- if we redrew, then make sure we only draw at most FPS/sec else -- if we redrew, then make sure we only draw at most FPS/sec
run_threads_full = 0
local now = system.get_time() local now = system.get_time()
local elapsed = now - core.frame_start local elapsed = now - core.frame_start
local next_frame = math.max(0, 1 / config.fps - elapsed) local next_frame = math.max(0, 1 / config.fps - elapsed)

View File

@ -42,15 +42,19 @@ local modkeys = modkeys_os.keys
---@return string ---@return string
local function normalize_stroke(stroke) local function normalize_stroke(stroke)
local stroke_table = {} local stroke_table = {}
for modkey in stroke:gmatch("(%w+)%+") do for key in stroke:gmatch("[^+]+") do
table.insert(stroke_table, modkey) table.insert(stroke_table, key)
end end
if not next(stroke_table) then table.sort(stroke_table, function(a, b)
return stroke if a == b then return false end
end for _, mod in ipairs(modkeys) do
table.sort(stroke_table) if a == mod or b == mod then
local new_stroke = table.concat(stroke_table, "+") .. "+" return a == mod
return new_stroke .. stroke:sub(new_stroke:len() + 1) end
end
return a < b
end)
return table.concat(stroke_table, "+")
end end
@ -58,13 +62,13 @@ end
---@param key string ---@param key string
---@return string ---@return string
local function key_to_stroke(key) local function key_to_stroke(key)
local stroke = "" local keys = { key }
for _, mk in ipairs(modkeys) do for _, mk in ipairs(modkeys) do
if keymap.modkeys[mk] then if keymap.modkeys[mk] then
stroke = stroke .. mk .. "+" table.insert(keys, mk)
end end
end end
return normalize_stroke(stroke) .. key return normalize_stroke(table.concat(keys, "+"))
end end
---Remove the given value from an array associated to a key in a table. ---Remove the given value from an array associated to a key in a table.
@ -92,12 +96,12 @@ end
---@param map keymap.map ---@param map keymap.map
local function remove_duplicates(map) local function remove_duplicates(map)
for stroke, commands in pairs(map) do for stroke, commands in pairs(map) do
stroke = normalize_stroke(stroke) local normalized_stroke = normalize_stroke(stroke)
if type(commands) == "string" or type(commands) == "function" then if type(commands) == "string" or type(commands) == "function" then
commands = { commands } commands = { commands }
end end
if keymap.map[stroke] then if keymap.map[normalized_stroke] then
for _, registered_cmd in ipairs(keymap.map[stroke]) do for _, registered_cmd in ipairs(keymap.map[normalized_stroke]) do
local j = 0 local j = 0
for i=1, #commands do for i=1, #commands do
while commands[i + j] == registered_cmd do while commands[i + j] == registered_cmd do
@ -120,7 +124,6 @@ end
function keymap.add_direct(map) function keymap.add_direct(map)
for stroke, commands in pairs(map) do for stroke, commands in pairs(map) do
stroke = normalize_stroke(stroke) stroke = normalize_stroke(stroke)
if type(commands) == "string" or type(commands) == "function" then if type(commands) == "string" or type(commands) == "function" then
commands = { commands } commands = { commands }
end end
@ -174,7 +177,8 @@ end
---@param shortcut string ---@param shortcut string
---@param cmd string ---@param cmd string
function keymap.unbind(shortcut, cmd) function keymap.unbind(shortcut, cmd)
remove_only(keymap.map, normalize_stroke(shortcut), cmd) shortcut = normalize_stroke(shortcut)
remove_only(keymap.map, shortcut, cmd)
remove_only(keymap.reverse_map, cmd, shortcut) remove_only(keymap.reverse_map, cmd, shortcut)
end end
@ -199,10 +203,6 @@ end
-- Events listening -- Events listening
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
function keymap.on_key_pressed(k, ...) function keymap.on_key_pressed(k, ...)
-- In MacOS and Windows during IME composition input is still sent to us
-- so we just ignore it
if PLATFORM ~= "Linux" and ime.editing then return false end
local mk = modkey_map[k] local mk = modkey_map[k]
if mk then if mk then
keymap.modkeys[mk] = true keymap.modkeys[mk] = true
@ -341,6 +341,7 @@ keymap.add_direct {
["ctrl+x"] = "doc:cut", ["ctrl+x"] = "doc:cut",
["ctrl+c"] = "doc:copy", ["ctrl+c"] = "doc:copy",
["ctrl+v"] = "doc:paste", ["ctrl+v"] = "doc:paste",
["insert"] = "doc:toggle-overwrite",
["ctrl+insert"] = "doc:copy", ["ctrl+insert"] = "doc:copy",
["shift+insert"] = "doc:paste", ["shift+insert"] = "doc:paste",
["escape"] = { "command:escape", "doc:select-none", "dialog:select-no" }, ["escape"] = { "command:escape", "doc:select-none", "dialog:select-no" },

View File

@ -7,8 +7,12 @@ modkeys.map = {
["right shift"] = "shift", ["right shift"] = "shift",
["left alt"] = "alt", ["left alt"] = "alt",
["right alt"] = "altgr", ["right alt"] = "altgr",
["left gui"] = "super",
["left windows"] = "super",
["right gui"] = "super",
["right windows"] = "super"
} }
modkeys.keys = { "ctrl", "alt", "altgr", "shift" } modkeys.keys = { "ctrl", "shift", "alt", "altgr", "super" }
return modkeys return modkeys

View File

@ -13,6 +13,6 @@ modkeys.map = {
["right alt"] = "altgr", ["right alt"] = "altgr",
} }
modkeys.keys = { "cmd", "ctrl", "alt", "option", "altgr", "shift" } modkeys.keys = { "ctrl", "alt", "option", "altgr", "shift", "cmd" }
return modkeys return modkeys

View File

@ -24,6 +24,7 @@ function NagView:new()
self.scrollable = true self.scrollable = true
self.target_height = 0 self.target_height = 0
self.on_mouse_pressed_root = nil self.on_mouse_pressed_root = nil
self.dim_alpha = 0
end end
function NagView:get_title() function NagView:get_title()
@ -68,7 +69,9 @@ function NagView:dim_window_content()
oy = oy + self.show_height oy = oy + self.show_height
local w, h = core.root_view.size.x, core.root_view.size.y - oy local w, h = core.root_view.size.x, core.root_view.size.y - oy
core.root_view:defer_draw(function() core.root_view:defer_draw(function()
renderer.draw_rect(ox, oy, w, h, style.nagbar_dim) local dim_color = { table.unpack(style.nagbar_dim) }
dim_color[4] = style.nagbar_dim[4] * self.dim_alpha
renderer.draw_rect(ox, oy, w, h, dim_color)
end) end)
end end
@ -172,10 +175,13 @@ function NagView:update()
NagView.super.update(self) NagView.super.update(self)
if self.visible and core.active_view == self and self.title then if self.visible and core.active_view == self and self.title then
self:move_towards(self, "show_height", self:get_target_height(), nil, "nagbar") local target_height = self:get_target_height()
self:move_towards(self, "show_height", target_height, nil, "nagbar")
self:move_towards(self, "underline_progress", 1, nil, "nagbar") self:move_towards(self, "underline_progress", 1, nil, "nagbar")
self:move_towards(self, "dim_alpha", self.show_height / target_height, nil, "nagbar")
else else
self:move_towards(self, "show_height", 0, nil, "nagbar") self:move_towards(self, "show_height", 0, nil, "nagbar")
self:move_towards(self, "dim_alpha", 0, nil, "nagbar")
if self.show_height <= 0 then if self.show_height <= 0 then
self.title = nil self.title = nil
self.message = nil self.message = nil

View File

@ -177,8 +177,12 @@ function Node:add_view(view, idx)
assert(not self.locked, "Tried to add view to locked node") assert(not self.locked, "Tried to add view to locked node")
if self.views[1] and self.views[1]:is(EmptyView) then if self.views[1] and self.views[1]:is(EmptyView) then
table.remove(self.views) table.remove(self.views)
if idx and idx > 1 then
idx = idx - 1
end
end end
table.insert(self.views, idx or (#self.views + 1), view) idx = common.clamp(idx or (#self.views + 1), 1, (#self.views + 1))
table.insert(self.views, idx, view)
self:set_active_view(view) self:set_active_view(view)
end end

View File

@ -313,10 +313,10 @@ function RootView:on_mouse_moved(x, y, dx, dy)
if self.dragged_divider then if self.dragged_divider then
local node = self.dragged_divider local node = self.dragged_divider
if node.type == "hsplit" then if node.type == "hsplit" then
x = common.clamp(x, 0, self.root_node.size.x * 0.95) x = common.clamp(x - node.position.x, 0, self.root_node.size.x * 0.95)
resize_child_node(node, "x", x, dx) resize_child_node(node, "x", x, dx)
elseif node.type == "vsplit" then elseif node.type == "vsplit" then
y = common.clamp(y, 0, self.root_node.size.y * 0.95) y = common.clamp(y - node.position.y, 0, self.root_node.size.y * 0.95)
resize_child_node(node, "y", y, dy) resize_child_node(node, "y", y, dy)
end end
node.divider = common.clamp(node.divider, 0.01, 0.99) node.divider = common.clamp(node.divider, 0.01, 0.99)
@ -406,10 +406,10 @@ function RootView:on_touch_moved(x, y, dx, dy, ...)
if self.dragged_divider then if self.dragged_divider then
local node = self.dragged_divider local node = self.dragged_divider
if node.type == "hsplit" then if node.type == "hsplit" then
x = common.clamp(x, 0, self.root_node.size.x * 0.95) x = common.clamp(x - node.position.x, 0, self.root_node.size.x * 0.95)
resize_child_node(node, "x", x, dx) resize_child_node(node, "x", x, dx)
elseif node.type == "vsplit" then elseif node.type == "vsplit" then
y = common.clamp(y, 0, self.root_node.size.y * 0.95) y = common.clamp(y - node.position.y, 0, self.root_node.size.y * 0.95)
resize_child_node(node, "y", y, dy) resize_child_node(node, "y", y, dy)
end end
node.divider = common.clamp(node.divider, 0.01, 0.99) node.divider = common.clamp(node.divider, 0.01, 0.99)

View File

@ -58,9 +58,9 @@ function Scrollbar:new(options)
---@type "expanded" | "contracted" | false @Force the scrollbar status ---@type "expanded" | "contracted" | false @Force the scrollbar status
self.force_status = options.force_status self.force_status = options.force_status
self:set_forced_status(options.force_status) self:set_forced_status(options.force_status)
---@type number? @Override the default value specified by `style.expanded_scrollbar_size`
self.contracted_size = options.contracted_size
---@type number? @Override the default value specified by `style.scrollbar_size` ---@type number? @Override the default value specified by `style.scrollbar_size`
self.contracted_size = options.contracted_size
---@type number? @Override the default value specified by `style.expanded_scrollbar_size`
self.expanded_size = options.expanded_size self.expanded_size = options.expanded_size
end end
@ -121,7 +121,7 @@ function Scrollbar:_get_thumb_rect_normal()
across_size = across_size + (expanded_scrollbar_size - scrollbar_size) * self.expand_percent across_size = across_size + (expanded_scrollbar_size - scrollbar_size) * self.expand_percent
return return
nr.across + nr.across_size - across_size, nr.across + nr.across_size - across_size,
nr.along + self.percent * nr.scrollable * (nr.along_size - along_size) / (sz - nr.along_size), nr.along + self.percent * (nr.along_size - along_size),
across_size, across_size,
along_size along_size
end end
@ -189,8 +189,9 @@ function Scrollbar:_on_mouse_pressed_normal(button, x, y, clicks)
self.drag_start_offset = along - y self.drag_start_offset = along - y
return true return true
elseif overlaps == "track" then elseif overlaps == "track" then
local nr = self.normal_rect
self.drag_start_offset = - along_size / 2 self.drag_start_offset = - along_size / 2
return (y - self.normal_rect.along - along_size / 2) / self.normal_rect.along_size return common.clamp((y - nr.along - along_size / 2) / (nr.along_size - along_size), 0, 1)
end end
end end
end end
@ -237,7 +238,8 @@ end
function Scrollbar:_on_mouse_moved_normal(x, y, dx, dy) function Scrollbar:_on_mouse_moved_normal(x, y, dx, dy)
if self.dragging then if self.dragging then
local nr = self.normal_rect local nr = self.normal_rect
return common.clamp((y - nr.along + self.drag_start_offset) / nr.along_size, 0, 1) local _, _, _, along_size = self:_get_thumb_rect_normal()
return common.clamp((y - nr.along + self.drag_start_offset) / (nr.along_size - along_size), 0, 1)
end end
return self:_update_hover_status_normal(x, y) return self:_update_hover_status_normal(x, y)
end end
@ -280,7 +282,7 @@ function Scrollbar:set_size(x, y, w, h, scrollable)
end end
---Updates the scrollbar location ---Updates the scrollbar location
---@param percent number @number between 0 and 1 representing the position of the middle part of the thumb ---@param percent number @number between 0 and 1 where 0 means thumb at the top and 1 at the bottom
function Scrollbar:set_percent(percent) function Scrollbar:set_percent(percent)
self.percent = percent self.percent = percent
end end

View File

@ -1,11 +1,11 @@
-- this file is used by lite-xl to setup the Lua environment when starting -- this file is used by lite-xl to setup the Lua environment when starting
VERSION = "2.1.1r3" VERSION = "2.1.2r1"
MOD_VERSION_MAJOR = 3 MOD_VERSION_MAJOR = 3
MOD_VERSION_MINOR = 0 MOD_VERSION_MINOR = 0
MOD_VERSION_PATCH = 0 MOD_VERSION_PATCH = 0
MOD_VERSION_STRING = string.format("%d.%d.%d", MOD_VERSION_MAJOR, MOD_VERSION_MINOR, MOD_VERSION_PATCH) MOD_VERSION_STRING = string.format("%d.%d.%d", MOD_VERSION_MAJOR, MOD_VERSION_MINOR, MOD_VERSION_PATCH)
SCALE = tonumber(os.getenv("LITE_SCALE") or os.getenv("GDK_SCALE") or os.getenv("QT_SCALE_FACTOR")) or SCALE SCALE = tonumber(os.getenv("LITE_SCALE") or os.getenv("GDK_SCALE") or os.getenv("QT_SCALE_FACTOR")) or 1
PATHSEP = package.config:sub(1, 1) PATHSEP = package.config:sub(1, 1)
EXEDIR = EXEFILE:match("^(.+)[/\\][^/\\]+$") EXEDIR = EXEFILE:match("^(.+)[/\\][^/\\]+$")

View File

@ -232,15 +232,27 @@ function StatusView:register_docview_items()
return { return {
style.text, line, ":", style.text, line, ":",
col > config.line_limit and style.accent or style.text, col, col > config.line_limit and style.accent or style.text, col,
style.text, style.text
self.separator,
string.format("%.f%%", line / #dv.doc.lines * 100)
} }
end, end,
command = "doc:go-to-line", command = "doc:go-to-line",
tooltip = "line : column" tooltip = "line : column"
}) })
self:add_item({
predicate = predicate_docview,
name = "doc:position-percent",
alignment = StatusView.Item.LEFT,
get_item = function()
local dv = core.active_view
local line = dv.doc:get_selection()
return {
string.format("%.f%%", line / #dv.doc.lines * 100)
}
end,
tooltip = "caret position"
})
self:add_item({ self:add_item({
predicate = predicate_docview, predicate = predicate_docview,
name = "doc:selections", name = "doc:selections",
@ -307,6 +319,19 @@ function StatusView:register_docview_items()
end, end,
command = "doc:toggle-line-ending" command = "doc:toggle-line-ending"
}) })
self:add_item {
predicate = predicate_docview,
name = "doc:overwrite-mode",
alignment = StatusView.Item.RIGHT,
get_item = function()
return {
style.text, core.active_view.doc.overwrite and "OVR" or "INS"
}
end,
command = "doc:toggle-overwrite",
separator = StatusView.separator2
}
end end

View File

@ -3,7 +3,7 @@ local common = require "core.common"
local syntax = {} local syntax = {}
syntax.items = {} syntax.items = {}
local plain_text_syntax = { name = "Plain Text", patterns = {}, symbols = {} } syntax.plain_text_syntax = { name = "Plain Text", patterns = {}, symbols = {} }
function syntax.add(t) function syntax.add(t)
@ -46,7 +46,7 @@ end
function syntax.get(filename, header) function syntax.get(filename, header)
return (filename and find(filename, "files")) return (filename and find(filename, "files"))
or (header and find(header, "headers")) or (header and find(header, "headers"))
or plain_text_syntax or syntax.plain_text_syntax
end end

View File

@ -210,9 +210,11 @@ function tokenizer.tokenize(incoming_syntax, text, state, resume)
-- Remove '^' from the beginning of the pattern -- Remove '^' from the beginning of the pattern
if type(target) == "table" then if type(target) == "table" then
target[p_idx] = code:usub(2) target[p_idx] = code:usub(2)
code = target[p_idx]
else else
p.pattern = p.pattern and code:usub(2) p.pattern = p.pattern and code:usub(2)
p.regex = p.regex and code:usub(2) p.regex = p.regex and code:usub(2)
code = p.pattern or p.regex
end end
end end
end end

View File

@ -142,14 +142,14 @@ function View:on_mouse_pressed(button, x, y, clicks)
local result = self.v_scrollbar:on_mouse_pressed(button, x, y, clicks) local result = self.v_scrollbar:on_mouse_pressed(button, x, y, clicks)
if result then if result then
if result ~= true then if result ~= true then
self.scroll.to.y = result * self:get_scrollable_size() self.scroll.to.y = result * (self:get_scrollable_size() - self.size.y)
end end
return true return true
end end
result = self.h_scrollbar:on_mouse_pressed(button, x, y, clicks) result = self.h_scrollbar:on_mouse_pressed(button, x, y, clicks)
if result then if result then
if result ~= true then if result ~= true then
self.scroll.to.x = result * self:get_h_scrollable_size() self.scroll.to.x = result * (self:get_h_scrollable_size() - self.size.x)
end end
return true return true
end end
@ -177,7 +177,7 @@ function View:on_mouse_moved(x, y, dx, dy)
result = self.v_scrollbar:on_mouse_moved(x, y, dx, dy) result = self.v_scrollbar:on_mouse_moved(x, y, dx, dy)
if result then if result then
if result ~= true then if result ~= true then
self.scroll.to.y = result * self:get_scrollable_size() self.scroll.to.y = result * (self:get_scrollable_size() - self.size.y)
if not config.animate_drag_scroll then if not config.animate_drag_scroll then
self:clamp_scroll_position() self:clamp_scroll_position()
self.scroll.y = self.scroll.to.y self.scroll.y = self.scroll.to.y
@ -191,7 +191,7 @@ function View:on_mouse_moved(x, y, dx, dy)
result = self.h_scrollbar:on_mouse_moved(x, y, dx, dy) result = self.h_scrollbar:on_mouse_moved(x, y, dx, dy)
if result then if result then
if result ~= true then if result ~= true then
self.scroll.to.x = result * self:get_h_scrollable_size() self.scroll.to.x = result * (self:get_h_scrollable_size() - self.size.x)
if not config.animate_drag_scroll then if not config.animate_drag_scroll then
self:clamp_scroll_position() self:clamp_scroll_position()
self.scroll.x = self.scroll.to.x self.scroll.x = self.scroll.to.x
@ -287,12 +287,16 @@ end
function View:update_scrollbar() function View:update_scrollbar()
local v_scrollable = self:get_scrollable_size() local v_scrollable = self:get_scrollable_size()
self.v_scrollbar:set_size(self.position.x, self.position.y, self.size.x, self.size.y, v_scrollable) self.v_scrollbar:set_size(self.position.x, self.position.y, self.size.x, self.size.y, v_scrollable)
self.v_scrollbar:set_percent(self.scroll.y/v_scrollable) local v_percent = self.scroll.y/(v_scrollable - self.size.y)
-- Avoid setting nan percent
self.v_scrollbar:set_percent(v_percent == v_percent and v_percent or 0)
self.v_scrollbar:update() self.v_scrollbar:update()
local h_scrollable = self:get_h_scrollable_size() local h_scrollable = self:get_h_scrollable_size()
self.h_scrollbar:set_size(self.position.x, self.position.y, self.size.x, self.size.y, h_scrollable) self.h_scrollbar:set_size(self.position.x, self.position.y, self.size.x, self.size.y, h_scrollable)
self.h_scrollbar:set_percent(self.scroll.x/h_scrollable) local h_percent = self.scroll.x/(h_scrollable - self.size.x)
-- Avoid setting nan percent
self.h_scrollbar:set_percent(h_percent == h_percent and h_percent or 0)
self.h_scrollbar:update() self.h_scrollbar:update()
end end

View File

@ -10,6 +10,10 @@ local RootView = require "core.rootview"
local DocView = require "core.docview" local DocView = require "core.docview"
local Doc = require "core.doc" local Doc = require "core.doc"
---Symbols cache of all open documents
---@type table<core.doc, table>
local cache = setmetatable({}, { __mode = "k" })
config.plugins.autocomplete = common.merge({ config.plugins.autocomplete = common.merge({
-- Amount of characters that need to be written for autocomplete -- Amount of characters that need to be written for autocomplete
min_len = 3, min_len = 3,
@ -19,8 +23,16 @@ config.plugins.autocomplete = common.merge({
max_suggestions = 100, max_suggestions = 100,
-- Maximum amount of symbols to cache per document -- Maximum amount of symbols to cache per document
max_symbols = 4000, max_symbols = 4000,
-- Which symbols to show on the suggestions list: global, local, related, none
suggestions_scope = "global",
-- Font size of the description box -- Font size of the description box
desc_font_size = 12, desc_font_size = 12,
-- Do not show the icons associated to the suggestions
hide_icons = false,
-- Position where icons will be displayed on the suggestions list
icon_position = "left",
-- Do not show the additional information related to a suggestion
hide_info = false,
-- The config specification used by gui generators -- The config specification used by gui generators
config_spec = { config_spec = {
name = "Autocomplete", name = "Autocomplete",
@ -60,6 +72,26 @@ config.plugins.autocomplete = common.merge({
min = 1000, min = 1000,
max = 10000 max = 10000
}, },
{
label = "Suggestions Scope",
description = "Which symbols to show on the suggestions list.",
path = "suggestions_scope",
type = "selection",
default = "global",
values = {
{"All Documents", "global"},
{"Current Document", "local"},
{"Related Documents", "related"},
{"Known Symbols", "none"}
},
on_apply = function(value)
if value == "global" then
for _, doc in ipairs(core.docs) do
if cache[doc] then cache[doc] = nil end
end
end
end
},
{ {
label = "Description Font Size", label = "Description Font Size",
description = "Font size of the description box.", description = "Font size of the description box.",
@ -67,6 +99,31 @@ config.plugins.autocomplete = common.merge({
type = "number", type = "number",
default = 12, default = 12,
min = 8 min = 8
},
{
label = "Hide Icons",
description = "Do not show icons on the suggestions list.",
path = "hide_icons",
type = "toggle",
default = false
},
{
label = "Icons Position",
description = "Position to display icons on the suggestions list.",
path = "icon_position",
type = "selection",
default = "left",
values = {
{"Left", "left"},
{"Right", "Right"}
}
},
{
label = "Hide Items Info",
description = "Do not show the additional info related to each suggestion.",
path = "hide_info",
type = "toggle",
default = false
} }
} }
}, config.plugins.autocomplete) }, config.plugins.autocomplete)
@ -76,6 +133,7 @@ local autocomplete = {}
autocomplete.map = {} autocomplete.map = {}
autocomplete.map_manually = {} autocomplete.map_manually = {}
autocomplete.on_close = nil autocomplete.on_close = nil
autocomplete.icons = {}
-- Flag that indicates if the autocomplete box was manually triggered -- Flag that indicates if the autocomplete box was manually triggered
-- with the autocomplete.complete() function to prevent the suggestions -- with the autocomplete.complete() function to prevent the suggestions
@ -95,6 +153,7 @@ function autocomplete.add(t, manually_triggered)
{ {
text = text, text = text,
info = info.info, info = info.info,
icon = info.icon, -- Name of icon to show
desc = info.desc, -- Description shown on item selected desc = info.desc, -- Description shown on item selected
onhover = info.onhover, -- A callback called once when item is hovered onhover = info.onhover, -- A callback called once when item is hovered
onselect = info.onselect, -- A callback called when item is selected onselect = info.onselect, -- A callback called when item is selected
@ -119,28 +178,35 @@ end
-- --
-- Thread that scans open document symbols and cache them -- Thread that scans open document symbols and cache them
-- --
local max_symbols = config.plugins.autocomplete.max_symbols local global_symbols = {}
core.add_thread(function() core.add_thread(function()
local cache = setmetatable({}, { __mode = "k" }) local function load_syntax_symbols(doc)
if doc.syntax and not autocomplete.map["language_"..doc.syntax.name] then
local function get_syntax_symbols(symbols, doc) local symbols = {
if doc.syntax then name = "language_"..doc.syntax.name,
for sym in pairs(doc.syntax.symbols) do files = doc.syntax.files,
symbols[sym] = true items = {}
}
for name, type in pairs(doc.syntax.symbols) do
symbols.items[name] = type
end end
autocomplete.add(symbols)
return symbols.items
end end
return {}
end end
local function get_symbols(doc) local function get_symbols(doc)
local s = {} local s = {}
get_syntax_symbols(s, doc) local syntax_symbols = load_syntax_symbols(doc)
local max_symbols = config.plugins.autocomplete.max_symbols
if doc.disable_symbols then return s end if doc.disable_symbols then return s end
local i = 1 local i = 1
local symbols_count = 0 local symbols_count = 0
while i <= #doc.lines do while i <= #doc.lines do
for sym in doc.lines[i]:gmatch(config.symbol_pattern) do for sym in doc.lines[i]:gmatch(config.symbol_pattern) do
if not s[sym] then if not s[sym] and not syntax_symbols[sym] then
symbols_count = symbols_count + 1 symbols_count = symbols_count + 1
if symbols_count > max_symbols then if symbols_count > max_symbols then
s = nil s = nil
@ -186,14 +252,18 @@ core.add_thread(function()
} }
end end
-- update symbol set with doc's symbol set -- update symbol set with doc's symbol set
for sym in pairs(cache[doc].symbols) do if config.plugins.autocomplete.suggestions_scope == "global" then
symbols[sym] = true for sym in pairs(cache[doc].symbols) do
symbols[sym] = true
end
end end
coroutine.yield() coroutine.yield()
end end
-- update symbols list -- update global symbols list
autocomplete.add { name = "open-docs", items = symbols } if config.plugins.autocomplete.suggestions_scope == "global" then
global_symbols = symbols
end
-- wait for next scan -- wait for next scan
local valid = true local valid = true
@ -240,12 +310,50 @@ local function update_suggestions()
map = autocomplete.map_manually map = autocomplete.map_manually
end end
local assigned_sym = {}
-- get all relevant suggestions for given filename -- get all relevant suggestions for given filename
local items = {} local items = {}
for _, v in pairs(map) do for _, v in pairs(map) do
if common.match_pattern(filename, v.files) then if common.match_pattern(filename, v.files) then
for _, item in pairs(v.items) do for _, item in pairs(v.items) do
table.insert(items, item) table.insert(items, item)
assigned_sym[item.text] = true
end
end
end
-- Append the global, local or related text symbols if applicable
local scope = config.plugins.autocomplete.suggestions_scope
if not triggered_manually then
local text_symbols = nil
if scope == "global" then
text_symbols = global_symbols
elseif scope == "local" and cache[doc] and cache[doc].symbols then
text_symbols = cache[doc].symbols
elseif scope == "related" then
for _, d in ipairs(core.docs) do
if doc.syntax == d.syntax then
if cache[d].symbols then
for name in pairs(cache[d].symbols) do
if not assigned_sym[name] then
table.insert(items, setmetatable(
{text = name, info = "normal"}, mt
))
end
end
end
end
end
end
if text_symbols then
for name in pairs(text_symbols) do
if not assigned_sym[name] then
table.insert(items, setmetatable({text = name, info = "normal"}, mt))
end
end end
end end
end end
@ -286,13 +394,23 @@ local function get_suggestions_rect(av)
y = y + av:get_line_height() + style.padding.y y = y + av:get_line_height() + style.padding.y
local font = av:get_font() local font = av:get_font()
local th = font:get_height() local th = font:get_height()
local has_icons = false
local hide_info = config.plugins.autocomplete.hide_info
local hide_icons = config.plugins.autocomplete.hide_icons
local max_width = 0 local max_width = 0
for _, s in ipairs(suggestions) do for _, s in ipairs(suggestions) do
local w = font:get_width(s.text) local w = font:get_width(s.text)
if s.info then if s.info and not hide_info then
w = w + style.font:get_width(s.info) + style.padding.x w = w + style.font:get_width(s.info) + style.padding.x
end end
local icon = s.icon or s.info
if not hide_icons and icon and autocomplete.icons[icon] then
w = w + autocomplete.icons[icon].font:get_width(
autocomplete.icons[icon].char
) + (style.padding.x / 2)
has_icons = true
end
max_width = math.max(max_width, w) max_width = math.max(max_width, w)
end end
@ -319,7 +437,8 @@ local function get_suggestions_rect(av)
x - style.padding.x, x - style.padding.x,
y - style.padding.y, y - style.padding.y,
max_width + style.padding.x * 2, max_width + style.padding.x * 2,
max_items * (th + style.padding.y) + style.padding.y max_items * (th + style.padding.y) + style.padding.y,
has_icons
end end
local function wrap_line(line, max_chars) local function wrap_line(line, max_chars)
@ -439,7 +558,7 @@ local function draw_suggestions_box(av)
local ah = config.plugins.autocomplete.max_height local ah = config.plugins.autocomplete.max_height
-- draw background rect -- draw background rect
local rx, ry, rw, rh = get_suggestions_rect(av) local rx, ry, rw, rh, has_icons = get_suggestions_rect(av)
renderer.draw_rect(rx, ry, rw, rh, style.background3) renderer.draw_rect(rx, ry, rw, rh, style.background3)
-- draw text -- draw text
@ -448,17 +567,52 @@ local function draw_suggestions_box(av)
local y = ry + style.padding.y / 2 local y = ry + style.padding.y / 2
local show_count = #suggestions <= ah and #suggestions or ah local show_count = #suggestions <= ah and #suggestions or ah
local start_index = suggestions_idx > ah and (suggestions_idx-(ah-1)) or 1 local start_index = suggestions_idx > ah and (suggestions_idx-(ah-1)) or 1
local hide_info = config.plugins.autocomplete.hide_info
for i=start_index, start_index+show_count-1, 1 do for i=start_index, start_index+show_count-1, 1 do
if not suggestions[i] then if not suggestions[i] then
break break
end end
local s = suggestions[i] local s = suggestions[i]
local icon_l_padding, icon_r_padding = 0, 0
if has_icons then
local icon = s.icon or s.info
if icon and autocomplete.icons[icon] then
local ifont = autocomplete.icons[icon].font
local itext = autocomplete.icons[icon].char
local icolor = autocomplete.icons[icon].color
if i == suggestions_idx then
icolor = style.accent
elseif type(icolor) == "string" then
icolor = style.syntax[icolor]
end
if config.plugins.autocomplete.icon_position == "left" then
common.draw_text(
ifont, icolor, itext, "left", rx + style.padding.x, y, rw, lh
)
icon_l_padding = ifont:get_width(itext) + (style.padding.x / 2)
else
common.draw_text(
ifont, icolor, itext, "right", rx, y, rw - style.padding.x, lh
)
icon_r_padding = ifont:get_width(itext) + (style.padding.x / 2)
end
end
end
local color = (i == suggestions_idx) and style.accent or style.text local color = (i == suggestions_idx) and style.accent or style.text
common.draw_text(font, color, s.text, "left", rx + style.padding.x, y, rw, lh) common.draw_text(
if s.info then font, color, s.text, "left",
rx + icon_l_padding + style.padding.x, y, rw, lh
)
if s.info and not hide_info then
color = (i == suggestions_idx) and style.text or style.dim color = (i == suggestions_idx) and style.text or style.dim
common.draw_text(style.font, color, s.info, "right", rx, y, rw - style.padding.x, lh) common.draw_text(
style.font, color, s.info, "right",
rx, y, rw - icon_r_padding - style.padding.x, lh
)
end end
y = y + lh y = y + lh
if suggestions_idx == i then if suggestions_idx == i then
@ -619,6 +773,31 @@ function autocomplete.can_complete()
return false return false
end end
---Register a font icon that can be assigned to completion items.
---@param name string
---@param character string
---@param font? renderer.font
---@param color? string | renderer.color A style.syntax[] name or specific color
function autocomplete.add_icon(name, character, font, color)
local color_type = type(color)
assert(
not color or color_type == "table"
or (color_type == "string" and style.syntax[color]),
"invalid icon color given"
)
autocomplete.icons[name] = {
char = character,
font = font or style.code_font,
color = color or "keyword"
}
end
--
-- Register built-in syntax symbol types icon
--
for name, _ in pairs(style.syntax) do
autocomplete.add_icon(name, "M", style.icon_font, name)
end
-- --
-- Commands -- Commands
@ -632,7 +811,6 @@ command.add(predicate, {
["autocomplete:complete"] = function(dv) ["autocomplete:complete"] = function(dv)
local doc = dv.doc local doc = dv.doc
local item = suggestions[suggestions_idx] local item = suggestions[suggestions_idx]
local text = item.text
local inserted = false local inserted = false
if item.onselect then if item.onselect then
inserted = item.onselect(suggestions_idx, item) inserted = item.onselect(suggestions_idx, item)

View File

@ -266,7 +266,7 @@ local function detect_indent_stat(doc)
local max_lines = auto_detect_max_lines local max_lines = auto_detect_max_lines
for i, text in get_non_empty_lines(doc.syntax, doc.lines) do for i, text in get_non_empty_lines(doc.syntax, doc.lines) do
local spaces = text:match("^ +") local spaces = text:match("^ +")
if spaces then table.insert(stat, spaces:len()) end if spaces and #spaces > 1 then table.insert(stat, #spaces) end
local tabs = text:match("^\t+") local tabs = text:match("^\t+")
if tabs then tab_count = tab_count + 1 end if tabs then tab_count = tab_count + 1 end
-- if nothing found for first lines try at least 4 more times -- if nothing found for first lines try at least 4 more times

View File

@ -347,7 +347,7 @@ end
command.add(nil, { command.add(nil, {
["draw-whitespace:toggle"] = function() ["draw-whitespace:toggle"] = function()
config.plugins.drawwhitespace.enabled = not config.drawwhitespace.enabled config.plugins.drawwhitespace.enabled = not config.plugins.drawwhitespace.enabled
end, end,
["draw-whitespace:disable"] = function() ["draw-whitespace:disable"] = function()

View File

@ -14,9 +14,9 @@ syntax.add {
{ pattern = { "/%*", "%*/" }, type = "comment" }, { pattern = { "/%*", "%*/" }, type = "comment" },
{ pattern = { '"', '"', '\\' }, type = "string" }, { pattern = { '"', '"', '\\' }, type = "string" },
{ pattern = { "'", "'", '\\' }, type = "string" }, { pattern = { "'", "'", '\\' }, type = "string" },
{ pattern = "0x%x+", type = "number" }, { pattern = "0x%x+[%x']*", type = "number" },
{ pattern = "%d+[%d%.'eE]*f?", type = "number" }, { pattern = "%d+[%d%.'eE]*f?", type = "number" },
{ pattern = "%.?%d+f?", type = "number" }, { pattern = "%.?%d+[%d']*f?", type = "number" },
{ pattern = "[%+%-=/%*%^%%<>!~|:&]", type = "operator" }, { pattern = "[%+%-=/%*%^%%<>!~|:&]", type = "operator" },
{ pattern = "##", type = "operator" }, { pattern = "##", type = "operator" },
{ pattern = "struct%s()[%a_][%w_]*", type = {"keyword", "keyword2"} }, { pattern = "struct%s()[%a_][%w_]*", type = {"keyword", "keyword2"} },

View File

@ -1,24 +1,74 @@
-- mod-version:3 -- mod-version:3
local syntax = require "core.syntax" local syntax = require "core.syntax"
-- Regex pattern explanation:
-- This will match / and will look ahead for something that looks like a regex.
--
-- (?!/) Don't match empty regexes.
--
-- (?>...) this is using an atomic group to minimize backtracking, as that'd
-- cause "Catastrophic Backtracking" in some cases.
--
-- [^\\[\/]++ will match anything that's isn't an escape, a start of character
-- class or an end of pattern, without backtracking (the second +).
--
-- \\. will match anything that's escaped.
--
-- \[(?:[^\\\]++]|\\.)*+\] will match character classes.
--
-- /[gmiyuvsd]*\s*[\n,;\)\]\}\.]) will match the end of pattern delimiter, optionally
-- followed by pattern options, and anything that can
-- be after a pattern.
--
-- Demo with some unit tests (click on the Unit Tests entry): https://regex101.com/r/R0w8Qw/1
-- Note that it has a couple of changes to make it work on that platform.
local regex_pattern = {
[=[/(?=(?!/)(?:(?>[^\\[\/]++|\\.|\[(?:[^\\\]]++|\\.)*+\])*+)++/[gmiyuvsd]*\s*[\n,;\)\]\}\.])()]=],
"/()[gmiyuvsd]*", "\\"
}
-- For the moment let's not actually differentiate the insides of the regex,
-- as this will need new token types...
local inner_regex_syntax = {
patterns = {
{ pattern = "%(()%?[:!=><]", type = { "string", "string" } },
{ pattern = "[.?+*%(%)|]", type = "string" },
{ pattern = "{%d*,?%d*}", type = "string" },
{ regex = { [=[\[()\^?]=], [=[(?:\]|(?=\n))()]=], "\\" },
type = { "string", "string" },
syntax = { -- Inside character class
patterns = {
{ pattern = "\\\\", type = "string" },
{ pattern = "\\%]", type = "string" },
{ pattern = "[^%]\n]", type = "string" }
},
symbols = {}
}
},
{ regex = "\\/", type = "string" },
{ regex = "[^/\n]", type = "string" },
},
symbols = {}
}
syntax.add { syntax.add {
name = "JavaScript", name = "JavaScript",
files = { "%.js$", "%.json$", "%.cson$", "%.mjs$", "%.cjs$" }, files = { "%.js$", "%.json$", "%.cson$", "%.mjs$", "%.cjs$" },
comment = "//", comment = "//",
block_comment = { "/*", "*/" }, block_comment = { "/*", "*/" },
patterns = { patterns = {
{ pattern = "//.*", type = "comment" }, { pattern = "//.*", type = "comment" },
{ pattern = { "/%*", "%*/" }, type = "comment" }, { pattern = { "/%*", "%*/" }, type = "comment" },
{ pattern = { '/[^= ]', '/', '\\' },type = "string" }, { regex = regex_pattern, syntax = inner_regex_syntax, type = {"string", "string"} },
{ pattern = { '"', '"', '\\' }, type = "string" }, { pattern = { '"', '"', '\\' }, type = "string" },
{ pattern = { "'", "'", '\\' }, type = "string" }, { pattern = { "'", "'", '\\' }, type = "string" },
{ pattern = { "`", "`", '\\' }, type = "string" }, { pattern = { "`", "`", '\\' }, type = "string" },
{ pattern = "0x[%da-fA-F_]+n?", type = "number" }, { pattern = "0x[%da-fA-F_]+n?()%s*()/?", type = {"number", "normal", "operator"} },
{ pattern = "-?%d+[%d%.eE_n]*", type = "number" }, { pattern = "-?%d+[%d%.eE_n]*()%s*()/?", type = {"number", "normal", "operator"} },
{ pattern = "-?%.?%d+", type = "number" }, { pattern = "-?%.?%d+()%s*()/?", type = {"number", "normal", "operator"} },
{ pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" }, { pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" },
{ pattern = "[%a_][%w_]*%f[(]", type = "function" }, { pattern = "[%a_][%w_]*%f[(]", type = "function" },
{ pattern = "[%a_][%w_]*", type = "symbol" }, { pattern = "[%a_][%w_]*()%s*()/?", type = {"symbol", "normal", "operator"} },
}, },
symbols = { symbols = {
["async"] = "keyword", ["async"] = "keyword",

View File

@ -3,25 +3,6 @@ local syntax = require "core.syntax"
local style = require "core.style" local style = require "core.style"
local core = require "core" local core = require "core"
local initial_color = style.syntax["keyword2"]
-- Add 3 type of font styles for use on markdown files
for _, attr in pairs({"bold", "italic", "bold_italic"}) do
local attributes = {}
if attr ~= "bold_italic" then
attributes[attr] = true
else
attributes["bold"] = true
attributes["italic"] = true
end
style.syntax_fonts["markdown_"..attr] = style.code_font:copy(
style.code_font:get_size(),
attributes
)
-- also add a color for it
style.syntax["markdown_"..attr] = style.syntax["keyword2"]
end
local in_squares_match = "^%[%]" local in_squares_match = "^%[%]"
local in_parenthesis_match = "^%(%)" local in_parenthesis_match = "^%(%)"
@ -225,12 +206,63 @@ syntax.add {
-- Adjust the color on theme changes -- Adjust the color on theme changes
core.add_thread(function() core.add_thread(function()
local custom_fonts = { bold = {font = nil, color = nil}, italic = {}, bold_italic = {} }
local initial_color
local last_code_font
local function set_font(attr)
local attributes = {}
if attr ~= "bold_italic" then
attributes[attr] = true
else
attributes["bold"] = true
attributes["italic"] = true
end
local font = style.code_font:copy(
style.code_font:get_size(),
attributes
)
custom_fonts[attr].font = font
style.syntax_fonts["markdown_"..attr] = font
end
local function set_color(attr)
custom_fonts[attr].color = style.syntax["keyword2"]
style.syntax["markdown_"..attr] = style.syntax["keyword2"]
end
-- Add 3 type of font styles for use on markdown files
for attr, _ in pairs(custom_fonts) do
-- Only set it if the font wasn't manually customized
if not style.syntax_fonts["markdown_"..attr] then
set_font(attr)
end
-- Only set it if the color wasn't manually customized
if not style.syntax["markdown_"..attr] then
set_color(attr)
end
end
while true do while true do
if initial_color ~= style.syntax["keyword2"] then if last_code_font ~= style.code_font then
for _, attr in pairs({"bold", "italic", "bold_italic"}) do last_code_font = style.code_font
style.syntax["markdown_"..attr] = style.syntax["keyword2"] for attr, _ in pairs(custom_fonts) do
-- Only set it if the font wasn't manually customized
if style.syntax_fonts["markdown_"..attr] == custom_fonts[attr].font then
set_font(attr)
end
end end
end
if initial_color ~= style.syntax["keyword2"] then
initial_color = style.syntax["keyword2"] initial_color = style.syntax["keyword2"]
for attr, _ in pairs(custom_fonts) do
-- Only set it if the color wasn't manually customized
if style.syntax["markdown_"..attr] == custom_fonts[attr].color then
set_color(attr)
end
end
end end
coroutine.yield(1) coroutine.yield(1)
end end

View File

@ -219,7 +219,7 @@ function LineWrapping.draw_guide(docview)
end end
function LineWrapping.update_docview_breaks(docview) function LineWrapping.update_docview_breaks(docview)
local x,y,w,h = docview.v_scrollbar:get_thumb_rect() local w = docview.v_scrollbar.expanded_size or style.expanded_scrollbar_size
local width = (type(config.plugins.linewrapping.width_override) == "function" and config.plugins.linewrapping.width_override(docview)) local width = (type(config.plugins.linewrapping.width_override) == "function" and config.plugins.linewrapping.width_override(docview))
or config.plugins.linewrapping.width_override or (docview.size.x - docview:get_gutter_width() - w) or config.plugins.linewrapping.width_override or (docview.size.x - docview:get_gutter_width() - w)
if (not docview.wrapped_settings or docview.wrapped_settings.width == nil or width ~= docview.wrapped_settings.width) then if (not docview.wrapped_settings or docview.wrapped_settings.width == nil or width ~= docview.wrapped_settings.width) then

View File

@ -29,7 +29,7 @@ local tooltip_alpha_rate = 1
local function get_depth(filename) local function get_depth(filename)
local n = 1 local n = 1
for sep in filename:gmatch("[\\/]") do for _ in filename:gmatch(PATHSEP) do
n = n + 1 n = n + 1
end end
return n return n

View File

@ -83,7 +83,8 @@ local function save_view(view)
filename = view.doc.filename, filename = view.doc.filename,
selection = { view.doc:get_selection() }, selection = { view.doc:get_selection() },
scroll = { x = view.scroll.to.x, y = view.scroll.to.y }, scroll = { x = view.scroll.to.x, y = view.scroll.to.y },
text = not view.doc.filename and view.doc:get_text(1, 1, math.huge, math.huge) crlf = view.doc.crlf,
text = view.doc.new_file and view.doc:get_text(1, 1, math.huge, math.huge)
} }
end end
if mt == LogView then return end if mt == LogView then return end
@ -106,7 +107,6 @@ local function load_view(t)
if not t.filename then if not t.filename then
-- document not associated to a file -- document not associated to a file
dv = DocView(core.open_doc()) dv = DocView(core.open_doc())
if t.text then dv.doc:insert(1, 1, t.text) end
else else
-- we have a filename, try to read the file -- we have a filename, try to read the file
local ok, doc = pcall(core.open_doc, t.filename) local ok, doc = pcall(core.open_doc, t.filename)
@ -114,9 +114,11 @@ local function load_view(t)
dv = DocView(doc) dv = DocView(doc)
end end
end end
-- doc view "dv" can be nil here if the filename associated to the document
-- cannot be read.
if dv and dv.doc then if dv and dv.doc then
if dv.doc.new_file and t.text then
dv.doc:insert(1, 1, t.text)
dv.doc.crlf = t.crlf
end
dv.doc:set_selection(table.unpack(t.selection)) dv.doc:set_selection(table.unpack(t.selection))
dv.last_line1, dv.last_col1, dv.last_line2, dv.last_col2 = dv.doc:get_selection() dv.last_line1, dv.last_col1, dv.last_line2, dv.last_col2 = dv.doc:get_selection()
dv.scroll.x, dv.scroll.to.x = t.scroll.x, t.scroll.x dv.scroll.x, dv.scroll.to.x = t.scroll.x, t.scroll.x

View File

@ -45,10 +45,14 @@ function dirmonitor:unwatch(fd_or_path) end
---edited, removed or added. A file descriptor will be passed to the ---edited, removed or added. A file descriptor will be passed to the
---callback in "multiple" mode or a path in "single" mode. ---callback in "multiple" mode or a path in "single" mode.
--- ---
---If an error occurred during the callback execution, the error callback will be called with the error object.
---This callback should not manipulate coroutines to avoid deadlocks.
---
---@param callback dirmonitor.callback ---@param callback dirmonitor.callback
---@param error_callback fun(error: any): nil
--- ---
---@return boolean? changes True when changes were detected. ---@return boolean? changes True when changes were detected.
function dirmonitor:check(callback) end function dirmonitor:check(callback, error_callback) end
--- ---
---Get the working mode for the current file system monitoring backend. ---Get the working mode for the current file system monitoring backend.

View File

@ -61,10 +61,10 @@ function system.poll_event() end
--- ---
---Wait until an event is triggered. ---Wait until an event is triggered.
--- ---
---@param timeout number Amount of seconds, also supports fractions ---@param timeout? number Amount of seconds, also supports fractions
---of a second, eg: 0.01 ---of a second, eg: 0.01. If not provided, waits forever.
--- ---
---@return boolean status True on success or false if there was an error. ---@return boolean status True on success or false if there was an error or if no event was received.
function system.wait_event(timeout) end function system.wait_event(timeout) end
--- ---

View File

@ -4,8 +4,7 @@ project('lite-xl',
license : 'MIT', license : 'MIT',
meson_version : '>= 0.56', meson_version : '>= 0.56',
default_options : [ default_options : [
'c_std=gnu11', 'c_std=gnu11'
'wrap_mode=nofallback'
] ]
) )
@ -84,23 +83,27 @@ if not get_option('source-only')
'lua', # Fedora 'lua', # Fedora
] ]
foreach lua : lua_names if get_option('use_system_lua')
last_lua = (lua == lua_names[-1] or get_option('wrap_mode') == 'forcefallback') foreach lua : lua_names
lua_dep = dependency(lua, fallback: last_lua ? ['lua', 'lua_dep'] : [], required : false, last_lua = (lua == lua_names[-1] or get_option('wrap_mode') == 'forcefallback')
version: '>= 5.4', lua_dep = dependency(lua, required : false,
default_options: default_fallback_options + ['default_library=static', 'line_editing=false', 'interpreter=false'] )
) if lua_dep.found()
if lua_dep.found() break
break endif
endif
if last_lua if last_lua
# If we could not find lua on the system and fallbacks are disabled # If we could not find lua on the system and fallbacks are disabled
# try the compiler as a last ditch effort, since Lua has no official # try the compiler as a last ditch effort, since Lua has no official
# pkg-config support. # pkg-config support.
lua_dep = cc.find_library('lua', required : true) lua_dep = cc.find_library('lua', required : true)
endif endif
endforeach endforeach
else
lua_dep = dependency('', fallback: ['lua', 'lua_dep'], required : true,
default_options: default_fallback_options + ['default_library=static', 'line_editing=disabled', 'interpreter=false']
)
endif
pcre2_dep = dependency('libpcre2-8', fallback: ['pcre2', 'libpcre2_8'], pcre2_dep = dependency('libpcre2-8', fallback: ['pcre2', 'libpcre2_8'],
default_options: default_fallback_options + ['default_library=static', 'grep=false', 'test=false'] default_options: default_fallback_options + ['default_library=static', 'grep=false', 'test=false']
@ -120,6 +123,7 @@ if not get_option('source-only')
sdl_options += 'use_atomic=enabled' sdl_options += 'use_atomic=enabled'
sdl_options += 'use_threads=enabled' sdl_options += 'use_threads=enabled'
sdl_options += 'use_timers=enabled' sdl_options += 'use_timers=enabled'
sdl_options += 'with_main=true'
# investigate if this is truly needed # investigate if this is truly needed
# Do not remove before https://github.com/libsdl-org/SDL/issues/5413 is released # Do not remove before https://github.com/libsdl-org/SDL/issues/5413 is released
sdl_options += 'use_events=enabled' sdl_options += 'use_events=enabled'
@ -152,12 +156,24 @@ if not get_option('source-only')
sdl_options += 'use_video_vulkan=disabled' sdl_options += 'use_video_vulkan=disabled'
sdl_options += 'use_video_offscreen=disabled' sdl_options += 'use_video_offscreen=disabled'
sdl_options += 'use_power=disabled' sdl_options += 'use_power=disabled'
sdl_options += 'system_iconv=disabled'
sdl_dep = dependency('sdl2', fallback: ['sdl2', 'sdl2_dep'], sdl_dep = dependency('sdl2', fallback: ['sdl2', 'sdl2_dep'],
default_options: default_fallback_options + sdl_options default_options: default_fallback_options + sdl_options
) )
lite_deps = [lua_dep, sdl_dep, freetype_dep, pcre2_dep, libm, libdl] if host_machine.system() == 'windows'
if sdl_dep.type_name() == 'internal'
sdlmain_dep = dependency('sdl2main', fallback: ['sdl2main_dep'])
else
sdlmain_dep = cc.find_library('SDL2main')
endif
else
sdlmain_dep = dependency('', required: false)
assert(not sdlmain_dep.found(), 'checking if fake dependency has been found')
endif
lite_deps = [lua_dep, sdl_dep, sdlmain_dep, freetype_dep, pcre2_dep, libm, libdl]
endif endif
#=============================================================================== #===============================================================================
# Install Configuration # Install Configuration

View File

@ -3,4 +3,5 @@ option('source-only', type : 'boolean', value : false, description: 'Configure s
option('portable', type : 'boolean', value : false, description: 'Portable install') option('portable', type : 'boolean', value : false, description: 'Portable install')
option('renderer', type : 'boolean', value : false, description: 'Use SDL renderer') option('renderer', type : 'boolean', value : false, description: 'Use SDL renderer')
option('dirmonitor_backend', type : 'combo', value : '', choices : ['', 'inotify', 'fsevents', 'kqueue', 'win32', 'dummy'], description: 'define what dirmonitor backend to use') option('dirmonitor_backend', type : 'combo', value : '', choices : ['', 'inotify', 'fsevents', 'kqueue', 'win32', 'dummy'], description: 'define what dirmonitor backend to use')
option('arch_tuple', type : 'string', value : '', description: 'Specify a custom architecture tuple') option('arch_tuple', type : 'string', value : '', description: 'Specify a custom architecture tuple')
option('use_system_lua', type : 'boolean', value : false, description: 'Prefer System Lua over a the meson wrap')

View File

@ -11,8 +11,9 @@ This folder contains resources that is used for building or packaging the projec
- `icons/icon.{icns,ico,inl,rc,svg}`: lite-xl icon in various formats. - `icons/icon.{icns,ico,inl,rc,svg}`: lite-xl icon in various formats.
- `linux/com.lite_xl.LiteXL.appdata.xml`: AppStream metadata. - `linux/com.lite_xl.LiteXL.appdata.xml`: AppStream metadata.
- `linux/com.lite_xl.LiteXL.desktop`: Desktop file for Linux desktops. - `linux/com.lite_xl.LiteXL.desktop`: Desktop file for Linux desktops.
- `macos/appdmg.png`: Background image for packaging MacOS DMGs. - `macos/dmg-cover.png`: Background image for packaging macOS DMGs.
- `macos/Info.plist.in`: Template for generating `info.plist` on MacOS. See `macos/macos-retina-display.md` for details. - `macos/Info.plist.in`: Template for generating `info.plist` on macOS. See `macos/macos-retina-display.md` for details.
- `macos/lite-xl-dmg.py`: Configuration options for dmgbuild for packaging macOS DMGs.
- `windows/001-lua-unicode.diff`: Patch for allowing Lua to load files with UTF-8 filenames on Windows. - `windows/001-lua-unicode.diff`: Patch for allowing Lua to load files with UTF-8 filenames on Windows.
### Development ### Development

View File

@ -2,13 +2,13 @@
* lite_xl_plugin_api.h * lite_xl_plugin_api.h
* API for writing C extension modules loaded by Lite XL. * API for writing C extension modules loaded by Lite XL.
* This file is licensed under MIT. * This file is licensed under MIT.
* *
* The Lite XL plugin API is quite simple. * The Lite XL plugin API is quite simple.
* You would write a lua C extension and replace any references to lua.h, lauxlib.h * You would write a lua C extension and replace any references to lua.h, lauxlib.h
* and lualib.h with lite_xl_plugin_api.h. * and lualib.h with lite_xl_plugin_api.h.
* In your main file (where your entrypoint resides), define LITE_XL_PLUGIN_ENTRYPOINT. * In your main file (where your entrypoint resides), define LITE_XL_PLUGIN_ENTRYPOINT.
* If you have multiple entrypoints, define LITE_XL_PLUGIN_ENTRYPOINT in one of them. * If you have multiple entrypoints, define LITE_XL_PLUGIN_ENTRYPOINT in one of them.
* *
* After that, you need to create a Lite XL entrypoint, which is formatted as * After that, you need to create a Lite XL entrypoint, which is formatted as
* luaopen_lite_xl_xxxxx instead of luaopen_xxxxx. * luaopen_lite_xl_xxxxx instead of luaopen_xxxxx.
* This entrypoint accepts a lua_State and an extra parameter of type void *. * This entrypoint accepts a lua_State and an extra parameter of type void *.
@ -16,9 +16,9 @@
* If you have multiple entrypoints, you must call lite_xl_plugin_init() in * If you have multiple entrypoints, you must call lite_xl_plugin_init() in
* each of them. * each of them.
* This function is not thread safe, so don't try to do anything stupid. * This function is not thread safe, so don't try to do anything stupid.
* *
* An example: * An example:
* *
* #define LITE_XL_PLUGIN_ENTRYPOINT * #define LITE_XL_PLUGIN_ENTRYPOINT
* #include "lite_xl_plugin_api.h" * #include "lite_xl_plugin_api.h"
* int luaopen_lite_xl_xxxxx(lua_State* L, void* XL) { * int luaopen_lite_xl_xxxxx(lua_State* L, void* XL) {
@ -26,14 +26,19 @@
* ... * ...
* return 1; * return 1;
* } * }
* *
* You can compile the library just like any Lua library without linking to Lua. * You can compile the library just like any Lua library without linking to Lua.
* An example command would be: gcc -shared -o xxxxx.so xxxxx.c * An example command would be: gcc -shared -o xxxxx.so xxxxx.c
* You must not link to ANY lua library to avoid symbol collision. * You must not link to ANY lua library to avoid symbol collision.
* *
* This file contains stock configuration for a typical installation of Lua 5.4. * This file contains stock configuration for a typical installation of Lua 5.4.6.
* DO NOT MODIFY ANYTHING. MODIFYING STUFFS IN HERE WILL BREAK * DO NOT MODIFY ANYTHING. MODIFYING STUFFS IN HERE WILL BREAK
* COMPATIBILITY WITH LITE XL AND CAUSE UNDEBUGGABLE BUGS. * COMPATIBILITY WITH LITE XL AND CAUSE UNDEBUGGABLE BUGS.
*
* For reference, here are a list of permalinks to previous version of this file that targets an older version of Lua.
* If you don't need functionalities offered by the new version, use the OLDEST FILE for backwards compatibility.
*
* - Lua 5.4.4: https://github.com/lite-xl/lite-xl/blob/397973067f14420b26e3b20a238a50016c0b75e2/resources/include/lite_xl_plugin_api.h
**/ **/
#ifndef LITE_XL_PLUGIN_API #ifndef LITE_XL_PLUGIN_API
#define LITE_XL_PLUGIN_API #define LITE_XL_PLUGIN_API
@ -60,8 +65,8 @@
#define FE_7(what, x, ...) what x,FE_6(what, __VA_ARGS__) #define FE_7(what, x, ...) what x,FE_6(what, __VA_ARGS__)
#define FE_8(what, x, ...) what x,FE_7(what, __VA_ARGS__) #define FE_8(what, x, ...) what x,FE_7(what, __VA_ARGS__)
#define FOR_EACH_NARG(...) FOR_EACH_NARG_(__VA_ARGS__, FOR_EACH_RSEQ_N()) #define FOR_EACH_NARG(...) FOR_EACH_NARG_(__VA_ARGS__, FOR_EACH_RSEQ_N())
#define FOR_EACH_NARG_(...) FOR_EACH_ARG_N(__VA_ARGS__) #define FOR_EACH_NARG_(...) FOR_EACH_ARG_N(__VA_ARGS__)
#define FOR_EACH_ARG_N(_1, _2, _3, _4, _5, _6, _7, _8, N, ...) N #define FOR_EACH_ARG_N(_1, _2, _3, _4, _5, _6, _7, _8, N, ...) N
#define FOR_EACH_RSEQ_N() 8, 7, 6, 5, 4, 3, 2, 1, 0 #define FOR_EACH_RSEQ_N() 8, 7, 6, 5, 4, 3, 2, 1, 0
#define FOR_EACH_(N, what, ...) CONCAT(FE_, N)(what, __VA_ARGS__) #define FOR_EACH_(N, what, ...) CONCAT(FE_, N)(what, __VA_ARGS__)
#define FOR_EACH(what, ...) FOR_EACH_(FOR_EACH_NARG(__VA_ARGS__), what, __VA_ARGS__) #define FOR_EACH(what, ...) FOR_EACH_(FOR_EACH_NARG(__VA_ARGS__), what, __VA_ARGS__)
@ -1028,6 +1033,7 @@ extern const char lua_ident[];
SYMBOL_DECLARE(lua_State *, lua_newstate, lua_Alloc f, void *ud) SYMBOL_DECLARE(lua_State *, lua_newstate, lua_Alloc f, void *ud)
SYMBOL_DECLARE(void, lua_close, lua_State *L) SYMBOL_DECLARE(void, lua_close, lua_State *L)
SYMBOL_DECLARE(lua_State *, lua_newthread, lua_State *L) SYMBOL_DECLARE(lua_State *, lua_newthread, lua_State *L)
SYMBOL_DECLARE(int, lua_closethread, lua_State *L, lua_State *from)
SYMBOL_DECLARE(int, lua_resetthread, lua_State *L) SYMBOL_DECLARE(int, lua_resetthread, lua_State *L)
SYMBOL_DECLARE(lua_CFunction, lua_atpanic, lua_State *L, lua_CFunction panicf) SYMBOL_DECLARE(lua_CFunction, lua_atpanic, lua_State *L, lua_CFunction panicf)
@ -1739,6 +1745,9 @@ SYMBOL_WRAP_DECL(void, lua_close, lua_State *L) {
SYMBOL_WRAP_DECL(lua_State *, lua_newthread, lua_State *L) { SYMBOL_WRAP_DECL(lua_State *, lua_newthread, lua_State *L) {
return SYMBOL_WRAP_CALL(lua_newthread, L); return SYMBOL_WRAP_CALL(lua_newthread, L);
} }
SYMBOL_WRAP_DECL(int, lua_closethread, lua_State *L, lua_State *from) {
return SYMBOL_WRAP_CALL(lua_closethread, L, from);
}
SYMBOL_WRAP_DECL(int, lua_resetthread, lua_State *L) { SYMBOL_WRAP_DECL(int, lua_resetthread, lua_State *L) {
return SYMBOL_WRAP_CALL(lua_resetthread, L); return SYMBOL_WRAP_CALL(lua_resetthread, L);
} }
@ -2351,6 +2360,7 @@ void lite_xl_plugin_init(void *XL) {
IMPORT_SYMBOL(lua_newstate, lua_State *, lua_Alloc f, void *ud); IMPORT_SYMBOL(lua_newstate, lua_State *, lua_Alloc f, void *ud);
IMPORT_SYMBOL(lua_close, void, lua_State *L); IMPORT_SYMBOL(lua_close, void, lua_State *L);
IMPORT_SYMBOL(lua_newthread, lua_State *, lua_State *L); IMPORT_SYMBOL(lua_newthread, lua_State *, lua_State *L);
IMPORT_SYMBOL(lua_closethread, int, lua_State *L, lua_State *from);
IMPORT_SYMBOL(lua_resetthread, int, lua_State *L); IMPORT_SYMBOL(lua_resetthread, int, lua_State *L);
IMPORT_SYMBOL(lua_atpanic, lua_CFunction, lua_State *L, lua_CFunction panicf); IMPORT_SYMBOL(lua_atpanic, lua_CFunction, lua_State *L, lua_CFunction panicf);
IMPORT_SYMBOL(lua_version, lua_Number, lua_State *L); IMPORT_SYMBOL(lua_version, lua_Number, lua_State *L);
@ -2560,4 +2570,4 @@ void lite_xl_plugin_init(void *XL);
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************************************************************/ ******************************************************************************/

View File

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@ -0,0 +1,28 @@
# configuration for dmgbuild
import os.path
app_path = "Lite XL.app"
app_name = os.path.basename(app_path)
# Image options
format = defines.get("format", "UDZO")
# Content options
files = [(app_path, app_name)]
symlinks = { "Applications": "/Applications" }
icon = "resources/icons/icon.icns"
icon_locations = {
app_name: (144, 248),
"Applications": (336, 248)
}
# Window options
background = "resources/macos/dmg-cover.png"
window_rect = ((360, 360), (480, 380))
default_view = "coverflow"
include_icon_view_settings = True
# Icon view options
icon_size = 80
text_size = 11.0

View File

@ -1,6 +1,6 @@
diff -ruN lua-5.4.4\meson.build lua-5.4.4-patched\meson.build diff -ruN lua-5.4.4/meson.build lua-5.4.4-patched/meson.build
--- lua-5.4.4\meson.build Wed Feb 22 18:16:56 2023 --- lua-5.4.4/meson.build Wed Feb 22 18:16:56 2023
+++ lua-5.4.4-patched\meson.build Wed Feb 22 04:10:01 2023 +++ lua-5.4.4-patched/meson.build Wed Feb 22 04:10:01 2023
@@ -85,6 +85,7 @@ @@ -85,6 +85,7 @@
'src/lutf8lib.c', 'src/lutf8lib.c',
'src/lvm.c', 'src/lvm.c',
@ -9,13 +9,13 @@ diff -ruN lua-5.4.4\meson.build lua-5.4.4-patched\meson.build
dependencies: lua_lib_deps, dependencies: lua_lib_deps,
version: meson.project_version(), version: meson.project_version(),
soversion: lua_versions[0] + '.' + lua_versions[1], soversion: lua_versions[0] + '.' + lua_versions[1],
diff -ruN lua-5.4.4\src\luaconf.h lua-5.4.4-patched\src\luaconf.h diff -ruN lua-5.4.4/src/luaconf.h lua-5.4.4-patched/src/luaconf.h
--- lua-5.4.4\src\luaconf.h Thu Jan 13 19:24:43 2022 --- lua-5.4.4/src/luaconf.h Thu Jan 13 19:24:43 2022
+++ lua-5.4.4-patched\src\luaconf.h Wed Feb 22 04:10:02 2023 +++ lua-5.4.4-patched/src/luaconf.h Wed Feb 22 04:10:02 2023
@@ -782,5 +782,15 @@ @@ -782,5 +782,15 @@
+#if defined(lua_c) || defined(luac_c) || (defined(LUA_LIB) && \ +#if defined(lua_c) || defined(luac_c) || (defined(LUA_LIB) && \
+ (defined(lauxlib_c) || defined(liolib_c) || \ + (defined(lauxlib_c) || defined(liolib_c) || \
+ defined(loadlib_c) || defined(loslib_c))) + defined(loadlib_c) || defined(loslib_c)))
@ -27,22 +27,22 @@ diff -ruN lua-5.4.4\src\luaconf.h lua-5.4.4-patched\src\luaconf.h
+ +
+ +
#endif #endif
diff -ruN lua-5.4.4\src\Makefile lua-5.4.4-patched\src\Makefile diff -ruN lua-5.4.4/src/Makefile lua-5.4.4-patched/src/Makefile
--- lua-5.4.4\src\Makefile Thu Jul 15 22:01:52 2021 --- lua-5.4.4/src/Makefile Thu Jul 15 22:01:52 2021
+++ lua-5.4.4-patched\src\Makefile Wed Feb 22 04:10:02 2023 +++ lua-5.4.4-patched/src/Makefile Wed Feb 22 04:10:02 2023
@@ -33,7 +33,7 @@ @@ -33,7 +33,7 @@
PLATS= guess aix bsd c89 freebsd generic linux linux-readline macosx mingw posix solaris PLATS= guess aix bsd c89 freebsd generic linux linux-readline macosx mingw posix solaris
LUA_A= liblua.a LUA_A= liblua.a
-CORE_O= lapi.o lcode.o lctype.o ldebug.o ldo.o ldump.o lfunc.o lgc.o llex.o lmem.o lobject.o lopcodes.o lparser.o lstate.o lstring.o ltable.o ltm.o lundump.o lvm.o lzio.o -CORE_O= lapi.o lcode.o lctype.o ldebug.o ldo.o ldump.o lfunc.o lgc.o llex.o lmem.o lobject.o lopcodes.o lparser.o lstate.o lstring.o ltable.o ltm.o lundump.o lvm.o lzio.o
+CORE_O= lapi.o lcode.o lctype.o ldebug.o ldo.o ldump.o lfunc.o lgc.o llex.o lmem.o lobject.o lopcodes.o lparser.o lstate.o lstring.o ltable.o ltm.o lundump.o lvm.o lzio.o utf8_wrappers.o +CORE_O= lapi.o lcode.o lctype.o ldebug.o ldo.o ldump.o lfunc.o lgc.o llex.o lmem.o lobject.o lopcodes.o lparser.o lstate.o lstring.o ltable.o ltm.o lundump.o lvm.o lzio.o utf8_wrappers.o
LIB_O= lauxlib.o lbaselib.o lcorolib.o ldblib.o liolib.o lmathlib.o loadlib.o loslib.o lstrlib.o ltablib.o lutf8lib.o linit.o LIB_O= lauxlib.o lbaselib.o lcorolib.o ldblib.o liolib.o lmathlib.o loadlib.o loslib.o lstrlib.o ltablib.o lutf8lib.o linit.o
BASE_O= $(CORE_O) $(LIB_O) $(MYOBJS) BASE_O= $(CORE_O) $(LIB_O) $(MYOBJS)
diff -ruN lua-5.4.4\src\utf8_wrappers.c lua-5.4.4-patched\src\utf8_wrappers.c diff -ruN lua-5.4.4/src/utf8_wrappers.c lua-5.4.4-patched/src/utf8_wrappers.c
--- lua-5.4.4\src\utf8_wrappers.c Thu Jan 01 08:00:00 1970 --- lua-5.4.4/src/utf8_wrappers.c Thu Jan 01 08:00:00 1970
+++ lua-5.4.4-patched\src\utf8_wrappers.c Wed Feb 22 18:13:45 2023 +++ lua-5.4.4-patched/src/utf8_wrappers.c Wed Feb 22 18:13:45 2023
@@ -0,0 +1,129 @@ @@ -0,0 +1,129 @@
+/** +/**
+ * Wrappers to provide Unicode (UTF-8) support on Windows. + * Wrappers to provide Unicode (UTF-8) support on Windows.
@ -173,9 +173,9 @@ diff -ruN lua-5.4.4\src\utf8_wrappers.c lua-5.4.4-patched\src\utf8_wrappers.c
+ return env_value; + return env_value;
+} +}
+#endif +#endif
diff -ruN lua-5.4.4\src\utf8_wrappers.h lua-5.4.4-patched\src\utf8_wrappers.h diff -ruN lua-5.4.4/src/utf8_wrappers.h lua-5.4.4-patched/src/utf8_wrappers.h
--- lua-5.4.4\src\utf8_wrappers.h Thu Jan 01 08:00:00 1970 --- lua-5.4.4/src/utf8_wrappers.h Thu Jan 01 08:00:00 1970
+++ lua-5.4.4-patched\src\utf8_wrappers.h Wed Feb 22 18:09:48 2023 +++ lua-5.4.4-patched/src/utf8_wrappers.h Wed Feb 22 18:09:48 2023
@@ -0,0 +1,46 @@ @@ -0,0 +1,46 @@
+/** +/**
+ * Wrappers to provide Unicode (UTF-8) support on Windows. + * Wrappers to provide Unicode (UTF-8) support on Windows.

View File

@ -10,7 +10,7 @@ Various scripts and configurations used to configure, build, and package Lite XL
### Package ### Package
- **appdmg.sh**: Create a macOS DMG image using [AppDMG][1]. - **appdmg.sh**: Create a macOS DMG image using [dmgbuild][1].
- **appimage.sh**: [AppImage][2] builder. - **appimage.sh**: [AppImage][2] builder.
- **innosetup.sh**: Creates a 32/64 bit [InnoSetup][3] installer package. - **innosetup.sh**: Creates a 32/64 bit [InnoSetup][3] installer package.
- **package.sh**: Creates all binary / DMG image / installer / source packages. - **package.sh**: Creates all binary / DMG image / installer / source packages.
@ -25,6 +25,6 @@ Various scripts and configurations used to configure, build, and package Lite XL
- **generate_header.sh**: Generates a header file for native plugin API - **generate_header.sh**: Generates a header file for native plugin API
- **keymap-generator**: Generates a JSON file containing the keymap - **keymap-generator**: Generates a JSON file containing the keymap
[1]: https://github.com/LinusU/node-appdmg [1]: https://github.com/dmgbuild/dmgbuild
[2]: https://docs.appimage.org/ [2]: https://docs.appimage.org/
[3]: https://jrsoftware.org/isinfo.php [3]: https://jrsoftware.org/isinfo.php

View File

@ -6,25 +6,4 @@ if [ ! -e "src/api/api.h" ]; then
exit 1 exit 1
fi fi
cat > lite-xl-dmg.json << EOF dmgbuild -s resources/macos/lite-xl-dmg.py "Lite XL" "$1.dmg"
{
"title": "Lite XL",
"icon": "$(pwd)/resources/icons/icon.icns",
"background": "$(pwd)/resources/macos/appdmg.png",
"window": {
"position": {
"x": 360,
"y": 360
},
"size": {
"width": 480,
"height": 360
}
},
"contents": [
{ "x": 144, "y": 248, "type": "file", "path": "$(pwd)/Lite XL.app" },
{ "x": 336, "y": 248, "type": "link", "path": "/Applications" }
]
}
EOF
~/node_modules/appdmg/bin/appdmg.js lite-xl-dmg.json "$(pwd)/$1.dmg"

View File

@ -181,7 +181,7 @@ main() {
# download the subprojects so we can start patching before configure. # download the subprojects so we can start patching before configure.
# this will prevent reconfiguring the project. # this will prevent reconfiguring the project.
meson subprojects download meson subprojects download
lua_subproject_path=$(echo subprojects/lua-*/) lua_subproject_path="subprojects/$(awk -F ' *= *' '/directory/ { printf $2 }' subprojects/lua.wrap)"
if [[ -d $lua_subproject_path ]]; then if [[ -d $lua_subproject_path ]]; then
patch -d $lua_subproject_path -p1 --forward < resources/windows/001-lua-unicode.diff patch -d $lua_subproject_path -p1 --forward < resources/windows/001-lua-unicode.diff
fi fi

View File

@ -1,7 +1,7 @@
#define MyAppName "Lite XL" #define MyAppName "Lite XL"
#define MyAppVersion "@PROJECT_VERSION@" #define MyAppVersion "@PROJECT_VERSION@"
#define MyAppPublisher "Lite XL Team" #define MyAppPublisher "Lite XL Team"
#define MyAppURL "https://lite-xl.github.io" #define MyAppURL "https://lite-xl.com"
#define MyAppExeName "lite-xl.exe" #define MyAppExeName "lite-xl.exe"
#define BuildDir "@PROJECT_BUILD_DIR@" #define BuildDir "@PROJECT_BUILD_DIR@"
#define SourceDir "@PROJECT_SOURCE_DIR@" #define SourceDir "@PROJECT_SOURCE_DIR@"
@ -57,9 +57,13 @@ OutputBaseFilename=LiteXL-{#MyAppVersion}-{#ArchInternal}-setup
LicenseFile={#SourceDir}/LICENSE LicenseFile={#SourceDir}/LICENSE
SetupIconFile={#SourceDir}/resources/icons/icon.ico SetupIconFile={#SourceDir}/resources/icons/icon.ico
UninstallDisplayIcon={app}\{#MyAppExeName}, 0
WizardImageFile="{#SourceDir}/scripts/innosetup/wizard-modern-image.bmp" WizardImageFile="{#SourceDir}/scripts/innosetup/wizard-modern-image.bmp"
WizardSmallImageFile="{#SourceDir}/scripts/innosetup/litexl-55px.bmp" WizardSmallImageFile="{#SourceDir}/scripts/innosetup/litexl-55px.bmp"
; Required for the add to path option to refresh environment
ChangesEnvironment=yes
[Languages] [Languages]
Name: "english"; MessagesFile: "compiler:Default.isl" Name: "english"; MessagesFile: "compiler:Default.isl"
@ -67,11 +71,10 @@ Name: "english"; MessagesFile: "compiler:Default.isl"
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked; OnlyBelowVersion: 6.1; Check: not IsAdminInstallMode Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked; OnlyBelowVersion: 6.1; Check: not IsAdminInstallMode
Name: "portablemode"; Description: "Portable Mode"; Flags: unchecked Name: "portablemode"; Description: "Portable Mode"; Flags: unchecked
Name: "envPath"; Description: "Add lite-xl to the PATH variable, allowing it to be run from a command line."
[Files] [Files]
Source: "{#BuildDir}/src/lite-xl.exe"; DestDir: "{app}"; Flags: ignoreversion Source: "{#SourceDir}/lite-xl/*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs
Source: "{#BuildDir}/mingwLibs{#Arch}/*"; DestDir: "{app}"; Flags: ignoreversion ; Check: DirExists(ExpandConstant('{#BuildDir}/mingwLibs{#Arch}'))
Source: "{#SourceDir}/data/*"; DestDir: "{app}/data"; Flags: ignoreversion recursesubdirs
; NOTE: Don't use "Flags: ignoreversion" on any shared system files ; NOTE: Don't use "Flags: ignoreversion" on any shared system files
[Icons] [Icons]
@ -81,8 +84,78 @@ Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}";
Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: quicklaunchicon; Check: not WizardIsTaskSelected('portablemode') Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: quicklaunchicon; Check: not WizardIsTaskSelected('portablemode')
; Name: "{usersendto}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" ; Name: "{usersendto}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
[Registry]
Root: "HKA"; Subkey: "Software\Classes\*\shell\{#MyAppName}"; ValueType: string; ValueName: ""; ValueData: "Open with {#MyAppName}"; Flags: uninsdeletekey
Root: "HKA"; Subkey: "Software\Classes\*\shell\{#MyAppName}"; ValueType: string; ValueName: "Icon"; ValueData: "{app}\{#MyAppExeName}, 0"; Flags: uninsdeletekey
Root: "HKA"; Subkey: "Software\Classes\*\shell\{#MyAppName}\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExename}"" ""%1"""; Flags: uninsdeletekey
Root: "HKA"; Subkey: "Software\Classes\directory\shell\{#MyAppName}"; ValueType: string; ValueName: ""; ValueData: "Open with {#MyAppName}"; Flags: uninsdeletekey
Root: "HKA"; Subkey: "Software\Classes\directory\shell\{#MyAppName}"; ValueType: string; ValueName: "Icon"; ValueData: "{app}\{#MyAppExeName}, 0"; Flags: uninsdeletekey
Root: "HKA"; Subkey: "Software\Classes\directory\shell\{#MyAppName}\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExename}"" ""%1"""; Flags: uninsdeletekey
Root: "HKA"; Subkey: "Software\Classes\directory\background\shell\{#MyAppName}"; ValueType: string; ValueName: ""; ValueData: "Open with {#MyAppName}"; Flags: uninsdeletekey
Root: "HKA"; Subkey: "Software\Classes\directory\background\shell\{#MyAppName}"; ValueType: string; ValueName: "Icon"; ValueData: "{app}\{#MyAppExeName}, 0"; Flags: uninsdeletekey
Root: "HKA"; Subkey: "Software\Classes\directory\background\shell\{#MyAppName}\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExename}"" ""%V"""; Flags: uninsdeletekey
[Run] [Run]
Filename: "{app}/{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent Filename: "{app}/{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
[Setup] [Setup]
Uninstallable=not WizardIsTaskSelected('portablemode') Uninstallable=not WizardIsTaskSelected('portablemode')
; Code to add installation path to environment taken from:
; https://stackoverflow.com/a/46609047
[Code]
const EnvironmentKey = 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment';
procedure EnvAddPath(Path: string);
var
Paths: string;
begin
{ Retrieve current path (use empty string if entry not exists) }
if not RegQueryStringValue(HKEY_LOCAL_MACHINE, EnvironmentKey, 'Path', Paths)
then Paths := '';
{ Skip if string already found in path }
if Pos(';' + Uppercase(Path) + ';', ';' + Uppercase(Paths) + ';') > 0 then exit;
{ App string to the end of the path variable }
Paths := Paths + ';'+ Path +';'
{ Overwrite (or create if missing) path environment variable }
if RegWriteStringValue(HKEY_LOCAL_MACHINE, EnvironmentKey, 'Path', Paths)
then Log(Format('The [%s] added to PATH: [%s]', [Path, Paths]))
else Log(Format('Error while adding the [%s] to PATH: [%s]', [Path, Paths]));
end;
procedure EnvRemovePath(Path: string);
var
Paths: string;
P: Integer;
begin
{ Skip if registry entry not exists }
if not RegQueryStringValue(HKEY_LOCAL_MACHINE, EnvironmentKey, 'Path', Paths) then
exit;
{ Skip if string not found in path }
P := Pos(';' + Uppercase(Path) + ';', ';' + Uppercase(Paths) + ';');
if P = 0 then exit;
{ Update path variable }
Delete(Paths, P - 1, Length(Path) + 1);
{ Overwrite path environment variable }
if RegWriteStringValue(HKEY_LOCAL_MACHINE, EnvironmentKey, 'Path', Paths)
then Log(Format('The [%s] removed from PATH: [%s]', [Path, Paths]))
else Log(Format('Error while removing the [%s] from PATH: [%s]', [Path, Paths]));
end;
procedure CurStepChanged(CurStep: TSetupStep);
begin
if (CurStep = ssPostInstall) and WizardIsTaskSelected('envPath')
then EnvAddPath(ExpandConstant('{app}'));
end;
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
begin
if CurUninstallStep = usPostUninstall
then EnvRemovePath(ExpandConstant('{app}'));
end;

View File

@ -57,9 +57,7 @@ main() {
else else
brew install bash ninja sdl2 brew install bash ninja sdl2
fi fi
pip3 install meson pip3 install meson dmgbuild
cd ~; npm install appdmg; cd -
~/node_modules/appdmg/bin/appdmg.js --version
elif [[ "$OSTYPE" == "msys" ]]; then elif [[ "$OSTYPE" == "msys" ]]; then
if [[ $lhelper == true ]]; then if [[ $lhelper == true ]]; then
pacman --noconfirm -S \ pacman --noconfirm -S \

View File

@ -25,7 +25,7 @@ show_help() {
echo "-A --appimage Create an AppImage (Linux only)." echo "-A --appimage Create an AppImage (Linux only)."
echo "-B --binary Create a normal / portable package or macOS bundle," echo "-B --binary Create a normal / portable package or macOS bundle,"
echo " depending on how the build was configured. (Default.)" echo " depending on how the build was configured. (Default.)"
echo "-D --dmg Create a DMG disk image with AppDMG (macOS only)." echo "-D --dmg Create a DMG disk image with dmgbuild (macOS only)."
echo "-I --innosetup Create a InnoSetup package (Windows only)." echo "-I --innosetup Create a InnoSetup package (Windows only)."
echo "-r --release Strip debugging symbols." echo "-r --release Strip debugging symbols."
echo "-S --source Create a source code package," echo "-S --source Create a source code package,"
@ -264,6 +264,11 @@ main() {
$stripcmd "${exe_file}" $stripcmd "${exe_file}"
fi fi
if [[ $bundle == true ]]; then
# https://eclecticlight.co/2019/01/17/code-signing-for-the-concerned-3-signing-an-app/
codesign --force --deep -s - "${dest_dir}"
fi
echo "Creating a compressed archive ${package_name}" echo "Creating a compressed archive ${package_name}"
if [[ $binary == true ]]; then if [[ $binary == true ]]; then
rm -f "${package_name}".tar.gz rm -f "${package_name}".tar.gz

View File

@ -1,4 +1,5 @@
#include "api.h" #include "api.h"
#include "lua.h"
#include <SDL.h> #include <SDL.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
@ -25,13 +26,16 @@ int get_mode_dirmonitor();
static int f_check_dir_callback(int watch_id, const char* path, void* L) { static int f_check_dir_callback(int watch_id, const char* path, void* L) {
lua_pushvalue(L, -1); // using absolute indices from f_dirmonitor_check (2: callback, 3: error_callback)
lua_pushvalue(L, 2);
if (path) if (path)
lua_pushlstring(L, path, watch_id); lua_pushlstring(L, path, watch_id);
else else
lua_pushnumber(L, watch_id); lua_pushnumber(L, watch_id);
lua_call(L, 1, 1);
int result = lua_toboolean(L, -1); int result = 0;
if (lua_pcall(L, 1, 1, 3) == LUA_OK)
result = lua_toboolean(L, -1);
lua_pop(L, 1); lua_pop(L, 1);
return !result; return !result;
} }
@ -95,8 +99,20 @@ static int f_dirmonitor_unwatch(lua_State *L) {
} }
static int f_noop(lua_State *L) { return 0; }
static int f_dirmonitor_check(lua_State* L) { static int f_dirmonitor_check(lua_State* L) {
struct dirmonitor* monitor = luaL_checkudata(L, 1, API_TYPE_DIRMONITOR); struct dirmonitor* monitor = luaL_checkudata(L, 1, API_TYPE_DIRMONITOR);
luaL_checktype(L, 2, LUA_TFUNCTION);
if (!lua_isnoneornil(L, 3)) {
luaL_checktype(L, 3, LUA_TFUNCTION);
} else {
lua_settop(L, 2);
lua_pushcfunction(L, f_noop);
}
lua_settop(L, 3);
SDL_LockMutex(monitor->mutex); SDL_LockMutex(monitor->mutex);
if (monitor->length < 0) if (monitor->length < 0)
lua_pushnil(L); lua_pushnil(L);

View File

@ -34,6 +34,8 @@
typedef DWORD process_error_t; typedef DWORD process_error_t;
typedef HANDLE process_stream_t; typedef HANDLE process_stream_t;
typedef HANDLE process_handle_t; typedef HANDLE process_handle_t;
typedef wchar_t process_arglist_t[32767];
typedef wchar_t *process_env_t;
#define HANDLE_INVALID (INVALID_HANDLE_VALUE) #define HANDLE_INVALID (INVALID_HANDLE_VALUE)
#define PROCESS_GET_HANDLE(P) ((P)->process_information.hProcess) #define PROCESS_GET_HANDLE(P) ((P)->process_information.hProcess)
@ -45,12 +47,20 @@ static volatile long PipeSerialNumber;
typedef int process_error_t; typedef int process_error_t;
typedef int process_stream_t; typedef int process_stream_t;
typedef pid_t process_handle_t; typedef pid_t process_handle_t;
typedef char **process_arglist_t;
typedef char **process_env_t;
#define HANDLE_INVALID (0) #define HANDLE_INVALID (0)
#define PROCESS_GET_HANDLE(P) ((P)->pid) #define PROCESS_GET_HANDLE(P) ((P)->pid)
#endif #endif
#ifdef __GNUC__
#define UNUSED __attribute__((__unused__))
#else
#define UNUSED
#endif
typedef struct { typedef struct {
bool running, detached; bool running, detached;
int returncode, deadline; int returncode, deadline;
@ -342,51 +352,264 @@ static bool signal_process(process_t* proc, signal_e sig) {
return true; return true;
} }
static int process_start(lua_State* L) {
int retval = 1; static UNUSED char *xstrdup(const char *str) {
size_t env_len = 0, key_len, val_len; char *result = str ? malloc(strlen(str) + 1) : NULL;
const char *cmd[256] = { NULL }, *env_names[256] = { NULL }, *env_values[256] = { NULL }, *cwd = NULL; if (result) strcpy(result, str);
bool detach = false, literal = false; return result;
int deadline = 10, new_fds[3] = { STDIN_FD, STDOUT_FD, STDERR_FD }; }
size_t arg_len = lua_gettop(L), cmd_len;
if (lua_type(L, 1) == LUA_TTABLE) {
#if LUA_VERSION_NUM > 501 static int process_arglist_init(process_arglist_t *list, size_t *list_len, size_t nargs) {
lua_len(L, 1); *list_len = 0;
#else #ifdef _WIN32
lua_pushinteger(L, (int)lua_objlen(L, 1)); memset(*list, 0, sizeof(process_arglist_t));
#endif #else
cmd_len = luaL_checknumber(L, -1); lua_pop(L, 1); *list = calloc(sizeof(char *), nargs + 1);
if (!cmd_len) if (!*list) return ENOMEM;
// we have not allocated anything here yet, so we can skip cleanup code #endif
// don't do this anywhere else! return 0;
return luaL_argerror(L, 1,"table cannot be empty"); }
for (size_t i = 1; i <= cmd_len; ++i) {
lua_pushinteger(L, i);
lua_rawget(L, 1); static int process_arglist_add(process_arglist_t *list, size_t *list_len, const char *arg, bool escape) {
cmd[i-1] = luaL_checkstring(L, -1); size_t len = *list_len;
} #ifdef _WIN32
int arg_len;
wchar_t *cmdline = *list;
wchar_t arg_w[32767];
// this length includes the null terminator!
if (!(arg_len = MultiByteToWideChar(CP_UTF8, 0, arg, -1, arg_w, 32767)))
return GetLastError();
if (arg_len + len > 32767)
return ERROR_NOT_ENOUGH_MEMORY;
if (!escape) {
// replace the current null terminator with a space
if (len > 0) cmdline[len-1] = ' ';
memcpy(cmdline + len, arg_w, arg_len * sizeof(wchar_t));
len += arg_len;
} else { } else {
literal = true; // if the string contains spaces, then we must quote it
cmd[0] = luaL_checkstring(L, 1); bool quote = wcspbrk(arg_w, L" \t\v\r\n");
cmd_len = 1; int backslash = 0, escaped_len = quote ? 2 : 0;
for (int i = 0; i < arg_len; i++) {
if (arg_w[i] == L'\\') {
backslash++;
} else if (arg_w[i] == L'"') {
escaped_len += backslash + 1;
backslash = 0;
} else {
backslash = 0;
}
escaped_len++;
}
// escape_len contains NUL terminator
if (escaped_len + len > 32767)
return ERROR_NOT_ENOUGH_MEMORY;
// replace our previous NUL terminator with space
if (len > 0) cmdline[len-1] = L' ';
if (quote) cmdline[len++] = L'"';
// we are not going to iterate over NUL terminator
for (int i = 0;arg_w[i]; i++) {
if (arg_w[i] == L'\\') {
backslash++;
} else if (arg_w[i] == L'"') {
// add backslash + 1 backslashes
for (int j = 0; j < backslash; j++)
cmdline[len++] = L'\\';
cmdline[len++] = L'\\';
backslash = 0;
} else {
backslash = 0;
}
cmdline[len++] = arg_w[i];
}
if (quote) cmdline[len++] = L'"';
cmdline[len++] = L'\0';
}
#else
char **cmd = *list;
cmd[len] = xstrdup(arg);
if (!cmd[len]) return ENOMEM;
len++;
#endif
*list_len = len;
return 0;
}
static void process_arglist_free(process_arglist_t *list) {
#ifndef _WIN32
char **cmd = *list;
for (int i = 0; cmd[i]; i++)
free(cmd[i]);
free(cmd);
*list = NULL;
#endif
}
static int process_env_init(process_env_t *env_list, size_t *env_len, size_t nenv) {
*env_len = 0;
#ifdef _WIN32
*env_list = NULL;
#else
*env_list = calloc(sizeof(char *), nenv * 2);
if (!*env_list) return ENOMEM;
#endif
return 0;
}
#ifdef _WIN32
static int cmp_name(wchar_t *a, wchar_t *b) {
wchar_t _A[32767], _B[32767], *A = _A, *B = _B, *a_eq, *b_eq;
int na, nb, r;
a_eq = wcschr(a, L'=');
b_eq = wcschr(b, L'=');
assert(a_eq);
assert(b_eq);
na = a_eq - a;
nb = b_eq - b;
r = LCMapStringW(LOCALE_INVARIANT, LCMAP_UPPERCASE, a, na, A, na);
assert(r == na);
A[na] = L'\0';
r = LCMapStringW(LOCALE_INVARIANT, LCMAP_UPPERCASE, b, nb, B, nb);
assert(r == nb);
B[nb] = L'\0';
for (;;) {
wchar_t AA = *A++, BB = *B++;
if (AA > BB)
return 1;
else if (AA < BB)
return -1;
else if (!AA && !BB)
return 0;
}
}
static int process_env_add_variable(process_env_t *env_list, size_t *env_list_len, wchar_t *var, size_t var_len) {
wchar_t *list, *list_p;
size_t block_var_len, list_len;
list = list_p = *env_list;
list_len = *env_list_len;
if (list_len) {
// check if it is already in the block
while ((block_var_len = wcslen(list_p))) {
if (cmp_name(list_p, var) == 0)
return -1; // already installed
list_p += block_var_len + 1;
}
}
// allocate list + 1 characters for the block terminator
list = realloc(list, (list_len + var_len + 1) * sizeof(wchar_t));
if (!list) return ERROR_NOT_ENOUGH_MEMORY;
// copy the env variable to the block
memcpy(list + list_len, var, var_len * sizeof(wchar_t));
// terminate the block again
list[list_len + var_len] = L'\0';
*env_list = list;
*env_list_len = (list_len + var_len);
return 0;
}
static int process_env_add_system(process_env_t *env_list, size_t *env_list_len) {
int retval = 0;
wchar_t *proc_env_block, *proc_env_block_p;
int proc_env_len;
proc_env_block = proc_env_block_p = GetEnvironmentStringsW();
while ((proc_env_len = wcslen(proc_env_block_p))) {
// try to add it to the list
if ((retval = process_env_add_variable(env_list, env_list_len, proc_env_block_p, proc_env_len + 1)) > 0)
goto cleanup;
proc_env_block_p += proc_env_len + 1;
}
retval = 0;
cleanup:
if (proc_env_block) FreeEnvironmentStringsW(proc_env_block);
return retval;
}
#endif
static int process_env_add(process_env_t *env_list, size_t *env_len, const char *key, const char *value) {
#ifdef _WIN32
wchar_t env_var[32767];
int r, var_len = 0;
if (!(r = MultiByteToWideChar(CP_UTF8, 0, key, -1, env_var, 32767)))
return GetLastError();
var_len += r;
env_var[var_len-1] = L'=';
if (!(r = MultiByteToWideChar(CP_UTF8, 0, value, -1, env_var + var_len, 32767 - var_len)))
return GetLastError();
var_len += r;
return process_env_add_variable(env_list, env_len, env_var, var_len);
#else
(*env_list)[*env_len] = xstrdup(key);
if (!(*env_list)[*env_len])
return ENOMEM;
(*env_list)[*env_len + 1] = xstrdup(value);
if (!(*env_list)[*env_len + 1])
return ENOMEM;
*env_len += 2;
#endif
return 0;
}
static void process_env_free(process_env_t *list) {
if (!*list) return;
#ifdef _WIN32
free(*list);
#else
for (size_t i = 0; (*list)[i]; i++) free((*list)[i]);
free(*list);
#endif
*list = NULL;
}
static int process_start(lua_State* L) {
int r, retval = 1;
size_t env_len = 0, cmd_len = 0, arglist_len = 0, env_vars_len = 0;
process_arglist_t arglist;
process_env_t env_vars = NULL;
const char *cwd = NULL;
bool detach = false, escape = true;
int deadline = 10, new_fds[3] = { STDIN_FD, STDOUT_FD, STDERR_FD };
if (lua_isstring(L, 1)) {
escape = false;
// create a table that contains the string as the value
lua_createtable(L, 1, 0);
lua_pushvalue(L, 1);
lua_rawseti(L, -2, 1);
lua_replace(L, 1);
} }
if (arg_len > 1) { luaL_checktype(L, 1, LUA_TTABLE);
lua_getfield(L, 2, "env"); #if LUA_VERSION_NUM > 501
if (!lua_isnil(L, -1)) { lua_len(L, 1);
lua_pushnil(L); #else
while (lua_next(L, -2) != 0) { lua_pushinteger(L, (int)lua_objlen(L, 1));
const char* key = luaL_checklstring(L, -2, &key_len); #endif
const char* val = luaL_checklstring(L, -1, &val_len); cmd_len = luaL_checknumber(L, -1); lua_pop(L, 1);
env_names[env_len] = malloc(key_len+1); if (!cmd_len)
strcpy((char*)env_names[env_len], key); return luaL_argerror(L, 1, "table cannot be empty");
env_values[env_len] = malloc(val_len+1); // check if each arguments is a string
strcpy((char*)env_values[env_len], val); for (size_t i = 1; i <= cmd_len; ++i) {
lua_pop(L, 1); lua_rawgeti(L, 1, i);
++env_len; luaL_checkstring(L, -1);
} lua_pop(L, 1);
} else }
lua_pop(L, 1);
if (lua_istable(L, 2)) {
lua_getfield(L, 2, "detach"); detach = lua_toboolean(L, -1); lua_getfield(L, 2, "detach"); detach = lua_toboolean(L, -1);
lua_getfield(L, 2, "timeout"); deadline = luaL_optnumber(L, -1, deadline); lua_getfield(L, 2, "timeout"); deadline = luaL_optnumber(L, -1, deadline);
lua_getfield(L, 2, "cwd"); cwd = luaL_optstring(L, -1, NULL); lua_getfield(L, 2, "cwd"); cwd = luaL_optstring(L, -1, NULL);
@ -394,20 +617,70 @@ static int process_start(lua_State* L) {
lua_getfield(L, 2, "stdout"); new_fds[STDOUT_FD] = luaL_optnumber(L, -1, STDOUT_FD); lua_getfield(L, 2, "stdout"); new_fds[STDOUT_FD] = luaL_optnumber(L, -1, STDOUT_FD);
lua_getfield(L, 2, "stderr"); new_fds[STDERR_FD] = luaL_optnumber(L, -1, STDERR_FD); lua_getfield(L, 2, "stderr"); new_fds[STDERR_FD] = luaL_optnumber(L, -1, STDERR_FD);
for (int stream = STDIN_FD; stream <= STDERR_FD; ++stream) { for (int stream = STDIN_FD; stream <= STDERR_FD; ++stream) {
if (new_fds[stream] > STDERR_FD || new_fds[stream] < REDIRECT_PARENT) { if (new_fds[stream] > STDERR_FD || new_fds[stream] < REDIRECT_PARENT)
lua_pushfstring(L, "error: redirect to handles, FILE* and paths are not supported"); return luaL_error(L, "error: redirect to handles, FILE* and paths are not supported");
}
lua_pop(L, 6); // pop all the values above
luaL_getsubtable(L, 2, "env");
// count environment variobles
lua_pushnil(L);
while (lua_next(L, -2) != 0) {
luaL_checkstring(L, -2);
luaL_checkstring(L, -1);
lua_pop(L, 1);
env_len++;
}
if (env_len) {
if ((r = process_env_init(&env_vars, &env_vars_len, env_len)) != 0) {
retval = -1; retval = -1;
push_error(L, "cannot allocate environment list", r);
goto cleanup; goto cleanup;
} }
lua_pushnil(L);
while (lua_next(L, -2) != 0) {
if ((r = process_env_add(&env_vars, &env_vars_len, lua_tostring(L, -2), lua_tostring(L, -1))) != 0) {
retval = -1;
push_error(L, "cannot copy environment variable", r);
goto cleanup;
}
lua_pop(L, 1);
env_len++;
}
} }
} }
// allocate and copy commands
if ((r = process_arglist_init(&arglist, &arglist_len, cmd_len)) != 0) {
retval = -1;
push_error(L, "cannot create argument list", r);
goto cleanup;
}
for (size_t i = 1; i <= cmd_len; i++) {
lua_rawgeti(L, 1, i);
if ((r = process_arglist_add(&arglist, &arglist_len, lua_tostring(L, -1), escape)) != 0) {
retval = -1;
push_error(L, "cannot add argument", r);
goto cleanup;
}
lua_pop(L, 1);
}
process_t* self = lua_newuserdata(L, sizeof(process_t)); process_t* self = lua_newuserdata(L, sizeof(process_t));
memset(self, 0, sizeof(process_t)); memset(self, 0, sizeof(process_t));
luaL_setmetatable(L, API_TYPE_PROCESS); luaL_setmetatable(L, API_TYPE_PROCESS);
self->deadline = deadline; self->deadline = deadline;
self->detached = detach; self->detached = detach;
#if _WIN32 #if _WIN32
if (env_vars) {
if ((r = process_env_add_system(&env_vars, &env_vars_len)) != 0) {
retval = -1;
push_error(L, "cannot add environment variable", r);
goto cleanup;
}
}
for (int i = 0; i < 3; ++i) { for (int i = 0; i < 3; ++i) {
switch (new_fds[i]) { switch (new_fds[i]) {
case REDIRECT_PARENT: case REDIRECT_PARENT:
@ -458,7 +731,7 @@ static int process_start(lua_State* L) {
self->child_pipes[i][1] = self->child_pipes[new_fds[i]][1]; self->child_pipes[i][1] = self->child_pipes[new_fds[i]][1];
} }
} }
STARTUPINFO siStartInfo; STARTUPINFOW siStartInfo;
memset(&self->process_information, 0, sizeof(self->process_information)); memset(&self->process_information, 0, sizeof(self->process_information));
memset(&siStartInfo, 0, sizeof(siStartInfo)); memset(&siStartInfo, 0, sizeof(siStartInfo));
siStartInfo.cb = sizeof(siStartInfo); siStartInfo.cb = sizeof(siStartInfo);
@ -466,48 +739,10 @@ static int process_start(lua_State* L) {
siStartInfo.hStdInput = self->child_pipes[STDIN_FD][0]; siStartInfo.hStdInput = self->child_pipes[STDIN_FD][0];
siStartInfo.hStdOutput = self->child_pipes[STDOUT_FD][1]; siStartInfo.hStdOutput = self->child_pipes[STDOUT_FD][1];
siStartInfo.hStdError = self->child_pipes[STDERR_FD][1]; siStartInfo.hStdError = self->child_pipes[STDERR_FD][1];
char commandLine[32767] = { 0 }, environmentBlock[32767], wideEnvironmentBlock[32767*2]; wchar_t cwd_w[MAX_PATH];
int offset = 0; if (cwd) // TODO: error handling
if (!literal) { MultiByteToWideChar(CP_UTF8, 0, cwd, -1, cwd_w, MAX_PATH);
for (size_t i = 0; i < cmd_len; ++i) { if (!CreateProcessW(NULL, arglist, NULL, NULL, true, (detach ? DETACHED_PROCESS : CREATE_NO_WINDOW) | CREATE_UNICODE_ENVIRONMENT, env_vars, cwd ? cwd_w : NULL, &siStartInfo, &self->process_information)) {
size_t len = strlen(cmd[i]);
if (offset + len + 2 >= sizeof(commandLine)) break;
if (i > 0)
commandLine[offset++] = ' ';
commandLine[offset++] = '"';
int backslashCount = 0; // Yes, this is necessary.
for (size_t j = 0; j < len && offset + 2 + backslashCount < sizeof(commandLine); ++j) {
if (cmd[i][j] == '\\')
++backslashCount;
else if (cmd[i][j] == '"') {
for (size_t k = 0; k < backslashCount; ++k)
commandLine[offset++] = '\\';
commandLine[offset++] = '\\';
backslashCount = 0;
} else
backslashCount = 0;
commandLine[offset++] = cmd[i][j];
}
if (offset + 1 + backslashCount >= sizeof(commandLine)) break;
for (size_t k = 0; k < backslashCount; ++k)
commandLine[offset++] = '\\';
commandLine[offset++] = '"';
}
commandLine[offset] = 0;
} else {
strncpy(commandLine, cmd[0], sizeof(commandLine));
}
offset = 0;
for (size_t i = 0; i < env_len; ++i) {
if (offset + strlen(env_values[i]) + strlen(env_names[i]) + 1 >= sizeof(environmentBlock))
break;
offset += snprintf(&environmentBlock[offset], sizeof(environmentBlock) - offset, "%s=%s", env_names[i], env_values[i]);
environmentBlock[offset++] = 0;
}
environmentBlock[offset++] = 0;
if (env_len > 0)
MultiByteToWideChar(CP_UTF8, MB_PRECOMPOSED, environmentBlock, offset, (LPWSTR)wideEnvironmentBlock, sizeof(wideEnvironmentBlock));
if (!CreateProcess(NULL, commandLine, NULL, NULL, true, (detach ? DETACHED_PROCESS : CREATE_NO_WINDOW) | CREATE_UNICODE_ENVIRONMENT, env_len > 0 ? wideEnvironmentBlock : NULL, cwd, &siStartInfo, &self->process_information)) {
push_error(L, NULL, GetLastError()); push_error(L, NULL, GetLastError());
retval = -1; retval = -1;
goto cleanup; goto cleanup;
@ -555,9 +790,9 @@ static int process_start(lua_State* L) {
close(self->child_pipes[stream][stream == STDIN_FD ? 1 : 0]); close(self->child_pipes[stream][stream == STDIN_FD ? 1 : 0]);
} }
size_t set; size_t set;
for (set = 0; set < env_len && setenv(env_names[set], env_values[set], 1) == 0; ++set); for (set = 0; set < env_vars_len && setenv(env_vars[set], env_vars[set+1], 1) == 0; set += 2);
if (set == env_len && (!detach || setsid() != -1) && (!cwd || chdir(cwd) != -1)) if (set == env_vars_len && (!detach || setsid() != -1) && (!cwd || chdir(cwd) != -1))
execvp(cmd[0], (char** const)cmd); execvp(arglist[0], (char** const)arglist);
write(control_pipe[1], &errno, sizeof(errno)); write(control_pipe[1], &errno, sizeof(errno));
_exit(-1); _exit(-1);
} }
@ -591,16 +826,15 @@ static int process_start(lua_State* L) {
if (control_pipe[0]) close(control_pipe[0]); if (control_pipe[0]) close(control_pipe[0]);
if (control_pipe[1]) close(control_pipe[1]); if (control_pipe[1]) close(control_pipe[1]);
#endif #endif
for (size_t i = 0; i < env_len; ++i) {
free((char*)env_names[i]);
free((char*)env_values[i]);
}
for (int stream = 0; stream < 3; ++stream) { for (int stream = 0; stream < 3; ++stream) {
process_stream_t* pipe = &self->child_pipes[stream][stream == STDIN_FD ? 0 : 1]; process_stream_t* pipe = &self->child_pipes[stream][stream == STDIN_FD ? 0 : 1];
if (*pipe) { if (*pipe) {
close_fd(pipe); close_fd(pipe);
} }
} }
process_arglist_free(&arglist);
process_env_free(&env_vars);
if (retval == -1) if (retval == -1)
return lua_error(L); return lua_error(L);
@ -741,6 +975,10 @@ static int self_signal(lua_State* L, signal_e sig) {
static int f_terminate(lua_State* L) { return self_signal(L, SIGNAL_TERM); } static int f_terminate(lua_State* L) { return self_signal(L, SIGNAL_TERM); }
static int f_kill(lua_State* L) { return self_signal(L, SIGNAL_KILL); } static int f_kill(lua_State* L) { return self_signal(L, SIGNAL_KILL); }
static int f_interrupt(lua_State* L) { return self_signal(L, SIGNAL_INTERRUPT); } static int f_interrupt(lua_State* L) { return self_signal(L, SIGNAL_INTERRUPT); }
<<<<<<< HEAD
=======
>>>>>>> master
static int f_gc(lua_State* L) { static int f_gc(lua_State* L) {
process_kill_list_t *list = NULL; process_kill_list_t *list = NULL;
process_kill_t *p = NULL; process_kill_t *p = NULL;

View File

@ -90,7 +90,7 @@ static int f_font_load(lua_State *L) {
return ret_code; return ret_code;
RenFont** font = lua_newuserdata(L, sizeof(RenFont*)); RenFont** font = lua_newuserdata(L, sizeof(RenFont*));
*font = ren_font_load(&window_renderer, filename, size, antialiasing, hinting, style); *font = ren_font_load(window_renderer, filename, size, antialiasing, hinting, style);
if (!*font) if (!*font)
return luaL_error(L, "failed to load font"); return luaL_error(L, "failed to load font");
luaL_setmetatable(L, API_TYPE_FONT); luaL_setmetatable(L, API_TYPE_FONT);
@ -130,7 +130,7 @@ static int f_font_copy(lua_State *L) {
} }
for (int i = 0; i < FONT_FALLBACK_MAX && fonts[i]; ++i) { for (int i = 0; i < FONT_FALLBACK_MAX && fonts[i]; ++i) {
RenFont** font = lua_newuserdata(L, sizeof(RenFont*)); RenFont** font = lua_newuserdata(L, sizeof(RenFont*));
*font = ren_font_copy(&window_renderer, fonts[i], size, antialiasing, hinting, style); *font = ren_font_copy(window_renderer, fonts[i], size, antialiasing, hinting, style);
if (!*font) if (!*font)
return luaL_error(L, "failed to copy font"); return luaL_error(L, "failed to copy font");
luaL_setmetatable(L, API_TYPE_FONT); luaL_setmetatable(L, API_TYPE_FONT);
@ -198,7 +198,7 @@ static int f_font_get_width(lua_State *L) {
size_t len; size_t len;
const char *text = luaL_checklstring(L, 2, &len); const char *text = luaL_checklstring(L, 2, &len);
lua_pushnumber(L, ren_font_group_get_width(&window_renderer, fonts, text, len)); lua_pushnumber(L, ren_font_group_get_width(window_renderer, fonts, text, len, NULL));
return 1; return 1;
} }
@ -217,7 +217,7 @@ static int f_font_get_size(lua_State *L) {
static int f_font_set_size(lua_State *L) { static int f_font_set_size(lua_State *L) {
RenFont* fonts[FONT_FALLBACK_MAX]; font_retrieve(L, fonts, 1); RenFont* fonts[FONT_FALLBACK_MAX]; font_retrieve(L, fonts, 1);
float size = luaL_checknumber(L, 2); float size = luaL_checknumber(L, 2);
ren_font_group_set_size(&window_renderer, fonts, size); ren_font_group_set_size(window_renderer, fonts, size);
return 0; return 0;
} }
@ -276,7 +276,7 @@ static int f_show_debug(lua_State *L) {
static int f_get_size(lua_State *L) { static int f_get_size(lua_State *L) {
int w, h; int w, h;
ren_get_size(&window_renderer, &w, &h); ren_get_size(window_renderer, &w, &h);
lua_pushnumber(L, w); lua_pushnumber(L, w);
lua_pushnumber(L, h); lua_pushnumber(L, h);
return 2; return 2;
@ -284,13 +284,13 @@ static int f_get_size(lua_State *L) {
static int f_begin_frame(UNUSED lua_State *L) { static int f_begin_frame(UNUSED lua_State *L) {
rencache_begin_frame(&window_renderer); rencache_begin_frame(window_renderer);
return 0; return 0;
} }
static int f_end_frame(UNUSED lua_State *L) { static int f_end_frame(UNUSED lua_State *L) {
rencache_end_frame(&window_renderer); rencache_end_frame(window_renderer);
// clear the font reference table // clear the font reference table
lua_newtable(L); lua_newtable(L);
lua_rawseti(L, LUA_REGISTRYINDEX, RENDERER_FONT_REF); lua_rawseti(L, LUA_REGISTRYINDEX, RENDERER_FONT_REF);
@ -311,7 +311,7 @@ static int f_set_clip_rect(lua_State *L) {
lua_Number w = luaL_checknumber(L, 3); lua_Number w = luaL_checknumber(L, 3);
lua_Number h = luaL_checknumber(L, 4); lua_Number h = luaL_checknumber(L, 4);
RenRect rect = rect_to_grid(x, y, w, h); RenRect rect = rect_to_grid(x, y, w, h);
rencache_set_clip_rect(&window_renderer, rect); rencache_set_clip_rect(window_renderer, rect);
return 0; return 0;
} }
@ -323,7 +323,7 @@ static int f_draw_rect(lua_State *L) {
lua_Number h = luaL_checknumber(L, 4); lua_Number h = luaL_checknumber(L, 4);
RenRect rect = rect_to_grid(x, y, w, h); RenRect rect = rect_to_grid(x, y, w, h);
RenColor color = checkcolor(L, 5, 255); RenColor color = checkcolor(L, 5, 255);
rencache_draw_rect(&window_renderer, rect, color); rencache_draw_rect(window_renderer, rect, color);
return 0; return 0;
} }
@ -348,7 +348,7 @@ static int f_draw_text(lua_State *L) {
double x = luaL_checknumber(L, 3); double x = luaL_checknumber(L, 3);
int y = luaL_checknumber(L, 4); int y = luaL_checknumber(L, 4);
RenColor color = checkcolor(L, 5, 255); RenColor color = checkcolor(L, 5, 255);
x = rencache_draw_text(&window_renderer, fonts, text, len, x, y, color); x = rencache_draw_text(window_renderer, fonts, text, len, x, y, color);
lua_pushnumber(L, x); lua_pushnumber(L, x);
return 1; return 1;
} }

View File

@ -78,7 +78,7 @@ static SDL_HitTestResult SDLCALL hit_test(SDL_Window *window, const SDL_Point *p
const int controls_width = hit_info->controls_width; const int controls_width = hit_info->controls_width;
int w, h; int w, h;
SDL_GetWindowSize(window_renderer.window, &w, &h); SDL_GetWindowSize(window_renderer->window, &w, &h);
if (pt->y < hit_info->title_height && if (pt->y < hit_info->title_height &&
#if RESIZE_FROM_TOP #if RESIZE_FROM_TOP
@ -197,7 +197,7 @@ top:
case SDL_WINDOWEVENT: case SDL_WINDOWEVENT:
if (e.window.event == SDL_WINDOWEVENT_RESIZED) { if (e.window.event == SDL_WINDOWEVENT_RESIZED) {
ren_resize_window(&window_renderer); ren_resize_window(window_renderer);
lua_pushstring(L, "resized"); lua_pushstring(L, "resized");
/* The size below will be in points. */ /* The size below will be in points. */
lua_pushinteger(L, e.window.data1); lua_pushinteger(L, e.window.data1);
@ -236,8 +236,8 @@ top:
SDL_GetMouseState(&mx, &my); SDL_GetMouseState(&mx, &my);
lua_pushstring(L, "filedropped"); lua_pushstring(L, "filedropped");
lua_pushstring(L, e.drop.file); lua_pushstring(L, e.drop.file);
lua_pushinteger(L, mx); lua_pushinteger(L, mx * window_renderer->scale_x);
lua_pushinteger(L, my); lua_pushinteger(L, my * window_renderer->scale_y);
SDL_free(e.drop.file); SDL_free(e.drop.file);
return 4; return 4;
@ -294,8 +294,8 @@ top:
if (e.button.button == 1) { SDL_CaptureMouse(1); } if (e.button.button == 1) { SDL_CaptureMouse(1); }
lua_pushstring(L, "mousepressed"); lua_pushstring(L, "mousepressed");
lua_pushstring(L, button_name(e.button.button)); lua_pushstring(L, button_name(e.button.button));
lua_pushinteger(L, e.button.x); lua_pushinteger(L, e.button.x * window_renderer->scale_x);
lua_pushinteger(L, e.button.y); lua_pushinteger(L, e.button.y * window_renderer->scale_y);
lua_pushinteger(L, e.button.clicks); lua_pushinteger(L, e.button.clicks);
return 5; return 5;
@ -303,8 +303,8 @@ top:
if (e.button.button == 1) { SDL_CaptureMouse(0); } if (e.button.button == 1) { SDL_CaptureMouse(0); }
lua_pushstring(L, "mousereleased"); lua_pushstring(L, "mousereleased");
lua_pushstring(L, button_name(e.button.button)); lua_pushstring(L, button_name(e.button.button));
lua_pushinteger(L, e.button.x); lua_pushinteger(L, e.button.x * window_renderer->scale_x);
lua_pushinteger(L, e.button.y); lua_pushinteger(L, e.button.y * window_renderer->scale_y);
return 4; return 4;
case SDL_MOUSEMOTION: case SDL_MOUSEMOTION:
@ -316,10 +316,10 @@ top:
e.motion.yrel += event_plus.motion.yrel; e.motion.yrel += event_plus.motion.yrel;
} }
lua_pushstring(L, "mousemoved"); lua_pushstring(L, "mousemoved");
lua_pushinteger(L, e.motion.x); lua_pushinteger(L, e.motion.x * window_renderer->scale_x);
lua_pushinteger(L, e.motion.y); lua_pushinteger(L, e.motion.y * window_renderer->scale_y);
lua_pushinteger(L, e.motion.xrel); lua_pushinteger(L, e.motion.xrel * window_renderer->scale_x);
lua_pushinteger(L, e.motion.yrel); lua_pushinteger(L, e.motion.yrel * window_renderer->scale_y);
return 5; return 5;
case SDL_MOUSEWHEEL: case SDL_MOUSEWHEEL:
@ -335,7 +335,7 @@ top:
return 3; return 3;
case SDL_FINGERDOWN: case SDL_FINGERDOWN:
SDL_GetWindowSize(window_renderer.window, &w, &h); SDL_GetWindowSize(window_renderer->window, &w, &h);
lua_pushstring(L, "touchpressed"); lua_pushstring(L, "touchpressed");
lua_pushinteger(L, (lua_Integer)(e.tfinger.x * w)); lua_pushinteger(L, (lua_Integer)(e.tfinger.x * w));
@ -344,7 +344,7 @@ top:
return 4; return 4;
case SDL_FINGERUP: case SDL_FINGERUP:
SDL_GetWindowSize(window_renderer.window, &w, &h); SDL_GetWindowSize(window_renderer->window, &w, &h);
lua_pushstring(L, "touchreleased"); lua_pushstring(L, "touchreleased");
lua_pushinteger(L, (lua_Integer)(e.tfinger.x * w)); lua_pushinteger(L, (lua_Integer)(e.tfinger.x * w));
@ -360,7 +360,7 @@ top:
e.tfinger.dx += event_plus.tfinger.dx; e.tfinger.dx += event_plus.tfinger.dx;
e.tfinger.dy += event_plus.tfinger.dy; e.tfinger.dy += event_plus.tfinger.dy;
} }
SDL_GetWindowSize(window_renderer.window, &w, &h); SDL_GetWindowSize(window_renderer->window, &w, &h);
lua_pushstring(L, "touchmoved"); lua_pushstring(L, "touchmoved");
lua_pushinteger(L, (lua_Integer)(e.tfinger.x * w)); lua_pushinteger(L, (lua_Integer)(e.tfinger.x * w));
@ -374,7 +374,7 @@ top:
#ifdef LITE_USE_SDL_RENDERER #ifdef LITE_USE_SDL_RENDERER
rencache_invalidate(); rencache_invalidate();
#else #else
SDL_UpdateWindowSurface(window_renderer.window); SDL_UpdateWindowSurface(window_renderer->window);
#endif #endif
lua_pushstring(L, e.type == SDL_APP_WILLENTERFOREGROUND ? "enteringforeground" : "enteredforeground"); lua_pushstring(L, e.type == SDL_APP_WILLENTERFOREGROUND ? "enteringforeground" : "enteredforeground");
return 1; return 1;
@ -397,6 +397,7 @@ static int f_wait_event(lua_State *L) {
int nargs = lua_gettop(L); int nargs = lua_gettop(L);
if (nargs >= 1) { if (nargs >= 1) {
double n = luaL_checknumber(L, 1); double n = luaL_checknumber(L, 1);
if (n < 0) n = 0;
lua_pushboolean(L, SDL_WaitEventTimeout(NULL, n * 1000)); lua_pushboolean(L, SDL_WaitEventTimeout(NULL, n * 1000));
} else { } else {
lua_pushboolean(L, SDL_WaitEvent(NULL)); lua_pushboolean(L, SDL_WaitEvent(NULL));
@ -439,7 +440,7 @@ static int f_set_cursor(lua_State *L) {
static int f_set_window_title(lua_State *L) { static int f_set_window_title(lua_State *L) {
const char *title = luaL_checkstring(L, 1); const char *title = luaL_checkstring(L, 1);
SDL_SetWindowTitle(window_renderer.window, title); SDL_SetWindowTitle(window_renderer->window, title);
return 0; return 0;
} }
@ -449,39 +450,39 @@ enum { WIN_NORMAL, WIN_MINIMIZED, WIN_MAXIMIZED, WIN_FULLSCREEN };
static int f_set_window_mode(lua_State *L) { static int f_set_window_mode(lua_State *L) {
int n = luaL_checkoption(L, 1, "normal", window_opts); int n = luaL_checkoption(L, 1, "normal", window_opts);
SDL_SetWindowFullscreen(window_renderer.window, SDL_SetWindowFullscreen(window_renderer->window,
n == WIN_FULLSCREEN ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0); n == WIN_FULLSCREEN ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0);
if (n == WIN_NORMAL) { SDL_RestoreWindow(window_renderer.window); } if (n == WIN_NORMAL) { SDL_RestoreWindow(window_renderer->window); }
if (n == WIN_MAXIMIZED) { SDL_MaximizeWindow(window_renderer.window); } if (n == WIN_MAXIMIZED) { SDL_MaximizeWindow(window_renderer->window); }
if (n == WIN_MINIMIZED) { SDL_MinimizeWindow(window_renderer.window); } if (n == WIN_MINIMIZED) { SDL_MinimizeWindow(window_renderer->window); }
return 0; return 0;
} }
static int f_set_window_bordered(lua_State *L) { static int f_set_window_bordered(lua_State *L) {
int bordered = lua_toboolean(L, 1); int bordered = lua_toboolean(L, 1);
SDL_SetWindowBordered(window_renderer.window, bordered); SDL_SetWindowBordered(window_renderer->window, bordered);
return 0; return 0;
} }
static int f_set_window_hit_test(lua_State *L) { static int f_set_window_hit_test(lua_State *L) {
if (lua_gettop(L) == 0) { if (lua_gettop(L) == 0) {
SDL_SetWindowHitTest(window_renderer.window, NULL, NULL); SDL_SetWindowHitTest(window_renderer->window, NULL, NULL);
return 0; return 0;
} }
window_hit_info->title_height = luaL_checknumber(L, 1); window_hit_info->title_height = luaL_checknumber(L, 1);
window_hit_info->controls_width = luaL_checknumber(L, 2); window_hit_info->controls_width = luaL_checknumber(L, 2);
window_hit_info->resize_border = luaL_checknumber(L, 3); window_hit_info->resize_border = luaL_checknumber(L, 3);
SDL_SetWindowHitTest(window_renderer.window, hit_test, window_hit_info); SDL_SetWindowHitTest(window_renderer->window, hit_test, window_hit_info);
return 0; return 0;
} }
static int f_get_window_size(lua_State *L) { static int f_get_window_size(lua_State *L) {
int x, y, w, h; int x, y, w, h;
SDL_GetWindowSize(window_renderer.window, &w, &h); SDL_GetWindowSize(window_renderer->window, &w, &h);
SDL_GetWindowPosition(window_renderer.window, &x, &y); SDL_GetWindowPosition(window_renderer->window, &x, &y);
lua_pushinteger(L, w); lua_pushinteger(L, w);
lua_pushinteger(L, h); lua_pushinteger(L, h);
lua_pushinteger(L, x); lua_pushinteger(L, x);
@ -495,22 +496,22 @@ static int f_set_window_size(lua_State *L) {
double h = luaL_checknumber(L, 2); double h = luaL_checknumber(L, 2);
double x = luaL_checknumber(L, 3); double x = luaL_checknumber(L, 3);
double y = luaL_checknumber(L, 4); double y = luaL_checknumber(L, 4);
SDL_SetWindowSize(window_renderer.window, w, h); SDL_SetWindowSize(window_renderer->window, w, h);
SDL_SetWindowPosition(window_renderer.window, x, y); SDL_SetWindowPosition(window_renderer->window, x, y);
ren_resize_window(&window_renderer); ren_resize_window(window_renderer);
return 0; return 0;
} }
static int f_window_has_focus(lua_State *L) { static int f_window_has_focus(lua_State *L) {
unsigned flags = SDL_GetWindowFlags(window_renderer.window); unsigned flags = SDL_GetWindowFlags(window_renderer->window);
lua_pushboolean(L, flags & SDL_WINDOW_INPUT_FOCUS); lua_pushboolean(L, flags & SDL_WINDOW_INPUT_FOCUS);
return 1; return 1;
} }
static int f_get_window_mode(lua_State *L) { static int f_get_window_mode(lua_State *L) {
unsigned flags = SDL_GetWindowFlags(window_renderer.window); unsigned flags = SDL_GetWindowFlags(window_renderer->window);
if (flags & SDL_WINDOW_FULLSCREEN_DESKTOP) { if (flags & SDL_WINDOW_FULLSCREEN_DESKTOP) {
lua_pushstring(L, "fullscreen"); lua_pushstring(L, "fullscreen");
} else if (flags & SDL_WINDOW_MINIMIZED) { } else if (flags & SDL_WINDOW_MINIMIZED) {
@ -548,8 +549,8 @@ static int f_raise_window(lua_State *L) {
to allow the window to be focused. Also on wayland the raise window event to allow the window to be focused. Also on wayland the raise window event
may not always be obeyed. may not always be obeyed.
*/ */
SDL_SetWindowInputFocus(window_renderer.window); SDL_SetWindowInputFocus(window_renderer->window);
SDL_RaiseWindow(window_renderer.window); SDL_RaiseWindow(window_renderer->window);
return 0; return 0;
} }
@ -876,6 +877,7 @@ static int f_get_time(lua_State *L) {
static int f_sleep(lua_State *L) { static int f_sleep(lua_State *L) {
double n = luaL_checknumber(L, 1); double n = luaL_checknumber(L, 1);
if (n < 0) n = 0;
SDL_Delay(n * 1000); SDL_Delay(n * 1000);
return 0; return 0;
} }
@ -929,7 +931,7 @@ static int f_fuzzy_match(lua_State *L) {
static int f_set_window_opacity(lua_State *L) { static int f_set_window_opacity(lua_State *L) {
double n = luaL_checknumber(L, 1); double n = luaL_checknumber(L, 1);
int r = SDL_SetWindowOpacity(window_renderer.window, n); int r = SDL_SetWindowOpacity(window_renderer->window, n);
lua_pushboolean(L, r > -1); lua_pushboolean(L, r > -1);
return 1; return 1;
} }
@ -1071,7 +1073,7 @@ static int f_load_native_plugin(lua_State *L) {
#endif #endif
/* Special purpose filepath compare function. Corresponds to the /* Special purpose filepath compare function. Corresponds to the
order used in the TreeView view of the project's files. Returns true iff order used in the TreeView view of the project's files. Returns true if
path1 < path2 in the TreeView order. */ path1 < path2 in the TreeView order. */
static int f_path_compare(lua_State *L) { static int f_path_compare(lua_State *L) {
size_t len1, len2; size_t len1, len2;

View File

@ -35,16 +35,6 @@
static SDL_Window *window; static SDL_Window *window;
static double get_scale(void) {
#ifndef __APPLE__
float dpi;
if (SDL_GetDisplayDPI(0, NULL, &dpi, NULL) == 0)
return dpi / 96.0;
#endif
return 1.0;
}
static void get_exe_filename(char *buf, int sz) { static void get_exe_filename(char *buf, int sz) {
#if _WIN32 #if _WIN32
int len; int len;
@ -204,6 +194,8 @@ int main(int argc, char **argv) {
SDL_SetHint("SDL_MOUSE_DOUBLE_CLICK_RADIUS", "4"); SDL_SetHint("SDL_MOUSE_DOUBLE_CLICK_RADIUS", "4");
#endif #endif
SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software");
SDL_DisplayMode dm; SDL_DisplayMode dm;
SDL_GetCurrentDisplayMode(0, &dm); SDL_GetCurrentDisplayMode(0, &dm);
@ -217,7 +209,7 @@ int main(int argc, char **argv) {
fprintf(stderr, "Error creating lite-xl window: %s", SDL_GetError()); fprintf(stderr, "Error creating lite-xl window: %s", SDL_GetError());
exit(1); exit(1);
} }
ren_init(window); window_renderer = ren_init(window);
lua_State *L; lua_State *L;
init_lua: init_lua:
@ -239,9 +231,6 @@ init_lua:
lua_pushstring(L, LITE_ARCH_TUPLE); lua_pushstring(L, LITE_ARCH_TUPLE);
lua_setglobal(L, "ARCH"); lua_setglobal(L, "ARCH");
lua_pushnumber(L, get_scale());
lua_setglobal(L, "SCALE");
char exename[2048]; char exename[2048];
get_exe_filename(exename, sizeof(exename)); get_exe_filename(exename, sizeof(exename));
if (*exename) { if (*exename) {
@ -314,7 +303,7 @@ init_lua:
// This allows the window to be destroyed before lite-xl is done with // This allows the window to be destroyed before lite-xl is done with
// reaping child processes // reaping child processes
ren_free_window_resources(&window_renderer); ren_free(window_renderer);
lua_close(L); lua_close(L);
#if defined(__amigaos4__) #if defined(__amigaos4__)

View File

@ -191,8 +191,9 @@ void rencache_draw_rect(RenWindow *window_renderer, RenRect rect, RenColor color
double rencache_draw_text(RenWindow *window_renderer, RenFont **fonts, const char *text, size_t len, double x, int y, RenColor color) double rencache_draw_text(RenWindow *window_renderer, RenFont **fonts, const char *text, size_t len, double x, int y, RenColor color)
{ {
double width = ren_font_group_get_width(window_renderer, fonts, text, len); int x_offset;
RenRect rect = { x, y, (int)width, ren_font_group_get_height(fonts) }; double width = ren_font_group_get_width(window_renderer, fonts, text, len, &x_offset);
RenRect rect = { x + x_offset, y, (int)(width - x_offset), ren_font_group_get_height(fonts) };
if (rects_overlap(last_clip_rect, rect)) { if (rects_overlap(last_clip_rect, rect)) {
int sz = len + 1; int sz = len + 1;
DrawTextCommand *cmd = push_command(window_renderer, DRAW_TEXT, sizeof(DrawTextCommand) + sz); DrawTextCommand *cmd = push_command(window_renderer, DRAW_TEXT, sizeof(DrawTextCommand) + sz);

View File

@ -22,7 +22,7 @@
#define MAX_LOADABLE_GLYPHSETS (MAX_UNICODE / GLYPHSET_SIZE) #define MAX_LOADABLE_GLYPHSETS (MAX_UNICODE / GLYPHSET_SIZE)
#define SUBPIXEL_BITMAPS_CACHED 3 #define SUBPIXEL_BITMAPS_CACHED 3
RenWindow window_renderer = {0}; RenWindow* window_renderer = NULL;
static FT_Library library; static FT_Library library;
// draw_rect_surface is used as a 1x1 surface to simplify ren_draw_rect with blending // draw_rect_surface is used as a 1x1 surface to simplify ren_draw_rect with blending
@ -167,7 +167,7 @@ static void font_load_glyphset(RenFont* font, int idx) {
for (unsigned int column = 0; column < slot->bitmap.width; ++column) { for (unsigned int column = 0; column < slot->bitmap.width; ++column) {
int current_source_offset = source_offset + (column / 8); int current_source_offset = source_offset + (column / 8);
int source_pixel = slot->bitmap.buffer[current_source_offset]; int source_pixel = slot->bitmap.buffer[current_source_offset];
pixels[++target_offset] = ((source_pixel >> (7 - (column % 8))) & 0x1) << 7; pixels[++target_offset] = ((source_pixel >> (7 - (column % 8))) & 0x1) * 0xFF;
} }
} else } else
memcpy(&pixels[target_offset], &slot->bitmap.buffer[source_offset], slot->bitmap.width); memcpy(&pixels[target_offset], &slot->bitmap.buffer[source_offset], slot->bitmap.width);
@ -348,10 +348,11 @@ int ren_font_group_get_height(RenFont **fonts) {
return fonts[0]->height; return fonts[0]->height;
} }
double ren_font_group_get_width(RenWindow *window_renderer, RenFont **fonts, const char *text, size_t len) { double ren_font_group_get_width(RenWindow *window_renderer, RenFont **fonts, const char *text, size_t len, int *x_offset) {
double width = 0; double width = 0;
const char* end = text + len; const char* end = text + len;
GlyphMetric* metric = NULL; GlyphSet* set = NULL; GlyphMetric* metric = NULL; GlyphSet* set = NULL;
bool set_x_offset = x_offset == NULL;
while (text < end) { while (text < end) {
unsigned int codepoint; unsigned int codepoint;
text = utf8_to_codepoint(text, &codepoint); text = utf8_to_codepoint(text, &codepoint);
@ -359,8 +360,15 @@ double ren_font_group_get_width(RenWindow *window_renderer, RenFont **fonts, con
if (!metric) if (!metric)
break; break;
width += (!font || metric->xadvance) ? metric->xadvance : fonts[0]->space_advance; width += (!font || metric->xadvance) ? metric->xadvance : fonts[0]->space_advance;
if (!set_x_offset) {
set_x_offset = true;
*x_offset = metric->bitmap_left; // TODO: should this be scaled by the surface scale?
}
} }
const int surface_scale = renwin_get_surface(window_renderer).scale; const int surface_scale = renwin_get_surface(window_renderer).scale;
if (!set_x_offset) {
*x_offset = 0;
}
return width / surface_scale; return width / surface_scale;
} }
@ -493,33 +501,38 @@ void ren_draw_rect(RenSurface *rs, RenRect rect, RenColor color) {
} }
/*************** Window Management ****************/ /*************** Window Management ****************/
void ren_free_window_resources(RenWindow *window_renderer) { RenWindow* ren_init(SDL_Window *win) {
assert(win);
int error = FT_Init_FreeType( &library );
if ( error ) {
fprintf(stderr, "internal font error when starting the application\n");
return NULL;
}
RenWindow* window_renderer = malloc(sizeof(RenWindow));
window_renderer->window = win;
renwin_init_surface(window_renderer);
renwin_init_command_buf(window_renderer);
renwin_clip_to_surface(window_renderer);
draw_rect_surface = SDL_CreateRGBSurface(0, 1, 1, 32,
0xFF000000, 0x00FF0000, 0x0000FF00, 0x000000FF);
return window_renderer;
}
void ren_free(RenWindow* window_renderer) {
assert(window_renderer);
renwin_free(window_renderer); renwin_free(window_renderer);
SDL_FreeSurface(draw_rect_surface); SDL_FreeSurface(draw_rect_surface);
free(window_renderer->command_buf); free(window_renderer->command_buf);
window_renderer->command_buf = NULL; window_renderer->command_buf = NULL;
window_renderer->command_buf_size = 0; window_renderer->command_buf_size = 0;
free(window_renderer);
} }
// TODO remove global and return RenWindow*
void ren_init(SDL_Window *win) {
assert(win);
int error = FT_Init_FreeType( &library );
if ( error ) {
fprintf(stderr, "internal font error when starting the application\n");
return;
}
window_renderer.window = win;
renwin_init_surface(&window_renderer);
renwin_init_command_buf(&window_renderer);
renwin_clip_to_surface(&window_renderer);
draw_rect_surface = SDL_CreateRGBSurface(0, 1, 1, 32,
0xFF000000, 0x00FF0000, 0x0000FF00, 0x000000FF);
}
void ren_resize_window(RenWindow *window_renderer) { void ren_resize_window(RenWindow *window_renderer) {
renwin_resize_surface(window_renderer); renwin_resize_surface(window_renderer);
renwin_update_scale(window_renderer);
} }

View File

@ -23,7 +23,7 @@ typedef struct { SDL_Surface *surface; int scale; } RenSurface;
struct RenWindow; struct RenWindow;
typedef struct RenWindow RenWindow; typedef struct RenWindow RenWindow;
extern RenWindow window_renderer; extern RenWindow* window_renderer;
RenFont* ren_font_load(RenWindow *window_renderer, const char *filename, float size, ERenFontAntialiasing antialiasing, ERenFontHinting hinting, unsigned char style); RenFont* ren_font_load(RenWindow *window_renderer, const char *filename, float size, ERenFontAntialiasing antialiasing, ERenFontHinting hinting, unsigned char style);
RenFont* ren_font_copy(RenWindow *window_renderer, RenFont* font, float size, ERenFontAntialiasing antialiasing, ERenFontHinting hinting, int style); RenFont* ren_font_copy(RenWindow *window_renderer, RenFont* font, float size, ERenFontAntialiasing antialiasing, ERenFontHinting hinting, int style);
@ -34,17 +34,17 @@ int ren_font_group_get_height(RenFont **font);
float ren_font_group_get_size(RenFont **font); float ren_font_group_get_size(RenFont **font);
void ren_font_group_set_size(RenWindow *window_renderer, RenFont **font, float size); void ren_font_group_set_size(RenWindow *window_renderer, RenFont **font, float size);
void ren_font_group_set_tab_size(RenFont **font, int n); void ren_font_group_set_tab_size(RenFont **font, int n);
double ren_font_group_get_width(RenWindow *window_renderer, RenFont **font, const char *text, size_t len); double ren_font_group_get_width(RenWindow *window_renderer, RenFont **font, const char *text, size_t len, int *x_offset);
double ren_draw_text(RenSurface *rs, RenFont **font, const char *text, size_t len, float x, int y, RenColor color); double ren_draw_text(RenSurface *rs, RenFont **font, const char *text, size_t len, float x, int y, RenColor color);
void ren_draw_rect(RenSurface *rs, RenRect rect, RenColor color); void ren_draw_rect(RenSurface *rs, RenRect rect, RenColor color);
void ren_init(SDL_Window *win); RenWindow* ren_init(SDL_Window *win);
void ren_free(RenWindow* window_renderer);
void ren_resize_window(RenWindow *window_renderer); void ren_resize_window(RenWindow *window_renderer);
void ren_update_rects(RenWindow *window_renderer, RenRect *rects, int count); void ren_update_rects(RenWindow *window_renderer, RenRect *rects, int count);
void ren_set_clip_rect(RenWindow *window_renderer, RenRect rect); void ren_set_clip_rect(RenWindow *window_renderer, RenRect rect);
void ren_get_size(RenWindow *window_renderer, int *x, int *y); /* Reports the size in points. */ void ren_get_size(RenWindow *window_renderer, int *x, int *y); /* Reports the size in points. */
void ren_free_window_resources(RenWindow *window_renderer);
#endif #endif

View File

@ -41,7 +41,8 @@ static void setup_renderer(RenWindow *ren, int w, int h) {
#endif #endif
void renwin_init_surface(UNUSED RenWindow *ren) { void renwin_init_surface(RenWindow *ren) {
ren->scale_x = ren->scale_y = 1;
#ifdef LITE_USE_SDL_RENDERER #ifdef LITE_USE_SDL_RENDERER
if (ren->rensurface.surface) { if (ren->rensurface.surface) {
SDL_FreeSurface(ren->rensurface.surface); SDL_FreeSurface(ren->rensurface.surface);
@ -107,6 +108,16 @@ void renwin_resize_surface(UNUSED RenWindow *ren) {
#endif #endif
} }
void renwin_update_scale(RenWindow *ren) {
#ifndef LITE_USE_SDL_RENDERER
SDL_Surface *surface = SDL_GetWindowSurface(ren->window);
int window_w = surface->w, window_h = surface->h;
SDL_GetWindowSize(ren->window, &window_w, &window_h);
ren->scale_x = (float)surface->w / window_w;
ren->scale_y = (float)surface->h / window_h;
#endif
}
void renwin_show_window(RenWindow *ren) { void renwin_show_window(RenWindow *ren) {
SDL_ShowWindow(ren->window); SDL_ShowWindow(ren->window);
} }

View File

@ -6,6 +6,8 @@ struct RenWindow {
uint8_t *command_buf; uint8_t *command_buf;
size_t command_buf_idx; size_t command_buf_idx;
size_t command_buf_size; size_t command_buf_size;
float scale_x;
float scale_y;
#ifdef LITE_USE_SDL_RENDERER #ifdef LITE_USE_SDL_RENDERER
SDL_Renderer *renderer; SDL_Renderer *renderer;
SDL_Texture *texture; SDL_Texture *texture;
@ -19,6 +21,7 @@ void renwin_init_command_buf(RenWindow *ren);
void renwin_clip_to_surface(RenWindow *ren); void renwin_clip_to_surface(RenWindow *ren);
void renwin_set_clip_rect(RenWindow *ren, RenRect rect); void renwin_set_clip_rect(RenWindow *ren, RenRect rect);
void renwin_resize_surface(RenWindow *ren); void renwin_resize_surface(RenWindow *ren);
void renwin_update_scale(RenWindow *ren);
void renwin_show_window(RenWindow *ren); void renwin_show_window(RenWindow *ren);
void renwin_update_rects(RenWindow *ren, RenRect *rects, int count); void renwin_update_rects(RenWindow *ren, RenRect *rects, int count);
void renwin_free(RenWindow *ren); void renwin_free(RenWindow *ren);

View File

@ -1,9 +1,10 @@
[wrap-file] [wrap-file]
directory = freetype-2.12.1 directory = freetype-2.13.2
source_url = https://download.savannah.gnu.org/releases/freetype/freetype-2.12.1.tar.xz source_url = https://download.savannah.gnu.org/releases/freetype/freetype-2.13.2.tar.xz
source_filename = freetype-2.12.1.tar.xz source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/freetype2_2.13.2-1/freetype-2.13.2.tar.xz
source_hash = 4766f20157cc4cf0cd292f80bf917f92d1c439b243ac3018debf6b9140c41a7f source_filename = freetype-2.13.2.tar.xz
wrapdb_version = 2.12.1-2 source_hash = 12991c4e55c506dd7f9b765933e62fd2be2e06d421505d7950a132e4f1bb484d
wrapdb_version = 2.13.2-1
[provide] [provide]
freetype2 = freetype_dep freetype2 = freetype_dep

View File

@ -1,12 +1,14 @@
[wrap-file] [wrap-file]
directory = lua-5.4.4 directory = lua-5.4.6
source_url = https://www.lua.org/ftp/lua-5.4.4.tar.gz source_url = https://www.lua.org/ftp/lua-5.4.6.tar.gz
source_filename = lua-5.4.4.tar.gz source_filename = lua-5.4.6.tar.gz
source_hash = 164c7849653b80ae67bec4b7473b884bf5cc8d2dca05653475ec2ed27b9ebf61 source_hash = 7d5ea1b9cb6aa0b59ca3dde1c6adcb57ef83a1ba8e5432c0ecd06bf439b3ad88
patch_filename = lua_5.4.4-1_patch.zip patch_filename = lua_5.4.6-3_patch.zip
patch_url = https://wrapdb.mesonbuild.com/v2/lua_5.4.4-1/get_patch patch_url = https://wrapdb.mesonbuild.com/v2/lua_5.4.6-3/get_patch
patch_hash = e61cd965c629d6543176f41a9f1cb9050edfd1566cf00ce768ff211086e40bdc patch_hash = 9b72a95422fd47f79f969d9abdb589ee95712d5512a5246f94e7e4f63d2cb7b7
source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/lua_5.4.6-3/lua-5.4.6.tar.gz
wrapdb_version = 5.4.6-3
[provide] [provide]
lua-5.4 = lua_dep lua-5.4 = lua_dep
lua = lua_dep

View File

@ -1,12 +1,13 @@
[wrap-file] [wrap-file]
directory = pcre2-10.42 directory = pcre2-10.42
source_url = https://github.com/PhilipHazel/pcre2/releases/download/pcre2-10.42/pcre2-10.42.tar.bz2 source_url = https://github.com/PCRE2Project/pcre2/releases/download/pcre2-10.42/pcre2-10.42.tar.bz2
source_filename = pcre2-10.42.tar.bz2 source_filename = pcre2-10.42.tar.bz2
source_hash = 8d36cd8cb6ea2a4c2bb358ff6411b0c788633a2a45dabbf1aeb4b701d1b5e840 source_hash = 8d36cd8cb6ea2a4c2bb358ff6411b0c788633a2a45dabbf1aeb4b701d1b5e840
patch_filename = pcre2_10.42-1_patch.zip patch_filename = pcre2_10.42-5_patch.zip
patch_url = https://wrapdb.mesonbuild.com/v2/pcre2_10.42-1/get_patch patch_url = https://wrapdb.mesonbuild.com/v2/pcre2_10.42-5/get_patch
patch_hash = 06969e916dfee663c189810df57d98574f15e0754a44cd93f3f0bc7234b05d89 patch_hash = 7ba1730a3786c46f41735658a9884b09bc592af3840716e0ccc552e7ddf5630c
wrapdb_version = 10.42-1 source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/pcre2_10.42-5/pcre2-10.42.tar.bz2
wrapdb_version = 10.42-5
[provide] [provide]
libpcre2-8 = libpcre2_8 libpcre2-8 = libpcre2_8

View File

@ -1,12 +1,15 @@
[wrap-file] [wrap-file]
directory = SDL2-2.26.0 directory = SDL2-2.28.1
source_url = https://github.com/libsdl-org/SDL/releases/download/release-2.26.0/SDL2-2.26.0.tar.gz source_url = https://github.com/libsdl-org/SDL/releases/download/release-2.28.1/SDL2-2.28.1.tar.gz
source_filename = SDL2-2.26.0.tar.gz source_filename = SDL2-2.28.1.tar.gz
source_hash = 8000d7169febce93c84b6bdf376631f8179132fd69f7015d4dadb8b9c2bdb295 source_hash = 4977ceba5c0054dbe6c2f114641aced43ce3bf2b41ea64b6a372d6ba129cb15d
patch_filename = sdl2_2.26.0-1_patch.zip patch_filename = sdl2_2.28.1-2_patch.zip
patch_url = https://wrapdb.mesonbuild.com/v2/sdl2_2.26.0-1/get_patch patch_url = https://wrapdb.mesonbuild.com/v2/sdl2_2.28.1-2/get_patch
patch_hash = 6fcfd727d71cf7837332723518d5e47ffd64f1e7630681cf4b50e99f2bf7676f patch_hash = 2dd332226ba2a4373c6d4eb29fa915e9d5414cf7bb9fa2e4a5ef3b16a06e2736
wrapdb_version = 2.26.0-1 source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/sdl2_2.28.1-2/SDL2-2.28.1.tar.gz
wrapdb_version = 2.28.1-2
[provide] [provide]
sdl2 = sdl2_dep sdl2 = sdl2_dep
sdl2main = sdl2main_dep
sdl2_test = sdl2_test_dep