11local state = require (' opencode.state' )
22local config = require (' opencode.config' )
3- local util = require (' opencode.util' )
43local icons = require (' opencode.ui.icons' )
54local output_window = require (' opencode.ui.output_window' )
65local snapshot = require (' opencode.snapshot' )
7- local config_file = require (' opencode.config_file' )
86local loading_animation = require (' opencode.ui.loading_animation' )
97
108local M = {}
119
12- function M .render ()
13- if not output_window .mounted () or not M .mounted () then
14- return
15- end
16- --- @cast state.windows { output_win : integer }
10+ local function get_mode_highlight ()
11+ local mode = (state .current_mode or ' ' ):lower ()
12+ local highlights = {
13+ build = ' OpencodeAgentBuild' ,
14+ plan = ' OpencodeAgentPlan' ,
15+ }
16+ return highlights [mode ] or ' OpencodeAgentCustom'
17+ end
1718
19+ local function build_left_segments ()
1820 local segments = {}
19-
20- local append_to_footer = function (text )
21- return text and text ~= ' ' and table.insert (segments , text )
21+ if not state .is_running () and state .current_model then
22+ table.insert (segments , state .current_model )
2223 end
24+ return segments
25+ end
26+
27+ local function build_right_segments ()
28+ local segments = {}
2329
2430 if state .is_running () then
25- local config_mod = require (' opencode.config' )
26- local cancel_keymap = config_mod .get_key_for_function (' input_window' , ' stop' ) or ' <C-c>'
27- local legend = string.format (' %s to cancel' , cancel_keymap )
28- append_to_footer (legend )
31+ local cancel_keymap = config .get_key_for_function (' input_window' , ' stop' ) or ' <C-c>'
32+ table.insert (segments , string.format (' %s to cancel' , cancel_keymap ))
2933 end
3034
31- if state .current_model then
32- if config .ui .display_context_size then
33- local provider , model = state .current_model :match (' ^(.-)/(.+)$' )
34- local model_info = config_file .get_model_info (provider , model )
35- local limit = state .tokens_count and model_info and model_info .limit and model_info .limit .context or 0
36- append_to_footer (util .format_number (state .tokens_count ))
37- append_to_footer (util .format_percentage (limit > 0 and state .tokens_count / limit ))
38- end
39- if config .ui .display_cost then
40- append_to_footer (util .format_cost (state .cost ))
41- end
42- end
4335 local restore_points = snapshot .get_restore_points ()
4436 if restore_points and # restore_points > 0 then
45- local restore_point_text = string.format (' %s %d' , icons .get (' restore_point' ), # restore_points )
46- append_to_footer (restore_point_text )
37+ table.insert (segments , string.format (' %s %d' , icons .get (' restore_point' ), # restore_points ))
38+ end
39+
40+ if state .current_mode then
41+ table.insert (segments , string.format (' %s ' , state .current_mode :upper ()))
42+ end
43+
44+ return segments
45+ end
46+
47+ local function build_footer_text (left_text , right_text , win_width )
48+ local left_len = # left_text > 0 and # left_text + 1 or 0
49+ local right_len = # right_text > 0 and # right_text + 1 or 0
50+ local padding = math.max (0 , win_width - left_len - right_len )
51+
52+ local parts = {}
53+ if # left_text > 0 then
54+ table.insert (parts , left_text )
55+ end
56+ table.insert (parts , string.rep (' ' , padding ))
57+ if # right_text > 0 then
58+ table.insert (parts , right_text )
59+ end
60+
61+ return table.concat (parts , ' ' )
62+ end
63+
64+ local function create_mode_highlight (left_len , right_text , padding )
65+ if not state .current_mode then
66+ return {}
4767 end
4868
49- local win_width = vim .api .nvim_win_get_width (state .windows .output_win )
50- local footer_text = table.concat (segments , ' | ' ) .. ' '
51- footer_text = string.rep (' ' , win_width - # footer_text ) .. footer_text
69+ local mode_text = string.format (' %s ' , state .current_mode :upper ())
70+ local mode_start = left_len + padding + (# right_text > 0 and 1 or 0 )
5271
53- M .set_content ({ footer_text })
72+ return {
73+ {
74+ group = get_mode_highlight (),
75+ start_col = mode_start + # right_text - # mode_text ,
76+ end_col = mode_start + # right_text ,
77+ },
78+ }
79+ end
80+
81+ function M .render ()
82+ if not output_window .mounted () or not M .mounted () then
83+ return
84+ end
85+ --- @cast state.windows OpencodeWindowState
86+
87+ local left_text = table.concat (build_left_segments (), ' ' )
88+ local right_text = table.concat (build_right_segments (), ' ' )
89+ local win_width = vim .api .nvim_win_get_width (state .windows .output_win --[[ @as integer]] )
90+
91+ local footer_text = build_footer_text (left_text , right_text , win_width )
92+ local highlights = create_mode_highlight (# left_text , right_text , win_width - # left_text - # right_text - 1 )
93+
94+ M .set_content ({ footer_text }, highlights )
5495end
5596
5697--- @param output_win integer
@@ -80,6 +121,7 @@ local function on_job_count_changed(_, new, old)
80121 end
81122end
82123
124+ --- @param windows table Windows table to set up footer in
83125function M .setup (windows )
84126 if not windows .output_win then
85127 return false
@@ -88,13 +130,20 @@ function M.setup(windows)
88130 windows .footer_win = vim .api .nvim_open_win (windows .footer_buf , false , M ._build_footer_win_config (windows .output_win ))
89131 vim .api .nvim_set_option_value (' winhl' , ' Normal:OpencodeHint' , { win = windows .footer_win })
90132
91- -- for stats changes
133+ -- for model changes
92134 state .subscribe (' current_model' , on_change )
135+ state .subscribe (' current_mode' , on_change )
93136 state .subscribe (' active_session' , on_change )
94137 -- to show C-c message
95138 state .subscribe (' job_count' , on_job_count_changed )
96139 state .subscribe (' restore_points' , on_change )
97140
141+ vim .api .nvim_create_autocmd ({ ' VimResized' , ' WinResized' }, {
142+ callback = function ()
143+ M .update_window (windows )
144+ end ,
145+ })
146+
98147 loading_animation .setup ()
99148end
100149
@@ -107,28 +156,38 @@ function M.close()
107156 end
108157
109158 state .unsubscribe (' current_model' , on_change )
159+ state .unsubscribe (' current_mode' , on_change )
160+ state .unsubscribe (' active_session' , on_change )
110161 state .unsubscribe (' job_count' , on_job_count_changed )
111162 state .unsubscribe (' restore_points' , on_change )
112163
113164 loading_animation .teardown ()
114165end
115166
167+ local function is_valid_state (windows )
168+ return windows and windows .footer_win and windows .output_win and windows .footer_buf
169+ end
170+
171+ --- @param windows ? table Optional windows table , defaults to state.windows
172+ --- @return _cast windows OpencodeWindowState
116173function M .mounted (windows )
117174 windows = windows or state .windows
118175 return windows
119- and windows .footer_win
120- and vim .api .nvim_win_is_valid (windows .footer_win )
121- and windows .output_win
122- and vim .api .nvim_win_is_valid (windows .output_win )
123- and windows .footer_buf
176+ and is_valid_state (windows )
177+ and vim .api .nvim_win_is_valid (windows .footer_win --[[ @as integer]] )
178+ and vim .api .nvim_win_is_valid (windows .output_win --[[ @as integer]] )
124179end
125180
181+ --- @param windows table Windows table with footer_win and output_win
126182function M .update_window (windows )
127183 if not M .mounted (windows ) then
128184 return
129185 end
130186
131- vim .api .nvim_win_set_config (windows .footer_win , M ._build_footer_win_config (windows .output_win ))
187+ vim .api .nvim_win_set_config (
188+ windows .footer_win --[[ @as integer]] ,
189+ M ._build_footer_win_config (windows .output_win --[[ @as integer]] )
190+ )
132191 M .render ()
133192end
134193
@@ -139,27 +198,41 @@ function M.create_buf()
139198 return footer_buf
140199end
141200
142- function M .clear ()
201+ --- @param lines string[] Content lines to set in footer
202+ --- @param highlights ? table[] Optional highlight definitions
203+ function M .set_content (lines , highlights )
143204 if not M .mounted () then
144205 return
145206 end
146- --- @cast state.windows { footer_buf : integer }
147-
148- local foot_ns_id = vim .api .nvim_create_namespace (' opencode_footer' )
149- vim .api .nvim_buf_clear_namespace (state .windows .footer_buf , foot_ns_id , 0 , - 1 )
207+ --- @cast state.windows OpencodeWindowState
208+
209+ local buf = state .windows .footer_buf --[[ @as integer]]
210+ local ns_id = vim .api .nvim_create_namespace (' opencode_footer' )
211+
212+ vim .api .nvim_set_option_value (' modifiable' , true , { buf = buf })
213+ vim .api .nvim_buf_set_lines (buf , 0 , - 1 , false , lines or {})
214+ vim .api .nvim_buf_clear_namespace (buf , ns_id , 0 , - 1 )
215+
216+ if highlights and # lines > 0 then
217+ local line_length = # (lines [1 ] or ' ' )
218+ for _ , highlight in ipairs (highlights ) do
219+ local start_col = math.max (0 , math.min (highlight .start_col , line_length ))
220+ local end_col = math.max (start_col , math.min (highlight .end_col , line_length ))
221+
222+ if start_col < end_col then
223+ vim .api .nvim_buf_set_extmark (buf , ns_id , 0 , start_col , {
224+ end_col = end_col ,
225+ hl_group = highlight .group ,
226+ })
227+ end
228+ end
229+ end
150230
151- M . set_content ({ })
231+ vim . api . nvim_set_option_value ( ' modifiable ' , false , { buf = buf })
152232end
153233
154- function M .set_content (lines )
155- if not M .mounted () then
156- return
157- end
158- --- @cast state.windows { footer_buf : integer }
159-
160- vim .api .nvim_set_option_value (' modifiable' , true , { buf = state .windows .footer_buf })
161- vim .api .nvim_buf_set_lines (state .windows .footer_buf , 0 , - 1 , false , lines )
162- vim .api .nvim_set_option_value (' modifiable' , false , { buf = state .windows .footer_buf })
234+ function M .clear ()
235+ M .set_content ({})
163236end
164237
165238return M
0 commit comments