mirror of
https://github.com/neovim/neovim.git
synced 2026-04-01 05:12:02 +00:00
feat(ui2): configure maximum window heights #38392
Problem: - Window height is set dynamically to match the text height, making it difficult for the user to use a different height. - Cmdwin is closed to enter the pager but still taken into account for the pager position, and not restored when the pager is closed. - Dialog pager handler may unnecessarily consume <Esc>. Solution: - Add maximum height config fields for each of the UI2 windows, where a number smaller than one is a fraction of 'lines', absolute height otherwise (i.e. `cfg.msg.pager.height = 0.5`). - If the cmdwin will be closed to enter the pager, don't try to position the pager above it. Re-enter the cmdwin when the pager is closed. - Only add vim.on_key() handler for the dialog paging is actually possible.
This commit is contained in:
@@ -5399,25 +5399,37 @@ To enable this feature (default opts shown): >lua
|
|||||||
---@type string|table<string, 'cmd'|'msg'|'pager'> Default message target
|
---@type string|table<string, 'cmd'|'msg'|'pager'> Default message target
|
||||||
---or table mapping |ui-messages| kinds and triggers to a target.
|
---or table mapping |ui-messages| kinds and triggers to a target.
|
||||||
targets = 'cmd',
|
targets = 'cmd',
|
||||||
timeout = 4000, -- Time a message is visible in the message window.
|
cmd = { -- Options related to messages in the cmdline window.
|
||||||
|
height = 0.5 -- Maximum height while expanded for messages beyond 'cmdheight'.
|
||||||
|
},
|
||||||
|
dialog = { -- Options related to dialog window.
|
||||||
|
height = 0.5, -- Maximum height.
|
||||||
|
},
|
||||||
|
msg = { -- Options related to msg window.
|
||||||
|
height = 0.5, -- Maximum height.
|
||||||
|
timeout = 4000, -- Time a message is visible in the message window.
|
||||||
|
},
|
||||||
|
pager = { -- Options related to message window.
|
||||||
|
height = 1, -- Maximum height.
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
<
|
<
|
||||||
|
|
||||||
There are four special windows/buffers for presenting messages and cmdline:
|
There are four special windows/buffers for presenting messages and cmdline:
|
||||||
• "cmd": Cmdline. Also used for 'showcmd', 'showmode', 'ruler', and messages
|
• "cmd": Cmdline. Also used for 'showcmd', 'showmode', 'ruler', and messages
|
||||||
if 'cmdheight' > 0.
|
by default.
|
||||||
• "msg": Message window, shows messages when 'cmdheight' == 0.
|
• "msg": Message window, shows fleeting messages useful for 'cmdheight' == 0.
|
||||||
• "pager": Pager window, shows |:messages| and certain messages that are never
|
• "pager": Pager window, shows |:messages| and certain messages that are never
|
||||||
"collapsed".
|
"collapsed".
|
||||||
• "dialog": Dialog window, shows modal prompts that expect user input.
|
• "dialog": Dialog window, shows modal prompts that expect user input.
|
||||||
|
|
||||||
The buffer 'filetype' is to the above-listed id ("cmd", "msg", …). Handle
|
The buffer 'filetype' is set to the above-listed id ("cmd", "msg", …).
|
||||||
the |FileType| event to configure any local options for these windows and
|
Handle the |FileType| event to configure any local options for these windows
|
||||||
their respective buffers.
|
and their respective buffers.
|
||||||
|
|
||||||
Unlike the legacy |hit-enter| prompt, messages that overflow the cmdline area
|
Unlike the legacy |hit-enter| prompt, messages exceeding 'cmdheight' are
|
||||||
are instead "collapsed", followed by a `[+x]` "spill" indicator, where `x`
|
instead "collapsed", followed by a `[+x]` "spill" indicator, where `x`
|
||||||
indicates the spilled lines. To see the full messages, do either:
|
indicates the spilled lines. To see the full messages, do either:
|
||||||
• ENTER immediately after a message from interactive |:| cmdline.
|
• ENTER immediately after a message from interactive |:| cmdline.
|
||||||
• |g<| at any time.
|
• |g<| at any time.
|
||||||
|
|||||||
@@ -13,23 +13,36 @@
|
|||||||
--- ---@type string|table<string, 'cmd'|'msg'|'pager'> Default message target
|
--- ---@type string|table<string, 'cmd'|'msg'|'pager'> Default message target
|
||||||
--- ---or table mapping |ui-messages| kinds and triggers to a target.
|
--- ---or table mapping |ui-messages| kinds and triggers to a target.
|
||||||
--- targets = 'cmd',
|
--- targets = 'cmd',
|
||||||
--- timeout = 4000, -- Time a message is visible in the message window.
|
--- cmd = { -- Options related to messages in the cmdline window.
|
||||||
|
--- height = 0.5 -- Maximum height while expanded for messages beyond 'cmdheight'.
|
||||||
|
--- },
|
||||||
|
--- dialog = { -- Options related to dialog window.
|
||||||
|
--- height = 0.5, -- Maximum height.
|
||||||
|
--- },
|
||||||
|
--- msg = { -- Options related to msg window.
|
||||||
|
--- height = 0.5, -- Maximum height.
|
||||||
|
--- timeout = 4000, -- Time a message is visible in the message window.
|
||||||
|
--- },
|
||||||
|
--- pager = { -- Options related to message window.
|
||||||
|
--- height = 1, -- Maximum height.
|
||||||
|
--- },
|
||||||
--- },
|
--- },
|
||||||
--- })
|
--- })
|
||||||
--- ```
|
--- ```
|
||||||
---
|
---
|
||||||
--- There are four special windows/buffers for presenting messages and cmdline:
|
--- There are four special windows/buffers for presenting messages and cmdline:
|
||||||
--- - "cmd": Cmdline. Also used for 'showcmd', 'showmode', 'ruler', and messages if 'cmdheight' > 0.
|
--- - "cmd": Cmdline. Also used for 'showcmd', 'showmode', 'ruler', and messages by default.
|
||||||
--- - "msg": Message window, shows messages when 'cmdheight' == 0.
|
--- - "msg": Message window, shows fleeting messages useful for 'cmdheight' == 0.
|
||||||
--- - "pager": Pager window, shows |:messages| and certain messages that are never "collapsed".
|
--- - "pager": Pager window, shows |:messages| and certain messages that are never "collapsed".
|
||||||
--- - "dialog": Dialog window, shows modal prompts that expect user input.
|
--- - "dialog": Dialog window, shows modal prompts that expect user input.
|
||||||
---
|
---
|
||||||
--- The buffer 'filetype' is to the above-listed id ("cmd", "msg", …). Handle the |FileType| event
|
--- The buffer 'filetype' is set to the above-listed id ("cmd", "msg", …).
|
||||||
--- to configure any local options for these windows and their respective buffers.
|
--- Handle the |FileType| event to configure any local options for these
|
||||||
|
--- windows and their respective buffers.
|
||||||
---
|
---
|
||||||
--- Unlike the legacy |hit-enter| prompt, messages that overflow the cmdline area are instead
|
--- Unlike the legacy |hit-enter| prompt, messages exceeding 'cmdheight' are
|
||||||
--- "collapsed", followed by a `[+x]` "spill" indicator, where `x` indicates the spilled lines. To
|
--- instead "collapsed", followed by a `[+x]` "spill" indicator, where `x`
|
||||||
--- see the full messages, do either:
|
--- indicates the spilled lines. To see the full messages, do either:
|
||||||
--- - ENTER immediately after a message from interactive |:| cmdline.
|
--- - ENTER immediately after a message from interactive |:| cmdline.
|
||||||
--- - |g<| at any time.
|
--- - |g<| at any time.
|
||||||
|
|
||||||
@@ -46,7 +59,19 @@ local M = {
|
|||||||
msg = { -- Options related to the message module.
|
msg = { -- Options related to the message module.
|
||||||
target = 'cmd', ---@type 'cmd'|'msg' Default message target if not present in targets.
|
target = 'cmd', ---@type 'cmd'|'msg' Default message target if not present in targets.
|
||||||
targets = {}, ---@type table<string, 'cmd'|'msg'|'pager'> Kind specific message targets.
|
targets = {}, ---@type table<string, 'cmd'|'msg'|'pager'> Kind specific message targets.
|
||||||
timeout = 4000, -- Time a message is visible in the message window.
|
cmd = { -- Options related to messages in the cmdline window.
|
||||||
|
height = 0.5, -- Maximum height while expanded for messages beyond 'cmdheight'.
|
||||||
|
},
|
||||||
|
dialog = { -- Options related to dialog window.
|
||||||
|
height = 0.5, -- Maximum height.
|
||||||
|
},
|
||||||
|
msg = { -- Options related to msg window.
|
||||||
|
height = 0.5, -- Maximum height.
|
||||||
|
timeout = 4000, -- Time a message is visible in the message window.
|
||||||
|
},
|
||||||
|
pager = { -- Options related to message window.
|
||||||
|
height = 1, -- Maximum height.
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ function M.msg:start_timer(buf, id)
|
|||||||
pcall(api.nvim_win_set_config, ui.wins.msg, { hide = true })
|
pcall(api.nvim_win_set_config, ui.wins.msg, { hide = true })
|
||||||
self.width, M.virt.msg[M.virt.idx.dupe][1] = 1, nil
|
self.width, M.virt.msg[M.virt.idx.dupe][1] = 1, nil
|
||||||
end
|
end
|
||||||
end, ui.cfg.msg.timeout)
|
end, ui.cfg.msg.msg.timeout)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Place or delete a virtual text mark in the cmdline or message window.
|
--- Place or delete a virtual text mark in the cmdline or message window.
|
||||||
@@ -537,6 +537,7 @@ local function set_top_bot_spill()
|
|||||||
M.virt.bot[1][1] = botspill > 0 and { 0, (' [+%d]'):format(botspill) } or nil
|
M.virt.bot[1][1] = botspill > 0 and { 0, (' [+%d]'):format(botspill) } or nil
|
||||||
set_virttext('bot', 'dialog')
|
set_virttext('bot', 'dialog')
|
||||||
api.nvim__redraw({ flush = true })
|
api.nvim__redraw({ flush = true })
|
||||||
|
return topspill > 0 or botspill > 0
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Allow paging in the dialog window, consume the key if the topline changes.
|
--- Allow paging in the dialog window, consume the key if the topline changes.
|
||||||
@@ -571,20 +572,26 @@ local dialog_on_key = function(_, typed)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local was_cmdwin = ''
|
||||||
---@param min integer Minimum window height.
|
---@param min integer Minimum window height.
|
||||||
local function win_row_height(win, min)
|
local function win_row_height(tgt, min)
|
||||||
if win ~= ui.wins.pager then
|
local cfgmin = ui.cfg.msg[tgt].height --[[@as number]]
|
||||||
return (win == ui.wins.msg and 0 or 1) - ui.cmd.wmnumode,
|
cfgmin = cfgmin > 1 and cfgmin or math.ceil(o.lines * cfgmin)
|
||||||
math.min(min, math.ceil(o.lines * 0.5))
|
if tgt ~= 'pager' then
|
||||||
|
return (tgt == 'msg' and 0 or 1) - ui.cmd.wmnumode, math.min(min, cfgmin)
|
||||||
end
|
end
|
||||||
local cmdwin = fn.getcmdwintype() ~= '' and api.nvim_win_get_height(0) or 0
|
local cmdwin = fn.getcmdwintype() ~= was_cmdwin and api.nvim_win_get_height(0) or 0
|
||||||
local global_stl = (cmdwin > 0 or o.laststatus == 3) and 1 or 0
|
local global_stl = (cmdwin > 0 or o.laststatus == 3) and 1 or 0
|
||||||
local row = 1 - cmdwin - global_stl
|
local row = 1 - cmdwin - global_stl
|
||||||
return row, math.min(min, o.lines - 1 - ui.cmdheight - global_stl - cmdwin)
|
return row, math.min(math.min(cfgmin, min), o.lines - 1 - ui.cmdheight - global_stl - cmdwin)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function enter_pager()
|
local function enter_pager()
|
||||||
in_pager = true
|
-- Cannot leave the cmdwin to enter the pager, so close and re-open it.
|
||||||
|
in_pager, was_cmdwin = true, fn.getcmdwintype()
|
||||||
|
if was_cmdwin ~= '' then
|
||||||
|
api.nvim_command('quit')
|
||||||
|
end
|
||||||
-- Cmdwin is closed one event iteration later so schedule in case it was open.
|
-- Cmdwin is closed one event iteration later so schedule in case it was open.
|
||||||
vim.schedule(function()
|
vim.schedule(function()
|
||||||
local height, id = api.nvim_win_get_height(ui.wins.pager), 0
|
local height, id = api.nvim_win_get_height(ui.wins.pager), 0
|
||||||
@@ -607,13 +614,16 @@ local function enter_pager()
|
|||||||
in_pager = in_pager and api.nvim_win_is_valid(ui.wins.pager)
|
in_pager = in_pager and api.nvim_win_is_valid(ui.wins.pager)
|
||||||
local cfg = in_pager and { relative = 'laststatus', col = 0 } or { hide = true }
|
local cfg = in_pager and { relative = 'laststatus', col = 0 } or { hide = true }
|
||||||
if in_pager then
|
if in_pager then
|
||||||
cfg.row, cfg.height = win_row_height(ui.wins.pager, height)
|
cfg.row, cfg.height = win_row_height('pager', height)
|
||||||
end
|
else
|
||||||
pcall(api.nvim_win_set_config, ui.wins.pager, cfg)
|
|
||||||
if not in_pager then
|
|
||||||
pcall(api.nvim_set_option_value, 'eiw', 'all', { scope = 'local', win = ui.wins.pager })
|
pcall(api.nvim_set_option_value, 'eiw', 'all', { scope = 'local', win = ui.wins.pager })
|
||||||
api.nvim_del_autocmd(id)
|
api.nvim_del_autocmd(id)
|
||||||
|
if was_cmdwin ~= '' then
|
||||||
|
api.nvim_feedkeys('q' .. was_cmdwin, 'n', false)
|
||||||
|
was_cmdwin = ''
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
pcall(api.nvim_win_set_config, ui.wins.pager, cfg)
|
||||||
end,
|
end,
|
||||||
desc = 'Hide or reposition pager window.',
|
desc = 'Hide or reposition pager window.',
|
||||||
})
|
})
|
||||||
@@ -624,48 +634,37 @@ end
|
|||||||
---
|
---
|
||||||
---@param tgt? 'cmd'|'dialog'|'msg'|'pager' Target window to be positioned (nil for all).
|
---@param tgt? 'cmd'|'dialog'|'msg'|'pager' Target window to be positioned (nil for all).
|
||||||
function M.set_pos(tgt)
|
function M.set_pos(tgt)
|
||||||
local function win_set_pos(win)
|
|
||||||
local texth = api.nvim_win_text_height(win, {})
|
|
||||||
local top = { vim.opt.fcs:get().msgsep or ' ', 'MsgSeparator' }
|
|
||||||
local title = { 'f/d/j: screen/page/line down, b/u/k: up, <Esc>: stop paging', 'MsgSeparator' }
|
|
||||||
local cfg = { hide = false, relative = 'laststatus', col = 10000 }
|
|
||||||
cfg.row, cfg.height = win_row_height(win, texth.all)
|
|
||||||
cfg.border = win ~= ui.wins.msg and { '', top, '', '', '', '', '', '' } or nil
|
|
||||||
cfg.mouse = tgt == 'cmd' or nil
|
|
||||||
cfg.title = tgt == 'dialog' and cfg.height < texth.all and { title } or nil
|
|
||||||
api.nvim_win_set_config(win, cfg)
|
|
||||||
|
|
||||||
if tgt == 'cmd' and not M.cmd_on_key then
|
|
||||||
-- Temporarily expand the cmdline, until next key press.
|
|
||||||
local save_spill = M.virt.msg[M.virt.idx.spill][1]
|
|
||||||
local spill = texth.all > cfg.height and (' [+%d]'):format(texth.all - cfg.height)
|
|
||||||
M.virt.msg[M.virt.idx.spill][1] = spill and { 0, spill } or nil
|
|
||||||
set_virttext('msg', 'cmd')
|
|
||||||
M.virt.msg[M.virt.idx.spill][1] = save_spill
|
|
||||||
M.cmd_on_key = vim.on_key(cmd_on_key, ui.ns)
|
|
||||||
elseif tgt == 'dialog' then
|
|
||||||
M.dialog_on_key = vim.on_key(dialog_on_key, M.dialog_on_key)
|
|
||||||
set_top_bot_spill()
|
|
||||||
elseif tgt == 'msg' then
|
|
||||||
-- Ensure last line is visible and first line is at top of window.
|
|
||||||
fn.win_execute(ui.wins.msg, 'norm! Gzb')
|
|
||||||
elseif tgt == 'pager' and not in_pager then
|
|
||||||
if fn.getcmdwintype() ~= '' then
|
|
||||||
-- Cannot leave the cmdwin to enter the pager, so close it.
|
|
||||||
-- NOTE: regression w.r.t. the message grid, which allowed this.
|
|
||||||
-- Resolving that would require somehow bypassing textlock for the pager.
|
|
||||||
api.nvim_command('quit')
|
|
||||||
end
|
|
||||||
enter_pager()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
for t, win in pairs(ui.wins) do
|
for t, win in pairs(ui.wins) do
|
||||||
local cfg = (t == tgt or (tgt == nil and t ~= 'cmd'))
|
local cfg = (t == tgt or (tgt == nil and t ~= 'cmd'))
|
||||||
and api.nvim_win_is_valid(win)
|
and api.nvim_win_is_valid(win)
|
||||||
and api.nvim_win_get_config(win)
|
and api.nvim_win_get_config(win)
|
||||||
if cfg and (tgt or not cfg.hide) then
|
if cfg and (tgt or not cfg.hide) then
|
||||||
win_set_pos(win)
|
local texth = api.nvim_win_text_height(win, {})
|
||||||
|
local top = { vim.opt.fcs:get().msgsep or ' ', 'MsgSeparator' }
|
||||||
|
local hint = { 'f/d/j: screen/page/line down, b/u/k: up, <Esc>: stop paging', 'MsgSeparator' }
|
||||||
|
cfg = { hide = false, relative = 'laststatus', col = 10000 } ---@type table
|
||||||
|
cfg.row, cfg.height = win_row_height(t, texth.all)
|
||||||
|
cfg.border = t ~= 'msg' and { '', top, '', '', '', '', '', '' } or nil
|
||||||
|
cfg.mouse = tgt == 'cmd' or nil
|
||||||
|
cfg.title = tgt == 'dialog' and cfg.height < texth.all and { hint } or nil
|
||||||
|
api.nvim_win_set_config(win, cfg)
|
||||||
|
|
||||||
|
if tgt == 'cmd' and not M.cmd_on_key then
|
||||||
|
-- Temporarily expand the cmdline, until next key press.
|
||||||
|
local save_spill = M.virt.msg[M.virt.idx.spill][1]
|
||||||
|
local spill = texth.all > cfg.height and (' [+%d]'):format(texth.all - cfg.height)
|
||||||
|
M.virt.msg[M.virt.idx.spill][1] = spill and { 0, spill } or nil
|
||||||
|
set_virttext('msg', 'cmd')
|
||||||
|
M.virt.msg[M.virt.idx.spill][1] = save_spill
|
||||||
|
M.cmd_on_key = vim.on_key(cmd_on_key, ui.ns)
|
||||||
|
elseif tgt == 'dialog' and set_top_bot_spill() then
|
||||||
|
M.dialog_on_key = vim.on_key(dialog_on_key, M.dialog_on_key)
|
||||||
|
elseif tgt == 'msg' then
|
||||||
|
-- Ensure last line is visible and first line is at top of window.
|
||||||
|
fn.win_execute(ui.wins.msg, 'norm! Gzb')
|
||||||
|
elseif tgt == 'pager' and not in_pager then
|
||||||
|
enter_pager()
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ local api, clear, command, exec_lua, feed = n.api, n.clear, n.command, n.exec_lu
|
|||||||
local msg_timeout = 200
|
local msg_timeout = 200
|
||||||
local function set_msg_target_zero_ch()
|
local function set_msg_target_zero_ch()
|
||||||
exec_lua(function()
|
exec_lua(function()
|
||||||
require('vim._core.ui2').enable({ msg = { target = 'msg', timeout = msg_timeout } })
|
require('vim._core.ui2').enable({ msg = { target = 'msg', msg = { timeout = msg_timeout } } })
|
||||||
vim.o.cmdheight = 0
|
vim.o.cmdheight = 0
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
@@ -212,6 +212,51 @@ describe('messages2', function()
|
|||||||
{3:[Pager] 1,1 Top}|
|
{3:[Pager] 1,1 Top}|
|
||||||
{16::}^ |
|
{16::}^ |
|
||||||
]])
|
]])
|
||||||
|
-- Can enter pager from cmdwin.
|
||||||
|
feed('<Esc>qq:')
|
||||||
|
screen:expect([[
|
||||||
|
x |
|
||||||
|
{1:~ }|*3
|
||||||
|
─────────────────────────────────────────────────────|
|
||||||
|
{1::}echo "foo" | echo "bar\nbaz\n"->repeat(&lines) |
|
||||||
|
{1::}^ |
|
||||||
|
{1:~ }|*5
|
||||||
|
{3:[Command Line] 2,0-1 All}|
|
||||||
|
|
|
||||||
|
]])
|
||||||
|
feed(':messages<CR>')
|
||||||
|
screen:expect([[
|
||||||
|
{3: }|
|
||||||
|
^foo |
|
||||||
|
foo |*10
|
||||||
|
{3:[Pager] 1,1 Top}|
|
||||||
|
|
|
||||||
|
]])
|
||||||
|
-- Cmdwin is restored after pager is closed.
|
||||||
|
feed('q')
|
||||||
|
screen:expect([[
|
||||||
|
x |
|
||||||
|
{1:~ }|*3
|
||||||
|
─────────────────────────────────────────────────────|
|
||||||
|
{1::}echo "foo" | echo "bar\nbaz\n"->repeat(&lines) |
|
||||||
|
{1::}messages |
|
||||||
|
{1::}^ |
|
||||||
|
{1:~ }|*4
|
||||||
|
{3:[Command Line] 3,0-1 All}|
|
||||||
|
|
|
||||||
|
]])
|
||||||
|
-- Configured maximum height.
|
||||||
|
command('quit | lua require("vim._core.ui2").enable({msg = {pager = {height = 2 } } })')
|
||||||
|
command('messages')
|
||||||
|
screen:expect([[
|
||||||
|
x |
|
||||||
|
{1:~ }|*8
|
||||||
|
{3: }|
|
||||||
|
^foo |
|
||||||
|
foo |
|
||||||
|
{3:[Pager] 1,1 Top}|
|
||||||
|
|
|
||||||
|
]])
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('new buffer, window and options after closing a buffer or switching tabpage', function()
|
it('new buffer, window and options after closing a buffer or switching tabpage', function()
|
||||||
@@ -435,6 +480,21 @@ describe('messages2', function()
|
|||||||
|
|
||||||
it('paging prompt dialog #35191', function()
|
it('paging prompt dialog #35191', function()
|
||||||
screen:try_resize(71, screen._height)
|
screen:try_resize(71, screen._height)
|
||||||
|
-- Don't consume <Esc> when paging is not necessary.
|
||||||
|
feed(':call confirm("Ok?")<CR>')
|
||||||
|
screen:expect([[
|
||||||
|
|
|
||||||
|
{1:~ }|*10
|
||||||
|
{3: }|
|
||||||
|
{6:Ok?} |
|
||||||
|
{6:[O]k: }^ |
|
||||||
|
]])
|
||||||
|
feed('<Esc>')
|
||||||
|
screen:expect([[
|
||||||
|
^ |
|
||||||
|
{1:~ }|*12
|
||||||
|
|
|
||||||
|
]])
|
||||||
local top = [[
|
local top = [[
|
||||||
|
|
|
|
||||||
{1:~ }|*4
|
{1:~ }|*4
|
||||||
|
|||||||
Reference in New Issue
Block a user