-
Notifications
You must be signed in to change notification settings - Fork 51
Expand file tree
/
Copy pathtypes.go
More file actions
423 lines (373 loc) · 11.1 KB
/
types.go
File metadata and controls
423 lines (373 loc) · 11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
// Copyright 2026 Phillip Cloud
// Licensed under the Apache License, Version 2.0
package app
import (
"fmt"
"time"
"github.com/charmbracelet/bubbles/table"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/huh"
"github.com/cpcloud/micasa/internal/extract"
"github.com/cpcloud/micasa/internal/llm"
)
type Mode int
const (
modeNormal Mode = iota
modeEdit
modeForm
)
type FormKind int
const (
formNone FormKind = iota
formHouse
formProject
formQuote
formMaintenance
formAppliance
formIncident
formServiceLog
formVendor
formDocument
)
// confirmKind represents mutually exclusive confirmation dialog states.
// Only one confirmation can be active at a time, making illegal states
// unrepresentable.
type confirmKind int
const (
confirmNone confirmKind = iota
confirmHardDelete // permanent incident deletion (y/n)
confirmFormDiscard // discard dirty form changes, stay in app
confirmFormQuitDiscard // discard dirty form changes and quit
)
// isFormConfirm reports whether the confirmation is a form-related dialog.
func (k confirmKind) isFormConfirm() bool {
return k == confirmFormDiscard || k == confirmFormQuitDiscard
}
// formData is implemented by every form data struct, binding it to a specific
// FormKind at compile time. This replaces the old `any` field and eliminates
// the risk of formKind/formData type mismatches.
type formData interface {
formKind() FormKind
}
type formState struct {
form *huh.Form
formData formData
formSnapshot formData
formDirty bool
formHasRequired bool
pendingFormInit tea.Cmd
editID *uint
notesEditMode bool
notesFieldPtr *string
pendingEditor *editorState
}
// formKind returns the FormKind of the current form data, or formNone when no
// form is active. Derived from formData so mismatch is impossible.
func (fs *formState) formKind() FormKind {
if fs.formData == nil {
return formNone
}
return fs.formData.formKind()
}
type extractState struct {
// Extraction-specific LLM connection settings. When extractionProvider
// differs from the chat provider, an independent client is created.
extractionProvider string
extractionBaseURL string
extractionModel string
extractionAPIKey string
extractionTimeout time.Duration
extractionThinking string
extractionEnabled bool
ocrTSV bool
ocrConfThreshold int
extractionClient *llm.Client
extractors []extract.Extractor
extractionReady bool
llmInferenceTimeout time.Duration
pendingExtractionDocID *uint
extraction *extractionLogState
bgExtractions []*extractionLogState
}
type TabKind int
const (
tabProjects TabKind = iota
tabQuotes
tabMaintenance
tabIncidents
tabAppliances
tabVendors
tabDocuments
)
func (k TabKind) String() string {
switch k {
case tabProjects:
return "Projects"
case tabQuotes:
return "Quotes"
case tabMaintenance:
return "Maintenance"
case tabIncidents:
return "Incidents"
case tabAppliances:
return "Appliances"
case tabVendors:
return "Vendors"
case tabDocuments:
return "Docs"
}
panic(fmt.Sprintf("unhandled TabKind: %d", k))
}
// singular returns the lowercase singular noun for a tab kind, used in
// context-aware empty-state messages for detail drilldowns.
func (k TabKind) singular() string {
switch k {
case tabProjects:
return "project"
case tabQuotes:
return "quote"
case tabMaintenance:
return "maintenance item"
case tabIncidents:
return "incident"
case tabAppliances:
return "appliance"
case tabVendors:
return "vendor"
case tabDocuments:
return "doc"
}
panic(fmt.Sprintf("unhandled TabKind: %d", k))
}
// plural returns the lowercase plural noun for a tab kind.
func (k TabKind) plural() string {
switch k {
case tabProjects:
return "projects"
case tabQuotes:
return "quotes"
case tabMaintenance:
return "maintenance items"
case tabIncidents:
return "incidents"
case tabAppliances:
return "appliances"
case tabVendors:
return "vendors"
case tabDocuments:
return "documents"
}
panic(fmt.Sprintf("unhandled TabKind: %d", k))
}
type rowMeta struct {
ID uint
Deleted bool
Dimmed bool // true in pin preview mode for non-matching rows
}
type sortDir int
const (
sortAsc sortDir = iota
sortDesc
)
type sortEntry struct {
Col int
Dir sortDir
}
// filterPin holds the set of pinned values for a single column.
// Multiple values in the same column use OR (IN) semantics.
type filterPin struct {
Col int // index in tab.Specs
Values map[string]bool // lowercased pinned values
}
type Tab struct {
Kind TabKind
Name string
Handler TabHandler
Table table.Model
Rows []rowMeta
Specs []columnSpec
CellRows [][]cell
ColCursor int
ViewOffset int // first visible column in horizontal scroll viewport
LastDeleted *uint
ShowDeleted bool
Sorts []sortEntry
Stale bool // true when data may be outdated; cleared on reload
// Pin-and-filter state.
Pins []filterPin // active pins; AND across columns, OR within
FilterActive bool // true = non-matching rows hidden; false = preview only
FilterInverted bool // true = show rows that DON'T match instead of rows that do
// Full data (pre-row-filter). Populated by reloadTab after project status
// filtering. Row filter operates on these without hitting the DB.
FullRows []table.Row
FullMeta []rowMeta
FullCellRows [][]cell
// cachedVP holds the last computed tableViewport, populated during View()
// and reused by mouse click handlers to avoid O(rows*cols) recomputation.
// Nil when stale; call Model.tabViewport to get-or-compute.
cachedVP *tableViewport
}
type statusKind int
const (
statusInfo statusKind = iota
statusError
)
type statusMsg struct {
Text string
Kind statusKind
}
// detailContext holds state for a drill-down sub-table (e.g. service log for
// a maintenance item). When non-nil on the Model, the detail tab replaces the
// main tab for all interaction.
type detailContext struct {
ParentTabIndex int
ParentRowID uint
Breadcrumb string
Tab Tab
}
type Options struct {
DBPath string
ConfigPath string
FilePickerDir string // starting directory for document file picker
LLMConfig *llmConfig // nil if LLM is not configured
ExtractionConfig extractionConfig
}
// llmConfig holds resolved LLM settings passed from main after loading the
// TOML config. Kept as a separate type so the app package doesn't import
// config directly.
type llmConfig struct {
Provider string
BaseURL string
Model string
APIKey string //nolint:gosec // G101 false positive: field name triggers heuristic, not a hardcoded credential
ExtraContext string
Timeout time.Duration
Thinking string // reasoning effort: none|low|medium|high|auto
}
// extractionConfig holds resolved extraction pipeline settings.
type extractionConfig struct {
// LLM connection settings for extraction. When Provider is non-empty,
// the extraction pipeline creates its own LLM client independent of
// the chat client. When empty, falls back to the chat client.
Provider string
BaseURL string
Model string
APIKey string //nolint:gosec // G117 false positive: field name, not a hardcoded credential
Timeout time.Duration
Thinking string // reasoning effort level
LLMInferenceTimeout time.Duration
Extractors []extract.Extractor // configured extractors; nil = defaults
Enabled bool // LLM extraction enabled
OCRTSV bool // send spatial layout annotations to LLM
OCRConfThreshold int // confidence threshold for spatial annotations
}
// SetExtraction configures the extraction pipeline on the Options.
func (o *Options) SetExtraction(
provider, baseURL, model, apiKey string,
timeout time.Duration,
thinking string,
extractors []extract.Extractor,
enabled bool,
llmInferenceTimeout time.Duration,
ocrTSV bool,
ocrConfThreshold int,
) {
o.ExtractionConfig = extractionConfig{
Provider: provider,
BaseURL: baseURL,
Model: model,
APIKey: apiKey,
Timeout: timeout,
Thinking: thinking,
LLMInferenceTimeout: llmInferenceTimeout,
Extractors: extractors,
Enabled: enabled,
OCRTSV: ocrTSV,
OCRConfThreshold: ocrConfThreshold,
}
}
// SetLLM configures the LLM backend on the Options. Pass empty strings to
// disable the LLM feature.
func (o *Options) SetLLM(
provider, baseURL, model, apiKey, extraContext string,
timeout time.Duration,
thinking string,
) {
if model == "" {
o.LLMConfig = nil
return
}
o.LLMConfig = &llmConfig{
Provider: provider,
BaseURL: baseURL,
Model: model,
APIKey: apiKey,
ExtraContext: extraContext,
Timeout: timeout,
Thinking: thinking,
}
}
type alignKind int
const (
alignLeft alignKind = iota
alignRight
)
type cellKind int
const (
cellText cellKind = iota
cellMoney
cellReadonly
cellDate
cellStatus
cellDrilldown // interactive count that opens a detail view
cellWarranty // date with green/red coloring based on expiry
cellUrgency // date colored by proximity (green -> yellow -> red)
cellNotes // text that can be expanded in a read-only overlay
cellEntity // entity ref with colored kind-letter prefix
)
type cell struct {
Value string
Kind cellKind
Null bool // true when the database value is NULL (not just empty)
LinkID uint // FK target ID for cross-tab navigation; 0 = no link
}
// nullPinKey is the internal key used by the pin/filter system to represent
// NULL cells. It cannot collide with any real display value.
const nullPinKey = "\x00null"
// columnLink describes a foreign-key relationship to another tab.
type columnLink struct {
TargetTab TabKind
}
type columnSpec struct {
Title string
Min int
Max int
Flex bool
Align alignKind
Kind cellKind
Link *columnLink // non-nil if this column references another tab
FixedValues []string // all possible values; used to stabilize column width
HideOrder int // 0 = visible; >0 = hidden (higher = more recently hidden)
}
// inlineInputState holds state for a single-field text edit rendered in the
// status bar, keeping the table visible. Used instead of a full form overlay
// for simple text/number fields.
type inlineInputState struct {
Input textinput.Model
Title string
EditID uint
FormData formData
FieldPtr *string // pointer into FormData
Validate func(string) error // nil = no validation
}
// editorState holds context for an in-flight $EDITOR session so we can
// restore the textarea with the edited content when the editor exits.
type editorState struct {
EditID uint
FormData formData
FieldPtr *string // pointer into FormData for the notes field
TempFile string // path to the temp file passed to the editor
}
// editorFinishedMsg is sent when an external $EDITOR process exits.
type editorFinishedMsg struct{ Err error }