diff --git a/.gitignore b/.gitignore index de1a653..873a0b6 100644 --- a/.gitignore +++ b/.gitignore @@ -107,4 +107,5 @@ go.work.sum .env config.toml +devices.json old diff --git a/apis/networking/main.go b/apis/networking/main.go new file mode 100644 index 0000000..006cd45 --- /dev/null +++ b/apis/networking/main.go @@ -0,0 +1,96 @@ +package networking + +import ( + "bytes" + "encoding/json" + "os" + "os/exec" + "strings" +) + +const deviceFile = "devices.json" + +type Device struct { + Name string `json:"name"` + IP string `json:"ip"` + Interface string `json:"interface"` + Icon string `json:"icon"` +} + +func GetDevices() map[string]map[string]Device { + result := make(map[string]map[string]Device) + devices := make(map[string]Device) + arpDevices := arpDevices() + knownDevices := knownDevices() + for mac, device := range arpDevices { + if strings.HasPrefix(device.Interface, "macvlan") { + continue + } + if known, ok := knownDevices[mac]; ok { + devices[mac] = Device{Name: known.Name, IP: device.IP, Interface: device.Interface, Icon: known.Icon} + } else { + devices[mac] = device + } + } + for mac, device := range knownDevices { + if _, ok := devices[mac]; !ok { + devices[mac] = Device{Name: device.Name, IP: device.IP, Interface: "offline", Icon: device.Icon} + } + } + writeDevices(devices) + + for mac, device := range devices { + if _, ok := result[device.Interface]; !ok { + result[device.Interface] = make(map[string]Device) + } + if strings.HasPrefix(device.Interface, "br") { + device.Icon = "\uE7B0" + device.Interface = "bridge" + } + if device.Icon == "" { + device.Icon = "\U000F0C8A" + } + result[device.Interface][mac] = device + } + + return result +} + +func writeDevices(devices map[string]Device) { + marshal, err := json.MarshalIndent(devices, "", " ") + if err != nil { + } + if err := os.WriteFile(deviceFile, marshal, 0644); err != nil { + panic(err) + } +} + +func knownDevices() map[string]Device { + knownDevices := make(map[string]Device) + file, err := os.ReadFile(deviceFile) + if err == nil { + if err := json.Unmarshal(file, &knownDevices); err != nil { + panic(err) + } + } + return knownDevices +} + +func arpDevices() map[string]Device { + arp := exec.Command("arp", "-a") + var out bytes.Buffer + arp.Stdout = &out + if err := arp.Run(); err != nil { + panic(err) + } + res := out.String() + lines := strings.Split(res, "\n") + devices := make(map[string]Device) + for l := range lines { + entry := strings.Fields(lines[l]) + if len(entry) > 6 { + devices[entry[3]] = Device{Name: entry[0], IP: strings.ReplaceAll(strings.ReplaceAll(entry[1], "(", ""), ")", ""), Interface: entry[6]} + } + } + return devices +} diff --git a/main.go b/main.go index 973b39c..000f8ca 100644 --- a/main.go +++ b/main.go @@ -36,6 +36,7 @@ func initWidgets(ctx context.Context, term *tcell.Terminal) { widgets.New("Date", widgets.Date()), widgets.New("ChangeTitle", widgets.TextInput(titleUpdate, textinput.Label("Update Title: "), textinput.PlaceHolder("New Title"))), widgets.New("Wifi", widgets.WifiQRCode()), + widgets.New("NetworkDevices", widgets.NetworkDevices()), widgets.New("Docker", widgets.DockerList()), widgets.New("PiHole", widgets.PiholeStats()), widgets.New("PiHoleBlocked", widgets.PiholeBlocked()), @@ -52,6 +53,12 @@ func layout() []container.Option { container.Border(linestyle.Light), container.BorderColor(cell.ColorWhite)), ), + grid.RowHeightFixed(44, + grid.Widget(widgets.Get["NetworkDevices"], + container.BorderTitle("Network Devices"), + container.Border(linestyle.Light), + container.BorderColor(cell.ColorWhite)), + ), grid.RowHeightFixed(1, grid.Widget(widgets.Get["empty"]), ), diff --git a/widgets/networkdevices.go b/widgets/networkdevices.go new file mode 100644 index 0000000..62928cb --- /dev/null +++ b/widgets/networkdevices.go @@ -0,0 +1,70 @@ +package widgets + +import ( + "ArinDash/apis/networking" + "ArinDash/util" + "context" + "fmt" + "sort" + "strings" + "time" + + "github.com/mum4k/termdash/cell" + "github.com/mum4k/termdash/terminal/terminalapi" + "github.com/mum4k/termdash/widgetapi" + "github.com/mum4k/termdash/widgets/text" +) + +type NetworkDevicesOptions struct { +} + +func NetworkDevices() NetworkDevicesOptions { + widgetOptions["NetworkDevicesOptions"] = createNetworkDevicesList + return NetworkDevicesOptions{} +} + +func createNetworkDevicesList(ctx context.Context, _ terminalapi.Terminal, _ interface{}) widgetapi.Widget { + list := util.PanicOnErrorWithResult(text.New()) + go util.Periodic(ctx, 20*time.Second, func() error { + interfaces := networking.GetDevices() + list.Reset() + for iface, devices := range interfaces { + status := cell.ColorGreen + if iface == "offline" { + status = cell.ColorGray + } + if err := list.Write(fmt.Sprintf("=== %s ===\n", iface)); err != nil { + return err + } + for _, mac := range sortedKeys(devices) { + if err := list.Write(fmt.Sprintf("|%-15s|", mac), text.WriteCellOpts(cell.FgColor(cell.ColorGray))); err != nil { + return err + } + if err := list.Write(fmt.Sprintf("%2s ", devices[mac].Icon), text.WriteCellOpts(cell.FgColor(status))); err != nil { + return err + } + if err := list.Write(fmt.Sprintf(" %s", devices[mac].Name), text.WriteCellOpts(cell.FgColor(cell.ColorWhite))); err != nil { + return err + } + if err := list.Write(fmt.Sprintf(" (%s)\n", devices[mac].IP), text.WriteCellOpts(cell.FgColor(cell.ColorGray))); err != nil { + return err + } + } + } + + return nil + }) + + return list +} + +func sortedKeys(devices map[string]networking.Device) []string { + macs := make([]string, 0, len(devices)) + for mac := range devices { + macs = append(macs, mac) + } + sort.SliceStable(macs, func(i, j int) bool { + return strings.Compare(devices[macs[i]].Name, devices[macs[j]].Name) < 0 + }) + return macs +}