package main import ( "os/exec" "strings" tea "charm.land/bubbletea/v2" ) type Service struct { Name string Desc string Active string } type ServicesLoadedMsg struct { services []Service err error } func LoadServices() tea.Msg { cmd := exec.Command("systemctl", "list-units", "--type=service", "--all", "--no-pager", "--plain") out, err := cmd.Output() if err != nil { return ServicesLoadedMsg{err: err} } services := parseServices(out) return ServicesLoadedMsg{services: services} } func parseServices(out []byte) []Service { lines := strings.Split(string(out), "\n") var services []Service for i, line := range lines { if i == 0 { continue } line = strings.TrimSpace(line) if line == "" { continue } if strings.HasPrefix(line, "Legend:") { return services } fields := strings.Fields(line) if len(fields) < 5 { continue } services = append(services, Service{ Name: fields[0], Active: fields[2], Desc: strings.Join(fields[4:], " "), }) } return services } func (m *model) filterServices() { q := strings.ToLower(strings.TrimSpace(m.query.Value())) if q == "" { m.filtered = slicesClone(m.services) } else { var filtered []Service for _, s := range m.services { if matchesService(s, q) { filtered = append(filtered, s) } } m.filtered = filtered } if len(m.filtered) == 0 { m.cursor = 0 m.offset = 0 return } if m.cursor >= len(m.filtered) { m.cursor = len(m.filtered) - 1 m.offset = max(0, m.cursor-m.amount) } } func matchesService(s Service, q string) bool { return strings.Contains(strings.ToLower(s.Name), q) || strings.Contains(strings.ToLower(s.Active), q) || strings.Contains(strings.ToLower(s.Desc), q) } func slicesClone(in []Service) []Service { out := make([]Service, len(in)) copy(out, in) return out } func (m *model) StopService() { s := m.filtered[m.cursor] if s.Active != "active" { return } cmd := exec.Command("systemctl", "stop", s.Name) cmd.Run() } func (m *model) StartService() { s := m.filtered[m.cursor] if s.Active != "active" { cmd := exec.Command("systemctl", "start", s.Name) cmd.Run() } }