fix(ui): accurately calculate help and main height in UI layout

Ayman Bagabas created

Change summary

internal/ui/model/editor.go | 41 ++++++++++++++++++++-
internal/ui/model/ui.go     | 76 ++++++++++++++++++++++++--------------
2 files changed, 87 insertions(+), 30 deletions(-)

Detailed changes

internal/ui/model/editor.go 🔗

@@ -15,6 +15,11 @@ type EditorKeyMap struct {
 	SendMessage key.Binding
 	OpenEditor  key.Binding
 	Newline     key.Binding
+
+	// Attachments key maps
+	AttachmentDeleteMode key.Binding
+	Escape               key.Binding
+	DeleteAllAttachments key.Binding
 }
 
 func DefaultEditorKeyMap() EditorKeyMap {
@@ -38,6 +43,18 @@ func DefaultEditorKeyMap() EditorKeyMap {
 			// to reflect that.
 			key.WithHelp("ctrl+j", "newline"),
 		),
+		AttachmentDeleteMode: key.NewBinding(
+			key.WithKeys("ctrl+r"),
+			key.WithHelp("ctrl+r+{i}", "delete attachment at index i"),
+		),
+		Escape: key.NewBinding(
+			key.WithKeys("esc", "alt+esc"),
+			key.WithHelp("esc", "cancel delete mode"),
+		),
+		DeleteAllAttachments: key.NewBinding(
+			key.WithKeys("r"),
+			key.WithHelp("ctrl+r+r", "delete all attachments"),
+		),
 	}
 }
 
@@ -49,6 +66,8 @@ type EditorModel struct {
 	keyMap   EditorKeyMap
 	textarea *textarea.Model
 
+	attachments []any // TODO: Implement attachments
+
 	readyPlaceholder   string
 	workingPlaceholder string
 }
@@ -110,12 +129,30 @@ func (m *EditorModel) View() string {
 
 // ShortHelp returns the short help view for the editor model.
 func (m *EditorModel) ShortHelp() []key.Binding {
-	return nil
+	k := m.keyMap
+	binds := []key.Binding{
+		k.AddFile,
+		k.SendMessage,
+		k.OpenEditor,
+		k.Newline,
+	}
+
+	if len(m.attachments) > 0 {
+		binds = append(binds,
+			k.AttachmentDeleteMode,
+			k.DeleteAllAttachments,
+			k.Escape,
+		)
+	}
+
+	return binds
 }
 
 // FullHelp returns the full help view for the editor model.
 func (m *EditorModel) FullHelp() [][]key.Binding {
-	return nil
+	return [][]key.Binding{
+		m.ShortHelp(),
+	}
 }
 
 // Cursor returns the relative cursor position of the editor.

internal/ui/model/ui.go 🔗

@@ -59,6 +59,7 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	case tea.WindowSizeMsg:
 		m.updateLayout(msg.Width, msg.Height)
 		m.editor.SetSize(m.layout.editor.Dx(), m.layout.editor.Dy())
+		m.help.Width = m.layout.help.Dx()
 	case tea.KeyPressMsg:
 		if m.dialog.HasDialogs() {
 			m.updateDialogs(msg, &cmds)
@@ -75,6 +76,7 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			case key.Matches(msg, m.keyMap.Help):
 				m.showFullHelp = !m.showFullHelp
 				m.help.ShowAll = m.showFullHelp
+				m.updateLayout(m.layout.area.Dx(), m.layout.area.Dy())
 			case key.Matches(msg, m.keyMap.Quit):
 				if !m.dialog.ContainsDialog(dialog.QuitDialogID) {
 					m.dialog.AddDialog(dialog.NewQuit(m.com))
@@ -130,27 +132,28 @@ func (m *UI) View() tea.View {
 		v.Cursor = cur
 	}
 
-	layers = append(layers, lipgloss.NewLayer(
-		lipgloss.NewStyle().Width(chatRect.Dx()).
-			Height(chatRect.Dy()).
-			Background(lipgloss.ANSIColor(rand.Intn(256))).
-			Render(" Main View "),
-	).X(chatRect.Min.X).Y(chatRect.Min.Y),
-		lipgloss.NewLayer(
-			lipgloss.NewStyle().Width(sideRect.Dx()).
-				Height(sideRect.Dy()).
-				Background(lipgloss.ANSIColor(rand.Intn(256))).
-				Render(" Side View "),
-		).X(sideRect.Min.X).Y(sideRect.Min.Y),
-		lipgloss.NewLayer(m.editor.View()).
-			X(editRect.Min.X).Y(editRect.Min.Y),
-		lipgloss.NewLayer(
-			lipgloss.NewStyle().Width(helpRect.Dx()).
-				Height(helpRect.Dy()).
-				Background(lipgloss.ANSIColor(rand.Intn(256))).
-				Render(m.help.View(helpKeyMap)),
-		).X(helpRect.Min.X).Y(helpRect.Min.Y),
-	)
+	mainLayer := lipgloss.NewLayer("").X(area.Min.X).Y(area.Min.Y).
+		Width(area.Dx()).Height(area.Dy()).
+		AddLayers(
+			lipgloss.NewLayer(
+				lipgloss.NewStyle().Width(chatRect.Dx()).
+					Height(chatRect.Dy()).
+					Background(lipgloss.ANSIColor(rand.Intn(256))).
+					Render(" Main View "),
+			).X(chatRect.Min.X).Y(chatRect.Min.Y),
+			lipgloss.NewLayer(
+				lipgloss.NewStyle().Width(sideRect.Dx()).
+					Height(sideRect.Dy()).
+					Background(lipgloss.ANSIColor(rand.Intn(256))).
+					Render(" Side View "),
+			).X(sideRect.Min.X).Y(sideRect.Min.Y),
+			lipgloss.NewLayer(m.editor.View()).
+				X(editRect.Min.X).Y(editRect.Min.Y),
+			lipgloss.NewLayer(m.help.View(helpKeyMap)).
+				X(helpRect.Min.X).Y(helpRect.Min.Y),
+		)
+
+	layers = append(layers, mainLayer)
 
 	v.Layer = lipgloss.NewCanvas(layers...)
 
@@ -209,23 +212,37 @@ func (m *UI) updateEditor(msg tea.KeyPressMsg, cmds *[]tea.Cmd) {
 // height given in cells.
 func (m *UI) updateLayout(w, h int) {
 	// The screen area we're working with
-	area := image.Rect(1, 1, w-1, h-1) // -1 for margins
+	area := image.Rect(0, 0, w, h)
 	helpKeyMap := m.focusedKeyMap()
 	helpHeight := 1
+	if m.dialog.HasDialogs() && len(m.dialog.FullHelp()) > 0 && len(m.dialog.ShortHelp()) > 0 {
+		helpKeyMap = m.dialog
+	}
 	if m.showFullHelp {
-		helpHeight = max(1, len(helpKeyMap.FullHelp()))
+		for _, row := range helpKeyMap.FullHelp() {
+			helpHeight = max(helpHeight, len(row))
+		}
 	}
 
-	chatRect, sideRect := uv.SplitHorizontal(area, uv.Fixed(area.Dx()-40))
-	chatRect, editRect := uv.SplitVertical(chatRect, uv.Fixed(area.Dy()-5-helpHeight))
-	// Add 1 line margin bottom of mainRect
+	// Add app margins
+	mainRect := area
+	mainRect.Min.X += 1
+	mainRect.Min.Y += 1
+	mainRect.Max.X -= 1
+	mainRect.Max.Y -= 1
+
+	mainRect, helpRect := uv.SplitVertical(mainRect, uv.Fixed(mainRect.Dy()-helpHeight))
+	chatRect, sideRect := uv.SplitHorizontal(mainRect, uv.Fixed(mainRect.Dx()-40))
+	chatRect, editRect := uv.SplitVertical(chatRect, uv.Fixed(mainRect.Dy()-5))
+
+	// Add 1 line margin bottom of chatRect
 	chatRect, _ = uv.SplitVertical(chatRect, uv.Fixed(chatRect.Dy()-1))
-	editRect, helpRect := uv.SplitVertical(editRect, uv.Fixed(5))
-	// Add 1 line margin bottom of footRect
+	// Add 1 line margin bottom of editRect
 	editRect, _ = uv.SplitVertical(editRect, uv.Fixed(editRect.Dy()-1))
 
 	m.layout = layout{
 		area:    area,
+		main:    mainRect,
 		chat:    chatRect,
 		editor:  editRect,
 		sidebar: sideRect,
@@ -238,6 +255,9 @@ type layout struct {
 	// area is the overall available area.
 	area uv.Rectangle
 
+	// main is the main area excluding help.
+	main uv.Rectangle
+
 	// chat is the area for the chat pane.
 	chat uv.Rectangle