pi-extensions 0.1.21 → 0.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/skill-apply-optimize.yml +19 -0
- package/.github/workflows/skill-review.yml +17 -0
- package/.github/workflows/weather-native-bridge.yml +86 -0
- package/extending-pi/SKILL.md +43 -12
- package/files-widget/package.json +3 -3
- package/package.json +2 -6
- package/ralph-wiggum/index.ts +10 -0
- package/ralph-wiggum/package.json +1 -1
- package/usage-extension/CHANGELOG.md +4 -0
- package/usage-extension/README.md +18 -2
- package/usage-extension/index.ts +168 -99
- package/usage-extension/package.json +1 -1
- package/weather/CHANGELOG.md +16 -0
- package/weather/LICENSE +21 -0
- package/weather/README.md +132 -0
- package/weather/index.ts +1319 -0
- package/weather/native/weathr-bridge/Cargo.toml +15 -0
- package/weather/native/weathr-bridge/build.rs +3 -0
- package/weather/native/weathr-bridge/index.d.ts +25 -0
- package/weather/native/weathr-bridge/index.js +315 -0
- package/weather/native/weathr-bridge/package.json +41 -0
- package/weather/native/weathr-bridge/src/lib.rs +347 -0
- package/weather/package.json +52 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
use napi::Result as NapiResult;
|
|
2
|
+
use napi_derive::napi;
|
|
3
|
+
use std::io::{ErrorKind, Read, Write};
|
|
4
|
+
#[cfg(unix)]
|
|
5
|
+
use std::os::unix::process::ExitStatusExt;
|
|
6
|
+
use std::process::{Child, ChildStdin, Command, ExitStatus, Stdio};
|
|
7
|
+
use std::sync::{Arc, Mutex};
|
|
8
|
+
use std::thread;
|
|
9
|
+
use std::time::Duration;
|
|
10
|
+
|
|
11
|
+
#[napi(object)]
|
|
12
|
+
pub struct WeatherProcessSnapshot {
|
|
13
|
+
pub stdout: String,
|
|
14
|
+
pub stderr: String,
|
|
15
|
+
pub exited: bool,
|
|
16
|
+
pub exit_code: Option<i32>,
|
|
17
|
+
pub exit_signal: Option<String>,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
struct SpawnedProcess {
|
|
21
|
+
child: Child,
|
|
22
|
+
stdin: ChildStdin,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
#[napi]
|
|
26
|
+
pub struct NativeWeatherProcess {
|
|
27
|
+
script_path: String,
|
|
28
|
+
weathr_path: String,
|
|
29
|
+
args: Vec<String>,
|
|
30
|
+
config_home: String,
|
|
31
|
+
columns: u16,
|
|
32
|
+
rows: u16,
|
|
33
|
+
child: Option<Child>,
|
|
34
|
+
stdin: Option<ChildStdin>,
|
|
35
|
+
stdout_buffer: Arc<Mutex<Vec<u8>>>,
|
|
36
|
+
stderr_buffer: Arc<Mutex<Vec<u8>>>,
|
|
37
|
+
exited: bool,
|
|
38
|
+
exit_code: Option<i32>,
|
|
39
|
+
exit_signal: Option<String>,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
#[napi]
|
|
43
|
+
impl NativeWeatherProcess {
|
|
44
|
+
#[napi(constructor)]
|
|
45
|
+
pub fn new(
|
|
46
|
+
script_path: String,
|
|
47
|
+
weathr_path: String,
|
|
48
|
+
args: Vec<String>,
|
|
49
|
+
config_home: String,
|
|
50
|
+
columns: u16,
|
|
51
|
+
rows: u16,
|
|
52
|
+
) -> Self {
|
|
53
|
+
let stdout_buffer = Arc::new(Mutex::new(Vec::new()));
|
|
54
|
+
let stderr_buffer = Arc::new(Mutex::new(Vec::new()));
|
|
55
|
+
|
|
56
|
+
let (child, stdin, exited, exit_signal) = match spawn_weathr_process(
|
|
57
|
+
&script_path,
|
|
58
|
+
&weathr_path,
|
|
59
|
+
&args,
|
|
60
|
+
&config_home,
|
|
61
|
+
columns,
|
|
62
|
+
rows,
|
|
63
|
+
stdout_buffer.clone(),
|
|
64
|
+
stderr_buffer.clone(),
|
|
65
|
+
) {
|
|
66
|
+
Ok(spawned) => (Some(spawned.child), Some(spawned.stdin), false, None),
|
|
67
|
+
Err(error) => {
|
|
68
|
+
let message = format!("Failed to start native weather bridge: {error}");
|
|
69
|
+
if let Ok(mut stderr) = stderr_buffer.lock() {
|
|
70
|
+
stderr.extend_from_slice(message.as_bytes());
|
|
71
|
+
}
|
|
72
|
+
(None, None, true, Some("start error".to_owned()))
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
Self {
|
|
77
|
+
script_path,
|
|
78
|
+
weathr_path,
|
|
79
|
+
args,
|
|
80
|
+
config_home,
|
|
81
|
+
columns,
|
|
82
|
+
rows,
|
|
83
|
+
child,
|
|
84
|
+
stdin,
|
|
85
|
+
stdout_buffer,
|
|
86
|
+
stderr_buffer,
|
|
87
|
+
exited,
|
|
88
|
+
exit_code: None,
|
|
89
|
+
exit_signal,
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
#[napi]
|
|
94
|
+
pub fn poll(&mut self) -> WeatherProcessSnapshot {
|
|
95
|
+
self.update_exit_state();
|
|
96
|
+
WeatherProcessSnapshot {
|
|
97
|
+
stdout: take_buffer_string(&self.stdout_buffer),
|
|
98
|
+
stderr: take_buffer_string(&self.stderr_buffer),
|
|
99
|
+
exited: self.exited,
|
|
100
|
+
exit_code: self.exit_code,
|
|
101
|
+
exit_signal: self.exit_signal.clone(),
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
#[napi]
|
|
106
|
+
pub fn write_input(&mut self, input: String) -> bool {
|
|
107
|
+
if self.exited {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
let Some(stdin) = self.stdin.as_mut() else {
|
|
111
|
+
return false;
|
|
112
|
+
};
|
|
113
|
+
if stdin.write_all(input.as_bytes()).is_err() {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
stdin.flush().is_ok()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
#[napi]
|
|
120
|
+
pub fn stop(&mut self) {
|
|
121
|
+
if self.exited {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let _ = self.write_input("q".to_owned());
|
|
126
|
+
thread::sleep(Duration::from_millis(100));
|
|
127
|
+
|
|
128
|
+
self.stdin = None;
|
|
129
|
+
let Some(mut child) = self.child.take() else {
|
|
130
|
+
self.exited = true;
|
|
131
|
+
self.exit_code = None;
|
|
132
|
+
self.exit_signal = Some("stopped".to_owned());
|
|
133
|
+
return;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
let status = match child.try_wait() {
|
|
137
|
+
Ok(Some(status)) => Some(status),
|
|
138
|
+
Ok(None) => {
|
|
139
|
+
let _ = child.kill();
|
|
140
|
+
child.wait().ok()
|
|
141
|
+
}
|
|
142
|
+
Err(_) => None,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
if let Some(status) = status {
|
|
146
|
+
self.record_exit_status(status);
|
|
147
|
+
} else {
|
|
148
|
+
self.exited = true;
|
|
149
|
+
self.exit_code = None;
|
|
150
|
+
self.exit_signal = Some("terminated".to_owned());
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
#[napi]
|
|
155
|
+
pub fn restart(&mut self) -> NapiResult<()> {
|
|
156
|
+
self.stop();
|
|
157
|
+
self.exited = false;
|
|
158
|
+
self.exit_code = None;
|
|
159
|
+
self.exit_signal = None;
|
|
160
|
+
|
|
161
|
+
let stdout_buffer = Arc::new(Mutex::new(Vec::new()));
|
|
162
|
+
let stderr_buffer = Arc::new(Mutex::new(Vec::new()));
|
|
163
|
+
let spawned = spawn_weathr_process(
|
|
164
|
+
&self.script_path,
|
|
165
|
+
&self.weathr_path,
|
|
166
|
+
&self.args,
|
|
167
|
+
&self.config_home,
|
|
168
|
+
self.columns,
|
|
169
|
+
self.rows,
|
|
170
|
+
stdout_buffer.clone(),
|
|
171
|
+
stderr_buffer.clone(),
|
|
172
|
+
)?;
|
|
173
|
+
|
|
174
|
+
self.child = Some(spawned.child);
|
|
175
|
+
self.stdin = Some(spawned.stdin);
|
|
176
|
+
self.stdout_buffer = stdout_buffer;
|
|
177
|
+
self.stderr_buffer = stderr_buffer;
|
|
178
|
+
Ok(())
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
#[napi]
|
|
182
|
+
pub fn resize(&mut self, columns: u16, rows: u16) -> NapiResult<()> {
|
|
183
|
+
self.columns = columns;
|
|
184
|
+
self.rows = rows;
|
|
185
|
+
self.restart()
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
#[napi]
|
|
189
|
+
pub fn is_running(&mut self) -> bool {
|
|
190
|
+
self.update_exit_state();
|
|
191
|
+
!self.exited
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
impl NativeWeatherProcess {
|
|
196
|
+
fn update_exit_state(&mut self) {
|
|
197
|
+
if self.exited {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
let Some(child) = self.child.as_mut() else {
|
|
201
|
+
return;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
match child.try_wait() {
|
|
205
|
+
Ok(Some(status)) => {
|
|
206
|
+
self.stdin = None;
|
|
207
|
+
self.child = None;
|
|
208
|
+
self.record_exit_status(status);
|
|
209
|
+
}
|
|
210
|
+
Ok(None) => {}
|
|
211
|
+
Err(error) => {
|
|
212
|
+
self.stdin = None;
|
|
213
|
+
self.child = None;
|
|
214
|
+
self.exited = true;
|
|
215
|
+
self.exit_code = None;
|
|
216
|
+
self.exit_signal = Some(format!("wait error: {error}"));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
fn record_exit_status(&mut self, status: ExitStatus) {
|
|
222
|
+
self.exited = true;
|
|
223
|
+
self.exit_code = status.code();
|
|
224
|
+
self.exit_signal = exit_signal_label(status);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
impl Drop for NativeWeatherProcess {
|
|
229
|
+
fn drop(&mut self) {
|
|
230
|
+
self.stop();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
fn spawn_weathr_process(
|
|
235
|
+
script_path: &str,
|
|
236
|
+
weathr_path: &str,
|
|
237
|
+
args: &[String],
|
|
238
|
+
config_home: &str,
|
|
239
|
+
columns: u16,
|
|
240
|
+
rows: u16,
|
|
241
|
+
stdout_buffer: Arc<Mutex<Vec<u8>>>,
|
|
242
|
+
stderr_buffer: Arc<Mutex<Vec<u8>>>,
|
|
243
|
+
) -> NapiResult<SpawnedProcess> {
|
|
244
|
+
let escaped_binary = shell_quote(weathr_path);
|
|
245
|
+
let escaped_args = args
|
|
246
|
+
.iter()
|
|
247
|
+
.map(|value| shell_quote(value))
|
|
248
|
+
.collect::<Vec<String>>()
|
|
249
|
+
.join(" ");
|
|
250
|
+
let weather_command = if escaped_args.is_empty() {
|
|
251
|
+
escaped_binary
|
|
252
|
+
} else {
|
|
253
|
+
format!("{escaped_binary} {escaped_args}")
|
|
254
|
+
};
|
|
255
|
+
let shell_command = format!("stty cols {columns} rows {rows}; exec {weather_command}");
|
|
256
|
+
|
|
257
|
+
let mut command = Command::new(script_path);
|
|
258
|
+
command
|
|
259
|
+
.arg("-q")
|
|
260
|
+
.arg("/dev/null")
|
|
261
|
+
.arg("sh")
|
|
262
|
+
.arg("-c")
|
|
263
|
+
.arg(shell_command)
|
|
264
|
+
.env("XDG_CONFIG_HOME", config_home)
|
|
265
|
+
.env_remove("NO_COLOR")
|
|
266
|
+
.stdin(Stdio::piped())
|
|
267
|
+
.stdout(Stdio::piped())
|
|
268
|
+
.stderr(Stdio::piped());
|
|
269
|
+
|
|
270
|
+
if std::env::var_os("COLORTERM").is_none() {
|
|
271
|
+
command.env("COLORTERM", "truecolor");
|
|
272
|
+
}
|
|
273
|
+
if std::env::var_os("TERM").is_none() {
|
|
274
|
+
command.env("TERM", "xterm-256color");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
let mut child = command.spawn().map_err(|error| {
|
|
278
|
+
napi::Error::from_reason(format!("Failed to start weather process: {error}"))
|
|
279
|
+
})?;
|
|
280
|
+
|
|
281
|
+
let stdin = child
|
|
282
|
+
.stdin
|
|
283
|
+
.take()
|
|
284
|
+
.ok_or_else(|| napi::Error::from_reason("Failed to open weather stdin".to_owned()))?;
|
|
285
|
+
let stdout = child
|
|
286
|
+
.stdout
|
|
287
|
+
.take()
|
|
288
|
+
.ok_or_else(|| napi::Error::from_reason("Failed to open weather stdout".to_owned()))?;
|
|
289
|
+
let stderr = child
|
|
290
|
+
.stderr
|
|
291
|
+
.take()
|
|
292
|
+
.ok_or_else(|| napi::Error::from_reason("Failed to open weather stderr".to_owned()))?;
|
|
293
|
+
|
|
294
|
+
spawn_reader_thread(stdout, stdout_buffer);
|
|
295
|
+
spawn_reader_thread(stderr, stderr_buffer);
|
|
296
|
+
|
|
297
|
+
Ok(SpawnedProcess { child, stdin })
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
fn spawn_reader_thread<R>(mut reader: R, buffer: Arc<Mutex<Vec<u8>>>)
|
|
301
|
+
where
|
|
302
|
+
R: Read + Send + 'static,
|
|
303
|
+
{
|
|
304
|
+
thread::spawn(move || {
|
|
305
|
+
let mut chunk = [0_u8; 8192];
|
|
306
|
+
loop {
|
|
307
|
+
match reader.read(&mut chunk) {
|
|
308
|
+
Ok(0) => break,
|
|
309
|
+
Ok(size) => {
|
|
310
|
+
let Ok(mut shared) = buffer.lock() else {
|
|
311
|
+
break;
|
|
312
|
+
};
|
|
313
|
+
shared.extend_from_slice(&chunk[..size]);
|
|
314
|
+
}
|
|
315
|
+
Err(error) if error.kind() == ErrorKind::Interrupted => continue,
|
|
316
|
+
Err(_) => break,
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
fn take_buffer_string(buffer: &Arc<Mutex<Vec<u8>>>) -> String {
|
|
323
|
+
let Ok(mut shared) = buffer.lock() else {
|
|
324
|
+
return String::new();
|
|
325
|
+
};
|
|
326
|
+
if shared.is_empty() {
|
|
327
|
+
return String::new();
|
|
328
|
+
}
|
|
329
|
+
let bytes = std::mem::take(&mut *shared);
|
|
330
|
+
String::from_utf8_lossy(&bytes).into_owned()
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
fn shell_quote(value: &str) -> String {
|
|
334
|
+
format!("'{}'", value.replace('\'', "'\"'\"'"))
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
fn exit_signal_label(status: ExitStatus) -> Option<String> {
|
|
338
|
+
#[cfg(unix)]
|
|
339
|
+
{
|
|
340
|
+
status.signal().map(|signal| signal.to_string())
|
|
341
|
+
}
|
|
342
|
+
#[cfg(not(unix))]
|
|
343
|
+
{
|
|
344
|
+
let _ = status;
|
|
345
|
+
None
|
|
346
|
+
}
|
|
347
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tmustier/pi-weather",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Weather widget for Pi (/weather)",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Thomas Mustier",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"pi-package"
|
|
9
|
+
],
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/tmustier/pi-extensions.git",
|
|
13
|
+
"directory": "weather"
|
|
14
|
+
},
|
|
15
|
+
"bugs": "https://github.com/tmustier/pi-extensions/issues",
|
|
16
|
+
"homepage": "https://github.com/tmustier/pi-extensions/tree/main/weather",
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
19
|
+
"@mariozechner/pi-tui": "*"
|
|
20
|
+
},
|
|
21
|
+
"optionalDependencies": {
|
|
22
|
+
"@tmustier/pi-weather-bridge-darwin-arm64": "0.1.0",
|
|
23
|
+
"@tmustier/pi-weather-bridge-darwin-x64": "0.1.0",
|
|
24
|
+
"@tmustier/pi-weather-bridge-linux-x64-gnu": "0.1.0",
|
|
25
|
+
"@tmustier/pi-weather-bridge-win32-x64-msvc": "0.1.0"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build:native": "cd native/weathr-bridge && npm install --no-package-lock && npm run build",
|
|
29
|
+
"build:native:debug": "cd native/weathr-bridge && npm install --no-package-lock && npm run build:debug",
|
|
30
|
+
"native:prepare-packages": "cd native/weathr-bridge && npm install --no-package-lock && npm run create-npm-dir",
|
|
31
|
+
"native:sync-artifacts": "cd native/weathr-bridge && npm install --no-package-lock && npm run artifacts",
|
|
32
|
+
"native:publish-packages": "cd native/weathr-bridge && for pkg in npm/*; do if ls \"$pkg\"/*.node >/dev/null 2>&1; then npm publish \"./$pkg\" --access public --provenance; else echo \"Skipping $pkg (no binary)\"; fi; done"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"index.ts",
|
|
36
|
+
"README.md",
|
|
37
|
+
"CHANGELOG.md",
|
|
38
|
+
"LICENSE",
|
|
39
|
+
"native/weathr-bridge/build.rs",
|
|
40
|
+
"native/weathr-bridge/Cargo.toml",
|
|
41
|
+
"native/weathr-bridge/index.d.ts",
|
|
42
|
+
"native/weathr-bridge/index.js",
|
|
43
|
+
"native/weathr-bridge/package.json",
|
|
44
|
+
"native/weathr-bridge/src/lib.rs"
|
|
45
|
+
],
|
|
46
|
+
"pi": {
|
|
47
|
+
"extensions": [
|
|
48
|
+
"index.ts"
|
|
49
|
+
],
|
|
50
|
+
"video": "https://raw.githubusercontent.com/tmustier/pi-extensions/main/weather/assets/weather-demo.mp4"
|
|
51
|
+
}
|
|
52
|
+
}
|