@@ -24,8 +24,10 @@ local utils = require("terminal.utils")
24
24
local width = require (" terminal.text.width" )
25
25
local output = require (" terminal.output" )
26
26
local EditLine = require (" terminal.editline" )
27
+ local Sequence = require (" terminal.sequence" )
27
28
local utf8 = require (" utf8" ) -- explicitly requires lua-utf8 for Lua < 5.3
28
29
30
+
29
31
-- Key bindings
30
32
local keys = t .input .keymap .get_keys ()
31
33
local keymap = t .input .keymap .get_keymap ()
107
109
-- @tparam [opt] number opts.position The initial cursor position (in char) of the input (default at the end).
108
110
-- @tparam [opt=80] number opts.max_length The maximum length of the input.
109
111
-- @tparam [opt] string opts.word_delimiters Word delimiters for word operations.
112
+ -- @tparam [opt] table opts.text_attr Text attributes for the prompt (input value only).
110
113
-- @treturn Prompt A new Prompt instance.
111
114
-- @name cli.Prompt
112
115
function Prompt :init (opts )
@@ -125,58 +128,106 @@ function Prompt:init(opts)
125
128
126
129
self .value = value
127
130
self .prompt = tostring (opts .prompt or " " ) -- the prompt to display
131
+ self .prompt_width = width .utf8swidth (self .prompt ) -- the width of the prompt in characters
128
132
self .max_length = opts .max_length or 80 -- the maximum length of the input
133
+ self .text_attr = opts .text_attr or {} -- text attributes for the input value
129
134
130
135
if self .value :len_char () > self .max_length then
131
136
-- truncate the value if it is too long, keep cursor position
132
137
local pos = self .value :pos_char ()
133
138
self .value = self .value :sub_char (1 , self .max_length )
134
139
self .value :goto_index (pos )
135
140
end
141
+
142
+ self .dirty = true -- whether the prompt needs to be redrawn
143
+ -- cached data
144
+ self .screen_rows = 0 -- the number of rows in the terminal screen
145
+ self .screen_cols = 0 -- the number of columns in the terminal screen
146
+ self .current_lines = {} -- the current formatted lines of the prompt
147
+ self .cursor_row = 1 -- the row where the cursor is currently located
148
+ self .cursor_col = 1 -- the column where the cursor is currently located
136
149
end
137
150
138
151
139
- --- Draw the whole thing: prompt and input value.
140
- -- This function writes the prompt and the current input value to the terminal.
152
+
153
+ -- Checks if terminal was resized, and if so, marks the prompt as dirty.
154
+ -- Will NOT update currently cached size!
141
155
-- @return nothing
142
- function Prompt :draw ()
143
- output .write (
144
- t .cursor .visible .set_seq (false ),
145
- t .cursor .position .column_seq (1 ),
146
- self .prompt ,
147
- self .value ,
148
- t .clear .eol_seq ()
149
- )
150
- self :updateCursor ()
156
+ function Prompt :check_resize ()
157
+ local new_rows , new_cols = t .size ()
158
+ if new_rows ~= self .screen_rows or new_cols ~= self .screen_cols then
159
+ self .dirty = true
160
+ end
151
161
end
152
162
153
163
154
- --- Draw the input value where the prompt ends.
155
- -- This function writes input value to the terminal .
164
+
165
+ -- updates cached data; terminal size, cursor pos, formatted lines .
156
166
-- @return nothing
157
- function Prompt :drawInput ()
158
- output .write (
159
- t .cursor .visible .set_seq (false ),
160
- t .cursor .position .column_seq (width .utf8swidth (self .prompt ) + 1 ),
161
- self .value ,
162
- t .clear .eol_seq ()
163
- )
164
- self :updateCursor ()
167
+ function Prompt :renew_cached_data ()
168
+ self .screen_rows , self .screen_cols = t .size ()
169
+ self .current_lines , self .cursor_row , self .cursor_col = self .value :format {
170
+ width = self .screen_cols ,
171
+ first_width = self .screen_cols - self .prompt_width ,
172
+ wordwrap = false ,
173
+ pad = true ,
174
+ pad_last = false ,
175
+ no_new_cursor_line = false ,
176
+ }
177
+ end
178
+
179
+
180
+
181
+ -- Move the cursor to the top-left (relative movement).
182
+ -- @treturn string sequence to move cursor
183
+ function Prompt :move_cursor_to_top_seq ()
184
+ return t .cursor .position .vertical_seq (1 - self .cursor_row ) ..
185
+ t .cursor .position .column_seq (1 )
186
+ end
187
+
188
+
189
+
190
+ -- Move cursor from top left to the current position (relative movement).
191
+ -- @treturn string sequence to move cursor
192
+ function Prompt :move_cursor_to_position_seq ()
193
+ return t .cursor .position .vertical_seq (self .cursor_row - 1 ) ..
194
+ t .cursor .position .column_seq (self .cursor_col )
165
195
end
166
196
167
197
168
- -- Update the cursor position.
169
- -- This function moves the cursor to the current position based on the prompt and input value.
170
- -- @ tparam number column The column to move the cursor to. If not provided, it defaults to the end of
171
- -- the prompt plus the current input value cursor position .
198
+
199
+ -- Draw the whole thing: prompt and input value.
200
+ -- Moves the current cursor back to the top and writes the prompt and input value.
201
+ -- Repositions the cursor at the proper place in the current input value.
172
202
-- @return nothing
173
- function Prompt :updateCursor (column )
174
- column = column or (width .utf8swidth (self .prompt ) + self .value :pos_col ())
175
- t .cursor .position .column (column )
176
- t .cursor .visible .set (true )
203
+ function Prompt :draw ()
204
+ self :check_resize ()
205
+ if not self .dirty then
206
+ return -- nothing changed, no need to redraw
207
+ end
208
+
209
+ -- move cursor to top
210
+ local to_top_seq = self :move_cursor_to_top_seq () -- create BEFORE we renew cached data
211
+
212
+ self :renew_cached_data ()
213
+
214
+ local l = Sequence (table.unpack (self .current_lines ))
215
+ local s = Sequence (
216
+ to_top_seq , -- move cursor to top
217
+ self .prompt , -- prompt
218
+ function () return t .text .stack .push_seq (self .text_attr ) end , -- push text attributes
219
+ l , -- all lines concatenated (we formatted using padding, so should properly wrap)
220
+ t .text .stack .pop_seq , -- pop text attributes
221
+ t .clear .eol_seq (), -- clear the rest of the last line
222
+ t .cursor .position .column_seq (self .cursor_col ), -- move cursor to proper column
223
+ t .cursor .position .up_seq (# self .current_lines - self .cursor_row - 1 ) -- move cursor to proper row
224
+ )
225
+
226
+ output .write (s )
177
227
end
178
228
179
229
230
+
180
231
--- Processes key input async
181
232
-- This function listens for key events and processes them.
182
233
-- @return string "returned" or "cancelled" based on the key pressed.
@@ -189,14 +240,10 @@ function Prompt:handleInput()
189
240
local action = Prompt .keyname2actions [keyname ]
190
241
191
242
if action then
192
- local redraw = Prompt .actions2redraw [action ]
193
243
local method = self .value [action ] or nop
194
244
method (self .value )
195
-
196
- if redraw then
197
- self :drawInput ()
198
- else
199
- self :updateCursor ()
245
+ if Prompt .actions2redraw [action ] then
246
+ self .dirty = true -- redraw needed
200
247
end
201
248
202
249
elseif keyname == keys .escape and self .cancellable then
@@ -213,7 +260,15 @@ function Prompt:handleInput()
213
260
214
261
else -- add the character at the current cursor
215
262
self .value :insert (keyname )
216
- self :drawInput ()
263
+ self .dirty = true
264
+ end
265
+
266
+ -- update UI
267
+ if self .dirty then
268
+ self :draw ()
269
+ else
270
+ -- just reposition cursor
271
+ output .write (self :move_cursor_to_top_seq (), self :move_cursor_to_position_seq ())
217
272
end
218
273
end
219
274
end
0 commit comments