Update viewer

Co-Authored-By: Liza Tsibur <liza@activecountermeasures.com>
This commit is contained in:
Naomi Kramer
2024-07-16 13:51:11 -04:00
parent b9974cea30
commit 9faeff565d
5 changed files with 169 additions and 131 deletions
+27 -20
View File
@@ -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"
text: "import-shadowing: The name ('require') shadows an import name"
+6 -4
View File
@@ -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)
+76 -63
View File
@@ -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
+36 -33
View File
@@ -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 {
+24 -11
View File
@@ -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 {