2025-10-10 22:43:47 -04:00
package tui
import (
"fmt"
"rmm-hunter/internal/suspicious"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
// RequestEliminateMsg is emitted by the detail view when '!' is pressed
type RequestEliminateMsg struct {
TypeKey string
Index int
}
// DeletedMsg is emitted after successful elimination to update lists
type DeletedMsg struct {
TypeKey string
Index int
}
type DetailViewModel struct {
2025-10-11 21:01:07 -04:00
typeKey string
index int
data suspicious . Suspicious
eliminated map [ string ] map [ int ] bool
2025-10-10 22:53:20 -04:00
// When modal* != "", show modal and require ESC to dismiss
modalErr string
modalWarn string
2025-10-10 22:43:47 -04:00
}
2025-10-11 21:01:07 -04:00
func NewDetailView ( typeKey string , index int , data suspicious . Suspicious , eliminated map [ string ] map [ int ] bool ) DetailViewModel {
return DetailViewModel { typeKey : typeKey , index : index , data : data , eliminated : eliminated }
2025-10-10 22:43:47 -04:00
}
func ( m DetailViewModel ) Init ( ) tea . Cmd { return nil }
func ( m DetailViewModel ) Update ( msg tea . Msg ) ( tea . Model , tea . Cmd ) {
switch v := msg . ( type ) {
case tea . KeyMsg :
2025-10-10 22:53:20 -04:00
if m . modalErr != "" || m . modalWarn != "" {
2025-10-10 22:43:47 -04:00
// Modal active: only ESC dismisses
if v . String ( ) == "esc" {
m . modalErr = ""
2025-10-10 22:53:20 -04:00
m . modalWarn = ""
2025-10-10 22:43:47 -04:00
}
return m , nil
}
switch v . String ( ) {
case "left" :
return m , func ( ) tea . Msg { return BackMsg { } }
case "!" :
return m , func ( ) tea . Msg { return RequestEliminateMsg { TypeKey : m . typeKey , Index : m . index } }
case "q" , "esc" , "ctrl+c" :
return m , tea . Quit
}
}
return m , nil
}
func ( m DetailViewModel ) View ( ) string {
2025-10-11 21:01:07 -04:00
isEliminated := m . eliminated [ m . typeKey ] != nil && m . eliminated [ m . typeKey ] [ m . index ]
var title string
if isEliminated {
title = lipgloss . NewStyle ( ) . Bold ( true ) . Foreground ( lipgloss . Color ( "10" ) ) . Render ( "Details — ELIMINATED — Left to go back, q to quit" )
} else {
title = lipgloss . NewStyle ( ) . Bold ( true ) . Render ( "Details — press ! to eliminate, Left to go back, q to quit" )
}
2025-10-10 22:43:47 -04:00
body := m . renderDetails ( )
2025-10-11 21:01:07 -04:00
// Apply green styling to body if eliminated
if isEliminated {
body = lipgloss . NewStyle ( ) . Foreground ( lipgloss . Color ( "10" ) ) . Render ( body )
}
2025-10-10 22:43:47 -04:00
view := title + "\n\n" + body
2025-10-10 22:53:20 -04:00
if m . modalWarn != "" {
modal := lipgloss . NewStyle ( ) . Padding ( 1 , 2 ) . Foreground ( lipgloss . Color ( "214" ) ) . Border ( lipgloss . RoundedBorder ( ) ) . Render ( "Warning:\n" + m . modalWarn + "\n\nPress ESC to dismiss" )
view += "\n\n" + modal
}
2025-10-10 22:43:47 -04:00
if m . modalErr != "" {
modal := lipgloss . NewStyle ( ) . Padding ( 1 , 2 ) . Foreground ( lipgloss . Color ( "203" ) ) . Border ( lipgloss . RoundedBorder ( ) ) . Render ( "Elimination failed:\n" + m . modalErr + "\n\nPress ESC to dismiss" )
view += "\n\n" + modal
}
return view
}
func ( m DetailViewModel ) renderDetails ( ) string {
switch m . typeKey {
case "autoruns" :
2025-10-11 21:01:07 -04:00
if m . index >= len ( m . data . AutoRuns ) {
return "Item no longer available"
}
2025-10-10 22:43:47 -04:00
ar := m . data . AutoRuns [ m . index ]
2025-10-11 15:15:35 -04:00
return fmt . Sprintf ( "Type: %s\nEntry: %s\nLaunch: %s\nLocation: %s\nImage: %s\nArgs: %s\nMD5: %s\nSHA1: %s\nSHA256: %s" , ar . Type , ar . Entry , ar . LaunchString , ar . Location , ar . ImagePath , ar . Arguments , ar . MD5 , ar . SHA1 , ar . SHA256 )
2025-10-10 22:43:47 -04:00
case "binaries" :
2025-10-11 21:01:07 -04:00
if m . index >= len ( m . data . Binaries ) {
return "Item no longer available"
}
2025-10-10 22:43:47 -04:00
b := m . data . Binaries [ m . index ]
2025-10-11 21:01:07 -04:00
return fmt . Sprintf ( "Binary: %s\nAction: delete file" , b . Path )
2025-10-10 22:43:47 -04:00
case "connections" :
2025-10-11 21:01:07 -04:00
if m . index >= len ( m . data . OutboundConnections ) {
return "Item no longer available"
}
2025-10-10 22:43:47 -04:00
c := m . data . OutboundConnections [ m . index ]
return fmt . Sprintf ( "Local: %s\nRemote: %s\nHost: %s\nState: %s\nPID: %s\nProcess: %s\nAction: add firewall block (placeholder)" , c . LocalAddr , c . RemoteAddr , c . RemoteHost , c . State , c . PID , c . Process )
case "directories" :
2025-10-11 21:01:07 -04:00
if m . index >= len ( m . data . Directories ) {
return "Item no longer available"
}
2025-10-10 22:43:47 -04:00
d := m . data . Directories [ m . index ]
2025-10-11 21:01:07 -04:00
return fmt . Sprintf ( "Directory: %s\nAction: delete recursively" , d . Path )
2025-10-10 22:43:47 -04:00
case "processes" :
2025-10-11 21:01:07 -04:00
if m . index >= len ( m . data . Processes ) {
return "Item no longer available"
}
2025-10-10 22:43:47 -04:00
p := m . data . Processes [ m . index ]
return fmt . Sprintf ( "Name: %s\nPID: %d\nPPID: %d\nParent: %s\nArgs: %s\nCreated: %s\nPath: %s\nAction: stop then delete (placeholder)" , p . Name , p . PID , p . PPID , p . Parent , p . Args , p . Created , p . Path )
case "scheduledTasks" :
2025-10-11 21:01:07 -04:00
if m . index >= len ( m . data . ScheduledTasks ) {
return "Item no longer available"
}
2025-10-10 22:43:47 -04:00
t := m . data . ScheduledTasks [ m . index ]
return fmt . Sprintf ( "Name: %s\nAuthor: %s\nState: %s\nEnabled: %v\nLastResult: %s\nNextRun: %s\nLastRun: %s\nPath: %s\nAction: disable then delete (placeholder)" , t . Name , t . Author , t . State , t . Enabled , t . LastResult , t . NextRun , t . LastRun , t . Path )
case "services" :
2025-10-11 21:01:07 -04:00
if m . index >= len ( m . data . Services ) {
return "Item no longer available"
}
2025-10-10 22:43:47 -04:00
s := m . data . Services [ m . index ]
return fmt . Sprintf ( "Name: %s\nDisplay: %s\nType: %s\nStartType: %s\nBinPath: %s\nStartName: %s\nDescription: %s\nAction: stop then delete (placeholder)" , s . Name , s . DisplayName , s . ServiceType , s . StartType , s . BinaryPathName , s . ServiceStartName , s . Description )
}
return ""
}