11--- @class ProgressBar
2- --- @field loading fun ( self : ProgressBar )
3- --- @field error fun ( self : ProgressBar , percent : number | nil )
4- --- @field progress fun ( self : ProgressBar , percent : number | nil )
5- --- @field clear fun ( self : ProgressBar )
2+ --- @field tasks table<string , { percentage ?: number } >
3+ --- @field render_interval integer
4+ --- @field clear_delay integer
5+ --- @field render_scheduled boolean
6+ --- @field render_timer uv.uv_timer_t ?
7+ --- @field clear_timer uv.uv_timer_t ?
8+ --- @field last_state integer ?
9+ --- @field last_percent integer ?
610local ProgressBar = {}
11+ ProgressBar .__index = ProgressBar
712
813local ESC = " \27 "
914
10- -- OSC ... ST (ST can be ESC \)
15+ --- @enum con_emu_state
16+ local con_emu_state = {
17+ clear = 0 ,
18+ progress = 1 ,
19+ error = 2 ,
20+ loading = 3 ,
21+ paused = 4 ,
22+ }
23+
1124--- @param payload string
1225--- @return string
1326local function osc (payload )
1427 local seq = ESC .. " ]" .. payload .. ESC .. " \\ "
15-
16- -- Wrap an escape sequence so tmux forwards it to the outer terminal.
17- -- This requires tmux allow-passthrough on.
18- if os.getenv (" TMUX" ) ~= nil then
19- -- Inside the DCS wrapper, tmux expects ESC bytes to be doubled.
28+ if vim .env .TMUX ~= nil then
2029 seq = seq :gsub (ESC , ESC .. ESC )
2130 return ESC .. " Ptmux;" .. seq .. ESC .. " \\ "
2231 end
23-
2432 return seq
2533end
2634
@@ -34,45 +42,150 @@ local function clamp(n, lo, hi)
3442 return n
3543end
3644
37- --- @enum con_emu_state
38- local con_emu_state = {
39- clear = 0 ,
40- progress = 1 , -- normal
41- error = 2 ,
42- loading = 3 , -- indeterminate
43- paused = 4 ,
44- }
45-
4645--- @param state con_emu_state
47- --- @param percent number | nil
46+ --- @param percent integer | nil
4847--- @return string
4948local function progress_osc (state , percent )
50- if percent ~= nil then
51- percent = math.floor (clamp (percent , 0 , 100 ))
52- return osc ((" 9;4;%d;%d" ):format (state , percent ))
53- end
49+ if percent ~= nil then return osc ((" 9;4;%d;%d" ):format (state , percent )) end
5450 return osc ((" 9;4;%d" ):format (state ))
5551end
5652
57- --- @param seq string
58- local function write_osc (seq )
59- -- vim.api.nvim_ui_send(seq)
60- -- NOTE: change this when upgrading to Neovim 0.12
61- -- selene: allow(incorrect_standard_library_use)
62- io.stdout :write (seq )
63- -- selene: allow(incorrect_standard_library_use)
64- io.stdout :flush ()
53+ --- @param client_id integer | string
54+ --- @param token unknown
55+ --- @return string
56+ local function task_key (client_id , token ) return tostring (client_id ) .. " :" .. tostring (token ) end
57+
58+ --- @param opts ? { render_interval ?: integer , clear_delay ?: integer }
59+ --- @return ProgressBar
60+ function ProgressBar .new (opts )
61+ opts = opts or {}
62+ return setmetatable ({
63+ tasks = {},
64+ render_interval = opts .render_interval or 100 ,
65+ clear_delay = opts .clear_delay or 300 ,
66+ render_scheduled = false ,
67+ render_timer = nil ,
68+ clear_timer = nil ,
69+ last_state = nil ,
70+ last_percent = nil ,
71+ }, ProgressBar )
72+ end
73+
74+ --- @param state con_emu_state
75+ --- @param percent number | nil
76+ --- @param force boolean | nil
77+ function ProgressBar :_send (state , percent , force )
78+ if percent ~= nil then percent = math.floor (clamp (percent , 0 , 100 )) end
79+ if not force and self .last_state == state and self .last_percent == percent then return end
80+
81+ local payload = progress_osc (state , percent )
82+ local ok = pcall (vim .api .nvim_ui_send , payload )
83+ if not ok then return end
84+
85+ self .last_state = state
86+ self .last_percent = percent
87+ end
88+
89+ function ProgressBar :_stop_clear_timer ()
90+ if self .clear_timer then self .clear_timer :stop () end
91+ end
92+
93+ function ProgressBar :_schedule_clear ()
94+ self :_stop_clear_timer ()
95+ if self .clear_timer == nil then self .clear_timer = vim .uv .new_timer () end
96+ self .clear_timer :start (
97+ self .clear_delay ,
98+ 0 ,
99+ vim .schedule_wrap (function ()
100+ if next (self .tasks ) ~= nil then return end
101+ self :_send (con_emu_state .clear , nil , true )
102+ end )
103+ )
104+ end
105+
106+ function ProgressBar :_render_now ()
107+ self :_stop_clear_timer ()
108+
109+ local count = 0
110+ local percent = nil
111+ for _ , task in pairs (self .tasks ) do
112+ count = count + 1
113+ if count == 1 then
114+ percent = task .percentage
115+ else
116+ percent = nil
117+ break
118+ end
119+ end
120+
121+ if count == 0 then
122+ self :_schedule_clear ()
123+ elseif percent ~= nil then
124+ self :_send (con_emu_state .progress , percent )
125+ else
126+ self :_send (con_emu_state .loading )
127+ end
128+ end
129+
130+ function ProgressBar :_schedule_render ()
131+ if self .render_scheduled then return end
132+
133+ self .render_scheduled = true
134+ if self .render_timer == nil then self .render_timer = vim .uv .new_timer () end
135+ self .render_timer :start (
136+ self .render_interval ,
137+ 0 ,
138+ vim .schedule_wrap (function ()
139+ self .render_scheduled = false
140+ self :_render_now ()
141+ end )
142+ )
143+ end
144+
145+ --- @param args vim.api.keyset.create_autocmd.callback_args
146+ function ProgressBar :on_lsp_progress (args )
147+ local data = args .data or {}
148+ local params = data .params or {}
149+ local value = params .value or {}
150+ local token = params .token
151+ if token == nil then return end
152+
153+ local key = task_key (data .client_id or 0 , token )
154+ if value .kind == " end" then
155+ self .tasks [key ] = nil
156+ else
157+ self .tasks [key ] = { percentage = value .percentage }
158+ end
159+
160+ self :_schedule_render ()
161+ end
162+
163+ function ProgressBar :clear ()
164+ self .tasks = {}
165+ self :_send (con_emu_state .clear , nil , true )
166+ end
167+
168+ --- @param self ProgressBar
169+ function ProgressBar :destroy ()
170+ if self .render_timer then
171+ self .render_timer :stop ()
172+ self .render_timer :close ()
173+ self .render_timer = nil
174+ end
175+ if self .clear_timer then
176+ self .clear_timer :stop ()
177+ self .clear_timer :close ()
178+ self .clear_timer = nil
179+ end
180+ self :clear ()
65181end
66182
67- function ProgressBar :loading () write_osc (progress_osc (con_emu_state .loading )) end
68- function ProgressBar :error (percent ) write_osc (progress_osc (con_emu_state .error , percent )) end
69- function ProgressBar :progress (percent ) write_osc (progress_osc (con_emu_state .progress , percent )) end
70- function ProgressBar :clear () write_osc (progress_osc (con_emu_state .clear )) end
183+ local progress_bar = ProgressBar .new ()
71184
72185vim .api .nvim_create_autocmd (" VimLeavePre" , {
73186 desc = " Clear progress bar on exit" ,
74187 group = vim .api .nvim_create_augroup (" clear-progress-bar" , { clear = true }),
75- callback = function () ProgressBar : clear () end ,
188+ callback = function () progress_bar : destroy () end ,
76189})
77190
78- return ProgressBar
191+ return progress_bar
0 commit comments