apps/jotts/tui/view.go 4.2 K raw
1
package tui
2
3
import (
4
	"fmt"
5
6
	tea "charm.land/bubbletea/v2"
7
	"charm.land/lipgloss/v2"
8
	sharedtui "github.com/stevedylandev/andromeda/pkg/tui"
9
)
10
11
var (
12
	borderStyle    = sharedtui.Border(lipgloss.NormalBorder())
13
	borderActive   = sharedtui.BorderActive(lipgloss.NormalBorder())
14
	titleStyle     = sharedtui.TitleStyle
15
	statusOKStyle  = sharedtui.StatusOKStyle
16
	statusErrStyle = sharedtui.StatusErrStyle
17
	hintStyle      = sharedtui.HintStyle
18
	modalStyle       = sharedtui.ModalStyle
19
	statusModalStyle = sharedtui.StatusModalStyle
20
)
21
22
func (m Model) View() tea.View {
23
	listW, contentW := splitWidths(m.width)
24
	bodyH := splitBodyHeight(m.height - 1)
25
26
	left := m.renderListPane(listW, bodyH)
27
	right := m.renderRightPane(contentW, bodyH)
28
29
	body := lipgloss.JoinHorizontal(lipgloss.Top, left, right)
30
	footer := m.renderFooter()
31
	base := lipgloss.JoinVertical(lipgloss.Left, body, footer)
32
33
	var overlays []*lipgloss.Layer
34
	if m.showHelp {
35
		overlays = append(overlays, centerLayer(m.width, m.height,
36
			modalStyle.Render(m.help.FullHelpView(m.keys.FullHelp())), 1))
37
	}
38
	if m.confirmDelete {
39
		title := ""
40
		if n, ok := m.list.Selected(); ok {
41
			title = n.Title
42
		}
43
		overlays = append(overlays, centerLayer(m.width, m.height,
44
			modalStyle.Render(fmt.Sprintf("Delete %q?\n\ny / n", title)), 2))
45
	}
46
	if m.status != "" {
47
		st := statusOKStyle
48
		if !m.statusOK {
49
			st = statusErrStyle
50
		}
51
		overlays = append(overlays, bottomCenterLayer(m.width, m.height,
52
			statusModalStyle.Render(st.Render(m.status)), 3))
53
	}
54
55
	content := base
56
	if len(overlays) > 0 {
57
		layers := append([]*lipgloss.Layer{lipgloss.NewLayer(base)}, overlays...)
58
		canvas := lipgloss.NewCanvas(m.width, m.height)
59
		canvas.Compose(lipgloss.NewCompositor(layers...))
60
		content = canvas.Render()
61
	}
62
63
	return tea.View{Content: content, AltScreen: true}
64
}
65
66
func (m Model) renderListPane(w, h int) string {
67
	style := borderStyle
68
	if m.state == stateList {
69
		style = borderActive
70
	}
71
	return style.Width(w).Height(h).Render(m.list.View())
72
}
73
74
func (m Model) renderRightPane(w, h int) string {
75
	if m.state == stateForm {
76
		return m.renderForm(w, h)
77
	}
78
	return m.renderContent(w, h)
79
}
80
81
func (m Model) renderContent(w, h int) string {
82
	style := borderStyle
83
	if m.state == stateContent {
84
		style = borderActive
85
	}
86
	header := "preview"
87
	if t := m.cont.Title(); t != "" {
88
		header = t
89
	}
90
	inner := lipgloss.JoinVertical(lipgloss.Left, titleStyle.Render(header), m.cont.View())
91
	return style.Width(w).Height(h).Render(inner)
92
}
93
94
func (m Model) renderForm(w, h int) string {
95
	header := "new note"
96
	if !m.form.IsCreate() {
97
		header = "edit"
98
	}
99
100
	titleField := m.form.title.View()
101
	if m.form.ActiveField() == formFieldTitle {
102
		titleField = borderActive.Render(titleField)
103
	} else {
104
		titleField = borderStyle.Render(titleField)
105
	}
106
107
	body := m.form.content.View()
108
	if m.form.ActiveField() == formFieldContent {
109
		body = borderActive.Render(body)
110
	} else {
111
		body = borderStyle.Render(body)
112
	}
113
114
	inner := lipgloss.JoinVertical(lipgloss.Left, titleStyle.Render(header), titleField, body)
115
	return borderActive.Width(w).Height(h).Render(inner)
116
}
117
118
func (m Model) renderFooter() string {
119
	mode := "local"
120
	if m.isRemote {
121
		mode = "remote " + m.backend.RemoteURL()
122
	}
123
	help := m.help.ShortHelpView(m.keys.ShortHelp())
124
	return hintStyle.Render(fmt.Sprintf("[%s] %s", mode, help))
125
}
126
127
func centerLayer(w, h int, content string, z int) *lipgloss.Layer {
128
	cw, ch := lipgloss.Width(content), lipgloss.Height(content)
129
	x := (w - cw) / 2
130
	y := (h - ch) / 2
131
	if x < 0 {
132
		x = 0
133
	}
134
	if y < 0 {
135
		y = 0
136
	}
137
	return lipgloss.NewLayer(content).X(x).Y(y).Z(z)
138
}
139
140
func bottomCenterLayer(w, h int, content string, z int) *lipgloss.Layer {
141
	cw, ch := lipgloss.Width(content), lipgloss.Height(content)
142
	x := (w - cw) / 2
143
	y := h - ch - 1
144
	if x < 0 {
145
		x = 0
146
	}
147
	if y < 0 {
148
		y = 0
149
	}
150
	return lipgloss.NewLayer(content).X(x).Y(y).Z(z)
151
}
152
153
func splitWidths(total int) (int, int) {
154
	if total < 44 {
155
		return total / 2, total - (total / 2)
156
	}
157
	list := total / 4
158
	if list < 24 {
159
		list = 24
160
	}
161
	if total-list < 20 {
162
		list = total - 20
163
	}
164
	if list < 1 {
165
		list = 1
166
	}
167
	return list, total - list
168
}
169
170
func splitBodyHeight(total int) int {
171
	if total < 3 {
172
		return 3
173
	}
174
	return total
175
}
176
177
func paneFrameWidth() int  { return borderStyle.GetHorizontalFrameSize() }
178
func paneFrameHeight() int { return borderStyle.GetVerticalFrameSize() }