Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions lua/opencode/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,18 @@ M.defaults = {
input_position = 'bottom',
window_width = 0.40,
zoom_width = 0.8,
float = {
width = 0.95,
height = 0.9,
row = nil,
col = nil,
border = 'rounded',
gap = 1,
zindex = 40,
opts = {
winblend = 0,
},
},
picker_width = 100,
display_model = true,
display_context_size = true,
Expand Down
2 changes: 1 addition & 1 deletion lua/opencode/state/ui.lua
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ local store = require('opencode.state.store')
---@field output_cursor integer[]|nil
---@field output_view table|nil
---@field focused_window 'input'|'output'|nil
---@field position 'right'|'left'|'current'|nil
---@field position 'right'|'left'|'current'|'float'|nil
---@field owner_tab integer|nil

---@class OpencodeWindowState
Expand Down
13 changes: 12 additions & 1 deletion lua/opencode/types.lua
Original file line number Diff line number Diff line change
Expand Up @@ -188,13 +188,24 @@
---@field path_map (string | fun(host_path: string): string) | nil -- Map host paths to server paths
---@field reverse_path_map (fun(server_path: string): string) | nil -- Map server paths back to host paths

---@class OpencodeUIFloatConfig
---@field width number # Width in columns, or ratio when <= 1 (default: 0.95)
---@field height number # Height in rows, or ratio when <= 1 (default: 0.9)
---@field row number|nil # Top row, centered when nil
---@field col number|nil # Left column, centered when nil
---@field border string|string[]|nil # Float border passed to nvim_open_win
---@field gap integer # Rows between output and input floats
---@field zindex integer # Output float zindex; input uses zindex + 1
---@field opts table<string, any> # Window-local options applied to float windows

---@class OpencodeUIConfig
---@field enable_treesitter_markdown boolean
---@field position 'right'|'left'|'current' # Position of the UI (default: 'right')
---@field position 'right'|'left'|'current'|'float' # Position of the UI (default: 'right')
---@field input_position 'bottom'|'top' # Position of the input window (default: 'bottom')
---@field window_width number
---@field persist_state boolean
---@field zoom_width number
---@field float OpencodeUIFloatConfig
---@field picker_width number|nil # Default width for all pickers (nil uses current window width)
---@field display_model boolean
---@field display_context_size boolean
Expand Down
144 changes: 144 additions & 0 deletions lua/opencode/ui/float_layout.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
local config = require('opencode.config')
local state = require('opencode.state')

local M = {}

---@param value number|nil
---@param total integer
---@param fallback number
---@return integer
local function resolve_dimension(value, total, fallback)
local resolved = value or fallback
if resolved > 0 and resolved <= 1 then
return math.floor(total * resolved)
end
return math.floor(resolved)
end

---@param value number|nil
---@param total integer
---@param size integer
---@return integer
local function resolve_position(value, total, size)
if value == nil then
return math.floor((total - size) / 2)
end
if value > 0 and value <= 1 then
return math.floor(total * value)
end
return math.floor(value)
end

---@param value integer
---@param min_value integer
---@param max_value integer
---@return integer
local function clamp(value, min_value, max_value)
return math.min(max_value, math.max(min_value, value))
end

---@param windows OpencodeWindowState|nil
---@return integer
local function input_height(windows)
local line_count = 1
if windows and windows.input_buf and vim.api.nvim_buf_is_valid(windows.input_buf) then
line_count = vim.api.nvim_buf_line_count(windows.input_buf)
end

local min_height = math.max(1, math.floor(vim.o.lines * config.ui.input.min_height))
local max_height = math.max(min_height, math.floor(vim.o.lines * config.ui.input.max_height))
return clamp(line_count, min_height, max_height)
end

---@param windows OpencodeWindowState|nil
---@return vim.api.keyset.win_config
local function base_config(windows)
local float = config.ui.float or {}
local width
if windows and windows.saved_width_ratio then
width = math.floor(vim.o.columns * windows.saved_width_ratio)
windows.saved_width_ratio = nil
elseif state.pre_zoom_width then
width = math.floor(vim.o.columns * config.ui.zoom_width)
else
width = resolve_dimension(float.width, vim.o.columns, 0.95)
end
local height = resolve_dimension(float.height, vim.o.lines, 0.9)

return {
relative = 'editor',
width = width,
height = height,
row = resolve_position(float.row, vim.o.lines, height),
col = resolve_position(float.col, vim.o.columns, width),
style = 'minimal',
border = float.border,
}
end

---@param windows OpencodeWindowState|nil
---@param show_input boolean
---@return vim.api.keyset.win_config output_config
function M.window_configs(windows, show_input)
local float = config.ui.float or {}
local base = base_config(windows)
local prompt_height = show_input and input_height(windows) or 0
local gap = show_input and (float.gap or 1) or 0
local output_height = math.max(1, base.height - prompt_height - gap)

local output_config = vim.tbl_deep_extend('force', base, {
height = output_height,
zindex = float.zindex or 40,
})

if not show_input then
return output_config, nil
end

local input_config = vim.tbl_deep_extend('force', base, {
height = prompt_height,
zindex = (float.zindex or 40) + 1,
})

if config.ui.input_position == 'top' then
output_config.row = base.row + prompt_height + gap
else
input_config.row = base.row + output_height + gap
end

return output_config, input_config
end

---@param buf integer
---@param enter boolean
---@param win_config vim.api.keyset.win_config
---@return integer
function M.open_win(buf, enter, win_config)
local win = vim.api.nvim_open_win(buf, enter, win_config)
local float = config.ui.float or {}
for opt, value in pairs(float.opts or {}) do
pcall(vim.api.nvim_set_option_value, opt, value, { win = win, scope = 'local' })
end
return win
end

---@param windows OpencodeWindowState|nil
---@param show_input boolean
function M.update(windows, show_input)
if not windows or not windows.output_win or not vim.api.nvim_win_is_valid(windows.output_win) then
return
end

local output_config, input_config = M.window_configs(windows, show_input)
pcall(vim.api.nvim_win_set_config, windows.output_win, output_config)

if show_input and input_config and windows.input_win and vim.api.nvim_win_is_valid(windows.input_win) then
pcall(vim.api.nvim_win_set_config, windows.input_win, input_config)
end

if windows.footer_win and vim.api.nvim_win_is_valid(windows.footer_win) then
require('opencode.ui.footer').update_window(windows)
end
end

return M
33 changes: 33 additions & 0 deletions lua/opencode/ui/input_window.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ local M = {}
local state = require('opencode.state')
local config = require('opencode.config')
local window_options = require('opencode.ui.window_options')
local float_layout = require('opencode.ui.float_layout')

-- Track hidden state
M._hidden = false
Expand Down Expand Up @@ -87,6 +88,12 @@ function M._build_input_win_config()
end

function M.create_window(windows)
if config.ui.position == 'float' then
local _, input_config = float_layout.window_configs(windows, true)
windows.input_win = float_layout.open_win(windows.input_buf, true, input_config)
return
end

windows.input_win = vim.api.nvim_open_win(windows.input_buf, true, M._build_input_win_config())
end

Expand Down Expand Up @@ -288,6 +295,11 @@ function M.update_dimensions(windows)
return
end

if config.ui.position == 'float' then
float_layout.update(windows, true)
return
end

local height = calculate_height(windows)
apply_dimensions(windows, height)
end
Expand Down Expand Up @@ -560,6 +572,10 @@ function M._hide()
pcall(vim.api.nvim_win_close, windows.input_win, false)
windows.input_win = nil

if config.ui.position == 'float' then
float_layout.update(windows, false)
end

vim.schedule(function()
M._toggling = false
end)
Expand Down Expand Up @@ -593,6 +609,23 @@ function M._show()
if not vim.api.nvim_win_is_valid(output_win) then
return
end

if config.ui.position == 'float' then
local output_config, input_config = float_layout.window_configs(windows, true)
pcall(vim.api.nvim_win_set_config, output_win, output_config)
windows.input_win = float_layout.open_win(windows.input_buf, true, input_config)
M.setup(windows)
M._hidden = false
M.focus_input()

if was_at_bottom then
vim.schedule(function()
require('opencode.ui.renderer').scroll_to_bottom(true)
end)
end
return
end

vim.api.nvim_set_current_win(output_win)

local input_position = config.ui.input_position or 'bottom'
Expand Down
6 changes: 6 additions & 0 deletions lua/opencode/ui/output_window.lua
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
local state = require('opencode.state')
local config = require('opencode.config')
local window_options = require('opencode.ui.window_options')
local float_layout = require('opencode.ui.float_layout')

local M = {}
M.namespace = vim.api.nvim_create_namespace('opencode_output')
Expand Down Expand Up @@ -216,6 +217,11 @@ function M.update_dimensions(windows)
return
end

if config.ui.position == 'float' then
float_layout.update(windows, windows.input_win ~= nil and vim.api.nvim_win_is_valid(windows.input_win))
return
end

local total_width = vim.api.nvim_get_option_value('columns', {})

local width_ratio
Expand Down
16 changes: 16 additions & 0 deletions lua/opencode/ui/ui.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ local state = require('opencode.state')
local renderer = require('opencode.ui.renderer')
local output_window = require('opencode.ui.output_window')
local input_window = require('opencode.ui.input_window')
local float_layout = require('opencode.ui.float_layout')
local footer = require('opencode.ui.footer')
local topbar = require('opencode.ui.topbar')

Expand Down Expand Up @@ -314,6 +315,17 @@ local function open_split(direction, type)
return vim.api.nvim_get_current_win()
end

---@param input_buf integer
---@param output_buf integer
---@return { input_win: integer, output_win: integer }
local function open_float(input_buf, output_buf)
local output_config, input_config = float_layout.window_configs({ input_buf = input_buf, output_buf = output_buf }, true)
local output_win = float_layout.open_win(output_buf, true, output_config)
local input_win = float_layout.open_win(input_buf, true, input_config)

return { input_win = input_win, output_win = output_win }
end

---@param input_buf integer
---@param output_buf integer
---@return { input_win: integer, output_win: integer }
Expand All @@ -323,6 +335,10 @@ function M.create_split_windows(input_buf, output_buf)
end
local ui_conf = config.ui

if ui_conf.position == 'float' then
return open_float(input_buf, output_buf)
end

local main_win
if ui_conf.position == 'current' then
main_win = vim.api.nvim_get_current_win()
Expand Down