From a6b11c82af6cfd223b662da5425c4cc86cc63afa Mon Sep 17 00:00:00 2001 From: Jahvon Dockery Date: Wed, 13 Aug 2025 19:20:48 -0400 Subject: [PATCH 1/3] refactor!: fix text fmt printing and rename funcs --- io/logger.go | 20 +++--- io/mocks/mock_logger.go | 148 ++++++++++++++++++++-------------------- io/output.go | 12 +--- io/output_test.go | 16 ++--- io/types.go | 14 ++-- 5 files changed, 101 insertions(+), 109 deletions(-) diff --git a/io/logger.go b/io/logger.go index 36ae263..1d5b5a5 100644 --- a/io/logger.go +++ b/io/logger.go @@ -225,14 +225,14 @@ func (l *StandardLogger) Debugf(msg string, args ...any) { } } -func (l *StandardLogger) Error(err error, msg string) { +func (l *StandardLogger) WrapError(err error, msg string) { if msg == "" { - l.Errorf(err.Error()) + l.Error(err.Error()) return } else if l.mode == Hidden { return } - l.Errorx(err.Error(), "err", err) + l.Error(err.Error(), "err", err) } func (l *StandardLogger) Errorf(msg string, args ...any) { @@ -288,7 +288,7 @@ func (l *StandardLogger) Fatalf(msg string, args ...any) { } } -func (l *StandardLogger) Infox(msg string, kv ...any) { +func (l *StandardLogger) Info(msg string, kv ...any) { l.syncLoggerFormat() if l.mode == Hidden { return @@ -299,7 +299,7 @@ func (l *StandardLogger) Infox(msg string, kv ...any) { } } -func (l *StandardLogger) Noticex(msg string, kv ...any) { +func (l *StandardLogger) Notice(msg string, kv ...any) { if l.mode == Hidden { return } @@ -310,7 +310,7 @@ func (l *StandardLogger) Noticex(msg string, kv ...any) { } } -func (l *StandardLogger) Debugx(msg string, kv ...any) { +func (l *StandardLogger) Debug(msg string, kv ...any) { if l.mode == Hidden { return } @@ -321,7 +321,7 @@ func (l *StandardLogger) Debugx(msg string, kv ...any) { } } -func (l *StandardLogger) Errorx(msg string, kv ...any) { +func (l *StandardLogger) Error(msg string, kv ...any) { if l.mode == Hidden { return } @@ -332,7 +332,7 @@ func (l *StandardLogger) Errorx(msg string, kv ...any) { } } -func (l *StandardLogger) Warnx(msg string, kv ...any) { +func (l *StandardLogger) Warn(msg string, kv ...any) { if l.mode == Hidden { return } @@ -343,7 +343,7 @@ func (l *StandardLogger) Warnx(msg string, kv ...any) { } } -func (l *StandardLogger) Fatalx(msg string, kv ...any) { +func (l *StandardLogger) Fatal(msg string, kv ...any) { l.syncLoggerFormat() if l.archiveHandler != nil { l.archiveHandler.Error(msg, kv...) @@ -438,6 +438,6 @@ func (l *StandardLogger) syncLoggerFormat() { } } -func defaultExit(_ string, args ...any) { +func defaultExit(_ string, _ ...any) { os.Exit(1) } diff --git a/io/mocks/mock_logger.go b/io/mocks/mock_logger.go index 2a7cbed..944dd8e 100644 --- a/io/mocks/mock_logger.go +++ b/io/mocks/mock_logger.go @@ -39,50 +39,55 @@ func (m *MockLogger) EXPECT() *MockLoggerMockRecorder { return m.recorder } -// Debugf mocks base method. -func (m *MockLogger) Debugf(arg0 string, arg1 ...any) { +// Debug mocks base method. +func (m *MockLogger) Debug(arg0 string, arg1 ...any) { m.ctrl.T.Helper() varargs := []any{arg0} for _, a := range arg1 { varargs = append(varargs, a) } - m.ctrl.Call(m, "Debugf", varargs...) + m.ctrl.Call(m, "Debug", varargs...) } -// Debugf indicates an expected call of Debugf. -func (mr *MockLoggerMockRecorder) Debugf(arg0 any, arg1 ...any) *gomock.Call { +// Debug indicates an expected call of Debug. +func (mr *MockLoggerMockRecorder) Debug(arg0 any, arg1 ...any) *gomock.Call { mr.mock.ctrl.T.Helper() varargs := append([]any{arg0}, arg1...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*MockLogger)(nil).Debugf), varargs...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), varargs...) } -// Debugx mocks base method. -func (m *MockLogger) Debugx(arg0 string, arg1 ...any) { +// Debugf mocks base method. +func (m *MockLogger) Debugf(arg0 string, arg1 ...any) { m.ctrl.T.Helper() varargs := []any{arg0} for _, a := range arg1 { varargs = append(varargs, a) } - m.ctrl.Call(m, "Debugx", varargs...) + m.ctrl.Call(m, "Debugf", varargs...) } -// Debugx indicates an expected call of Debugx. -func (mr *MockLoggerMockRecorder) Debugx(arg0 any, arg1 ...any) *gomock.Call { +// Debugf indicates an expected call of Debugf. +func (mr *MockLoggerMockRecorder) Debugf(arg0 any, arg1 ...any) *gomock.Call { mr.mock.ctrl.T.Helper() varargs := append([]any{arg0}, arg1...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugx", reflect.TypeOf((*MockLogger)(nil).Debugx), varargs...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*MockLogger)(nil).Debugf), varargs...) } // Error mocks base method. -func (m *MockLogger) Error(arg0 error, arg1 string) { +func (m *MockLogger) Error(arg0 string, arg1 ...any) { m.ctrl.T.Helper() - m.ctrl.Call(m, "Error", arg0, arg1) + varargs := []any{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Error", varargs...) } // Error indicates an expected call of Error. -func (mr *MockLoggerMockRecorder) Error(arg0, arg1 any) *gomock.Call { +func (mr *MockLoggerMockRecorder) Error(arg0 any, arg1 ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockLogger)(nil).Error), arg0, arg1) + varargs := append([]any{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockLogger)(nil).Error), varargs...) } // Errorf mocks base method. @@ -102,21 +107,21 @@ func (mr *MockLoggerMockRecorder) Errorf(arg0 any, arg1 ...any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Errorf", reflect.TypeOf((*MockLogger)(nil).Errorf), varargs...) } -// Errorx mocks base method. -func (m *MockLogger) Errorx(arg0 string, arg1 ...any) { +// Fatal mocks base method. +func (m *MockLogger) Fatal(arg0 string, arg1 ...any) { m.ctrl.T.Helper() varargs := []any{arg0} for _, a := range arg1 { varargs = append(varargs, a) } - m.ctrl.Call(m, "Errorx", varargs...) + m.ctrl.Call(m, "Fatal", varargs...) } -// Errorx indicates an expected call of Errorx. -func (mr *MockLoggerMockRecorder) Errorx(arg0 any, arg1 ...any) *gomock.Call { +// Fatal indicates an expected call of Fatal. +func (mr *MockLoggerMockRecorder) Fatal(arg0 any, arg1 ...any) *gomock.Call { mr.mock.ctrl.T.Helper() varargs := append([]any{arg0}, arg1...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Errorx", reflect.TypeOf((*MockLogger)(nil).Errorx), varargs...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fatal", reflect.TypeOf((*MockLogger)(nil).Fatal), varargs...) } // FatalErr mocks base method. @@ -148,23 +153,6 @@ func (mr *MockLoggerMockRecorder) Fatalf(arg0 any, arg1 ...any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fatalf", reflect.TypeOf((*MockLogger)(nil).Fatalf), varargs...) } -// Fatalx mocks base method. -func (m *MockLogger) Fatalx(arg0 string, arg1 ...any) { - m.ctrl.T.Helper() - varargs := []any{arg0} - for _, a := range arg1 { - varargs = append(varargs, a) - } - m.ctrl.Call(m, "Fatalx", varargs...) -} - -// Fatalx indicates an expected call of Fatalx. -func (mr *MockLoggerMockRecorder) Fatalx(arg0 any, arg1 ...any) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]any{arg0}, arg1...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fatalx", reflect.TypeOf((*MockLogger)(nil).Fatalx), varargs...) -} - // Flush mocks base method. func (m *MockLogger) Flush() error { m.ctrl.T.Helper() @@ -179,38 +167,38 @@ func (mr *MockLoggerMockRecorder) Flush() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Flush", reflect.TypeOf((*MockLogger)(nil).Flush)) } -// Infof mocks base method. -func (m *MockLogger) Infof(arg0 string, arg1 ...any) { +// Info mocks base method. +func (m *MockLogger) Info(arg0 string, arg1 ...any) { m.ctrl.T.Helper() varargs := []any{arg0} for _, a := range arg1 { varargs = append(varargs, a) } - m.ctrl.Call(m, "Infof", varargs...) + m.ctrl.Call(m, "Info", varargs...) } -// Infof indicates an expected call of Infof. -func (mr *MockLoggerMockRecorder) Infof(arg0 any, arg1 ...any) *gomock.Call { +// Info indicates an expected call of Info. +func (mr *MockLoggerMockRecorder) Info(arg0 any, arg1 ...any) *gomock.Call { mr.mock.ctrl.T.Helper() varargs := append([]any{arg0}, arg1...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Infof", reflect.TypeOf((*MockLogger)(nil).Infof), varargs...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockLogger)(nil).Info), varargs...) } -// Infox mocks base method. -func (m *MockLogger) Infox(arg0 string, arg1 ...any) { +// Infof mocks base method. +func (m *MockLogger) Infof(arg0 string, arg1 ...any) { m.ctrl.T.Helper() varargs := []any{arg0} for _, a := range arg1 { varargs = append(varargs, a) } - m.ctrl.Call(m, "Infox", varargs...) + m.ctrl.Call(m, "Infof", varargs...) } -// Infox indicates an expected call of Infox. -func (mr *MockLoggerMockRecorder) Infox(arg0 any, arg1 ...any) *gomock.Call { +// Infof indicates an expected call of Infof. +func (mr *MockLoggerMockRecorder) Infof(arg0 any, arg1 ...any) *gomock.Call { mr.mock.ctrl.T.Helper() varargs := append([]any{arg0}, arg1...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Infox", reflect.TypeOf((*MockLogger)(nil).Infox), varargs...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Infof", reflect.TypeOf((*MockLogger)(nil).Infof), varargs...) } // LogMode mocks base method. @@ -227,38 +215,38 @@ func (mr *MockLoggerMockRecorder) LogMode() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LogMode", reflect.TypeOf((*MockLogger)(nil).LogMode)) } -// Noticef mocks base method. -func (m *MockLogger) Noticef(arg0 string, arg1 ...any) { +// Notice mocks base method. +func (m *MockLogger) Notice(arg0 string, arg1 ...any) { m.ctrl.T.Helper() varargs := []any{arg0} for _, a := range arg1 { varargs = append(varargs, a) } - m.ctrl.Call(m, "Noticef", varargs...) + m.ctrl.Call(m, "Notice", varargs...) } -// Noticef indicates an expected call of Noticef. -func (mr *MockLoggerMockRecorder) Noticef(arg0 any, arg1 ...any) *gomock.Call { +// Notice indicates an expected call of Notice. +func (mr *MockLoggerMockRecorder) Notice(arg0 any, arg1 ...any) *gomock.Call { mr.mock.ctrl.T.Helper() varargs := append([]any{arg0}, arg1...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Noticef", reflect.TypeOf((*MockLogger)(nil).Noticef), varargs...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Notice", reflect.TypeOf((*MockLogger)(nil).Notice), varargs...) } -// Noticex mocks base method. -func (m *MockLogger) Noticex(arg0 string, arg1 ...any) { +// Noticef mocks base method. +func (m *MockLogger) Noticef(arg0 string, arg1 ...any) { m.ctrl.T.Helper() varargs := []any{arg0} for _, a := range arg1 { varargs = append(varargs, a) } - m.ctrl.Call(m, "Noticex", varargs...) + m.ctrl.Call(m, "Noticef", varargs...) } -// Noticex indicates an expected call of Noticex. -func (mr *MockLoggerMockRecorder) Noticex(arg0 any, arg1 ...any) *gomock.Call { +// Noticef indicates an expected call of Noticef. +func (mr *MockLoggerMockRecorder) Noticef(arg0 any, arg1 ...any) *gomock.Call { mr.mock.ctrl.T.Helper() varargs := append([]any{arg0}, arg1...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Noticex", reflect.TypeOf((*MockLogger)(nil).Noticex), varargs...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Noticef", reflect.TypeOf((*MockLogger)(nil).Noticef), varargs...) } // PlainTextDebug mocks base method. @@ -381,6 +369,23 @@ func (mr *MockLoggerMockRecorder) SetMode(arg0 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetMode", reflect.TypeOf((*MockLogger)(nil).SetMode), arg0) } +// Warn mocks base method. +func (m *MockLogger) Warn(arg0 string, arg1 ...any) { + m.ctrl.T.Helper() + varargs := []any{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Warn", varargs...) +} + +// Warn indicates an expected call of Warn. +func (mr *MockLoggerMockRecorder) Warn(arg0 any, arg1 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warn", reflect.TypeOf((*MockLogger)(nil).Warn), varargs...) +} + // Warnf mocks base method. func (m *MockLogger) Warnf(arg0 string, arg1 ...any) { m.ctrl.T.Helper() @@ -398,19 +403,14 @@ func (mr *MockLoggerMockRecorder) Warnf(arg0 any, arg1 ...any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warnf", reflect.TypeOf((*MockLogger)(nil).Warnf), varargs...) } -// Warnx mocks base method. -func (m *MockLogger) Warnx(arg0 string, arg1 ...any) { +// WrapError mocks base method. +func (m *MockLogger) WrapError(arg0 error, arg1 string) { m.ctrl.T.Helper() - varargs := []any{arg0} - for _, a := range arg1 { - varargs = append(varargs, a) - } - m.ctrl.Call(m, "Warnx", varargs...) + m.ctrl.Call(m, "WrapError", arg0, arg1) } -// Warnx indicates an expected call of Warnx. -func (mr *MockLoggerMockRecorder) Warnx(arg0 any, arg1 ...any) *gomock.Call { +// WrapError indicates an expected call of WrapError. +func (mr *MockLoggerMockRecorder) WrapError(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]any{arg0}, arg1...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warnx", reflect.TypeOf((*MockLogger)(nil).Warnx), varargs...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WrapError", reflect.TypeOf((*MockLogger)(nil).WrapError), arg0, arg1) } diff --git a/io/output.go b/io/output.go index 5495597..28bd74c 100644 --- a/io/output.go +++ b/io/output.go @@ -38,11 +38,7 @@ func (w StdOutWriter) Write(p []byte) (n int, err error) { if strings.TrimSpace(line) == "" { continue } - if len(w.LogFields) > 0 { - w.Logger.Infox(line, w.LogFields...) - } else { - w.Logger.Infof(line) - } + w.Logger.Info(line, w.LogFields...) } default: return len(p), fmt.Errorf("unknown log mode %v", curMode) @@ -83,11 +79,7 @@ func (w StdErrWriter) Write(p []byte) (n int, err error) { if strings.TrimSpace(line) == "" { continue } - if len(w.LogFields) > 0 { - w.Logger.Noticex(line, w.LogFields...) - } else { - w.Logger.Noticef(line) - } + w.Logger.Notice(line, w.LogFields...) } default: return len(p), fmt.Errorf("unknown log mode %v", w.LogMode) diff --git a/io/output_test.go b/io/output_test.go index 4deca91..6dc968c 100644 --- a/io/output_test.go +++ b/io/output_test.go @@ -37,10 +37,10 @@ func TestStdOutWriter_WriteLogFmt(t *testing.T) { input := []byte("line 1\nline 2\nline 3\nline 4") mockLogger.EXPECT().LogMode().Return(io.Logfmt).AnyTimes() - mockLogger.EXPECT().Infox("line 1", fields...) - mockLogger.EXPECT().Infox("line 2", fields...) - mockLogger.EXPECT().Infox("line 3", fields...) - mockLogger.EXPECT().Infox("line 4", fields...) + mockLogger.EXPECT().Info("line 1", fields...) + mockLogger.EXPECT().Info("line 2", fields...) + mockLogger.EXPECT().Info("line 3", fields...) + mockLogger.EXPECT().Info("line 4", fields...) _, err := writer.Write(input) if err != nil { @@ -92,10 +92,10 @@ func TestStdErrWriter_WriteLogFmt(t *testing.T) { input := []byte("line 1\nline 2\nline 3\nline 4") mockLogger.EXPECT().LogMode().Return(io.Logfmt).AnyTimes() - mockLogger.EXPECT().Noticex("line 1", fields...) - mockLogger.EXPECT().Noticex("line 2", fields...) - mockLogger.EXPECT().Noticex("line 3", fields...) - mockLogger.EXPECT().Noticex("line 4", fields...) + mockLogger.EXPECT().Notice("line 1", fields...) + mockLogger.EXPECT().Notice("line 2", fields...) + mockLogger.EXPECT().Notice("line 3", fields...) + mockLogger.EXPECT().Notice("line 4", fields...) _, err := writer.Write(input) if err != nil { diff --git a/io/types.go b/io/types.go index 76dcba7..61e050a 100644 --- a/io/types.go +++ b/io/types.go @@ -37,17 +37,17 @@ type Logger interface { Infof(msg string, args ...any) Noticef(msg string, args ...any) Debugf(msg string, args ...any) - Error(err error, msg string) + WrapError(err error, msg string) Errorf(msg string, args ...any) Warnf(msg string, args ...any) Fatalf(msg string, args ...any) - Infox(msg string, kv ...any) - Noticex(msg string, kv ...any) - Debugx(msg string, kv ...any) - Errorx(msg string, kv ...any) - Warnx(msg string, kv ...any) - Fatalx(msg string, kv ...any) + Info(msg string, kv ...any) + Notice(msg string, kv ...any) + Debug(msg string, kv ...any) + Error(msg string, kv ...any) + Warn(msg string, kv ...any) + Fatal(msg string, kv ...any) Print(data string) Println(data string) From 0c4ecc6704b0b648ae0ba9ba959768fa3c8878d9 Mon Sep 17 00:00:00 2001 From: Jahvon Dockery Date: Wed, 13 Aug 2025 19:22:59 -0400 Subject: [PATCH 2/3] fix: return without err for piped form EOF --- views/form.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/form.go b/views/form.go index e3fdb6d..057749e 100644 --- a/views/form.go +++ b/views/form.go @@ -339,7 +339,7 @@ func readPipedInput(in *os.File, fields []*FormField) error { if err != nil && !errors.Is(err, io.EOF) { return fmt.Errorf("error reading input line: %w", err) } else if line == "" && errors.Is(err, io.EOF) { - return fmt.Errorf("not enough input lines") + return nil } if !field.Required && line == "" && field.Default != "" { line = field.Default From 920b56f3b4479510a4a7bb8a4dc0d85377529e3a Mon Sep 17 00:00:00 2001 From: Jahvon Dockery Date: Wed, 13 Aug 2025 20:19:02 -0400 Subject: [PATCH 3/3] feat: simple table view --- sample/main.go | 91 +++++++++++++ views/table.go | 358 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 449 insertions(+) create mode 100644 views/table.go diff --git a/sample/main.go b/sample/main.go index 179060b..fdf798e 100644 --- a/sample/main.go +++ b/sample/main.go @@ -9,6 +9,7 @@ import ( "github.com/flowexec/tuikit" sampleTypes "github.com/flowexec/tuikit/sample/types" + "github.com/flowexec/tuikit/themes" "github.com/flowexec/tuikit/types" "github.com/flowexec/tuikit/views" ) @@ -53,6 +54,96 @@ func main() { &types.EntityInfo{ID: "mark", Header: "Mark Twain", SubHeader: "American Author"}, ) view = views.NewCollectionView(container.RenderState(), c, types.CollectionFormatList, nil) + case "table": + columns := []views.TableColumn{ + {Title: "Workspace", Percentage: 40}, + {Title: "Description", Percentage: 35}, + {Title: "Status", Percentage: 25}, + } + + rows := []views.TableRow{ + { + Data: []string{"flow-workspace", "Main development workspace", "Active"}, + Children: []views.TableRow{ + {Data: []string{"docs", "", "5 exec"}}, + {Data: []string{"api", "", "12 exec"}}, + {Data: []string{"frontend", "", "8 exec"}}, + }, + }, + { + Data: []string{"home-lab", "Infrastructure automation", "Inactive"}, + Children: []views.TableRow{ + {Data: []string{"k8s", "", "15 exec"}}, + {Data: []string{"monitoring", "", "6 exec"}}, + }, + }, + { + Data: []string{"personal-tools", "Personal utility scripts", "Active"}, + Children: []views.TableRow{}, + }, + } + + table := views.NewTable(container.RenderState(), columns, rows, views.TableDisplayFull) + + table.SetOnSelect(func(index int) error { + selectedRow := table.GetSelectedRow() + if selectedRow != nil { + container.SetNotice(fmt.Sprintf("Selected: %s", selectedRow.Data()[0]), themes.OutputLevelInfo) + } + return nil + }) + + table.SetOnHover(func(index int) { + selectedRow := table.GetSelectedRow() + if selectedRow != nil { + container.SetState("Current", selectedRow.Data()[0]) + } + }) + + view = views.NewFrameView(table) + case "table-mini": + columns := []views.TableColumn{{Title: "Available Executables", Percentage: 100}} + + rows := []views.TableRow{ + {Data: []string{"build app"}}, + {Data: []string{"test unit"}}, + {Data: []string{"deploy staging"}}, + {Data: []string{"deploy production"}}, + {Data: []string{"clean artifacts"}}, + } + + table := views.NewTable(container.RenderState(), columns, rows, views.TableDisplayMini) + + table.SetOnSelect(func(index int) error { + selectedRow := table.GetSelectedRow() + if selectedRow != nil { + container.SetNotice(fmt.Sprintf("Executing: %s", selectedRow.Data()[0]), themes.OutputLevelInfo) + } + return nil + }) + + view = views.NewFrameView(table) + case "table-mini-multi": + columns := []views.TableColumn{{Title: "Template", Percentage: 60}, {Title: "Type", Percentage: 40}} + + rows := []views.TableRow{ + {Data: []string{"k8s-deployment", "Kubernetes"}}, + {Data: []string{"react-app", "Frontend"}}, + {Data: []string{"go-service", "Backend"}}, + {Data: []string{"terraform-module", "Infrastructure"}}, + } + + table := views.NewTable(container.RenderState(), columns, rows, views.TableDisplayMini) + + table.SetOnSelect(func(index int) error { + selectedRow := table.GetSelectedRow() + if selectedRow != nil { + container.SetNotice(fmt.Sprintf("Selected template: %s (%s)", selectedRow.Data()[0], selectedRow.Data()[1]), themes.OutputLevelInfo) + } + return nil + }) + + view = views.NewFrameView(table) case "form": f, err := views.NewFormView( container.RenderState(), diff --git a/views/table.go b/views/table.go new file mode 100644 index 0000000..703b5b1 --- /dev/null +++ b/views/table.go @@ -0,0 +1,358 @@ +package views + +import ( + "strings" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + + "github.com/flowexec/tuikit/types" +) + +const TableViewType = "table" + +type TableDisplayMode int + +const ( + TableDisplayFull TableDisplayMode = iota + TableDisplayMini +) + +type TableRow struct { + Data []string + Children []TableRow + Expanded bool +} + +type TableColumn struct { + Title string + Percentage int // width as percentage of total table width +} + +type Table struct { + render *types.RenderState + columns []TableColumn + rows []TableRow + displayMode TableDisplayMode + + selectedIndex int + visibleRows []VisibleRow + + OnSelect func(index int) error + OnHover func(index int) + + showBorder bool +} + +type VisibleRow struct { + data []string + isChild bool + parentIdx int + childIdx int + rowIdx int // index in original rows slice (-1 for children) +} + +func (vr *VisibleRow) Data() []string { + return vr.data +} + +func NewTable(render *types.RenderState, columns []TableColumn, rows []TableRow, mode TableDisplayMode) *Table { + t := &Table{ + render: render, + columns: columns, + rows: rows, + displayMode: mode, + showBorder: mode == TableDisplayMini, + } + t.buildVisibleRows() + return t +} + +func (t *Table) Init() tea.Cmd { + return nil +} + +func (t *Table) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case *types.RenderState: + t.render = msg + return t, nil + case tea.KeyMsg: + switch msg.String() { + case "up", "k": + if t.selectedIndex > 0 { + t.selectedIndex-- + if t.OnHover != nil { + t.OnHover(t.selectedIndex) + } + } + case "down", "j": + if t.selectedIndex < len(t.visibleRows)-1 { + t.selectedIndex++ + if t.OnHover != nil { + t.OnHover(t.selectedIndex) + } + } + case "enter": + if t.OnSelect != nil { + return t, func() tea.Msg { + err := t.OnSelect(t.selectedIndex) + if err != nil { + return err + } + return nil + } + } + case " ", "tab": + t.toggleExpansion() + t.buildVisibleRows() + } + } + return t, nil +} + +func (t *Table) View() string { + if t.render == nil || len(t.visibleRows) == 0 { + return "No data" + } + + tableWidth := t.calculateTableWidth() + colWidths := t.calculateColumnWidths(tableWidth) + + var content strings.Builder + + header := t.renderHeader(colWidths) + content.WriteString(header) + content.WriteString("\n") + + for i, row := range t.visibleRows { + rowStr := t.renderRow(row, colWidths, i == t.selectedIndex) + content.WriteString(rowStr) + content.WriteString("\n") + } + + result := content.String() + if t.displayMode == TableDisplayMini && t.showBorder { + return t.renderMiniTable(result, tableWidth) + } + + return result +} + +func (t *Table) HelpMsg() string { + return "↑/↓: navigate • enter: select • space/tab: expand/collapse" +} + +func (t *Table) ShowFooter() bool { + return true +} + +func (t *Table) Type() string { + return TableViewType +} + +func (t *Table) SetOnSelect(callback func(index int) error) { + t.OnSelect = callback +} + +func (t *Table) SetOnHover(callback func(index int)) { + t.OnHover = callback +} + +func (t *Table) SetRows(rows []TableRow) { + t.rows = rows + t.selectedIndex = 0 + t.buildVisibleRows() +} + +func (t *Table) GetSelectedRow() *VisibleRow { + if t.selectedIndex >= 0 && t.selectedIndex < len(t.visibleRows) { + return &t.visibleRows[t.selectedIndex] + } + return nil +} + +func (t *Table) calculateTableWidth() int { + if t.displayMode == TableDisplayMini { + maxWidth := int(float64(t.render.ContentWidth) * 0.66) + minWidth := 30 + if maxWidth < minWidth { + return minWidth + } + return maxWidth + } + return t.render.ContentWidth +} + +func (t *Table) calculateColumnWidths(totalWidth int) []int { + widths := make([]int, len(t.columns)) + usedWidth := 0 + + for i, col := range t.columns { + if i == len(t.columns)-1 { + // last column gets remaining width + widths[i] = totalWidth - usedWidth + } else { + width := (totalWidth * col.Percentage) / 100 + widths[i] = width + usedWidth += width + } + } + + return widths +} + +func (t *Table) renderHeader(colWidths []int) string { + var header string + + style := lipgloss.NewStyle(). + Bold(true). + Border(lipgloss.NormalBorder(), false). + BorderBottom(true). + BorderBottomForeground(t.render.Theme.ColorPalette().BorderColor()). + Foreground(t.render.Theme.ColorPalette().PrimaryColor()) + + for i, col := range t.columns { + title := col.Title + if len(title) > colWidths[i]-1 { + title = title[:colWidths[i]-4] + "..." + } + + cellContent := style.Width(colWidths[i] - 1).Render(title) + header = lipgloss.JoinHorizontal(lipgloss.Right, header, cellContent) + } + + return header +} + +func (t *Table) renderRow(row VisibleRow, colWidths []int, selected bool) string { + var rowStr strings.Builder + + var style lipgloss.Style + if selected { + style = lipgloss.NewStyle(). + Background(t.render.Theme.ColorPalette().PrimaryColor()). + Foreground(t.render.Theme.ColorPalette().GrayColor()).Bold(true) + } else if row.isChild { + style = lipgloss.NewStyle(). + Foreground(t.render.Theme.ColorPalette().TertiaryColor()) + } else { + style = lipgloss.NewStyle(). + Foreground(t.render.Theme.ColorPalette().BodyColor()) + } + + for i, cellData := range row.data { + if i >= len(colWidths) { + break + } + + content := cellData + if i == 0 && !row.isChild && row.rowIdx >= 0 { + if len(t.rows[row.rowIdx].Children) > 0 { + if t.rows[row.rowIdx].Expanded { + content = "◉ " + content + } else { + content = "● " + content + } + } else { + content = "◌ " + content + } + } + + if i == 0 && row.isChild { + if selected { + content = " > " + content + } else { + content = " " + content + } + } + + maxLen := colWidths[i] - 1 + if len(content) > maxLen { + if maxLen > 3 { + content = content[:maxLen-3] + "..." + } else { + content = content[:maxLen] + } + } + + cellContent := style.Width(colWidths[i] - 1).Render(content) + rowStr.WriteString(cellContent) + } + + return rowStr.String() +} + +func (t *Table) renderMiniTable(content string, tableWidth int) string { + leftPadding := (t.render.ContentWidth - tableWidth) / 2 + if leftPadding < 0 { + leftPadding = 0 + } + + borderStyle := lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(t.render.Theme.ColorPalette().BorderColor()). + Padding(1). + MarginLeft(leftPadding) + + return borderStyle.Render(content) +} + +func (t *Table) buildVisibleRows() { + t.visibleRows = make([]VisibleRow, 0) + + for i, row := range t.rows { + t.visibleRows = append(t.visibleRows, VisibleRow{ + data: row.Data, + isChild: false, + parentIdx: -1, + childIdx: -1, + rowIdx: i, + }) + + if row.Expanded { + for j, child := range row.Children { + t.visibleRows = append(t.visibleRows, VisibleRow{ + data: child.Data, + isChild: true, + parentIdx: i, + childIdx: j, + rowIdx: -1, + }) + } + } + } + + if t.selectedIndex >= len(t.visibleRows) { + t.selectedIndex = len(t.visibleRows) - 1 + } + if t.selectedIndex < 0 { + t.selectedIndex = 0 + } +} + +func (t *Table) toggleExpansion() { + if t.selectedIndex < 0 || t.selectedIndex >= len(t.visibleRows) { + return + } + + selectedRow := t.visibleRows[t.selectedIndex] + if selectedRow.isChild || selectedRow.rowIdx < 0 { + return + } + + rowIdx := selectedRow.rowIdx + if rowIdx >= len(t.rows) { + return + } + if len(t.rows[rowIdx].Children) == 0 { + return + } + + for i := range t.rows { + if i != rowIdx { + t.rows[i].Expanded = false + } + } + t.rows[rowIdx].Expanded = !t.rows[rowIdx].Expanded +}