diff --git a/.golangci.yml b/.golangci.yml index c70520a..a04a06b 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -34,7 +34,7 @@ linters-settings: sizeThreshold: 512 # Default: 512 skipTestFuncs: true rangeValCopy: - sizeThreshold: 200 # Default: 128 + sizeThreshold: 200 # Default: 128 skipTestFuncs: true hugeParam: sizeThreshold: 100 # Default: 80 @@ -74,23 +74,23 @@ linters-settings: # Enable to require nolint directives to mention the specific linter being suppressed. # Default: false require-specific: true - + revive: - enable-all-rules: true + enable-all-rules: true # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md rules: - name: add-constant disabled: true - name: argument-limit arguments: [10] - - name: cognitive-complexity # enable in future cleanup + - name: cognitive-complexity disabled: true - name: confusing-results disabled: true - - name: cyclomatic # enable in future cleanup + - name: cyclomatic disabled: true - name: deep-exit - disabled: true # enable in future cleanup + disabled: true - name: empty-lines disabled: true - name: exported @@ -113,16 +113,23 @@ linters-settings: arguments: [8] - name: var-naming arguments: - - [ ] # allowlist - - [ ] # blocklist - - [ { "upperCaseConst": true, "skipPackageNameChecks": true } ] + - [] # allowlist + - [] # blocklist + - [{ "upperCaseConst": true, "skipPackageNameChecks": true }] - name: unexported-return disabled: true - name: unhandled-error - arguments: [ "fmt.Printf", "fmt.Fprintf", "fmt.Println", "io.Closer.Close", "os.File.Close" ] + arguments: + [ + "fmt.Printf", + "fmt.Fprintf", + "fmt.Println", + "io.Closer.Close", + "os.File.Close", + ] - name: unused-receiver disabled: true - + goconst: # Minimum occurrences of constant string count to trigger issue. # Default: 3 @@ -130,7 +137,7 @@ linters-settings: # Ignore test files. # Default: false ignore-tests: true - + stylecheck: checks: ["all", "-ST1003"] @@ -155,7 +162,7 @@ linters: - durationcheck # Forces to not skip error check. - - errcheck + - errcheck # Checks `Err-` prefix for var and `-Error` suffix for error type. - errname @@ -178,7 +185,7 @@ linters: # Checks standard formatting (whitespace, indentation, etc.) - gofmt - # Checks + # Checks - goimports # Allow or ban replace directives in go.mod or force explanation for retract directives. @@ -264,25 +271,25 @@ issues: # - "var-naming: don't use an underscore in package name" exclude-rules: # ignore shadowing of err and ctx - - linters: [ govet ] + - linters: [govet] text: 'shadow: declaration of "(err|ctx)" shadows declaration at' # tolerate long functions in tests - - linters: [ revive ] + - linters: [revive] path: "(.+)_test.go" text: "function-length: .*" # tolerate long lines in tests - - linters: [ revive ] + - linters: [revive] path: "(.+)_test.go" text: "line-length-limit: .*" # tolerate deep-exit in tests - - linters: [ revive ] + - linters: [revive] path: "(.+)_test.go" text: "deep-exit: .*" # tolerate import shadowing in tests - - linters: [ revive ] + - linters: [revive] path: "(.+)_test.go" - text: "import-shadowing: The name ('require') shadows an import name" \ No newline at end of file + text: "import-shadowing: The name ('require') shadows an import name" diff --git a/viewer/list.go b/viewer/list.go index 4de7626..415290f 100644 --- a/viewer/list.go +++ b/viewer/list.go @@ -51,10 +51,11 @@ const ( ) type listModel struct { - Rows list.Model - width int - totalHeight int - columns []column + Rows list.Model + width int + totalHeight int + headerHeight int + columns []column } func MakeList(items []list.Item, columns []column, width int, height int) listModel { @@ -95,6 +96,7 @@ func (m *listModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m *listModel) SetHeight(height int) { _, v := listStyle.GetFrameSize() header := lipgloss.Height(renderColumnHeader(m.columns, m.width)) + m.headerHeight = header h := (height - header - v) m.totalHeight = header + v + h m.Rows.SetSize(m.width, h) diff --git a/viewer/results.go b/viewer/results.go index 6ea3f5f..b9612be 100644 --- a/viewer/results.go +++ b/viewer/results.go @@ -64,7 +64,6 @@ func (i *Item) GetDst() string { return i.Dst.String() } -// func (i *Item) FQDN() string { return i.fqdn } func (i *Item) GetBeacon() string { // if connection is a strobe, set beacon score to 100% if i.StrobeScore > 0 { @@ -72,6 +71,7 @@ func (i *Item) GetBeacon() string { } return renderIndicator(i.BeaconThreatScore, fmt.Sprintf("%1.2f%%", i.BeaconScore*100)) } + func (i *Item) GetFirstSeen(relativeTimestamp time.Time) string { timeAgo := relativeTimestamp.Sub(i.FirstSeen) switch { @@ -130,8 +130,11 @@ func (i *Item) GetThreatIntel() string { return "" } -// no-op -func (i Item) FilterValue() string { return i.GetSrc() } //nolint:gocritic // filtervalue cannot be a pointer method +//nolint:gocritic // filtervalue cannot be a pointer method +func (i Item) FilterValue() string { return i.GetSrc() } // no-op, bubble tea requires this method to be implemented but it is unused + +// GetSeverity returns the severity of the mixtape result based on the final score and +// adds a color based on the severity level if color is set to true func (i *Item) GetSeverity(color bool) string { caser := cases.Title(language.English) @@ -157,6 +160,7 @@ func (i *Item) GetSeverity(color bool) string { return caser.String(string(severity)) } +// GetResults queries the database for mixtape results based on the filter and pagination parameters func GetResults(db *database.DB, filter *Filter, currentPage, pageSize int, minTimestamp time.Time) ([]list.Item, bool, error) { // build query query, params, appliedFilter := BuildResultsQuery(filter, currentPage, pageSize, minTimestamp) @@ -184,6 +188,7 @@ func GetResults(db *database.DB, filter *Filter, currentPage, pageSize int, minT return items, appliedFilter, nil } +// BuildResultsQuery builds a query for fetching mixtape results based on the filter and pagination parameters func BuildResultsQuery(filter *Filter, currentPage, pageSize int, minTimestamp time.Time) (string, clickhouse.Parameters, bool) { params := clickhouse.Parameters{} query := `--sql @@ -254,25 +259,27 @@ func BuildResultsQuery(filter *Filter, currentPage, pageSize int, minTimestamp t // set where conditions for src and dst filters whereConditions := []string{} - if filter.Src != "" { - whereConditions = append(whereConditions, "src={src:String}") - params["src"] = filter.Src - } - if filter.Dst != "" { - whereConditions = append(whereConditions, "dst={dst:String}") - params["dst"] = filter.Dst - } - if filter.Fqdn != "" { - whereConditions = append(whereConditions, "fqdn={fqdn:String}") - params["fqdn"] = filter.Fqdn - } - if filter.ThreatIntel != "" { - whereConditions = append(whereConditions, "threat_intel={threat_intel:Bool}") - params["threat_intel"] = filter.ThreatIntel - } - if !filter.LastSeen.IsZero() { - whereConditions = append(whereConditions, "toStartOfHour(last_seen) >= {last_seen:Int64}") - params["last_seen"] = fmt.Sprintf("%d", filter.LastSeen.UTC().Unix()) + if filter != nil { + if filter.Src != "" { + whereConditions = append(whereConditions, "src={src:String}") + params["src"] = filter.Src + } + if filter.Dst != "" { + whereConditions = append(whereConditions, "dst={dst:String}") + params["dst"] = filter.Dst + } + if filter.Fqdn != "" { + whereConditions = append(whereConditions, "fqdn={fqdn:String}") + params["fqdn"] = filter.Fqdn + } + if filter.ThreatIntel != "" { + whereConditions = append(whereConditions, "threat_intel={threat_intel:Bool}") + params["threat_intel"] = filter.ThreatIntel + } + if !filter.LastSeen.IsZero() { + whereConditions = append(whereConditions, "toStartOfHour(last_seen) >= {last_seen:Int64}") + params["last_seen"] = fmt.Sprintf("%d", filter.LastSeen.UTC().Unix()) + } } // set where conditions for src and dst filters to query if any were specified @@ -287,29 +294,31 @@ func BuildResultsQuery(filter *Filter, currentPage, pageSize int, minTimestamp t // set having conditions for numerical filters havingConditions := []string{} - if filter.Count.Value != "" && filter.Count.Operator != "" { - havingConditions = append(havingConditions, "count "+filter.Count.Operator+" {count:Int64}") - params["count"] = filter.Count.Value - } - - if filter.Beacon.Value != "" && filter.Beacon.Operator != "" { - havingConditions = append(havingConditions, "beacon_score "+filter.Beacon.Operator+" {beacon:Float32}") - params["beacon"] = filter.Beacon.Value - } - - if filter.Subdomains.Value != "" && filter.Subdomains.Operator != "" { - havingConditions = append(havingConditions, "subdomain_count "+filter.Subdomains.Operator+" {subdomains:Int64}") - params["subdomains"] = filter.Subdomains.Value - } - - if filter.Duration.Value != "" && filter.Duration.Operator != "" { - if filter.Duration.Operator == "=" { - // round column down to the nearest integer if the operator is equ - havingConditions = append(havingConditions, "floor(total_duration) "+filter.Duration.Operator+" {duration:Float64}") - } else { - havingConditions = append(havingConditions, "total_duration "+filter.Duration.Operator+" {duration:Float64}") + if filter != nil { + if filter.Count.Value != "" && filter.Count.Operator != "" { + havingConditions = append(havingConditions, "count "+filter.Count.Operator+" {count:Int64}") + params["count"] = filter.Count.Value + } + + if filter.Beacon.Value != "" && filter.Beacon.Operator != "" { + havingConditions = append(havingConditions, "beacon_score "+filter.Beacon.Operator+" {beacon:Float32}") + params["beacon"] = filter.Beacon.Value + } + + if filter.Subdomains.Value != "" && filter.Subdomains.Operator != "" { + havingConditions = append(havingConditions, "subdomain_count "+filter.Subdomains.Operator+" {subdomains:Int64}") + params["subdomains"] = filter.Subdomains.Value + } + + if filter.Duration.Value != "" && filter.Duration.Operator != "" { + if filter.Duration.Operator == "=" { + // round column down to the nearest integer if the operator is equ + havingConditions = append(havingConditions, "floor(total_duration) "+filter.Duration.Operator+" {duration:Float64}") + } else { + havingConditions = append(havingConditions, "total_duration "+filter.Duration.Operator+" {duration:Float64}") + } + params["duration"] = filter.Duration.Value } - params["duration"] = filter.Duration.Value } // add having conditions to query if any were specified @@ -323,29 +332,33 @@ func BuildResultsQuery(filter *Filter, currentPage, pageSize int, minTimestamp t // add where conditions to the outer part of the query if any were specified outerWhereConditions := []string{} - // add conditions for severity filter to query - if len(filter.Severity) > 0 { - for i, op := range filter.Severity { - paramName := fmt.Sprintf("final_score_%d", i) - outerWhereConditions = append(outerWhereConditions, "final_score "+op.Operator+fmt.Sprintf("{%s:Float32}", paramName)) - params[paramName] = op.Value + if filter != nil { + // add conditions for severity filter to query + if len(filter.Severity) > 0 { + for i, op := range filter.Severity { + paramName := fmt.Sprintf("final_score_%d", i) + outerWhereConditions = append(outerWhereConditions, "final_score "+op.Operator+fmt.Sprintf("{%s:Float32}", paramName)) + params[paramName] = op.Value + } + query += "WHERE " + strings.Join(outerWhereConditions, " AND ") } - query += "WHERE " + strings.Join(outerWhereConditions, " AND ") } // set sorting conditions if any were specified sortingConditions := []string{} - if filter.SortSeverity != "" { - sortingConditions = append(sortingConditions, "final_score "+filter.SortSeverity) - } - if filter.SortBeacon != "" { - sortingConditions = append(sortingConditions, "beacon_score "+filter.SortBeacon) - } - if filter.SortDuration != "" { - sortingConditions = append(sortingConditions, "total_duration "+filter.SortDuration) - } - if filter.SortSubdomains != "" { - sortingConditions = append(sortingConditions, "subdomains "+filter.SortSubdomains) + if filter != nil { + if filter.SortSeverity != "" { + sortingConditions = append(sortingConditions, "final_score "+filter.SortSeverity) + } + if filter.SortBeacon != "" { + sortingConditions = append(sortingConditions, "beacon_score "+filter.SortBeacon) + } + if filter.SortDuration != "" { + sortingConditions = append(sortingConditions, "total_duration "+filter.SortDuration) + } + if filter.SortSubdomains != "" { + sortingConditions = append(sortingConditions, "subdomains "+filter.SortSubdomains) + } } // add sorting conditions to query if any were specified @@ -357,8 +370,8 @@ func BuildResultsQuery(filter *Filter, currentPage, pageSize int, minTimestamp t ` } - offset := currentPage * pageSize // set offset ; fetch if the offset is greater than 0, otherwise set limit + offset := currentPage * pageSize if offset > 0 { query += `--sql OFFSET {skip:Int32} ROWS FETCH NEXT {page_size:Int32} ROWS ONLY diff --git a/viewer/sidebar.go b/viewer/sidebar.go index 09c461c..97f6dfe 100644 --- a/viewer/sidebar.go +++ b/viewer/sidebar.go @@ -23,6 +23,7 @@ type modifier struct { type sidebarModel struct { Viewport viewport.Model Data *Item + Height int maxTimestamp time.Time useCurrentTime bool ScrollEnabled bool @@ -41,27 +42,42 @@ func (m *sidebarModel) Init() tea.Cmd { m.Viewport.SetContent(m.getSidebarContents()) return nil } -func (m *sidebarModel) Update(_ tea.Msg) (tea.Model, tea.Cmd) { - return m, nil +func (m *sidebarModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmds []tea.Cmd + switch msg.(type) { + case tea.KeyMsg: + // if k := msg.String(); k == "ctrl+c" || k == "q" || k == "esc" { + // return m, tea.Quit + // } + + case tea.WindowSizeMsg: + cmds = append(cmds, viewport.Sync(m.Viewport)) + + // return m, nil + } + return m, tea.Batch(cmds...) } func (m *sidebarModel) View() string { m.Viewport.SetContent(m.getSidebarContents()) + m.Viewport.Height = m.Height borderColor := mauve if m.ScrollEnabled { borderColor = green } style := sideBarStyle. - // .Width(m.width).Height(m.height). + // .Width(m.width) + // Height(m.Viewport.Height). Padding(0, 1). Border(lipgloss.RoundedBorder()). BorderForeground(borderColor) - - return style.Render(m.Viewport.View()) + hi := style.Render(m.Viewport.View()) + return lipgloss.NewStyle().Render(hi) + // Border(lipgloss.NormalBorder()) } +// getSidebarContents gets and formats the data to be displayed in the sidebar func (m *sidebarModel) getSidebarContents() string { - // get header var target string headerPadding := 2 @@ -74,8 +90,8 @@ func (m *sidebarModel) getSidebarContents() string { // dstStyle := lipgloss.NewStyle().Width(m.viewport.Width - (headerPadding * 2)) fqdnLabel := "FQDN" dstStyle := lipgloss.NewStyle().Width(m.Viewport.Width - len(fqdnLabel) - (headerPadding * 4)) - - target = lipgloss.JoinHorizontal(lipgloss.Left, headerLabelStyle.Render(fqdnLabel), headerValueStyle.Render(Truncate(m.Data.GetDst(), &dstStyle))) + valueStyle := headerValueStyle.Render(Truncate(m.Data.GetDst(), &dstStyle)) + target = lipgloss.JoinHorizontal(lipgloss.Left, headerLabelStyle.Render(fqdnLabel), valueStyle) target = lipgloss.NewStyle().MarginBottom(2).Render(target) } else { // handle connection pair, ip -> ip or ip -> fqdn @@ -83,15 +99,14 @@ func (m *sidebarModel) getSidebarContents() string { srcStyle := lipgloss.NewStyle().Width(m.Viewport.Width - len(srcLabel) - (headerPadding * 4)) dstLabel := "DST" dstStyle := lipgloss.NewStyle().Width(m.Viewport.Width - len(dstLabel) - (headerPadding * 4)) - // - 6 - len(m.data.GetSrc()) - // dstStyle := lipgloss.NewStyle().Width(dstWidth) - src := lipgloss.JoinHorizontal(lipgloss.Left, headerLabelStyle.Render(srcLabel), headerValueStyle.Render(Truncate(m.Data.GetSrc(), &srcStyle))) - dst := lipgloss.JoinHorizontal(lipgloss.Left, headerLabelStyle.Render(dstLabel), headerValueStyle.Render(Truncate(m.Data.GetDst(), &dstStyle))) + srcValueStyle := headerValueStyle.Render(Truncate(m.Data.GetSrc(), &srcStyle)) + dstValueStyle := headerValueStyle.Render(Truncate(m.Data.GetDst(), &dstStyle)) + + src := lipgloss.JoinHorizontal(lipgloss.Left, headerLabelStyle.Render(srcLabel), srcValueStyle) + dst := lipgloss.JoinHorizontal(lipgloss.Left, headerLabelStyle.Render(dstLabel), dstValueStyle) target = lipgloss.JoinVertical(lipgloss.Top, lipgloss.NewStyle().MarginBottom(1).Render(src), dst) } - heading := lipgloss.NewStyle(). - // PaddingLeft(headerPadding). - MarginBottom(1).Render(target) + heading := lipgloss.NewStyle().MarginBottom(1).Render(target) // get modifiers sectionStyle := lipgloss.NewStyle(). @@ -130,8 +145,8 @@ func (m *sidebarModel) getSidebarContents() string { // render header portsHeader := portsHeaderStyle.Render("Port : Proto : Service") - ports = lipgloss.JoinVertical(lipgloss.Top, portsHeader, strings.Join(portProtoService, "\n")) - + ports = lipgloss.JoinVertical(lipgloss.Top, portsHeader, strings.Join(portProtoService, ",")) + // strings.Join(portProtoService, "\n") // calculate the number of lines available for port data // remainingLines := m.viewport.Height - (lipgloss.Height(heading) + lipgloss.Height(modifiers) + lipgloss.Height(modifierLabel) + lipgloss.Height(connInfoLabel) + lipgloss.Height(bytes) + lipgloss.Height(connCount)) // ports = renderPorts(portProtoService, m.viewport.Width, remainingLines) @@ -142,61 +157,48 @@ func (m *sidebarModel) getSidebarContents() string { return lipgloss.JoinVertical(lipgloss.Top, heading, modifierLabel, modifiers, connInfoLabel, connCount, bytes, ports) } +// renderModifiers aggregates and formats the modifiers for the currently selected item +// for rendering in the sidebar func (m *sidebarModel) renderModifiers() string { modifierList := m.getModifiers() - // panic(modifierList) var modifiers string var renderedModifiers []string for _, modifier := range modifierList { renderedModifier := renderModifier(modifier) - // if i > 0 { - // renderedModifier = lipgloss.NewStyle().MarginLeft(1).Render(renderedModifier) - // } renderedModifiers = append(renderedModifiers, renderedModifier) } newlineStyle := lipgloss.NewStyle().PaddingRight(1).BorderForeground(overlay2).Border(lipgloss.NormalBorder(), false, true, false, false) linebreakStyle := lipgloss.NewStyle().MarginBottom(1) - // if len(renderedModifiers) > 2 { var modifierLines []string - // lastUsedIndex := 0 var currentModifiers string for i, mod := range renderedModifiers { if i == 0 { currentModifiers = newlineStyle.Render(mod) } else { - // modifier = lipgloss.NewStyle().Border(lipgloss.NormalBorder(), false, true, false, false).Padding(0, 2, 0, 0).Render(modifier) newMod := lipgloss.JoinHorizontal(lipgloss.Left, currentModifiers, lipgloss.NewStyle().Padding(0, 1).BorderForeground(overlay2).Border(lipgloss.NormalBorder(), false, true, false, false).Render(mod)) - // panic(newMod) width := lipgloss.Width(newMod) if m.Viewport.Width <= width { modifierLines = append(modifierLines, lipgloss.JoinHorizontal(lipgloss.Left, linebreakStyle.Render(currentModifiers))) - // lastUsedIndex = i currentModifiers = mod if i != len(renderedModifiers)-1 { currentModifiers = newlineStyle.Render(mod) } } else { currentModifiers = newMod - // if i != len(renderedModifiers)-1 { - // currentModifiers = newlineStyle.Render(newMod) - // } } } - // modifiers += mod } modifierLines = append(modifierLines, linebreakStyle.Render(currentModifiers)) - // panic(modifierLines) modifiers = lipgloss.JoinVertical(lipgloss.Top, modifierLines...) - // modifiers = lipgloss.NewStyle().MarginBottom(1).PaddingBottom(1).Border(lipgloss.NormalBorder(), false, false, true, false).BorderForeground(surface0).Render(modifiers) - // } return modifiers } +// getModifiers gets all the modifiers for the currently selected item func (m *sidebarModel) getModifiers() []modifier { var modifiers []modifier @@ -245,6 +247,7 @@ func (m *sidebarModel) getModifiers() []modifier { return modifiers } +// renderModifier formats and styles a single modifier for rendering func renderModifier(mod modifier) string { var color lipgloss.AdaptiveColor switch { diff --git a/viewer/viewer.go b/viewer/viewer.go index ffa3a96..2205cb5 100644 --- a/viewer/viewer.go +++ b/viewer/viewer.go @@ -18,7 +18,7 @@ import ( "github.com/charmbracelet/lipgloss/table" ) -var DebugMode bool +var DebugMode bool // set true by rita if debug flag is passed in var mainStyle = lipgloss.NewStyle().Margin(0, 0) type Model struct { @@ -57,6 +57,9 @@ type column struct { width int } +type FinishedLoadingResults string +type StillLoadingResults string + // CreateUI creates the terminal UI func CreateUI(_ *config.Config, db *database.DB, useCurrentTime bool, maxTimestamp time.Time, minTimestamp time.Time) error { // create model @@ -76,6 +79,7 @@ func CreateUI(_ *config.Config, db *database.DB, useCurrentTime bool, maxTimesta return nil } +// NewModel creates a new model func NewModel(maxTimestamp, minTimestamp time.Time, useCurrentTime bool, db *database.DB) (*Model, error) { pageSize := 100 // get results from database @@ -101,11 +105,14 @@ func NewModel(maxTimestamp, minTimestamp time.Time, useCurrentTime bool, db *dat sideBar := NewSidebarModel(maxTimestamp, useCurrentTime, &Item{}) if len(dataList.Rows.Items()) > 0 { // set sidebar data to whichever item is selected in the list - data, ok := dataList.Rows.Items()[dataList.Rows.Index()].(Item) + tmp := dataList.Rows.Items()[dataList.Rows.Index()] + data, ok := tmp.(*Item) + fmt.Println(data, tmp) + if !ok { return nil, fmt.Errorf("error setting sidebar data") } - sideBar.Data = &data + sideBar.Data = data } @@ -134,6 +141,7 @@ func NewModel(maxTimestamp, minTimestamp time.Time, useCurrentTime bool, db *dat return m, nil } +// Init initializes the model func (m *Model) Init() tea.Cmd { // set title @@ -178,6 +186,7 @@ func (m *Model) Init() tea.Cmd { return m.Footer.spinner.Tick } +// Update updates the model func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd @@ -185,12 +194,14 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.WindowSizeMsg: // make the footer the entire width of the terminal m.Footer.width = msg.Width - + height := msg.Height - int(math.Max(float64(lipgloss.Height(m.SearchBar.View())), float64(lipgloss.Height(m.title)))) - lipgloss.Height(m.dbFooterBar) // make the list fill the extra vertical space - m.List.SetHeight(msg.Height - int(math.Max(float64(lipgloss.Height(m.SearchBar.View())), float64(lipgloss.Height(m.title)))) - lipgloss.Height(m.dbFooterBar)) + m.List.SetHeight(height) // make the sidebar the same height as the list m.SideBar.Viewport.Height = m.List.totalHeight + m.SideBar.Height = m.List.totalHeight + // m.SideBar.Viewport.Height = lipgloss.Height(m.List.View()) // make sidebar fill the extra horizontal space m.SideBar.Viewport.Width = msg.Width - lipgloss.Width(m.List.View()) - 4 @@ -258,11 +269,13 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.List.Rows.Select(index) } - if data, ok := m.List.Rows.Items()[m.List.Rows.Index()].(Item); ok { - m.SideBar.Data = &data + // set sidebar data to the selected item + if data, ok := m.List.Rows.Items()[m.List.Rows.Index()].(*Item); ok { + m.SideBar.Data = data } } else { + // if there are no items to display, set the sidebar data to an empty item m.SideBar.Data = &Item{} } @@ -280,9 +293,10 @@ func (m *Model) View() string { case m.ViewHelp: mainContent = helpPanel(m.SideBar.Viewport.Height, m.List.width, mainHelpText()) default: + resultList := mainStyle.Render(m.List.View()) mainContent = lipgloss.JoinHorizontal( lipgloss.Left, - mainStyle.Render(m.List.View()), + resultList, mainStyle.Render(m.SideBar.View()), ) } @@ -295,9 +309,6 @@ func (m *Model) View() string { ) } -type FinishedLoadingResults string -type StillLoadingResults string - // handleFiltering handles key presses on the search bar func (m *Model) handleFiltering(msg tea.KeyMsg) tea.Cmd { var cmd tea.Cmd @@ -594,12 +605,14 @@ func mainHelpText() string { } +// helpPanel returns a help panel with the given height, width, and contents func helpPanel(height int, width int, contents string) string { return mainStyle.Height(height).Width(width). Border(lipgloss.RoundedBorder()).BorderForeground(surface0). Render(contents) } +// getTableWidth returns the total width of the table func getTableWidth(columns []column) int { width := 0 for _, column := range columns {