├── .gitignore
├── .gitmodules
├── .luacheckrc
├── LICENSE
├── README.md
├── assets
├── composite.png
├── confirm.png
├── example.gif
├── list-multiple.png
├── list-single.png
├── logo.gvdesign
├── logo.png
├── password.png
└── prompt.png
├── example-wrapped.lua
├── example.lua
├── sirocco-0.0.1-5.rockspec
└── sirocco
├── char.lua
├── composite.lua
├── confirm.lua
├── init.lua
├── list.lua
├── password.lua
├── prompt.lua
└── winsize.c
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled Lua sources
2 | luac.out
3 |
4 | # luarocks build files
5 | *.src.rock
6 | *.zip
7 | *.tar.gz
8 |
9 | # Object files
10 | *.o
11 | *.os
12 | *.ko
13 | *.obj
14 | *.elf
15 |
16 | # Precompiled Headers
17 | *.gch
18 | *.pch
19 |
20 | # Libraries
21 | *.lib
22 | *.a
23 | *.la
24 | *.lo
25 | *.def
26 | *.exp
27 |
28 | # Shared objects (inc. Windows DLLs)
29 | *.dll
30 | *.so
31 | *.so.*
32 | *.dylib
33 |
34 | # Executables
35 | *.exe
36 | *.out
37 | *.app
38 | *.i*86
39 | *.x86_64
40 | *.hex
41 |
42 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "lib/tui"]
2 | path = lib/tui
3 | url = https://github.com/daurnimator/lua-tui.git
4 | [submodule "lib/utf8_simple"]
5 | path = lib/utf8_simple
6 | url = https://github.com/blitmap/lua-utf8-simple.git
7 |
--------------------------------------------------------------------------------
/.luacheckrc:
--------------------------------------------------------------------------------
1 | self = false --Ignore unused self warnings
2 |
3 | ignore = {
4 | "212" -- Unused argument
5 | }
6 |
7 | globals = {
8 | "string.utf8sub",
9 | "string.utf8width",
10 | "string.utf8height",
11 | }
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Benoit Giannangeli
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # Sirocco
6 | A collection of interactive command line prompts for Lua
7 |
8 |
9 |
10 |
11 |
12 | **Note:** Sirocco is in active development.
13 |
14 | ## Installing
15 |
16 | Requirements:
17 | - Lua 5.1/JIT/5.2/5.3
18 | - luarocks >= 3.0 (_Note: `hererocks -rlatest` will install 2.4, you need to specify it with `-r3.0`_)
19 |
20 | ```bash
21 | luarocks install sirocco
22 | ```
23 |
24 | ## Quickstart
25 |
26 | See [`example.lua`](https://github.com/giann/sirocco/blob/master/example.lua) for an exhaustive snippet of all sirocco's features.
27 |
28 | ### Text prompt
29 |
30 |
31 |
32 |
33 |
34 | Basic text prompt. Every prompt inherits from it so most of its options apply to them.
35 |
36 | ```lua
37 | Prompt {
38 | -- The prompt
39 | prompt = "A simple question\n❱ ",
40 | -- A placeholder that will dissappear once the user types something
41 | placeholder = "A simple answer",
42 | -- Whether the answer is required or not
43 | required = true,
44 | -- The default answer
45 | default = "A default answer",
46 | -- When hitting `tab`, will try to autocomplete based on those values
47 | possibleValues = {
48 | "some",
49 | "possible",
50 | "values",
51 | },
52 | -- Must return whether the current text is valid + a message in case it's not
53 | validator = function(text)
54 | return text:match("[a-zA-Z]*"), "Message when not valid"
55 | end,
56 | -- If returns false, input will not appear at all
57 | filter = function(input)
58 | return input:match("[a-zA-Z]*")
59 | end
60 | }:ask() -- Returns the answer
61 | ```
62 |
63 | ### Password
64 |
65 |
66 |
67 |
68 |
69 | Obfuscates the input.
70 |
71 | ```lua
72 | Password {
73 | prompt = "Enter your secret (hidden answer)\n❱ ",
74 | -- When false *** are printed otherwise nothing
75 | hidden = false
76 | }:ask() -- Returns the actual answer
77 | ```
78 |
79 | ### Confirm
80 |
81 |
82 |
83 |
84 |
85 | A simple yes/no prompt.
86 |
87 | ```lua
88 | Confirm {
89 | prompt = "All finished?"
90 | }:ask() -- Returns the answer
91 | ```
92 |
93 | ### List
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 | Will choose the appropriate list (check list or radio list).
104 |
105 | ```lua
106 | List {
107 | prompt = "Where are you from?",
108 | -- If true can select multiple choices (check list) otherwise one (radio list)
109 | multiple = false,
110 | -- List of choices
111 | items = {
112 | {
113 | -- The actual value returned if selected
114 | value = "New York",
115 | -- The value displayed to the user
116 | label = "New York"
117 | },
118 | {
119 | value = "Paris",
120 | label = "Paris"
121 | },
122 | {
123 | value = "Rome",
124 | label = "Rome"
125 | }
126 | },
127 | -- Indexes of already selected choices
128 | default = { 2, 4 },
129 | }:ask() -- Returns a table of the selected choices
130 | ```
131 |
132 | ### Composite
133 |
134 |
135 |
136 |
137 |
138 | Will jump from field to field when appropriate.
139 |
140 | **TODO:** field's length should be optional
141 |
142 | ```lua
143 | Composite {
144 | prompt = "What's your birthday? ",
145 | -- Separator between each fields
146 | separator = " / ",
147 | -- Fields definition
148 | fields = {
149 | {
150 | placeholder = "YYYY",
151 | filter = function(input)
152 | return input:match("%d")
153 | and input
154 | or ""
155 | end,
156 | -- Required
157 | length = 4,
158 | },
159 | {
160 | placeholder = "mm",
161 | filter = function(input)
162 | return input:match("%d")
163 | and input
164 | or ""
165 | end,
166 | length = 2,
167 | },
168 | {
169 | placeholder = "dd",
170 | filter = function(input)
171 | return input:match("%d")
172 | and input
173 | or ""
174 | end,
175 | length = 2,
176 | },
177 | }
178 | }:ask() -- Returns a table of each field's answer
179 | ```
--------------------------------------------------------------------------------
/assets/composite.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/giann/sirocco/b2af2d336e808e763b424d2ea42e6a2c2b4aa24d/assets/composite.png
--------------------------------------------------------------------------------
/assets/confirm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/giann/sirocco/b2af2d336e808e763b424d2ea42e6a2c2b4aa24d/assets/confirm.png
--------------------------------------------------------------------------------
/assets/example.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/giann/sirocco/b2af2d336e808e763b424d2ea42e6a2c2b4aa24d/assets/example.gif
--------------------------------------------------------------------------------
/assets/list-multiple.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/giann/sirocco/b2af2d336e808e763b424d2ea42e6a2c2b4aa24d/assets/list-multiple.png
--------------------------------------------------------------------------------
/assets/list-single.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/giann/sirocco/b2af2d336e808e763b424d2ea42e6a2c2b4aa24d/assets/list-single.png
--------------------------------------------------------------------------------
/assets/logo.gvdesign:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/giann/sirocco/b2af2d336e808e763b424d2ea42e6a2c2b4aa24d/assets/logo.gvdesign
--------------------------------------------------------------------------------
/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/giann/sirocco/b2af2d336e808e763b424d2ea42e6a2c2b4aa24d/assets/logo.png
--------------------------------------------------------------------------------
/assets/password.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/giann/sirocco/b2af2d336e808e763b424d2ea42e6a2c2b4aa24d/assets/password.png
--------------------------------------------------------------------------------
/assets/prompt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/giann/sirocco/b2af2d336e808e763b424d2ea42e6a2c2b4aa24d/assets/prompt.png
--------------------------------------------------------------------------------
/example-wrapped.lua:
--------------------------------------------------------------------------------
1 | package.path = package.path .. ";./lib/tui/?/init.lua;./lib/tui/?.lua;./lib/utf8_simple/?.lua"
2 |
3 | local sirocco = require "sirocco"
4 | local Prompt = sirocco.prompt
5 | local List = sirocco.list
6 |
7 | -- Clear whole screen for demo
8 | -- io.write("\27[2J\27[1;1H")
9 |
10 | List {
11 | prompt = ("A long prompt that should wrap"):rep(10),
12 | required = true,
13 | items = {
14 | {
15 | value = "Hello",
16 | label = "Hello"
17 | },
18 | {
19 | value = "Bonjour",
20 | label = "Bonjour"
21 | },
22 | {
23 | value = "Ciao",
24 | label = "Ciao"
25 | },
26 | },
27 | }:ask()
28 |
29 | Prompt {
30 | prompt = ("A long prompt that should wrap"):rep(10) .. "\n❱ ",
31 | placeholder = "A simple answer",
32 | required = true
33 | }:ask()
34 |
35 | Prompt {
36 | prompt = "A simple question\n❱ ",
37 | placeholder = "A simple answer",
38 | }:ask()
39 |
--------------------------------------------------------------------------------
/example.lua:
--------------------------------------------------------------------------------
1 | package.path = package.path .. ";./lib/tui/?/init.lua;./lib/tui/?.lua;./lib/utf8_simple/?.lua;./?/init.lua"
2 |
3 | local sirocco = require "sirocco"
4 | local Prompt = sirocco.prompt
5 | local List = sirocco.list
6 | local Password = sirocco.password
7 | local Confirm = sirocco.confirm
8 | local Composite = sirocco.composite
9 | local colors = require "term".colors
10 |
11 | -- Clear whole screen for demo
12 | -- io.write("\27[2J\27[1;1H")
13 |
14 | Prompt {
15 | prompt = "A simple question\n❱ ",
16 | placeholder = "A simple answer",
17 | required = true
18 | }:ask()
19 |
20 | Composite {
21 | prompt = "What's your birthday? ",
22 | separator = " / ",
23 | fields = {
24 | {
25 | placeholder = "YYYY",
26 | filter = function(input)
27 | return input:match("%d")
28 | and input
29 | or ""
30 | end,
31 | length = 4,
32 | },
33 | {
34 | placeholder = "mm",
35 | filter = function(input)
36 | return input:match("%d")
37 | and input
38 | or ""
39 | end,
40 | length = 2,
41 | },
42 | {
43 | placeholder = "dd",
44 | filter = function(input)
45 | return input:match("%d")
46 | and input
47 | or ""
48 | end,
49 | length = 2,
50 | },
51 | }
52 | }:ask()
53 |
54 | Prompt {
55 | prompt = "Another question\n❱ ",
56 | default = "With a default answer",
57 | }:ask()
58 |
59 | Prompt {
60 | prompt = "What programming languages do you know ?\n❱ ",
61 | placeholder = "Try tab to get some suggestions...",
62 | possibleValues = {
63 | "lua",
64 | "c",
65 | "javascript",
66 | "php",
67 | "python",
68 | "rust",
69 | "go"
70 | }
71 | }:ask()
72 |
73 | Prompt {
74 | prompt = "What's you education level?",
75 | showPossibleValues = true,
76 | possibleValues = {
77 | "highschool",
78 | "college",
79 | "doctorate"
80 | },
81 | validator = function(buffer)
82 | local ok = false
83 | for _, v in ipairs {
84 | "highschool",
85 | "college",
86 | "doctorate"
87 | } do
88 | if v == buffer then
89 | ok = true
90 | break
91 | end
92 | end
93 |
94 | return ok, not ok and colors.red .. "Not a valid answer" .. colors.reset
95 | end
96 | }:ask()
97 |
98 | List {
99 | prompt = "How do you say 'Hello'?",
100 | required = true,
101 | items = {
102 | {
103 | value = "Hello",
104 | label = "Hello"
105 | },
106 | {
107 | value = "Bonjour",
108 | label = "Bonjour"
109 | },
110 | {
111 | value = "Ciao",
112 | label = "Ciao"
113 | },
114 | },
115 | }:ask()
116 |
117 | List {
118 | prompt = "Here's a list with some already selected options:",
119 | default = { 2, 4 },
120 | required = true,
121 | items = {
122 | {
123 | value = "First",
124 | label = "First"
125 | },
126 | {
127 | value = "Second",
128 | label = "Second"
129 | },
130 | {
131 | value = "Third",
132 | label = "Third"
133 | },
134 | {
135 | value = "Fourth",
136 | label = "Fourth"
137 | },
138 | },
139 | }:ask()
140 |
141 | List {
142 | prompt = "Where are you from?",
143 | multiple = false,
144 | items = {
145 | {
146 | value = "New York",
147 | label = "New York"
148 | },
149 | {
150 | value = "Paris",
151 | label = "Paris"
152 | },
153 | {
154 | value = "Rome",
155 | label = "Rome"
156 | }
157 | },
158 | }:ask()
159 |
160 | Password {
161 | prompt = "Enter your secret\n❱ ",
162 | }:ask()
163 |
164 | Password {
165 | prompt = "Enter your secret (hidden answer)\n❱ ",
166 | hidden = true
167 | }:ask()
168 |
169 | Prompt {
170 | prompt = "What's your birthday?\n❱ ",
171 | placeholder = "YYYY-mm-dd",
172 | validator = function(buffer)
173 | if utf8.len(buffer) > 0 then
174 | local matches = { buffer:match("([1-9][0-9][0-9][0-9])%-([0-1][0-9])%-([0-3][0-9])") }
175 |
176 | if matches[1] == nil
177 | or tonumber(matches[2]) > 12
178 | or tonumber(matches[3]) > 31 then
179 | return false, colors.yellow .. "Not a valid date!" .. colors.reset
180 | end
181 | end
182 |
183 | return true
184 | end
185 | }:ask()
186 |
187 | Prompt {
188 | prompt = "Only numbers allowed\n❱ ",
189 | filter = function(input)
190 | return input:match("%d")
191 | and input
192 | or ""
193 | end
194 | }:ask()
195 |
196 | Confirm {
197 | prompt = "All finished?"
198 | }:ask()
199 |
--------------------------------------------------------------------------------
/sirocco-0.0.1-5.rockspec:
--------------------------------------------------------------------------------
1 |
2 | package = "sirocco"
3 | version = "0.0.1-5"
4 | rockspec_format = "3.0"
5 |
6 | source = {
7 | url = "git://github.com/giann/sirocco",
8 | }
9 |
10 | description = {
11 | summary = "A collection of useful cli prompts",
12 | homepage = "https://github.com/giann/sirocco",
13 | license = "MIT/X11",
14 | }
15 |
16 | build = {
17 | modules = {
18 | ["sirocco"] = "sirocco/init.lua",
19 | ["sirocco.composite"] = "sirocco/composite.lua",
20 | ["sirocco.confirm"] = "sirocco/confirm.lua",
21 | ["sirocco.list"] = "sirocco/list.lua",
22 | ["sirocco.password"] = "sirocco/password.lua",
23 | ["sirocco.prompt"] = "sirocco/prompt.lua",
24 | ["sirocco.char"] = "sirocco/char.lua",
25 | ["sirocco.winsize"] = "sirocco/winsize.c",
26 | ["tui"] = "lib/tui/tui/init.lua",
27 | ["tui.filters"] = "lib/tui/tui/filters.lua",
28 | ["tui.terminfo"] = "lib/tui/tui/terminfo.lua",
29 | ["tui.tparm"] = "lib/tui/tui/tparm.lua",
30 | ["tui.tput"] = "lib/tui/tui/tput.lua",
31 | ["tui.util"] = "lib/tui/tui/util.lua",
32 | ["utf8_simple"] = "lib/utf8_simple/utf8_simple.lua"
33 | },
34 | type = "builtin",
35 | }
36 |
37 | dependencies = {
38 | "lua >= 5.1",
39 | "lua-term >= 0.7-1",
40 | "hump >= 0.4-2",
41 | "wcwidth >= 0.3-1",
42 | "compat53 >= 0.7-1",
43 | "bit32 >= 5.2.0-1"
44 | }
45 |
--------------------------------------------------------------------------------
/sirocco/char.lua:
--------------------------------------------------------------------------------
1 | local wcwidth = require "wcwidth"
2 | local filters = require "tui.filters"
3 |
4 | local bit32 = require "bit32"
5 | local band = bit32.band
6 | local bor = bit32.bor
7 | local bnot = bit32.bnot
8 |
9 | -- Overwrite default filter to add utf8
10 | filters.default_chain = filters.make_chain {
11 | -- Always before CSI
12 | filters.mouse;
13 | -- These can be in any order
14 | filters.SS2;
15 | filters.SS3;
16 | filters.CSI;
17 | filters.OSC;
18 | filters.DCS;
19 | filters.SOS;
20 | filters.PM;
21 | filters.APC;
22 | -- Should be before ESC but after CSI and OSC
23 | filters.linux_quirks;
24 | filters.multibyte_UTF8;
25 | }
26 |
27 | local control_character_threshold = 0x020 -- Smaller than this is control.
28 | local control_character_mask = 0x1f -- 0x20 - 1
29 | local meta_character_threshold = 0x07f -- Larger than this is Meta.
30 | local control_character_bit = 0x40 -- 0x000000, must be off.
31 | local meta_character_bit = 0x080 -- x0000000, must be on.
32 | local largest_char = 255 -- Largest character value.
33 |
34 | local function ctrl_char(c)
35 | return c < control_character_threshold and band(c, 0x80) == 0
36 | end
37 |
38 | local function meta_char(c)
39 | return c > meta_character_threshold and c <= largest_char
40 | end
41 |
42 |
43 | local function ctrl(c)
44 | return string.char(band(c:byte(), control_character_mask))
45 | end
46 |
47 | -- Nobody really has a Meta key, use Esc instead
48 | local function meta(c)
49 | return string.char(bor(c:byte(), meta_character_bit))
50 | end
51 |
52 | local function Esc(c)
53 | return "\27" .. c
54 | end
55 |
56 | local function unMeta(c)
57 | return string.char(band(c:byte(), bnot(meta_character_bit)))
58 | end
59 |
60 | local function unCtrl(c)
61 | return string.upper(string.char(bor(c:byte(), control_character_bit)))
62 | end
63 |
64 | -- Utf8 aware sub
65 | string.utf8sub = require "utf8_simple".sub
66 |
67 | string.utf8width = function(self)
68 | -- First remove color escape codes
69 | local str = self:gsub("\27%[%d+m", "")
70 |
71 | local len = 0
72 |
73 | for _, rune in utf8.codes(str) do
74 | local l = wcwidth(rune)
75 | if l >= 0 then
76 | len = len + l
77 | end
78 | end
79 |
80 | return len
81 | end
82 |
83 | string.utf8height = function(self, width)
84 | local height = 1
85 | for line in self:gmatch("([^\n]*)\n") do
86 | height = height + 1
87 |
88 | for _ = width, line:utf8width(), width do
89 | height = height + 1
90 | end
91 | end
92 |
93 | return height
94 | end
95 |
96 | return {
97 | isC = ctrl_char,
98 | isM = meta_char,
99 | C = ctrl,
100 | M = meta,
101 | Esc = Esc,
102 | unM = unMeta,
103 | unC = unCtrl,
104 | }
105 |
--------------------------------------------------------------------------------
/sirocco/composite.lua:
--------------------------------------------------------------------------------
1 | local Class = require "hump.class"
2 | local colors = require "term".colors
3 |
4 | local Prompt = require "sirocco.prompt"
5 |
6 | local Composite = Class {
7 |
8 | __includes = Prompt,
9 |
10 | init = function(self, options)
11 | self.fields = options.fields or {}
12 | self.separator = options.separator or " • "
13 |
14 | Prompt.init(self, options)
15 |
16 | for _, field in ipairs(self.fields) do
17 | field.buffer = field.default or ""
18 | end
19 | end
20 |
21 | }
22 |
23 | function Composite:moveOffsetBy(chars)
24 | local currentField, i = self:getCurrentField()
25 | local currentPosition = self.currentPosition.x - currentField.position
26 |
27 | if chars > 0 then
28 | -- Jump to text field
29 | if currentPosition + chars > currentField.length
30 | and i < #self.fields then
31 | self.currentPosition.x = self.fields[i + 1].position
32 | else
33 | chars = math.min(currentField.buffer:utf8width() - currentPosition, chars)
34 |
35 | if chars > 0 then
36 | self.currentPosition.x = self.currentPosition.x + chars
37 | end
38 | end
39 | elseif chars < 0 then
40 | -- Jump to previous field
41 | if currentPosition + chars < 0
42 | and i > 1 then
43 | local previousField = self.fields[i - 1]
44 | self.currentPosition.x = previousField.position + previousField.buffer:utf8width()
45 | else
46 | self.currentPosition.x = math.max(currentField.position, self.currentPosition.x + chars)
47 | end
48 | end
49 | end
50 |
51 | function Composite:render()
52 | Prompt.render(self)
53 |
54 | self:setCursor(
55 | self.promptPosition.x,
56 | self.promptPosition.y
57 | )
58 |
59 | local len = #self.fields
60 | local fieldPosition = 0
61 | for i, field in ipairs(self.fields) do
62 | if not field.buffer or field.buffer:utf8width() == 0 then
63 | -- Truncate placeholder to field length
64 | local placeholder = (field.placeholder or ""):utf8sub(1, field.length)
65 | -- Add padding to match field length
66 | placeholder = placeholder .. (" "):rep(field.length - placeholder:utf8width())
67 |
68 | self.output:write(
69 | colors.bright .. colors.black
70 | .. placeholder
71 | .. colors.reset
72 |
73 | .. (i < len and self.separator or "")
74 | )
75 |
76 | if not field.position then
77 | field.position = fieldPosition
78 | end
79 | else
80 | local buffer = field.buffer .. (" "):rep(field.length - Prompt.len(field.buffer))
81 |
82 | self.output:write(
83 | buffer
84 | .. (i < len and self.separator or "")
85 | )
86 | end
87 |
88 | fieldPosition = fieldPosition + field.length + (i < len and self.separator:utf8width() or 0)
89 | end
90 |
91 | self:setCursor(
92 | self.promptPosition.x + self.currentPosition.x,
93 | self.promptPosition.y + self.currentPosition.y
94 | )
95 | end
96 |
97 | function Composite:getCurrentField()
98 | local currentField
99 |
100 | local len = #self.fields
101 | local i = 1
102 | repeat
103 | currentField = self.fields[i]
104 | i = i + 1
105 | until (self.currentPosition.x >= currentField.position
106 | and self.currentPosition.x <= currentField.position + currentField.length)
107 | or i > len
108 |
109 | return currentField, i - 1
110 | end
111 |
112 | function Composite:processInput(input)
113 | -- Jump cursor to next field if necessary
114 | local len = #self.fields
115 | for i, field in ipairs(self.fields) do
116 | if self.currentPosition.x > field.position + field.length - 1
117 | and i < len
118 | and self.currentPosition.x < self.fields[i + 1].position then
119 | self.currentPosition.x = self.fields[i + 1].position
120 | end
121 | end
122 |
123 | -- Get current field
124 | local currentField = self:getCurrentField()
125 |
126 | -- Filter input
127 | input = currentField.filter
128 | and currentField.filter(input)
129 | or input
130 |
131 | if Prompt.len(currentField.buffer) >= currentField.length then
132 | input = ""
133 | end
134 |
135 | -- Insert in current field
136 | currentField.buffer =
137 | (currentField.buffer:utf8sub(1, self.currentPosition.x - currentField.position)
138 | .. input
139 | .. currentField.buffer:utf8sub(self.currentPosition.x + 1 - currentField.position))
140 |
141 | -- Increment current position
142 | self.currentPosition.x = self.currentPosition.x + input:utf8width()
143 |
144 | -- Validation
145 | if currentField.validator then
146 | local _, message = currentField.validator(currentField.buffer)
147 | self.message = message
148 | end
149 | end
150 |
151 | function Composite:processedResult()
152 | local result = {}
153 |
154 | for _, field in ipairs(self.fields) do
155 | table.insert(result, field.buffer)
156 | end
157 |
158 | return result
159 | end
160 |
161 | -- TODO: redefine all Prompt command_ to operate on current field instead of buffer
162 |
163 | function Composite:command_end_of_line()
164 | local currentField = self:getCurrentField()
165 | self.currentPosition.x = currentField.position + currentField.length
166 | end
167 |
168 | function Composite:command_kill_line()
169 | local currentField = self:getCurrentField()
170 | currentField.buffer = currentField.buffer:utf8sub(
171 | 1,
172 | self.currentPosition.x - currentField.position
173 | )
174 | end
175 |
176 | function Composite:command_delete_back()
177 | if self.currentPosition.x > 0 then
178 | self:moveOffsetBy(-1)
179 |
180 | -- Maybe we jumped back to previous field
181 | local currentField = self:getCurrentField()
182 |
183 | -- Delete char at currentPosition
184 | currentField.buffer = currentField.buffer:utf8sub(1, self.currentPosition.x - currentField.position)
185 | .. currentField.buffer:utf8sub(self.currentPosition.x + 2 - currentField.position)
186 | end
187 | end
188 |
189 |
190 | function Composite:command_complete()
191 | -- TODO
192 | end
193 |
194 | return Composite
195 |
--------------------------------------------------------------------------------
/sirocco/confirm.lua:
--------------------------------------------------------------------------------
1 | local Class = require "hump.class"
2 | local colors = require "term".colors
3 |
4 | local Prompt = require "sirocco.prompt"
5 | local List = require "sirocco.list"
6 | local char = require "sirocco.char"
7 | local C, Esc = char.C, char.Esc
8 |
9 | local Confirm = Class {
10 |
11 | __includes = List,
12 |
13 | init = function(self, options)
14 | options.items = {
15 | {
16 | value = true,
17 | label = "Yes"
18 | },
19 | {
20 | value = false,
21 | label = "No"
22 | },
23 | }
24 |
25 | options.multiple = false
26 |
27 | List.init(self, options)
28 |
29 | self.currentChoice = #self.chosen > 0
30 | and self.chosen[1]
31 | or 1
32 | end
33 |
34 | }
35 |
36 | function Confirm:registerKeybinding()
37 | self.keybinding = {
38 | command_get_next_choice = {
39 | Prompt.escapeCodes.key_right,
40 | C "n",
41 | Esc "[C" -- backup
42 | },
43 |
44 | command_get_previous_choice = {
45 | Prompt.escapeCodes.key_left,
46 | C "p",
47 | Esc "[D" -- backup
48 | },
49 |
50 | -- TODO: those should be signals
51 | command_exit = {
52 | C "c",
53 | },
54 |
55 | command_validate = {
56 | "\n",
57 | "\r"
58 | },
59 | }
60 | end
61 |
62 | function Confirm:render()
63 | Prompt.render(self)
64 |
65 | self.output:write(
66 | " "
67 | .. (self.currentChoice == 1
68 | and colors.underscore
69 | or "")
70 | .. self.items[1].label
71 | .. colors.reset
72 | .. " / "
73 | .. (self.currentChoice == 2
74 | and colors.underscore
75 | or "")
76 | .. self.items[2].label
77 | .. colors.reset
78 | )
79 | end
80 |
81 | function Confirm:endCondition()
82 | self.chosen = {
83 | [self.currentChoice] = true
84 | }
85 |
86 | return List.endCondition(self)
87 | end
88 |
89 | function Confirm:after(result)
90 | -- Show selected label
91 | self:setCursor(self.promptPosition.x, self.promptPosition.y)
92 |
93 | -- Clear down
94 | self.output:write(Prompt.escapeCodes.clr_eos)
95 |
96 | self.output:write(" " .. (result[1] and "Yes" or "No"))
97 |
98 | -- Show cursor
99 | self.output:write(Prompt.escapeCodes.cursor_visible)
100 |
101 | Prompt.after(self)
102 | end
103 |
104 | return Confirm
105 |
--------------------------------------------------------------------------------
/sirocco/init.lua:
--------------------------------------------------------------------------------
1 | require "compat53"
2 |
3 | return {
4 | prompt = require "sirocco.prompt",
5 | password = require "sirocco.password",
6 | confirm = require "sirocco.confirm",
7 | composite = require "sirocco.composite",
8 | list = require "sirocco.list",
9 | }
10 |
--------------------------------------------------------------------------------
/sirocco/list.lua:
--------------------------------------------------------------------------------
1 | local Class = require "hump.class"
2 | local colors = require "term".colors
3 |
4 | local Prompt = require "sirocco.prompt"
5 | local char = require "sirocco.char"
6 | local C, Esc = char.C, char.Esc
7 |
8 | local List = Class {
9 |
10 | __includes = Prompt,
11 |
12 | init = function(self, options)
13 | self.items = options.items or {
14 | -- {
15 | -- value = "a",
16 | -- label = "the first choice"
17 | -- }
18 | }
19 |
20 | self.multiple = true
21 | if options.multiple ~= nil then
22 | self.multiple = options.multiple
23 | end
24 |
25 | self.currentChoice = 1
26 | self.chosen = options.default or {}
27 |
28 | -- Don't let prompt use options.default
29 | options.default = nil
30 |
31 | Prompt.init(self, options)
32 | end
33 |
34 | }
35 |
36 | function List:getHeight()
37 | local everything = self.prompt
38 |
39 | if not self.prompt:match("\n$") then
40 | everything = everything .. "\n"
41 | end
42 |
43 | for i, item in ipairs(self.items) do
44 | local chosen = self.chosen[i]
45 |
46 | -- TODO: should not copy render
47 | everything = everything
48 | .. " "
49 | .. (i == self.currentChoice and "❱ " or " ")
50 |
51 | .. (self.multiple and "[" or "(")
52 | .. (
53 | self.multiple
54 | and (chosen and "✔" or " ")
55 | or (chosen and "●" or " ")
56 | )
57 | .. (self.multiple and "]" or ")")
58 |
59 | .. " "
60 |
61 | .. (chosen and colors.underscore or "")
62 | .. item.label
63 |
64 | .. "\n"
65 | end
66 |
67 | everything = everything
68 | .. (self.message or "message") -- At least something otherwise line is ignored by textHeight
69 |
70 | return everything:utf8height(self.terminalWidth)
71 | end
72 |
73 | function List:registerKeybinding()
74 | Prompt.registerKeybinding(self)
75 |
76 | self.keybinding = {
77 | command_get_next_choice = {
78 | Prompt.escapeCodes.key_down,
79 | C "n",
80 | Esc "[B" -- backup
81 | },
82 |
83 | command_get_previous_choice = {
84 | Prompt.escapeCodes.key_up,
85 | C "p",
86 | Esc "[A" -- backup
87 | },
88 |
89 | command_select_choice = {
90 | " "
91 | },
92 |
93 | -- TODO: those should be signals
94 | command_exit = {
95 | C "c",
96 | },
97 |
98 | command_validate = {
99 | "\n",
100 | "\r"
101 | },
102 | }
103 | end
104 |
105 | function List:complete()
106 | end
107 |
108 | function List:setCurrentChoice(newChoice)
109 | self.currentChoice = math.max(1, math.min(#self.items, self.currentChoice + newChoice))
110 | end
111 |
112 | function List:render()
113 | Prompt.render(self)
114 |
115 | -- List must begin under prompt
116 | if not self.prompt:match("\n$") then
117 | self.output:write("\n")
118 | end
119 |
120 | for i, item in ipairs(self.items) do
121 | local chosen = self.chosen[i]
122 |
123 | self.output:write(
124 | " "
125 | .. (i == self.currentChoice and "❱ " or " ")
126 |
127 | .. colors.magenta
128 | .. (self.multiple and "[" or "(")
129 | .. (
130 | self.multiple
131 | and (chosen and "✔" or " ")
132 | or (chosen and "●" or " ")
133 | )
134 | .. (self.multiple and "]" or ")")
135 | .. colors.reset
136 |
137 | .. " "
138 |
139 | .. (chosen and colors.underscore or "")
140 | .. colors.green
141 | .. item.label
142 | .. colors.reset
143 |
144 | .. "\n"
145 | )
146 | end
147 | end
148 |
149 | function List:renderMessage()
150 | if self.message then
151 | self:setCursor(
152 | 1,
153 | self.promptPosition.y + self.currentPosition.y + #self.items + 1
154 | )
155 |
156 | self.output:write(self.message)
157 |
158 | self:setCursor(
159 | self.promptPosition.x + self.currentPosition.x,
160 | self.promptPosition.y + self.currentPosition.y
161 | )
162 | end
163 | end
164 |
165 | function List:processInput(input)
166 | end
167 |
168 | function List:processedResult()
169 | local result = {}
170 | for i, selected in pairs(self.chosen) do
171 | if selected then
172 | table.insert(result, self.items[i].value)
173 | end
174 | end
175 |
176 | return result
177 | end
178 |
179 | function List:endCondition()
180 | if self.finished == "force" then
181 | return true
182 | end
183 |
184 | local count = 0
185 | for _, v in pairs(self.chosen) do
186 | count = count + (v and 1 or 0)
187 | end
188 |
189 | local condition = not self.required or count > 0
190 |
191 | if self.finished and not condition then
192 | self.message = colors.red .. "Answer is required" .. colors.reset
193 | end
194 |
195 | self.finished = self.finished and (not self.required or count > 0)
196 |
197 | return self.finished
198 | end
199 |
200 | function List:before()
201 | -- Hide cursor
202 | self.output:write(Prompt.escapeCodes.cursor_invisible)
203 | -- Backup
204 | self.output:write(Esc "[?25l")
205 |
206 | Prompt.before(self)
207 | end
208 |
209 | function List:after(result)
210 | -- Show selected label
211 | self:setCursor(self.promptPosition.x, self.promptPosition.y)
212 |
213 | -- Clear down
214 | self.output:write(Prompt.escapeCodes.clr_eos)
215 |
216 | if result then
217 | self.output:write(" " .. (#result == 1 and tostring(result[1]) or table.concat(result, ", ")))
218 | end
219 |
220 | -- Show cursor
221 | self.output:write(Prompt.escapeCodes.cursor_visible)
222 | -- Backup
223 | self.output:write(Esc "[?25h")
224 |
225 | Prompt.after(self)
226 | end
227 |
228 | function List:command_get_next_choice()
229 | self:setCurrentChoice(1)
230 | end
231 |
232 | function List:command_get_previous_choice()
233 | self:setCurrentChoice(-1)
234 | end
235 |
236 | function List:command_select_choice()
237 | local count = 0
238 | for _, v in pairs(self.chosen) do
239 | count = count + (v and 1 or 0)
240 | end
241 |
242 | self.chosen[self.currentChoice] = not self.chosen[self.currentChoice]
243 |
244 | -- Only one choice allowed ? unselect previous choice
245 | if self.chosen[self.currentChoice] and not self.multiple and count > 0 then
246 | self.chosen = {
247 | [self.currentChoice] = true
248 | }
249 |
250 | self.message = nil
251 | end
252 | end
253 |
254 | return List
255 |
--------------------------------------------------------------------------------
/sirocco/password.lua:
--------------------------------------------------------------------------------
1 | local Class = require "hump.class"
2 |
3 | local Prompt = require "sirocco.prompt"
4 |
5 | local Password = Class {
6 |
7 | __includes = Prompt,
8 |
9 | init = function(self, options)
10 | -- Can't suggest anything
11 | options.default = nil
12 | options.possibleValues = nil
13 |
14 | self.hidden = options.hidden
15 |
16 | Prompt.init(self, options)
17 |
18 | self.actual = ""
19 | end
20 |
21 | }
22 |
23 | function Password:renderDisplayBuffer()
24 | self.displayBuffer = self.hidden
25 | and ""
26 | or ("*"):rep(Prompt.len(self.buffer))
27 | end
28 |
29 | function Password:processInput(input)
30 | Prompt.processInput(self, input)
31 |
32 | if self.hidden then
33 | self.currentPosition.x = 0
34 | end
35 | end
36 |
37 | function Password:complete()
38 | end
39 |
40 | return Password
41 |
--------------------------------------------------------------------------------
/sirocco/prompt.lua:
--------------------------------------------------------------------------------
1 | require "compat53"
2 |
3 | local Class = require "hump.class"
4 | local winsize = require "sirocco.winsize"
5 | local char = require "sirocco.char"
6 | local C, M, Esc = char.C, char.M, char.Esc
7 | local tui = require "tui"
8 | local tparm = require "tui.tparm".tparm
9 |
10 | -- TODO: remove
11 | local term = require "term"
12 | local colors = term.colors
13 |
14 | local Prompt
15 | Prompt = Class {
16 |
17 | init = function(self, options)
18 | self.input = options.input or io.stdin
19 | self.output = options.output or io.stdout
20 | self.prompt = options.prompt or "> "
21 | self.placeholder = options.placeholder
22 |
23 | assert(
24 | not self.placeholder
25 | or not self.placeholder:find("\n"),
26 | "New line not allowed in placeholder"
27 | )
28 |
29 | self.possibleValues = options.possibleValues or {}
30 | self.showPossibleValues = options.showPossibleValues
31 | self.validator = options.validator
32 | self.filter = options.filter
33 |
34 | self.required = false
35 | if options.required ~= nil then
36 | self.required = options.required
37 | end
38 |
39 | -- Printed buffer (can be wrapped, colored etc.)
40 | self.displayBuffer = options.default or ""
41 | -- Unaltered buffer
42 | self.buffer = options.default or ""
43 | -- Current utf8-aware offset in buffer (has to be translated in (x,y) cursor position)
44 | self.bufferOffset = options.default and options.default:utf8width() + 1 or 1
45 |
46 | self.currentPosition = {
47 | x = 0,
48 | y = 0
49 | }
50 |
51 | self.startingPosition = {
52 | x = false,
53 | y = false
54 | }
55 |
56 | self.promptPosition = {
57 | x = false,
58 | y = false
59 | }
60 |
61 | self.width = 80
62 | -- Height is prompt rows + message row
63 | self.height = 1
64 |
65 | -- Will be printed below
66 | self.message = nil
67 |
68 | self:registerKeybinding()
69 | end,
70 |
71 | -- Must be use for cursor position, NOT to get len of a string
72 | len = function(input)
73 | -- utf8.len will fail if input is not valid utf8
74 | return utf8.len(input) or #input
75 | end,
76 |
77 | }
78 |
79 | function Prompt:registerKeybinding()
80 | self.keybinding = {
81 | command_beg_of_line = {
82 | Prompt.escapeCodes.key_home,
83 | C "a",
84 | },
85 |
86 | command_end_of_line = {
87 | Prompt.escapeCodes.key_end,
88 | C "e",
89 | },
90 |
91 | command_backward_char = {
92 | Prompt.escapeCodes.key_left,
93 | C "b",
94 | Esc "[D" -- backup
95 | },
96 |
97 | command_forward_char = {
98 | Prompt.escapeCodes.key_right,
99 | C "f",
100 | Esc "[C" -- backup
101 | },
102 |
103 | command_complete = {
104 | Prompt.escapeCodes.tab
105 | },
106 |
107 | command_kill_line = {
108 | C "k",
109 | },
110 |
111 | command_clear_screen = {
112 | C "l",
113 | },
114 |
115 | command_delete_back = {
116 | Prompt.escapeCodes.key_backspace,
117 | "\127"
118 | },
119 |
120 | command_unix_line_discard = {
121 | C "u",
122 | },
123 |
124 | command_unix_word_rubout = {
125 | C "w",
126 | Esc "\127"
127 | },
128 |
129 | command_transpose_chars = {
130 | C "t",
131 | },
132 |
133 | command_delete = {
134 | C "d",
135 | },
136 |
137 | command_exit = {
138 | C "c", -- TODO: should be signal
139 | C "g"
140 | },
141 |
142 | command_backward_word = {
143 | M "b",
144 | Esc "b",
145 | Esc(Prompt.escapeCodes.key_left),
146 | Esc "[1;3D", -- Alt+left on termite
147 | },
148 |
149 | command_forward_word = {
150 | M "f",
151 | Esc "f",
152 | Esc(Prompt.escapeCodes.key_right),
153 | Esc "[1;3C", -- Alt+left on termite
154 | },
155 |
156 | command_kill_word = {
157 | M "d",
158 | Esc "d"
159 | },
160 |
161 | command_validate = {
162 | "\n",
163 | "\r"
164 | },
165 | }
166 | end
167 |
168 | function Prompt:insertAtCurrentPosition(text)
169 | -- Insert text at currentPosition
170 | self.buffer =
171 | self.buffer:utf8sub(1, self.bufferOffset - 1)
172 | .. text
173 | .. self.buffer:utf8sub(self.bufferOffset)
174 | end
175 |
176 | -- Necessary because we erase everything each time
177 | -- and reposition to startingPosition
178 | function Prompt:getHeight()
179 | -- TODO: should not copy render
180 | local everything =
181 | self.prompt
182 | .. (self.showPossibleValues and #self.possibleValues > 0
183 | and " ("
184 | .. table.concat(self.possibleValues, ", ")
185 | .. ") "
186 | or "")
187 | .. (self.buffer or "")
188 | .. "\n"
189 | .. (self.message or "message") -- At least something otherwise line is ignored by textHeight
190 |
191 | return everything:utf8height(self.terminalWidth)
192 | end
193 |
194 | function Prompt:updateCurrentPosition()
195 | -- Some utf8 chars can take more than one cell
196 | local visualOffset = self.buffer:utf8sub(1, self.bufferOffset - 1):utf8width() + 1
197 | local offset = self.promptPosition.x + visualOffset
198 |
199 | local rows = 0
200 | while offset - 1 > self.terminalWidth do
201 | offset = offset - self.terminalWidth
202 | rows = rows + 1
203 | end
204 |
205 | self.currentPosition.x = offset - self.promptPosition.x - 1
206 | self.currentPosition.y = rows
207 | end
208 |
209 | -- Take value buffer and format/wrap it
210 | function Prompt:renderDisplayBuffer()
211 | -- Terminal wraps printed text on its own
212 | self.displayBuffer = self.buffer
213 | end
214 |
215 | -- Set offset and move cursor accordingly
216 | function Prompt:setOffset(offset)
217 | self.bufferOffset = offset
218 | self:updateCurrentPosition()
219 | end
220 |
221 | -- Move offset by increment and move cursor accordingly
222 | function Prompt:moveOffsetBy(chars)
223 | if chars > 0 then
224 | chars = math.min(Prompt.len(self.buffer) - self.bufferOffset + 1, chars)
225 |
226 | if chars > 0 then
227 | self.bufferOffset = self.bufferOffset + chars
228 | end
229 | elseif chars < 0 then
230 | self.bufferOffset = math.max(1, self.bufferOffset + chars)
231 | end
232 |
233 | self:updateCurrentPosition()
234 | end
235 |
236 | function Prompt:processInput(input)
237 | input = self.filter
238 | and self.filter(input)
239 | or input
240 |
241 | self:insertAtCurrentPosition(input)
242 |
243 | self.bufferOffset = self.bufferOffset + Prompt.len(input)
244 |
245 | self:updateCurrentPosition()
246 |
247 | if self.validator then
248 | local _, message = self.validator(self.buffer)
249 | self.message = message
250 | end
251 |
252 | self:renderDisplayBuffer()
253 | end
254 |
255 | function Prompt:handleInput()
256 | local input = tui.getnext()
257 |
258 | for command, keys in pairs(self.keybinding) do
259 | for _, key in ipairs(keys) do
260 | if key == input then
261 | self[command](self)
262 | return true
263 | end
264 | end
265 | end
266 |
267 | -- Not an escape code
268 | self:processInput(input)
269 | end
270 |
271 | function Prompt:render()
272 | -- Go back to start
273 | self:setCursor(self.startingPosition.x, self.startingPosition.y)
274 |
275 | -- Clear down
276 | self.output:write(Prompt.escapeCodes.clr_eos)
277 |
278 | local inlinePossibleValues = self.showPossibleValues and #self.possibleValues > 0
279 | and " ("
280 | .. table.concat(self.possibleValues, ", ")
281 | .. ") "
282 | or ""
283 |
284 | -- Print prompt
285 | self.output:write(
286 | colors.bright .. colors.blue
287 | .. self.prompt
288 | .. inlinePossibleValues
289 | .. colors.reset
290 | )
291 |
292 | -- Print placeholder
293 | if self.placeholder
294 | and (not self.promptPosition.x or not self.promptPosition.y)
295 | and Prompt.len(self.displayBuffer) == 0 then
296 | self.output:write(
297 | colors.bright .. colors.black
298 | .. (self.placeholder or "")
299 | .. colors.reset
300 | )
301 | end
302 |
303 | -- Print current value
304 | self.output:write(self.displayBuffer)
305 |
306 | -- First time we need to initialize current position
307 | if not self.promptPosition.x or not self.promptPosition.y then
308 | local x, y = self.startingPosition.x, self.startingPosition.y - 1
309 | local prompt = self.prompt .. inlinePossibleValues
310 |
311 | local promptHeight = prompt:utf8height(self.terminalWidth)
312 |
313 | local lastLine = prompt:sub(-1) ~= "\n"
314 | and prompt:match("([^\n]*)$")
315 | or ""
316 |
317 | self.promptPosition.x, self.promptPosition.y =
318 | x + lastLine:utf8width() + (self.showPossibleValues and inlinePossibleValues:utf8width() or 0),
319 | y + promptHeight
320 | end
321 |
322 | self:renderMessage()
323 |
324 | self:setCursor(
325 | self.promptPosition.x + self.currentPosition.x,
326 | self.promptPosition.y + self.currentPosition.y
327 | )
328 | end
329 |
330 | function Prompt:renderMessage()
331 | if self.message then
332 | self:setCursor(
333 | 1,
334 | self.promptPosition.y + self.currentPosition.y + 1
335 | )
336 |
337 | self.output:write(self.message)
338 |
339 | self:setCursor(
340 | self.promptPosition.x + self.currentPosition.x,
341 | self.promptPosition.y + self.currentPosition.y
342 | )
343 | end
344 | end
345 |
346 | function Prompt:update()
347 | self.terminalWidth, self.terminalHeight = winsize()
348 |
349 | self:renderDisplayBuffer()
350 |
351 | self.width = self.terminalWidth
352 | self.height = self:getHeight()
353 |
354 | -- Scroll up if at the terminal's bottom
355 | local heightDelta = (self.startingPosition.y + self.height) - self.terminalHeight - 1
356 | if heightDelta > 0 then
357 | -- Scroll up
358 | self.output:write(tparm(Prompt.escapeCodes.parm_index, heightDelta))
359 |
360 | -- Shift everything up
361 | self.startingPosition.y = self.startingPosition.y - heightDelta
362 | self.promptPosition.y = self.promptPosition.y
363 | and (self.promptPosition.y - heightDelta)
364 | or self.promptPosition.y
365 | end
366 | end
367 |
368 | function Prompt:processedResult()
369 | return self.buffer
370 | end
371 |
372 | function Prompt:endCondition()
373 | if self.finished == "force" then
374 | return true
375 | end
376 |
377 | if self.finished and self.required and Prompt.len(self.buffer) == 0 then
378 | self.finished = false
379 | self.message = colors.red .. "Answer is required" .. colors.reset
380 | end
381 |
382 | -- Only validate if required or if something is in the buffer
383 | if self.finished and self.validator and (self.required or Prompt.len(self.buffer) > 0) then
384 | local ok, message = self.validator(self.buffer)
385 | self.finished = self.finished and (ok or not self.required)
386 | self.message = message
387 | end
388 |
389 | return self.finished
390 | end
391 |
392 | function Prompt:getCursor()
393 | local y, x = tui.getnext():match("([0-9]*);([0-9]*)")
394 | return tonumber(x), tonumber(y)
395 | end
396 |
397 | function Prompt:setCursor(x, y)
398 | self.output:write(tparm(Prompt.escapeCodes.cursor_address, y, x))
399 | end
400 |
401 | function Prompt:before()
402 | if self.input == io.stdin then
403 | -- Raw mode to get chars by chars
404 | os.execute("/usr/bin/env stty raw opost -echo 2> /dev/null")
405 | end
406 |
407 | -- Get current position
408 | self.output:write(Prompt.escapeCodes.user7)
409 |
410 | self.startingPosition.x,
411 | self.startingPosition.y = self:getCursor()
412 |
413 | -- Wrap prompt if needed
414 | self.terminalWidth, self.terminalHeight = winsize()
415 | self.height = self:getHeight()
416 | end
417 |
418 | function Prompt:after()
419 | self.output:write("\n")
420 |
421 | -- Restore normal mode
422 | os.execute("/usr/bin/env stty sane")
423 | end
424 |
425 | function Prompt:ask()
426 | self:before()
427 |
428 | repeat
429 | self:update()
430 |
431 | self:render()
432 |
433 | self:handleInput()
434 | until self:endCondition()
435 |
436 | local result = self:processedResult()
437 |
438 | self:after(result)
439 |
440 | return result
441 | end
442 |
443 | -- COMMANDS
444 |
445 | function Prompt:command_beg_of_line() -- Control-a
446 | self:setOffset(1)
447 | end
448 |
449 | function Prompt:command_backward_char() -- Control-b, left arrow
450 | self:moveOffsetBy(-1)
451 | end
452 |
453 | function Prompt:command_delete() -- Control-d
454 | if Prompt.len(self.buffer) > 0 then
455 | self.buffer =
456 | self.buffer:utf8sub(1, math.max(1, self.bufferOffset - 1))
457 | .. self.buffer:utf8sub(self.bufferOffset + 1)
458 | else
459 | self:command_exit()
460 | end
461 | end
462 |
463 | function Prompt:command_end_of_line() -- Control-e
464 | self:setOffset(Prompt.len(self.buffer) + 1)
465 | end
466 |
467 | function Prompt:command_forward_char() -- Control-f, right arrow
468 | self:moveOffsetBy(1)
469 | end
470 |
471 | function Prompt:command_complete() -- Control-i, tab
472 | if #self.possibleValues > 0 then
473 | local matches = {}
474 | local count = 0
475 | for _, value in ipairs(self.possibleValues) do
476 | if value:utf8sub(1, #self.buffer) == self.buffer then
477 | table.insert(matches, value)
478 | count = count + 1
479 | end
480 | end
481 |
482 | if count > 1 then
483 | self.message = table.concat(matches, " ")
484 | elseif count == 1 then
485 | self.buffer = matches[1]
486 | self:setOffset(Prompt.len(self.buffer) + 1)
487 |
488 | if self.validator then
489 | local _, message = self.validator(self.buffer)
490 | self.message = message
491 | end
492 | end
493 | end
494 | end
495 |
496 | function Prompt:command_kill_line() -- Control-k
497 | self.buffer = self.buffer:utf8sub(
498 | 1,
499 | self.bufferOffset - 1
500 | )
501 | end
502 |
503 | function Prompt:command_clear_screen() -- Control-l
504 | self:setCursor(1,1)
505 |
506 | self.startingPosition = {
507 | x = 1,
508 | y = 1
509 | }
510 |
511 | self.promptPosition = {
512 | x = false,
513 | y = false
514 | }
515 |
516 | self.output:write(Prompt.escapeCodes.clr_eos)
517 | end
518 |
519 | function Prompt:command_transpose_chars() -- Control-t
520 | local len = Prompt.len(self.buffer)
521 | if len > 1 and self.bufferOffset > 1 then
522 | local offset = math.max(1, (self.bufferOffset > len and len or self.bufferOffset) - 1)
523 |
524 | self.buffer =
525 | self.buffer:utf8sub(1, offset - 1)
526 | .. self.buffer:utf8sub(offset + 1, offset + 1)
527 | .. self.buffer:utf8sub(offset, offset)
528 | .. self.buffer:utf8sub(offset + 2)
529 |
530 | if self.bufferOffset <= len then
531 | self:moveOffsetBy(1)
532 | end
533 | end
534 | end
535 |
536 | function Prompt:command_unix_line_discard() -- Control-u
537 | self:setOffset(1)
538 | self.buffer = ""
539 | end
540 |
541 | function Prompt:command_unix_word_rubout() -- Control-w
542 | local s, e = self.buffer:utf8sub(1, self.bufferOffset - 1):find("[%g]+[^%g]*$")
543 |
544 | if s then
545 | self.buffer = self.buffer:utf8sub(1, s - 1) .. self.buffer:utf8sub(e + 1)
546 | self:moveOffsetBy(s - e - 1)
547 | end
548 | end
549 |
550 | function Prompt:command_delete_back()
551 | if self.bufferOffset > 1 then
552 | self:moveOffsetBy(-1)
553 |
554 | -- Delete char at currentPosition
555 | self.buffer = self.buffer:utf8sub(1, self.bufferOffset-1)
556 | .. self.buffer:utf8sub(self.bufferOffset + 1)
557 | end
558 | end
559 |
560 | function Prompt:command_validate()
561 | self.finished = true
562 | self.pendingBuffer = ""
563 | end
564 |
565 | function Prompt:command_abort()
566 | self.finished = "force"
567 | end
568 |
569 | function Prompt:command_exit() -- Control-g, Control-c
570 | self:after()
571 | os.exit()
572 | end
573 |
574 | -- See: https://github.com/Distrotech/readline/blob/master/emacs_keymap.c
575 |
576 | function Prompt:command_backward_word() -- Meta-b
577 | local s, e = self.buffer:utf8sub(1, self.bufferOffset - 1):find("[%g]+[^%g]*$")
578 |
579 | if e then
580 | self:moveOffsetBy(s - e - 1)
581 | end
582 | end
583 |
584 | function Prompt:command_kill_word() -- Meta-d
585 | local s, e = self.buffer:utf8sub(self.bufferOffset):find("[%g]+[^%g]*")
586 | s, e = self.bufferOffset + s - 1, self.bufferOffset + e - 1
587 |
588 | if s then
589 | self.buffer =
590 | self.buffer:utf8sub(1, s - 1)
591 | .. self.buffer:utf8sub(e + 1)
592 | end
593 | end
594 |
595 | function Prompt:command_forward_word() -- Meta-f
596 | local _, e = self.buffer:utf8sub(self.bufferOffset):find("[%g]+[^%g]*")
597 |
598 | if e then
599 | self:moveOffsetBy(e)
600 | end
601 | end
602 |
603 | function Prompt:command_transpose_words() -- Meta-t
604 | end
605 |
606 | function Prompt:command_get_next_history() -- Control-n
607 | end
608 |
609 | function Prompt:command_get_previous_history() -- Control-p
610 | end
611 |
612 | function Prompt:command_reverse_search_history() -- Control-r
613 | end
614 |
615 | function Prompt:command_forward_search_history() -- Control-s
616 | end
617 |
618 | function Prompt:command_yank() -- Control-y
619 | end
620 |
621 | function Prompt:command_undo_command() -- Control-_
622 | end
623 |
624 | function Prompt:command_possible_completions() -- Meta-=, Meta-?
625 | end
626 |
627 | function Prompt:command_beginning_of_history() -- Meta-<
628 | end
629 |
630 | function Prompt:command_end_of_history() -- Meta->
631 | end
632 |
633 | function Prompt:command_delete_horizontal_space() -- Meta-\
634 | end
635 |
636 | function Prompt:command_capitalize_word() -- Meta-c
637 | end
638 |
639 | function Prompt:command_downcase_word() -- Meta-l
640 | end
641 |
642 | function Prompt:command_upcase_word() -- Meta-u
643 | end
644 |
645 | -- Here for reference, won't likely be implemented
646 |
647 | function Prompt:command_set_mark() -- Control-@
648 | end
649 |
650 | function Prompt:command_rubout() -- Control-h
651 | end
652 |
653 | function Prompt:command_quoted_insert() -- Control-q v
654 | end
655 |
656 | function Prompt:command_char_search() -- Control-]
657 | end
658 |
659 | function Prompt:command_backward_kill_word() -- Meta-Control-h
660 | end
661 |
662 | function Prompt:command_tab_insert() -- Meta-Control-i
663 | end
664 |
665 | function Prompt:command_revert_line() -- Meta-Control-r
666 | end
667 |
668 | function Prompt:command_yank_nth_arg() -- Meta-Control-y
669 | end
670 |
671 | function Prompt:command_backward_char_search() -- Meta-Control-]
672 | end
673 |
674 | function Prompt:command_set_mark() -- Meta-SPACE
675 | end
676 |
677 | function Prompt:command_insert_comment() -- Meta-#
678 | end
679 |
680 | function Prompt:command_tilde_expand() -- Meta-&, Meta-~
681 | end
682 |
683 | function Prompt:command_insert_completions() -- Meta-*
684 | end
685 |
686 | function Prompt:command_digit_argument() -- Meta--
687 | end
688 |
689 | function Prompt:command_yank_last_arg() -- Meta-., Meta-_
690 | end
691 |
692 | function Prompt:command_noninc_forward_search() -- Meta-n
693 | end
694 |
695 | function Prompt:command_noninc_reverse_search() -- Meta-p
696 | end
697 |
698 | function Prompt:command_revert_line() -- Meta-r
699 | end
700 |
701 | function Prompt:command_yank_pop() -- Meta-y
702 | end
703 |
704 |
705 | -- If can't find terminfo, fallback to minimal list of necessary codes
706 | local ok, terminfo = pcall(require "tui.terminfo".find)
707 |
708 | Prompt.escapeCodes = ok and terminfo or {}
709 |
710 | -- Make sure we got everything we need
711 | Prompt.escapeCodes.cursor_invisible = Prompt.escapeCodes.cursor_invisible or Esc "[?25l"
712 | Prompt.escapeCodes.cursor_visible = Prompt.escapeCodes.cursor_visible or Esc "[?25h"
713 | Prompt.escapeCodes.clr_eos = Prompt.escapeCodes.clr_eos or Esc "[J"
714 | Prompt.escapeCodes.cursor_address = Prompt.escapeCodes.cursor_address or Esc "[%i%p1%d;%p2%dH"
715 | -- Get cursor position (https://invisible-island.net/ncurses/terminfo.ti.html)
716 | Prompt.escapeCodes.user7 = Prompt.escapeCodes.user7 or Esc "[6n"
717 | Prompt.escapeCodes.key_left = Prompt.escapeCodes.key_left or Esc "[D"
718 | Prompt.escapeCodes.key_right = Prompt.escapeCodes.key_right or Esc "[C"
719 | Prompt.escapeCodes.key_down = Prompt.escapeCodes.key_down or Esc "[B"
720 | Prompt.escapeCodes.key_up = Prompt.escapeCodes.key_up or Esc "[A"
721 | Prompt.escapeCodes.key_backspace = Prompt.escapeCodes.key_backspace or "\127"
722 | Prompt.escapeCodes.tab = Prompt.escapeCodes.tab or "\9"
723 | Prompt.escapeCodes.key_home = Prompt.escapeCodes.key_home or Esc "0H"
724 | Prompt.escapeCodes.key_end = Prompt.escapeCodes.key_end or Esc "0F"
725 | Prompt.escapeCodes.key_enter = Prompt.escapeCodes.key_enter or Esc "0M"
726 | Prompt.escapeCodes.parm_index = Prompt.escapeCodes.parm_inde or Esc "[%p1%dS"
727 |
728 | return Prompt
729 |
--------------------------------------------------------------------------------
/sirocco/winsize.c:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 | #include
5 |
6 | int lua_winsize(lua_State *L) {
7 | struct winsize sz;
8 |
9 | ioctl(0, TIOCGWINSZ, &sz);
10 |
11 | lua_pushinteger(L, sz.ws_col);
12 | lua_pushinteger(L, sz.ws_row);
13 |
14 | return 2;
15 | }
16 |
17 |
18 | int luaopen_sirocco_winsize(lua_State *L) {
19 | lua_pushcfunction(L, lua_winsize);
20 |
21 | return 1;
22 | }
23 |
--------------------------------------------------------------------------------