Skip to content

Commit d53e0ac

Browse files
committed
wip rewrite prompt
1 parent e4403d4 commit d53e0ac

File tree

2 files changed

+93
-37
lines changed

2 files changed

+93
-37
lines changed

examples/prompt.lua

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ local Prompt = require("terminal.cli.prompt")
44
local pr = Prompt {
55
prompt = "Enter something: ",
66
value = "Hello, 你-好 World 🚀!",
7-
max_length = 62,
7+
max_length = 262,
88
position = 2,
99
cancellable = true,
10+
text_attr = { brightness = "high" },
1011
}
1112

1213
t.initwrap(function () -- on Windows: wrap for utf8 output

src/terminal/cli/prompt.lua

Lines changed: 91 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@ local utils = require("terminal.utils")
2424
local width = require("terminal.text.width")
2525
local output = require("terminal.output")
2626
local EditLine = require("terminal.editline")
27+
local Sequence = require("terminal.sequence")
2728
local utf8 = require("utf8") -- explicitly requires lua-utf8 for Lua < 5.3
2829

30+
2931
-- Key bindings
3032
local keys = t.input.keymap.get_keys()
3133
local keymap = t.input.keymap.get_keymap()
@@ -107,6 +109,7 @@ end
107109
-- @tparam[opt] number opts.position The initial cursor position (in char) of the input (default at the end).
108110
-- @tparam[opt=80] number opts.max_length The maximum length of the input.
109111
-- @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).
110113
-- @treturn Prompt A new Prompt instance.
111114
-- @name cli.Prompt
112115
function Prompt:init(opts)
@@ -125,58 +128,106 @@ function Prompt:init(opts)
125128

126129
self.value = value
127130
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
128132
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
129134

130135
if self.value:len_char() > self.max_length then
131136
-- truncate the value if it is too long, keep cursor position
132137
local pos = self.value:pos_char()
133138
self.value = self.value:sub_char(1, self.max_length)
134139
self.value:goto_index(pos)
135140
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
136149
end
137150

138151

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!
141155
-- @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
151161
end
152162

153163

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.
156166
-- @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)
165195
end
166196

167197

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.
172202
-- @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)
177227
end
178228

179229

230+
180231
--- Processes key input async
181232
-- This function listens for key events and processes them.
182233
-- @return string "returned" or "cancelled" based on the key pressed.
@@ -189,14 +240,10 @@ function Prompt:handleInput()
189240
local action = Prompt.keyname2actions[keyname]
190241

191242
if action then
192-
local redraw = Prompt.actions2redraw[action]
193243
local method = self.value[action] or nop
194244
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
200247
end
201248

202249
elseif keyname == keys.escape and self.cancellable then
@@ -213,7 +260,15 @@ function Prompt:handleInput()
213260

214261
else -- add the character at the current cursor
215262
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())
217272
end
218273
end
219274
end

0 commit comments

Comments
 (0)