tangled
alpha
login
or
join now
vielle.dev
/
wol
1
fork
atom
[WIP] A simple wake-on-lan service
1
fork
atom
overview
issues
pulls
pipelines
add support for custom themes and urls
vielle.dev
3 weeks ago
a610e804
2c6a9beb
verified
This commit was signed with the committer's
known signature
.
vielle.dev
SSH Key Fingerprint:
SHA256:EoUuRRBFQKUfYh74C568g83i9g4fVi5OTtOENMSfa+0=
+108
-24
6 changed files
expand all
collapse all
unified
split
src
config.rs
mac.rs
server.rs
web
src
App.svelte
lib
Power.svelte
api.ts
+43
-7
src/config.rs
···
11
11
pub struct Config {
12
12
#[serde(default = "default_binding")]
13
13
binding: String,
14
14
+
#[serde(default = "default_theme")]
15
15
+
theme: Theme,
14
16
pinned: Option<Vec<String>>,
15
17
targets: HashMap<String, Target>,
16
18
}
17
19
20
20
+
/// all colours are in rgb
21
21
+
#[derive(Deserialize, Serialize, Debug, Clone)]
22
22
+
pub struct Theme {
23
23
+
background: (u8, u8, u8),
24
24
+
foreground: (u8, u8, u8),
25
25
+
text: (u8, u8, u8),
26
26
+
text_secondary: (u8, u8, u8),
27
27
+
accent_success: (u8, u8, u8),
28
28
+
accent_fail: (u8, u8, u8),
29
29
+
link: (u8, u8, u8),
30
30
+
link_visited: (u8, u8, u8),
31
31
+
highlight: (u8, u8, u8),
32
32
+
highlight_opacity: u8,
33
33
+
}
34
34
+
18
35
#[derive(Deserialize, Serialize, Debug, Clone)]
19
36
pub struct Target {
20
37
#[serde(
···
23
40
)]
24
41
pub mac: crate::mac::MacAddress,
25
42
pub ip: Option<String>,
43
43
+
pub url: Option<String>,
26
44
}
27
45
28
46
fn default_binding() -> String {
29
47
"0.0.0.0:3000".to_string()
30
48
}
31
49
50
50
+
fn default_theme() -> Theme {
51
51
+
Theme {
52
52
+
background: (48, 52, 70),
53
53
+
foreground: (35, 38, 52),
54
54
+
text: (198, 208, 245),
55
55
+
text_secondary: (165, 173, 206),
56
56
+
accent_success: (166, 209, 137),
57
57
+
accent_fail: (231, 130, 132),
58
58
+
link: (140, 170, 238),
59
59
+
link_visited: (202, 158, 230),
60
60
+
highlight: (148, 156, 187),
61
61
+
highlight_opacity: 64,
62
62
+
}
63
63
+
}
64
64
+
32
65
#[derive(Error, Debug)]
33
66
pub enum ConfigError {
34
67
#[error("Io error: {}", .0)]
···
53
86
if let Some(mismatch) = &config
54
87
.pinned
55
88
.as_ref()
56
56
-
.and_then(|p| p.iter().skip_while(|x| targets.contains(x)).next())
89
89
+
.and_then(|p| p.iter().find(|x| !targets.contains(x)))
57
90
{
58
91
return Err(ConfigError::UnknownPin(mismatch.to_string()));
59
92
};
60
93
61
94
let mut uniq = HashSet::<String>::new();
62
62
-
if let Some(dupe) = &config.pinned.as_ref().and_then(|p| {
63
63
-
p.iter()
64
64
-
.skip_while(move |x| uniq.insert(x.to_string()))
65
65
-
.next()
66
66
-
}) {
95
95
+
if let Some(dupe) = &config
96
96
+
.pinned
97
97
+
.as_ref()
98
98
+
.and_then(|p| p.iter().find(move |x| !uniq.insert(x.to_string())))
99
99
+
{
67
100
return Err(ConfigError::DupePin(dupe.to_string()));
68
101
};
69
102
70
70
-
return Ok(config);
103
103
+
Ok(config)
71
104
}
72
105
106
106
+
#[allow(unused)]
73
107
pub fn get_binding(&self) -> &String {
74
108
&self.binding
75
109
}
76
110
111
111
+
#[allow(unused)]
77
112
pub fn get_pinned(&self) -> &Option<Vec<String>> {
78
113
&self.pinned
79
114
}
80
115
116
116
+
#[allow(unused)]
81
117
pub fn get_targets(&self) -> &HashMap<String, Target> {
82
118
&self.targets
83
119
}
+9
-6
src/mac.rs
···
59
59
type Err = MacAddressParseError;
60
60
61
61
fn from_str(s: &str) -> Result<Self, Self::Err> {
62
62
-
let mut parts = s.split(":");
63
63
-
let mut address: [u8; 6] = [0, 0, 0, 0, 0, 0];
64
64
-
for i in 0..address.len() {
65
65
-
address[i] =
66
66
-
u8::from_str_radix(parts.next().ok_or(MacAddressParseError::TooShort)?, 16)?;
62
62
+
let address = s.split(":");
63
63
+
let address = address
64
64
+
.map(|x| u8::from_str_radix(x, 16))
65
65
+
.collect::<Result<Vec<_>, ParseIntError>>()?;
66
66
+
if address.len() != 6 {
67
67
+
return Err(MacAddressParseError::TooShort);
67
68
}
68
68
-
Ok(MacAddress(address))
69
69
+
Ok(MacAddress([
70
70
+
address[0], address[1], address[2], address[3], address[4], address[5],
71
71
+
]))
69
72
}
70
73
}
71
74
+14
-2
src/server.rs
···
1
1
-
use std::sync::Arc;
1
1
+
use std::{sync::Arc, time::Instant};
2
2
3
3
use axum::{
4
4
Json, Router,
5
5
-
extract::State,
5
5
+
extract::{Request, State},
6
6
http::Response,
7
7
+
middleware::{self},
7
8
routing::{get, post},
8
9
};
9
10
use serde::Deserialize;
···
33
34
Json((*conf).clone())
34
35
}
35
36
37
37
+
async fn log(req: Request, next: axum::middleware::Next) -> axum::response::Response {
38
38
+
let start = Instant::now();
39
39
+
let method = req.method().to_string();
40
40
+
let uri = req.uri().to_string();
41
41
+
let res = next.run(req).await;
42
42
+
43
43
+
println!("({:?}) [{}] {}", start.elapsed(), method, uri);
44
44
+
res
45
45
+
}
46
46
+
36
47
pub fn router(conf: Config) -> Router {
37
48
Router::new()
38
49
.route("/wake", post(wake))
39
50
.route("/config", get(config))
40
51
.with_state(Arc::new(conf))
41
52
.merge(dist::main())
53
53
+
.layer(middleware::from_fn(log))
42
54
}
+4
-2
web/src/App.svelte
···
15
15
([k]) => !(config.pinned ?? []).includes(k),
16
16
),
17
17
];
18
18
+
19
19
+
console.log(config);
18
20
</script>
19
21
20
22
<h1>Wake on Lan</h1>
21
21
-
{#each targets as [name, { mac, ip }]}
22
22
-
<Power {name} {mac} {ip} />
23
23
+
{#each targets as [name, { mac, ip, url }]}
24
24
+
<Power {name} {mac} {ip} {url} />
23
25
{/each}
+20
-6
web/src/lib/Power.svelte
···
5
5
name,
6
6
mac,
7
7
ip,
8
8
+
url,
8
9
}: {
9
10
name: string;
10
11
mac: string;
11
12
ip: string | null;
13
13
+
url: string | null;
12
14
} = $props();
13
15
</script>
14
16
15
15
-
<button
16
16
-
onclick={() =>
17
17
-
Api.wake({ mac }).then((res) => alert(`Wake: ${mac} (${ip}) ${res}`))}
18
18
-
>
17
17
+
<section>
18
18
+
<button
19
19
+
onclick={() =>
20
20
+
Api.wake({ mac }).then((res) => alert(`Wake: ${mac} (${ip}) ${res}`))}
21
21
+
>
22
22
+
Power On
23
23
+
</button>
24
24
+
25
25
+
{#if url}
26
26
+
<a href={url}>{url}</a>
27
27
+
•
28
28
+
{/if}
19
29
{name}
30
30
+
•
20
31
{mac}
21
21
-
{ip}
22
22
-
</button>
32
32
+
{#if ip}
33
33
+
•
34
34
+
<a href={`http://${ip}/`}>{ip}</a>
35
35
+
{/if}
36
36
+
</section>
+18
-1
web/src/lib/api.ts
···
57
57
: () => fetch(route, { method }).then(then);
58
58
}
59
59
60
60
+
const u8 = z.int().min(0).max(255);
61
61
+
const colour = z.tuple([u8, u8, u8]);
62
62
+
60
63
const Api = {
61
64
config: route({
62
65
route: "/config",
63
66
method: "GET",
64
67
output: z.object({
65
68
binding: z.string(),
66
66
-
pinned: z.array(z.string()).optional(),
69
69
+
theme: z.object({
70
70
+
background: colour,
71
71
+
foreground: colour,
72
72
+
text: colour,
73
73
+
text_secondary: colour,
74
74
+
accent_success: colour,
75
75
+
accent_fail: colour,
76
76
+
link: colour,
77
77
+
link_visited: colour,
78
78
+
highlight: colour,
79
79
+
highlight_opacity: u8,
80
80
+
}),
81
81
+
pinned: z.array(z.string()).nullable(),
67
82
targets: z.record(
68
83
z.string(),
69
84
z.object({
70
85
mac: z.string(),
71
86
ip: z.string().nullable(),
87
87
+
// should be a url but we allow any string in rust
88
88
+
url: z.string().nullable(),
72
89
}),
73
90
),
74
91
}),