diff --git a/CMakeLists.txt b/CMakeLists.txt index 7f5dde9e..82f1ffe7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,8 +16,14 @@ list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/src") project(tangara) +# /lua partition on internal flash, for storing the lua application spiffs_create_partition_image(lua lua FLASH_IN_PROJECT) +# /repl partition on internal flash, for storing the developer repl and its deps. +file(COPY lib/lua-repl/repl DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/repl) +file(COPY lib/lua-term/term DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/repl) +spiffs_create_partition_image(repl ${CMAKE_CURRENT_BINARY_DIR}/repl FLASH_IN_PROJECT) + add_custom_target(check-for-sdkconfig-changes ALL COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/tools/cmake/check-for-sdkconfig-changes.sh ) diff --git a/lib/lua-linenoise/.gitignore b/lib/lua-linenoise/.gitignore new file mode 100644 index 00000000..37458033 --- /dev/null +++ b/lib/lua-linenoise/.gitignore @@ -0,0 +1,13 @@ +*.o +*.so +*.dylib +*.rock +*.dll +*.def +*.exp +*.lib +*.obj +history.txt + +VSProj/Release/*.* +VSProj/Debug/*.* diff --git a/lib/lua-linenoise/CMakeLists.txt b/lib/lua-linenoise/CMakeLists.txt new file mode 100644 index 00000000..eb50ba80 --- /dev/null +++ b/lib/lua-linenoise/CMakeLists.txt @@ -0,0 +1,4 @@ +idf_component_register( + SRCS "linenoise.c" "encodings/utf8.c" + INCLUDE_DIRS "." + REQUIRES "console" "esp-idf-lua") diff --git a/lib/lua-linenoise/COPYING b/lib/lua-linenoise/COPYING new file mode 100644 index 00000000..28b41f02 --- /dev/null +++ b/lib/lua-linenoise/COPYING @@ -0,0 +1,18 @@ +Copyright (c) 2011-2015 Rob Hoelz + +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 the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/lib/lua-linenoise/Changes b/lib/lua-linenoise/Changes new file mode 100644 index 00000000..d8566b06 --- /dev/null +++ b/lib/lua-linenoise/Changes @@ -0,0 +1,24 @@ +0.9 2018 Apr 04 + - Add syntactic sugar for addcompletion (thanks, Natanael Copa!) + - Add bindings for new linenoise functions: setmultiline, sethintscallback, printkeycodes (GH #16, GH #17) + - Propagate errors in completion callback (GH #14) + - Remove ability to use custom linenoise library (explanation in fa53845) + - Unicode 9.0 support (thanks, Alec Larson!) + - UTF8 support (thanks, Alec Larson!) + - ANSI support (GH #15 - thanks, Alec Larson!) + +0.8 2015 Apr 29 + - Fix memory leak (thanks, Dirk Feytons!) + +0.7 2015 Feb 24 + - Bump linenoise version + +0.4 2012 Dec 18 + - Offer more options when building. + - Update bundled version of linenoise. + +0.3 2012 Sep 19 + - Update rockspec and upload to luarocks. + +0.2 ??? + - Initial release. diff --git a/lib/lua-linenoise/Makefile b/lib/lua-linenoise/Makefile new file mode 100644 index 00000000..430bf5f5 --- /dev/null +++ b/lib/lua-linenoise/Makefile @@ -0,0 +1,15 @@ +OS=$(shell uname) + +OBJECTS=linenoise.o encodings/utf8.o linenoiselib.o + +ifeq ($(OS),Darwin) +linenoise.dylib: $(OBJECTS) + gcc -o $@ -bundle -undefined dynamic_lookup $^ $(OPT_LIB) +else +CFLAGS=-fPIC -I/usr/include/lua5.1 +linenoise.so: $(OBJECTS) + gcc -o $@ -shared $^ $(OPT_LIB) +endif + +clean: + rm -f *.o encodings/*.o *.so *.dylib diff --git a/lib/lua-linenoise/README.md b/lib/lua-linenoise/README.md new file mode 100644 index 00000000..a97c503a --- /dev/null +++ b/lib/lua-linenoise/README.md @@ -0,0 +1,134 @@ +# lua-linenoise - Lua binding for the linenoise command line library + +Linenoise (https://github.com/antirez/linenoise) is a delightfully simple command +line library. This Lua module is simply a binding for it. + +The main Linenoise upstream has stagnated a bit, so this binding tracks https://github.com/yhirose/linenoise/tree/utf8-support, which +includes things like UTF-8 support and ANSI terminal escape sequence detection. + +This repository also contains a Windows-compatible version of linenoise taken from MSOpenTech's [Windows port](https://github.com/MSOpenTech/redis) of redis. + +# Compilation + +If you use LuaRocks, you can run `luarocks make` on the latest rockspec. + +You can also build with make. When building this module using make, you may use the original linenoise source included in +the repository, or you may set the Makefile variable `LIBLINENOISE` to override +it: + +```sh +make LIBLINENOISE=-llinenoise +# OR: +make LIBLINENOISE=/path/to/liblinenoise.a +``` + +You may need to change the value of the LN_EXPORT macro in lua-linenoise.c to the appropriate keyword to ensure the luaopen_linenoise function is exported properly (I don't know much about C or Unix-like systems, so I may have gotten it wrong). + +If you have Visual Studio 2012 (even the free Express version), you can compile this module with the Windows-compatible linenoise source using the included solution file (you'll need to edit the include paths and import library dependencies to match your configuration). + +If you prefer to compile using other tools, just link lua-linenoise.c with line-noise-windows/linenoise.c and line-noise-windows/win32fixes.c to create the Windows-compatible DLL. + +# Usage + +This library is a fairly thin wrapper over linenoise itself, so the function calls +are named similarly. I may develop a "porcelain" layer in the future. + +## L.linenoise(prompt) + +Prompts for a line of input, using *prompt* as the prompt string. Returns nil if +no more input is available; Returns nil and an error string if an error occurred. + +## L.historyadd(line) + +Adds *line* to the history list. + +## L.historysetmaxlen(length) + +Sets the history list size to *length*. + +## L.historysave(filename) + +Saves the history list to *filename*. + +## L.historyload(filename) + +Loads the history list from *filename*. + +## L.clearscreen() + +Clears the screen. + +## L.setcompletion(callback) + +Sets the completion callback. This callback is called with two arguments: + + * A completions object. Use object:add or L.addcompletion to add a completion to this object. + * The current line of input. + +## L.addcompletion(completions, string) + +Adds *string* to the list of completions. + +All functions return nil on error; functions that don't have an obvious return value +return true on success. + +## L.setmultiline(multiline) + +Enables multi-line mode if *multiline* is true, disables otherwise. + +## L.sethints(callback) + +Sets a hints callback to provide hint information on the right hand side of the +prompt. *calback* should be a function that takes a single parameter (a +string, the line entered so far) and returns zero, one, or two values. Zero +values means no hint. The first value may be *nil* for no hint, or a string +value for a hint. If the first value is a string, the second value may be a table +with the *color* and *bold* keys - *color* is an ANSI terminal color code (such as +those provided by the [lua-term](https://luarocks.org/modules/hoelzro/lua-term) colors +module), whereas *bold* is a boolean indicating whether or not the hint should be printed +as bold. + +## L.printkeycodes() + +Prints linenoise key codes. Primarly used for debugging. + +## L.enableutf8() + +Enables UTF-8 handling. + +# Example + +```lua +local L = require 'linenoise' +local colors = require('term').colors -- optional +-- L.clearscreen() +print '----- Testing lua-linenoise! ------' +local prompt, history = '? ', 'history.txt' +L.historyload(history) -- load existing history +L.setcompletion(function(completion,str) + if str == 'h' then + completion:add('help') + completion:add('halt') + end +end) +L.sethints(function(str) + if str == 'h' then + return ' bold hints in red', { color = colors.red, bold = true } + end +end) + +L.enableutf8() + +local line, err = L.linenoise(prompt) +while line do + if #line > 0 then + print(line:upper()) + L.historyadd(line) + L.historysave(history) -- save every new line + end + line, err = L.linenoise(prompt) +end +if err then + print('An error occurred: ' .. err) +end +``` diff --git a/lib/lua-linenoise/VSProj/lua-linenoise.sdf b/lib/lua-linenoise/VSProj/lua-linenoise.sdf new file mode 100644 index 00000000..e738868c Binary files /dev/null and b/lib/lua-linenoise/VSProj/lua-linenoise.sdf differ diff --git a/lib/lua-linenoise/VSProj/lua-linenoise.sln b/lib/lua-linenoise/VSProj/lua-linenoise.sln new file mode 100644 index 00000000..51d5d1f1 --- /dev/null +++ b/lib/lua-linenoise/VSProj/lua-linenoise.sln @@ -0,0 +1,20 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2012 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "lua-linenoise", "lua-linenoise.vcxproj", "{CC66E7EE-467D-4397-B311-99B5F2A06829}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Win32 = Debug|Win32 + Release|Win32 = Release|Win32 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {CC66E7EE-467D-4397-B311-99B5F2A06829}.Debug|Win32.ActiveCfg = Debug|Win32 + {CC66E7EE-467D-4397-B311-99B5F2A06829}.Debug|Win32.Build.0 = Debug|Win32 + {CC66E7EE-467D-4397-B311-99B5F2A06829}.Release|Win32.ActiveCfg = Release|Win32 + {CC66E7EE-467D-4397-B311-99B5F2A06829}.Release|Win32.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/lib/lua-linenoise/VSProj/lua-linenoise.v11.suo b/lib/lua-linenoise/VSProj/lua-linenoise.v11.suo new file mode 100644 index 00000000..b6f4df03 Binary files /dev/null and b/lib/lua-linenoise/VSProj/lua-linenoise.v11.suo differ diff --git a/lib/lua-linenoise/VSProj/lua-linenoise.vcxproj b/lib/lua-linenoise/VSProj/lua-linenoise.vcxproj new file mode 100644 index 00000000..2b871060 --- /dev/null +++ b/lib/lua-linenoise/VSProj/lua-linenoise.vcxproj @@ -0,0 +1,94 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + + {CC66E7EE-467D-4397-B311-99B5F2A06829} + Win32Proj + + + + DynamicLibrary + true + v110 + + + DynamicLibrary + false + v110 + + + + + + + + + + + + + true + linenoise + .dll + + + true + linenoise + .dll + + + + WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions) + MultiThreadedDebugDLL + Level3 + ProgramDatabase + Disabled + C:\Lua\LuaJIT_2.0\include;%(AdditionalIncludeDirectories) + + + MachineX86 + true + Windows + ws2_32.lib;C:\Lua\LuaJIT_2.0\lib\lua51.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) + + + + + WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions) + MultiThreadedDLL + Level3 + ProgramDatabase + C:\Lua\LuaJIT\include;%(AdditionalIncludeDirectories) + + + MachineX86 + true + Windows + true + true + ws2_32.lib;C:\Lua\LuaJIT\lib\lua51.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lib/lua-linenoise/VSProj/lua-linenoise.vcxproj.filters b/lib/lua-linenoise/VSProj/lua-linenoise.vcxproj.filters new file mode 100644 index 00000000..1ccc2515 --- /dev/null +++ b/lib/lua-linenoise/VSProj/lua-linenoise.vcxproj.filters @@ -0,0 +1,39 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav + + + + + Header Files + + + Header Files + + + Header Files + + + + + Source Files + + + Source Files + + + Source Files + + + \ No newline at end of file diff --git a/lib/lua-linenoise/encodings/utf8.c b/lib/lua-linenoise/encodings/utf8.c new file mode 100644 index 00000000..4950f514 --- /dev/null +++ b/lib/lua-linenoise/encodings/utf8.c @@ -0,0 +1,505 @@ +/* encoding/utf8.c -- VERSION 1.0 + * + * Guerrilla line editing library against the idea that a line editing lib + * needs to be 20,000 lines of C code. + * + * You can find the latest source code at: + * + * http://github.com/antirez/linenoise + * + * Does a number of crazy assumptions that happen to be true in 99.9999% of + * the 2010 UNIX computers around. + * + * ------------------------------------------------------------------------ + * + * Copyright (c) 2010-2014, Salvatore Sanfilippo + * Copyright (c) 2010-2013, Pieter Noordhuis + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include + +#define UNUSED(x) (void)(x) + +/* ============================ UTF8 utilities ============================== */ + +static unsigned long wideCharTable[][2] = { + { 0x1100, 0x115F }, + { 0x231A, 0x231B }, + { 0x2329, 0x232A }, + { 0x23E9, 0x23EC }, + { 0x23F0, 0x23F0 }, + { 0x23F3, 0x23F3 }, + { 0x25FD, 0x25FE }, + { 0x2614, 0x2615 }, + { 0x2648, 0x2653 }, + { 0x267F, 0x267F }, + { 0x2693, 0x2693 }, + { 0x26A1, 0x26A1 }, + { 0x26AA, 0x26AB }, + { 0x26BD, 0x26BE }, + { 0x26C4, 0x26C5 }, + { 0x26CE, 0x26CE }, + { 0x26D4, 0x26D4 }, + { 0x26EA, 0x26EA }, + { 0x26F2, 0x26F3 }, + { 0x26F5, 0x26F5 }, + { 0x26FA, 0x26FA }, + { 0x26FD, 0x26FD }, + { 0x2705, 0x2705 }, + { 0x270A, 0x270B }, + { 0x2728, 0x2728 }, + { 0x274C, 0x274C }, + { 0x274E, 0x274E }, + { 0x2753, 0x2755 }, + { 0x2757, 0x2757 }, + { 0x2795, 0x2797 }, + { 0x27B0, 0x27B0 }, + { 0x27BF, 0x27BF }, + { 0x2B1B, 0x2B1C }, + { 0x2B50, 0x2B50 }, + { 0x2B55, 0x2B55 }, + { 0x2E80, 0x2E99 }, + { 0x2E9B, 0x2EF3 }, + { 0x2F00, 0x2FD5 }, + { 0x2FF0, 0x2FFB }, + { 0x3001, 0x303E }, + { 0x3041, 0x3096 }, + { 0x3099, 0x30FF }, + { 0x3105, 0x312D }, + { 0x3131, 0x318E }, + { 0x3190, 0x31BA }, + { 0x31C0, 0x31E3 }, + { 0x31F0, 0x321E }, + { 0x3220, 0x3247 }, + { 0x3250, 0x32FE }, + { 0x3300, 0x4DBF }, + { 0x4E00, 0xA48C }, + { 0xA490, 0xA4C6 }, + { 0xA960, 0xA97C }, + { 0xAC00, 0xD7A3 }, + { 0xF900, 0xFAFF }, + { 0xFE10, 0xFE19 }, + { 0xFE30, 0xFE52 }, + { 0xFE54, 0xFE66 }, + { 0xFE68, 0xFE6B }, + { 0x16FE0, 0x16FE0 }, + { 0x17000, 0x187EC }, + { 0x18800, 0x18AF2 }, + { 0x1B000, 0x1B001 }, + { 0x1F004, 0x1F004 }, + { 0x1F0CF, 0x1F0CF }, + { 0x1F18E, 0x1F18E }, + { 0x1F191, 0x1F19A }, + { 0x1F200, 0x1F202 }, + { 0x1F210, 0x1F23B }, + { 0x1F240, 0x1F248 }, + { 0x1F250, 0x1F251 }, + { 0x1F300, 0x1F320 }, + { 0x1F32D, 0x1F335 }, + { 0x1F337, 0x1F37C }, + { 0x1F37E, 0x1F393 }, + { 0x1F3A0, 0x1F3CA }, + { 0x1F3CF, 0x1F3D3 }, + { 0x1F3E0, 0x1F3F0 }, + { 0x1F3F4, 0x1F3F4 }, + { 0x1F3F8, 0x1F43E }, + { 0x1F440, 0x1F440 }, + { 0x1F442, 0x1F4FC }, + { 0x1F4FF, 0x1F53D }, + { 0x1F54B, 0x1F54E }, + { 0x1F550, 0x1F567 }, + { 0x1F57A, 0x1F57A }, + { 0x1F595, 0x1F596 }, + { 0x1F5A4, 0x1F5A4 }, + { 0x1F5FB, 0x1F64F }, + { 0x1F680, 0x1F6C5 }, + { 0x1F6CC, 0x1F6CC }, + { 0x1F6D0, 0x1F6D2 }, + { 0x1F6EB, 0x1F6EC }, + { 0x1F6F4, 0x1F6F6 }, + { 0x1F910, 0x1F91E }, + { 0x1F920, 0x1F927 }, + { 0x1F930, 0x1F930 }, + { 0x1F933, 0x1F93E }, + { 0x1F940, 0x1F94B }, + { 0x1F950, 0x1F95E }, + { 0x1F980, 0x1F991 }, + { 0x1F9C0, 0x1F9C0 }, + { 0x20000, 0x2FFFD }, + { 0x30000, 0x3FFFD }, +}; + +static size_t wideCharTableSize = sizeof(wideCharTable) / sizeof(wideCharTable[0]); + +static unsigned long combiningCharTable[] = { + 0x0300,0x0301,0x0302,0x0303,0x0304,0x0305,0x0306,0x0307, + 0x0308,0x0309,0x030A,0x030B,0x030C,0x030D,0x030E,0x030F, + 0x0310,0x0311,0x0312,0x0313,0x0314,0x0315,0x0316,0x0317, + 0x0318,0x0319,0x031A,0x031B,0x031C,0x031D,0x031E,0x031F, + 0x0320,0x0321,0x0322,0x0323,0x0324,0x0325,0x0326,0x0327, + 0x0328,0x0329,0x032A,0x032B,0x032C,0x032D,0x032E,0x032F, + 0x0330,0x0331,0x0332,0x0333,0x0334,0x0335,0x0336,0x0337, + 0x0338,0x0339,0x033A,0x033B,0x033C,0x033D,0x033E,0x033F, + 0x0340,0x0341,0x0342,0x0343,0x0344,0x0345,0x0346,0x0347, + 0x0348,0x0349,0x034A,0x034B,0x034C,0x034D,0x034E,0x034F, + 0x0350,0x0351,0x0352,0x0353,0x0354,0x0355,0x0356,0x0357, + 0x0358,0x0359,0x035A,0x035B,0x035C,0x035D,0x035E,0x035F, + 0x0360,0x0361,0x0362,0x0363,0x0364,0x0365,0x0366,0x0367, + 0x0368,0x0369,0x036A,0x036B,0x036C,0x036D,0x036E,0x036F, + 0x0483,0x0484,0x0485,0x0486,0x0487,0x0591,0x0592,0x0593, + 0x0594,0x0595,0x0596,0x0597,0x0598,0x0599,0x059A,0x059B, + 0x059C,0x059D,0x059E,0x059F,0x05A0,0x05A1,0x05A2,0x05A3, + 0x05A4,0x05A5,0x05A6,0x05A7,0x05A8,0x05A9,0x05AA,0x05AB, + 0x05AC,0x05AD,0x05AE,0x05AF,0x05B0,0x05B1,0x05B2,0x05B3, + 0x05B4,0x05B5,0x05B6,0x05B7,0x05B8,0x05B9,0x05BA,0x05BB, + 0x05BC,0x05BD,0x05BF,0x05C1,0x05C2,0x05C4,0x05C5,0x05C7, + 0x0610,0x0611,0x0612,0x0613,0x0614,0x0615,0x0616,0x0617, + 0x0618,0x0619,0x061A,0x064B,0x064C,0x064D,0x064E,0x064F, + 0x0650,0x0651,0x0652,0x0653,0x0654,0x0655,0x0656,0x0657, + 0x0658,0x0659,0x065A,0x065B,0x065C,0x065D,0x065E,0x065F, + 0x0670,0x06D6,0x06D7,0x06D8,0x06D9,0x06DA,0x06DB,0x06DC, + 0x06DF,0x06E0,0x06E1,0x06E2,0x06E3,0x06E4,0x06E7,0x06E8, + 0x06EA,0x06EB,0x06EC,0x06ED,0x0711,0x0730,0x0731,0x0732, + 0x0733,0x0734,0x0735,0x0736,0x0737,0x0738,0x0739,0x073A, + 0x073B,0x073C,0x073D,0x073E,0x073F,0x0740,0x0741,0x0742, + 0x0743,0x0744,0x0745,0x0746,0x0747,0x0748,0x0749,0x074A, + 0x07A6,0x07A7,0x07A8,0x07A9,0x07AA,0x07AB,0x07AC,0x07AD, + 0x07AE,0x07AF,0x07B0,0x07EB,0x07EC,0x07ED,0x07EE,0x07EF, + 0x07F0,0x07F1,0x07F2,0x07F3,0x0816,0x0817,0x0818,0x0819, + 0x081B,0x081C,0x081D,0x081E,0x081F,0x0820,0x0821,0x0822, + 0x0823,0x0825,0x0826,0x0827,0x0829,0x082A,0x082B,0x082C, + 0x082D,0x0859,0x085A,0x085B,0x08D4,0x08D5,0x08D6,0x08D7, + 0x08D8,0x08D9,0x08DA,0x08DB,0x08DC,0x08DD,0x08DE,0x08DF, + 0x08E0,0x08E1,0x08E3,0x08E4,0x08E5,0x08E6,0x08E7,0x08E8, + 0x08E9,0x08EA,0x08EB,0x08EC,0x08ED,0x08EE,0x08EF,0x08F0, + 0x08F1,0x08F2,0x08F3,0x08F4,0x08F5,0x08F6,0x08F7,0x08F8, + 0x08F9,0x08FA,0x08FB,0x08FC,0x08FD,0x08FE,0x08FF,0x0900, + 0x0901,0x0902,0x093A,0x093C,0x0941,0x0942,0x0943,0x0944, + 0x0945,0x0946,0x0947,0x0948,0x094D,0x0951,0x0952,0x0953, + 0x0954,0x0955,0x0956,0x0957,0x0962,0x0963,0x0981,0x09BC, + 0x09C1,0x09C2,0x09C3,0x09C4,0x09CD,0x09E2,0x09E3,0x0A01, + 0x0A02,0x0A3C,0x0A41,0x0A42,0x0A47,0x0A48,0x0A4B,0x0A4C, + 0x0A4D,0x0A51,0x0A70,0x0A71,0x0A75,0x0A81,0x0A82,0x0ABC, + 0x0AC1,0x0AC2,0x0AC3,0x0AC4,0x0AC5,0x0AC7,0x0AC8,0x0ACD, + 0x0AE2,0x0AE3,0x0B01,0x0B3C,0x0B3F,0x0B41,0x0B42,0x0B43, + 0x0B44,0x0B4D,0x0B56,0x0B62,0x0B63,0x0B82,0x0BC0,0x0BCD, + 0x0C00,0x0C3E,0x0C3F,0x0C40,0x0C46,0x0C47,0x0C48,0x0C4A, + 0x0C4B,0x0C4C,0x0C4D,0x0C55,0x0C56,0x0C62,0x0C63,0x0C81, + 0x0CBC,0x0CBF,0x0CC6,0x0CCC,0x0CCD,0x0CE2,0x0CE3,0x0D01, + 0x0D41,0x0D42,0x0D43,0x0D44,0x0D4D,0x0D62,0x0D63,0x0DCA, + 0x0DD2,0x0DD3,0x0DD4,0x0DD6,0x0E31,0x0E34,0x0E35,0x0E36, + 0x0E37,0x0E38,0x0E39,0x0E3A,0x0E47,0x0E48,0x0E49,0x0E4A, + 0x0E4B,0x0E4C,0x0E4D,0x0E4E,0x0EB1,0x0EB4,0x0EB5,0x0EB6, + 0x0EB7,0x0EB8,0x0EB9,0x0EBB,0x0EBC,0x0EC8,0x0EC9,0x0ECA, + 0x0ECB,0x0ECC,0x0ECD,0x0F18,0x0F19,0x0F35,0x0F37,0x0F39, + 0x0F71,0x0F72,0x0F73,0x0F74,0x0F75,0x0F76,0x0F77,0x0F78, + 0x0F79,0x0F7A,0x0F7B,0x0F7C,0x0F7D,0x0F7E,0x0F80,0x0F81, + 0x0F82,0x0F83,0x0F84,0x0F86,0x0F87,0x0F8D,0x0F8E,0x0F8F, + 0x0F90,0x0F91,0x0F92,0x0F93,0x0F94,0x0F95,0x0F96,0x0F97, + 0x0F99,0x0F9A,0x0F9B,0x0F9C,0x0F9D,0x0F9E,0x0F9F,0x0FA0, + 0x0FA1,0x0FA2,0x0FA3,0x0FA4,0x0FA5,0x0FA6,0x0FA7,0x0FA8, + 0x0FA9,0x0FAA,0x0FAB,0x0FAC,0x0FAD,0x0FAE,0x0FAF,0x0FB0, + 0x0FB1,0x0FB2,0x0FB3,0x0FB4,0x0FB5,0x0FB6,0x0FB7,0x0FB8, + 0x0FB9,0x0FBA,0x0FBB,0x0FBC,0x0FC6,0x102D,0x102E,0x102F, + 0x1030,0x1032,0x1033,0x1034,0x1035,0x1036,0x1037,0x1039, + 0x103A,0x103D,0x103E,0x1058,0x1059,0x105E,0x105F,0x1060, + 0x1071,0x1072,0x1073,0x1074,0x1082,0x1085,0x1086,0x108D, + 0x109D,0x135D,0x135E,0x135F,0x1712,0x1713,0x1714,0x1732, + 0x1733,0x1734,0x1752,0x1753,0x1772,0x1773,0x17B4,0x17B5, + 0x17B7,0x17B8,0x17B9,0x17BA,0x17BB,0x17BC,0x17BD,0x17C6, + 0x17C9,0x17CA,0x17CB,0x17CC,0x17CD,0x17CE,0x17CF,0x17D0, + 0x17D1,0x17D2,0x17D3,0x17DD,0x180B,0x180C,0x180D,0x1885, + 0x1886,0x18A9,0x1920,0x1921,0x1922,0x1927,0x1928,0x1932, + 0x1939,0x193A,0x193B,0x1A17,0x1A18,0x1A1B,0x1A56,0x1A58, + 0x1A59,0x1A5A,0x1A5B,0x1A5C,0x1A5D,0x1A5E,0x1A60,0x1A62, + 0x1A65,0x1A66,0x1A67,0x1A68,0x1A69,0x1A6A,0x1A6B,0x1A6C, + 0x1A73,0x1A74,0x1A75,0x1A76,0x1A77,0x1A78,0x1A79,0x1A7A, + 0x1A7B,0x1A7C,0x1A7F,0x1AB0,0x1AB1,0x1AB2,0x1AB3,0x1AB4, + 0x1AB5,0x1AB6,0x1AB7,0x1AB8,0x1AB9,0x1ABA,0x1ABB,0x1ABC, + 0x1ABD,0x1B00,0x1B01,0x1B02,0x1B03,0x1B34,0x1B36,0x1B37, + 0x1B38,0x1B39,0x1B3A,0x1B3C,0x1B42,0x1B6B,0x1B6C,0x1B6D, + 0x1B6E,0x1B6F,0x1B70,0x1B71,0x1B72,0x1B73,0x1B80,0x1B81, + 0x1BA2,0x1BA3,0x1BA4,0x1BA5,0x1BA8,0x1BA9,0x1BAB,0x1BAC, + 0x1BAD,0x1BE6,0x1BE8,0x1BE9,0x1BED,0x1BEF,0x1BF0,0x1BF1, + 0x1C2C,0x1C2D,0x1C2E,0x1C2F,0x1C30,0x1C31,0x1C32,0x1C33, + 0x1C36,0x1C37,0x1CD0,0x1CD1,0x1CD2,0x1CD4,0x1CD5,0x1CD6, + 0x1CD7,0x1CD8,0x1CD9,0x1CDA,0x1CDB,0x1CDC,0x1CDD,0x1CDE, + 0x1CDF,0x1CE0,0x1CE2,0x1CE3,0x1CE4,0x1CE5,0x1CE6,0x1CE7, + 0x1CE8,0x1CED,0x1CF4,0x1CF8,0x1CF9,0x1DC0,0x1DC1,0x1DC2, + 0x1DC3,0x1DC4,0x1DC5,0x1DC6,0x1DC7,0x1DC8,0x1DC9,0x1DCA, + 0x1DCB,0x1DCC,0x1DCD,0x1DCE,0x1DCF,0x1DD0,0x1DD1,0x1DD2, + 0x1DD3,0x1DD4,0x1DD5,0x1DD6,0x1DD7,0x1DD8,0x1DD9,0x1DDA, + 0x1DDB,0x1DDC,0x1DDD,0x1DDE,0x1DDF,0x1DE0,0x1DE1,0x1DE2, + 0x1DE3,0x1DE4,0x1DE5,0x1DE6,0x1DE7,0x1DE8,0x1DE9,0x1DEA, + 0x1DEB,0x1DEC,0x1DED,0x1DEE,0x1DEF,0x1DF0,0x1DF1,0x1DF2, + 0x1DF3,0x1DF4,0x1DF5,0x1DFB,0x1DFC,0x1DFD,0x1DFE,0x1DFF, + 0x20D0,0x20D1,0x20D2,0x20D3,0x20D4,0x20D5,0x20D6,0x20D7, + 0x20D8,0x20D9,0x20DA,0x20DB,0x20DC,0x20E1,0x20E5,0x20E6, + 0x20E7,0x20E8,0x20E9,0x20EA,0x20EB,0x20EC,0x20ED,0x20EE, + 0x20EF,0x20F0,0x2CEF,0x2CF0,0x2CF1,0x2D7F,0x2DE0,0x2DE1, + 0x2DE2,0x2DE3,0x2DE4,0x2DE5,0x2DE6,0x2DE7,0x2DE8,0x2DE9, + 0x2DEA,0x2DEB,0x2DEC,0x2DED,0x2DEE,0x2DEF,0x2DF0,0x2DF1, + 0x2DF2,0x2DF3,0x2DF4,0x2DF5,0x2DF6,0x2DF7,0x2DF8,0x2DF9, + 0x2DFA,0x2DFB,0x2DFC,0x2DFD,0x2DFE,0x2DFF,0x302A,0x302B, + 0x302C,0x302D,0x3099,0x309A,0xA66F,0xA674,0xA675,0xA676, + 0xA677,0xA678,0xA679,0xA67A,0xA67B,0xA67C,0xA67D,0xA69E, + 0xA69F,0xA6F0,0xA6F1,0xA802,0xA806,0xA80B,0xA825,0xA826, + 0xA8C4,0xA8C5,0xA8E0,0xA8E1,0xA8E2,0xA8E3,0xA8E4,0xA8E5, + 0xA8E6,0xA8E7,0xA8E8,0xA8E9,0xA8EA,0xA8EB,0xA8EC,0xA8ED, + 0xA8EE,0xA8EF,0xA8F0,0xA8F1,0xA926,0xA927,0xA928,0xA929, + 0xA92A,0xA92B,0xA92C,0xA92D,0xA947,0xA948,0xA949,0xA94A, + 0xA94B,0xA94C,0xA94D,0xA94E,0xA94F,0xA950,0xA951,0xA980, + 0xA981,0xA982,0xA9B3,0xA9B6,0xA9B7,0xA9B8,0xA9B9,0xA9BC, + 0xA9E5,0xAA29,0xAA2A,0xAA2B,0xAA2C,0xAA2D,0xAA2E,0xAA31, + 0xAA32,0xAA35,0xAA36,0xAA43,0xAA4C,0xAA7C,0xAAB0,0xAAB2, + 0xAAB3,0xAAB4,0xAAB7,0xAAB8,0xAABE,0xAABF,0xAAC1,0xAAEC, + 0xAAED,0xAAF6,0xABE5,0xABE8,0xABED,0xFB1E,0xFE00,0xFE01, + 0xFE02,0xFE03,0xFE04,0xFE05,0xFE06,0xFE07,0xFE08,0xFE09, + 0xFE0A,0xFE0B,0xFE0C,0xFE0D,0xFE0E,0xFE0F,0xFE20,0xFE21, + 0xFE22,0xFE23,0xFE24,0xFE25,0xFE26,0xFE27,0xFE28,0xFE29, + 0xFE2A,0xFE2B,0xFE2C,0xFE2D,0xFE2E,0xFE2F, + 0x101FD,0x102E0,0x10376,0x10377,0x10378,0x10379,0x1037A,0x10A01, + 0x10A02,0x10A03,0x10A05,0x10A06,0x10A0C,0x10A0D,0x10A0E,0x10A0F, + 0x10A38,0x10A39,0x10A3A,0x10A3F,0x10AE5,0x10AE6,0x11001,0x11038, + 0x11039,0x1103A,0x1103B,0x1103C,0x1103D,0x1103E,0x1103F,0x11040, + 0x11041,0x11042,0x11043,0x11044,0x11045,0x11046,0x1107F,0x11080, + 0x11081,0x110B3,0x110B4,0x110B5,0x110B6,0x110B9,0x110BA,0x11100, + 0x11101,0x11102,0x11127,0x11128,0x11129,0x1112A,0x1112B,0x1112D, + 0x1112E,0x1112F,0x11130,0x11131,0x11132,0x11133,0x11134,0x11173, + 0x11180,0x11181,0x111B6,0x111B7,0x111B8,0x111B9,0x111BA,0x111BB, + 0x111BC,0x111BD,0x111BE,0x111CA,0x111CB,0x111CC,0x1122F,0x11230, + 0x11231,0x11234,0x11236,0x11237,0x1123E,0x112DF,0x112E3,0x112E4, + 0x112E5,0x112E6,0x112E7,0x112E8,0x112E9,0x112EA,0x11300,0x11301, + 0x1133C,0x11340,0x11366,0x11367,0x11368,0x11369,0x1136A,0x1136B, + 0x1136C,0x11370,0x11371,0x11372,0x11373,0x11374,0x11438,0x11439, + 0x1143A,0x1143B,0x1143C,0x1143D,0x1143E,0x1143F,0x11442,0x11443, + 0x11444,0x11446,0x114B3,0x114B4,0x114B5,0x114B6,0x114B7,0x114B8, + 0x114BA,0x114BF,0x114C0,0x114C2,0x114C3,0x115B2,0x115B3,0x115B4, + 0x115B5,0x115BC,0x115BD,0x115BF,0x115C0,0x115DC,0x115DD,0x11633, + 0x11634,0x11635,0x11636,0x11637,0x11638,0x11639,0x1163A,0x1163D, + 0x1163F,0x11640,0x116AB,0x116AD,0x116B0,0x116B1,0x116B2,0x116B3, + 0x116B4,0x116B5,0x116B7,0x1171D,0x1171E,0x1171F,0x11722,0x11723, + 0x11724,0x11725,0x11727,0x11728,0x11729,0x1172A,0x1172B,0x11C30, + 0x11C31,0x11C32,0x11C33,0x11C34,0x11C35,0x11C36,0x11C38,0x11C39, + 0x11C3A,0x11C3B,0x11C3C,0x11C3D,0x11C3F,0x11C92,0x11C93,0x11C94, + 0x11C95,0x11C96,0x11C97,0x11C98,0x11C99,0x11C9A,0x11C9B,0x11C9C, + 0x11C9D,0x11C9E,0x11C9F,0x11CA0,0x11CA1,0x11CA2,0x11CA3,0x11CA4, + 0x11CA5,0x11CA6,0x11CA7,0x11CAA,0x11CAB,0x11CAC,0x11CAD,0x11CAE, + 0x11CAF,0x11CB0,0x11CB2,0x11CB3,0x11CB5,0x11CB6,0x16AF0,0x16AF1, + 0x16AF2,0x16AF3,0x16AF4,0x16B30,0x16B31,0x16B32,0x16B33,0x16B34, + 0x16B35,0x16B36,0x16F8F,0x16F90,0x16F91,0x16F92,0x1BC9D,0x1BC9E, + 0x1D167,0x1D168,0x1D169,0x1D17B,0x1D17C,0x1D17D,0x1D17E,0x1D17F, + 0x1D180,0x1D181,0x1D182,0x1D185,0x1D186,0x1D187,0x1D188,0x1D189, + 0x1D18A,0x1D18B,0x1D1AA,0x1D1AB,0x1D1AC,0x1D1AD,0x1D242,0x1D243, + 0x1D244,0x1DA00,0x1DA01,0x1DA02,0x1DA03,0x1DA04,0x1DA05,0x1DA06, + 0x1DA07,0x1DA08,0x1DA09,0x1DA0A,0x1DA0B,0x1DA0C,0x1DA0D,0x1DA0E, + 0x1DA0F,0x1DA10,0x1DA11,0x1DA12,0x1DA13,0x1DA14,0x1DA15,0x1DA16, + 0x1DA17,0x1DA18,0x1DA19,0x1DA1A,0x1DA1B,0x1DA1C,0x1DA1D,0x1DA1E, + 0x1DA1F,0x1DA20,0x1DA21,0x1DA22,0x1DA23,0x1DA24,0x1DA25,0x1DA26, + 0x1DA27,0x1DA28,0x1DA29,0x1DA2A,0x1DA2B,0x1DA2C,0x1DA2D,0x1DA2E, + 0x1DA2F,0x1DA30,0x1DA31,0x1DA32,0x1DA33,0x1DA34,0x1DA35,0x1DA36, + 0x1DA3B,0x1DA3C,0x1DA3D,0x1DA3E,0x1DA3F,0x1DA40,0x1DA41,0x1DA42, + 0x1DA43,0x1DA44,0x1DA45,0x1DA46,0x1DA47,0x1DA48,0x1DA49,0x1DA4A, + 0x1DA4B,0x1DA4C,0x1DA4D,0x1DA4E,0x1DA4F,0x1DA50,0x1DA51,0x1DA52, + 0x1DA53,0x1DA54,0x1DA55,0x1DA56,0x1DA57,0x1DA58,0x1DA59,0x1DA5A, + 0x1DA5B,0x1DA5C,0x1DA5D,0x1DA5E,0x1DA5F,0x1DA60,0x1DA61,0x1DA62, + 0x1DA63,0x1DA64,0x1DA65,0x1DA66,0x1DA67,0x1DA68,0x1DA69,0x1DA6A, + 0x1DA6B,0x1DA6C,0x1DA75,0x1DA84,0x1DA9B,0x1DA9C,0x1DA9D,0x1DA9E, + 0x1DA9F,0x1DAA1,0x1DAA2,0x1DAA3,0x1DAA4,0x1DAA5,0x1DAA6,0x1DAA7, + 0x1DAA8,0x1DAA9,0x1DAAA,0x1DAAB,0x1DAAC,0x1DAAD,0x1DAAE,0x1DAAF, + 0x1E000,0x1E001,0x1E002,0x1E003,0x1E004,0x1E005,0x1E006,0x1E008, + 0x1E009,0x1E00A,0x1E00B,0x1E00C,0x1E00D,0x1E00E,0x1E00F,0x1E010, + 0x1E011,0x1E012,0x1E013,0x1E014,0x1E015,0x1E016,0x1E017,0x1E018, + 0x1E01B,0x1E01C,0x1E01D,0x1E01E,0x1E01F,0x1E020,0x1E021,0x1E023, + 0x1E024,0x1E026,0x1E027,0x1E028,0x1E029,0x1E02A,0x1E8D0,0x1E8D1, + 0x1E8D2,0x1E8D3,0x1E8D4,0x1E8D5,0x1E8D6,0x1E944,0x1E945,0x1E946, + 0x1E947,0x1E948,0x1E949,0x1E94A,0xE0100,0xE0101,0xE0102,0xE0103, + 0xE0104,0xE0105,0xE0106,0xE0107,0xE0108,0xE0109,0xE010A,0xE010B, + 0xE010C,0xE010D,0xE010E,0xE010F,0xE0110,0xE0111,0xE0112,0xE0113, + 0xE0114,0xE0115,0xE0116,0xE0117,0xE0118,0xE0119,0xE011A,0xE011B, + 0xE011C,0xE011D,0xE011E,0xE011F,0xE0120,0xE0121,0xE0122,0xE0123, + 0xE0124,0xE0125,0xE0126,0xE0127,0xE0128,0xE0129,0xE012A,0xE012B, + 0xE012C,0xE012D,0xE012E,0xE012F,0xE0130,0xE0131,0xE0132,0xE0133, + 0xE0134,0xE0135,0xE0136,0xE0137,0xE0138,0xE0139,0xE013A,0xE013B, + 0xE013C,0xE013D,0xE013E,0xE013F,0xE0140,0xE0141,0xE0142,0xE0143, + 0xE0144,0xE0145,0xE0146,0xE0147,0xE0148,0xE0149,0xE014A,0xE014B, + 0xE014C,0xE014D,0xE014E,0xE014F,0xE0150,0xE0151,0xE0152,0xE0153, + 0xE0154,0xE0155,0xE0156,0xE0157,0xE0158,0xE0159,0xE015A,0xE015B, + 0xE015C,0xE015D,0xE015E,0xE015F,0xE0160,0xE0161,0xE0162,0xE0163, + 0xE0164,0xE0165,0xE0166,0xE0167,0xE0168,0xE0169,0xE016A,0xE016B, + 0xE016C,0xE016D,0xE016E,0xE016F,0xE0170,0xE0171,0xE0172,0xE0173, + 0xE0174,0xE0175,0xE0176,0xE0177,0xE0178,0xE0179,0xE017A,0xE017B, + 0xE017C,0xE017D,0xE017E,0xE017F,0xE0180,0xE0181,0xE0182,0xE0183, + 0xE0184,0xE0185,0xE0186,0xE0187,0xE0188,0xE0189,0xE018A,0xE018B, + 0xE018C,0xE018D,0xE018E,0xE018F,0xE0190,0xE0191,0xE0192,0xE0193, + 0xE0194,0xE0195,0xE0196,0xE0197,0xE0198,0xE0199,0xE019A,0xE019B, + 0xE019C,0xE019D,0xE019E,0xE019F,0xE01A0,0xE01A1,0xE01A2,0xE01A3, + 0xE01A4,0xE01A5,0xE01A6,0xE01A7,0xE01A8,0xE01A9,0xE01AA,0xE01AB, + 0xE01AC,0xE01AD,0xE01AE,0xE01AF,0xE01B0,0xE01B1,0xE01B2,0xE01B3, + 0xE01B4,0xE01B5,0xE01B6,0xE01B7,0xE01B8,0xE01B9,0xE01BA,0xE01BB, + 0xE01BC,0xE01BD,0xE01BE,0xE01BF,0xE01C0,0xE01C1,0xE01C2,0xE01C3, + 0xE01C4,0xE01C5,0xE01C6,0xE01C7,0xE01C8,0xE01C9,0xE01CA,0xE01CB, + 0xE01CC,0xE01CD,0xE01CE,0xE01CF,0xE01D0,0xE01D1,0xE01D2,0xE01D3, + 0xE01D4,0xE01D5,0xE01D6,0xE01D7,0xE01D8,0xE01D9,0xE01DA,0xE01DB, + 0xE01DC,0xE01DD,0xE01DE,0xE01DF,0xE01E0,0xE01E1,0xE01E2,0xE01E3, + 0xE01E4,0xE01E5,0xE01E6,0xE01E7,0xE01E8,0xE01E9,0xE01EA,0xE01EB, + 0xE01EC,0xE01ED,0xE01EE,0xE01EF, +}; + +static unsigned long combiningCharTableSize = sizeof(combiningCharTable) / sizeof(combiningCharTable[0]); + +/* Check if the code is a wide character + */ +static int isWideChar(unsigned long cp) { + size_t i; + for (i = 0; i < wideCharTableSize; i++) + if (wideCharTable[i][0] <= cp && cp <= wideCharTable[i][1]) return 1; + return 0; +} + +/* Check if the code is a combining character + */ +static int isCombiningChar(unsigned long cp) { + size_t i; + for (i = 0; i < combiningCharTableSize; i++) + if (combiningCharTable[i] == cp) return 1; + return 0; +} + +/* Get length of previous UTF8 character + */ +static size_t prevUtf8CharLen(const char* buf, int pos) { + int end = pos--; + while (pos >= 0 && ((unsigned char)buf[pos] & 0xC0) == 0x80) + pos--; + return end - pos; +} + +/* Convert UTF8 to Unicode code point + */ +static size_t utf8BytesToCodePoint(const char* buf, size_t len, int* cp) { + if (len) { + unsigned char byte = buf[0]; + if ((byte & 0x80) == 0) { + *cp = byte; + return 1; + } else if ((byte & 0xE0) == 0xC0) { + if (len >= 2) { + *cp = (((unsigned long)(buf[0] & 0x1F)) << 6) | + ((unsigned long)(buf[1] & 0x3F)); + return 2; + } + } else if ((byte & 0xF0) == 0xE0) { + if (len >= 3) { + *cp = (((unsigned long)(buf[0] & 0x0F)) << 12) | + (((unsigned long)(buf[1] & 0x3F)) << 6) | + ((unsigned long)(buf[2] & 0x3F)); + return 3; + } + } else if ((byte & 0xF8) == 0xF0) { + if (len >= 4) { + *cp = (((unsigned long)(buf[0] & 0x07)) << 18) | + (((unsigned long)(buf[1] & 0x3F)) << 12) | + (((unsigned long)(buf[2] & 0x3F)) << 6) | + ((unsigned long)(buf[3] & 0x3F)); + return 4; + } + } + } + return 0; +} + +/* Get length of next grapheme + */ +size_t linenoiseUtf8NextCharLen(const char* buf, size_t buf_len, size_t pos, size_t *col_len) { + size_t beg = pos; + int cp; + size_t len = utf8BytesToCodePoint(buf + pos, buf_len - pos, &cp); + if (isCombiningChar(cp)) { + /* NOTREACHED */ + return 0; + } + if (col_len != NULL) *col_len = isWideChar(cp) ? 2 : 1; + pos += len; + while (pos < buf_len) { + int cp; + len = utf8BytesToCodePoint(buf + pos, buf_len - pos, &cp); + if (!isCombiningChar(cp)) return pos - beg; + pos += len; + } + return pos - beg; +} + +/* Get length of previous grapheme + */ +size_t linenoiseUtf8PrevCharLen(const char* buf, size_t buf_len, size_t pos, size_t *col_len) { + UNUSED(buf_len); + size_t end = pos; + while (pos > 0) { + size_t len = prevUtf8CharLen(buf, pos); + pos -= len; + int cp; + utf8BytesToCodePoint(buf + pos, len, &cp); + if (!isCombiningChar(cp)) { + if (col_len != NULL) *col_len = isWideChar(cp) ? 2 : 1; + return end - pos; + } + } + /* NOTREACHED */ + return 0; +} + +/* Read a Unicode from file. + */ +size_t linenoiseUtf8ReadCode(int fd, char* buf, size_t buf_len, int* cp) { + if (buf_len < 1) return -1; + size_t nread = read(fd,&buf[0],1); + if (nread <= 0) return nread; + + unsigned char byte = buf[0]; + if ((byte & 0x80) == 0) { + ; + } else if ((byte & 0xE0) == 0xC0) { + if (buf_len < 2) return -1; + nread = read(fd,&buf[1],1); + if (nread <= 0) return nread; + } else if ((byte & 0xF0) == 0xE0) { + if (buf_len < 3) return -1; + nread = read(fd,&buf[1],2); + if (nread <= 0) return nread; + } else if ((byte & 0xF8) == 0xF0) { + if (buf_len < 3) return -1; + nread = read(fd,&buf[1],3); + if (nread <= 0) return nread; + } else { + return -1; + } + + return utf8BytesToCodePoint(buf, buf_len, cp); +} diff --git a/lib/lua-linenoise/encodings/utf8.h b/lib/lua-linenoise/encodings/utf8.h new file mode 100644 index 00000000..d401bc86 --- /dev/null +++ b/lib/lua-linenoise/encodings/utf8.h @@ -0,0 +1,55 @@ +/* encodings/utf8.h -- VERSION 1.0 + * + * Guerrilla line editing library against the idea that a line editing lib + * needs to be 20,000 lines of C code. + * + * See linenoise.c for more information. + * + * ------------------------------------------------------------------------ + * + * Copyright (c) 2010-2014, Salvatore Sanfilippo + * Copyright (c) 2010-2013, Pieter Noordhuis + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef __LINENOISE_ENCODINGS_UTF8_H +#define __LINENOISE_ENCODINGS_UTF8_H + +#ifdef __cplusplus +extern "C" { +#endif + +size_t linenoiseUtf8PrevCharLen(const char* buf, size_t buf_len, size_t pos, size_t *col_len); +size_t linenoiseUtf8NextCharLen(const char* buf, size_t buf_len, size_t pos, size_t *col_len); +size_t linenoiseUtf8ReadCode(int fd, char* buf, size_t buf_len, int* cp); + +#ifdef __cplusplus +} +#endif + +#endif /* __LINENOISE_ENCODINGS_UTF8_H */ + diff --git a/lib/lua-linenoise/example.lua b/lib/lua-linenoise/example.lua new file mode 100644 index 00000000..8b8ba173 --- /dev/null +++ b/lib/lua-linenoise/example.lua @@ -0,0 +1,32 @@ +local L = require 'linenoise' +-- L.clearscreen() +print '----- Testing lua-linenoise! ------' +local prompt, history = '? ', 'history.txt' +L.historyload(history) -- load existing history +L.setcompletion(function(c,s) + if s == 'h' then + c:add('help') -- same as L.addcompletion(c,'help) + L.addcompletion(c,'halt') -- same as c:add('halt') + end +end) +L.sethints(function(s) + if s == 'h' then + return ' test hint' + end +end) + +L.enableutf8() + +local line, err = L.linenoise(prompt) +while line do + if #line > 0 then + print(line:upper()) + L.historyadd(line) + L.historysave(history) -- save every new line + end + line, err = L.linenoise(prompt) +end + +if err then + print('An error occurred: ' .. err) +end diff --git a/lib/lua-linenoise/fmacros.h b/lib/lua-linenoise/fmacros.h new file mode 100644 index 00000000..27a91d5b --- /dev/null +++ b/lib/lua-linenoise/fmacros.h @@ -0,0 +1,19 @@ +#ifndef _REDIS_FMACRO_H +#define _REDIS_FMACRO_H + +#define _BSD_SOURCE + +#if defined(__linux__) || defined(__OpenBSD__) +#define _XOPEN_SOURCE 700 +#else +#define _XOPEN_SOURCE +#endif + +#define _LARGEFILE_SOURCE +#define _FILE_OFFSET_BITS 64 + +#ifdef _WIN32 +#define off off_t +#endif + +#endif diff --git a/lib/lua-linenoise/linenoise-0.9-1.rockspec b/lib/lua-linenoise/linenoise-0.9-1.rockspec new file mode 100644 index 00000000..1d6f7ed5 --- /dev/null +++ b/lib/lua-linenoise/linenoise-0.9-1.rockspec @@ -0,0 +1,33 @@ +package = 'linenoise' +version = '0.9-1' +source = { + url = 'https://github.com/hoelzro/lua-linenoise/archive/0.9.tar.gz', + dir = 'lua-linenoise-0.9', +} +description = { + summary = 'A binding for the linenoise command line library', + homepage = 'https://github.com/hoelzro/lua-linenoise', + license = 'MIT/X11', +} +dependencies = { + 'lua >= 5.1' +} + +build = { + type = 'builtin', + modules = { + linenoise = { + sources = { 'linenoise.c', 'linenoiselib.c', 'encodings/utf8.c' }, + }, + }, + platforms = { + win32 = { + modules = { + linenoise = { + sources = { 'linenoise.c', 'linenoiselib.c', 'encodings/utf8.c', 'win32fixes.c' }, + libraries = { 'ws2_32' }, + }, + }, + }, + }, +} diff --git a/lib/lua-linenoise/linenoise.c b/lib/lua-linenoise/linenoise.c new file mode 100644 index 00000000..3acb01da --- /dev/null +++ b/lib/lua-linenoise/linenoise.c @@ -0,0 +1,376 @@ +/* vim:sts=4 sw=4 expandtab + */ + +/* +* Copyright (c) 2011-2015 Rob Hoelz +* +* 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 the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is furnished +* to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +#include +#include +#include +#include +#include "linenoise.h" +#include "encodings/utf8.h" + +#define LN_COMPLETION_TYPE "linenoiseCompletions*" + +#ifdef _WIN32 +#define LN_EXPORT __declspec(dllexport) +#else +#define LN_EXPORT extern +#endif + +#ifndef LUA_OK +#define LUA_OK 0 +#endif + +static int completion_func_ref = LUA_NOREF; +static int hints_func_ref = LUA_NOREF; +static lua_State *completion_state; +static int callback_error_ref; + +static int handle_ln_error(lua_State *L) +{ + lua_pushnil(L); + return 1; +} + +static int handle_ln_ok(lua_State *L) +{ + lua_pushboolean(L, 1); + return 1; +} + +static int completion_callback_wrapper(const char *line, linenoiseCompletions *completions) +{ + lua_State *L = completion_state; + int status; + + lua_rawgeti(L, LUA_REGISTRYINDEX, completion_func_ref); + *((linenoiseCompletions **) lua_newuserdata(L, sizeof(linenoiseCompletions *))) = completions; + luaL_getmetatable(L, LN_COMPLETION_TYPE); + lua_setmetatable(L, -2); + + lua_pushstring(L, line); + + status = lua_pcall(L, 2, 0, 0); + + if(status == LUA_OK) { + return 0; + } else { + lua_rawseti(L, LUA_REGISTRYINDEX, callback_error_ref); + return 1; + } +} + +static char * +hints_callback_wrapper(const char *line, int *color, int *bold, int *err) +{ + lua_State *L = completion_state; + char *result = NULL; + int status; + + lua_rawgeti(L, LUA_REGISTRYINDEX, hints_func_ref); + + lua_pushstring(L, line); + + status = lua_pcall(L, 1, 2, 0); + if(status != LUA_OK) { + lua_rawseti(L, LUA_REGISTRYINDEX, callback_error_ref); + *err = 1; + return NULL; + } + + if(!lua_isnoneornil(L, -2)) { + if(lua_isstring(L, -2)) { + const char *hint; + lua_Alloc alloc_f; + void *ud; + + hint = lua_tostring(L, -2); + alloc_f = lua_getallocf(L, &ud); + result = alloc_f(&ud, NULL, LUA_TSTRING, strlen(hint) + 1); + if(result) { + strcpy(result, hint); + } + } else { + lua_pushfstring(L, "Invalid first value of type '%s' from hints callback - string or nil required", lua_typename(L, lua_type(L, -2))); + lua_rawseti(L, LUA_REGISTRYINDEX, callback_error_ref); + *err = 1; + lua_pop(L, 2); + return NULL; + } + + if(!lua_isnoneornil(L, -1)) { + if(lua_istable(L, -1)) { + lua_getfield(L, -1, "color"); + if(lua_isnumber(L, -1)) { + *color = lua_tointeger(L, -1); + } else if(!lua_isnoneornil(L, -1)) { + lua_pushfstring(L, "Invalid color value of type '%s' from hints callback - number or nil required", lua_typename(L, lua_type(L, -1))); + lua_rawseti(L, LUA_REGISTRYINDEX, callback_error_ref); + *err = 1; + lua_pop(L, 3); + // return the result to allow linenoise to free it + return result; + } + lua_pop(L, 1); + + lua_getfield(L, -1, "bold"); + *bold = lua_toboolean(L, -1); + lua_pop(L, 1); + } else { + lua_pushfstring(L, "Invalid second value of type '%s' from hints callback - table or nil required", lua_typename(L, lua_type(L, -1))); + lua_rawseti(L, LUA_REGISTRYINDEX, callback_error_ref); + *err = 1; + lua_pop(L, 2); + // return the result to allow linenoise to free it + return result; + } + } + } + + lua_pop(L, 2); + + return result; +} + +static void +free_hints_callback(void *p) +{ + lua_State *L = completion_state; + lua_Alloc alloc_f; + void *ud; + + alloc_f = lua_getallocf(L, &ud); + + alloc_f(ud, p, 0, 0); +} + +static int l_linenoise(lua_State *L) +{ + const char *prompt = luaL_checkstring(L, 1); + char *line; + + completion_state = L; + lua_pushliteral(L, ""); + lua_rawseti(L, LUA_REGISTRYINDEX, callback_error_ref); + line = linenoise(prompt); + completion_state = NULL; + + lua_rawgeti(L, LUA_REGISTRYINDEX, callback_error_ref); + if(strlen(lua_tostring(L, -1)) != 0) { + lua_pushnil(L); + lua_insert(L, -2); + if(line) { + linenoiseFree(line); + } + return 2; + } + + if(! line) { + return handle_ln_error(L); + } + lua_pushstring(L, line); + linenoiseFree(line); + return 1; +} + +static int lines_next(lua_State *L) +{ + lua_pushcfunction(L, l_linenoise); + lua_pushvalue(L, lua_upvalueindex(1)); + lua_call(L, 1, 1); + return 1; +} + +static int l_lines(lua_State *L) +{ + luaL_checkstring(L, 1); + lua_pushcclosure(L, lines_next, 1); + return 1; +} + +static int l_historyadd(lua_State *L) +{ + const char *line = luaL_checkstring(L, 1); + + if(! linenoiseHistoryAdd(line)) { + return handle_ln_error(L); + } + + return handle_ln_ok(L); +} + +static int l_historysetmaxlen(lua_State *L) +{ + int len = luaL_checkinteger(L, 1); + + if(! linenoiseHistorySetMaxLen(len)) { + return handle_ln_error(L); + } + + return handle_ln_ok(L); +} + +static int l_historysave(lua_State *L) +{ + const char *filename = luaL_checkstring(L, 1); + + if(linenoiseHistorySave((char *) filename) < 0) { + return handle_ln_error(L); + } + return handle_ln_ok(L); +} + +static int l_historyload(lua_State *L) +{ + const char *filename = luaL_checkstring(L, 1); + + if(linenoiseHistoryLoad((char *) filename) < 0) { + return handle_ln_error(L); + } + return handle_ln_ok(L); +} + +static int l_clearscreen(lua_State *L) +{ + linenoiseClearScreen(); + return handle_ln_ok(L); +} + +static int l_setcompletion(lua_State *L) +{ + if(lua_isnoneornil(L, 1)) { + luaL_unref(L, LUA_REGISTRYINDEX, completion_func_ref); + completion_func_ref = LUA_NOREF; + linenoiseSetCompletionCallback(NULL); + } else { + luaL_checktype(L, 1, LUA_TFUNCTION); + + lua_pushvalue(L, 1); + if(completion_func_ref == LUA_NOREF) { + completion_func_ref = luaL_ref(L, LUA_REGISTRYINDEX); + } else { + lua_rawseti(L, LUA_REGISTRYINDEX, completion_func_ref); + } + linenoiseSetCompletionCallback(completion_callback_wrapper); + } + + return handle_ln_ok(L); +} + +static int l_addcompletion(lua_State *L) +{ + linenoiseCompletions *completions = *((linenoiseCompletions **) luaL_checkudata(L, 1, LN_COMPLETION_TYPE)); + const char *entry = luaL_checkstring(L, 2); + + linenoiseAddCompletion(completions, (char *) entry); + + return handle_ln_ok(L); +} + +static int +l_setmultiline(lua_State *L) +{ + int is_multi_line = lua_toboolean(L, 1); + + linenoiseSetMultiLine(is_multi_line); + + return handle_ln_ok(L); +} + +static int +l_sethints(lua_State *L) +{ + if(lua_isnoneornil(L, 1)) { + luaL_unref(L, LUA_REGISTRYINDEX, hints_func_ref); + hints_func_ref = LUA_NOREF; + + linenoiseSetHintsCallback(NULL); + linenoiseSetFreeHintsCallback(NULL); + } else { + luaL_checktype(L, 1, LUA_TFUNCTION); + + lua_pushvalue(L, 1); + if(hints_func_ref == LUA_NOREF) { + hints_func_ref = luaL_ref(L, LUA_REGISTRYINDEX); + } else { + lua_rawseti(L, LUA_REGISTRYINDEX, hints_func_ref); + } + linenoiseSetHintsCallback(hints_callback_wrapper); + linenoiseSetFreeHintsCallback(free_hints_callback); + } + return handle_ln_ok(L); +} + +luaL_Reg linenoise_funcs[] = { + { "linenoise", l_linenoise }, + { "historyadd", l_historyadd }, + { "historysetmaxlen", l_historysetmaxlen }, + { "historysave", l_historysave }, + { "historyload", l_historyload }, + { "clearscreen", l_clearscreen }, + { "setcompletion", l_setcompletion}, + { "addcompletion", l_addcompletion }, + { "setmultiline", l_setmultiline }, + { "sethints", l_sethints }, + + /* Aliases for more consistent function names */ + { "addhistory", l_historyadd }, + { "sethistorymaxlen", l_historysetmaxlen }, + { "savehistory", l_historysave }, + { "loadhistory", l_historyload }, + + { "line", l_linenoise }, + { "lines", l_lines }, + + { NULL, NULL } +}; + +luaL_Reg linenoise_methods[] = { + { "add", l_addcompletion }, + { NULL, NULL } +}; + +LN_EXPORT int luaopen_linenoise(lua_State *L) +{ + lua_pushliteral(L, ""); + callback_error_ref = luaL_ref(L, LUA_REGISTRYINDEX); + + lua_newtable(L); + + luaL_newmetatable(L, LN_COMPLETION_TYPE); + lua_pushboolean(L, 0); + lua_setfield(L, -2, "__metatable"); + lua_pushvalue(L, -1); + lua_setfield(L, -2, "__index"); + +#if LUA_VERSION_NUM > 501 + luaL_setfuncs(L, linenoise_methods, 0); + lua_pop(L, 1); + luaL_setfuncs(L,linenoise_funcs,0); +#else + luaL_register(L, NULL, linenoise_methods); + lua_pop(L, 1); + luaL_register(L, NULL, linenoise_funcs); +#endif + return 1; +} diff --git a/lib/lua-linenoise/linenoise.h b/lib/lua-linenoise/linenoise.h new file mode 100644 index 00000000..16d719eb --- /dev/null +++ b/lib/lua-linenoise/linenoise.h @@ -0,0 +1,82 @@ +/* linenoise.h -- VERSION 1.0 + * + * Guerrilla line editing library against the idea that a line editing lib + * needs to be 20,000 lines of C code. + * + * See linenoise.c for more information. + * + * ------------------------------------------------------------------------ + * + * Copyright (c) 2010-2014, Salvatore Sanfilippo + * Copyright (c) 2010-2013, Pieter Noordhuis + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef __LINENOISE_H +#define __LINENOISE_H + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct linenoiseCompletions { + size_t len; + char **cvec; +} linenoiseCompletions; + +typedef int(linenoiseCompletionCallback)(const char *, linenoiseCompletions *); +typedef char*(linenoiseHintsCallback)(const char *, int *color, int *bold, int *err); +typedef void(linenoiseFreeHintsCallback)(void *); +void linenoiseSetCompletionCallback(linenoiseCompletionCallback *); +void linenoiseSetHintsCallback(linenoiseHintsCallback *); +void linenoiseSetFreeHintsCallback(linenoiseFreeHintsCallback *); +void linenoiseAddCompletion(linenoiseCompletions *, const char *); + +char *linenoise(const char *prompt); +void linenoiseFree(void *ptr); +int linenoiseHistoryAdd(const char *line); +int linenoiseHistorySetMaxLen(int len); +int linenoiseHistorySave(const char *filename); +int linenoiseHistoryLoad(const char *filename); +void linenoiseClearScreen(void); +void linenoiseSetMultiLine(int ml); +void linenoisePrintKeyCodes(void); + +typedef size_t (linenoisePrevCharLen)(const char *buf, size_t buf_len, size_t pos, size_t *col_len); +typedef size_t (linenoiseNextCharLen)(const char *buf, size_t buf_len, size_t pos, size_t *col_len); +typedef size_t (linenoiseReadCode)(int fd, char *buf, size_t buf_len, int* c); + +void linenoiseSetEncodingFunctions( + linenoisePrevCharLen *prevCharLenFunc, + linenoiseNextCharLen *nextCharLenFunc, + linenoiseReadCode *readCodeFunc); + +#ifdef __cplusplus +} +#endif + +#endif /* __LINENOISE_H */ diff --git a/lib/lua-linenoise/linenoiselib.c b/lib/lua-linenoise/linenoiselib.c new file mode 100644 index 00000000..fd5b6d63 --- /dev/null +++ b/lib/lua-linenoise/linenoiselib.c @@ -0,0 +1,1410 @@ +/* linenoise.c -- guerrilla line editing library against the idea that a + * line editing lib needs to be 20,000 lines of C code. + * + * You can find the latest source code at: + * + * http://github.com/antirez/linenoise + * + * Does a number of crazy assumptions that happen to be true in 99.9999% of + * the 2010 UNIX computers around. + * + * ------------------------------------------------------------------------ + * + * Copyright (c) 2010-2016, Salvatore Sanfilippo + * Copyright (c) 2010-2013, Pieter Noordhuis + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * ------------------------------------------------------------------------ + * + * References: + * - http://invisible-island.net/xterm/ctlseqs/ctlseqs.html + * - http://www.3waylabs.com/nw/WWW/products/wizcon/vt220.html + * + * Todo list: + * - Filter bogus Ctrl+ combinations. + * - Win32 support + * + * Bloat: + * - History search like Ctrl+r in readline? + * + * List of escape sequences used by this program, we do everything just + * with three sequences. In order to be so cheap we may have some + * flickering effect with some slow terminal, but the lesser sequences + * the more compatible. + * + * EL (Erase Line) + * Sequence: ESC [ n K + * Effect: if n is 0 or missing, clear from cursor to end of line + * Effect: if n is 1, clear from beginning of line to cursor + * Effect: if n is 2, clear entire line + * + * CUF (CUrsor Forward) + * Sequence: ESC [ n C + * Effect: moves cursor forward n chars + * + * CUB (CUrsor Backward) + * Sequence: ESC [ n D + * Effect: moves cursor backward n chars + * + * The following is used to get the terminal width if getting + * the width with the TIOCGWINSZ ioctl fails + * + * DSR (Device Status Report) + * Sequence: ESC [ 6 n + * Effect: reports the current cusor position as ESC [ n ; m R + * where n is the row and m is the column + * + * When multi line mode is enabled, we also use an additional escape + * sequence. However multi line editing is disabled by default. + * + * CUU (Cursor Up) + * Sequence: ESC [ n A + * Effect: moves cursor up of n chars. + * + * CUD (Cursor Down) + * Sequence: ESC [ n B + * Effect: moves cursor down of n chars. + * + * When linenoiseClearScreen() is called, two additional escape sequences + * are used in order to clear the screen and position the cursor at home + * position. + * + * CUP (Cursor position) + * Sequence: ESC [ H + * Effect: moves the cursor to upper left corner + * + * ED (Erase display) + * Sequence: ESC [ 2 J + * Effect: clear the whole screen + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "linenoise.h" + +#define LINENOISE_DEFAULT_HISTORY_MAX_LEN 100 +#define LINENOISE_MAX_LINE 4096 +#define UNUSED(x) (void)(x) +static char *unsupported_term[] = {"dumb","cons25","emacs",NULL}; +static linenoiseCompletionCallback *completionCallback = NULL; +static linenoiseHintsCallback *hintsCallback = NULL; +static linenoiseFreeHintsCallback *freeHintsCallback = NULL; + +static struct termios orig_termios; /* In order to restore at exit.*/ +static int rawmode = 0; /* For atexit() function to check if restore is needed*/ +static int mlmode = 0; /* Multi line mode. Default is single line. */ +static int atexit_registered = 0; /* Register atexit just 1 time. */ +static int history_max_len = LINENOISE_DEFAULT_HISTORY_MAX_LEN; +static int history_len = 0; +static char **history = NULL; + +/* The linenoiseState structure represents the state during line editing. + * We pass this state to functions implementing specific editing + * functionalities. */ +struct linenoiseState { + int ifd; /* Terminal stdin file descriptor. */ + int ofd; /* Terminal stdout file descriptor. */ + char *buf; /* Edited line buffer. */ + size_t buflen; /* Edited line buffer size. */ + const char *prompt; /* Prompt to display. */ + size_t plen; /* Prompt length. */ + size_t pos; /* Current cursor position. */ + size_t oldcolpos; /* Previous refresh cursor column position. */ + size_t len; /* Current edited line length. */ + size_t cols; /* Number of columns in terminal. */ + size_t maxrows; /* Maximum num of rows used so far (multiline mode) */ + int history_index; /* The history index we are currently editing. */ +}; + +enum KEY_ACTION{ + KEY_NULL = 0, /* NULL */ + CTRL_A = 1, /* Ctrl+a */ + CTRL_B = 2, /* Ctrl-b */ + CTRL_C = 3, /* Ctrl-c */ + CTRL_D = 4, /* Ctrl-d */ + CTRL_E = 5, /* Ctrl-e */ + CTRL_F = 6, /* Ctrl-f */ + CTRL_H = 8, /* Ctrl-h */ + TAB = 9, /* Tab */ + CTRL_K = 11, /* Ctrl+k */ + CTRL_L = 12, /* Ctrl+l */ + ENTER = 13, /* Enter */ + CTRL_N = 14, /* Ctrl-n */ + CTRL_P = 16, /* Ctrl-p */ + CTRL_T = 20, /* Ctrl-t */ + CTRL_U = 21, /* Ctrl+u */ + CTRL_W = 23, /* Ctrl+w */ + ESC = 27, /* Escape */ + BACKSPACE = 127 /* Backspace */ +}; + +static void linenoiseAtExit(void); +int linenoiseHistoryAdd(const char *line); +static int refreshLine(struct linenoiseState *l); + +/* Debugging macro. */ +#if 0 +FILE *lndebug_fp = NULL; +#define lndebug(...) \ + do { \ + if (lndebug_fp == NULL) { \ + lndebug_fp = fopen("/tmp/lndebug.txt","a"); \ + fprintf(lndebug_fp, \ + "[%d %d %d] p: %d, rows: %d, rpos: %d, max: %d, oldmax: %d\n", \ + (int)l->len,(int)l->pos,(int)l->oldcolpos,plen,rows,rpos, \ + (int)l->maxrows,old_rows); \ + } \ + fprintf(lndebug_fp, ", " __VA_ARGS__); \ + fflush(lndebug_fp); \ + } while (0) +#else +#define lndebug(fmt, ...) +#endif + +/* ========================== Encoding functions ============================= */ + +/* Get byte length and column length of the previous character */ +static size_t defaultPrevCharLen(const char *buf, size_t buf_len, size_t pos, size_t *col_len) { + UNUSED(buf); UNUSED(buf_len); UNUSED(pos); + if (col_len != NULL) *col_len = 1; + return 1; +} + +/* Get byte length and column length of the next character */ +static size_t defaultNextCharLen(const char *buf, size_t buf_len, size_t pos, size_t *col_len) { + UNUSED(buf); UNUSED(buf_len); UNUSED(pos); + if (col_len != NULL) *col_len = 1; + return 1; +} + +/* Read bytes of the next character */ +static size_t defaultReadCode(int fd, char *buf, size_t buf_len, int* c) { + if (buf_len < 1) return -1; + int nread = read(fd,&buf[0],1); + if (nread == 1) *c = buf[0]; + return nread; +} + +/* Set default encoding functions */ +static linenoisePrevCharLen *prevCharLen = defaultPrevCharLen; +static linenoiseNextCharLen *nextCharLen = defaultNextCharLen; +static linenoiseReadCode *readCode = defaultReadCode; + +/* Set used defined encoding functions */ +void linenoiseSetEncodingFunctions( + linenoisePrevCharLen *prevCharLenFunc, + linenoiseNextCharLen *nextCharLenFunc, + linenoiseReadCode *readCodeFunc) { + prevCharLen = prevCharLenFunc; + nextCharLen = nextCharLenFunc; + readCode = readCodeFunc; +} + +/* Get column length from begining of buffer to current byte position */ +static size_t columnPos(const char *buf, size_t buf_len, size_t pos) { + size_t ret = 0; + size_t off = 0; + while (off < pos) { + size_t col_len; + size_t len = nextCharLen(buf,buf_len,off,&col_len); + off += len; + ret += col_len; + } + return ret; +} + +/* Get column length from begining of buffer to current byte position for multiline mode*/ +static size_t columnPosForMultiLine(const char *buf, size_t buf_len, size_t pos, size_t cols, size_t ini_pos) { + size_t ret = 0; + size_t colwid = ini_pos; + + size_t off = 0; + while (off < buf_len) { + size_t col_len; + size_t len = nextCharLen(buf,buf_len,off,&col_len); + + int dif = (int)(colwid + col_len) - (int)cols; + if (dif > 0) { + ret += dif; + colwid = col_len; + } else if (dif == 0) { + colwid = 0; + } else { + colwid += col_len; + } + + if (off >= pos) break; + off += len; + ret += col_len; + } + + return ret; +} + +/* ======================= Low level terminal handling ====================== */ + +/* Set if to use or not the multi line mode. */ +void linenoiseSetMultiLine(int ml) { + mlmode = ml; +} + +/* Return true if the terminal name is in the list of terminals we know are + * not able to understand basic escape sequences. */ +static int isUnsupportedTerm(void) { + char *term = getenv("TERM"); + int j; + + if (term == NULL) return 0; + for (j = 0; unsupported_term[j]; j++) + if (!strcasecmp(term,unsupported_term[j])) return 1; + return 0; +} + +/* Raw mode: 1960 magic shit. */ +static int enableRawMode(int fd) { + struct termios raw; + + if (!isatty(STDIN_FILENO)) goto fatal; + if (!atexit_registered) { + atexit(linenoiseAtExit); + atexit_registered = 1; + } + if (tcgetattr(fd,&orig_termios) == -1) goto fatal; + + raw = orig_termios; /* modify the original mode */ + /* input modes: no break, no CR to NL, no parity check, no strip char, + * no start/stop output control. */ + raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); + /* output modes - disable post processing */ + raw.c_oflag &= ~(OPOST); + /* control modes - set 8 bit chars */ + raw.c_cflag |= (CS8); + /* local modes - choing off, canonical off, no extended functions, + * no signal chars (^Z,^C) */ + raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); + /* control chars - set return condition: min number of bytes and timer. + * We want read to return every single byte, without timeout. */ + raw.c_cc[VMIN] = 1; raw.c_cc[VTIME] = 0; /* 1 byte, no timer */ + + /* put terminal in raw mode after flushing */ + if (tcsetattr(fd,TCSAFLUSH,&raw) < 0) goto fatal; + rawmode = 1; + return 0; + +fatal: + errno = ENOTTY; + return -1; +} + +static void disableRawMode(int fd) { + /* Don't even check the return value as it's too late. */ + if (rawmode && tcsetattr(fd,TCSAFLUSH,&orig_termios) != -1) + rawmode = 0; +} + +/* Use the ESC [6n escape sequence to query the horizontal cursor position + * and return it. On error -1 is returned, on success the position of the + * cursor. */ +static int getCursorPosition(int ifd, int ofd) { + char buf[32]; + int cols, rows; + unsigned int i = 0; + + /* Report cursor location */ + if (write(ofd, "\x1b[6n", 4) != 4) return -1; + + /* Read the response: ESC [ rows ; cols R */ + while (i < sizeof(buf)-1) { + if (read(ifd,buf+i,1) != 1) break; + if (buf[i] == 'R') break; + i++; + } + buf[i] = '\0'; + + /* Parse it. */ + if (buf[0] != ESC || buf[1] != '[') return -1; + if (sscanf(buf+2,"%d;%d",&rows,&cols) != 2) return -1; + return cols; +} + +/* Try to get the number of columns in the current terminal, or assume 80 + * if it fails. */ +static int getColumns(int ifd, int ofd) { + struct winsize ws; + + if (ioctl(1, TIOCGWINSZ, &ws) == -1 || ws.ws_col == 0) { + /* ioctl() failed. Try to query the terminal itself. */ + int start, cols; + + /* Get the initial position so we can restore it later. */ + start = getCursorPosition(ifd,ofd); + if (start == -1) goto failed; + + /* Go to right margin and get position. */ + if (write(ofd,"\x1b[999C",6) != 6) goto failed; + cols = getCursorPosition(ifd,ofd); + if (cols == -1) goto failed; + + /* Restore position. */ + if (cols > start) { + char seq[32]; + snprintf(seq,32,"\x1b[%dD",cols-start); + if (write(ofd,seq,strlen(seq)) == -1) { + /* Can't recover... */ + } + } + return cols; + } else { + return ws.ws_col; + } + +failed: + return 80; +} + +/* Clear the screen. Used to handle ctrl+l */ +void linenoiseClearScreen(void) { + if (write(STDOUT_FILENO,"\x1b[H\x1b[2J",7) <= 0) { + /* nothing to do, just to avoid warning. */ + } +} + +/* Beep, used for completion when there is nothing to complete or when all + * the choices were already shown. */ +static void linenoiseBeep(void) { + fprintf(stderr, "\x7"); + fflush(stderr); +} + +/* ============================== Completion ================================ */ + +/* Free a list of completion option populated by linenoiseAddCompletion(). */ +static void freeCompletions(linenoiseCompletions *lc) { + size_t i; + for (i = 0; i < lc->len; i++) + free(lc->cvec[i]); + if (lc->cvec != NULL) + free(lc->cvec); +} + +/* This is an helper function for linenoiseEdit() and is called when the + * user types the key in order to complete the string currently in the + * input. + * + * The state of the editing is encapsulated into the pointed linenoiseState + * structure as described in the structure definition. */ +static int completeLine(struct linenoiseState *ls, char *cbuf, size_t cbuf_len, int *c) { + linenoiseCompletions lc = { 0, NULL }; + int nread = 0, nwritten; + *c = 0; + + if(completionCallback(ls->buf,&lc)) { + *c = -1; + goto cleanup; + } + if (lc.len == 0) { + linenoiseBeep(); + } else { + size_t stop = 0, i = 0; + + while(!stop) { + /* Show completion or original buffer */ + if (i < lc.len) { + struct linenoiseState saved = *ls; + + ls->len = ls->pos = strlen(lc.cvec[i]); + ls->buf = lc.cvec[i]; + if(refreshLine(ls)) { + goto cleanup; + } + ls->len = saved.len; + ls->pos = saved.pos; + ls->buf = saved.buf; + } else { + if(refreshLine(ls)) { + goto cleanup; + } + } + + nread = readCode(ls->ifd,cbuf,cbuf_len,c); + if (nread <= 0) { + *c = -1; + goto cleanup; + } + + switch(*c) { + case 9: /* tab */ + i = (i+1) % (lc.len+1); + if (i == lc.len) linenoiseBeep(); + break; + case 27: /* escape */ + /* Re-show original buffer */ + if (i < lc.len) { + if(refreshLine(ls)) { + goto cleanup; + } + } + stop = 1; + break; + default: + /* Update buffer and return */ + if (i < lc.len) { + nwritten = snprintf(ls->buf,ls->buflen,"%s",lc.cvec[i]); + ls->len = ls->pos = nwritten; + } + stop = 1; + break; + } + } + } + +cleanup: + freeCompletions(&lc); + return nread; +} + +/* Register a callback function to be called for tab-completion. */ +void linenoiseSetCompletionCallback(linenoiseCompletionCallback *fn) { + completionCallback = fn; +} + +/* Register a hits function to be called to show hits to the user at the + * right of the prompt. */ +void linenoiseSetHintsCallback(linenoiseHintsCallback *fn) { + hintsCallback = fn; +} + +/* Register a function to free the hints returned by the hints callback + * registered with linenoiseSetHintsCallback(). */ +void linenoiseSetFreeHintsCallback(linenoiseFreeHintsCallback *fn) { + freeHintsCallback = fn; +} + +/* This function is used by the callback function registered by the user + * in order to add completion options given the input string when the + * user typed . See the example.c source code for a very easy to + * understand example. */ +void linenoiseAddCompletion(linenoiseCompletions *lc, const char *str) { + size_t len = strlen(str); + char *copy, **cvec; + + copy = malloc(len+1); + if (copy == NULL) return; + memcpy(copy,str,len+1); + cvec = realloc(lc->cvec,sizeof(char*)*(lc->len+1)); + if (cvec == NULL) { + free(copy); + return; + } + lc->cvec = cvec; + lc->cvec[lc->len++] = copy; +} + +/* =========================== Line editing ================================= */ + +/* We define a very simple "append buffer" structure, that is an heap + * allocated string where we can append to. This is useful in order to + * write all the escape sequences in a buffer and flush them to the standard + * output in a single call, to avoid flickering effects. */ +struct abuf { + char *b; + int len; +}; + +static void abInit(struct abuf *ab) { + ab->b = NULL; + ab->len = 0; +} + +static void abAppend(struct abuf *ab, const char *s, int len) { + char *new = realloc(ab->b,ab->len+len); + + if (new == NULL) return; + memcpy(new+ab->len,s,len); + ab->b = new; + ab->len += len; +} + +static void abFree(struct abuf *ab) { + free(ab->b); +} + +/* Helper of refreshSingleLine() and refreshMultiLine() to show hints + * to the right of the prompt. */ +int refreshShowHints(struct abuf *ab, struct linenoiseState *l, int pcollen) { + char seq[64]; + seq[0] = '\0'; + size_t collen = pcollen+columnPos(l->buf,l->len,l->len); + if (hintsCallback && collen < l->cols) { + int color = -1, bold = 0, err = 0; + char *hint = hintsCallback(l->buf,&color,&bold,&err); + if(err) { + if (freeHintsCallback && hint) freeHintsCallback(hint); + return -1; + } + if (hint) { + int hintlen = strlen(hint); + int hintmaxlen = l->cols-collen; + if (hintlen > hintmaxlen) hintlen = hintmaxlen; + if (bold == 1 && color == -1) color = 37; + if (color != -1 || bold != 0) + snprintf(seq,64,"\033[%d;%d;49m",bold,color); + abAppend(ab,seq,strlen(seq)); + abAppend(ab,hint,hintlen); + if (color != -1 || bold != 0) + abAppend(ab,"\033[0m",4); + /* Call the function to free the hint returned. */ + if (freeHintsCallback) freeHintsCallback(hint); + } + } + return 0; +} + +/* Check if text is an ANSI escape sequence + */ +static int isAnsiEscape(const char *buf, size_t buf_len, size_t* len) { + if (buf_len > 2 && !memcmp("\033[", buf, 2)) { + size_t off = 2; + while (off < buf_len) { + switch (buf[off++]) { + case 'A': case 'B': case 'C': case 'D': case 'E': + case 'F': case 'G': case 'H': case 'J': case 'K': + case 'S': case 'T': case 'f': case 'm': + *len = off; + return 1; + } + } + } + return 0; +} + +/* Get column length of prompt text + */ +static size_t promptTextColumnLen(const char *prompt, size_t plen) { + char buf[LINENOISE_MAX_LINE]; + size_t buf_len = 0; + size_t off = 0; + while (off < plen) { + size_t len; + if (isAnsiEscape(prompt + off, plen - off, &len)) { + off += len; + continue; + } + buf[buf_len++] = prompt[off++]; + } + return columnPos(buf,buf_len,buf_len); +} + +/* Single line low level line refresh. + * + * Rewrite the currently edited line accordingly to the buffer content, + * cursor position, and number of columns of the terminal. */ +static int refreshSingleLine(struct linenoiseState *l) { + char seq[64]; + size_t pcollen = promptTextColumnLen(l->prompt,strlen(l->prompt)); + int fd = l->ofd; + char *buf = l->buf; + size_t len = l->len; + size_t pos = l->pos; + struct abuf ab; + + while((pcollen+columnPos(buf,len,pos)) >= l->cols) { + int chlen = nextCharLen(buf,len,0,NULL); + buf += chlen; + len -= chlen; + pos -= chlen; + } + while (pcollen+columnPos(buf,len,len) > l->cols) { + len -= prevCharLen(buf,len,len,NULL); + } + + abInit(&ab); + /* Cursor to left edge */ + snprintf(seq,64,"\r"); + abAppend(&ab,seq,strlen(seq)); + /* Write the prompt and the current buffer content */ + abAppend(&ab,l->prompt,strlen(l->prompt)); + abAppend(&ab,buf,len); + /* Show hints if any. */ + if(refreshShowHints(&ab,l,pcollen)) { + abFree(&ab); + return -1; + } + /* Erase to right */ + snprintf(seq,64,"\x1b[0K"); + abAppend(&ab,seq,strlen(seq)); + /* Move cursor to original position. */ + snprintf(seq,64,"\r\x1b[%dC", (int)(columnPos(buf,len,pos)+pcollen)); + abAppend(&ab,seq,strlen(seq)); + if (write(fd,ab.b,ab.len) == -1) {} /* Can't recover from write error. */ + abFree(&ab); + return 0; +} + +/* Multi line low level line refresh. + * + * Rewrite the currently edited line accordingly to the buffer content, + * cursor position, and number of columns of the terminal. */ +static int refreshMultiLine(struct linenoiseState *l) { + char seq[64]; + size_t pcollen = promptTextColumnLen(l->prompt,strlen(l->prompt)); + int colpos = columnPosForMultiLine(l->buf, l->len, l->len, l->cols, pcollen); + int colpos2; /* cursor column position. */ + int rows = (pcollen+colpos+l->cols-1)/l->cols; /* rows used by current buf. */ + int rpos = (pcollen+l->oldcolpos+l->cols)/l->cols; /* cursor relative row. */ + int rpos2; /* rpos after refresh. */ + int col; /* colum position, zero-based. */ + int old_rows = l->maxrows; + int fd = l->ofd, j; + struct abuf ab; + + /* Update maxrows if needed. */ + if (rows > (int)l->maxrows) l->maxrows = rows; + + /* First step: clear all the lines used before. To do so start by + * going to the last row. */ + abInit(&ab); + if (old_rows-rpos > 0) { + lndebug("go down %d", old_rows-rpos); + snprintf(seq,64,"\x1b[%dB", old_rows-rpos); + abAppend(&ab,seq,strlen(seq)); + } + + /* Now for every row clear it, go up. */ + for (j = 0; j < old_rows-1; j++) { + lndebug("clear+up"); + snprintf(seq,64,"\r\x1b[0K\x1b[1A"); + abAppend(&ab,seq,strlen(seq)); + } + + /* Clean the top line. */ + lndebug("clear"); + snprintf(seq,64,"\r\x1b[0K"); + abAppend(&ab,seq,strlen(seq)); + + /* Write the prompt and the current buffer content */ + abAppend(&ab,l->prompt,strlen(l->prompt)); + abAppend(&ab,l->buf,l->len); + + /* Show hints if any. */ + if(refreshShowHints(&ab,l,pcollen)) { + abFree(&ab); + return -1; + } + + /* Get column length to cursor position */ + colpos2 = columnPosForMultiLine(l->buf,l->len,l->pos,l->cols,pcollen); + + /* If we are at the very end of the screen with our prompt, we need to + * emit a newline and move the prompt to the first column. */ + if (l->pos && + l->pos == l->len && + (colpos2+pcollen) % l->cols == 0) + { + lndebug(""); + abAppend(&ab,"\n",1); + snprintf(seq,64,"\r"); + abAppend(&ab,seq,strlen(seq)); + rows++; + if (rows > (int)l->maxrows) l->maxrows = rows; + } + + /* Move cursor to right position. */ + rpos2 = (pcollen+colpos2+l->cols)/l->cols; /* current cursor relative row. */ + lndebug("rpos2 %d", rpos2); + + /* Go up till we reach the expected positon. */ + if (rows-rpos2 > 0) { + lndebug("go-up %d", rows-rpos2); + snprintf(seq,64,"\x1b[%dA", rows-rpos2); + abAppend(&ab,seq,strlen(seq)); + } + + /* Set column. */ + col = (pcollen + colpos2) % l->cols; + lndebug("set col %d", 1+col); + if (col) + snprintf(seq,64,"\r\x1b[%dC", col); + else + snprintf(seq,64,"\r"); + abAppend(&ab,seq,strlen(seq)); + + lndebug("\n"); + l->oldcolpos = colpos2; + + if (write(fd,ab.b,ab.len) == -1) {} /* Can't recover from write error. */ + abFree(&ab); + return 0; +} + +/* Calls the two low level functions refreshSingleLine() or + * refreshMultiLine() according to the selected mode. */ +static int refreshLine(struct linenoiseState *l) { + if (mlmode) + return refreshMultiLine(l); + else + return refreshSingleLine(l); +} + +/* Insert the character 'c' at cursor current position. + * + * On error writing to the terminal -1 is returned, otherwise 0. */ +int linenoiseEditInsert(struct linenoiseState *l, const char *cbuf, int clen) { + if (l->len+clen <= l->buflen) { + if (l->len == l->pos) { + memcpy(&l->buf[l->pos],cbuf,clen); + l->pos+=clen; + l->len+=clen;; + l->buf[l->len] = '\0'; + if ((!mlmode && promptTextColumnLen(l->prompt,l->plen)+columnPos(l->buf,l->len,l->len) < l->cols && !hintsCallback)) { + /* Avoid a full update of the line in the + * trivial case. */ + if (write(l->ofd,cbuf,clen) == -1) return -1; + } else { + return refreshLine(l); + } + } else { + memmove(l->buf+l->pos+clen,l->buf+l->pos,l->len-l->pos); + memcpy(&l->buf[l->pos],cbuf,clen); + l->pos+=clen; + l->len+=clen; + l->buf[l->len] = '\0'; + return refreshLine(l); + } + } + return 0; +} + +/* Move cursor on the left. */ +int linenoiseEditMoveLeft(struct linenoiseState *l) { + if (l->pos > 0) { + l->pos -= prevCharLen(l->buf,l->len,l->pos,NULL); + return refreshLine(l); + } + return 0; +} + +/* Move cursor on the right. */ +int linenoiseEditMoveRight(struct linenoiseState *l) { + if (l->pos != l->len) { + l->pos += nextCharLen(l->buf,l->len,l->pos,NULL); + return refreshLine(l); + } + return 0; +} + +/* Move cursor to the start of the line. */ +int linenoiseEditMoveHome(struct linenoiseState *l) { + if (l->pos != 0) { + l->pos = 0; + return refreshLine(l); + } + return 0; +} + +/* Move cursor to the end of the line. */ +int linenoiseEditMoveEnd(struct linenoiseState *l) { + if (l->pos != l->len) { + l->pos = l->len; + return refreshLine(l); + } + return 0; +} + +/* Substitute the currently edited line with the next or previous history + * entry as specified by 'dir'. */ +#define LINENOISE_HISTORY_NEXT 0 +#define LINENOISE_HISTORY_PREV 1 +int linenoiseEditHistoryNext(struct linenoiseState *l, int dir) { + if (history_len > 1) { + /* Update the current history entry before to + * overwrite it with the next one. */ + free(history[history_len - 1 - l->history_index]); + history[history_len - 1 - l->history_index] = strdup(l->buf); + /* Show the new entry */ + l->history_index += (dir == LINENOISE_HISTORY_PREV) ? 1 : -1; + if (l->history_index < 0) { + l->history_index = 0; + return 0; + } else if (l->history_index >= history_len) { + l->history_index = history_len-1; + return 0; + } + strncpy(l->buf,history[history_len - 1 - l->history_index],l->buflen); + l->buf[l->buflen-1] = '\0'; + l->len = l->pos = strlen(l->buf); + return refreshLine(l); + } + return 0; +} + +/* Delete the character at the right of the cursor without altering the cursor + * position. Basically this is what happens with the "Delete" keyboard key. */ +int linenoiseEditDelete(struct linenoiseState *l) { + if (l->len > 0 && l->pos < l->len) { + int chlen = nextCharLen(l->buf,l->len,l->pos,NULL); + memmove(l->buf+l->pos,l->buf+l->pos+chlen,l->len-l->pos-chlen); + l->len-=chlen; + l->buf[l->len] = '\0'; + return refreshLine(l); + } + return 0; +} + +/* Backspace implementation. */ +int linenoiseEditBackspace(struct linenoiseState *l) { + if (l->pos > 0 && l->len > 0) { + int chlen = prevCharLen(l->buf,l->len,l->pos,NULL); + memmove(l->buf+l->pos-chlen,l->buf+l->pos,l->len-l->pos); + l->pos-=chlen; + l->len-=chlen; + l->buf[l->len] = '\0'; + return refreshLine(l); + } + return 0; +} + +/* Delete the previosu word, maintaining the cursor at the start of the + * current word. */ +int linenoiseEditDeletePrevWord(struct linenoiseState *l) { + size_t old_pos = l->pos; + size_t diff; + + while (l->pos > 0 && l->buf[l->pos-1] == ' ') + l->pos--; + while (l->pos > 0 && l->buf[l->pos-1] != ' ') + l->pos--; + diff = old_pos - l->pos; + memmove(l->buf+l->pos,l->buf+old_pos,l->len-old_pos+1); + l->len -= diff; + return refreshLine(l); +} + +/* This function is the core of the line editing capability of linenoise. + * It expects 'fd' to be already in "raw mode" so that every key pressed + * will be returned ASAP to read(). + * + * The resulting string is put into 'buf' when the user type enter, or + * when ctrl+d is typed. + * + * The function returns the length of the current buffer. */ +static int linenoiseEdit(int stdin_fd, int stdout_fd, char *buf, size_t buflen, const char *prompt) +{ + struct linenoiseState l; + + /* Populate the linenoise state that we pass to functions implementing + * specific editing functionalities. */ + l.ifd = stdin_fd; + l.ofd = stdout_fd; + l.buf = buf; + l.buflen = buflen; + l.prompt = prompt; + l.plen = strlen(prompt); + l.oldcolpos = l.pos = 0; + l.len = 0; + l.cols = getColumns(stdin_fd, stdout_fd); + l.maxrows = 0; + l.history_index = 0; + + /* Buffer starts empty. */ + l.buf[0] = '\0'; + l.buflen--; /* Make sure there is always space for the nulterm */ + + /* The latest history entry is always our current buffer, that + * initially is just an empty string. */ + linenoiseHistoryAdd(""); + + if (write(l.ofd,prompt,l.plen) == -1) return -1; + while(1) { + int c; + char cbuf[32]; // large enough for any encoding? + int nread; + char seq[3]; + + nread = readCode(l.ifd,cbuf,sizeof(cbuf),&c); + if (nread <= 0) return l.len; + + /* Only autocomplete when the callback is set. It returns < 0 when + * there was an error reading from fd. Otherwise it will return the + * character that should be handled next. */ + if (c == 9 && completionCallback != NULL) { + nread = completeLine(&l,cbuf,sizeof(cbuf),&c); + /* Return on errors */ + if (c < 0) return l.len; + /* Read next character when 0 */ + if (c == 0) continue; + } + + switch(c) { + case ENTER: /* enter */ + history_len--; + free(history[history_len]); + if (mlmode) { + if(linenoiseEditMoveEnd(&l)) { + return -1; + } + } + if (hintsCallback) { + int status; + /* Force a refresh without hints to leave the previous + * line as the user typed it after a newline. */ + linenoiseHintsCallback *hc = hintsCallback; + hintsCallback = NULL; + status = refreshLine(&l); + hintsCallback = hc; + if(status != 0) { + return -1; + } + } + return (int)l.len; + case CTRL_C: /* ctrl-c */ + errno = EAGAIN; + return -1; + case BACKSPACE: /* backspace */ + case 8: /* ctrl-h */ + if(linenoiseEditBackspace(&l)) { + return -1; + } + break; + case CTRL_D: /* ctrl-d, remove char at right of cursor, or if the + line is empty, act as end-of-file. */ + if (l.len > 0) { + if(linenoiseEditDelete(&l)) { + return -1; + } + } else { + history_len--; + free(history[history_len]); + return -1; + } + break; + case CTRL_T: /* ctrl-t, swaps current character with previous. */ + if (l.pos > 0 && l.pos < l.len) { + int aux = buf[l.pos-1]; + buf[l.pos-1] = buf[l.pos]; + buf[l.pos] = aux; + if (l.pos != l.len-1) l.pos++; + if(refreshLine(&l)) { + return -1; + } + } + break; + case CTRL_B: /* ctrl-b */ + if(linenoiseEditMoveLeft(&l)) { + return -1; + } + break; + case CTRL_F: /* ctrl-f */ + if(linenoiseEditMoveRight(&l)) { + return -1; + } + break; + case CTRL_P: /* ctrl-p */ + if(linenoiseEditHistoryNext(&l, LINENOISE_HISTORY_PREV)) { + return -1; + } + break; + case CTRL_N: /* ctrl-n */ + if(linenoiseEditHistoryNext(&l, LINENOISE_HISTORY_NEXT)) { + return -1; + } + break; + case ESC: /* escape sequence */ + /* Read the next two bytes representing the escape sequence. + * Use two calls to handle slow terminals returning the two + * chars at different times. */ + if (read(l.ifd,seq,1) == -1) break; + if (read(l.ifd,seq+1,1) == -1) break; + + /* ESC [ sequences. */ + if (seq[0] == '[') { + if (seq[1] >= '0' && seq[1] <= '9') { + /* Extended escape, read additional byte. */ + if (read(l.ifd,seq+2,1) == -1) break; + if (seq[2] == '~') { + switch(seq[1]) { + case '3': /* Delete key. */ + if(linenoiseEditDelete(&l)) { + return -1; + } + break; + } + } + } else { + switch(seq[1]) { + case 'A': /* Up */ + if(linenoiseEditHistoryNext(&l, LINENOISE_HISTORY_PREV)) { + return -1; + } + break; + case 'B': /* Down */ + if(linenoiseEditHistoryNext(&l, LINENOISE_HISTORY_NEXT)) { + return -1; + } + break; + case 'C': /* Right */ + if(linenoiseEditMoveRight(&l)) { + return -1; + } + break; + case 'D': /* Left */ + if(linenoiseEditMoveLeft(&l)) { + return -1; + } + break; + case 'H': /* Home */ + if(linenoiseEditMoveHome(&l)) { + return -1; + } + break; + case 'F': /* End*/ + if(linenoiseEditMoveEnd(&l)) { + return -1; + } + break; + } + } + } + + /* ESC O sequences. */ + else if (seq[0] == 'O') { + switch(seq[1]) { + case 'H': /* Home */ + if(linenoiseEditMoveHome(&l)) { + return -1; + } + break; + case 'F': /* End*/ + if(linenoiseEditMoveEnd(&l)) { + return -1; + } + break; + } + } + break; + default: + if (linenoiseEditInsert(&l,cbuf,nread)) return -1; + break; + case CTRL_U: /* Ctrl+u, delete the whole line. */ + buf[0] = '\0'; + l.pos = l.len = 0; + if(refreshLine(&l)) { + return -1; + } + break; + case CTRL_K: /* Ctrl+k, delete from current to end of line. */ + buf[l.pos] = '\0'; + l.len = l.pos; + if(refreshLine(&l)) { + return -1; + } + break; + case CTRL_A: /* Ctrl+a, go to the start of the line */ + if(linenoiseEditMoveHome(&l)) { + return -1; + } + break; + case CTRL_E: /* ctrl+e, go to the end of the line */ + if(linenoiseEditMoveEnd(&l)) { + return -1; + } + break; + case CTRL_L: /* ctrl+l, clear screen */ + linenoiseClearScreen(); + if(refreshLine(&l)) { + return -1; + } + break; + case CTRL_W: /* ctrl+w, delete previous word */ + if(linenoiseEditDeletePrevWord(&l)) { + return -1; + } + break; + } + } + return l.len; +} + +/* This special mode is used by linenoise in order to print scan codes + * on screen for debugging / development purposes. It is implemented + * by the linenoise_example program using the --keycodes option. */ +void linenoisePrintKeyCodes(void) { + char quit[4]; + + printf("Linenoise key codes debugging mode.\n" + "Press keys to see scan codes. Type 'quit' at any time to exit.\n"); + if (enableRawMode(STDIN_FILENO) == -1) return; + memset(quit,' ',4); + while(1) { + char c; + int nread; + + nread = read(STDIN_FILENO,&c,1); + if (nread <= 0) continue; + memmove(quit,quit+1,sizeof(quit)-1); /* shift string to left. */ + quit[sizeof(quit)-1] = c; /* Insert current char on the right. */ + if (memcmp(quit,"quit",sizeof(quit)) == 0) break; + + printf("'%c' %02x (%d) (type quit to exit)\n", + isprint((int)c) ? c : '?', (int)c, (int)c); + printf("\r"); /* Go left edge manually, we are in raw mode. */ + fflush(stdout); + } + disableRawMode(STDIN_FILENO); +} + +/* This function calls the line editing function linenoiseEdit() using + * the STDIN file descriptor set in raw mode. */ +static int linenoiseRaw(char *buf, size_t buflen, const char *prompt) { + int count; + + if (buflen == 0) { + errno = EINVAL; + return -1; + } + + if (enableRawMode(STDIN_FILENO) == -1) return -1; + count = linenoiseEdit(STDIN_FILENO, STDOUT_FILENO, buf, buflen, prompt); + disableRawMode(STDIN_FILENO); + printf("\n"); + return count; +} + +/* This function is called when linenoise() is called with the standard + * input file descriptor not attached to a TTY. So for example when the + * program using linenoise is called in pipe or with a file redirected + * to its standard input. In this case, we want to be able to return the + * line regardless of its length (by default we are limited to 4k). */ +static char *linenoiseNoTTY(void) { + char *line = NULL; + size_t len = 0, maxlen = 0; + + while(1) { + if (len == maxlen) { + if (maxlen == 0) maxlen = 16; + maxlen *= 2; + char *oldval = line; + line = realloc(line,maxlen); + if (line == NULL) { + if (oldval) free(oldval); + return NULL; + } + } + int c = fgetc(stdin); + if (c == EOF || c == '\n') { + if (c == EOF && len == 0) { + free(line); + return NULL; + } else { + line[len] = '\0'; + return line; + } + } else { + line[len] = c; + len++; + } + } +} + +/* The high level function that is the main API of the linenoise library. + * This function checks if the terminal has basic capabilities, just checking + * for a blacklist of stupid terminals, and later either calls the line + * editing function or uses dummy fgets() so that you will be able to type + * something even in the most desperate of the conditions. */ +char *linenoise(const char *prompt) { + char buf[LINENOISE_MAX_LINE]; + int count; + + if (!isatty(STDIN_FILENO)) { + /* Not a tty: read from file / pipe. In this mode we don't want any + * limit to the line size, so we call a function to handle that. */ + return linenoiseNoTTY(); + } else if (isUnsupportedTerm()) { + size_t len; + + printf("%s",prompt); + fflush(stdout); + if (fgets(buf,LINENOISE_MAX_LINE,stdin) == NULL) return NULL; + len = strlen(buf); + while(len && (buf[len-1] == '\n' || buf[len-1] == '\r')) { + len--; + buf[len] = '\0'; + } + return strdup(buf); + } else { + count = linenoiseRaw(buf,LINENOISE_MAX_LINE,prompt); + if (count == -1) return NULL; + return strdup(buf); + } +} + +/* This is just a wrapper the user may want to call in order to make sure + * the linenoise returned buffer is freed with the same allocator it was + * created with. Useful when the main program is using an alternative + * allocator. */ +void linenoiseFree(void *ptr) { + free(ptr); +} + +/* ================================ History ================================= */ + +/* Free the history, but does not reset it. Only used when we have to + * exit() to avoid memory leaks are reported by valgrind & co. */ +static void freeHistory(void) { + if (history) { + int j; + + for (j = 0; j < history_len; j++) + free(history[j]); + free(history); + } +} + +/* At exit we'll try to fix the terminal to the initial conditions. */ +static void linenoiseAtExit(void) { + disableRawMode(STDIN_FILENO); + freeHistory(); +} + +/* This is the API call to add a new entry in the linenoise history. + * It uses a fixed array of char pointers that are shifted (memmoved) + * when the history max length is reached in order to remove the older + * entry and make room for the new one, so it is not exactly suitable for huge + * histories, but will work well for a few hundred of entries. + * + * Using a circular buffer is smarter, but a bit more complex to handle. */ +int linenoiseHistoryAdd(const char *line) { + char *linecopy; + + if (history_max_len == 0) return 0; + + /* Initialization on first call. */ + if (history == NULL) { + history = malloc(sizeof(char*)*history_max_len); + if (history == NULL) return 0; + memset(history,0,(sizeof(char*)*history_max_len)); + } + + /* Don't add duplicated lines. */ + if (history_len && !strcmp(history[history_len-1], line)) return 0; + + /* Add an heap allocated copy of the line in the history. + * If we reached the max length, remove the older line. */ + linecopy = strdup(line); + if (!linecopy) return 0; + if (history_len == history_max_len) { + free(history[0]); + memmove(history,history+1,sizeof(char*)*(history_max_len-1)); + history_len--; + } + history[history_len] = linecopy; + history_len++; + return 1; +} + +/* Set the maximum length for the history. This function can be called even + * if there is already some history, the function will make sure to retain + * just the latest 'len' elements if the new history length value is smaller + * than the amount of items already inside the history. */ +int linenoiseHistorySetMaxLen(int len) { + char **new; + + if (len < 1) return 0; + if (history) { + int tocopy = history_len; + + new = malloc(sizeof(char*)*len); + if (new == NULL) return 0; + + /* If we can't copy everything, free the elements we'll not use. */ + if (len < tocopy) { + int j; + + for (j = 0; j < tocopy-len; j++) free(history[j]); + tocopy = len; + } + memset(new,0,sizeof(char*)*len); + memcpy(new,history+(history_len-tocopy), sizeof(char*)*tocopy); + free(history); + history = new; + } + history_max_len = len; + if (history_len > history_max_len) + history_len = history_max_len; + return 1; +} + +/* Save the history in the specified file. On success 0 is returned + * otherwise -1 is returned. */ +int linenoiseHistorySave(const char *filename) { + mode_t old_umask = umask(S_IXUSR|S_IRWXG|S_IRWXO); + FILE *fp; + int j; + + fp = fopen(filename,"w"); + umask(old_umask); + if (fp == NULL) return -1; + chmod(filename,S_IRUSR|S_IWUSR); + for (j = 0; j < history_len; j++) + fprintf(fp,"%s\n",history[j]); + fclose(fp); + return 0; +} + +/* Load the history from the specified file. If the file does not exist + * zero is returned and no operation is performed. + * + * If the file exists and the operation succeeded 0 is returned, otherwise + * on error -1 is returned. */ +int linenoiseHistoryLoad(const char *filename) { + FILE *fp = fopen(filename,"r"); + char buf[LINENOISE_MAX_LINE]; + + if (fp == NULL) return -1; + + while (fgets(buf,LINENOISE_MAX_LINE,fp) != NULL) { + char *p; + + p = strchr(buf,'\r'); + if (!p) p = strchr(buf,'\n'); + if (p) *p = '\0'; + linenoiseHistoryAdd(buf); + } + fclose(fp); + return 0; +} diff --git a/lib/lua-linenoise/readline-readme.md b/lib/lua-linenoise/readline-readme.md new file mode 100644 index 00000000..6c693ed0 --- /dev/null +++ b/lib/lua-linenoise/readline-readme.md @@ -0,0 +1,47 @@ +# Linenoise + +A minimal, zero-config, BSD licensed, readline replacement. + +News: linenoise now includes minimal completion support, thanks to Pieter Noordhuis (@pnoordhuis). + +News: linenoise is now part of [Android](http://android.git.kernel.org/?p=platform/system/core.git;a=tree;f=liblinenoise;h=56450eaed7f783760e5e6a5993ef75cde2e29dea;hb=HEAD Android)! + +## Can a line editing library be 20k lines of code? + +Line editing with some support for history is a really important feature for command line utilities. Instead of retyping almost the same stuff again and again it's just much better to hit the up arrow and edit on syntax errors, or in order to try a slightly different command. But apparently code dealing with terminals is some sort of Black Magic: readline is 30k lines of code, libedit 20k. Is it reasonable to link small utilities to huge libraries just to get a minimal support for line editing? + +So what usually happens is either: + + * Large programs with configure scripts disabling line editing if readline is not present in the system, or not supporting it at all since readline is GPL licensed and libedit (the BSD clone) is not as known and available as readline is (Readl world example of this problem: Tclsh). + * Smaller programs not using a configure script not supporting line editing at all (A problem we had with Redis-cli for instance). + +The result is a pollution of binaries without line editing support. + +So I spent more or less two hours doing a reality check resulting in this little library: is it *really* needed for a line editing library to be 20k lines of code? Apparently not, it is possibe to get a very small, zero configuration, trivial to embed library, that solves the problem. Smaller programs will just include this, supporing line editing out of the box. Larger programs may use this little library or just checking with configure if readline/libedit is available and resorting to linenoise if not. + +## Terminals, in 2010. + +Apparently almost every terminal you can happen to use today has some kind of support for VT100 alike escape sequences. So I tried to write a lib using just very basic VT100 features. The resulting library appears to work everywhere I tried to use it. + +Since it's so young I guess there are a few bugs, or the lib may not compile or work with some operating system, but it's a matter of a few weeks and eventually we'll get it right, and there will be no excuses for not shipping command line tools without built-in line editing support. + +The library is currently less than 400 lines of code. In order to use it in your project just look at the *example.c* file in the source distribution, it is trivial. Linenoise is BSD code, so you can use both in free software and commercial software. + +## Tested with... + + * Linux text only console ($TERM = linux) + * Linux KDE terminal application ($TERM = xterm) + * Linux xterm ($TERM = xterm) + * Mac OS X iTerm ($TERM = xterm) + * Mac OS X default Terminal.app ($TERM = xterm) + * OpenBSD 4.5 through an OSX Terminal.app ($TERM = screen) + * IBM AIX 6.1 + * FreeBSD xterm ($TERM = xterm) + +Please test it everywhere you can and report back! + +## Let's push this forward! + +Please fork it and add something interesting and send me a pull request. What's especially interesting are fixes, new key bindings, completion. + +Send feedbacks to antirez at gmail diff --git a/lib/lua-linenoise/win32fixes.c b/lib/lua-linenoise/win32fixes.c new file mode 100644 index 00000000..f4684edf --- /dev/null +++ b/lib/lua-linenoise/win32fixes.c @@ -0,0 +1,539 @@ +/* +* Modified by Henry Rawas (henryr@schakra.com) +* - make it compatible with Visual Studio builds +* - added wstrtod to handle INF, NAN +* - added gettimeofday routine +* - modified rename to retry after failure +*/ + +#if defined(_WIN32) && !defined(__MINGW32__) /* MinGW doesn't like this file */ + +#include +#include +#include +#ifndef FD_SETSIZE +#define FD_SETSIZE 16000 +#endif +#include +#include +#include +#include +#include +#include +#include "win32fixes.h" + + +/* Redefined here to avoid redis.h so it can be used in other projects */ +#define REDIS_NOTUSED(V) ((void) V) +#define REDIS_THREAD_STACK_SIZE (1024*1024*4) + +/* Winsock requires library initialization on startup */ +int w32initWinSock(void) { + + WSADATA t_wsa; + WORD wVers; + int iError; + + wVers = MAKEWORD(2, 2); + iError = WSAStartup(wVers, &t_wsa); + + if(iError != NO_ERROR || LOBYTE(t_wsa.wVersion) != 2 || HIBYTE(t_wsa.wVersion) != 2 ) { + return 0; /* not done; check WSAGetLastError() for error number */ + }; + + return 1; +} + +/* Behaves as posix, works without ifdefs, makes compiler happy */ +int sigaction(int sig, struct sigaction *in, struct sigaction *out) { + REDIS_NOTUSED(out); + + /* When the SA_SIGINFO flag is set in sa_flags then sa_sigaction + * is used. Otherwise, sa_handler is used */ + if (in->sa_flags & SA_SIGINFO) + signal(sig, in->sa_sigaction); + else + signal(sig, in->sa_handler); + + return 0; +} + +/* Terminates process, implemented only for SIGKILL */ +int kill(pid_t pid, int sig) { + + if (sig == SIGKILL) { + + HANDLE h = OpenProcess(PROCESS_TERMINATE, 0, pid); + + if (!TerminateProcess(h, 127)) { + errno = EINVAL; /* GetLastError() */ + CloseHandle(h); + return -1; + }; + + CloseHandle(h); + return 0; + } else { + errno = EINVAL; + return -1; + }; +} + +/* Forced write to disk */ +int fsync (int fd) { + HANDLE h = (HANDLE) _get_osfhandle(fd); + DWORD err; + + if (h == INVALID_HANDLE_VALUE) { + errno = EBADF; + return -1; + } + + if (!FlushFileBuffers(h)) { + /* Windows error -> Unix */ + err = GetLastError(); + switch (err) { + case ERROR_INVALID_HANDLE: + errno = EINVAL; + break; + + default: + errno = EIO; + } + return -1; + } + + return 0; +} + +/* Missing wait3() implementation */ +pid_t wait3(int *stat_loc, int options, void *rusage) { + REDIS_NOTUSED(stat_loc); + REDIS_NOTUSED(options); + REDIS_NOTUSED(rusage); + return (pid_t) waitpid((intptr_t) -1, 0, WAIT_FLAGS); +} + +/* Replace MS C rtl rand which is 15bit with 32 bit */ +int replace_random() { + unsigned int x=0; + if (RtlGenRandom == NULL) { + // load proc if not loaded + HMODULE lib = LoadLibraryA("advapi32.dll"); + RtlGenRandom = (RtlGenRandomFunc)GetProcAddress(lib, "SystemFunction036"); + if (RtlGenRandom == NULL) return 1; + } + RtlGenRandom(&x, sizeof(UINT_MAX)); + return (int)(x >> 1); +} + +/* BSD sockets compatibile replacement */ +int replace_setsockopt(int socket, int level, int optname, const void *optval, socklen_t optlen) { + return (setsockopt)((SOCKET)socket, level, optname, (const char *)optval, optlen); +} + +/* set size with 64bit support */ +int replace_ftruncate(int fd, off64_t length) { + HANDLE h = (HANDLE) _get_osfhandle (fd); + LARGE_INTEGER l, o; + + if (h == INVALID_HANDLE_VALUE) { + errno = EBADF; + return -1; + } + + l.QuadPart = length; + + if (!SetFilePointerEx(h, l, &o, FILE_BEGIN)) return -1; + if (!SetEndOfFile(h)) return -1; + + return 0; +} + +/* Rename which works on Windows when file exists */ +int replace_rename(const char *src, const char *dst) { + /* anti-virus may lock file - error code 5. Retry until it works or get a different error */ + int retries = 50; + while (1) { + if (MoveFileEx(src, dst, MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED | MOVEFILE_WRITE_THROUGH)) { + return 0; + } else { + errno = GetLastError(); + if (errno != 5) break; + retries--; + if (retries == 0) { + retries = 50; + Sleep(10); + } + } + } + /* On error we will return generic error code without GetLastError() */ + return -1; +} + +/* Proxy structure to pass func and arg to thread */ +typedef struct thread_params +{ + void *(*func)(void *); + void * arg; +} thread_params; + +/* Proxy function by windows thread requirements */ +static unsigned __stdcall win32_proxy_threadproc(void *arg) { + + thread_params *p = (thread_params *) arg; + p->func(p->arg); + + /* Dealocate params */ + free(p); + + _endthreadex(0); + return 0; +} + +int pthread_create(pthread_t *thread, const void *unused, + void *(*start_routine)(void*), void *arg) { + HANDLE h; + thread_params *params = (thread_params *)malloc(sizeof(thread_params)); + REDIS_NOTUSED(unused); + + params->func = start_routine; + params->arg = arg; + + h =(HANDLE) _beginthreadex(NULL, /* Security not used */ + REDIS_THREAD_STACK_SIZE, /* Set custom stack size */ + win32_proxy_threadproc, /* calls win32 stdcall proxy */ + params, /* real threadproc is passed as paremeter */ + STACK_SIZE_PARAM_IS_A_RESERVATION, /* reserve stack */ + thread /* returned thread id */ + ); + + if (!h) + return errno; + + CloseHandle(h); + return 0; +} + +/* Noop in windows */ +int pthread_detach (pthread_t thread) { + REDIS_NOTUSED(thread); + return 0; /* noop */ + } + +pthread_t pthread_self(void) { + return GetCurrentThreadId(); +} + +int win32_pthread_join(pthread_t *thread, void **value_ptr) { + int result; + HANDLE h = OpenThread(SYNCHRONIZE, FALSE, *thread); + REDIS_NOTUSED(value_ptr); + + switch (WaitForSingleObject(h, INFINITE)) { + case WAIT_OBJECT_0: + result = 0; + case WAIT_ABANDONED: + result = EINVAL; + default: + result = GetLastError(); + } + + CloseHandle(h); + return result; +} + +int pthread_cond_init(pthread_cond_t *cond, const void *unused) { + REDIS_NOTUSED(unused); + cond->waiters = 0; + cond->was_broadcast = 0; + + InitializeCriticalSection(&cond->waiters_lock); + + cond->sema = CreateSemaphore(NULL, 0, LONG_MAX, NULL); + if (!cond->sema) { + errno = GetLastError(); + return -1; + } + + cond->continue_broadcast = CreateEvent(NULL, /* security */ + FALSE, /* auto-reset */ + FALSE, /* not signaled */ + NULL); /* name */ + if (!cond->continue_broadcast) { + errno = GetLastError(); + return -1; + } + + return 0; +} + +int pthread_cond_destroy(pthread_cond_t *cond) { + CloseHandle(cond->sema); + CloseHandle(cond->continue_broadcast); + DeleteCriticalSection(&cond->waiters_lock); + return 0; +} + +int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex) { + int last_waiter; + + EnterCriticalSection(&cond->waiters_lock); + cond->waiters++; + LeaveCriticalSection(&cond->waiters_lock); + + /* + * Unlock external mutex and wait for signal. + * NOTE: we've held mutex locked long enough to increment + * waiters count above, so there's no problem with + * leaving mutex unlocked before we wait on semaphore. + */ + LeaveCriticalSection(mutex); + + /* let's wait - ignore return value */ + WaitForSingleObject(cond->sema, INFINITE); + + /* + * Decrease waiters count. If we are the last waiter, then we must + * notify the broadcasting thread that it can continue. + * But if we continued due to cond_signal, we do not have to do that + * because the signaling thread knows that only one waiter continued. + */ + EnterCriticalSection(&cond->waiters_lock); + cond->waiters--; + last_waiter = cond->was_broadcast && cond->waiters == 0; + LeaveCriticalSection(&cond->waiters_lock); + + if (last_waiter) { + /* + * cond_broadcast was issued while mutex was held. This means + * that all other waiters have continued, but are contending + * for the mutex at the end of this function because the + * broadcasting thread did not leave cond_broadcast, yet. + * (This is so that it can be sure that each waiter has + * consumed exactly one slice of the semaphor.) + * The last waiter must tell the broadcasting thread that it + * can go on. + */ + SetEvent(cond->continue_broadcast); + /* + * Now we go on to contend with all other waiters for + * the mutex. Auf in den Kampf! + */ + } + /* lock external mutex again */ + EnterCriticalSection(mutex); + + return 0; +} + +/* + * IMPORTANT: This implementation requires that pthread_cond_signal + * is called while the mutex is held that is used in the corresponding + * pthread_cond_wait calls! + */ +int pthread_cond_signal(pthread_cond_t *cond) { + int have_waiters; + + EnterCriticalSection(&cond->waiters_lock); + have_waiters = cond->waiters > 0; + LeaveCriticalSection(&cond->waiters_lock); + + /* + * Signal only when there are waiters + */ + if (have_waiters) + return ReleaseSemaphore(cond->sema, 1, NULL) ? + 0 : GetLastError(); + else + return 0; +} + + +/* Redis forks to perform background writing */ +/* fork() on unix will split process in two */ +/* marking memory pages as Copy-On-Write so */ +/* child process will have data snapshot. */ +/* Windows has no support for fork(). */ +int fork(void) { + return -1; + } + +/* Redis CPU GetProcessTimes -> rusage */ +int getrusage(int who, struct rusage * r) { + + FILETIME starttime, exittime, kerneltime, usertime; + ULARGE_INTEGER li; + + if (r == NULL) { + errno = EFAULT; + return -1; + } + + memset(r, 0, sizeof(struct rusage)); + + if (who == RUSAGE_SELF) { + if (!GetProcessTimes(GetCurrentProcess(), + &starttime, + &exittime, + &kerneltime, + &usertime)) + { + errno = EFAULT; + return -1; + } + } + + if (who == RUSAGE_CHILDREN) { + /* Childless on windows */ + starttime.dwLowDateTime = 0; + starttime.dwHighDateTime = 0; + exittime.dwLowDateTime = 0; + exittime.dwHighDateTime = 0; + kerneltime.dwLowDateTime = 0; + kerneltime.dwHighDateTime = 0; + usertime.dwLowDateTime = 0; + usertime.dwHighDateTime = 0; + } + memcpy(&li, &kerneltime, sizeof(FILETIME)); + li.QuadPart /= 10L; + r->ru_stime.tv_sec = (long)(li.QuadPart / 1000000L); + r->ru_stime.tv_usec = (long)(li.QuadPart % 1000000L); + + memcpy(&li, &usertime, sizeof(FILETIME)); + li.QuadPart /= 10L; + r->ru_utime.tv_sec = (long)(li.QuadPart / 1000000L); + r->ru_utime.tv_usec = (long)(li.QuadPart % 1000000L); + + return 0; +} + +#define DELTA_EPOCH_IN_MICROSECS 11644473600000000Ui64 + +struct timezone +{ + int tz_minuteswest; /* minutes W of Greenwich */ + int tz_dsttime; /* type of dst correction */ +}; + +int gettimeofday(struct timeval *tv, struct timezone *tz) +{ + FILETIME ft; + unsigned __int64 tmpres = 0; + static int tzflag; + + if (NULL != tv) + { + GetSystemTimeAsFileTime(&ft); + + tmpres |= ft.dwHighDateTime; + tmpres <<= 32; + tmpres |= ft.dwLowDateTime; + + /*converting file time to unix epoch*/ + tmpres -= DELTA_EPOCH_IN_MICROSECS; + tmpres /= 10; /*convert into microseconds*/ + tv->tv_sec = (long)(tmpres / 1000000UL); + tv->tv_usec = (long)(tmpres % 1000000UL); + } + + if (NULL != tz) + { + if (!tzflag) + { + _tzset(); + tzflag++; + } + tz->tz_minuteswest = _timezone / 60; + tz->tz_dsttime = _daylight; + } + + return 0; +} + +static _locale_t clocale = NULL; +double wstrtod(const char *nptr, char **eptr) { + double d; + char *leptr; + if (clocale == NULL) + clocale = _create_locale(LC_ALL, "C"); + d = _strtod_l(nptr, &leptr, clocale); + /* if 0, check if input was inf */ + if (d == 0 && nptr == leptr) { + int neg = 0; + while (isspace(*nptr)) + nptr++; + if (*nptr == '+') + nptr++; + else if (*nptr == '-') { + nptr++; + neg = 1; + } + + if (strnicmp("INF", nptr, 3) == 0) { + if (eptr != NULL) { + if (strnicmp("INFINITE", nptr, 8) == 0) + *eptr = (char*)(nptr + 8); + else + *eptr = (char*)(nptr + 3); + } + if (neg == 1) + return -HUGE_VAL; + else + return HUGE_VAL; + } else if (strnicmp("NAN", nptr, 3) == 0) { + if (eptr != NULL) + *eptr = (char*)(nptr + 3); + /* create a NaN : 0 * infinity*/ + d = HUGE_VAL; + return d * 0; + } + } + if (eptr != NULL) + *eptr = leptr; + return d; +} + +int strerror_r(int err, char* buf, size_t buflen) { + int size = FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, + err, + 0, + buf, + (DWORD)buflen, + NULL); + if (size == 0) { + char* strerr = strerror(err); + if (strlen(strerr) >= buflen) { + errno = ERANGE; + return -1; + } + strcpy(buf, strerr); + } + if (size > 2 && buf[size - 2] == '\r') { + /* remove extra CRLF */ + buf[size - 2] = '\0'; + } + return 0; +} + +char wsa_strerror_buf[128]; +char *wsa_strerror(int err) { + int size = FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, + err, + 0, + wsa_strerror_buf, + 128, + NULL); + if (size == 0) return strerror(err); + if (size > 2 && wsa_strerror_buf[size - 2] == '\r') { + /* remove extra CRLF */ + wsa_strerror_buf[size - 2] = '\0'; + } + return wsa_strerror_buf; +} + +#endif diff --git a/lib/lua-linenoise/win32fixes.h b/lib/lua-linenoise/win32fixes.h new file mode 100644 index 00000000..d2f8a092 --- /dev/null +++ b/lib/lua-linenoise/win32fixes.h @@ -0,0 +1,318 @@ +/* +* Modified by Henry Rawas (henryr@schakra.com) +* - make it compatible with Visual Studio builds +* - added wstrtod to handle INF, NAN +* - added support for using IOCP with sockets +*/ + +#ifndef WIN32FIXES_H +#define WIN32FIXES_H + +#ifdef WIN32 +#ifndef _WIN32 +#define _WIN32 +#endif +#endif + +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#define NOGDI +#define __USE_W32_SOCKETS + +#include "fmacros.h" +#include +#include +#include +#include +#include +#ifndef FD_SETSIZE +#define FD_SETSIZE 16000 +#endif +#include /* setsocketopt */ +#include +#include +#include +#include /* _O_BINARY */ +#include /* INT_MAX */ +#include +#include + +#define fseeko fseeko64 +#define ftello ftello64 + +#define inline __inline + +#undef ftruncate +#define ftruncate replace_ftruncate +#ifndef off64_t +#define off64_t off_t +#endif + +int replace_ftruncate(int fd, off64_t length); + + +#define snprintf _snprintf +#define ftello64 _ftelli64 +#define fseeko64 _fseeki64 +#define strcasecmp _stricmp +#define strtoll _strtoi64 +#define isnan _isnan +#define isfinite _finite +#define isinf(x) (!_finite(x)) +#define lseek64 lseek +/* following defined to choose little endian byte order */ +#define __i386__ 1 +#if !defined(va_copy) +#define va_copy(d,s) d = (s) +#endif + +#define sleep(x) Sleep((x)*1000) + +#ifndef __RTL_GENRANDOM +#define __RTL_GENRANDOM 1 +typedef BOOLEAN (_stdcall* RtlGenRandomFunc)(void * RandomBuffer, ULONG RandomBufferLength); +#endif +RtlGenRandomFunc RtlGenRandom; + +#define random() (long)replace_random() +#define rand() replace_random() +int replace_random(); + +#if !defined(ssize_t) +typedef int ssize_t; +#endif + +#if !defined(mode_t) +#define mode_t long +#endif + +#if !defined(u_int32_t) +/* sha1 */ +typedef unsigned __int32 u_int32_t; +#endif + +/* Redis calls usleep(1) to give thread some time +* Sleep(0) should do the same on windows +* In other cases, usleep is called with milisec resolution, +* which can be directly translated to winapi Sleep() */ +#undef usleep +#define usleep(x) (x == 1) ? Sleep(0) : Sleep((int)((x)/1000)) + +#define pipe(fds) _pipe(fds, 8192, _O_BINARY|_O_NOINHERIT) + +/* Processes */ +#define waitpid(pid,statusp,options) _cwait (statusp, pid, WAIT_CHILD) + +#define WAIT_T int +#define WTERMSIG(x) ((x) & 0xff) /* or: SIGABRT ?? */ +#define WCOREDUMP(x) 0 +#define WEXITSTATUS(x) (((x) >> 8) & 0xff) /* or: (x) ?? */ +#define WIFSIGNALED(x) (WTERMSIG (x) != 0) /* or: ((x) == 3) ?? */ +#define WIFEXITED(x) (WTERMSIG (x) == 0) /* or: ((x) != 3) ?? */ +#define WIFSTOPPED(x) 0 + +#define WNOHANG 1 + +/* file mapping */ +#define PROT_READ 1 +#define PROT_WRITE 2 + +#define MAP_FAILED (void *) -1 + +#define MAP_SHARED 1 +#define MAP_PRIVATE 2 + +/* rusage */ +#define RUSAGE_SELF 0 +#define RUSAGE_CHILDREN (-1) + +#ifndef _RUSAGE_T_ +#define _RUSAGE_T_ +struct rusage { + struct timeval ru_utime; /* user time used */ + struct timeval ru_stime; /* system time used */ +}; +#endif + +int getrusage(int who, struct rusage * rusage); + +/* Signals */ +#define SIGNULL 0 /* Null Check access to pid*/ +#define SIGHUP 1 /* Hangup Terminate; can be trapped*/ +#define SIGINT 2 /* Interrupt Terminate; can be trapped */ +#define SIGQUIT 3 /* Quit Terminate with core dump; can be trapped */ +#define SIGTRAP 5 +#define SIGBUS 7 +#define SIGKILL 9 /* Kill Forced termination; cannot be trapped */ +#define SIGPIPE 13 +#define SIGALRM 14 +#define SIGTERM 15 /* Terminate Terminate; can be trapped */ +#define SIGSTOP 17 +#define SIGTSTP 18 +#define SIGCONT 19 +#define SIGCHLD 20 +#define SIGTTIN 21 +#define SIGTTOU 22 +#define SIGABRT 22 +/* #define SIGSTOP 24 /*Pause the process; cannot be trapped*/ +/* #define SIGTSTP 25 /*Terminal stop Pause the process; can be trapped*/ +/* #define SIGCONT 26 */ +#define SIGWINCH 28 +#define SIGUSR1 30 +#define SIGUSR2 31 + +#define ucontext_t void* + +#define SA_NOCLDSTOP 0x00000001u +#define SA_NOCLDWAIT 0x00000002u +#define SA_SIGINFO 0x00000004u +#define SA_ONSTACK 0x08000000u +#define SA_RESTART 0x10000000u +#define SA_NODEFER 0x40000000u +#define SA_RESETHAND 0x80000000u +#define SA_NOMASK SA_NODEFER +#define SA_ONESHOT SA_RESETHAND +#define SA_RESTORER 0x04000000 + + +#define sigemptyset(pset) (*(pset) = 0) +#define sigfillset(pset) (*(pset) = (unsigned int)-1) +#define sigaddset(pset, num) (*(pset) |= (1L<<(num))) +#define sigdelset(pset, num) (*(pset) &= ~(1L<<(num))) +#define sigismember(pset, num) (*(pset) & (1L<<(num))) + +#ifndef SIG_SETMASK +#define SIG_SETMASK (0) +#define SIG_BLOCK (1) +#define SIG_UNBLOCK (2) +#endif /*SIG_SETMASK*/ + +typedef void (*__p_sig_fn_t)(int); +typedef int pid_t; + +#ifndef _SIGSET_T_ +#define _SIGSET_T_ +#ifdef _WIN64 +typedef unsigned long long _sigset_t; +#else +typedef unsigned long _sigset_t; +#endif +# define sigset_t _sigset_t +#endif /* _SIGSET_T_ */ + +struct sigaction { + int sa_flags; + sigset_t sa_mask; + __p_sig_fn_t sa_handler; + __p_sig_fn_t sa_sigaction; +}; + +int sigaction(int sig, struct sigaction *in, struct sigaction *out); + +/* Sockets */ + +#ifndef ECONNRESET +#define ECONNRESET WSAECONNRESET +#endif + +#ifndef EINPROGRESS +#define EINPROGRESS WSAEINPROGRESS +#endif + +#ifndef ETIMEDOUT +#define ETIMEDOUT WSAETIMEDOUT +#endif + +#define setsockopt(a,b,c,d,e) replace_setsockopt(a,b,c,d,e) + +int replace_setsockopt(int socket, int level, int optname, + const void *optval, socklen_t optlen); + +#define rename(a,b) replace_rename(a,b) +int replace_rename(const char *src, const char *dest); + +//threads avoiding pthread.h + +#define pthread_mutex_t CRITICAL_SECTION +#define pthread_attr_t ssize_t + +#define pthread_mutex_init(a,b) (InitializeCriticalSectionAndSpinCount((a), 0x80000400),0) +#define pthread_mutex_destroy(a) DeleteCriticalSection((a)) +#define pthread_mutex_lock EnterCriticalSection +#define pthread_mutex_unlock LeaveCriticalSection + +#define pthread_equal(t1, t2) ((t1) == (t2)) + +#define pthread_attr_init(x) (*(x) = 0) +#define pthread_attr_getstacksize(x, y) (*(y) = *(x)) +#define pthread_attr_setstacksize(x, y) (*(x) = y) + +#define pthread_t u_int + +int pthread_create(pthread_t *thread, const void *unused, + void *(*start_routine)(void*), void *arg); + +pthread_t pthread_self(void); + +typedef struct { + CRITICAL_SECTION waiters_lock; + LONG waiters; + int was_broadcast; + HANDLE sema; + HANDLE continue_broadcast; +} pthread_cond_t; + +int pthread_cond_init(pthread_cond_t *cond, const void *unused); +int pthread_cond_destroy(pthread_cond_t *cond); +int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); +int pthread_cond_signal(pthread_cond_t *cond); + +int pthread_detach (pthread_t thread); + +/* Misc Unix -> Win32 */ +int kill(pid_t pid, int sig); +int fsync (int fd); +pid_t wait3(int *stat_loc, int options, void *rusage); + +int w32initWinSock(void); +/* int inet_aton(const char *cp_arg, struct in_addr *addr) */ + +/* redis-check-dump */ +void *mmap(void *start, size_t length, int prot, int flags, int fd, off offset); +int munmap(void *start, size_t length); + +int fork(void); +int gettimeofday(struct timeval *tv, struct timezone *tz); + +/* strtod does not handle Inf and Nan +We need to do the check before calling strtod */ +#undef strtod +#define strtod(nptr, eptr) wstrtod((nptr), (eptr)) + +double wstrtod(const char *nptr, char **eptr); + + +/* structs and functions for using IOCP with windows sockets */ + +/* need callback on write complete. aeWinSendReq is used to pass parameters */ +typedef struct aeWinSendReq { + void *client; + void *data; + char *buf; + int len; +} aeWinSendReq; + + +int aeWinSocketAttach(int fd); +int aeWinSocketDetach(int fd, int shutd); +int aeWinReceiveDone(int fd); +int aeWinSocketSend(int fd, char *buf, int len, int flags, + void *eventLoop, void *client, void *data, void *proc); +int aeWinListen(SOCKET sock, int backlog); +int aeWinAccept(int fd, struct sockaddr *sa, socklen_t *len); + +int strerror_r(int err, char* buf, size_t buflen); +char *wsa_strerror(int err); + +#endif /* WIN32 */ +#endif /* WIN32FIXES_H */ diff --git a/lib/lua-repl/.editorconfig b/lib/lua-repl/.editorconfig new file mode 100644 index 00000000..ca4cf17b --- /dev/null +++ b/lib/lua-repl/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[{Makefile,*.makefile}] +indent_style = tab + +[*.md] +trim_trailing_whitespace = false + diff --git a/lib/lua-repl/.gitignore b/lib/lua-repl/.gitignore new file mode 100644 index 00000000..08b58b64 --- /dev/null +++ b/lib/lua-repl/.gitignore @@ -0,0 +1,3 @@ +doc/ +*.tar.gz +*.rock diff --git a/lib/lua-repl/.proverc b/lib/lua-repl/.proverc new file mode 100644 index 00000000..a5f42ae6 --- /dev/null +++ b/lib/lua-repl/.proverc @@ -0,0 +1,2 @@ +--ext .lua +--exec lua diff --git a/lib/lua-repl/.travis.yml b/lib/lua-repl/.travis.yml new file mode 100644 index 00000000..d6390739 --- /dev/null +++ b/lib/lua-repl/.travis.yml @@ -0,0 +1,31 @@ +language: generic +sudo: false + +env: + - LUA=5.1 LUAROCKS=2.3.0 + - LUA=5.1.5 LUAROCKS=2.3.0 + - LUA=5.2.0 LUAROCKS=2.3.0 + - LUA=5.2.4 LUAROCKS=2.3.0 + - LUA=5.3.0 LUAROCKS=2.3.0 + - LUA=5.3.3 LUAROCKS=2.3.0 + +cache: + apt: true + directories: + - $HOME/luas/ + +addons: + apt: + packages: + - luarocks + +before_install: + - luarocks --local install vert + +install: + - ~/.luarocks/bin/vert init --lua-version=$LUA --luarocks-version=$LUAROCKS $HOME/luas/$LUA + - source $HOME/luas/$LUA/bin/activate + - luarocks install lua-testmore + +script: + - make test diff --git a/lib/lua-repl/COPYING b/lib/lua-repl/COPYING new file mode 100644 index 00000000..959dbc98 --- /dev/null +++ b/lib/lua-repl/COPYING @@ -0,0 +1,17 @@ +Copyright (c) 2011-2015 Rob Hoelz + +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 +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/lib/lua-repl/Changes b/lib/lua-repl/Changes new file mode 100644 index 00000000..1ccb438b --- /dev/null +++ b/lib/lua-repl/Changes @@ -0,0 +1,71 @@ +NEXT + +0.10 Jan 14 2022 + --- Bug Fixes --- + - Fixed issue where current buffer is stuck after certain syntax errors (GH #61, thanks Justin Blanchard!) + +0.9 Oct 30 2018 + --- Bug Fixes --- + - Fixed rlwrap plugin under Lua 5.2 and greater (GH #57) + + --- Features/Enhancements --- + - Improve support for Lua 5.3 (thanks, Iqbal Ansari!) + +0.8 + Aug 23 2015 + --- Bug Fixes --- + - Use package.searchers if package.loaders is not available (thanks, Simon Cozens!) + + --- Backwards Incompatible Changes --- + - The default plugin bundle is no longer loaded if an rcfile is present. See + https://github.com/hoelzro/lua-repl/blob/0.8/README.md#removal-of-default-plugins-in-08 + for details. + +0.7 May 22 2015 + --- Features/Enhancements --- + - iffeature is implemented, thanks to Henry Kielmann! + - loadplugin now returns any values returned by the plugin function. + + --- Deprecations --- + - 0.8 will break backwards compatability due a change in rcfile handling; + please consult the README for details. + + --- Plugins --- + - we now have a filename completion plugin, thanks to Henry Kielmann! + - rlwrap functionality has been moved into a plugin. + +0.6 Oct 18 2014 + --- Bug Fixes --- + - Fix rcfile plugin for Lua 5.2 + +0.5 Oct 17 2014 + --- Features/Enhancements --- + - Lua 5.2 support (Thanks to Simon Cozens!) + +0.4 Mar 29 2013 + --- API Additions --- + - The repl object now has a VERSION property. + + --- Bug Fixes --- + - Overriding/augmenting getcontext will actually have an effect. + - Fix pretty_print with 'unsupported' types + + --- Features/Enhancements --- + - Change the example REPL's prompt to resemble the standalone interpreter's + - The example REPL now prints a welcome message + - If linenoise is not available and rlwrap is, the example REPL falls back to rlwrap + + --- Plugins --- + - Added some new plugins + - semicolon_suppress_output + +0.3 Oct 17 2012 + - Add plugins API. + - Add a bunch of plugins. + +0.2 Sep 28 2012 + - Add linenoise REPL. + - Add completion support. + +0.1 Oct 20 2011 + Initial release. Basic REPL functionality. diff --git a/lib/lua-repl/IDEAS.md b/lib/lua-repl/IDEAS.md new file mode 100644 index 00000000..8b7eba24 --- /dev/null +++ b/lib/lua-repl/IDEAS.md @@ -0,0 +1,43 @@ + * plugins + * replace /^=/ with 'return' + * handle locals + * override debug.debug + * supple (http://cgit.gitano.org.uk/supple.git) + * handle locals (debug.sethook?) + * debug.sethook, catch return of our chunk and grab its locals + * rewrite source code/bytecode before evaluation + * custom interpreter patch to "pcall and get bindings" + * custom module that dips into internals to "pcall and get bindings" + * some sort of debugger? + * don't contaminate globals + * tab completion (\_\_complete metamethod) + * "safe" evaluation (don't allow calling of C functions, except for those in a whitelist?) + * displaystack instead of displayerror(err)? (should xpcall return false, stack\_table?) + * visual REPL (like Factor; being able to print multi-colored/multi-sized text, images, etc) + * syntax highlighting + * paren/brace matching? + * snippets? + * code navigation (go to definition?) + * repls that "attach" to different objects (ie. inspect a single object; self is that object. completions happen against that object?) + * browsable/searchable REPL history + * not entirely sure what I mean here... + * safe termination of evaluated code (if I Control-C during an evaluation) + * store stdout/stderr output in a variable somewhere? + * persistence (pluto-based image) + +hooks +===== + + * what to do when we encounter an incomplete Lua fragment + * processing a line + * something for debug.debug... + +Implementations +=============== + + * Console + * GUI + * Web + * IRC + * safety hooks + * Awesome diff --git a/lib/lua-repl/Makefile b/lib/lua-repl/Makefile new file mode 100644 index 00000000..a092d939 --- /dev/null +++ b/lib/lua-repl/Makefile @@ -0,0 +1,14 @@ +LUA_FILES=$(shell find repl -type f -name '*.lua') +.PHONY: doc install + +doc: + luadoc -d doc $(LUA_FILES) + +install: + # TODO + +test: + LUA_INIT='' LUA_PATH=';;$(LUA_PATH);?.lua;?/init.lua;t/lib/?.lua' prove + +clean: + rm -rf doc/ diff --git a/lib/lua-repl/README.md b/lib/lua-repl/README.md new file mode 100644 index 00000000..58f128b0 --- /dev/null +++ b/lib/lua-repl/README.md @@ -0,0 +1,102 @@ +# REPL.lua - a reusable Lua REPL written in Lua, and an alternative to /usr/bin/lua + +This project has two uses: + + - An alternative to the standalone interpreter included with Lua, one that supports + things like plugins, tab completion, and automatic insertion of `return` in front + of expressions. + + - A REPL library you may embed in your application, to provide all of the niceties + of the standalone interpreter included with Lua and then some. + +Many software projects have made the choice to embed Lua in their projects to +allow their users some extra flexibility. Some of these projects would also +like to provide a Lua REPL in their programs for debugging or rapid development. +Most Lua programmers are familiar with the standalone Lua interpreter as a Lua REPL; +however, it is bound to the command line. Until now, Lua programmers would have to +implement their own REPL from scratch if they wanted to include one in their programs. +This project aims to provide a REPL implemented in pure Lua that almost any project can +make use of. + +This library also includes an example application (rep.lua), which serves as an alternative +to the standalone interpreter included with Lua. If the lua-linenoise library is installed, +it uses linenoise for history and tab completion; otherwise, it tries to use rlwrap for +basic line editing. If you would like the arrow keys to work as expected rather than printing +things like `^[[A`, please install the lua-linenoise library or the rlwrap program. + +# Project Goals + + * Provide REPL logic to Lua programs that include this module. + + * Be extensible through polymorphism and plugins. + + * Abstract away I/O, so you can run this REPL on the command line or in your own event loop and expect the same behavior. + +# Building + + * You need Luadoc (http://keplerproject.github.com/luadoc/) installed to build the documentation. + + * You need Test.More (http://fperrad.github.com/lua-TestMore/testmore.html) installed to run the tests. + +# Compatibility + +The current version of the software runs on Lua 5.1, LuaJIT ?.? etc. +A port to Lua 5.2 is envisaged, but is not at this stage a priority. +Since it is written purely in Lua, it should work on any platform that +has one of those versions of Lua installed. + +XXX Check which version of LuaJIT this works with +XXX Check that it works with other Lua interpreters + +# Installation + +You can install lua-repl via LuaRocks: + + luarocks install luarepl + +You can also install it by hand by copying the `repl` +directory to a location in your `package.path`, and +copying rep.lua to somewhere in your `PATH`. + +# Recommended packages + +`rep.lua` works best if you also have `linenoise` installed, +available from https://github.com/hoelzro/lua-linenoise. +`rep.lua` will fallback to using rlwrap if you have that as well; +without either of these, you will have command editing, history, +or other features generally provided by `readline`. + +# Features + +`rep.lua` prints the results of simple expressions without requiring +a `return ` or a `= ` in front of it. If `linenoise` is installed, +it also offers persistent history and tab completion. It also offers +a number of plugins; see plugins.md for a list of plugins that come +with lua-repl. + +# Backwards Compatibility Changes + +## Removal of default plugins in 0.8 + +Lua REPL 0.8 breaks backwards compatability by disabling the loading of the +default plugins (currently `linenoise`, `rlwrap`, `history`, `completion`, and +`autoreturn`) if an rcfile is found for a user. This is so that plugins may +not be forced onto a user if they don't want them, or play tricks with their +setup (see issue #47). If you would like to continue using these plugins, please +put the following code into your `~/.rep.lua`: + +```lua +if repl.VERSION >= 0.8 then + -- default plugins + repl:loadplugin 'linenoise' + repl:loadplugin 'history' + repl:loadplugin 'completion' + repl:loadplugin 'autoreturn' +end + +-- suppress warning message +repl.quiet_default_plugins = true +``` + +As mentioned in the code snippet, `repl.quiet_default_plugins` suppresses the warning. +You can remove this after upgrading to Lua REPL 0.8. diff --git a/lib/lua-repl/RELEASE-GUIDE.md b/lib/lua-repl/RELEASE-GUIDE.md new file mode 100644 index 00000000..1efcb728 --- /dev/null +++ b/lib/lua-repl/RELEASE-GUIDE.md @@ -0,0 +1,34 @@ +# lua-repl release guide + + - [ ] Bump VERSION (in repl/init.lua, look for references to current version) + - [ ] Update Changelog + - [ ] Rename and update rockspec + - [ ] Make sure tests pass + - [ ] Push & tag latest release + - [ ] Submit rockspec to luarocks + - [ ] E-mail lua-l + - [ ] Submit lua-l gmane link to reddit + +## E-mail template for lua-l + +Hi Lua users, + +I have just released version {{VERSION}} of lua-repl, an alternative to the +standalone REPL included with Lua and a library for embedding a Lua +REPL within a Lua application. + +lua-repl provides a library for adding a Lua REPL to your Lua-powered +application. It also provides an example REPL in the form of rep.lua, +which can take the place of the standalone interpreter provided with +Lua. It has a plugin facility; plugins for things like history and tab +completion of symbols are included in the lua-repl distribution. + +{{CHANGES}} + +You can install lua-repl via Luarocks (called luarepl there), or +manually from the source [{{REFERENCE}}]. + +-Rob + +[{{REFERENCE}}] https://github.com/hoelzro/lua-repl/archive/0.8.tar.gz + diff --git a/lib/lua-repl/Roadmap.md b/lib/lua-repl/Roadmap.md new file mode 100644 index 00000000..baaa3870 --- /dev/null +++ b/lib/lua-repl/Roadmap.md @@ -0,0 +1,44 @@ +0.10 +=== + + * Process Lua command line options with rep.lua + * Verify that it works with LuaJIT, Lua 5.0, Lua 5.2, LuaJ or something + * __pretty support for pretty print plugin + * __complete support for completion plugin + * Documentation improvements + * More clearly reference PLUGINS.md from README.md + * More clearly reference rep.lua from README.md + * Make sure that autocompletion is talked up in plugins.md (and mention in readme that many default/optional behaviors are present there) + * Make sure documentation on ~/.rep.lua is clear + * Move docs into doc/ + +Future +====== + + * Plugins + * where do plugins store values (self, storage object, etc?) + * configuration + * global assignments in plugins + * we need a way to do method advice in REPL "subclasses" + * test: using advice from within ifplugin/iffeature + * luaish plugin + * moonscript plugin - compile Moonscript instead of Lua + * Steal ideas from ilua + * Variables in ilua must be declared before use + * -L is like -l, except it automatically brings it into the global NS + * require() wrapper that does this ↑ + * table display logic control, float precision control + * print\_handler (custom print logic for types) + * \_\_pretty + * global\_handler (custom lookup logic to complement strict mode) + * easily done via a plugin + * line\_handler (custom handling of lines before being processed) + * Steal ideas from luaish + * Shell commands (lines beginning with ., filename completion) + * Steal ideas from http://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print\_loop + * Steal ideas from pry, ipython, bpython, Devel::REPL, Factor REPL + * Steal ideas from https://github.com/tpope/vim-foreplay + * Async implementation + * GTK implementation + * IRC bot implementation + * Awesome library diff --git a/lib/lua-repl/dev/RELEASE-MESSAGE b/lib/lua-repl/dev/RELEASE-MESSAGE new file mode 100644 index 00000000..0fdbdf34 --- /dev/null +++ b/lib/lua-repl/dev/RELEASE-MESSAGE @@ -0,0 +1,21 @@ +To: +Subject: [ANN] lua-repl $VERSION + +Hi everyone, + +I'm happy to announce a new version of lua-repl! + +For those of you who aren't aware of it, lua-repl is a library to make +it easy to embed a REPL similar to the standalone interpreter in your +Lua application. It also comes with a suite of plugins for enhancing +itself, as well as a "demo" console REPL that may be used as an +alternative to the standalone REPL (named rep.lua). + +$CHANGE_SUMMARY + +The source for lua-repl is located on Github: +https://github.com/hoelzro/lua-repl. The release tarball is here: +https://github.com/hoelzro/lua-repl/archive/$VERSION.tar.gz + +Thanks, +Rob diff --git a/lib/lua-repl/luarepl-0.10-1.rockspec b/lib/lua-repl/luarepl-0.10-1.rockspec new file mode 100644 index 00000000..4b3401a5 --- /dev/null +++ b/lib/lua-repl/luarepl-0.10-1.rockspec @@ -0,0 +1,38 @@ +package = 'luarepl' +version = '0.10-1' +source = { + url = 'https://github.com/hoelzro/lua-repl/archive/0.10.tar.gz', + dir = 'lua-repl-0.10', +} +description = { + summary = 'A reusable REPL component for Lua, written in Lua', + homepage = 'https://github.com/hoelzro/lua-repl', + license = 'MIT/X11', +} +dependencies = { + 'lua >= 5.1' +} +build = { + type = 'builtin', + modules = { + ['repl'] = 'repl/init.lua', + ['repl.utils'] = 'repl/utils.lua', + ['repl.sync'] = 'repl/sync.lua', + ['repl.compat'] = 'repl/compat.lua', + ['repl.console'] = 'repl/console.lua', + ['repl.plugins.autoreturn'] = 'repl/plugins/autoreturn.lua', + ['repl.plugins.completion'] = 'repl/plugins/completion.lua', + ['repl.plugins.example'] = 'repl/plugins/example.lua', + ['repl.plugins.history'] = 'repl/plugins/history.lua', + ['repl.plugins.keep_last_eval'] = 'repl/plugins/keep_last_eval.lua', + ['repl.plugins.linenoise'] = 'repl/plugins/linenoise.lua', + ['repl.plugins.pretty_print'] = 'repl/plugins/pretty_print.lua', + ['repl.plugins.rcfile'] = 'repl/plugins/rcfile.lua', + ['repl.plugins.semicolon_suppress_output'] = 'repl/plugins/semicolon_suppress_output.lua', + ['repl.plugins.filename_completion'] = 'repl/plugins/filename_completion.lua', + ['repl.plugins.rlwrap'] = 'repl/plugins/rlwrap.lua', + }, + install = { + bin = { 'rep.lua' }, + } +} diff --git a/lib/lua-repl/plugins.md b/lib/lua-repl/plugins.md new file mode 100644 index 00000000..2a3591b4 --- /dev/null +++ b/lib/lua-repl/plugins.md @@ -0,0 +1,285 @@ +# Plugins + +As of version 0.3, lua-repl supports a plugin mechanism. For an example, please download the source code for +lua-repl and look at `repl/plugins/example.lua`. + +# Available Plugins + +lua-repl 0.3 ships with several plugins. If you use the example script `rep.lua`, some of them are loaded automatically; +these are marked with an asterisk. + +## autoreturn (\*) + +Automatically returns the the results of the evaluated expression. So instead of having to type 'return 3', you may +simply type '3'. + +## completion (\*) + +Provides completion facilities via the 'completion' feature. Other plugins may hook into this to provide tab completion. + +## example + +Simply an example plugin. + +## history (\*) + +Provides history facilities (and the 'history' feature), storing each line entered as an individual history entry, +and persisting the history in `$HOME/.rep.lua.history`. Other plugins may hook into this. + +## keep\_last\_eval + +Retains the results of the previously evaluated expression in global variables. The first result is stored in `_1`, the +second in `_2`, etc. For brevity's sake, the first result is also stored in `_`. + +## linenoise (\*) + +Hooks into the linenoise library. Allows the use of tab completion and history. + +## pretty\_print + +'Pretty prints' return values. Tables are printed in expanded form. Colors are provided if lua-term is installed. + +## rcfile (\*) + +Loads Lua code in `$HOME/.rep.lua` if the file exists. The repl object is provided to the file in a variable named `repl`, so +users may load plugins of their choosing. + +## semicolon_suppress_output + +Suppresses automatic printing of an expression's result if the expression ends in a semicolon. + +# Creating a Plugin + +If you would like to create your own plugin, it must be in a file under `repl/plugins/` in your `package.path`. + +# Plugin Objects + +When a plugin is loaded, it is provided with five special objects that are used to affect the behavior of the REPL +object loading the plugin. + +## repl + +The `repl` object is a proxy object for the REPL object loading the plugin; you can invoke methods on it, create methods on +it, set properties on it, etc. However, if you try to create a method on the `repl` object that already exists, an error +will occur. This is to keep plugin authors from stepping on each others' toes. + +## before + +The `before` object is another proxy object from the plugin environment. If you add methods to the `before` object, the original +method remains intact; however, the method you added will be called before the original. For example, if you wanted to print +"I got some results!" before you display them on the command line, your plugin could do this: + +```lua +function before:displayresults(results) + print 'I got some results!' +end +``` + +This is called *advice*, and is stolen from Moose, an object system for the Perl programming language. + +When you apply multiple pieces of advice via `before`, they are called in last-in-first-out order: + +```lua +function before:method() + print 'Second!' +end + +function before:method() + print 'First!' +end +``` + +`before` also receives all of the parameters to the original method. If they are tables, userdata, etc, you may alter them, +which can alter the behavior of the original method, for better or for worse. + +If the method you are applying advice to does not exist on the current REPL object, an error will occur. This way, developers +can find out about API changes quickly, albeit noisily. + +## after + +The `after` object is another proxy object that attaches advice to the loading REPL object. As you can likely tell from its name, +advice applied via the `after` object occurs after the original method. Advice applied via after is called in first-in-first-out order: + +```lua +function after:method() + print 'First!' +end + +function after:method() + print 'Second!' +end +``` + +Like `before`, if you try applying advice to a method that doesn't exist, an error will occur. Also like `before`, after advice receives +all of the parameters passed to the original method. + +## around + +The `around` object is another advice object, but it works a little differently than `before` or `after`. `around` replaces the current +method will the advice, and like `before` and `after`, receives all of the parameters that would be passed to the original. However, +`around` also receives an additional parameter immediately before the parameters: the original method. This way, you can invoke +the method's original functionality if needed. For example: + +```lua +function around:displayresults(orig, results) + print "I'm displaying some results!" + orig(self, results) -- don't forget self! + print "Now I'm done!" +end +``` + +Like the other advice objects, you can't apply advice to a method that doesn't exist. Also, be warned: the `around` advice does nothing +to make sure that the parameters are passed to the original function, and it doesn't make sure that the return values from the original +function are returned. You need to do that yourself. + +## override + +The `override` object isn't really an advice object; adding methods to it will replace the methods in the REPL object itself. However, +it will fail if that method does not already exist. The rule of thumb is if you want to add new methods to the REPL object, use +`repl`; if you want to completely override an existing method, use `override`. Keep in mind this will blow away all advice applied to +a method from other plugins; use with caution! + +# Features + +Sometimes, different plugins will want to provide a method, but implemented in a different way. For example, the completion plugin +included with lua-repl implements a tab completion method; however, if you are embedding lua-repl into your own environment, you may +have a more sophisicated way to provide completions. Other plugins (like the linenoise plugin) may want to hook into the completion +feature itself, without being tied to a particular implementation. So plugins may advertise a list of *features* that they provide, +so that they can develop loose relationships between one another. To advertise features for your plugin, simply set the features variable: + +```lua +features = 'completion' -- make sure you're not setting a local! +``` + +If you wish you provide multiple features, simply use a table: + +```lua +features = { 'completion', 'something_else' } +``` + +Obviously, plugins providing a feature need to agree on a standard interface of methods that they provide. No framework is in place for this as +of yet. + +REPL objects may provide features as well; for example, `repl.console` provides the 'console' feature. You can use this to make sure your +plugins are only loaded in certain environments. + +# REPL Methods + +Now that you know how to affect the behavior of lua-repl with plugins, let's go over the methods you may advise/override, or call yourself from +within your advised/overridden methods. Please keep in mind that since lua-repl is still a young project, this API is subject to change. + +## repl:getprompt(level) + +Returns the prompt string displayed for the given prompt level, which is either 1 or 2. +1 signifies that the REPL is not in a multi-line expression (like a for loop); 2 signifies +otherwise. + +## repl:prompt(level) + +Actually displays the prompt for the given level. You more likely want to deal with +getprompt or showprompt. + +## repl:name() + +Returns the name of the REPL, used when compiling the chunks for evaluation. + +## repl:traceback(err) + +Returns a stack trace, prefixed by the given error message. + +## repl:detectcontinue(err) + +Detects whether or not the given error message means that more input is needed for a complete +chunk. You probably shouldn't touch this. + +## repl:compilechunk(code) + +Compiles the given chunk of code, returning a function, or a falsy value and an error message. + +## repl:getcontext() + +Returns the function environment that the REPL evaluates code in. + +## repl:handleline(line) + +Handles a line of input, returning the prompt level (1 or 2). Note that if this method is +called, an evaluation does not necessarily occur. + +## repl:showprompt(prompt) + +Displays the given prompt. + +## repl:displayresults(results) + +Displays the results from an evaluation. `results` is a table with the individual values in +the integer indices of the table, with the `n` key containing the number of values in the table. + +## repl:displayerror(err) + +Displays an error from an evaluation. + +## repl:hasplugin(plugin) + +Returns `true` if the given plugin has been loaded, `false` otherwise. + +## repl:hasfeature(feature) + +Returns `true` if the given feature has been loaded, `false` otherwise. + +## repl:requirefeature(feature) + +If the given feature has been loaded, do nothing. Otherwise, raise an error. + +## repl:ifplugin(plugin, action) + +If the given plugin has been loaded, call `action`. Otherwise, if the plugin +is ever loaded in the future, call `action` after that loading occurs. + +## repl:iffeature(feature, action) + +If the given feature has been loaded, call `action`. Otherwise, if the feature +is ever loaded in the future, call `action` after that loading occurs. + +## repl:loadplugin(plugin) + +Loads the given plugin. If the plugin returns a value, that value is returned. + +## repl:shutdown() + +Called when the REPL is exited. Don't call this yourself! + +## repl:lines() -- repl.sync only + +Returns an iterator that yields a line of input per invocation. + +# The Future + +This is the first release of lua-repl with plugins. The future will bring various +refinements to the plugin interface, along with the following planned features: + +## Feature Interfaces + +Earlier I mentioned that features have a sort of "gentlemen's aggreement" on what +methods they will provide. It would be nice if the plugin system had a way of +enforcing that. + +## Attribute Storage + +Currently, if a plugin wants to store some information between method calls, it needs +to store it on the REPL object (`self`) and hope no other plugins (or REPL clone) will +use the same name. Plugin-specific storage is a high priority. + +## Configuration + +Currently, plugins don't have any sort of configuration mechanism. I plan to change that. + +## Library Plugins + +Some plugins may want to leverage functionality of others without loading those others into +the REPL itself. I call these *library plugins*. + +## Better Diagnostics + +If you try to add a method that has already been added, or provide a feature that has already been +provided, you receive no information on which plugin provided the method or feature in question. +It would be nice to know. diff --git a/lib/lua-repl/rep.lua b/lib/lua-repl/rep.lua new file mode 100755 index 00000000..029c3192 --- /dev/null +++ b/lib/lua-repl/rep.lua @@ -0,0 +1,41 @@ +#!/usr/bin/env lua + +-- Copyright (c) 2011-2015 Rob Hoelz +-- +-- 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 +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +-- the Software, and to permit persons to whom the Software is furnished to do so, +-- subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +-- FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +-- COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +-- IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +-- CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +-- Not as cool a name as re.pl, but I tried. + +local repl = require 'repl.console' +local rcfile_loaded = repl:loadplugin 'rcfile' + +if not rcfile_loaded then + local has_linenoise = pcall(require, 'linenoise') + + if has_linenoise then + repl:loadplugin 'linenoise' + else + pcall(repl.loadplugin, repl, 'rlwrap') + end + + repl:loadplugin 'history' + repl:loadplugin 'completion' + repl:loadplugin 'autoreturn' +end + +print('Lua REPL ' .. tostring(repl.VERSION)) +repl:run() diff --git a/lib/lua-repl/repl/compat.lua b/lib/lua-repl/repl/compat.lua new file mode 100644 index 00000000..fd47748c --- /dev/null +++ b/lib/lua-repl/repl/compat.lua @@ -0,0 +1,30 @@ +-- Copyright (c) 2011-2015 Rob Hoelz +-- +-- 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 +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +-- the Software, and to permit persons to whom the Software is furnished to do so, +-- subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +-- FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +-- COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +-- IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +-- CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +return { + -- unpack was moved to table.unpack on Lua version 5.2 + -- See https://www.lua.org/manual/5.2/manual.html#8 + unpack = unpack or table.unpack, + -- loadstring was deprecated in favor of load, which was updated + -- to handle string arguments + loadstring = loadstring or load, + -- package.loaders was renamed package.searchers in Lua 5.2 + package = { + searchers = package.loaders or package.searchers + } +} diff --git a/lib/lua-repl/repl/console.lua b/lib/lua-repl/repl/console.lua new file mode 100644 index 00000000..154d0372 --- /dev/null +++ b/lib/lua-repl/repl/console.lua @@ -0,0 +1,57 @@ +-- Copyright (c) 2011-2015 Rob Hoelz +-- +-- 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 +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +-- the Software, and to permit persons to whom the Software is furnished to do so, +-- subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +-- FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +-- COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +-- IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +-- CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +-- @class repl.console +--- This module implements a command line-based REPL, +--- similar to the standalone Lua interpreter. + +local sync_repl = require 'repl.sync' +local compat = require 'repl.compat' +local console_repl = sync_repl:clone() +local stdout = io.stdout +local stdin = io.stdin +local print = print +local unpack = unpack + +-- @see repl:showprompt(prompt) +function console_repl:showprompt(prompt) + stdout:write(prompt .. ' ') +end + +-- @see repl.sync:lines() +function console_repl:lines() + return stdin:lines() +end + +-- @see repl:displayresults(results) +function console_repl:displayresults(results) + if results.n == 0 then + return + end + + print(compat.unpack(results, 1, results.n)) +end + +-- @see repl:displayerror(err) +function console_repl:displayerror(err) + print(err) +end + +console_repl._features.console = true + +return console_repl diff --git a/lib/lua-repl/repl/init.lua b/lib/lua-repl/repl/init.lua new file mode 100644 index 00000000..b5499f59 --- /dev/null +++ b/lib/lua-repl/repl/init.lua @@ -0,0 +1,416 @@ +-- Copyright (c) 2011-2015 Rob Hoelz +-- +-- 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 +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +-- the Software, and to permit persons to whom the Software is furnished to do so, +-- subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +-- FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +-- COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +-- IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +-- CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +-- @class repl +--- This module implements the core functionality of a REPL. + +local plugins_lookup_meta = { __mode = 'k' } + +local repl = { _buffer = '', _plugins = setmetatable({}, plugins_lookup_meta), _features = {}, _ifplugin = {}, _iffeature = {}, VERSION = 0.10 } +local compat = require 'repl.compat' +local select = select +local dtraceback = debug.traceback +local setmetatable = setmetatable +local sformat = string.format +local smatch = string.match +local error = error +local setfenv = require('repl.utils').setfenv + +local function gather_results(success, ...) + local n = select('#', ...) + return success, { n = n, ... } +end + +local function tcopy(t, copy) + copy = copy or {} + + for k, v in pairs(t) do + copy[k] = v + end + + return copy +end + +--- Returns the prompt for a given level. +-- @param level The prompt level. Either 1 or 2. +function repl:getprompt(level) + return level == 1 and '>' or '>>' +end + +--- Displays a prompt for the given prompt level. +-- @param level The prompt level. Either 1 or 2. +function repl:prompt(level) + self:showprompt(self:getprompt(level)) +end + +--- Returns the name of the REPL. For usage in chunk compilation. +-- @return The REPL's name. +-- @see load +function repl:name() + return 'REPL' +end + +--- Gets a traceback for an error. +-- @param ... All of the stuff that xpcall passes to error functions. +-- @see xpcall +-- @return A stack trace. The default implementation returns a simple string-based trace. +function repl:traceback(...) + return dtraceback(...) +end + +--- Uses the compilation error to determine whether or not further input +--- is pending after the last line. You shouldn't have to override this +--- unless you use an implementation of Lua that varies in its error +--- messages. +-- @param err The compilation error from Lua. +-- @return Whether or not the input should continue after this line. +function repl:detectcontinue(err) + return smatch(err, "''$") or smatch(err, "$") +end + +function repl:compilechunk(chunk) + return compat.loadstring(chunk, self:name()) +end + +--- Evaluates a line of input, and displays return value(s). +-- @param line The line to evaluate +-- @return The prompt level (1 or 2) +function repl:handleline(line) + local chunk = self._buffer .. line + local f, err = self:compilechunk(chunk) + + if f then + self._buffer = '' + + setfenv(f, self:getcontext()) + local success, results = gather_results(xpcall(f, function(...) return self:traceback(...) end)) + if success then + self:displayresults(results) + else + self:displayerror(results[1]) + end + else + if self:detectcontinue(err) then + self._buffer = chunk .. '\n' + return 2 + else + self:displayerror(err) + self._buffer = '' + end + end + + return 1 +end + +--- Creates a new REPL object, so you can override methods without fear. +-- @return A REPL clone. +function repl:clone() + local plugins_copy = tcopy(self._plugins, setmetatable({}, plugins_lookup_meta)) + local features_copy = tcopy(self._features) + local ifplugin_copy = {} + local iffeature_copy = {} + + for k, v in pairs(self._ifplugin) do + ifplugin_copy[k] = tcopy(v) + end + + for k, v in pairs(self._iffeature) do + iffeature_copy[k] = tcopy(v) + end + + return setmetatable({ + _buffer = '', + _plugins = plugins_copy, + _features = features_copy, + _ifplugin = ifplugin_copy, + _iffeature = iffeature_copy, + }, { __index = self }) +end + +--- Displays the given prompt to the user. Must be overriden. +-- @param prompt The prompt to display. +function repl:showprompt(prompt) + error 'You must implement the showprompt method' +end + +--- Displays the results from evaluate(). Must be overriden. +-- @param results The results to display. The results are a table, with the integer keys containing the results, and the 'n' key containing the highest integer key. + +function repl:displayresults(results) + error 'You must implement the displayresults method' +end + +--- Displays errors from evaluate(). Must be overriden. +-- @param err The error value returned from repl:traceback(). +-- @see repl:traceback +function repl:displayerror(err) + error 'You must implement the displayerror method' +end + +--- Checks whether this REPL object has loaded the given plugin. +-- @param plugin The plugin that the REPL may have loaded. +function repl:hasplugin(plugin) + return self._plugins[plugin] +end + +function repl:hasfeature(feature) + return self._features[feature] +end + +function repl:requirefeature(feature) + if not self:hasfeature(feature) then + error(sformat('required feature %q not present', feature), 2) + end +end + +function repl:ifplugin(plugin, action) + if self:hasplugin(plugin) then + action() + else + local pending_actions = self._ifplugin[plugin] + + if not pending_actions then + pending_actions = {} + self._ifplugin[plugin] = pending_actions + end + + pending_actions[#pending_actions + 1] = action + end +end + +--- If the given feature has been loaded, call `action`. Otherwise, if the +-- feature is ever loaded in the future, call `action` after that loading occurs. +function repl:iffeature(feature, action) + if self:hasfeature(feature) then + action() + else + local pending_features = self._iffeature[feature] + + if not pending_features then + pending_features = {} + self._iffeature[feature] = pending_features + end + + pending_features[#pending_features + 1] = action + end +end + +local function setup_before(repl) + local mt = {} + + function mt:__newindex(key, value) + if type(value) ~= 'function' then + error(tostring(value) .. " is not a function", 2) + end + + local old_value = repl[key] + + if old_value == nil then + error(sformat("The '%s' method does not exist", key), 2) + end + + repl[key] = function(...) + value(...) + return old_value(...) + end + end + + return setmetatable({}, mt) +end + +local function setup_after(repl) + local mt = {} + + function mt:__newindex(key, value) + if type(value) ~= 'function' then + error(tostring(value) .. " is not a function", 2) + end + + local old_value = repl[key] + + if old_value == nil then + error(sformat("The '%s' method does not exist", key), 2) + end + + repl[key] = function(...) + local _, results = gather_results(true, old_value(...)) + value(...) + return compat.unpack(results, 1, results.n) + end + end + + return setmetatable({}, mt) +end + +local function setup_around(repl) + local mt = {} + + function mt:__newindex(key, value) + if type(value) ~= 'function' then + error(tostring(value) .. " is not a function", 2) + end + + local old_value = repl[key] + + if old_value == nil then + error(sformat("The '%s' method does not exist", key), 2) + end + + repl[key] = function(self, ...) + return value(self, old_value, ...) + end + end + + return setmetatable({}, mt) +end + +local function setup_override(repl) + local mt = {} + + function mt:__newindex(key, value) + if type(value) ~= 'function' then + error(tostring(value) .. " is not a function", 2) + end + + local old_value = repl[key] + + if old_value == nil then + error(sformat("The '%s' method does not exist", key), 2) + end + + repl[key] = value + end + + return setmetatable({}, mt) +end + +local function setup_repl(repl) + local mt = {} + + function mt:__newindex(key, value) + local old_value = repl[key] + + if old_value ~= nil then + error(sformat("The '%s' method already exists", key), 2) + end + + repl[key] = value + end + + function mt:__index(key) + local value = repl[key] + + if type(value) == 'function' then + -- XXX cache this? + return function(_, ...) + return value(repl, ...) + end + end + + return value + end + + return setmetatable({}, mt) +end + +-- TODO use lua-procure for this (eventually) +local function findchunk(name) + for _, loader in pairs(compat.package.searchers) do + local chunk = loader(name) + + if type(chunk) == 'function' then + return chunk + end + end + + error('unable to locate plugin', 3) +end + +function repl:loadplugin(chunk) + if self:hasplugin(chunk) then + error(sformat('plugin %q has already been loaded', tostring(chunk)), 2) + end + self._plugins[chunk] = true + + local plugin_actions = self._ifplugin[chunk] + self._ifplugin[chunk] = nil + + if type(chunk) == 'string' then + chunk = findchunk('repl.plugins.' .. chunk) + end + + local plugin_env = { + repl = setup_repl(self), + before = setup_before(self), + after = setup_after(self), + around = setup_around(self), + override = setup_override(self), + init = function() end, + } + + local function ro_globals(_, key, _) + error(sformat('global environment is read-only (key = %q)', key), 2) + end + + plugin_env._G = plugin_env + plugin_env.features = {} + setmetatable(plugin_env, { __index = _G, __newindex = ro_globals }) + + setfenv(chunk, plugin_env) + local _, results = gather_results(nil, chunk()) + + local features = plugin_env.features or {} + + if type(features) == 'string' then + features = { features } + end + + for _, feature in ipairs(features) do + if self._features[feature] then + error(sformat('feature %q already present', feature), 2) + end + + self._features[feature] = true + + local feature_actions = self._iffeature[feature] + self._iffeature[feature] = nil + if feature_actions then + for _, action in ipairs(feature_actions) do + action() + end + end + end + + if plugin_actions then + for _, action in ipairs(plugin_actions) do + action() + end + end + + return compat.unpack(results, 1, results.n) +end + +-- XXX how to guarantee this gets called? +function repl:shutdown() +end + +function repl:getcontext() + return _G +end + +return repl diff --git a/lib/lua-repl/repl/plugins/autoreturn.lua b/lib/lua-repl/repl/plugins/autoreturn.lua new file mode 100644 index 00000000..a0e15eab --- /dev/null +++ b/lib/lua-repl/repl/plugins/autoreturn.lua @@ -0,0 +1,29 @@ +-- Copyright (c) 2011-2015 Rob Hoelz +-- +-- 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 +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +-- the Software, and to permit persons to whom the Software is furnished to do so, +-- subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +-- FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +-- COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +-- IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +-- CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +-- A plugin that causes the REPL to automatically return evaluation results + +function around:compilechunk(orig, chunk) + local f, err = orig(self, 'return ' .. chunk) + + if not f then + f, err = orig(self, chunk) + end + + return f, err +end diff --git a/lib/lua-repl/repl/plugins/completion.lua b/lib/lua-repl/repl/plugins/completion.lua new file mode 100644 index 00000000..938c7439 --- /dev/null +++ b/lib/lua-repl/repl/plugins/completion.lua @@ -0,0 +1,192 @@ +-- Copyright (c) 2011-2015 Rob Hoelz +-- +-- 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 +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +-- the Software, and to permit persons to whom the Software is furnished to do so, +-- subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +-- FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +-- COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +-- IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +-- CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +local utils = require 'repl.utils' +local getmetatable = getmetatable +local pairs = pairs +local sfind = string.find +local sgmatch = string.gmatch +local smatch = string.match +local ssub = string.sub +local tconcat = table.concat +local tsort = table.sort +local type = type + +local function isindexable(value) + if type(value) == 'table' then + return true + end + + local mt = getmetatable(value) + + return mt and mt.__index +end + +local function getcompletions(t) + local union = {} + + while isindexable(t) do + if type(t) == 'table' then + -- XXX what about the pairs metamethod in 5.2? + -- either we don't care, we implement a __pairs-friendly + -- pairs for 5.1, or implement a 'rawpairs' for 5.2 + for k, v in pairs(t) do + if union[k] == nil then + union[k] = v + end + end + end + + local mt = getmetatable(t) + t = mt and mt.__index or nil + end + + return pairs(union) +end + +local function split_ns(expr) + if expr == '' then + return { '' } + end + + local pieces = {} + + -- XXX method calls too (option?) + for m in sgmatch(expr, '[^.]+') do + pieces[#pieces + 1] = m + end + + -- logic for determining whether to pad the matches with the empty + -- string (ugly) + if ssub(expr, -1) == '.' then + pieces[#pieces + 1] = '' + end + + return pieces +end + +local function determine_ns(expr) + local ns = _G -- XXX what if the REPL lives in a special context? (option?) + local pieces = split_ns(expr) + + for index = 1, #pieces - 1 do + local key = pieces[index] + -- XXX rawget? or regular access? (option?) + ns = ns[key] + + if not isindexable(ns) then + return {}, '', '' + end + end + + expr = pieces[#pieces] + + local prefix = '' + + if #pieces > 1 then + prefix = tconcat(pieces, '.', 1, #pieces - 1) .. '.' + end + + local last_piece = pieces[#pieces] + + local before, after = smatch(last_piece, '(.*):(.*)') + + if before then + ns = ns[before] -- XXX rawget + prefix = prefix .. before .. ':' + expr = after + end + + return ns, prefix, expr +end + +local isidentifierchar + +do + local ident_chars_set = {} + -- XXX I think this can be done with isalpha in C... + local ident_chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.:_0123456789' + + for i = 1, #ident_chars do + local char = ssub(ident_chars, i, i) + ident_chars_set[char] = true + end + + function isidentifierchar(char) + return ident_chars_set[char] + end +end + +local function extract_innermost_expr(expr) + local index = #expr + + while index > 0 do + local char = ssub(expr, index, index) + if isidentifierchar(char) then + index = index - 1 + else + break + end + end + + index = index + 1 + + return ssub(expr, 1, index - 1), ssub(expr, index) +end + +-- XXX is this logic (namely, returning the entire line) too specific to +-- linenoise? +function repl:complete(expr, callback) + if utils.ends_in_unfinished_string(expr) then + return + end + + local ns, prefix, path + + prefix, expr = extract_innermost_expr(expr) + + ns, path, expr = determine_ns(expr) + + prefix = prefix .. path + + local completions = {} + + for k, v in getcompletions(ns) do + if sfind(k, expr, 1, true) == 1 then + local suffix = '' + local type = type(v) + + -- XXX this should be optional + if type == 'function' then + suffix = '(' + elseif type == 'table' then + suffix = '.' + end + + completions[#completions + 1] = prefix .. k .. suffix + end + end + + tsort(completions) + + for _, completion in ipairs(completions) do + callback(completion) + end +end + +features = 'completion' diff --git a/lib/lua-repl/repl/plugins/example.lua b/lib/lua-repl/repl/plugins/example.lua new file mode 100644 index 00000000..d55fd076 --- /dev/null +++ b/lib/lua-repl/repl/plugins/example.lua @@ -0,0 +1,41 @@ +-- Example plugin that demonstrates the objects available to a +-- plugin, as well as the methods that a plugin should make use +-- of + +-- Adding methods and properties to the repl object adds them to +-- the REPL object loading the plugin. If such a method or property +-- already exists, the current plugin will fail to load. +function repl:newmethod(...) +end + +-- Adding methods to the before object causes them to be called +-- before the actual method itself. If the method being added +-- (in this case displayresults) does not exist on the REPL object +-- loading this plugin, the current plugin will fail to load. +function before:displayresults(results) +end + +-- Adding methods to the after object causes them to be called +-- after the actual method itself. If the method being added +-- (in this case displayresults) does not exist on the REPL object +-- loading this plugin, the current plugin will fail to load. +function after:displayresults(results) +end + +-- Adding methods to the around object causes them to be called +-- instead of the original method of the same name. The new +-- method receives all of the arguments that the original would, +-- except it also receives the original method as the first argument. +-- This way, the new method may invoke the original as it pleases. +-- If the method being added (in this case displayresults) does not exist on +-- the REPL object loading this plugin, the current plugin will fail to load. +function around:evalute(orig, chunk) +end + +-- Adding methods to the override object causes them to be called +-- instead of the original method of the same name. If the method being added +-- (in this case displayresults) does not exist on the REPL object loading this +-- plugin, the current plugin will fail to load. +function override:name() + return 'Plugin!' +end diff --git a/lib/lua-repl/repl/plugins/filename_completion.lua b/lib/lua-repl/repl/plugins/filename_completion.lua new file mode 100644 index 00000000..c16729f3 --- /dev/null +++ b/lib/lua-repl/repl/plugins/filename_completion.lua @@ -0,0 +1,63 @@ +local utils = require 'repl.utils' +local lfs = require 'lfs' + +repl:requirefeature 'completion' + +local function guess_directory_separator(file_name) + return file_name:match('/') or + file_name:match('\\') or + '/' +end + +local function split_parent_directory(file_name) + local parent_directory, directory_entry = + file_name:match('^(.+)[\\/](.+)$') + if not parent_directory then + parent_directory = '.' + directory_entry = file_name + end + return parent_directory, directory_entry +end + +local function is_ignored_directory_entry(entry) + return entry == '.' or + entry == '..' +end + +local function replace_end_of_string(str, suffix, replacement) + assert(str:sub(-#suffix) == suffix) + return str:sub(1, -(#suffix+1)) .. replacement +end + +local function complete_file_name(file_name, expr, callback) + local directory, partial_entry = split_parent_directory(file_name) + for entry in lfs.dir(directory) do + if not is_ignored_directory_entry(entry) and + entry:find(partial_entry, 1, true) == 1 then + callback(replace_end_of_string(expr, partial_entry, entry)) + end + end +end + +local function complete_directory(directory, expr, callback) + for entry in lfs.dir(directory) do + if not is_ignored_directory_entry(entry) then + callback(expr..entry) + end + end +end + +function after:complete(expr, callback) + if utils.ends_in_unfinished_string(expr) then + local file_name = expr:match('[%w@/\\.-_+#$%%{}[%]!~ ]+$') + if file_name then + if file_name:find('[/\\]$') then + complete_directory(file_name, expr, callback) + else + complete_file_name(file_name, expr, callback) + end + else + complete_directory('.', expr, callback) + end + end +end diff --git a/lib/lua-repl/repl/plugins/history.lua b/lib/lua-repl/repl/plugins/history.lua new file mode 100644 index 00000000..6330dbd2 --- /dev/null +++ b/lib/lua-repl/repl/plugins/history.lua @@ -0,0 +1,60 @@ +-- Copyright (c) 2011-2015 Rob Hoelz +-- +-- 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 +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +-- the Software, and to permit persons to whom the Software is furnished to do so, +-- subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +-- FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +-- COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +-- IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +-- CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +local history_file + +local function invokecallback(self, name, ...) + if not self._history_callbacks then + return + end + + local impl = self._history_callbacks[name] + return impl(...) +end + +local function init() + if os.getenv 'HOME' then + history_file = os.getenv('HOME') .. '/.rep.lua.history' + end +end + +-- XXX I don't know if this callback setup way +-- is the best way to go about this (in fact +-- I'm pretty sure it isn't), but I just need +-- something that works right now. +function repl:setuphistorycallbacks(callbacks) + self._history_callbacks = callbacks + + if history_file then + invokecallback(self, 'load', history_file) + end +end + +function after:handleline(line) + invokecallback(self, 'addline', line) +end + +function before:shutdown() + if history_file then + invokecallback(self, 'save', history_file) + end +end + +features = 'history' + +init() diff --git a/lib/lua-repl/repl/plugins/keep_last_eval.lua b/lib/lua-repl/repl/plugins/keep_last_eval.lua new file mode 100644 index 00000000..01a77946 --- /dev/null +++ b/lib/lua-repl/repl/plugins/keep_last_eval.lua @@ -0,0 +1,43 @@ +-- Copyright (c) 2011-2015 Rob Hoelz +-- +-- 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 +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +-- the Software, and to permit persons to whom the Software is furnished to do so, +-- subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +-- FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +-- COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +-- IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +-- CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +-- A plugin that stores the results of the last evaluation in _G._ + +local tostring = tostring + +function before:displayresults(results) + local context = self:getcontext() + + if self._keep_eval_lastn then + context._ = nil + + for i = 1, self._keep_eval_lastn do + context['_' .. tostring(i)] = nil + end + end + + if results.n > 0 then + context._ = results[1] + + for i = 1, results.n do + context['_' .. tostring(i)] = results[i] + end + + self._keep_eval_lastn = results.n + end +end diff --git a/lib/lua-repl/repl/plugins/linenoise.lua b/lib/lua-repl/repl/plugins/linenoise.lua new file mode 100644 index 00000000..4407c535 --- /dev/null +++ b/lib/lua-repl/repl/plugins/linenoise.lua @@ -0,0 +1,59 @@ +-- Copyright (c) 2011-2015 Rob Hoelz +-- +-- 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 +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +-- the Software, and to permit persons to whom the Software is furnished to do so, +-- subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +-- FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +-- COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +-- IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +-- CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +-- A plugin that uses linenoise (https://github.com/hoelzro/lua-linenoise) for prompting + +local ln = require 'linenoise' + +repl:requirefeature 'console' + +function override:showprompt(prompt) + self._prompt = prompt -- XXX how do we make sure other plugins don't step on this? +end + +function override:lines() + return function() + return ln.linenoise(self._prompt .. ' ') + end +end + +repl:iffeature('completion', function() + ln.setcompletion(function(completions, line) + repl:complete(line, function(completion) + ln.addcompletion(completions, completion) + end) + end) +end) + +repl:ifplugin('history', function() + repl:setuphistorycallbacks { + load = function(filename) + ln.historyload(filename) + end, + + addline = function(line) + ln.historyadd(line) + end, + + save = function(filename) + ln.historysave(filename) + end, + } +end) + +features = 'input' diff --git a/lib/lua-repl/repl/plugins/pretty_print.lua b/lib/lua-repl/repl/plugins/pretty_print.lua new file mode 100644 index 00000000..2318f444 --- /dev/null +++ b/lib/lua-repl/repl/plugins/pretty_print.lua @@ -0,0 +1,262 @@ +-- Copyright (c) 2011-2015 Rob Hoelz +-- +-- 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 +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +-- the Software, and to permit persons to whom the Software is furnished to do so, +-- subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +-- FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +-- COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +-- IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +-- CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +-- Pretty prints expression results (console only) + +local format = string.format +local tconcat = table.concat +local tsort = table.sort +local tostring = tostring +local type = type +local floor = math.floor +local pairs = pairs +local ipairs = ipairs +local error = error +local stderr = io.stderr + +pcall(require, 'luarocks.require') +local ok, term = pcall(require, 'term') +if not ok then + term = nil +end + +local keywords = { + ['and'] = true, + ['break'] = true, + ['do'] = true, + ['else'] = true, + ['elseif'] = true, + ['end'] = true, + ['false'] = true, + ['for'] = true, + ['function'] = true, + ['if'] = true, + ['in'] = true, + ['local'] = true, + ['nil'] = true, + ['not'] = true, + ['or'] = true, + ['repeat'] = true, + ['return'] = true, + ['then'] = true, + ['true'] = true, + ['until'] = true, + ['while'] = true, +} + +local function compose(f, g) + return function(...) + return f(g(...)) + end +end + +local emptycolormap = setmetatable({}, { __index = function() + return function(s) + return s + end +end}) + +local colormap = emptycolormap + +if term then + colormap = { + ['nil'] = term.colors.blue, + string = term.colors.yellow, + punctuation = compose(term.colors.green, term.colors.bright), + ident = term.colors.red, + boolean = term.colors.green, + number = term.colors.cyan, + path = term.colors.white, + misc = term.colors.magenta, + } +end + +local function isinteger(n) + return type(n) == 'number' and floor(n) == n +end + +local function isident(s) + return type(s) == 'string' and not keywords[s] and s:match('^[a-zA-Z_][a-zA-Z0-9_]*$') +end + +-- most of these are arbitrary, I *do* want numbers first, though +local type_order = { + number = 0, + string = 1, + userdata = 2, + table = 3, + thread = 4, + boolean = 5, + ['function'] = 6, + cdata = 7, +} + +local function cross_type_order(a, b) + local pos_a = type_order[ type(a) ] + local pos_b = type_order[ type(b) ] + + if pos_a == pos_b then + return a < b + else + return pos_a < pos_b + end +end + +local function sortedpairs(t) + local keys = {} + + local seen_non_string + + for k in pairs(t) do + keys[#keys + 1] = k + + if not seen_non_string and type(k) ~= 'string' then + seen_non_string = true + end + end + + local sort_func = seen_non_string and cross_type_order or nil + tsort(keys, sort_func) + + local index = 1 + return function() + if keys[index] == nil then + return nil + else + local key = keys[index] + local value = t[key] + index = index + 1 + + return key, value + end + end, keys +end + +local function find_longstring_nest_level(s) + local level = 0 + + while s:find(']' .. string.rep('=', level) .. ']', 1, true) do + level = level + 1 + end + + return level +end + +local function dump(params) + local pieces = params.pieces + local seen = params.seen + local path = params.path + local v = params.value + local indent = params.indent + + local t = type(v) + + if t == 'nil' or t == 'boolean' or t == 'number' then + pieces[#pieces + 1] = colormap[t](tostring(v)) + elseif t == 'string' then + if v:match '\n' then + local level = find_longstring_nest_level(v) + pieces[#pieces + 1] = colormap.string('[' .. string.rep('=', level) .. '[' .. v .. ']' .. string.rep('=', level) .. ']') + else + pieces[#pieces + 1] = colormap.string(format('%q', v)) + end + elseif t == 'table' then + if seen[v] then + pieces[#pieces + 1] = colormap.path(seen[v]) + return + end + + seen[v] = path + + local lastintkey = 0 + + pieces[#pieces + 1] = colormap.punctuation '{\n' + for i, v in ipairs(v) do + for j = 1, indent do + pieces[#pieces + 1] = ' ' + end + dump { + pieces = pieces, + seen = seen, + path = path .. '[' .. tostring(i) .. ']', + value = v, + indent = indent + 1, + } + pieces[#pieces + 1] = colormap.punctuation ',\n' + lastintkey = i + end + + for k, v in sortedpairs(v) do + if not (isinteger(k) and k <= lastintkey and k > 0) then + for j = 1, indent do + pieces[#pieces + 1] = ' ' + end + + if isident(k) then + pieces[#pieces + 1] = colormap.ident(k) + else + pieces[#pieces + 1] = colormap.punctuation '[' + dump { + pieces = pieces, + seen = seen, + path = path .. '.' .. tostring(k), + value = k, + indent = indent + 1, + } + pieces[#pieces + 1] = colormap.punctuation ']' + end + pieces[#pieces + 1] = colormap.punctuation ' = ' + dump { + pieces = pieces, + seen = seen, + path = path .. '.' .. tostring(k), + value = v, + indent = indent + 1, + } + pieces[#pieces + 1] = colormap.punctuation ',\n' + end + end + + for j = 1, indent - 1 do + pieces[#pieces + 1] = ' ' + end + + pieces[#pieces + 1] = colormap.punctuation '}' + else + pieces[#pieces + 1] = colormap.misc(tostring(v)) + end +end + +repl:requirefeature 'console' + +function override:displayresults(results) + local pieces = {} + + for i = 1, results.n do + dump { + pieces = pieces, + seen = {}, + path = '', + value = results[i], + indent = 1, + } + pieces[#pieces + 1] = '\n' + end + + stderr:write(tconcat(pieces, '')) +end diff --git a/lib/lua-repl/repl/plugins/rcfile.lua b/lib/lua-repl/repl/plugins/rcfile.lua new file mode 100644 index 00000000..a74e81ac --- /dev/null +++ b/lib/lua-repl/repl/plugins/rcfile.lua @@ -0,0 +1,54 @@ +-- Copyright (c) 2011-2015 Rob Hoelz +-- +-- 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 +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +-- the Software, and to permit persons to whom the Software is furnished to do so, +-- subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +-- FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +-- COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +-- IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +-- CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +-- A plugin that runs code in $HOME/.rep.lua before the REPL starts + +local setfenv = require('repl.utils').setfenv + +local function readable(filename) + local f = io.open(filename, 'r') + if not f then + return false + end + f:close() + return true +end + +local function init() + local home = os.getenv 'HOME' + + if not home then + return + end + + local rcfile = home .. '/.rep.lua' + + if not readable(rcfile) then + return + end + + local chunk = assert(loadfile(rcfile)) + local env = setmetatable({ repl = repl }, { __index = _G, __newindex = _G }) + + setfenv(chunk, env) + + chunk() + return true +end + +return init() diff --git a/lib/lua-repl/repl/plugins/rlwrap.lua b/lib/lua-repl/repl/plugins/rlwrap.lua new file mode 100644 index 00000000..9f195a3f --- /dev/null +++ b/lib/lua-repl/repl/plugins/rlwrap.lua @@ -0,0 +1,41 @@ +-- Copyright (c) 2011-2015 Rob Hoelz +-- +-- 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 +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +-- the Software, and to permit persons to whom the Software is furnished to do so, +-- subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +-- FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +-- COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +-- IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +-- CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +if os.getenv 'LUA_REPL_RLWRAP' then + features = 'input' +else + -- XXX check that we're not receiving input from a non-tty + local has_rlwrap = os.execute('which rlwrap >/dev/null 2>/dev/null') + + if type(has_rlwrap) ~= 'boolean' then + has_rlwrap = has_rlwrap == 0 + end + + if not has_rlwrap then + error 'Please install rlwrap in order to use the rlwrap plugin' + end + + local lowest_index = -1 + + while arg[lowest_index] ~= nil do + lowest_index = lowest_index - 1 + end + lowest_index = lowest_index + 1 + os.execute(string.format('LUA_REPL_RLWRAP=1 rlwrap %q %q', arg[lowest_index], arg[0])) + os.exit(0) +end diff --git a/lib/lua-repl/repl/plugins/semicolon_suppress_output.lua b/lib/lua-repl/repl/plugins/semicolon_suppress_output.lua new file mode 100644 index 00000000..4adaecb6 --- /dev/null +++ b/lib/lua-repl/repl/plugins/semicolon_suppress_output.lua @@ -0,0 +1,36 @@ +-- Copyright (c) 2011-2015 Rob Hoelz +-- +-- 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 +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +-- the Software, and to permit persons to whom the Software is furnished to do so, +-- subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +-- FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +-- COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +-- IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +-- CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +local smatch = string.match + +-- XXX will this affect any other plugins? +function around:compilechunk(orig, chunk) + local f, err = orig(self, chunk) + + if not f then + return f, err + end + + if smatch(chunk, ';%s*$') then + return function() + f() + end + end + + return f +end diff --git a/lib/lua-repl/repl/sync.lua b/lib/lua-repl/repl/sync.lua new file mode 100644 index 00000000..a422ed4b --- /dev/null +++ b/lib/lua-repl/repl/sync.lua @@ -0,0 +1,46 @@ +-- Copyright (c) 2011-2015 Rob Hoelz +-- +-- 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 +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +-- the Software, and to permit persons to whom the Software is furnished to do so, +-- subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +-- FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +-- COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +-- IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +-- CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +local repl = require 'repl' +local sync_repl = repl:clone() +local error = error + +-- @class repl.sync +--- This module implements a synchronous REPL. It provides +--- a run() method for actually running the REPL, and requires +--- that implementors implement the lines() method. + +--- Run a REPL loop in a synchronous fashion. +-- @name repl.sync:run +function sync_repl:run() + self:prompt(1) + for line in self:lines() do + local level = self:handleline(line) + self:prompt(level) + end + self:shutdown() +end + +--- Returns an iterator that yields lines to be evaluated. +-- @name repl.sync:lines +-- @return An iterator. +function sync_repl:lines() + error 'You must implement the lines method' +end + +return sync_repl diff --git a/lib/lua-repl/repl/utils.lua b/lib/lua-repl/repl/utils.lua new file mode 100644 index 00000000..3e2ca18f --- /dev/null +++ b/lib/lua-repl/repl/utils.lua @@ -0,0 +1,70 @@ +-- Copyright (c) 2011-2015 Rob Hoelz +-- +-- 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 +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +-- the Software, and to permit persons to whom the Software is furnished to do so, +-- subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +-- FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +-- COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +-- IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +-- CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +local setfenv = setfenv or function(f, t) + local upvalue_index = 1 + + -- XXX we may need a utility library if debug isn't available + while true do + local name = debug.getupvalue(f, upvalue_index) + -- some functions don't have an _ENV upvalue, because + -- they never refer to globals + if not name then + return + end + if name == '_ENV' then + debug.setupvalue(f, upvalue_index, t) + return + end + upvalue_index = upvalue_index + 1 + end +end + +--- Tests wheter an expression ends in an unfinished string literal. +-- @return First position in the unfinished string literal or `nil`. +local function ends_in_unfinished_string(expr) + local position = 0 + local quote + local current_delimiter + local last_unmatched_start + while true do + -- find all quotes: + position, quote = expr:match('()([\'"])', position+1) + if not position then break end + -- if we're currently in a string: + if current_delimiter then + -- would end current string? + if current_delimiter == quote then + -- not escaped? + if expr:sub(position-1, position-1) ~= '\\' then + current_delimiter = nil + last_unmatched_start = nil + end + end + else + current_delimiter = quote + last_unmatched_start = position+1 + end + end + return last_unmatched_start +end + +return { + setfenv = setfenv, + ends_in_unfinished_string = ends_in_unfinished_string +} diff --git a/lib/lua-repl/t/abstract-repl-tests.lua b/lib/lua-repl/t/abstract-repl-tests.lua new file mode 100644 index 00000000..50d4b07c --- /dev/null +++ b/lib/lua-repl/t/abstract-repl-tests.lua @@ -0,0 +1,33 @@ +-- vim:foldmethod=marker +local repl = require 'repl' +pcall(require, 'luarocks.loader') +require 'Test.More' + +plan(8) + +do -- getprompt tests {{{ + is(repl:getprompt(1), '>') + is(repl:getprompt(2), '>>') +end -- }}} + +do -- prompt abstract tests {{{ + error_like(function() + repl:prompt(1) + end, 'You must implement the showprompt method') + + error_like(function() + repl:prompt(2) + end, 'You must implement the showprompt method') +end -- }}} + +do -- name tests {{{ + is(repl:name(), 'REPL') +end -- }}} + +do -- handleline abstract tests {{{ + is(_G.testresult, nil) + error_like(function() + repl:handleline '_G.testresult = 17' + end, 'You must implement the displayresults method') + is(_G.testresult, 17) +end -- }}} diff --git a/lib/lua-repl/t/clone-repl-tests.lua b/lib/lua-repl/t/clone-repl-tests.lua new file mode 100644 index 00000000..3ec82f92 --- /dev/null +++ b/lib/lua-repl/t/clone-repl-tests.lua @@ -0,0 +1,119 @@ +-- vim:foldmethod=marker +local repl = require 'repl' +pcall(require, 'luarocks.loader') +require 'Test.More' + +plan(45) + +local clone = repl:clone() +local prompt +local results +local errmsg + +isnt(type(clone), 'nil') + +function clone:showprompt(p) + prompt = p +end + +function clone:displayresults(r) + results = r +end + +function clone:displayerror(err) + errmsg = err +end + +do -- prompt tests {{{ + lives_ok(function() + clone:prompt(1) + end) + + is(prompt, '>') + + lives_ok(function() + clone:prompt(2) + end) + + is(prompt, '>>') +end -- }}} + +do -- handleline tests {{{ + is(_G.testresult, nil) + is(results, nil) + + lives_ok(function() + clone:handleline '_G.testresult = 18' + end) + + is(_G.testresult, 18) + + is(type(results), 'table') + is(results.n, 0) + is(#results, 0) + + lives_ok(function() + clone:handleline 'return 19' + end) + + is(type(results), 'table') + is(results.n, 1) + is(#results, 1) + is(results[1], 19) + + lives_ok(function() + clone:handleline 'return 20, 21, 22' + end) + + is(type(results), 'table') + is(results.n, 3) + is(#results, 3) + is(results[1], 20) + is(results[2], 21) + is(results[3], 22) + + lives_ok(function() + clone:handleline 'return 1, nil, nil, nil, nil, nil, nil, 2' + end) + + is(type(results), 'table') + is(results.n, 8) + is(results[1], 1) + for i = 2, 7 do + is(results[i], nil) + end + is(results[8], 2) +end -- }}} + +do -- error handling tests {{{ + lives_ok(function() + clone:handleline '3 4' + end) + + isnt(type(errmsg), 'nil') + + errmsg = nil + + lives_ok(function() + clone:handleline 'error "foo"' + end) + + like(errmsg, 'foo') +end -- }}} + +do -- multi-line input tests {{{ + errmsg = nil + _G.t = {} + + lives_ok(function() + clone:handleline 'for i = 1, 3 do' + clone:handleline ' table.insert(_G.t, i)' + clone:handleline 'end' + end) + + is(errmsg, nil) + is(#_G.t, 3) + is(_G.t[1], 1) + is(_G.t[2], 2) + is(_G.t[3], 3) +end -- }}} diff --git a/lib/lua-repl/t/lib/test-utils.lua b/lib/lua-repl/t/lib/test-utils.lua new file mode 100644 index 00000000..a0bb16ac --- /dev/null +++ b/lib/lua-repl/t/lib/test-utils.lua @@ -0,0 +1,56 @@ +local _M = {} + +function _M.next_line_number() + local info = debug.getinfo(2, 'l') + return info.currentline + 1 -- doesn't work with whitespace +end + +function _M.cmp_tables(lhs, rhs) + local ok = true + local got + local expected + local failing_k + + for k, v in pairs(lhs) do + local rv = rhs[k] + + if v ~= rv then + ok = false + failing_k = k + got = v + expected = rv + break + end + end + + if ok then + for k, v in pairs(rhs) do + local lv = lhs[k] + + if v ~= lv then + ok = false + failing_k = k + got = lv + expected = v + break + end + end + end + + if ok then + pass() + else + fail 'value mismatch' + diag(string.format(' got[%q]: %s', tostring(failing_k), tostring(got))) + diag(string.format('expected[%q]: %s', tostring(failing_k), tostring(expected))) + end +end + +function _M.gather_results(...) + return { + n = select('#', ...), + ..., + } +end + +return _M diff --git a/lib/lua-repl/t/plugin-after-tests.lua b/lib/lua-repl/t/plugin-after-tests.lua new file mode 100644 index 00000000..15a24c11 --- /dev/null +++ b/lib/lua-repl/t/plugin-after-tests.lua @@ -0,0 +1,202 @@ +-- vim:foldmethod=marker +local repl = require 'repl' +pcall(require, 'luarocks.loader') +require 'Test.More' +local utils = require 'test-utils' + +plan(26) + +local clone = repl:clone() + +do -- basic tests {{{ + local with_plugin = clone:clone() + + local has_called_normal + local has_called_after + + function with_plugin:foo() + has_called_normal = true + end + + with_plugin:loadplugin(function() + function after:foo() + has_called_after = true + ok(has_called_normal) + end + end) + + with_plugin:foo() + ok(has_called_normal) + ok(has_called_after) + + local line_no + + local _, err = pcall(function() + with_plugin:loadplugin(function() + line_no = utils.next_line_number() + function after:nonexistent() + end + end) + end) + + like(err, string.format("%d: The 'nonexistent' method does not exist", line_no)) + + _, err = pcall(function() + with_plugin:loadplugin(function() + line_no = utils.next_line_number() + after.foo = 17 + end) + end) + + like(err, string.format('%d: 17 is not a function', line_no)) +end -- }}} + +do -- arguments tests {{{ + local with_plugin = clone:clone() + local orig_args + local got_args + + function with_plugin:foo(...) + orig_args = { + n = select('#', ...), + ..., + } + end + + with_plugin:loadplugin(function() + function after:foo(...) + got_args = { + n = select('#', ...), + ..., + } + end + end) + + with_plugin:foo() + is(got_args.n, 0) + utils.cmp_tables(orig_args, got_args) + + with_plugin:foo(1, 2, 3) + is(got_args.n, 3) + is(got_args[1], 1) + is(got_args[2], 2) + is(got_args[3], 3) + utils.cmp_tables(orig_args, got_args) + + with_plugin:foo(1, nil, nil, nil, 5) + is(got_args.n, 5) + is(got_args[1], 1) + is(got_args[2], nil) + is(got_args[3], nil) + is(got_args[4], nil) + is(got_args[5], 5) + utils.cmp_tables(orig_args, got_args) +end -- }}} + +do -- exception tests {{{ + local with_plugin = clone:clone() + + local has_called_original + + function with_plugin:foo() + has_called_original = true + end + + with_plugin:loadplugin(function() + function after:foo() + error 'uh-oh' + end + end) + + local _, err = pcall(with_plugin.foo, with_plugin) + + like(err, 'uh%-oh') + ok(has_called_original) +end -- }}} + +do -- return value tests {{{ + local with_plugin = clone:clone() + + function with_plugin:foo() + return 17 + end + + function with_plugin:bar() + return 18, 19, 20 + end + + function with_plugin:baz() + return 1, nil, nil, nil, 5 + end + + with_plugin:loadplugin(function() + function after:foo() + return 18 + end + + function after:bar() + return 18 + end + + function after:baz() + return 18 + end + end) + + local result = with_plugin:foo() + is(result, 17) + + local results = utils.gather_results(with_plugin:bar()) + utils.cmp_tables(results, { n = 3, 18, 19, 20 }) + + results = utils.gather_results(with_plugin:baz()) + utils.cmp_tables(results, { n = 5, 1, nil, nil, nil, 5 }) +end -- }}} + +do -- multiple advice, multiple plugins {{{ + local with_plugin = clone:clone() + local calls = {} + + function with_plugin:foo() + calls[#calls + 1] = 'original' + end + + with_plugin:loadplugin(function() + function after:foo() + calls[#calls + 1] = 'first' + end + end) + + with_plugin:loadplugin(function() + function after:foo() + calls[#calls + 1] = 'second' + end + end) + + with_plugin:foo() + + utils.cmp_tables(calls, { 'original', 'first', 'second' }) +end -- }}} + +do -- multiple advice, single plugin {{{ + local with_plugin = clone:clone() + local calls = {} + + function with_plugin:foo() + calls[#calls + 1] = 'original' + end + + with_plugin:loadplugin(function() + function after:foo() + calls[#calls + 1] = 'first' + end + + function after:foo() + calls[#calls + 1] = 'second' + end + end) + + with_plugin:foo() + + utils.cmp_tables(calls, { 'original', 'first', 'second' }) +end -- }}} diff --git a/lib/lua-repl/t/plugin-around-tests.lua b/lib/lua-repl/t/plugin-around-tests.lua new file mode 100644 index 00000000..df0ea073 --- /dev/null +++ b/lib/lua-repl/t/plugin-around-tests.lua @@ -0,0 +1,218 @@ +-- vim:foldmethod=marker +local repl = require 'repl' +local compat = require 'repl.compat' +pcall(require, 'luarocks.loader') +require 'Test.More' +local utils = require 'test-utils' + +plan(27) + +local clone = repl:clone() + +do -- basic tests {{{ + local with_plugin = clone:clone() + + local has_called_normal + local has_called_around + + function with_plugin:foo() + has_called_normal = true + end + + with_plugin:loadplugin(function() + function around:foo(orig, ...) + has_called_around = true + ok(not has_called_normal) + local return_values = utils.gather_results(orig(self, ...)) + ok(has_called_normal) + return compat.unpack(return_values, 1, return_values.n) + end + end) + + with_plugin:foo() + ok(has_called_normal) + ok(has_called_around) + + local line_no + + local _, err = pcall(function() + with_plugin:loadplugin(function() + line_no = utils.next_line_number() + function around:nonexistent() + end + end) + end) + + like(err, string.format("%d: The 'nonexistent' method does not exist", line_no)) + + _, err = pcall(function() + with_plugin:loadplugin(function() + line_no = utils.next_line_number() + around.foo = 17 + end) + end) + + like(err, string.format('%d: 17 is not a function', line_no)) +end -- }}} + +do -- arguments tests {{{ + local with_plugin = clone:clone() + local orig_args + local got_args + + function with_plugin:foo(...) + orig_args = { + n = select('#', ...), + ..., + } + end + + with_plugin:loadplugin(function() + function around:foo(orig, ...) + got_args = { + n = select('#', ...), + ..., + } + + return orig(self, ...) + end + end) + + with_plugin:foo() + is(got_args.n, 0) + utils.cmp_tables(orig_args, got_args) + + with_plugin:foo(1, 2, 3) + is(got_args.n, 3) + is(got_args[1], 1) + is(got_args[2], 2) + is(got_args[3], 3) + utils.cmp_tables(orig_args, got_args) + + with_plugin:foo(1, nil, nil, nil, 5) + is(got_args.n, 5) + is(got_args[1], 1) + is(got_args[2], nil) + is(got_args[3], nil) + is(got_args[4], nil) + is(got_args[5], 5) + utils.cmp_tables(orig_args, got_args) +end -- }}} + +do -- exception tests {{{ + local with_plugin = clone:clone() + + local has_called_original + + function with_plugin:foo() + has_called_original = true + end + + with_plugin:loadplugin(function() + function around:foo() + error 'uh-oh' + end + end) + + local _, err = pcall(with_plugin.foo, with_plugin) + + like(err, 'uh%-oh') + ok(not has_called_original) +end -- }}} + +do -- return value tests {{{ + local with_plugin = clone:clone() + + function with_plugin:foo() + return 17 + end + + function with_plugin:bar() + return 18, 19, 20 + end + + function with_plugin:baz() + return 1, nil, nil, nil, 5 + end + + with_plugin:loadplugin(function() + function around:foo() + return 18 + end + + function around:bar() + return 19, 20, 21 + end + + function around:baz() + return 1, nil, nil, 4 + end + end) + + local result = with_plugin:foo() + is(result, 18) + + local results = utils.gather_results(with_plugin:bar()) + utils.cmp_tables(results, { n = 3, 19, 20, 21 }) + + results = utils.gather_results(with_plugin:baz()) + utils.cmp_tables(results, { n = 4, 1, nil, nil, 4 }) +end -- }}} + +do -- multiple advice, multiple plugins {{{ + local with_plugin = clone:clone() + local calls = {} + + function with_plugin:foo() + calls[#calls + 1] = 'original' + end + + with_plugin:loadplugin(function() + function around:foo(orig) + calls[#calls + 1] = 'before_one' + orig() + calls[#calls + 1] = 'after_one' + end + end) + + with_plugin:loadplugin(function() + function around:foo(orig) + calls[#calls + 1] = 'before_two' + orig() + calls[#calls + 1] = 'after_two' + end + end) + + with_plugin:foo() + + utils.cmp_tables(calls, { 'before_two', 'before_one', 'original', + 'after_one', 'after_two' }) +end -- }}} + +do -- multiple advice, single plugin {{{ + local with_plugin = clone:clone() + local calls = {} + + function with_plugin:foo() + calls[#calls + 1] = 'original' + end + + with_plugin:loadplugin(function() + function around:foo(orig) + calls[#calls + 1] = 'before_one' + orig() + calls[#calls + 1] = 'after_one' + end + + function around:foo(orig) + calls[#calls + 1] = 'before_two' + orig() + calls[#calls + 1] = 'after_two' + end + end) + + with_plugin:foo() + + utils.cmp_tables(calls, { 'before_two', 'before_one', 'original', + 'after_one', 'after_two' }) +end -- }}} diff --git a/lib/lua-repl/t/plugin-basic-tests.lua b/lib/lua-repl/t/plugin-basic-tests.lua new file mode 100644 index 00000000..21283ef8 --- /dev/null +++ b/lib/lua-repl/t/plugin-basic-tests.lua @@ -0,0 +1,206 @@ +-- vim:foldmethod=marker +local repl = require 'repl' +local utils = require 'test-utils' +pcall(require, 'luarocks.loader') +require 'Test.More' + +plan(28) + +local clone = repl:clone() + +do -- basic tests {{{ + local loaded + + clone:loadplugin(function() + loaded = true + end) + + ok(loaded) + + error_like(function() + clone:loadplugin(function() + error 'uh-oh' + end) + end, 'uh%-oh') + +end -- }}} + +do -- loading the same plugin twice {{{ + local function plugin() + end + + local line_no + + clone:loadplugin(plugin) + local _, err = pcall(function() + line_no = utils.next_line_number() + clone:loadplugin(plugin) + end) + like(err, tostring(line_no) .. ': plugin "function:%s+%S+" has already been loaded') + + _, err = pcall(function() + line_no = utils.next_line_number() + clone:clone():loadplugin(plugin) + end) + like(err, tostring(line_no) .. ': plugin "function:%s+%S+" has already been loaded') + + repl:clone():loadplugin(plugin) + repl:clone():loadplugin(plugin) +end -- }}} + +do -- loading plugins by name {{{ + local loaded + + package.preload['repl.plugins.test'] = function() + loaded = true + end + + clone:clone():loadplugin 'test' + + ok(loaded) + loaded = false + + clone:clone():loadplugin 'test' + + ok(loaded, 'loading a plugin twice should initialize it twice') + + package.preload['repl.plugins.test'] = function() + error 'uh-oh' + end + + error_like(function() + clone:clone():loadplugin 'test' + end, 'uh%-oh') + + package.preload['repl.plugins.test'] = nil + + local line_no + + local _, err = pcall(function() + line_no = utils.next_line_number() + clone:clone():loadplugin 'test' + end) + like(err, tostring(line_no) .. ': unable to locate plugin') +end -- }}} + +do -- hasplugin tests {{{ + local child = repl:clone() + + local plugin = function() + end + + child:loadplugin(plugin) + + local grandchild = child:clone() + + ok(not repl:hasplugin(plugin)) + ok(child:hasplugin(plugin)) + ok(grandchild:hasplugin(plugin)) + + plugin = function() + end + + child:loadplugin(plugin) + + ok(not repl:hasplugin(plugin)) + ok(child:hasplugin(plugin)) + ok(not grandchild:hasplugin(plugin)) +end -- }}} + +do -- global tests {{{ + local clone = repl:clone() + local line_no + + local _, err = pcall(function() + clone:loadplugin(function() + line_no = utils.next_line_number() + foo = 17 + end) + end) + + like(err, tostring(line_no) .. ': global environment is read%-only %(key = "foo"%)') + + _, err = pcall(function() + clone:loadplugin(function() + line_no = utils.next_line_number() + _G.foo = 17 + end) + end) + + like(err, tostring(line_no) .. ': global environment is read%-only %(key = "foo"%)') +end -- }}} + +do -- ifplugin tests {{{ + local clone = repl:clone() + local has_run + + package.preload['repl.plugins.test'] = function() + end + + clone:ifplugin('test', function() + has_run = true + end) + + ok(not has_run) + + clone:loadplugin 'test' + + ok(has_run) + + has_run = false + + clone:ifplugin('test', function() + has_run = true + end) + + ok(has_run) +end -- }}} + +do -- ifplugin multiple times {{{ + local clone = repl:clone() + local has_run + local has_run2 + + package.preload['repl.plugins.test'] = function() + end + + clone:ifplugin('test', function() + has_run = true + end) + + clone:ifplugin('test', function() + has_run2 = true + end) + + clone:loadplugin 'test' + + ok(has_run) + ok(has_run2) +end -- }}} + +do -- plugin return value {{{ + local clone = repl:clone() + + local result = clone:loadplugin(function() + return 17 + end) + + local result2 = clone:loadplugin(function() + end) + + local result3, result4 = clone:loadplugin(function() + return 18, 19 + end) + + local result5, result6, result7 = clone:loadplugin(function() + return 20, nil, 21 + end) + + is(result, 17) + is(result2, nil) + is(result3, 18) + is(result4, 19) + is(result5, 20) + is(result6, nil) + is(result7, 21) +end -- }}} diff --git a/lib/lua-repl/t/plugin-before-tests.lua b/lib/lua-repl/t/plugin-before-tests.lua new file mode 100644 index 00000000..cc23ba78 --- /dev/null +++ b/lib/lua-repl/t/plugin-before-tests.lua @@ -0,0 +1,201 @@ +-- vim:foldmethod=marker +local repl = require 'repl' +pcall(require, 'luarocks.loader') +require 'Test.More' +local utils = require 'test-utils' + +plan(26) + +local clone = repl:clone() + +do -- basic tests {{{ + local with_plugin = clone:clone() + + local has_called_normal + local has_called_before + + function with_plugin:foo() + has_called_normal = true + end + + with_plugin:loadplugin(function() + function before:foo() + has_called_before = true + ok(not has_called_normal) + end + end) + + with_plugin:foo() + ok(has_called_normal) + ok(has_called_before) + + local line_no + + local _, err = pcall(function() + with_plugin:loadplugin(function() + line_no = utils.next_line_number() + function before:nonexistent() + end + end) + end) + + like(err, string.format("%d: The 'nonexistent' method does not exist", line_no)) + + _, err = pcall(function() + with_plugin:loadplugin(function() + line_no = utils.next_line_number() + before.foo = 17 + end) + end) + + like(err, string.format('%d: 17 is not a function', line_no)) +end -- }}} + +do -- arguments tests {{{ + local with_plugin = clone:clone() + local orig_args + local got_args + + function with_plugin:foo(...) + orig_args = { + n = select('#', ...), + ..., + } + end + + with_plugin:loadplugin(function() + function before:foo(...) + got_args = { + n = select('#', ...), + ..., + } + end + end) + + with_plugin:foo() + is(got_args.n, 0) + utils.cmp_tables(orig_args, got_args) + + with_plugin:foo(1, 2, 3) + is(got_args.n, 3) + is(got_args[1], 1) + is(got_args[2], 2) + is(got_args[3], 3) + utils.cmp_tables(orig_args, got_args) + + with_plugin:foo(1, nil, nil, nil, 5) + is(got_args.n, 5) + is(got_args[1], 1) + is(got_args[2], nil) + is(got_args[3], nil) + is(got_args[4], nil) + is(got_args[5], 5) + utils.cmp_tables(orig_args, got_args) +end -- }}} + +do -- exception tests {{{ + local with_plugin = clone:clone() + + local has_called_original + + function with_plugin:foo() + has_called_original = true + end + + with_plugin:loadplugin(function() + function before:foo() + error 'uh-oh' + end + end) + + local _, err = pcall(with_plugin.foo, with_plugin) + + like(err, 'uh%-oh') + ok(not has_called_original) +end -- }}} + +do -- return value tests {{{ + local with_plugin = clone:clone() + + function with_plugin:foo() + return 17 + end + + function with_plugin:bar() + return 18, 19, 20 + end + + function with_plugin:baz() + return 1, nil, nil, nil, 5 + end + + with_plugin:loadplugin(function() + function before:foo() + return 18 + end + + function before:bar() + return 18 + end + + function before:baz() + end + end) + + local result = with_plugin:foo() + is(result, 17) + + local results = utils.gather_results(with_plugin:bar()) + utils.cmp_tables(results, { n = 3, 18, 19, 20 }) + + results = utils.gather_results(with_plugin:baz()) + utils.cmp_tables(results, { n = 5, 1, nil, nil, nil, 5 }) +end -- }}} + +do -- multiple advice, multiple plugins {{{ + local with_plugin = clone:clone() + local calls = {} + + function with_plugin:foo() + calls[#calls + 1] = 'original' + end + + with_plugin:loadplugin(function() + function before:foo() + calls[#calls + 1] = 'first' + end + end) + + with_plugin:loadplugin(function() + function before:foo() + calls[#calls + 1] = 'second' + end + end) + + with_plugin:foo() + + utils.cmp_tables(calls, { 'second', 'first', 'original' }) +end -- }}} + +do -- multiple advice, single plugin {{{ + local with_plugin = clone:clone() + local calls = {} + + function with_plugin:foo() + calls[#calls + 1] = 'original' + end + + with_plugin:loadplugin(function() + function before:foo() + calls[#calls + 1] = 'first' + end + + function before:foo() + calls[#calls + 1] = 'second' + end + end) + + with_plugin:foo() + + utils.cmp_tables(calls, { 'second', 'first', 'original' }) +end -- }}} diff --git a/lib/lua-repl/t/plugin-feature-tests.lua b/lib/lua-repl/t/plugin-feature-tests.lua new file mode 100644 index 00000000..4c74a4c1 --- /dev/null +++ b/lib/lua-repl/t/plugin-feature-tests.lua @@ -0,0 +1,132 @@ +-- vim:foldmethod=marker +local repl = require 'repl' +local utils = require 'test-utils' +pcall(require, 'luarocks.loader') +require 'Test.More' + +plan(19) + +do -- basic tests {{{ + local clone = repl:clone() + + clone:loadplugin(function() + features = 'foo' + end) + + ok(clone:hasfeature 'foo') + ok(not clone:hasfeature 'bar') + ok(not clone:hasfeature 'baz') + + clone:loadplugin(function() + features = { 'bar', 'baz' } + end) + + ok(clone:hasfeature 'foo') + ok(clone:hasfeature 'bar') + ok(clone:hasfeature 'baz') +end -- }}} + +do -- requirefeature {{{ + local clone = repl:clone() + + clone:loadplugin(function() + features = 'foo' + end) + + clone:requirefeature 'foo' + + local line_no + local _, err = pcall(function() + line_no = utils.next_line_number() + clone:requirefeature 'bar' + end) + + like(err, tostring(line_no) .. ': required feature "bar" not present') +end -- }}} + +do -- conflicts {{{ + local clone = repl:clone() + local line_no + + clone:loadplugin(function() + features = 'foo' + end) + + local _, err = pcall(function() + line_no = utils.next_line_number() + clone:loadplugin(function() + features = 'foo' + end) + end) + like(err, tostring(line_no) .. ': feature "foo" already present') + + -- XXX what about methods injected into the object? +end -- }}} + +do -- clone:hasfeature {{{ + local child = repl:clone() + + child:loadplugin(function() + features = 'foo' + end) + + local grandchild = child:clone() + + ok(not repl:hasfeature 'foo') + ok(child:hasfeature 'foo') + ok(grandchild:hasfeature 'foo') + + child:loadplugin(function() + features = 'bar' + end) + + ok(not repl:hasfeature 'bar') + ok(child:hasfeature 'bar') + ok(not grandchild:hasfeature 'bar') +end -- }}} + +do -- iffeature tests {{{ + local clone = repl:clone() + local has_run + + clone:iffeature('foo', function() + has_run = true + end) + + ok(not has_run) + + clone:loadplugin(function() + features = 'foo' + end) + + ok(has_run) + + has_run = false + + clone:iffeature('foo', function() + has_run = true + end) + + ok(has_run) +end -- }}} + +do -- iffeature multiple times {{{ + local clone = repl:clone() + local has_run + local has_run2 + + clone:iffeature('foo', function() + has_run = true + end) + + clone:iffeature('foo', function() + has_run2 = true + end) + + clone:loadplugin(function() + features = 'foo' + end) + + ok(has_run) + ok(has_run2) +end -- }}} diff --git a/lib/lua-repl/t/plugin-override-tests.lua b/lib/lua-repl/t/plugin-override-tests.lua new file mode 100644 index 00000000..6ea11d8b --- /dev/null +++ b/lib/lua-repl/t/plugin-override-tests.lua @@ -0,0 +1,201 @@ +-- vim:foldmethod=marker +local repl = require 'repl' +pcall(require, 'luarocks.loader') +require 'Test.More' +local utils = require 'test-utils' + +plan(25) + +local clone = repl:clone() + +do -- basic tests {{{ + local with_plugin = clone:clone() + + local has_called_normal + local has_called_override + + function with_plugin:foo() + has_called_normal = true + end + + with_plugin:loadplugin(function() + function override:foo() + has_called_override = true + end + end) + + with_plugin:foo() + ok(not has_called_normal) + ok(has_called_override) + + local line_no + + local _, err = pcall(function() + with_plugin:loadplugin(function() + line_no = utils.next_line_number() + function override:nonexistent() + end + end) + end) + + like(err, string.format("%d: The 'nonexistent' method does not exist", line_no)) + + _, err = pcall(function() + with_plugin:loadplugin(function() + line_no = utils.next_line_number() + override.foo = 17 + end) + end) + + like(err, string.format('%d: 17 is not a function', line_no)) +end -- }}} + +do -- arguments tests {{{ + local with_plugin = clone:clone() + local orig_args + local got_args + + function with_plugin:foo(...) + orig_args = { + n = select('#', ...), + ..., + } + end + + with_plugin:loadplugin(function() + function override:foo(...) + got_args = { + n = select('#', ...), + ..., + } + end + end) + + with_plugin:foo() + is(got_args.n, 0) + ok(not orig_args) + + with_plugin:foo(1, 2, 3) + is(got_args.n, 3) + is(got_args[1], 1) + is(got_args[2], 2) + is(got_args[3], 3) + ok(not orig_args) + + with_plugin:foo(1, nil, nil, nil, 5) + is(got_args.n, 5) + is(got_args[1], 1) + is(got_args[2], nil) + is(got_args[3], nil) + is(got_args[4], nil) + is(got_args[5], 5) + ok(not orig_args) +end -- }}} + +do -- exception tests {{{ + local with_plugin = clone:clone() + + local has_called_original + + function with_plugin:foo() + has_called_original = true + end + + with_plugin:loadplugin(function() + function override:foo() + error 'uh-oh' + end + end) + + local _, err = pcall(with_plugin.foo, with_plugin) + + like(err, 'uh%-oh') + ok(not has_called_original) +end -- }}} + +do -- return value tests {{{ + local with_plugin = clone:clone() + + function with_plugin:foo() + return 17 + end + + function with_plugin:bar() + return 18, 19, 20 + end + + function with_plugin:baz() + return 1, nil, nil, nil, 5 + end + + with_plugin:loadplugin(function() + function override:foo() + return 18 + end + + function override:bar() + return 19, 20, 21 + end + + function override:baz() + return 1, nil, nil, 4 + end + end) + + local result = with_plugin:foo() + is(result, 18) + + local results = utils.gather_results(with_plugin:bar()) + utils.cmp_tables(results, { n = 3, 19, 20, 21 }) + + results = utils.gather_results(with_plugin:baz()) + utils.cmp_tables(results, { n = 4, 1, nil, nil, 4 }) +end -- }}} + +do -- multiple advice, multiple plugins {{{ + local with_plugin = clone:clone() + local calls = {} + + function with_plugin:foo() + calls[#calls + 1] = 'original' + end + + with_plugin:loadplugin(function() + function override:foo() + calls[#calls + 1] = 'first' + end + end) + + with_plugin:loadplugin(function() + function override:foo() + calls[#calls + 1] = 'second' + end + end) + + with_plugin:foo() + + utils.cmp_tables(calls, { 'second' }) +end + +do -- multiple advice, single plugin {{{ + local with_plugin = clone:clone() + local calls = {} + + function with_plugin:foo() + calls[#calls + 1] = 'original' + end + + with_plugin:loadplugin(function() + function override:foo() + calls[#calls + 1] = 'first' + end + + function override:foo() + calls[#calls + 1] = 'second' + end + end) + + with_plugin:foo() + + utils.cmp_tables(calls, { 'second' }) +end diff --git a/lib/lua-repl/t/plugin-repl-tests.lua b/lib/lua-repl/t/plugin-repl-tests.lua new file mode 100644 index 00000000..b3aa7665 --- /dev/null +++ b/lib/lua-repl/t/plugin-repl-tests.lua @@ -0,0 +1,75 @@ +-- vim:foldmethod=marker +local r = require 'repl' -- we don't call it 'repl' so we don't shadow + -- repl in the plugin environment +pcall(require, 'luarocks.loader') +require 'Test.More' +local utils = require 'test-utils' + +plan(5) + +local clone = r:clone() + +do -- basic tests {{{ + local with_plugin = clone:clone() + + function with_plugin:foo() + end + + local line_no + + local _, err = pcall(function() + with_plugin:loadplugin(function() + line_no = utils.next_line_number() + function repl:foo() + end + end) + end) + + like(err, string.format("%d: The 'foo' method already exists", line_no)) + + with_plugin:loadplugin(function() + function repl:bar() + return 17 + end + end) + + is(with_plugin:bar(), 17) + + with_plugin:loadplugin(function() + repl.baz = 18 + end) + + is(with_plugin.baz, 18) +end -- }}} + +do -- conflict tests {{{ + local clone = r:clone() + local line_no + + clone:loadplugin(function() + function repl:foo() + end + end) + + local _, err = pcall(function() + clone:loadplugin(function() + line_no = utils.next_line_number() + function repl:foo() + end + end) + end) + + like(err, tostring(line_no) .. ": The 'foo' method already exists") +end -- }}} + +do -- proxy tests {{{ + local clone = r:clone() + + clone:loadplugin(function() + features = 'foo' + end) + + clone:loadplugin(function() + ok(repl:hasfeature 'foo') + end) +end -- }}} diff --git a/lib/lua-repl/t/sync-repl-tests.lua b/lib/lua-repl/t/sync-repl-tests.lua new file mode 100644 index 00000000..d5249ec2 --- /dev/null +++ b/lib/lua-repl/t/sync-repl-tests.lua @@ -0,0 +1,52 @@ +-- vim:foldmethod=marker +local sync = require 'repl.sync' +pcall(require, 'luarocks.loader') +require 'Test.More' + +plan(13) + +local clone = sync:clone() +local resultlist = {} + +function clone:lines() + local index = 0 + local function iterator(s) + index = index + 1 + return s[index] + end + + return iterator, { + 'return foo', + 'return 1', + 'return "bar"', + 'return {}', + 'return 1, 2, 3', + } +end + +function clone:showprompt() +end + +function clone:displayresults(results) + resultlist[#resultlist + 1] = results +end + +clone:run() + +is(#resultlist, 5) +is(resultlist[1].n, 1) +is(resultlist[1][1], nil) + +is(resultlist[2].n, 1) +is(resultlist[2][1], 1) + +is(resultlist[3].n, 1) +is(resultlist[3][1], 'bar') + +is(resultlist[4].n, 1) +is(type(resultlist[4][1]), 'table') + +is(resultlist[5].n, 3) +is(resultlist[5][1], 1) +is(resultlist[5][2], 2) +is(resultlist[5][3], 3) diff --git a/lib/lua-term/.gitignore b/lib/lua-term/.gitignore new file mode 100644 index 00000000..ab0a9118 --- /dev/null +++ b/lib/lua-term/.gitignore @@ -0,0 +1,3 @@ +*.o +*.so +*.rock diff --git a/lib/lua-term/CHANGES b/lib/lua-term/CHANGES new file mode 100644 index 00000000..22b7ddb1 --- /dev/null +++ b/lib/lua-term/CHANGES @@ -0,0 +1,25 @@ +0.07 2016-04-05 22:00:00 + - Fix compilation errors for Lua 5.3 on GCC < 5.0 + +0.06 2016-04-04 20:00:00 + - Fix compilation flags for Lua 5.3 + +0.05 2016-04-03 15:50:00 + - Don't include unistd.h on Windows + +0.04 2016-03-06 16:25:47 + - Include missing unistd.h header file + +0.03 2014-04-02 08:00:00 + - Fix syntax error for term.cursor.goto for Lua 5.2 + - Add jump as an alias for goto + +0.02 2013-02-21 21:15:00 + - Add cursor functions. + - Add clear functions. + - Deprecate term.isatty. Use luaposix instead. + - Add colors.default as a synonym for colors.reset. + - The metatable for colors is no longer hidden. + +0.01 2012-06-25 23:30:00 + - Initial release. Includes colors and isatty. diff --git a/lib/lua-term/CMakeLists.txt b/lib/lua-term/CMakeLists.txt new file mode 100644 index 00000000..2350152d --- /dev/null +++ b/lib/lua-term/CMakeLists.txt @@ -0,0 +1 @@ +idf_component_register(SRCS "core.c" INCLUDE_DIRS "." REQUIRES "esp-idf-lua") diff --git a/lib/lua-term/COPYING b/lib/lua-term/COPYING new file mode 100644 index 00000000..48de251d --- /dev/null +++ b/lib/lua-term/COPYING @@ -0,0 +1,19 @@ +Copyright (c) 2009 Rob Hoelz + +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 the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/lib/lua-term/Makefile b/lib/lua-term/Makefile new file mode 100644 index 00000000..75a87f34 --- /dev/null +++ b/lib/lua-term/Makefile @@ -0,0 +1,38 @@ +#this file builds lua-term \o/ + +LUA_VER ?= 5.1 +LUA_DIR ?= /usr +LUA_LIBDIR := $(LUA_DIR)/lib/lua/$(LUA_VER)/term +LUA_INC := $(LUA_DIR)/include/lua$(LUA_VER) +LUA_SHARE := $(LUA_DIR)/share/lua/$(LUA_VER)/term +CWARNS := -Wall -pedantic +CFLAGS += $(CWARNS) -O3 -I$(LUA_INC) -fPIC +LIB_OPTION := -shared + +SONAME := core.so +SONAMEV := $(SONAME).1 +LIBRARY := $(SONAMEV).0.1 +SRC := core.c +OBJ := $(patsubst %.c, %.o, $(SRC)) + +FILES := term/init.lua term/cursor.lua term/colors.lua + +all: $(LIBRARY) $(SONAMEV) $(SONAME) + +$(SONAMEV): + ln -s $(LIBRARY) $@ + +$(SONAME): + ln -s $(SONAMEV) $@ + +$(LIBRARY): $(OBJ) + $(CC) $(CFLAGS) $(LIB_OPTION) -o $(LIBRARY) $(OBJ) -lc + +install: + mkdir -p $(LUA_LIBDIR) + cp $(SONAME) $(LUA_LIBDIR) + mkdir -p $(LUA_SHARE) + cp $(FILES) $(LUA_SHARE) + +clean: + $(RM) $(LIBRARY) $(SONAMEV) $(SONAME) *.o diff --git a/lib/lua-term/README.md b/lib/lua-term/README.md new file mode 100644 index 00000000..edc805b3 --- /dev/null +++ b/lib/lua-term/README.md @@ -0,0 +1,189 @@ +Overview +-------- + +lua-term is a Lua module for manipulating a terminal. + +Installation +------------ + +lua-term is available on Luarocks. + + +## OpenBSD + +lua-term is available as an OpenBSD package. Use the proper Lua flavour to +get the package for your Lua version: + +``` +# For Lua 5.1 +$ doas pkg_add -r lua-term +# For Lua 5.2 +$ doas pkg_add -r lua52-term +# For Lua 5.3 +$ doas pkg_add -r lua53-term +``` + +Or install from ports: + +``` +$ cd /usr/ports/devel/lua-term +$ env FLAVOR=lua51 doas make install +``` + +## openSUSE +lua-term is available in the `devel:languages:lua` devel project on [OBS](https://build.opensuse.org/package/show/devel:languages:lua/lua-luaterm). + +Add the repository and install lua-term via: +``` +zypper addrepo http://download.opensuse.org/repositories/devel:/languages:/lua/openSUSE_Tumbleweed/devel:languages:lua.repo +zypper refresh +zypper in lua-luaterm +``` + +Adjust the repository URL to your version of openSUSE by substituting `openSUSE_Tumbleweed` with your actual version eg `opensSUSE_42.2`. + +Usage +----- + +```lua + local term = require 'term' + local colors = term.colors -- or require 'term.colors' + + print(term.isatty(io.stdout)) -- true if standard output goes to the terminal + + print(colors.red 'hello') + print(colors.red .. 'hello' .. colors.reset) + print(colors.red, 'hello', colors.reset) + + -- The following functions take an optional IO handle (like io.stdout); + -- io.stdout is the default if you don't specify one + term.clear() -- clears the screen + term.cleareol() -- clears from the cursor to the end of the line + --term.cursor.goto(1, 1) -- It will fail in Lua >= 5.2 because goto is a reserved word. + term.cursor['goto'](1, 1) -- This will work on Lua >= 5.2, please use jump instead + term.cursor.jump(1, 1) -- jump is just an alias for goto + term.cursor.jump(io.stdout, 1, 1) + term.cursor.goup(1) + term.cursor.godown(1) + term.cursor.goright(1) + term.cursor.goleft(1) + term.cursor.save() -- save position + term.cursor.restore() -- restore position +``` + +`term` Functions +-------------- + +Some functions in lua-term take an optional file handle argument; if this is +not provided, `io.stdout` is used. + +### `term.clear([opt_file])` + +Clear the terminal's contents. + +### `term.cleareol([opt_file])` + +Clear from the current cursor position to the end of the current line. + +### `term.isatty(file)` + +Returns `true` if `file` is a TTY; `false` otherwise. + +*NOTE*: This function has been deprecated in favor of luaposix's implementation. +If you would like this functionality in the future, please use luaposix. + +`term.colors` Values +------------------ + +The following values are available in `term.colors`: + +### Terminal Attributes + + * reset + * clear (a synonym for reset) + * default (a synonym for reset) + * bright + * dim + * underscore + * blink + * reverse + * hidden + +### Foreground Colors + + * black + * red + * green + * yellow + * blue + * magenta + * cyan + * white + +### Background Colors + + * onblack + * onred + * ongreen + * onyellow + * onblue + * onmagenta + * oncyan + * onwhite + +Every value in `term.colors` may be used in several ways: + +### As a Function + +```lua +print(colors.red 'hello') +``` + +### As a String + +```lua +print(colors.red .. 'hello' .. colors.reset) +print(colors.red, 'hello', colors.reset) +``` + +`term.cursor` Functions +--------------------- + +### `term.cursor.goto([opt_file], x, y)` + +Place the cursor at (`x`, `y`). + +### `term.cursor.jump([opt_file], x, y)` + +An alias for `term.cursor.goto`. + +### `term.cursor.goup([opt_file], nlines)` + +Moves the cursor up `nlines` lines. + +### `term.cursor.godown([opt_file], nlines)` + +Moves the cursor down `nlines` lines. + +### `term.cursor.goright([opt_file], ncols)` + +Moves the cursor right `ncols` columns. + +### `term.cursor.goleft([opt_file], ncols)` + +Moves the cursor left `ncols` columns. + +### `term.cursor.save([opt_file])` + +Saves the cursor position. + +### `term.cursor.restore([opt_file])` + +Restores the cursor position. + +Alternatives +------------ + +If you are looking to simply provide coloration to a terminal application and would +like to use a more "tag-like" API (ex. `colors '%{red}hello%{reset}'`), there is a Lua rock +named ansicolors: https://github.com/kikito/ansicolors.lua diff --git a/lib/lua-term/core.c b/lib/lua-term/core.c new file mode 100644 index 00000000..7a144086 --- /dev/null +++ b/lib/lua-term/core.c @@ -0,0 +1,27 @@ +#define _POSIX_C_SOURCE 200112L + +#include +#include +#include +#ifndef _MSC_VER +# include +#endif + +static int +lua_isatty(lua_State *L) +{ + FILE **fp = (FILE **) luaL_checkudata(L, 1, LUA_FILEHANDLE); + + lua_pushboolean(L, isatty(fileno(*fp))); + return 1; +} + +int +luaopen_term_core(lua_State *L) +{ + lua_newtable(L); + lua_pushcfunction(L, lua_isatty); + lua_setfield(L, -2, "isatty"); + + return 1; +} diff --git a/lib/lua-term/lua-term-0.7-1.rockspec b/lib/lua-term/lua-term-0.7-1.rockspec new file mode 100644 index 00000000..ba1a2bf2 --- /dev/null +++ b/lib/lua-term/lua-term-0.7-1.rockspec @@ -0,0 +1,23 @@ +package = 'lua-term' +version = '0.7-1' + +source = { + url = 'https://github.com/hoelzro/lua-term/archive/0.07.tar.gz', + dir = 'lua-term-0.07', +} + +description = { + summary = 'Terminal functions for Lua', + homepage = 'https://github.com/hoelzro/lua-term', + license = "MIT/X11", +} + +build = { + modules = { + ['term'] = 'term/init.lua', + ['term.colors'] = 'term/colors.lua', + ['term.cursor'] = 'term/cursor.lua', + ['term.core'] = 'core.c', + }, + type = 'builtin', +} diff --git a/lib/lua-term/term/colors.lua b/lib/lua-term/term/colors.lua new file mode 100644 index 00000000..76f8b2e0 --- /dev/null +++ b/lib/lua-term/term/colors.lua @@ -0,0 +1,83 @@ +-- Copyright (c) 2009 Rob Hoelz +-- +-- 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 the Software without restriction, including without limitation the rights +-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +-- copies of the Software, and to permit persons to whom the Software is +-- furnished to do so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in +-- all copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +-- THE SOFTWARE. + +local pairs = pairs +local tostring = tostring +local setmetatable = setmetatable +local schar = string.char + +local colors = {} + +local colormt = {} + +function colormt:__tostring() + return self.value +end + +function colormt:__concat(other) + return tostring(self) .. tostring(other) +end + +function colormt:__call(s) + return self .. s .. colors.reset +end + +local function makecolor(value) + return setmetatable({ value = schar(27) .. '[' .. tostring(value) .. 'm' }, colormt) +end + +local colorvalues = { + -- attributes + reset = 0, + clear = 0, + default = 0, + bright = 1, + dim = 2, + underscore = 4, + blink = 5, + reverse = 7, + hidden = 8, + + -- foreground + black = 30, + red = 31, + green = 32, + yellow = 33, + blue = 34, + magenta = 35, + cyan = 36, + white = 37, + + -- background + onblack = 40, + onred = 41, + ongreen = 42, + onyellow = 43, + onblue = 44, + onmagenta = 45, + oncyan = 46, + onwhite = 47, +} + +for c, v in pairs(colorvalues) do + colors[c] = makecolor(v) +end + +return colors diff --git a/lib/lua-term/term/cursor.lua b/lib/lua-term/term/cursor.lua new file mode 100644 index 00000000..e37864c7 --- /dev/null +++ b/lib/lua-term/term/cursor.lua @@ -0,0 +1,35 @@ +-- Copyright (c) 2009 Rob Hoelz +-- +-- 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 the Software without restriction, including without limitation the rights +-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +-- copies of the Software, and to permit persons to whom the Software is +-- furnished to do so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in +-- all copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +-- THE SOFTWARE. + +local term = require 'term.core' + +local cursor = { + ['goto'] = term.maketermfunc '%d;%dH', + goup = term.maketermfunc '%dA', + godown = term.maketermfunc '%dB', + goright = term.maketermfunc '%dC', + goleft = term.maketermfunc '%dD', + save = term.maketermfunc 's', + restore = term.maketermfunc 'u', +} + +cursor.jump = cursor['goto'] + +return cursor diff --git a/lib/lua-term/term/init.lua b/lib/lua-term/term/init.lua new file mode 100644 index 00000000..bd2024bd --- /dev/null +++ b/lib/lua-term/term/init.lua @@ -0,0 +1,51 @@ +-- Copyright (c) 2009 Rob Hoelz +-- +-- 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 the Software without restriction, including without limitation the rights +-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +-- copies of the Software, and to permit persons to whom the Software is +-- furnished to do so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in +-- all copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +-- THE SOFTWARE. + +local term = require 'term.core' +local sformat = string.format +local iotype = io.type +local stdout = io.stdout + +function term.maketermfunc(sequence_fmt) + sequence_fmt = '\027[' .. sequence_fmt + + local func + + func = function(handle, ...) + if iotype(handle) ~= 'file' then + return func(stdout, handle, ...) + end + + return handle:write(sformat(sequence_fmt, ...)) + end + + return func +end + +term.colors = require 'term.colors' +term.cursor = require 'term.cursor' + +term.clear = term.maketermfunc '2J' +term.cleareol = term.maketermfunc 'K' +term.clearend = term.maketermfunc 'J' + +term.maketermfunc = nil + +return term diff --git a/partitions.csv b/partitions.csv index bb0ee129..1d14bf40 100644 --- a/partitions.csv +++ b/partitions.csv @@ -7,4 +7,5 @@ ota_0, app, ota_0, 0x10000, 4M, ota_1, app, ota_1, 0x410000, 4M, collate, 0x40, 0x00, 0x810000, 3M, lua, data, spiffs, 0xb10000, 4M, +repl, data, spiffs, 0xf10000, 128K, diff --git a/sdkconfig.common b/sdkconfig.common index bf3230d8..a4948833 100644 --- a/sdkconfig.common +++ b/sdkconfig.common @@ -69,6 +69,7 @@ CONFIG_MBEDTLS_SSL_MAX_CONTENT_LEN=512 # CONFIG_MQTT_TRANSPORT_SSL is not set # CONFIG_MQTT_TRANSPORT_WEBSOCKET is not set # CONFIG_SPIFFS_CACHE is not set +CONFIG_SPIFFS_OBJ_NAME_LEN=64 CONFIG_WIFI_PROV_BLE_FORCE_ENCRYPTION=y # CONFIG_LV_CONF_SKIP is not set CONFIG_LV_COLOR_16_SWAP=y diff --git a/src/app_console/app_console.cpp b/src/app_console/app_console.cpp index 63accc21..f99f7536 100644 --- a/src/app_console/app_console.cpp +++ b/src/app_console/app_console.cpp @@ -37,6 +37,7 @@ #include "freertos/projdefs.h" #include "haptics.hpp" #include "index.hpp" +#include "lua_thread.hpp" #include "memory_resource.hpp" #include "service_locator.hpp" #include "system_events.hpp" @@ -557,6 +558,60 @@ void RegisterHapticEffect() { esp_console_cmd_register(&cmd); } +static const char kReplMain[] = + "package.path = '/repl/?.lua;/repl/?/init.lua;' .. package.path\n" + "local repl = require 'repl.console'\n" + "local col = require('term').colors\n" + "function repl:getprompt(level)\n" + "if level == 1 then\n" + "return col.blue .. '>>' .. col.reset\n" + "else\n" + "return '..'\n" + "end\n" + "end\n" + "repl:loadplugin 'linenoise'\n" + "repl:loadplugin 'history'\n" + "repl:loadplugin 'completion'\n" + "repl:loadplugin 'autoreturn'\n" + "repl:loadplugin 'pretty_print'\n" + "print 'Lua 5.4.4 Copyright (C) 1994-2023 Lua.org, PUC-Rio'\n" + "print 'luarepl 0.10 Copyright (C) 2011-2015 Rob Hoelz'\n" + "repl:run()\n"; + +int CmdLua(int argc, char** argv) { + std::unique_ptr context{ + lua::LuaThread::Start(*AppConsole::sServices)}; + if (!context) { + return 1; + } + + if (argc == 1) { + return context->RunString(kReplMain); + } else { + std::ostringstream path; + path << argv[0]; + for (size_t i = 1; i < argc; i++) { + path << " " << argv[i]; + } + FILINFO info; + if (f_stat(path.str().c_str(), &info) != FR_OK) { + std::cout << "file not found: " << path.str() << std::endl; + } + return context->RunScript(path.str()); + } + return 0; +} + +void RegisterLua() { + esp_console_cmd_t cmd{ + .command = "lua", + .help = "Executes a lua script. With no args, begins a lua repl session", + .hint = NULL, + .func = &CmdLua, + .argtable = NULL}; + esp_console_cmd_register(&cmd); +} + auto AppConsole::RegisterExtraComponents() -> void { RegisterListDir(); RegisterPlayFile(); @@ -579,6 +634,7 @@ auto AppConsole::RegisterExtraComponents() -> void { RegisterCoreDump(); RegisterHapticEffect(); + RegisterLua(); } } // namespace console diff --git a/src/dev_console/include/console.hpp b/src/dev_console/include/console.hpp index fedf3632..fd4050c2 100644 --- a/src/dev_console/include/console.hpp +++ b/src/dev_console/include/console.hpp @@ -18,7 +18,7 @@ class Console { auto Launch() -> void; protected: - virtual auto GetStackSizeKiB() -> uint16_t { return 8; } + virtual auto GetStackSizeKiB() -> uint16_t { return 16; } virtual auto RegisterExtraComponents() -> void {} private: diff --git a/src/drivers/include/gpios.hpp b/src/drivers/include/gpios.hpp index fe4b1c4c..a201c173 100644 --- a/src/drivers/include/gpios.hpp +++ b/src/drivers/include/gpios.hpp @@ -78,7 +78,7 @@ class IGpios { */ virtual auto Get(Pin) const -> bool = 0; - virtual auto IsLocked() const -> bool { return Get(Pin::kKeyLock); } + virtual auto IsLocked() const -> bool { return !Get(Pin::kKeyLock); } }; class Gpios : public IGpios { diff --git a/src/drivers/spiffs.cpp b/src/drivers/spiffs.cpp index 9a85c0d3..f03d2f68 100644 --- a/src/drivers/spiffs.cpp +++ b/src/drivers/spiffs.cpp @@ -14,7 +14,7 @@ namespace drivers { [[maybe_unused]] static constexpr char kTag[] = "spiffs"; -esp_err_t spiffs_mount() { +static auto mount_script_dir() -> esp_err_t { esp_vfs_spiffs_conf_t config{ .base_path = "/lua", .partition_label = "lua", @@ -26,10 +26,41 @@ esp_err_t spiffs_mount() { if (res == ESP_OK) { size_t total, used; esp_spiffs_info("lua", &total, &used); - ESP_LOGI(kTag, "spiffs mounted okay. %d / %d ", used / 1024, total / 1024); + ESP_LOGI(kTag, "lua scripts mounted okay. %d / %d ", used / 1024, + total / 1024); } return res; } +static auto mount_repl_dir() -> esp_err_t { + esp_vfs_spiffs_conf_t config{ + .base_path = "/repl", + .partition_label = "repl", + .max_files = 5, + .format_if_mount_failed = false, + }; + + esp_err_t res = esp_vfs_spiffs_register(&config); + if (res == ESP_OK) { + size_t total, used; + esp_spiffs_info("repl", &total, &used); + ESP_LOGI(kTag, "lua repl mounted okay. %d / %d ", used / 1024, + total / 1024); + } + + return res; +} + +esp_err_t spiffs_mount() { + esp_err_t res; + if ((res = mount_script_dir()) != ESP_OK) { + return res; + } + if ((res = mount_repl_dir()) != ESP_OK) { + return res; + } + return ESP_OK; +} + } // namespace drivers diff --git a/src/lua/CMakeLists.txt b/src/lua/CMakeLists.txt index 654e2763..5c67a57e 100644 --- a/src/lua/CMakeLists.txt +++ b/src/lua/CMakeLists.txt @@ -5,5 +5,6 @@ idf_component_register( SRCS "lua_thread.cpp" "bridge.cpp" "property.cpp" "lua_database.cpp" "lua_queue.cpp" INCLUDE_DIRS "include" - REQUIRES "drivers" "lvgl" "tinyfsm" "events" "system_fsm" "database" "esp_timer" "battery" "esp-idf-lua" "luavgl") + REQUIRES "drivers" "lvgl" "tinyfsm" "events" "system_fsm" "database" + "esp_timer" "battery" "esp-idf-lua" "luavgl" "lua-linenoise" "lua-term") target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) diff --git a/src/lua/bridge.cpp b/src/lua/bridge.cpp index 8d7b4fd0..1063cfbf 100644 --- a/src/lua/bridge.cpp +++ b/src/lua/bridge.cpp @@ -25,6 +25,11 @@ #include "service_locator.hpp" #include "ui_events.hpp" +extern "C" { +int luaopen_linenoise(lua_State* L); +int luaopen_term_core(lua_State* L); +} + namespace lua { [[maybe_unused]] static constexpr char kTag[] = "lua_bridge"; @@ -61,6 +66,12 @@ Bridge::Bridge(system_fsm::ServiceLocator& services, lua_State& s) luaL_requiref(&s, "legacy_ui", lua_legacy_ui, true); lua_pop(&s, 1); + luaL_requiref(&s, "linenoise", luaopen_linenoise, true); + lua_pop(&s, 1); + + luaL_requiref(&s, "term.core", luaopen_term_core, true); + lua_pop(&s, 1); + RegisterDatabaseModule(&s); RegisterQueueModule(&s); } diff --git a/src/lua/include/lua_thread.hpp b/src/lua/include/lua_thread.hpp index b10fdadf..c85ccb91 100644 --- a/src/lua/include/lua_thread.hpp +++ b/src/lua/include/lua_thread.hpp @@ -28,6 +28,7 @@ class LuaThread { ~LuaThread(); auto RunScript(const std::string& path) -> bool; + auto RunString(const std::string& path) -> bool; auto bridge() -> Bridge& { return *bridge_; } auto state() -> lua_State* { return state_; } diff --git a/src/lua/lua_thread.cpp b/src/lua/lua_thread.cpp index 4704c3e8..dc588144 100644 --- a/src/lua/lua_thread.cpp +++ b/src/lua/lua_thread.cpp @@ -114,6 +114,15 @@ auto LuaThread::RunScript(const std::string& path) -> bool { return true; } +auto LuaThread::RunString(const std::string& script) -> bool { + int res = luaL_loadstring(state_, script.c_str()); + if (res != LUA_OK) { + return false; + } + CallProtected(state_, 0, 0); + return true; +} + static int msg_handler(lua_State* L) { if (!lua_isstring(L, 1)) { return 1; diff --git a/tools/cmake/common.cmake b/tools/cmake/common.cmake index f6e00cae..62e91837 100644 --- a/tools/cmake/common.cmake +++ b/tools/cmake/common.cmake @@ -19,6 +19,8 @@ list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/libcppbor") list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/libfoxenflac") list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/libmad") list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/libtags") +list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/lua-linenoise") +list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/lua-term") list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/luavgl") list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/lvgl") list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/millershuffle")