feat: add "flag" toggle in replay & findings

Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
This commit is contained in:
Hadi
2026-05-30 20:15:51 +02:00
parent ec0fb61099
commit cc0176a73d
15 changed files with 155 additions and 28 deletions
+3
View File
@@ -83,12 +83,15 @@ keybindings:
delete_entry: "x" delete_entry: "x"
delete_all: "X" delete_all: "X"
filter: "/" filter: "/"
flag: "m"
diff: diff:
clear: "x" clear: "x"
findings: findings:
dismiss: "x" dismiss: "x"
dismiss_all: "X"
flag: "m"
plugins: plugins:
toggle: "space" toggle: "space"
+4 -1
View File
@@ -57,6 +57,7 @@ type ReplayKeys struct {
Delete string `mapstructure:"delete_entry"` Delete string `mapstructure:"delete_entry"`
DeleteAll string `mapstructure:"delete_all"` DeleteAll string `mapstructure:"delete_all"`
Filter string `mapstructure:"filter"` Filter string `mapstructure:"filter"`
Flag string `mapstructure:"flag"`
} }
type DiffKeys struct { type DiffKeys struct {
@@ -64,7 +65,9 @@ type DiffKeys struct {
} }
type FindingsKeys struct { type FindingsKeys struct {
Dismiss string `mapstructure:"dismiss"` Dismiss string `mapstructure:"dismiss"`
DismissAll string `mapstructure:"dismiss_all"`
Flag string `mapstructure:"flag"`
} }
type PluginsKeys struct { type PluginsKeys struct {
+3 -1
View File
@@ -79,7 +79,8 @@ CREATE TABLE IF NOT EXISTS replay_entries (
request_raw TEXT NOT NULL, request_raw TEXT NOT NULL,
response_raw TEXT NOT NULL, response_raw TEXT NOT NULL,
status_code INTEGER NOT NULL, status_code INTEGER NOT NULL,
error_msg TEXT NOT NULL error_msg TEXT NOT NULL,
flagged INTEGER NOT NULL DEFAULT 0
); );
CREATE TABLE IF NOT EXISTS findings ( CREATE TABLE IF NOT EXISTS findings (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -89,6 +90,7 @@ CREATE TABLE IF NOT EXISTS findings (
description TEXT NOT NULL DEFAULT '', description TEXT NOT NULL DEFAULT '',
severity TEXT NOT NULL DEFAULT 'info', severity TEXT NOT NULL DEFAULT 'info',
dismissed INTEGER NOT NULL DEFAULT 0, dismissed INTEGER NOT NULL DEFAULT 0,
flagged INTEGER NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL, created_at DATETIME NOT NULL,
UNIQUE(plugin_name, dedup_key) UNIQUE(plugin_name, dedup_key)
); );
+25 -2
View File
@@ -11,6 +11,7 @@ type Finding struct {
Title string Title string
Description string Description string
Severity string Severity string
Flagged bool
CreatedAt time.Time CreatedAt time.Time
} }
@@ -34,7 +35,7 @@ func (d *DB) UpsertFinding(f Finding) (bool, error) {
func (d *DB) LoadFindings() ([]Finding, error) { func (d *DB) LoadFindings() ([]Finding, error) {
rows, err := d.conn.Query( rows, err := d.conn.Query(
`SELECT id, plugin_name, dedup_key, title, description, severity, created_at `SELECT id, plugin_name, dedup_key, title, description, severity, flagged, created_at
FROM findings WHERE dismissed = 0 ORDER BY id ASC`, FROM findings WHERE dismissed = 0 ORDER BY id ASC`,
) )
if err != nil { if err != nil {
@@ -45,9 +46,11 @@ func (d *DB) LoadFindings() ([]Finding, error) {
for rows.Next() { for rows.Next() {
var f Finding var f Finding
var ts string var ts string
if err := rows.Scan(&f.ID, &f.PluginName, &f.DedupKey, &f.Title, &f.Description, &f.Severity, &ts); err != nil { var flagged int
if err := rows.Scan(&f.ID, &f.PluginName, &f.DedupKey, &f.Title, &f.Description, &f.Severity, &flagged, &ts); err != nil {
return nil, err return nil, err
} }
f.Flagged = flagged != 0
for _, layout := range findingTimeFormats { for _, layout := range findingTimeFormats {
if t, err := time.Parse(layout, ts); err == nil { if t, err := time.Parse(layout, ts); err == nil {
f.CreatedAt = t.Local() f.CreatedAt = t.Local()
@@ -63,3 +66,23 @@ func (d *DB) DismissFinding(id int64) error {
_, err := d.conn.Exec(`UPDATE findings SET dismissed = 1 WHERE id = ?`, id) _, err := d.conn.Exec(`UPDATE findings SET dismissed = 1 WHERE id = ?`, id)
return err return err
} }
func (d *DB) ToggleFindingFlag(id int64) error {
_, err := d.conn.Exec(`UPDATE findings SET flagged = NOT flagged WHERE id = ?`, id)
return err
}
// DismissAllFindings dismisses unflagged findings first. If none are unflagged
// (only flagged ones remain), it dismisses everything.
func (d *DB) DismissAllFindings() error {
var count int
if err := d.conn.QueryRow(`SELECT COUNT(*) FROM findings WHERE dismissed = 0 AND flagged = 0`).Scan(&count); err != nil {
return err
}
if count > 0 {
_, err := d.conn.Exec(`UPDATE findings SET dismissed = 1 WHERE flagged = 0`)
return err
}
_, err := d.conn.Exec(`UPDATE findings SET dismissed = 1`)
return err
}
+30 -7
View File
@@ -17,16 +17,17 @@ type ReplayEntry struct {
ResponseRaw string ResponseRaw string
StatusCode int StatusCode int
ErrorMsg string ErrorMsg string
Flagged bool
} }
func (d *DB) InsertReplayEntry(e ReplayEntry) (int64, error) { func (d *DB) InsertReplayEntry(e ReplayEntry) (int64, error) {
res, err := d.conn.Exec( res, err := d.conn.Exec(
`INSERT INTO replay_entries (timestamp, scheme, host, path, method, original_raw, request_raw, response_raw, status_code, error_msg) `INSERT INTO replay_entries (timestamp, scheme, host, path, method, original_raw, request_raw, response_raw, status_code, error_msg, flagged)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
e.Timestamp.UTC().Format(time.RFC3339), e.Timestamp.UTC().Format(time.RFC3339),
e.Scheme, e.Host, e.Path, e.Method, e.Scheme, e.Host, e.Path, e.Method,
e.OriginalRaw, e.RequestRaw, e.ResponseRaw, e.OriginalRaw, e.RequestRaw, e.ResponseRaw,
e.StatusCode, e.ErrorMsg, e.StatusCode, e.ErrorMsg, e.Flagged,
) )
if err != nil { if err != nil {
return 0, err return 0, err
@@ -36,15 +37,15 @@ func (d *DB) InsertReplayEntry(e ReplayEntry) (int64, error) {
func (d *DB) UpdateReplayEntry(e ReplayEntry) error { func (d *DB) UpdateReplayEntry(e ReplayEntry) error {
_, err := d.conn.Exec( _, err := d.conn.Exec(
`UPDATE replay_entries SET request_raw=?, response_raw=?, status_code=?, error_msg=? WHERE id=?`, `UPDATE replay_entries SET request_raw=?, response_raw=?, status_code=?, error_msg=?, flagged=? WHERE id=?`,
e.RequestRaw, e.ResponseRaw, e.StatusCode, e.ErrorMsg, e.ID, e.RequestRaw, e.ResponseRaw, e.StatusCode, e.ErrorMsg, e.Flagged, e.ID,
) )
return err return err
} }
func (d *DB) ListReplayEntries() ([]ReplayEntry, error) { func (d *DB) ListReplayEntries() ([]ReplayEntry, error) {
rows, err := d.conn.Query( rows, err := d.conn.Query(
`SELECT id, timestamp, scheme, host, path, method, original_raw, request_raw, response_raw, status_code, error_msg `SELECT id, timestamp, scheme, host, path, method, original_raw, request_raw, response_raw, status_code, error_msg, flagged
FROM replay_entries ORDER BY id ASC`, FROM replay_entries ORDER BY id ASC`,
) )
if err != nil { if err != nil {
@@ -56,8 +57,9 @@ func (d *DB) ListReplayEntries() ([]ReplayEntry, error) {
for rows.Next() { for rows.Next() {
var e ReplayEntry var e ReplayEntry
var ts string var ts string
var flagged int
if err := rows.Scan(&e.ID, &ts, &e.Scheme, &e.Host, &e.Path, &e.Method, if err := rows.Scan(&e.ID, &ts, &e.Scheme, &e.Host, &e.Path, &e.Method,
&e.OriginalRaw, &e.RequestRaw, &e.ResponseRaw, &e.StatusCode, &e.ErrorMsg); err != nil { &e.OriginalRaw, &e.RequestRaw, &e.ResponseRaw, &e.StatusCode, &e.ErrorMsg, &flagged); err != nil {
return nil, err return nil, err
} }
var tsErr error var tsErr error
@@ -65,11 +67,17 @@ func (d *DB) ListReplayEntries() ([]ReplayEntry, error) {
if tsErr != nil { if tsErr != nil {
log.Printf("db: parse replay timestamp %q: %v", ts, tsErr) log.Printf("db: parse replay timestamp %q: %v", ts, tsErr)
} }
e.Flagged = flagged != 0
entries = append(entries, e) entries = append(entries, e)
} }
return entries, rows.Err() return entries, rows.Err()
} }
func (d *DB) ToggleReplayFlag(id int64) error {
_, err := d.conn.Exec(`UPDATE replay_entries SET flagged = NOT flagged WHERE id = ?`, id)
return err
}
func (d *DB) DeleteReplayEntry(id int64) error { func (d *DB) DeleteReplayEntry(id int64) error {
_, err := d.conn.Exec(`DELETE FROM replay_entries WHERE id = ?`, id) _, err := d.conn.Exec(`DELETE FROM replay_entries WHERE id = ?`, id)
return err return err
@@ -79,3 +87,18 @@ func (d *DB) DeleteAllReplayEntries() error {
_, err := d.conn.Exec(`DELETE FROM replay_entries`) _, err := d.conn.Exec(`DELETE FROM replay_entries`)
return err return err
} }
// DeleteAllReplayEntriesExceptFlagged deletes all unflagged replay entries. If
// none are unflagged (only flagged ones remain), it deletes everything.
func (d *DB) DeleteAllReplayEntriesExceptFlagged() error {
var count int
if err := d.conn.QueryRow(`SELECT COUNT(*) FROM replay_entries WHERE flagged = 0`).Scan(&count); err != nil {
return err
}
if count > 0 {
_, err := d.conn.Exec(`DELETE FROM replay_entries WHERE flagged = 0`)
return err
}
_, err := d.conn.Exec(`DELETE FROM replay_entries`)
return err
}
+3 -1
View File
@@ -48,6 +48,8 @@ func Init(cfg *config.Config) {
Flag: "󰈻 ", Flag: "󰈻 ",
} }
} else { } else {
I = &Icons{} I = &Icons{
Flag: "*",
}
} }
} }
+7 -3
View File
@@ -6,15 +6,19 @@ import (
) )
type FindingsKeyMap struct { type FindingsKeyMap struct {
Dismiss key.Binding Dismiss key.Binding
DismissAll key.Binding
Flag key.Binding
} }
func newFindingsKeyMap(cfg config.FindingsKeys) FindingsKeyMap { func newFindingsKeyMap(cfg config.FindingsKeys) FindingsKeyMap {
return FindingsKeyMap{ return FindingsKeyMap{
Dismiss: binding(cfg.Dismiss, "dismiss"), Dismiss: binding(cfg.Dismiss, "dismiss"),
DismissAll: binding(cfg.DismissAll, "dismiss all"),
Flag: binding(cfg.Flag, "flag"),
} }
} }
func (f FindingsKeyMap) Bindings() []key.Binding { func (f FindingsKeyMap) Bindings() []key.Binding {
return []key.Binding{f.Dismiss} return []key.Binding{f.Flag, f.Dismiss, f.DismissAll}
} }
+3 -1
View File
@@ -13,6 +13,7 @@ type ReplayKeyMap struct {
Delete key.Binding Delete key.Binding
DeleteAll key.Binding DeleteAll key.Binding
Filter key.Binding Filter key.Binding
Flag key.Binding
} }
func newReplayKeyMap(cfg config.ReplayKeys) ReplayKeyMap { func newReplayKeyMap(cfg config.ReplayKeys) ReplayKeyMap {
@@ -24,9 +25,10 @@ func newReplayKeyMap(cfg config.ReplayKeys) ReplayKeyMap {
Delete: binding(cfg.Delete, "delete"), Delete: binding(cfg.Delete, "delete"),
DeleteAll: binding(cfg.DeleteAll, "delete all"), DeleteAll: binding(cfg.DeleteAll, "delete all"),
Filter: binding(cfg.Filter, "filter"), Filter: binding(cfg.Filter, "filter"),
Flag: binding(cfg.Flag, "flag"),
} }
} }
func (r ReplayKeyMap) Bindings() []key.Binding { func (r ReplayKeyMap) Bindings() []key.Binding {
return []key.Binding{r.Send, r.Edit, r.EditExt, r.UndoEdits, r.Delete, r.DeleteAll, r.Filter} return []key.Binding{r.Send, r.Edit, r.EditExt, r.UndoEdits, r.Flag, r.Delete, r.DeleteAll, r.Filter}
} }
+1 -1
View File
@@ -186,7 +186,7 @@ type findingsKeyMap struct{ width int }
func (findingsKeyMap) ShortHelp() []key.Binding { func (findingsKeyMap) ShortHelp() []key.Binding {
g := keys.Keys.Global g := keys.Keys.Global
f := keys.Keys.Findings f := keys.Keys.Findings
return []key.Binding{g.Up, g.Down, f.Dismiss, g.Copy, g.Help} return []key.Binding{g.Up, g.Down, f.Flag, f.Dismiss, f.DismissAll, g.Copy, g.Help}
} }
func (m findingsKeyMap) FullHelp() [][]key.Binding { func (m findingsKeyMap) FullHelp() [][]key.Binding {
+16
View File
@@ -72,6 +72,14 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.refreshListViewport() m.refreshListViewport()
m.refreshBody() m.refreshBody()
} }
case key.Matches(msg, f.Flag):
if len(m.findings) > 0 && m.database != nil {
if err := m.database.ToggleFindingFlag(m.findings[m.cursor].ID); err != nil {
log.Printf("findings: toggle flag: %v", err)
return m, nil
}
return m, RefreshCmd(m.database)
}
case key.Matches(msg, f.Dismiss): case key.Matches(msg, f.Dismiss):
if len(m.findings) > 0 && m.database != nil { if len(m.findings) > 0 && m.database != nil {
if err := m.database.DismissFinding(m.findings[m.cursor].ID); err != nil { if err := m.database.DismissFinding(m.findings[m.cursor].ID); err != nil {
@@ -80,6 +88,14 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
return m, RefreshCmd(m.database) return m, RefreshCmd(m.database)
} }
case key.Matches(msg, f.DismissAll):
if m.database != nil {
if err := m.database.DismissAllFindings(); err != nil {
log.Printf("findings: dismiss all: %v", err)
return m, nil
}
return m, RefreshCmd(m.database)
}
case key.Matches(msg, g.ScrollUp): case key.Matches(msg, g.ScrollUp):
util.ScrollViewport(&m.bodyViewport, -1) util.ScrollViewport(&m.bodyViewport, -1)
case key.Matches(msg, g.ScrollDown): case key.Matches(msg, g.ScrollDown):
+12 -1
View File
@@ -66,9 +66,10 @@ func (m *Model) renderList() string {
sevStyle := style.SeverityStyle(f.Severity) sevStyle := style.SeverityStyle(f.Severity)
sevLabel := sevStyle.Width(8).Render(f.Severity) sevLabel := sevStyle.Width(8).Render(f.Severity)
ts := f.CreatedAt.Format("15:04:05") ts := f.CreatedAt.Format("15:04:05")
flagSt := lipgloss.NewStyle().Foreground(ilovetui.S.Primary)
w := m.listViewport.Width() w := m.listViewport.Width()
const fixedW = 2 + 8 + 1 + 8 + 1 + 10 + 1 const fixedW = 2 + 2 + 8 + 1 + 8 + 1 + 10 + 1
titleW := w - fixedW titleW := w - fixedW
if titleW < 0 { if titleW < 0 {
titleW = 0 titleW = 0
@@ -79,8 +80,13 @@ func (m *Model) renderList() string {
var line string var line string
if selected { if selected {
bg := lipgloss.NewStyle().Background(ilovetui.S.Selection) bg := lipgloss.NewStyle().Background(ilovetui.S.Selection)
flagStr := " "
if f.Flagged {
flagStr = icons.I.Flag + " "
}
line = lipgloss.JoinHorizontal(lipgloss.Top, line = lipgloss.JoinHorizontal(lipgloss.Top,
bg.Bold(true).Foreground(ilovetui.S.Primary).Width(2).Render(">"), bg.Bold(true).Foreground(ilovetui.S.Primary).Width(2).Render(">"),
bg.Foreground(ilovetui.S.Primary).Width(2).Render(flagStr),
sevStyle.Background(ilovetui.S.Selection).Width(8).Render(f.Severity), sevStyle.Background(ilovetui.S.Selection).Width(8).Render(f.Severity),
bg.Width(1).Render(""), bg.Width(1).Render(""),
bg.Foreground(ilovetui.S.Subtle).Width(8).Render(util.Truncate(f.PluginName, 8)), bg.Foreground(ilovetui.S.Subtle).Width(8).Render(util.Truncate(f.PluginName, 8)),
@@ -90,8 +96,13 @@ func (m *Model) renderList() string {
bg.Bold(true).Width(titleW).Render(f.Title), bg.Bold(true).Width(titleW).Render(f.Title),
) )
} else { } else {
flagStr := " "
if f.Flagged {
flagStr = icons.I.Flag + " "
}
line = lipgloss.JoinHorizontal(lipgloss.Top, line = lipgloss.JoinHorizontal(lipgloss.Top,
" ", " ",
flagSt.Width(2).Render(flagStr),
sevLabel, sevLabel,
" ", " ",
pluginStr, pluginStr,
-6
View File
@@ -120,9 +120,6 @@ func (m *Model) renderList() string {
flagStr := " " flagStr := " "
if e.Flagged { if e.Flagged {
flagStr = icons.I.Flag + " " flagStr = icons.I.Flag + " "
if icons.I.Flag == "" {
flagStr = "★ "
}
} }
line = lipgloss.JoinHorizontal(lipgloss.Top, line = lipgloss.JoinHorizontal(lipgloss.Top,
bg.Bold(true).Foreground(ilovetui.S.Primary).Width(2).Render(">"), bg.Bold(true).Foreground(ilovetui.S.Primary).Width(2).Render(">"),
@@ -139,9 +136,6 @@ func (m *Model) renderList() string {
flagStr := " " flagStr := " "
if e.Flagged { if e.Flagged {
flagStr = icons.I.Flag + " " flagStr = icons.I.Flag + " "
if icons.I.Flag == "" {
flagStr = "★ "
}
} }
line = lipgloss.JoinHorizontal(lipgloss.Top, line = lipgloss.JoinHorizontal(lipgloss.Top,
" ", " ",
+3 -1
View File
@@ -33,6 +33,7 @@ type Entry struct {
ResponseRaw string // filled after send ResponseRaw string // filled after send
StatusCode int // 0 = not sent yet StatusCode int // 0 = not sent yet
Sending bool Sending bool
Flagged bool
Err error Err error
} }
@@ -148,6 +149,7 @@ func entryFromDB(dbe db.ReplayEntry) Entry {
RequestRaw: dbe.RequestRaw, RequestRaw: dbe.RequestRaw,
ResponseRaw: dbe.ResponseRaw, ResponseRaw: dbe.ResponseRaw,
StatusCode: dbe.StatusCode, StatusCode: dbe.StatusCode,
Flagged: dbe.Flagged,
Err: err, Err: err,
} }
} }
@@ -277,7 +279,7 @@ type replayKeyMap struct{ width int }
func (replayKeyMap) ShortHelp() []key.Binding { func (replayKeyMap) ShortHelp() []key.Binding {
g := keys.Keys.Global g := keys.Keys.Global
r := keys.Keys.Replay r := keys.Keys.Replay
return []key.Binding{g.Up, g.Down, g.CycleFocus, r.Send, r.Edit, r.Filter, g.Help} return []key.Binding{g.Up, g.Down, g.CycleFocus, r.Send, r.Edit, r.Flag, r.Filter, g.Help}
} }
func (m replayKeyMap) FullHelp() [][]key.Binding { func (m replayKeyMap) FullHelp() [][]key.Binding {
+33 -2
View File
@@ -241,6 +241,18 @@ func (m Model) updateNormalMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
m.requestViewport.ScrollRight(6) m.requestViewport.ScrollRight(6)
m.responseViewport.ScrollRight(6) m.responseViewport.ScrollRight(6)
case key.Matches(msg, r.Flag):
if len(m.entries) > 0 {
allIdx := m.currentAllIdx()
if m.database != nil && m.allEntries[allIdx].DBID != 0 {
if err := m.database.ToggleReplayFlag(m.allEntries[allIdx].DBID); err != nil {
log.Printf("replay: toggle flag: %v", err)
}
}
m.allEntries[allIdx].Flagged = !m.allEntries[allIdx].Flagged
m.applyFilter(allIdx)
}
case key.Matches(msg, r.Delete): case key.Matches(msg, r.Delete):
if len(m.entries) > 0 { if len(m.entries) > 0 {
allIdx := m.currentAllIdx() allIdx := m.currentAllIdx()
@@ -260,11 +272,29 @@ func (m Model) updateNormalMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
case key.Matches(msg, r.DeleteAll): case key.Matches(msg, r.DeleteAll):
if m.database != nil { if m.database != nil {
if err := m.database.DeleteAllReplayEntries(); err != nil { if err := m.database.DeleteAllReplayEntriesExceptFlagged(); err != nil {
log.Printf("replay: delete all entries: %v", err) log.Printf("replay: delete all entries: %v", err)
} }
} }
m.allEntries = nil // Mirror the DB logic in memory: delete unflagged first; if none, delete all.
hasUnflagged := false
for _, e := range m.allEntries {
if !e.Flagged {
hasUnflagged = true
break
}
}
if hasUnflagged {
filtered := m.allEntries[:0]
for _, e := range m.allEntries {
if e.Flagged {
filtered = append(filtered, e)
}
}
m.allEntries = filtered
} else {
m.allEntries = nil
}
m.cursor = 0 m.cursor = 0
m.filterActive = false m.filterActive = false
m.filterAccepted = false m.filterAccepted = false
@@ -551,6 +581,7 @@ func entryToDB(e Entry) db.ReplayEntry {
ResponseRaw: e.ResponseRaw, ResponseRaw: e.ResponseRaw,
StatusCode: e.StatusCode, StatusCode: e.StatusCode,
ErrorMsg: errMsg, ErrorMsg: errMsg,
Flagged: e.Flagged,
} }
} }
+12 -1
View File
@@ -106,19 +106,25 @@ func (m *Model) renderList() string {
selBg := ilovetui.S.Selection selBg := ilovetui.S.Selection
w := m.listViewport.Width() w := m.listViewport.Width()
const fixedW = 2 + 7 + 1 + 3 + 1 const fixedW = 2 + 2 + 7 + 1 + 3 + 1
hostPathW := w - fixedW hostPathW := w - fixedW
if hostPathW < 0 { if hostPathW < 0 {
hostPathW = 0 hostPathW = 0
} }
statusStr, statusSt := entryStatus(e) statusStr, statusSt := entryStatus(e)
flagSt := lipgloss.NewStyle().Foreground(ilovetui.S.Primary)
var line string var line string
if selected { if selected {
bg := lipgloss.NewStyle().Background(selBg) bg := lipgloss.NewStyle().Background(selBg)
flagStr := " "
if e.Flagged {
flagStr = icons.I.Flag + " "
}
line = lipgloss.JoinHorizontal(lipgloss.Top, line = lipgloss.JoinHorizontal(lipgloss.Top,
bg.Bold(true).Foreground(ilovetui.S.Primary).Width(2).Render(">"), bg.Bold(true).Foreground(ilovetui.S.Primary).Width(2).Render(">"),
bg.Foreground(ilovetui.S.Primary).Width(2).Render(flagStr),
style.S.Method(e.Method).Background(selBg).Render(e.Method), style.S.Method(e.Method).Background(selBg).Render(e.Method),
bg.Width(1).Render(""), bg.Width(1).Render(""),
statusSt.Background(selBg).Render(statusStr), statusSt.Background(selBg).Render(statusStr),
@@ -126,8 +132,13 @@ func (m *Model) renderList() string {
bg.Bold(true).Width(hostPathW).Render(e.Host+e.Path), bg.Bold(true).Width(hostPathW).Render(e.Host+e.Path),
) )
} else { } else {
flagStr := " "
if e.Flagged {
flagStr = icons.I.Flag + " "
}
line = lipgloss.JoinHorizontal(lipgloss.Top, line = lipgloss.JoinHorizontal(lipgloss.Top,
" ", " ",
flagSt.Width(2).Render(flagStr),
style.S.Method(e.Method).Render(e.Method), style.S.Method(e.Method).Render(e.Method),
" ", " ",
statusSt.Render(statusStr), statusSt.Render(statusStr),