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", "axum-extra",
"color-eyre", "color-eyre",
"futures", "futures",
"lda-ipjs",
"macaddr", "macaddr",
"serde", "serde",
"serde_html_form", "serde_html_form",
+3
View File
@@ -36,3 +36,6 @@ very-smart-parsing = [
[workspace] [workspace]
members = ["ipjs"] 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 // 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"); let mut cmd = tokio::process::Command::new("ip");
cmd.args(["-j", "address", "show"]); 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. // 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 // it all turns to live osstr tho so ts just for my own sanity
// thiserror? anyhow // 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( pub async fn get(
ip: Option<IpAddr>, ip: Option<IpAddr>,
dev: Option<&str>, dev: Option<&str>,
@@ -23,8 +27,7 @@ pub async fn get(
}; };
if let Some(dev) = dev { if let Some(dev) = dev {
cmd.arg("dev"); cmd.args(["dev", dev]);
cmd.arg(dev);
} }
for nud in nud { for nud in nud {
cmd.arg("nud"); cmd.arg("nud");
+2
View File
@@ -1,3 +1,5 @@
//! this is purely experimental. im not doing ts no mo
// hallo // hallo
use std::{ use std::{
collections::{HashMap, HashSet}, 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" "thanks chatgpt"
from abc import ABC, abstractmethod
from collections.abc import Iterable
from functools import wraps
from pathlib import Path from pathlib import Path
from pprint import pprint
from typing import Callable, TypedDict, overload
root = Path(__file__).parent.parent root = Path(__file__).parent.parent
static = root / "static" static = root / "static"
src = root / "src" src = root / "src"
out_rs = src / "assets.rs" out_rs = src / "assets.rs"
assert 'name = "wakey"' in (root / "Cargo.toml").read_text( assert 'name = "wakey"' in (root / "Cargo.toml").read_text(encoding="utf-8"), (
encoding="utf-8" "uhh how do i explain this"
), "uhh how do i explain this" )
def sanitize(name: str) -> str: def sanitize(name: str) -> str:
# Valid Rust identifiers: letters, digits, underscores; no starting digit # 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()) return "\n".join(pad + line if line.strip() else line for line in text.splitlines())
class RsAssetFile: class RsAsset(ABC):
def __init__(self, path: Path): 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 self.path = path
if name is None:
self.name = sanitize(self.path.name).upper()
else:
self.name = name
def __str__(self): def _apply_template(self, template):
const_name = sanitize(self.path.name).upper() return template.format(
return ( const_name=self.name,
f"pub const {const_name}: &str = " relative_path=self.path.relative_to(src, walk_up=True).as_posix(),
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 * "}"
) )
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 # generate
rs_code = "// generated with ./scripts/map_static.py\n" + str(
RsAssetModule(static, True)
)
out_rs.write_text(rs_code, encoding="utf-8") out_rs.write_text(rs_code, encoding="utf-8")
print(f"written to {out_rs.relative_to(Path.cwd(), walk_up=True)}") 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 // generated with ./scripts/map_static.py
pub const HOME_2_HTML: &str = include_str!("../static/home_2.html"); macro_rules! hehe {
pub mod home_2 { // Folder
pub const DOM_JS: &str = include_str!("../static/home_2/dom.js"); (folder $name:ident { $($children:tt)* } $($rest:tt)*) => {
pub const LEASES_JS: &str = include_str!("../static/home_2/leases.js"); pub mod $name {
pub const MAIN_JS: &str = include_str!("../static/home_2/main.js"); hehe!{$($children)*}
pub const STATUS_JS: &str = include_str!("../static/home_2/status.js"); }
pub const STYLES_CSS: &str = include_str!("../static/home_2/styles.css"); hehe!{$($rest)*}
pub const UTILS_JS: &str = include_str!("../static/home_2/utils.js"); };
pub const WAKE_JS: &str = include_str!("../static/home_2/wake.js"); // 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"
}
} }