3 Commits
8 changed files with 284 additions and 217 deletions

No files matched your search

Generated
+1
View File
@@ -1316,6 +1316,7 @@ dependencies = [
"axum-extra",
"color-eyre",
"futures",
"lda-ipjs",
"macaddr",
"serde",
"serde_html_form",
+3
View File
@@ -36,3 +36,6 @@ very-smart-parsing = [
[workspace]
members = ["ipjs"]
[dependencies.lda-ipjs]
path = "ipjs"
+2 -2
View File
@@ -19,9 +19,9 @@
// prefix seems to be a cidr. both 6 and 4 works. idfk dog
use super::AddrInfo;
use super::AddrOutput;
pub async fn get(dev: Option<&str>) -> anyhow::Result<Vec<AddrInfo>> {
pub async fn get(dev: Option<&str>) -> anyhow::Result<Vec<AddrOutput>> {
let mut cmd = tokio::process::Command::new("ip");
cmd.args(["-j", "address", "show"]);
+5 -2
View File
@@ -10,6 +10,10 @@ use super::{NUDState, NeighborItem};
// i think ill write tokio::process every time tho (for this if let thing) because iterate through all ts youll have to as str and all the hooplas.
// it all turns to live osstr tho so ts just for my own sanity
// thiserror? anyhow
// what is vro sayin
// ahh. instead of using [wakey::utils::cmd::exec_command] which is ass we jus write everything out. so i dont have to .as_str() so often.
pub async fn get(
ip: Option<IpAddr>,
dev: Option<&str>,
@@ -23,8 +27,7 @@ pub async fn get(
};
if let Some(dev) = dev {
cmd.arg("dev");
cmd.arg(dev);
cmd.args(["dev", dev]);
}
for nud in nud {
cmd.arg("nud");
+2
View File
@@ -1,3 +1,5 @@
//! this is purely experimental. im not doing ts no mo
// hallo
use std::{
collections::{HashMap, HashSet},
-173
View File
@@ -1,173 +0,0 @@
use macaddr::MacAddr6;
use serde::Deserialize;
use std::{net::IpAddr, process::Command};
// Raw JSON shape from ip -j -4 address show
#[derive(Debug, Deserialize)]
struct IpJAddrInfo {
family: Option<String>,
local: Option<String>,
broadcast: Option<String>,
scope: Option<String>,
label: Option<String>,
prefixlen: Option<u8>,
// many more exist; we only take what we need
}
#[derive(Debug, Deserialize)]
struct IpJAddrEntry {
ifname: String,
// interface MAC address (present on non-loopback):
address: Option<String>,
addr_info: Option<Vec<IpJAddrInfo>>,
}
#[derive(Debug, Clone)]
pub struct AddrInfo {
pub local: IpAddr,
pub broadcast: Option<IpAddr>,
pub scope: Option<String>,
pub label: Option<String>,
pub prefixlen: Option<u8>,
}
#[derive(Debug, Clone)]
pub struct IpAEntry {
pub ifname: String,
pub mac: Option<MacAddr6>,
pub addr_info: Vec<AddrInfo>,
// IPv4 only (inet)
}
// Run ip -j -4 address show and deserialize
fn read_ip_json() -> Result<Vec<IpJAddrEntry>, Box<dyn std::error::Error>> {
let out = Command::new("ip")
.args(["-j", "-4", "address", "show"])
.output()?;
if !out.status.success() {
return Err(format!("ip exited with {}", out.status).into());
}
let entries: Vec<IpJAddrEntry> = serde_json::from_slice(&out.stdout)?;
Ok(entries)
}
fn parse_ip(s: &str) -> Option<IpAddr> {
s.parse::<IpAddr>().ok()
}
fn parse_mac(s: &str) -> Option<MacAddr6> {
// ip outputs lowercase aa:bb:..., which MacAddr6 can parse
s.parse::<MacAddr6>().ok()
}
// Public: read and convert to a cleaned model
pub fn get_ip_addr_entries() -> Result<Vec<IpAEntry>, Box<dyn std::error::Error>> {
let raw = read_ip_json()?;
let mut out = Vec::new();
for e in raw {
let mac = e.address.as_deref().and_then(parse_mac);
let mut infos = Vec::new();
if let Some(list) = e.addr_info {
for ai in list {
// keep only IPv4
if ai.family.as_deref() != Some("inet") {
continue;
}
if let Some(local) = ai.local.as_deref().and_then(parse_ip) {
let broadcast = ai.broadcast.as_deref().and_then(parse_ip);
infos.push(AddrInfo {
local,
broadcast,
scope: ai.scope,
label: ai.label,
prefixlen: ai.prefixlen,
});
}
}
}
out.push(IpAEntry {
ifname: e.ifname,
mac,
addr_info: infos,
});
}
Ok(out)
}
// Helper: first “good” (global) broadcast on an interface
pub fn broadcast_for_ifname(ifname: &str) -> Result<Option<IpAddr>, Box<dyn std::error::Error>> {
let entries = get_ip_addr_entries()?;
let dev = entries.into_iter().find(|e| e.ifname == ifname);
if let Some(dev) = dev {
// Prefer scope=global with a broadcast; else any broadcast
if let Some(b) = dev
.addr_info
.iter()
.filter(|ai| ai.scope.as_deref() == Some("global"))
.filter_map(|ai| ai.broadcast)
.next()
{
return Ok(Some(b));
}
if let Some(b) = dev.addr_info.iter().filter_map(|ai| ai.broadcast).next() {
return Ok(Some(b));
}
}
Ok(None)
}
// Helper: broadcast for a given interface MAC
pub fn broadcast_for_mac(
mac: MacAddr6,
) -> Result<Option<(String, IpAddr)>, Box<dyn std::error::Error>> {
let entries = get_ip_addr_entries()?;
for e in entries {
if let Some(m) = e.mac {
if m == mac {
// prefer global broadcast
if let Some(b) = e
.addr_info
.iter()
.filter(|ai| ai.scope.as_deref() == Some("global"))
.filter_map(|ai| ai.broadcast)
.next()
{
return Ok(Some((e.ifname, b)));
}
if let Some(b) = e.addr_info.iter().filter_map(|ai| ai.broadcast).next() {
return Ok(Some((e.ifname, b)));
}
}
}
}
Ok(None)
}
// Fallback: if nothing else, use limited broadcast (may be filtered on some networks)
pub fn fallback_broadcast() -> IpAddr {
// 255.255.255.255
IpAddr::from([255, 255, 255, 255])
}
// Convenience: all broadcasts grouped by interface (IPv4)
pub fn all_broadcasts() -> Result<Vec<(String, IpAddr)>, Box<dyn std::error::Error>> {
let Some(r) = Some(12) else {
e.addr_info
.iter()
.filter(|ai| ai.scope.as_deref() == Some("global"))
.filter_map(|ai| ai.broadcast)
};
let entries = get_ip_addr_entries()?;
let mut out = Vec::new();
for e in entries {
for ai in e.addr_info {
if let Some(b) = ai.broadcast {
out.push((e.ifname.clone(), b));
}
}
}
Ok(out)
}
+243 -31
View File
@@ -1,15 +1,22 @@
"thanks chatgpt"
from abc import ABC, abstractmethod
from collections.abc import Iterable
from functools import wraps
from pathlib import Path
from pprint import pprint
from typing import Callable, TypedDict, overload
root = Path(__file__).parent.parent
static = root / "static"
src = root / "src"
out_rs = src / "assets.rs"
assert 'name = "wakey"' in (root / "Cargo.toml").read_text(
encoding="utf-8"
), "uhh how do i explain this"
assert 'name = "wakey"' in (root / "Cargo.toml").read_text(encoding="utf-8"), (
"uhh how do i explain this"
)
def sanitize(name: str) -> str:
# Valid Rust identifiers: letters, digits, underscores; no starting digit
@@ -30,39 +37,244 @@ def indent(text: str, n: int) -> str:
return "\n".join(pad + line if line.strip() else line for line in text.splitlines())
class RsAssetFile:
def __init__(self, path: Path):
class RsAsset(ABC):
path: Path
@abstractmethod
def plain(self) -> str: ...
@abstractmethod
def macroed(self) -> str: ...
class RsAssetFile(RsAsset):
def __init__(self, path: Path, name: str | None = None):
self.path = path
if name is None:
self.name = sanitize(self.path.name).upper()
else:
self.name = name
def __str__(self):
const_name = sanitize(self.path.name).upper()
return (
f"pub const {const_name}: &str = "
f'include_str!("{self.path.relative_to(src, walk_up=True).as_posix()}");'
)
# specify walk_up to have .. in yo path
class RsAssetModule:
def __init__(self, folder: Path, body_only: bool = False):
self.folder = folder
self.full = not body_only
def __str__(self):
files = [str(RsAssetFile(s)) for s in self.folder.iterdir() if s.is_file()]
subs = [str(RsAssetModule(s)) for s in self.folder.iterdir() if s.is_dir()]
body = "\n".join(files + subs)
return (
f"pub mod {sanitize(self.folder.name).lower()} {{" * self.full
+ f"\n{indent(body, 4 * self.full)}\n"
+ self.full * "}"
def _apply_template(self, template):
return template.format(
const_name=self.name,
relative_path=self.path.relative_to(src, walk_up=True).as_posix(),
)
plain_template = 'pub const {const_name}: &str = include_str!("{relative_path}");'
macroed_template = 'file {const_name} "{relative_path}"'
def plain(self):
return self._apply_template(self.plain_template)
def macroed(self):
return self._apply_template(self.macroed_template)
class RsAssetModule(RsAsset):
def __init__(self, path: Path, name: str | None = None):
self.path = path
if name is None:
self.name = sanitize(self.path.name).lower()
else:
self.name = name
plain_template = "pub mod {sanitized_name} {{\n{indented_body}\n}}"
macroed_template = "folder {sanitized_name} {{\n{indented_body}\n}}"
def plain(self):
return self._apply_template(self.plain_template, lambda it: it.plain())
def macroed(self):
return self._apply_template(self.macroed_template, lambda it: it.macroed())
def _apply_template(self, template: str, renderer: Callable[[RsAsset], str]):
body = self._process_body(self.path, renderer)
return template.format(
sanitized_name=self.name,
indented_body=indent(body, 4),
)
@staticmethod
def _process_body(path: Path, renderer: Callable[[RsAsset], str]):
body = RsAssetModule.iterate_assets(path)
return "\n".join(map(renderer, body))
@staticmethod
def iterate_assets(path: Path) -> "Iterable[RsAssetFile | RsAssetModule]":
subs = []
for f in path.iterdir():
if f.is_file():
yield RsAssetFile(f)
elif f.is_dir():
subs.append(RsAssetModule(f))
yield from subs
# i meant plain
def render_pain(ass: RsAsset) -> str:
"""
fym i have to deal with all RsAsset subclasses
whats the point then. unless there is a `Intermediate Representation` whats the point then.
"""
match ass:
case RsAssetRoot() as r:
return r.plain()
case RsAssetModule() as folder:
return folder.plain()
case RsAssetFile() as file:
return file.plain()
case _:
raise TypeError("who are you?")
def render_macro(ass: RsAsset) -> str:
"""
ts is ridiculous
"""
match ass:
case RsAssetRoot() as r:
return r.macroed()
case RsAssetModule() as folder:
return folder.macroed()
case RsAssetFile() as file:
return file.macroed()
case _:
raise TypeError("who are you?")
class RsAssetModule2(TypedDict):
name: str
path: Path
children: "list[RsAssetModule2 | RsAssetFile2]"
class RsAssetFile2(TypedDict):
name: str
path: Path
@overload
def dictionary(im: "RsAssetRoot") -> RsAssetModule2: ...
@overload
def dictionary(im: RsAssetModule) -> RsAssetModule2: ...
@overload
def dictionary(im: RsAssetFile) -> RsAssetFile2: ...
@overload
def dictionary(im: RsAssetFile | RsAssetModule) -> RsAssetFile2 | RsAssetModule2: ...
def dictionary(im: RsAsset) -> RsAssetModule2 | RsAssetFile2:
match im:
case RsAssetRoot() as r:
return RsAssetModule2(
name="ROOT",
path=r.path,
children=list(map(dictionary, RsAssetModule.iterate_assets(r.path))),
)
case RsAssetModule() as folder:
return RsAssetModule2(
name=sanitize(folder.path.name).lower(),
path=folder.path,
children=list(
map(dictionary, RsAssetModule.iterate_assets(folder.path))
),
)
case RsAssetFile() as file:
return RsAssetFile2(
name=sanitize(file.path.name).upper(),
path=file.path,
)
case _:
raise TypeError("who are you?")
lda_macro = """
macro_rules! hehe {
// Folder
(folder $name:ident { $($children:tt)* } $($rest:tt)*) => {
pub mod $name {
hehe!{$($children)*}
}
hehe!{$($rest)*}
};
// File
(file $name:ident $file:literal $($rest:tt)*) => {
pub const $name: &str = include_str!($file);
hehe!{$($rest)*}
};
// Base case
() => {};
}
"""
header = "// generated with ./scripts/map_static.py"
def announce_yourself(*pa, **pkw):
def wpr[**P, R](f: Callable[P, R]) -> Callable[P, R]:
@wraps(f)
def wpd(*a: P.args, **k: P.kwargs) -> R:
print(*pa, **pkw)
return f(*a, **k)
return wpd
return wpr
class RsAssetRoot(RsAsset):
plain_template = """{header}
{body}
"""
macroed_template = """{header}
{lda_macro}
hehe! {{
{indented_body}
}}
"""
def __init__(self, asset_root: Path) -> None:
self.path = asset_root
self._w = RsAssetModule(asset_root)
def plain(self) -> str:
body = RsAssetModule._process_body(self.path, lambda it: it.plain())
return self.plain_template.format(
header=header,
body=body,
)
def macroed(self) -> str:
body = RsAssetModule._process_body(self.path, lambda it: it.macroed())
return self.macroed_template.format(
header=header,
lda_macro=lda_macro,
indented_body=indent(body, 4),
)
asset_root = RsAssetRoot(static)
pprint(dictionary(asset_root))
def render_ir(node: RsAssetModule2 | RsAssetFile2) -> str:
"gemini 3 pro"
if "children" in node:
body = "\n".join(map(render_ir, node["children"])) # type: ignore[typeddict-item]
if node["name"] == "ROOT":
return f"{header}\n\n{body}\n"
return f"pub mod {node['name']} {{\n{indent(body, 4)}\n}}"
rel = node["path"].relative_to(src, walk_up=True).as_posix()
return f'pub const {node["name"]}: &str = include_str!("{rel}");'
rs_code = render_macro(asset_root)
# generate
rs_code = "// generated with ./scripts/map_static.py\n" + str(
RsAssetModule(static, True)
)
out_rs.write_text(rs_code, encoding="utf-8")
print(f"written to {out_rs.relative_to(Path.cwd(), walk_up=True)}")
+28 -9
View File
@@ -1,12 +1,31 @@
// generated with ./scripts/map_static.py
pub const HOME_2_HTML: &str = include_str!("../static/home_2.html");
pub mod home_2 {
pub const DOM_JS: &str = include_str!("../static/home_2/dom.js");
pub const LEASES_JS: &str = include_str!("../static/home_2/leases.js");
pub const MAIN_JS: &str = include_str!("../static/home_2/main.js");
pub const STATUS_JS: &str = include_str!("../static/home_2/status.js");
pub const STYLES_CSS: &str = include_str!("../static/home_2/styles.css");
pub const UTILS_JS: &str = include_str!("../static/home_2/utils.js");
pub const WAKE_JS: &str = include_str!("../static/home_2/wake.js");
macro_rules! hehe {
// Folder
(folder $name:ident { $($children:tt)* } $($rest:tt)*) => {
pub mod $name {
hehe!{$($children)*}
}
hehe!{$($rest)*}
};
// File
(file $name:ident $file:literal $($rest:tt)*) => {
pub const $name: &str = include_str!($file);
hehe!{$($rest)*}
};
// Base case
() => {};
}
hehe! {
file HOME_2_HTML "../static/home_2.html"
folder home_2 {
file DOM_JS "../static/home_2/dom.js"
file LEASES_JS "../static/home_2/leases.js"
file MAIN_JS "../static/home_2/main.js"
file STATUS_JS "../static/home_2/status.js"
file STYLES_CSS "../static/home_2/styles.css"
file UTILS_JS "../static/home_2/utils.js"
file WAKE_JS "../static/home_2/wake.js"
}
}