Compare commits

...
11 Commits
Author SHA1 Message Date
Dax Raad 87d21ebf2b Revert "fix: prevent sparse spacing in hyphenated words (#1102)"
publish / publish (push) Has been cancelled
This reverts commit 2b44dbdbf1.
2025-07-19 19:25:15 -04:00
Timo Clasen a524fc545c fix(hooks): prevent session_complete hook from firing on subagent sessions (#1149)
publish / publish (push) Has been cancelled
2025-07-19 18:20:07 -05:00
Dax Raad 4316edaf43 fix first run github copilot
publish / publish (push) Has been cancelled
2025-07-19 19:19:38 -04:00
Dax Raad d845924e8b ci: ignore
publish / publish (push) Has been cancelled
2025-07-19 19:00:17 -04:00
Dax Raad a29b322bdd ci: ignore
publish / publish (push) Has been cancelled
2025-07-19 18:54:46 -04:00
Dax Raad 9723ffa7a6 ignore: ci
publish / publish (push) Has been cancelled
2025-07-19 18:48:43 -04:00
Dax Raad f06cd88773 perf: more performance improvements
publish / publish (push) Has been cancelled
2025-07-19 18:41:21 -04:00
Dax Raad 9af92b6914 perf: scroll to bottom in thread
publish / publish (push) Has been cancelled
2025-07-19 17:55:01 -04:00
Dax Raad 8f64c4b312 disable todo tools when running as task
publish / publish (push) Has been cancelled
2025-07-19 15:54:11 -04:00
Dax Raad a32877e908 ignore: create memo abstraction
publish / publish (push) Has been cancelled
2025-07-19 15:26:26 -04:00
Dax Raad 6465c9c44a fix openrouter caching
publish / publish (push) Has been cancelled
2025-07-19 15:11:21 -04:00
13 changed files with 107 additions and 75 deletions
+1
View File
@@ -152,6 +152,7 @@ if (!snapshot) {
for (const pkg of ["opencode", "opencode-bin"]) { for (const pkg of ["opencode", "opencode-bin"]) {
await $`rm -rf ./dist/aur-${pkg}` await $`rm -rf ./dist/aur-${pkg}`
await $`git clone ssh://aur@aur.archlinux.org/${pkg}.git ./dist/aur-${pkg}` await $`git clone ssh://aur@aur.archlinux.org/${pkg}.git ./dist/aur-${pkg}`
await $`cd ./dist/aur-${pkg} && git checkout master`
await Bun.file(`./dist/aur-${pkg}/PKGBUILD`).write(pkgbuild.replace("${pkg}", pkg)) await Bun.file(`./dist/aur-${pkg}/PKGBUILD`).write(pkgbuild.replace("${pkg}", pkg))
await $`cd ./dist/aur-${pkg} && makepkg --printsrcinfo > .SRCINFO` await $`cd ./dist/aur-${pkg} && makepkg --printsrcinfo > .SRCINFO`
await $`cd ./dist/aur-${pkg} && git add PKGBUILD .SRCINFO` await $`cd ./dist/aur-${pkg} && git add PKGBUILD .SRCINFO`
+2 -1
View File
@@ -4,11 +4,12 @@ import path from "path"
export const AuthCopilot = lazy(async () => { export const AuthCopilot = lazy(async () => {
const file = Bun.file(path.join(Global.Path.state, "plugin", "copilot.ts")) const file = Bun.file(path.join(Global.Path.state, "plugin", "copilot.ts"))
const exists = await file.exists()
const response = fetch("https://raw.githubusercontent.com/sst/opencode-github-copilot/refs/heads/main/auth.ts") const response = fetch("https://raw.githubusercontent.com/sst/opencode-github-copilot/refs/heads/main/auth.ts")
.then((x) => Bun.write(file, x)) .then((x) => Bun.write(file, x))
.catch(() => {}) .catch(() => {})
if (!file.exists()) { if (!exists) {
const worked = await response const worked = await response
if (!worked) return if (!worked) return
} }
+5 -1
View File
@@ -31,9 +31,13 @@ export namespace ConfigHooks {
} }
}) })
Bus.subscribe(Session.Event.Idle, async () => { Bus.subscribe(Session.Event.Idle, async (payload) => {
const cfg = await Config.get() const cfg = await Config.get()
if (cfg.experimental?.hook?.session_completed) { if (cfg.experimental?.hook?.session_completed) {
const session = await Session.get(payload.properties.sessionID)
// Only fire hook for top-level sessions (not subagent sessions)
if (session.parentID) return
for (const item of cfg.experimental.hook.session_completed) { for (const item of cfg.experimental.hook.session_completed) {
log.info("session_completed", { log.info("session_completed", {
command: item.command, command: item.command,
+4 -11
View File
@@ -13,22 +13,15 @@ export namespace ProviderTransform {
anthropic: { anthropic: {
cacheControl: { type: "ephemeral" }, cacheControl: { type: "ephemeral" },
}, },
openaiCompatible: { openrouter: {
cache_control: { type: "ephemeral" }, cache_control: { type: "ephemeral" },
}, },
}
}
}
if (providerID === "amazon-bedrock" || modelID.includes("anthropic")) {
const system = msgs.filter((msg) => msg.role === "system").slice(0, 2)
const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
for (const msg of unique([...system, ...final])) {
msg.providerOptions = {
...msg.providerOptions,
bedrock: { bedrock: {
cachePoint: { type: "ephemeral" }, cachePoint: { type: "ephemeral" },
}, },
openaiCompatible: {
cache_control: { type: "ephemeral" },
},
} }
} }
} }
+2
View File
@@ -325,6 +325,7 @@ export namespace Session {
providerID: z.string(), providerID: z.string(),
modelID: z.string(), modelID: z.string(),
mode: z.string().optional(), mode: z.string().optional(),
tools: z.record(z.boolean()).optional(),
parts: z.array( parts: z.array(
z.discriminatedUnion("type", [ z.discriminatedUnion("type", [
MessageV2.TextPart.omit({ MessageV2.TextPart.omit({
@@ -618,6 +619,7 @@ export namespace Session {
for (const item of await Provider.tools(input.providerID)) { for (const item of await Provider.tools(input.providerID)) {
if (mode.tools[item.id] === false) continue if (mode.tools[item.id] === false) continue
if (input.tools?.[item.id] === false) continue
if (session.parentID && item.id === "task") continue if (session.parentID && item.id === "task") continue
tools[item.id] = tool({ tools[item.id] = tool({
id: item.id as any, id: item.id as any,
+4
View File
@@ -41,6 +41,10 @@ export const TaskTool = Tool.define({
sessionID: session.id, sessionID: session.id,
modelID: msg.modelID, modelID: msg.modelID,
providerID: msg.providerID, providerID: msg.providerID,
tools: {
todoread: false,
todowrite: false,
},
parts: [ parts: [
{ {
id: Identifier.ascending("part"), id: Identifier.ascending("part"),
+3
View File
@@ -91,6 +91,9 @@ func main() {
stream := httpClient.Event.ListStreaming(ctx) stream := httpClient.Event.ListStreaming(ctx)
for stream.Next() { for stream.Next() {
evt := stream.Current().AsUnion() evt := stream.Current().AsUnion()
if _, ok := evt.(opencode.EventListResponseEventStorageWrite); ok {
continue
}
program.Send(evt) program.Send(evt)
} }
if err := stream.Err(); err != nil { if err := stream.Err(); err != nil {
@@ -196,8 +196,6 @@ func renderText(
case opencode.UserMessage: case opencode.UserMessage:
ts = time.UnixMilli(int64(casted.Time.Created)) ts = time.UnixMilli(int64(casted.Time.Created))
base := styles.NewStyle().Foreground(t.Text()).Background(backgroundColor) base := styles.NewStyle().Foreground(t.Text()).Background(backgroundColor)
// Process @ mentions and styling with hyphen preservation
words := strings.Fields(text) words := strings.Fields(text)
for i, word := range words { for i, word := range words {
if strings.HasPrefix(word, "@") { if strings.HasPrefix(word, "@") {
@@ -206,14 +204,9 @@ func renderText(
words[i] = base.Render(word + " ") words[i] = base.Render(word + " ")
} }
} }
styledText := strings.Join(words, "") text = strings.Join(words, "")
text = ansi.WordwrapWc(text, width-6, " -")
// Apply word wrapping with hyphen preservation content = base.Width(width - 6).Render(text)
frameSize := util.GetMessageContainerFrame()
wrappedText := util.ProcessTextWithHyphens(styledText, func(t string) string {
return ansi.WordwrapWc(t, width-frameSize, " ")
})
content = base.Width(width - frameSize).Render(wrappedText)
} }
timestamp := ts. timestamp := ts.
@@ -715,4 +708,5 @@ func renderDiagnostics(
// if !ok { // if !ok {
// return "" // return ""
// } // }
} }
@@ -53,6 +53,8 @@ func (m *messagesComponent) Init() tea.Cmd {
} }
func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
measure := util.Measure("messages.Update")
defer measure("from", fmt.Sprintf("%T", msg))
var cmds []tea.Cmd var cmds []tea.Cmd
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.WindowSizeMsg: case tea.WindowSizeMsg:
@@ -102,9 +104,7 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.loading = false m.loading = false
m.tail = m.viewport.AtBottom() m.tail = m.viewport.AtBottom()
m.viewport = msg.viewport m.viewport = msg.viewport
if m.tail { m.header = msg.header
m.viewport.GotoBottom()
}
if m.dirty { if m.dirty {
cmds = append(cmds, m.renderView()) cmds = append(cmds, m.renderView())
} }
@@ -120,12 +120,12 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
type renderCompleteMsg struct { type renderCompleteMsg struct {
viewport viewport.Model viewport viewport.Model
header string
partCount int partCount int
lineCount int lineCount int
} }
func (m *messagesComponent) renderView() tea.Cmd { func (m *messagesComponent) renderView() tea.Cmd {
m.header = m.renderHeader()
if m.rendering { if m.rendering {
slog.Debug("pending render, skipping") slog.Debug("pending render, skipping")
@@ -138,8 +138,10 @@ func (m *messagesComponent) renderView() tea.Cmd {
m.rendering = true m.rendering = true
viewport := m.viewport viewport := m.viewport
tail := m.tail
return func() tea.Msg { return func() tea.Msg {
header := m.renderHeader()
measure := util.Measure("messages.renderView") measure := util.Measure("messages.renderView")
defer measure() defer measure()
@@ -402,8 +404,12 @@ func (m *messagesComponent) renderView() tea.Cmd {
content := "\n" + strings.Join(blocks, "\n\n") content := "\n" + strings.Join(blocks, "\n\n")
viewport.SetHeight(m.height - lipgloss.Height(m.header)) viewport.SetHeight(m.height - lipgloss.Height(m.header))
viewport.SetContent(content) viewport.SetContent(content)
if tail {
viewport.GotoBottom()
}
return renderCompleteMsg{ return renderCompleteMsg{
header: header,
viewport: viewport, viewport: viewport,
partCount: partCount, partCount: partCount,
lineCount: lineCount, lineCount: lineCount,
@@ -2051,10 +2051,6 @@ func wrapInterfaces(content []any, width int) [][]any {
if unicode.IsSpace(r) { if unicode.IsSpace(r) {
isSpace = true isSpace = true
} }
// Use hyphen-aware word boundary detection
if r == '-' {
isSpace = false
}
itemW = rw.RuneWidth(r) itemW = rw.RuneWidth(r)
} else if att, ok := item.(*Attachment); ok { } else if att, ok := item.(*Attachment); ok {
itemW = uniseg.StringWidth(att.Display) itemW = uniseg.StringWidth(att.Display)
+3 -3
View File
@@ -103,7 +103,7 @@ func (a appModel) Init() tea.Cmd {
} }
func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
measure := util.Measure("Update") measure := util.Measure("app.Update")
defer measure("from", fmt.Sprintf("%T", msg)) defer measure("from", fmt.Sprintf("%T", msg))
var cmd tea.Cmd var cmd tea.Cmd
@@ -528,17 +528,17 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
func (a appModel) View() string { func (a appModel) View() string {
measure := util.Measure("app.View")
defer measure()
t := theme.CurrentTheme() t := theme.CurrentTheme()
var mainLayout string var mainLayout string
measure := util.Measure("app.View")
if a.app.Session.ID == "" { if a.app.Session.ID == "" {
mainLayout = a.home() mainLayout = a.home()
} else { } else {
mainLayout = a.chat() mainLayout = a.chat()
} }
measure()
mainLayout = styles.NewStyle(). mainLayout = styles.NewStyle().
Background(t.Background()). Background(t.Background()).
Padding(0, 2). Padding(0, 2).
+3 -9
View File
@@ -83,17 +83,11 @@ func Extension(path string) string {
} }
func ToMarkdown(content string, width int, backgroundColor compat.AdaptiveColor) string { func ToMarkdown(content string, width int, backgroundColor compat.AdaptiveColor) string {
renderWidth := width - GetMarkdownContainerFrame() r := styles.GetMarkdownRenderer(width-6, backgroundColor)
r := styles.GetMarkdownRenderer(renderWidth, backgroundColor)
content = strings.ReplaceAll(content, RootPath+"/", "") content = strings.ReplaceAll(content, RootPath+"/", "")
rendered, _ := r.Render(content)
// Apply hyphen preservation during markdown rendering
rendered := ProcessTextWithHyphens(content, func(t string) string {
result, _ := r.Render(t)
return result
})
lines := strings.Split(rendered, "\n") lines := strings.Split(rendered, "\n")
// Clean up empty lines at start/end
if len(lines) > 0 { if len(lines) > 0 {
firstLine := lines[0] firstLine := lines[0]
cleaned := ansi.Strip(firstLine) cleaned := ansi.Strip(firstLine)
+66 -32
View File
@@ -43,17 +43,37 @@ func New(opts ...Option) (m Model) {
opt(&m) opt(&m)
} }
m.setInitialValues() m.setInitialValues()
m.memo = &Memo{}
return m return m
} }
type Memo struct {
dirty bool
cache string
}
func (m *Memo) View(render func() string) string {
if m.dirty {
// slog.Debug("memo dirty")
m.cache = render()
m.dirty = false
return m.cache
}
// slog.Debug("memo cache")
return m.cache
}
func (m *Memo) Invalidate() {
m.dirty = true
}
// Model is the Bubble Tea model for this viewport element. // Model is the Bubble Tea model for this viewport element.
type Model struct { type Model struct {
memo *Memo
width int width int
height int height int
KeyMap KeyMap KeyMap KeyMap
cache string
// Whether or not to wrap text. If false, it'll allow horizontal scrolling // Whether or not to wrap text. If false, it'll allow horizontal scrolling
// instead. // instead.
SoftWrap bool SoftWrap bool
@@ -160,6 +180,7 @@ func (m Model) Height() int {
// SetHeight sets the height of the viewport. // SetHeight sets the height of the viewport.
func (m *Model) SetHeight(h int) { func (m *Model) SetHeight(h int) {
m.height = h m.height = h
m.memo.Invalidate()
} }
// Width returns the width of the viewport. // Width returns the width of the viewport.
@@ -170,6 +191,7 @@ func (m Model) Width() int {
// SetWidth sets the width of the viewport. // SetWidth sets the width of the viewport.
func (m *Model) SetWidth(w int) { func (m *Model) SetWidth(w int) {
m.width = w m.width = w
m.memo.Invalidate()
} }
// AtTop returns whether or not the viewport is at the very top position. // AtTop returns whether or not the viewport is at the very top position.
@@ -220,7 +242,7 @@ func (m Model) HorizontalScrollPercent() float64 {
func (m *Model) SetContent(s string) { func (m *Model) SetContent(s string) {
s = strings.ReplaceAll(s, "\r\n", "\n") // normalize line endings s = strings.ReplaceAll(s, "\r\n", "\n") // normalize line endings
m.SetContentLines(strings.Split(s, "\n")) m.SetContentLines(strings.Split(s, "\n"))
m.render() m.memo.Invalidate()
} }
// SetContentLines allows to set the lines to be shown instead of the content. // SetContentLines allows to set the lines to be shown instead of the content.
@@ -239,7 +261,7 @@ func (m *Model) SetContentLines(lines []string) {
if m.YOffset > m.maxYOffset() { if m.YOffset > m.maxYOffset() {
m.GotoBottom() m.GotoBottom()
} }
m.render() m.memo.Invalidate()
} }
// GetContent returns the entire content as a single string. // GetContent returns the entire content as a single string.
@@ -442,12 +464,14 @@ func (m Model) setupGutter(lines []string) []string {
} }
result[i] = strings.Join(line, "\n") result[i] = strings.Join(line, "\n")
} }
m.memo.Invalidate()
return result return result
} }
// SetYOffset sets the Y offset. // SetYOffset sets the Y offset.
func (m *Model) SetYOffset(n int) { func (m *Model) SetYOffset(n int) {
m.YOffset = clamp(n, 0, m.maxYOffset()) m.YOffset = clamp(n, 0, m.maxYOffset())
m.memo.Invalidate()
} }
// SetXOffset sets the X offset. // SetXOffset sets the X offset.
@@ -457,6 +481,7 @@ func (m *Model) SetXOffset(n int) {
return return
} }
m.xOffset = clamp(n, 0, m.maxXOffset()) m.xOffset = clamp(n, 0, m.maxXOffset())
m.memo.Invalidate()
} }
// EnsureVisible ensures that the given line and column are in the viewport. // EnsureVisible ensures that the given line and column are in the viewport.
@@ -483,7 +508,7 @@ func (m *Model) ViewDown() {
} }
m.LineDown(m.Height()) m.LineDown(m.Height())
m.render() m.memo.Invalidate()
} }
// ViewUp moves the view up by one height of the viewport. Basically, "page up". // ViewUp moves the view up by one height of the viewport. Basically, "page up".
@@ -493,7 +518,7 @@ func (m *Model) ViewUp() {
} }
m.LineUp(m.Height()) m.LineUp(m.Height())
m.render() m.memo.Invalidate()
} }
// HalfViewDown moves the view down by half the height of the viewport. // HalfViewDown moves the view down by half the height of the viewport.
@@ -503,7 +528,7 @@ func (m *Model) HalfViewDown() {
} }
m.LineDown(m.Height() / 2) //nolint:mnd m.LineDown(m.Height() / 2) //nolint:mnd
m.render() m.memo.Invalidate()
} }
// HalfViewUp moves the view up by half the height of the viewport. // HalfViewUp moves the view up by half the height of the viewport.
@@ -513,7 +538,7 @@ func (m *Model) HalfViewUp() {
} }
m.LineUp(m.Height() / 2) //nolint:mnd m.LineUp(m.Height() / 2) //nolint:mnd
m.render() m.memo.Invalidate()
} }
// LineDown moves the view down by the given number of lines. // LineDown moves the view down by the given number of lines.
@@ -527,7 +552,7 @@ func (m *Model) LineDown(n int) {
// the bottom. // the bottom.
m.SetYOffset(m.YOffset + n) m.SetYOffset(m.YOffset + n)
m.hiIdx = m.findNearedtMatch() m.hiIdx = m.findNearedtMatch()
m.render() m.memo.Invalidate()
} }
// LineUp moves the view down by the given number of lines. Returns the new // LineUp moves the view down by the given number of lines. Returns the new
@@ -541,7 +566,7 @@ func (m *Model) LineUp(n int) {
// greater than the number of lines we are from the top. // greater than the number of lines we are from the top.
m.SetYOffset(m.YOffset - n) m.SetYOffset(m.YOffset - n)
m.hiIdx = m.findNearedtMatch() m.hiIdx = m.findNearedtMatch()
m.render() m.memo.Invalidate()
} }
// TotalLineCount returns the total number of lines (both hidden and visible) within the viewport. // TotalLineCount returns the total number of lines (both hidden and visible) within the viewport.
@@ -562,7 +587,7 @@ func (m *Model) GotoTop() (lines []string) {
m.SetYOffset(0) m.SetYOffset(0)
m.hiIdx = m.findNearedtMatch() m.hiIdx = m.findNearedtMatch()
m.render() m.memo.Invalidate()
return m.visibleLines() return m.visibleLines()
} }
@@ -570,7 +595,7 @@ func (m *Model) GotoTop() (lines []string) {
func (m *Model) GotoBottom() (lines []string) { func (m *Model) GotoBottom() (lines []string) {
m.SetYOffset(m.maxYOffset()) m.SetYOffset(m.maxYOffset())
m.hiIdx = m.findNearedtMatch() m.hiIdx = m.findNearedtMatch()
m.render() m.memo.Invalidate()
return m.visibleLines() return m.visibleLines()
} }
@@ -583,6 +608,7 @@ func (m *Model) SetHorizontalStep(n int) {
} }
m.horizontalStep = n m.horizontalStep = n
m.memo.Invalidate()
} }
// MoveLeft moves the viewport to the left by the given number of columns. // MoveLeft moves the viewport to the left by the given number of columns.
@@ -590,6 +616,7 @@ func (m *Model) MoveLeft(cols int) {
m.xOffset -= cols m.xOffset -= cols
if m.xOffset < 0 { if m.xOffset < 0 {
m.xOffset = 0 m.xOffset = 0
m.memo.Invalidate()
} }
} }
@@ -606,6 +633,7 @@ func (m *Model) MoveRight(cols int) {
// Resets lines indent to zero. // Resets lines indent to zero.
func (m *Model) ResetIndent() { func (m *Model) ResetIndent() {
m.xOffset = 0 m.xOffset = 0
m.memo.Invalidate()
} }
// SetHighlights sets ranges of characters to highlight. // SetHighlights sets ranges of characters to highlight.
@@ -623,12 +651,14 @@ func (m *Model) SetHighlights(matches [][]int) {
m.highlights = parseMatches(m.GetContent(), matches) m.highlights = parseMatches(m.GetContent(), matches)
m.hiIdx = m.findNearedtMatch() m.hiIdx = m.findNearedtMatch()
m.showHighlight() m.showHighlight()
m.memo.Invalidate()
} }
// ClearHighlights clears previously set highlights. // ClearHighlights clears previously set highlights.
func (m *Model) ClearHighlights() { func (m *Model) ClearHighlights() {
m.highlights = nil m.highlights = nil
m.hiIdx = -1 m.hiIdx = -1
m.memo.Invalidate()
} }
func (m *Model) showHighlight() { func (m *Model) showHighlight() {
@@ -637,6 +667,7 @@ func (m *Model) showHighlight() {
} }
line, colstart, colend := m.highlights[m.hiIdx].coords() line, colstart, colend := m.highlights[m.hiIdx].coords()
m.EnsureVisible(line, colstart, colend) m.EnsureVisible(line, colstart, colend)
m.memo.Invalidate()
} }
// HighlightNext highlights the next match. // HighlightNext highlights the next match.
@@ -647,6 +678,7 @@ func (m *Model) HighlightNext() {
m.hiIdx = (m.hiIdx + 1) % len(m.highlights) m.hiIdx = (m.hiIdx + 1) % len(m.highlights)
m.showHighlight() m.showHighlight()
m.memo.Invalidate()
} }
// HighlightPrevious highlights the previous match. // HighlightPrevious highlights the previous match.
@@ -657,6 +689,7 @@ func (m *Model) HighlightPrevious() {
m.hiIdx = (m.hiIdx - 1 + len(m.highlights)) % len(m.highlights) m.hiIdx = (m.hiIdx - 1 + len(m.highlights)) % len(m.highlights)
m.showHighlight() m.showHighlight()
m.memo.Invalidate()
} }
func (m Model) findNearedtMatch() int { func (m Model) findNearedtMatch() int {
@@ -728,29 +761,30 @@ func (m Model) updateAsModel(msg tea.Msg) Model {
// View renders the viewport into a string. // View renders the viewport into a string.
func (m *Model) render() { func (m *Model) render() {
w, h := m.Width(), m.Height()
if sw := m.Style.GetWidth(); sw != 0 {
w = min(w, sw)
}
if sh := m.Style.GetHeight(); sh != 0 {
h = min(h, sh)
}
contentWidth := w - m.Style.GetHorizontalFrameSize()
contentHeight := h - m.Style.GetVerticalFrameSize()
visible := m.visibleLines()
contents := lipgloss.NewStyle().
Width(contentWidth). // pad to width.
Height(contentHeight). // pad to height.
MaxHeight(contentHeight). // truncate height if taller.
MaxWidth(contentWidth). // truncate width if wider.
Render(strings.Join(visible, "\n"))
m.cache = m.Style.
UnsetWidth().UnsetHeight(). // Style size already applied in contents.
Render(contents)
} }
func (m Model) View() string { func (m Model) View() string {
return m.cache return m.memo.View(func() string {
w, h := m.Width(), m.Height()
if sw := m.Style.GetWidth(); sw != 0 {
w = min(w, sw)
}
if sh := m.Style.GetHeight(); sh != 0 {
h = min(h, sh)
}
contentWidth := w - m.Style.GetHorizontalFrameSize()
contentHeight := h - m.Style.GetVerticalFrameSize()
visible := m.visibleLines()
contents := lipgloss.NewStyle().
Width(contentWidth). // pad to width.
Height(contentHeight). // pad to height.
MaxHeight(contentHeight). // truncate height if taller.
MaxWidth(contentWidth). // truncate width if wider.
Render(strings.Join(visible, "\n"))
return m.Style.
UnsetWidth().UnsetHeight(). // Style size already applied in contents.
Render(contents)
})
} }
func clamp(v, low, high int) int { func clamp(v, low, high int) int {