sesame-kit 0.4.1 → 0.5.0

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/src/session-ui.js CHANGED
@@ -5,6 +5,9 @@
5
5
  // 状態が変わった瞬間に描き直す Ink (React for CLI) で実装する。BLE の mechStatus publish や
6
6
  // バックグラウンド接続の完了を `bus` の "update" イベントで受け、React state を更新して再描画する。
7
7
  //
8
+ // 操作: ↑↓ 移動 / → か Enter で決定 / ← か Esc で戻る / q で終了。
9
+ // アクション実行後はホームに戻らず、その操作メニューに留まる (続けて操作できる)。
10
+ //
8
11
  // JSX は使わない (本リポは src を素の ESM で実行しビルド工程が無いため)。React.createElement = h。
9
12
 
10
13
  import React from "react";
@@ -43,6 +46,7 @@ export function SessionApp({ devices, hasCloud, bus, exec, actionsFor, fmtState,
43
46
  const [numVal, setNumVal] = React.useState(""); // autolock 秒数 / LED duty 入力
44
47
  const [selRemote, setSelRemote] = React.useState(null); // IR: 選択中リモコン
45
48
  const [irKeys, setIrKeys] = React.useState(null); // IR: 取得したキー一覧 (null=未取得)
49
+ const [hi, setHi] = React.useState(null); // → 決定用: ハイライト中の項目
46
50
 
47
51
  // BLE onStatus / 背景接続完了 → 再描画。
48
52
  React.useEffect(() => {
@@ -51,18 +55,82 @@ export function SessionApp({ devices, hasCloud, bus, exec, actionsFor, fmtState,
51
55
  return () => bus.off("update", on);
52
56
  }, [bus]);
53
57
 
58
+ // hi (→ 決定用ハイライト) は各 SelectInput の onHighlight が先頭項目で更新する。
59
+ // SelectInput を描画しない可能性のある mode (空の IR リスト) に入る時だけ明示的にクリアし、
60
+ // 前メニューの項目が残って → が誤爆しないようにする (下の selectAction / selectIrRemote 参照)。
61
+
54
62
  const backToActions = () => { setMode("actions"); };
55
63
 
56
- // q / Esc。Esc は深い階層では1つ戻る、devices(or single actions)では終了。
64
+ // ---- 各メニューの選択ハンドラ (SelectInput.onSelect 決定の両方から呼ぶ) ----
65
+ const selectDevice = (it) => {
66
+ if (!it) return;
67
+ if (it.value === "__quit") { exit(); return; }
68
+ setSelName(it.value); setMsg(""); setMode("actions");
69
+ };
70
+ const selectAction = (it) => {
71
+ if (!it) return;
72
+ if (it.value === "__back") { if (single) exit(); else { setMode("devices"); setMsg(""); } return; }
73
+ if (it.value === "autolock") { setNumVal(""); setMode("autolock"); return; }
74
+ if (it.value === "led") { setNumVal(""); setMode("led"); return; }
75
+ if (it.value === "ir") { setHi(null); setSelRemote(null); setIrKeys(null); setMode("ir-remote"); return; }
76
+ runExec(it.value, devices.get(selName));
77
+ };
78
+ const selectIrRemote = (it) => {
79
+ if (!it) return;
80
+ if (it.value === "__back") { backToActions(); return; }
81
+ setHi(null); setSelRemote(it.value); setIrKeys(null); setMode("ir-key");
82
+ Promise.resolve(listKeysFor ? listKeysFor(it.value) : []).then(setIrKeys).catch(() => setIrKeys([]));
83
+ };
84
+ const selectIrKey = (it) => {
85
+ if (!it) return;
86
+ if (it.value === "__back") { setMode("ir-remote"); return; }
87
+ runExec("ir", devices.get(selName), { remote: selRemote, key: it.value });
88
+ };
89
+
90
+ // ← / Esc で 1 つ戻る。Esc は最上位 (devices / single の actions) では終了する。
91
+ // ← は最上位では何もしない (誤操作で終了しないように)。
92
+ const goBack = (allowExitAtTop) => {
93
+ if (mode === "actions") {
94
+ if (single) { if (allowExitAtTop) exit(); }
95
+ else { setMode("devices"); setMsg(""); }
96
+ } else if (mode === "ir-key") setMode("ir-remote");
97
+ else if (mode === "devices") { if (allowExitAtTop) exit(); }
98
+ else backToActions(); // autolock / led / ir-remote
99
+ };
100
+
101
+ // 現在 mode のメニュー項目 (render と → 決定で共有。順序が両者で一致する)。
102
+ const menuItems = () => {
103
+ if (mode === "devices") return [...names.map((n) => ({ label: n, value: n })), { label: "終了", value: "__quit" }];
104
+ if (mode === "actions") return [...actionsFor(devices.get(selName)), { label: single ? "終了" : "← 戻る", value: "__back" }];
105
+ if (mode === "ir-remote") {
106
+ const r = hub3RemotesFor ? hub3RemotesFor(devices.get(selName)) : [];
107
+ return r.length ? [...r, { label: "← 戻る", value: "__back" }] : [];
108
+ }
109
+ if (mode === "ir-key") return (irKeys && irKeys.length) ? [...irKeys, { label: "← 戻る", value: "__back" }] : [];
110
+ return [];
111
+ };
112
+
113
+ // → で決定。ink-select-input の onHighlight は初期項目では発火しないため、移動前は hi=null。
114
+ // その場合は先頭 (= 既定でハイライトされている項目) にフォールバックする。
115
+ const goForward = () => {
116
+ const it = hi || menuItems()[0];
117
+ if (!it) return;
118
+ if (mode === "devices") selectDevice(it);
119
+ else if (mode === "actions") selectAction(it);
120
+ else if (mode === "ir-remote") selectIrRemote(it);
121
+ else if (mode === "ir-key") selectIrKey(it);
122
+ };
123
+
124
+ const isList = mode === "devices" || mode === "actions" || mode === "ir-remote" || mode === "ir-key";
125
+
57
126
  useInput((input, key) => {
58
127
  if (mode === "busy") return;
59
- if (input === "q") { exit(); return; }
60
- if (key.escape) {
61
- if (mode === "actions") { if (single) exit(); else { setMode("devices"); setMsg(""); } }
62
- else if (mode === "ir-key") { setMode("ir-remote"); }
63
- else if (mode === "devices") exit();
64
- else { backToActions(); } // autolock / led / ir-remote
65
- }
128
+ // q は**メニュー系のみ**で終了。autolock/LED の数値入力中は文字として TextInput へ渡す。
129
+ if (input === "q" && isList) { exit(); return; }
130
+ if (key.escape) { goBack(true); return; } // Esc: 戻る (最上位では終了)
131
+ // / は数値入力 (autolock/led) ではテキストカーソル移動に使うので、リスト系のみで奪う。
132
+ if (isList && key.leftArrow) { goBack(false); return; } // ←: 戻る (最上位では何もしない)
133
+ if (isList && key.rightArrow) { goForward(); return; } // →: 決定
66
134
  });
67
135
 
68
136
  const runExec = (op, d, extra) => {
@@ -70,10 +138,10 @@ export function SessionApp({ devices, hasCloud, bus, exec, actionsFor, fmtState,
70
138
  exec(op, d, extra)
71
139
  .then((m) => setMsg(m))
72
140
  .catch((e) => setMsg(`error: ${e?.message || e}`))
73
- .finally(() => setMode(single ? "actions" : "devices"));
141
+ .finally(() => setMode("actions")); // ホームに戻らず操作メニューに留まる (続けて操作できる)
74
142
  };
75
143
 
76
- // ヘッダ: 全デバイスの現在状態 (ライブ)
144
+ // ヘッダ: 全デバイスの現在状態 (ライブ) + 操作ヒント。
77
145
  const header = h(
78
146
  Box,
79
147
  { flexDirection: "column" },
@@ -85,6 +153,7 @@ export function SessionApp({ devices, hasCloud, bus, exec, actionsFor, fmtState,
85
153
  return h(Text, { key: n, color: d.ble ? "green" : undefined },
86
154
  ` ${n} [${label}·${tag}]: ${fmtState(d)}`);
87
155
  }),
156
+ h(Text, { dimColor: true }, " ↑↓ 移動 → 決定 ← 戻る q 終了"),
88
157
  msg ? h(Text, { color: "yellow" }, msg) : null,
89
158
  );
90
159
  const box = (...kids) => h(Box, { flexDirection: "column" }, header, ...kids);
@@ -116,66 +185,30 @@ export function SessionApp({ devices, hasCloud, bus, exec, actionsFor, fmtState,
116
185
  const d = devices.get(selName);
117
186
  const remotes = (hub3RemotesFor ? hub3RemotesFor(d) : []);
118
187
  if (remotes.length === 0) {
119
- return box(h(Text, null, `${selName}: 登録リモコンがありません ( sesame remote add で登録 )Esc で戻る`));
188
+ return box(h(Text, null, `${selName}: 登録リモコンがありません ( sesame remote add で登録 )。← / Esc で戻る`));
120
189
  }
121
- const items = [...remotes, { label: "← 戻る", value: "__back" }];
122
190
  return box(h(Text, null, `${selName} の IR: リモコン選択`),
123
- h(SelectInput, {
124
- items,
125
- onSelect: (it) => {
126
- if (it.value === "__back") { backToActions(); return; }
127
- setSelRemote(it.value); setIrKeys(null); setMode("ir-key");
128
- Promise.resolve(listKeysFor ? listKeysFor(it.value) : []).then(setIrKeys).catch(() => setIrKeys([]));
129
- },
130
- }),
191
+ h(SelectInput, { items: menuItems(), onHighlight: setHi, onSelect: selectIrRemote }),
131
192
  );
132
193
  }
133
194
 
134
195
  // IR: キー選択 (非同期取得中はローディング表示)。
135
196
  if (mode === "ir-key") {
136
- const d = devices.get(selName);
137
197
  if (irKeys === null) return box(h(Text, null, `${selRemote}: キー取得中...`));
138
- if (irKeys.length === 0) return box(h(Text, null, `${selRemote}: キーがありません ( sesame remote sync-keys )Esc で戻る`));
139
- const items = [...irKeys, { label: "← 戻る", value: "__back" }];
198
+ if (irKeys.length === 0) return box(h(Text, null, `${selRemote}: キーがありません ( sesame remote sync-keys )。← / Esc で戻る`));
140
199
  return box(h(Text, null, `${selRemote} のキー選択 (送信)`),
141
- h(SelectInput, {
142
- items,
143
- onSelect: (it) => {
144
- if (it.value === "__back") { setMode("ir-remote"); return; }
145
- runExec("ir", d, { remote: selRemote, key: it.value });
146
- },
147
- }),
200
+ h(SelectInput, { items: menuItems(), onHighlight: setHi, onSelect: selectIrKey }),
148
201
  );
149
202
  }
150
203
 
151
204
  if (mode === "actions") {
152
- const d = devices.get(selName);
153
- const items = [...actionsFor(d)];
154
- items.push({ label: single ? "終了" : "← 戻る", value: "__back" });
155
205
  return box(h(Text, null, `${selName} の操作:`),
156
- h(SelectInput, {
157
- items,
158
- onSelect: (it) => {
159
- if (it.value === "__back") { if (single) exit(); else { setMode("devices"); setMsg(""); } return; }
160
- if (it.value === "autolock") { setNumVal(""); setMode("autolock"); return; }
161
- if (it.value === "led") { setNumVal(""); setMode("led"); return; }
162
- if (it.value === "ir") { setSelRemote(null); setIrKeys(null); setMode("ir-remote"); return; }
163
- runExec(it.value, d);
164
- },
165
- }),
206
+ h(SelectInput, { items: menuItems(), onHighlight: setHi, onSelect: selectAction }),
166
207
  );
167
208
  }
168
209
 
169
210
  // mode === "devices"
170
- const items = names.map((n) => ({ label: n, value: n }));
171
- items.push({ label: "終了", value: "__quit" });
172
211
  return box(h(Text, null, "操作するデバイス:"),
173
- h(SelectInput, {
174
- items,
175
- onSelect: (it) => {
176
- if (it.value === "__quit") { exit(); return; }
177
- setSelName(it.value); setMsg(""); setMode("actions");
178
- },
179
- }),
212
+ h(SelectInput, { items: menuItems(), onHighlight: setHi, onSelect: selectDevice }),
180
213
  );
181
214
  }
package/src/tokens.js CHANGED
@@ -17,7 +17,7 @@ function writeJson(path, data) {
17
17
  // - Windows では fs.writeFileSync の mode は read-only flag に degrade される
18
18
  // - mkdirSync の mode は **新規作成時のみ** 適用される (既存ディレクトリの
19
19
  // パーミッションは変わらない)。旧バージョンで 0755 で作られたディレクトリは
20
- // `chmod 700 ~/.config/sesame-hub3` で手動修正が必要。
20
+ // `chmod 700 ~/.config/sesame-kit` で手動修正が必要。
21
21
  mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
22
22
  writeFileSync(path, JSON.stringify(data, null, 2) + "\n", { mode: 0o600 });
23
23
  }
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.js"],"names":[],"mappings":"AA+9CA,oDAqQC"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.js"],"names":[],"mappings":"AA+9CA,oDAuQC"}
package/types/client.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export class SesameHub3 {
2
2
  /**
3
- * 既定の設定ディレクトリ (~/.config/sesame-hub3 等) から読み込んで構築。
3
+ * 既定の設定ディレクトリ (~/.config/sesame-kit 等) から読み込んで構築。
4
4
  * CLI 内部はこのファクトリを使う。
5
5
  * @param {{ configDir?: string, debug?: boolean }} [opts]
6
6
  */
@@ -1 +1 @@
1
- {"version":3,"file":"session-ui.d.ts","sourceRoot":"","sources":["../src/session-ui.js"],"names":[],"mappings":"AAgBA;;;;;;;;;;GAUG;AACH,oCATW;IACN,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE;QAAC,KAAK,EAAC,MAAM,CAAC;QAAC,GAAG,EAAC,MAAM,GAAC,IAAI,CAAA;KAAC,CAAC,CAAC;IACtD,QAAQ,EAAE,OAAO,CAAC;IAClB,GAAG,EAAE,OAAO,aAAa,EAAE,YAAY,CAAC;IACxC,IAAI,EAAE,CAAC,EAAE,EAAC,MAAM,EAAE,MAAM,EAAC,MAAM,EAAE,OAAO,CAAC,EAAC,MAAM,KAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACnE,UAAU,EAAE,CAAC,MAAM,EAAC,MAAM,KAAG,KAAK,CAAC;QAAC,KAAK,EAAC,MAAM,CAAC;QAAC,KAAK,EAAC,MAAM,CAAA;KAAC,CAAC,CAAC;IACjE,QAAQ,EAAE,CAAC,MAAM,EAAC,MAAM,KAAG,MAAM,CAAC;CACnC,iBAKH;AAED;;;;;;;;;QAoJC"}
1
+ {"version":3,"file":"session-ui.d.ts","sourceRoot":"","sources":["../src/session-ui.js"],"names":[],"mappings":"AAmBA;;;;;;;;;;GAUG;AACH,oCATW;IACN,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE;QAAC,KAAK,EAAC,MAAM,CAAC;QAAC,GAAG,EAAC,MAAM,GAAC,IAAI,CAAA;KAAC,CAAC,CAAC;IACtD,QAAQ,EAAE,OAAO,CAAC;IAClB,GAAG,EAAE,OAAO,aAAa,EAAE,YAAY,CAAC;IACxC,IAAI,EAAE,CAAC,EAAE,EAAC,MAAM,EAAE,MAAM,EAAC,MAAM,EAAE,OAAO,CAAC,EAAC,MAAM,KAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACnE,UAAU,EAAE,CAAC,MAAM,EAAC,MAAM,KAAG,KAAK,CAAC;QAAC,KAAK,EAAC,MAAM,CAAC;QAAC,KAAK,EAAC,MAAM,CAAA;KAAC,CAAC,CAAC;IACjE,QAAQ,EAAE,CAAC,MAAM,EAAC,MAAM,KAAG,MAAM,CAAC;CACnC,iBAKH;AAED;;;;;;;;;QAkLC"}
@@ -1,13 +0,0 @@
1
- <!-- [English](./migration.md) | 日本語 -->
2
-
3
- # 旧版からのマイグレーション
4
-
5
- > English: [migration.md](./migration.md)
6
-
7
- 旧 `sesame-hub3` (CLI=`hub3-ir`) からのアップグレード:
8
-
9
- - CLI 名は `sesame` に変更 (旧 `hub3-ir` は廃止)。シェルスクリプトを使っている場合は置換。
10
- - 設定ディレクトリ (`~/.config/sesame-hub3`) は変更なし、既存 config.json はそのまま使える。
11
- - `locks` キーは自動で追加される (空 `{}` から開始)。`sesame locks sync-from-devices` で `devices` コマンドの出力から取り込み可能。
12
-
13
- 旧 `.env + .tokens.json + keys.json` からの移行は `sesame migrate` がそのまま使える。
package/docs/migration.md DELETED
@@ -1,13 +0,0 @@
1
- <!-- English | [日本語](./migration.ja.md) -->
2
-
3
- # Migrating from older versions
4
-
5
- > 日本語: [migration.ja.md](./migration.ja.md)
6
-
7
- Upgrading from the old `sesame-hub3` (CLI = `hub3-ir`):
8
-
9
- - The CLI is renamed to `sesame` (the old `hub3-ir` is removed). Replace it in any shell scripts.
10
- - The config directory (`~/.config/sesame-hub3`) is unchanged; an existing config.json works as-is.
11
- - The `locks` key is added automatically (starting from an empty `{}`). Import from the `devices` command output with `sesame locks sync-from-devices`.
12
-
13
- Migrating from an old `.env` + `.tokens.json` + `keys.json` works with `sesame migrate`.