Finished the app
This commit is contained in:
parent
309e463578
commit
42a0e33b0d
148
src/gui.rs
Normal file
148
src/gui.rs
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
//! This module handles rendering parsed data to a window
|
||||||
|
|
||||||
|
use std::{collections::HashMap, sync::mpsc::Receiver};
|
||||||
|
|
||||||
|
use eframe::{egui, epi};
|
||||||
|
|
||||||
|
use crate::ipc::{MdnsPacket, Service};
|
||||||
|
|
||||||
|
/// Defines a single host record
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct HostRecord {
|
||||||
|
pub name: String,
|
||||||
|
pub services: HashMap<String, Vec<Service>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This is the GUI, and everything it needs to keep state
|
||||||
|
pub struct BrowseApp {
|
||||||
|
/// The data stream coming from the subprocess
|
||||||
|
packet_stream: Receiver<MdnsPacket>,
|
||||||
|
|
||||||
|
/// The currently rendered hosts
|
||||||
|
current_hosts: HashMap<String, HashMap<String, HostRecord>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BrowseApp {
|
||||||
|
pub fn new(packet_stream: Receiver<MdnsPacket>) -> Self {
|
||||||
|
Self {
|
||||||
|
packet_stream,
|
||||||
|
current_hosts: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl epi::App for BrowseApp {
|
||||||
|
fn update(&mut self, ctx: &eframe::egui::CtxRef, frame: &mut eframe::epi::Frame<'_>) {
|
||||||
|
// Poll the packet stream for new data, and update the hosts list accordingly
|
||||||
|
for packet in self.packet_stream.try_iter() {
|
||||||
|
match packet.mode {
|
||||||
|
crate::ipc::PacketMode::New => {
|
||||||
|
// Create the interface if not already existing
|
||||||
|
if !self.current_hosts.contains_key(&packet.interface_name) {
|
||||||
|
self.current_hosts
|
||||||
|
.insert(packet.interface_name.clone(), HashMap::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the host to the interface
|
||||||
|
self.current_hosts
|
||||||
|
.get_mut(&packet.interface_name)
|
||||||
|
.unwrap()
|
||||||
|
.insert(
|
||||||
|
packet.hostname.clone(),
|
||||||
|
HostRecord {
|
||||||
|
name: packet.hostname.clone(),
|
||||||
|
services: match packet.service {
|
||||||
|
Some(service) => {
|
||||||
|
let mut services = HashMap::new();
|
||||||
|
services.insert(service.name.clone(), vec![service]);
|
||||||
|
services
|
||||||
|
}
|
||||||
|
None => HashMap::new(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
crate::ipc::PacketMode::Update => {
|
||||||
|
// Get the interface
|
||||||
|
let interface = self.current_hosts.get_mut(&packet.interface_name).unwrap();
|
||||||
|
|
||||||
|
// Get the host
|
||||||
|
let host = interface.get_mut(&packet.hostname).unwrap();
|
||||||
|
|
||||||
|
// Only update the host if the new service is real
|
||||||
|
if let Some(service) = packet.service {
|
||||||
|
// If the service name does not exist, create the service on the host
|
||||||
|
if !host.services.contains_key(&service.name) {
|
||||||
|
host.services.insert(service.name.clone(), vec![service]);
|
||||||
|
} else {
|
||||||
|
// Update an existing service if the address is not already used
|
||||||
|
let services = host.services.get_mut(&service.name).unwrap();
|
||||||
|
if !services.iter().any(|s| {
|
||||||
|
format!("{} {}:{}", s.hostname, s.ip, s.port)
|
||||||
|
== format!(
|
||||||
|
"{} {}:{}",
|
||||||
|
service.hostname, service.ip, service.port
|
||||||
|
)
|
||||||
|
}) {
|
||||||
|
services.push(service);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
crate::ipc::PacketMode::Remove => {
|
||||||
|
// TODO: I don't want to actually remove data from the view. Maybe this can get implemented in the future?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a menu bar with quit button
|
||||||
|
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
|
||||||
|
egui::menu::bar(ui, |ui| {
|
||||||
|
egui::menu::menu(ui, "File", |ui| {
|
||||||
|
if ui.button("Quit").clicked() {
|
||||||
|
frame.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a scrollable area to display the packet data
|
||||||
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
|
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||||
|
// Split the GUI into interfaces
|
||||||
|
for (interface, hosts) in &self.current_hosts {
|
||||||
|
// Create a header for the interface
|
||||||
|
ui.add(egui::Label::new(interface).heading());
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
// Render each host
|
||||||
|
for host in hosts.values() {
|
||||||
|
// Render the packet data
|
||||||
|
egui::CollapsingHeader::new(&host.name).show(ui, |ui| {
|
||||||
|
// Render every service name as a dropdown
|
||||||
|
for service_name in host.services.keys() {
|
||||||
|
egui::CollapsingHeader::new(service_name).show(ui, |ui| {
|
||||||
|
// Render provider for the service as its own dropdown, containing optional metadata
|
||||||
|
for service in host.services.get(service_name).unwrap() {
|
||||||
|
egui::CollapsingHeader::new(format!(
|
||||||
|
"{} (IP: {} Port: {})",
|
||||||
|
service.hostname, service.ip, service.port
|
||||||
|
))
|
||||||
|
.show(ui, |ui| {
|
||||||
|
// Render the metadata
|
||||||
|
ui.label(service.data.join("\n"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"NetBrowse"
|
||||||
|
}
|
||||||
|
}
|
91
src/ipc.rs
91
src/ipc.rs
@ -3,7 +3,9 @@
|
|||||||
use std::{
|
use std::{
|
||||||
convert::TryFrom,
|
convert::TryFrom,
|
||||||
io::BufRead,
|
io::BufRead,
|
||||||
|
net::IpAddr,
|
||||||
process::{Child, Stdio},
|
process::{Child, Stdio},
|
||||||
|
str::FromStr,
|
||||||
sync::mpsc::{self, Receiver, Sender},
|
sync::mpsc::{self, Receiver, Sender},
|
||||||
thread::{self, JoinHandle},
|
thread::{self, JoinHandle},
|
||||||
};
|
};
|
||||||
@ -19,6 +21,10 @@ pub enum PacketParseError {
|
|||||||
IpTypeParseError(String),
|
IpTypeParseError(String),
|
||||||
#[error("Ran out of arguments")]
|
#[error("Ran out of arguments")]
|
||||||
NotEnoughArgsError,
|
NotEnoughArgsError,
|
||||||
|
#[error(transparent)]
|
||||||
|
AddrParseError(#[from] std::net::AddrParseError),
|
||||||
|
#[error(transparent)]
|
||||||
|
IntParseError(#[from] std::num::ParseIntError),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tries to unwrap a string from an arg iter, or just returns an error
|
/// Tries to unwrap a string from an arg iter, or just returns an error
|
||||||
@ -71,6 +77,21 @@ impl TryFrom<String> for IpType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The metadat for a single service
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Service {
|
||||||
|
/// The name of the service
|
||||||
|
pub name: String,
|
||||||
|
/// The mDNS hostname of the service
|
||||||
|
pub hostname: String,
|
||||||
|
/// The IP address serving the service
|
||||||
|
pub ip: IpAddr,
|
||||||
|
/// The port the service is listening on
|
||||||
|
pub port: u16,
|
||||||
|
/// All additional data
|
||||||
|
pub data: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
/// An information packet parsed from a `avahi-browse` subprocess
|
/// An information packet parsed from a `avahi-browse` subprocess
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct MdnsPacket {
|
pub struct MdnsPacket {
|
||||||
@ -86,8 +107,8 @@ pub struct MdnsPacket {
|
|||||||
pub service_type: String,
|
pub service_type: String,
|
||||||
/// The domain of the service
|
/// The domain of the service
|
||||||
pub domain: String,
|
pub domain: String,
|
||||||
/// Additional service metadata
|
/// The service defined in this packet
|
||||||
pub metadata: Vec<String>,
|
pub service: Option<Service>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<String> for MdnsPacket {
|
impl TryFrom<String> for MdnsPacket {
|
||||||
@ -98,26 +119,52 @@ impl TryFrom<String> for MdnsPacket {
|
|||||||
// Convert the incoming data into an iter over its components
|
// Convert the incoming data into an iter over its components
|
||||||
let mut iter = s.split(';');
|
let mut iter = s.split(';');
|
||||||
|
|
||||||
|
// Load the data before constructing the structure
|
||||||
|
// Grabs the first char of the first arg, and reads it as a mode
|
||||||
|
let mode = PacketMode::try_from(
|
||||||
|
try_unwrap_arg(iter.next())?
|
||||||
|
.chars()
|
||||||
|
.next()
|
||||||
|
.ok_or(PacketParseError::NotEnoughArgsError)?,
|
||||||
|
)?;
|
||||||
|
// Grabs the second arg, and reads it as an interface name
|
||||||
|
let interface_name = try_unwrap_arg(iter.next())?.to_string();
|
||||||
|
// Grabs the third arg, and reads it as an internet protocol type
|
||||||
|
let internet_protocol = IpType::try_from(try_unwrap_arg(iter.next())?.to_string())?;
|
||||||
|
// Grabs the fourth arg, and reads it as a hostname
|
||||||
|
let hostname = try_unwrap_arg(iter.next())?
|
||||||
|
.to_string()
|
||||||
|
.replace("\\.", ".")
|
||||||
|
.replace("\\0", " ");
|
||||||
|
// Grabs the fifth arg, and reads it as a service type
|
||||||
|
let service_type = try_unwrap_arg(iter.next())?.to_string();
|
||||||
|
// Grabs the sixth arg, and reads it as a domain
|
||||||
|
let domain = try_unwrap_arg(iter.next())?.to_string();
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
// Grabs the first char of the first arg, and reads it as a mode
|
mode,
|
||||||
mode: PacketMode::try_from(
|
interface_name,
|
||||||
try_unwrap_arg(iter.next())?
|
internet_protocol,
|
||||||
.chars()
|
hostname,
|
||||||
.next()
|
service_type: service_type.clone(),
|
||||||
.ok_or(PacketParseError::NotEnoughArgsError)?,
|
domain,
|
||||||
)?,
|
// Reads the remaining args as service data
|
||||||
// Grabs the second arg, and reads it as an interface name
|
// We do this by assuming there is a service, and turning any "no args" errors into a `None` value
|
||||||
interface_name: try_unwrap_arg(iter.next())?.to_string(),
|
service: match (|| -> Result<Service, PacketParseError> {
|
||||||
// Grabs the third arg, and reads it as an internet protocol type
|
Ok(Service {
|
||||||
internet_protocol: IpType::try_from(try_unwrap_arg(iter.next())?.to_string())?,
|
name: service_type,
|
||||||
// Grabs the fourth arg, and reads it as a hostname
|
hostname: try_unwrap_arg(iter.next())?.to_string(),
|
||||||
hostname: try_unwrap_arg(iter.next())?.to_string(),
|
ip: IpAddr::from_str(try_unwrap_arg(iter.next())?)?,
|
||||||
// Grabs the fifth arg, and reads it as a service type
|
port: u16::from_str(try_unwrap_arg(iter.next())?)?,
|
||||||
service_type: try_unwrap_arg(iter.next())?.to_string(),
|
data: iter.map(|s| s.to_string()).collect(),
|
||||||
// Grabs the sixth arg, and reads it as a domain
|
})
|
||||||
domain: try_unwrap_arg(iter.next())?.to_string(),
|
})() {
|
||||||
// Grabs the remaining args, and reads them as metadata
|
Ok(s) => Some(s),
|
||||||
metadata: iter.map(|s| s.to_string()).collect(),
|
Err(e) => {
|
||||||
|
// eprintln!("{}", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -161,7 +208,7 @@ impl AvahiSubprocess {
|
|||||||
match MdnsPacket::try_from(line.to_string()) {
|
match MdnsPacket::try_from(line.to_string()) {
|
||||||
Ok(packet) => {
|
Ok(packet) => {
|
||||||
// Send the packet to the main thread
|
// Send the packet to the main thread
|
||||||
println!("{:?}", packet);
|
// println!("{:?}", packet);
|
||||||
packet_sender
|
packet_sender
|
||||||
.send(packet)
|
.send(packet)
|
||||||
.expect("Failed to send packet to main thread");
|
.expect("Failed to send packet to main thread");
|
||||||
|
13
src/main.rs
13
src/main.rs
@ -65,10 +65,12 @@
|
|||||||
)]
|
)]
|
||||||
|
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
|
use eframe::{egui::Vec2, NativeOptions};
|
||||||
|
use gui::BrowseApp;
|
||||||
|
mod gui;
|
||||||
mod ipc;
|
mod ipc;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
|
||||||
// Check for avahi-browse on this system
|
// Check for avahi-browse on this system
|
||||||
if let Err(_) = which::which("avahi-browse") {
|
if let Err(_) = which::which("avahi-browse") {
|
||||||
eprintln!("{}", "`avahi-browse` was not found in system $PATH".red());
|
eprintln!("{}", "`avahi-browse` was not found in system $PATH".red());
|
||||||
@ -78,6 +80,9 @@ fn main() {
|
|||||||
// Spawn the subprocess
|
// Spawn the subprocess
|
||||||
let subprocess = ipc::AvahiSubprocess::spawn();
|
let subprocess = ipc::AvahiSubprocess::spawn();
|
||||||
|
|
||||||
loop{}
|
// Build the egui app and run it
|
||||||
|
let app = BrowseApp::new(subprocess.packet_stream);
|
||||||
}
|
let mut options = NativeOptions::default();
|
||||||
|
options.initial_window_size = Some(Vec2::new(500.0, 800.0));
|
||||||
|
eframe::run_native(Box::new(app), options);
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user