ccoding 0.2.1__py3-none-any.whl → 0.2.2__py3-none-any.whl

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.
ccode/web/app.js CHANGED
@@ -3,23 +3,127 @@ const sessionListEl = document.getElementById("session-list");
3
3
  const tokenFormEl = document.getElementById("token-form");
4
4
  const tokenInputEl = document.getElementById("token-input");
5
5
  const statusEl = document.getElementById("status");
6
+ const actionsEl = document.getElementById("actions");
7
+ const actionsToggleButton = document.getElementById("actions-toggle");
6
8
  const backButton = document.getElementById("back");
7
9
  const refreshButton = document.getElementById("refresh");
8
10
  const changeTokenButton = document.getElementById("change-token");
9
11
  const reconnectButton = document.getElementById("reconnect");
10
12
  const focusButton = document.getElementById("focus");
13
+ const mobileKeybarEl = document.getElementById("mobile-keybar");
14
+
15
+ const inputSequences = {
16
+ tab: "\t",
17
+ esc: "\x1b",
18
+ "ctrl-c": "\x03",
19
+ "ctrl-d": "\x04",
20
+ "ctrl-l": "\x0c",
21
+ enter: "\r",
22
+ backspace: "\x7f",
23
+ up: "\x1b[A",
24
+ down: "\x1b[B",
25
+ right: "\x1b[C",
26
+ left: "\x1b[D",
27
+ };
11
28
 
12
29
  const tokenStorageKey = "ccode.remote.adminToken";
30
+ const fontSizeStorageKey = "ccode.remote.terminalFontSize";
13
31
  let token = window.sessionStorage.getItem(tokenStorageKey) || "";
14
32
  let socket = null;
15
33
  let term = null;
16
34
  let fitAddon = null;
17
35
  let currentSession = null;
36
+ let terminalFontSize = Number(window.localStorage.getItem(fontSizeStorageKey)) || 14;
37
+ let terminalTouchScroll = null;
38
+ let terminalViewportEl = null;
39
+ let resizeFrame = null;
40
+ let viewportHeight = "";
41
+ let keyboardInset = "";
18
42
 
19
43
  function setStatus(message) {
20
44
  statusEl.textContent = message;
21
45
  }
22
46
 
47
+ function sendInput(data) {
48
+ if (!socket || socket.readyState !== WebSocket.OPEN) {
49
+ return;
50
+ }
51
+ socket.send(JSON.stringify({ type: "input", data }));
52
+ }
53
+
54
+ function focusTerminal() {
55
+ ensureTerminal();
56
+ term.focus();
57
+ }
58
+
59
+ function adjustTerminalFontSize(delta) {
60
+ const nextFontSize = Math.min(22, Math.max(10, terminalFontSize + delta));
61
+ if (nextFontSize === terminalFontSize) {
62
+ return;
63
+ }
64
+ terminalFontSize = nextFontSize;
65
+ window.localStorage.setItem(fontSizeStorageKey, String(terminalFontSize));
66
+ if (term) {
67
+ term.options.fontSize = terminalFontSize;
68
+ fitAndSendResize();
69
+ }
70
+ }
71
+
72
+ function closeActionMenu() {
73
+ actionsEl.classList.remove("open");
74
+ actionsToggleButton.setAttribute("aria-expanded", "false");
75
+ }
76
+
77
+ function beginTerminalTouchScroll(y) {
78
+ if (!term || terminalTouchScroll) {
79
+ return;
80
+ }
81
+ terminalTouchScroll = { y, remainder: 0, active: false };
82
+ }
83
+
84
+ function scrollTerminalFromTouch(y, event) {
85
+ if (!terminalTouchScroll || !term) {
86
+ return;
87
+ }
88
+ const deltaY = terminalTouchScroll.y - y;
89
+ if (!terminalTouchScroll.active && Math.abs(deltaY) < 8) {
90
+ return;
91
+ }
92
+ terminalTouchScroll.active = true;
93
+ event.preventDefault();
94
+ if (terminalViewportEl) {
95
+ terminalViewportEl.dispatchEvent(new WheelEvent("wheel", { deltaY, bubbles: true, cancelable: true }));
96
+ }
97
+ const lineHeight = Math.max(14, terminalFontSize * 1.35);
98
+ const delta = deltaY + terminalTouchScroll.remainder;
99
+ const lines = Math.trunc(delta / lineHeight);
100
+ terminalTouchScroll.remainder = delta - lines * lineHeight;
101
+ terminalTouchScroll.y = y;
102
+ if (lines) {
103
+ term.scrollLines(lines);
104
+ }
105
+ }
106
+
107
+ function endTerminalTouchScroll() {
108
+ terminalTouchScroll = null;
109
+ }
110
+
111
+ function startTerminalTouchScroll(event) {
112
+ if (event.touches.length === 1) {
113
+ beginTerminalTouchScroll(event.touches[0].clientY);
114
+ }
115
+ }
116
+
117
+ function moveTerminalTouchScroll(event) {
118
+ if (event.touches.length === 1) {
119
+ scrollTerminalFromTouch(event.touches[0].clientY, event);
120
+ }
121
+ }
122
+
123
+ function stopTerminalTouchScroll() {
124
+ endTerminalTouchScroll();
125
+ }
126
+
23
127
  function ensureTerminal() {
24
128
  if (term) {
25
129
  return;
@@ -28,7 +132,7 @@ function ensureTerminal() {
28
132
  cursorBlink: true,
29
133
  convertEol: true,
30
134
  fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace',
31
- fontSize: 14,
135
+ fontSize: terminalFontSize,
32
136
  theme: {
33
137
  background: "#05070a",
34
138
  foreground: "#d6e2f0",
@@ -39,15 +143,36 @@ function ensureTerminal() {
39
143
  fitAddon = new FitAddon.FitAddon();
40
144
  term.loadAddon(fitAddon);
41
145
  term.open(terminalEl);
42
- term.onData((data) => {
43
- if (!socket || socket.readyState !== WebSocket.OPEN) {
44
- return;
45
- }
46
- socket.send(JSON.stringify({ type: "input", data }));
146
+ terminalViewportEl = terminalEl.querySelector(".xterm-viewport");
147
+ term.onData(sendInput);
148
+ }
149
+
150
+ function updateViewportInsets() {
151
+ const viewport = window.visualViewport;
152
+ const nextViewportHeight = `${viewport ? viewport.height : window.innerHeight}px`;
153
+ const nextKeyboardInset = `${viewport ? Math.max(0, window.innerHeight - viewport.height - viewport.offsetTop) : 0}px`;
154
+ if (nextViewportHeight !== viewportHeight) {
155
+ viewportHeight = nextViewportHeight;
156
+ document.documentElement.style.setProperty("--viewport-height", viewportHeight);
157
+ }
158
+ if (nextKeyboardInset !== keyboardInset) {
159
+ keyboardInset = nextKeyboardInset;
160
+ document.documentElement.style.setProperty("--keyboard-inset", keyboardInset);
161
+ }
162
+ }
163
+
164
+ function requestFitAndSendResize() {
165
+ if (resizeFrame !== null) {
166
+ return;
167
+ }
168
+ resizeFrame = window.requestAnimationFrame(() => {
169
+ resizeFrame = null;
170
+ fitAndSendResize();
47
171
  });
48
172
  }
49
173
 
50
174
  function fitAndSendResize() {
175
+ updateViewportInsets();
51
176
  if (!term || !fitAddon || terminalEl.hidden) {
52
177
  return;
53
178
  }
@@ -71,9 +196,12 @@ function showTokenMode(message = "enter token") {
71
196
  terminalEl.hidden = true;
72
197
  sessionListEl.hidden = true;
73
198
  tokenFormEl.hidden = false;
199
+ actionsToggleButton.hidden = true;
200
+ closeActionMenu();
74
201
  backButton.hidden = true;
75
202
  focusButton.hidden = true;
76
203
  reconnectButton.hidden = true;
204
+ mobileKeybarEl.hidden = true;
77
205
  refreshButton.hidden = true;
78
206
  changeTokenButton.hidden = true;
79
207
  setStatus(message);
@@ -87,9 +215,12 @@ function showListMode() {
87
215
  tokenFormEl.hidden = true;
88
216
  terminalEl.hidden = true;
89
217
  sessionListEl.hidden = false;
218
+ actionsToggleButton.hidden = false;
219
+ closeActionMenu();
90
220
  backButton.hidden = true;
91
221
  focusButton.hidden = true;
92
222
  reconnectButton.hidden = true;
223
+ mobileKeybarEl.hidden = true;
93
224
  refreshButton.hidden = false;
94
225
  changeTokenButton.hidden = false;
95
226
  }
@@ -98,9 +229,12 @@ function showTerminalMode() {
98
229
  tokenFormEl.hidden = true;
99
230
  sessionListEl.hidden = true;
100
231
  terminalEl.hidden = false;
232
+ actionsToggleButton.hidden = false;
233
+ closeActionMenu();
101
234
  backButton.hidden = false;
102
235
  focusButton.hidden = false;
103
236
  reconnectButton.hidden = false;
237
+ mobileKeybarEl.hidden = false;
104
238
  refreshButton.hidden = true;
105
239
  changeTokenButton.hidden = false;
106
240
  }
@@ -276,13 +410,45 @@ changeTokenButton.addEventListener("click", () => {
276
410
  token = "";
277
411
  showTokenMode("enter token");
278
412
  });
279
- window.addEventListener("resize", fitAndSendResize);
413
+ window.addEventListener("resize", requestFitAndSendResize);
414
+ actionsToggleButton.addEventListener("click", () => {
415
+ const open = actionsEl.classList.toggle("open");
416
+ actionsToggleButton.setAttribute("aria-expanded", String(open));
417
+ });
418
+ actionsEl.addEventListener("click", (event) => {
419
+ if (event.target.closest("button")) {
420
+ closeActionMenu();
421
+ }
422
+ });
280
423
  backButton.addEventListener("click", loadSessions);
281
424
  refreshButton.addEventListener("click", loadSessions);
282
425
  reconnectButton.addEventListener("click", connect);
283
- focusButton.addEventListener("click", () => {
284
- ensureTerminal();
285
- term.focus();
426
+ focusButton.addEventListener("click", focusTerminal);
427
+ terminalEl.addEventListener("touchstart", startTerminalTouchScroll, { passive: true });
428
+ terminalEl.addEventListener("touchmove", moveTerminalTouchScroll, { passive: false });
429
+ terminalEl.addEventListener("touchend", stopTerminalTouchScroll);
430
+ terminalEl.addEventListener("touchcancel", stopTerminalTouchScroll);
431
+ mobileKeybarEl.addEventListener("pointerdown", (event) => {
432
+ const inputButton = event.target.closest("button[data-sequence]");
433
+ const zoomButton = event.target.closest("button[data-zoom]");
434
+ if (!inputButton && !zoomButton) {
435
+ return;
436
+ }
437
+ event.preventDefault();
438
+ if (zoomButton) {
439
+ adjustTerminalFontSize(Number(zoomButton.dataset.zoom));
440
+ return;
441
+ }
442
+ const data = inputSequences[inputButton.dataset.sequence];
443
+ if (data) {
444
+ sendInput(data);
445
+ focusTerminal();
446
+ }
286
447
  });
448
+ if (window.visualViewport) {
449
+ window.visualViewport.addEventListener("resize", requestFitAndSendResize);
450
+ window.visualViewport.addEventListener("scroll", requestFitAndSendResize);
451
+ }
452
+ updateViewportInsets();
287
453
 
288
454
  loadSessions();
ccode/web/index.html CHANGED
@@ -14,7 +14,8 @@
14
14
  <h1>ccode remote</h1>
15
15
  <p id="status">enter token</p>
16
16
  </div>
17
- <div class="actions">
17
+ <button id="actions-toggle" class="actions-toggle" type="button" hidden>Menu</button>
18
+ <div id="actions" class="actions">
18
19
  <button id="back" type="button" hidden>Sessions</button>
19
20
  <button id="refresh" type="button" hidden>Refresh</button>
20
21
  <button id="change-token" type="button" hidden>Change token</button>
@@ -31,6 +32,21 @@
31
32
  </form>
32
33
  <section id="session-list" hidden></section>
33
34
  <div id="terminal" tabindex="0" hidden></div>
35
+ <div id="mobile-keybar" class="mobile-keybar" hidden>
36
+ <button type="button" data-zoom="-1">A−</button>
37
+ <button type="button" data-zoom="1">A+</button>
38
+ <button type="button" data-sequence="tab">Tab</button>
39
+ <button type="button" data-sequence="esc">Esc</button>
40
+ <button type="button" data-sequence="ctrl-c">Ctrl+C</button>
41
+ <button type="button" data-sequence="ctrl-d">Ctrl+D</button>
42
+ <button type="button" data-sequence="ctrl-l">Ctrl+L</button>
43
+ <button type="button" data-sequence="enter">Enter</button>
44
+ <button type="button" data-sequence="backspace">⌫</button>
45
+ <button type="button" data-sequence="up">↑</button>
46
+ <button type="button" data-sequence="down">↓</button>
47
+ <button type="button" data-sequence="left">←</button>
48
+ <button type="button" data-sequence="right">→</button>
49
+ </div>
34
50
  </main>
35
51
  <script src="assets/xterm.js"></script>
36
52
  <script src="assets/addon-fit.js"></script>
ccode/web/style.css CHANGED
@@ -10,6 +10,11 @@ main {
10
10
  margin: 0;
11
11
  }
12
12
 
13
+ :root {
14
+ --keyboard-inset: 0px;
15
+ --viewport-height: 100vh;
16
+ }
17
+
13
18
  body {
14
19
  background: #090b10;
15
20
  color: #e8eef8;
@@ -47,6 +52,10 @@ p {
47
52
  justify-content: flex-end;
48
53
  }
49
54
 
55
+ .actions-toggle {
56
+ display: none;
57
+ }
58
+
50
59
  button,
51
60
  input {
52
61
  border-radius: 8px;
@@ -168,12 +177,105 @@ button[hidden],
168
177
  min-height: 0;
169
178
  overflow: hidden;
170
179
  padding: 12px;
180
+ touch-action: none;
171
181
  }
172
182
 
173
183
  #terminal:focus-within {
174
184
  outline: 2px solid #4f8cff;
175
185
  }
176
186
 
187
+ .mobile-keybar {
188
+ display: none;
189
+ }
190
+
177
191
  .xterm {
178
192
  height: 100%;
179
193
  }
194
+
195
+ @media (max-width: 720px), (pointer: coarse) {
196
+ html,
197
+ body,
198
+ main {
199
+ height: var(--viewport-height);
200
+ overflow: hidden;
201
+ }
202
+
203
+ main {
204
+ padding: 8px;
205
+ gap: 8px;
206
+ }
207
+
208
+ header {
209
+ align-items: flex-start;
210
+ gap: 8px;
211
+ position: relative;
212
+ }
213
+
214
+ .actions-toggle:not([hidden]) {
215
+ display: block;
216
+ margin-left: auto;
217
+ }
218
+
219
+ .actions {
220
+ background: rgba(9, 11, 16, 0.98);
221
+ border: 1px solid #1c2638;
222
+ border-radius: 12px;
223
+ box-shadow: 0 14px 40px rgba(0, 0, 0, 0.38);
224
+ display: none;
225
+ flex-direction: column;
226
+ min-width: 180px;
227
+ padding: 8px;
228
+ position: absolute;
229
+ right: 0;
230
+ top: calc(100% + 6px);
231
+ z-index: 20;
232
+ }
233
+
234
+ .actions.open {
235
+ display: flex;
236
+ }
237
+
238
+ .actions button {
239
+ text-align: left;
240
+ width: 100%;
241
+ }
242
+
243
+ #session-list {
244
+ overflow: auto;
245
+ }
246
+
247
+ #terminal {
248
+ margin-bottom: 58px;
249
+ min-height: 120px;
250
+ padding: 8px;
251
+ }
252
+
253
+ .mobile-keybar:not([hidden]) {
254
+ background: rgba(9, 11, 16, 0.96);
255
+ border: 1px solid #1c2638;
256
+ border-radius: 12px;
257
+ bottom: calc(8px + var(--keyboard-inset));
258
+ display: flex;
259
+ gap: 6px;
260
+ left: 8px;
261
+ max-width: calc(100vw - 16px);
262
+ overflow-x: auto;
263
+ padding: 8px;
264
+ position: fixed;
265
+ right: 8px;
266
+ scrollbar-width: none;
267
+ z-index: 10;
268
+ }
269
+
270
+ .mobile-keybar::-webkit-scrollbar {
271
+ display: none;
272
+ }
273
+
274
+ .mobile-keybar button {
275
+ flex: 0 0 auto;
276
+ min-width: 48px;
277
+ padding: 10px 12px;
278
+ touch-action: manipulation;
279
+ user-select: none;
280
+ }
281
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ccoding
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Curses-based Claude Code launcher
5
5
  Project-URL: Homepage, https://github.com/8DE4732A/ccode
6
6
  Project-URL: Repository, https://github.com/8DE4732A/ccode
@@ -57,6 +57,11 @@ Description-Content-Type: text/markdown
57
57
 
58
58
  ![](screenshot.png)
59
59
 
60
+ <p align="center">
61
+ <img src="screenshot_mobile1.jpeg" alt="Mobile remote terminal screenshot 1" width="45%" />
62
+ <img src="screenshot_mobile2.jpeg" alt="Mobile remote terminal screenshot 2" width="45%" />
63
+ </p>
64
+
60
65
  ## 功能特点
61
66
 
62
67
  - 终端 curses UI,键盘全操作
@@ -127,6 +132,15 @@ ccode-remote # 查看 remote hub / client connector 状态
127
132
 
128
133
  需要 Python >=3.10。基础 TUI 不再强制安装 remote 依赖。Remote/Web 模式需要额外依赖,并需要系统中已安装 `tmux` 和 `claude`;Windows 建议在 WSL 中使用 remote 模式。
129
134
 
135
+ 使用 Remote/Web 时,推荐在 `~/.tmux.conf` 中配置:
136
+
137
+ ```tmux
138
+ set -g history-limit 50000
139
+ set -g mouse on
140
+ set -g default-terminal "screen-256color"
141
+ set-option -ga terminal-overrides ",xterm-256color:Tc"
142
+ ```
143
+
130
144
  ```bash
131
145
  git clone https://github.com/8DE4732A/ccode
132
146
  cd ccode
@@ -12,16 +12,16 @@ ccode/remote_common.py,sha256=B2qmlS8n9Ik4-GpGtlIQQKBQM4qkOEMgXqMsijzC6Fc,6093
12
12
  ccode/remote_server.py,sha256=HjzKEZLeOzsm9Rf-hSLhzlJSIwGjpn_zLbMeHoJRiuk,11894
13
13
  ccode/ui.py,sha256=R4yKNMoMZCryoDyCHcXrGQ7sh02BYM8tEjY9ndEq4VE,37274
14
14
  ccode/web/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
- ccode/web/app.js,sha256=eedURduIk3vFCS209HVbY7woW0o7TwL6hnIKwlH50sw,8056
16
- ccode/web/index.html,sha256=6ZmLOxKXE27UOZhQnrzEEADoRcuwdk-QSpEkGLxARVM,1450
17
- ccode/web/style.css,sha256=mqBpIBsVkXvD4Ti_Z-XGplseFyYZFqtv0BTWkLHp8Q0,2599
15
+ ccode/web/app.js,sha256=w9CbPuf2uAACCaBrXRxUGWphP4Fp_XP7iHY7KSUeEag,13150
16
+ ccode/web/index.html,sha256=zLI7h4iLKXUWXvlFqD3h_HZDmnwg8P2agNKCXB9qEJY,2469
17
+ ccode/web/style.css,sha256=3RClaSS48-Ne2PQOwG5rhwbPwZKxyp-gG_OmKYRQAzo,4270
18
18
  ccode/web/assets/addon-fit.js,sha256=uj6iVs4GIKCZKhl9bJuupkgj_JPY2gep42bKmUPBhSc,1521
19
19
  ccode/web/assets/xterm-LICENSE,sha256=tWn2KdAPJiaoEA3yoXmCEFNWIeQhZN_UJqb-Wqx7DM0,1261
20
20
  ccode/web/assets/xterm-addon-fit-LICENSE,sha256=4lbwEYivUn5NBtIdBvv3ha6cUNSzKL8Dy-C6fwqkIo8,1103
21
21
  ccode/web/assets/xterm.css,sha256=hUp8D7cOixoIPBZ5ergnKZ-xh0T1rTTyJ7SDN-Myk8Y,7112
22
22
  ccode/web/assets/xterm.js,sha256=FJA1ef9UZkzXL46GmeaWGmJywhhj7Bw7EYzcivXUqXI,488663
23
- ccoding-0.2.1.dist-info/METADATA,sha256=0PYt0MFIz1Z50oy0KAuZloV-h5StBWXdJwJfxVfY1_w,18015
24
- ccoding-0.2.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
25
- ccoding-0.2.1.dist-info/entry_points.txt,sha256=uxtngaM0Eazhs4dCpcQvAIVdQBRLJ_6u-BpN3pYaGrQ,125
26
- ccoding-0.2.1.dist-info/licenses/LICENSE,sha256=ghzeEUUCG5lu2DDgFe37bgLJk_0_Y6v-ulXwmYWjQ20,1065
27
- ccoding-0.2.1.dist-info/RECORD,,
23
+ ccoding-0.2.2.dist-info/METADATA,sha256=uqOhoUzG38ibC7m7nuarEN8OwOvahN-VC6L0jszTgQ4,18444
24
+ ccoding-0.2.2.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
25
+ ccoding-0.2.2.dist-info/entry_points.txt,sha256=uxtngaM0Eazhs4dCpcQvAIVdQBRLJ_6u-BpN3pYaGrQ,125
26
+ ccoding-0.2.2.dist-info/licenses/LICENSE,sha256=ghzeEUUCG5lu2DDgFe37bgLJk_0_Y6v-ulXwmYWjQ20,1065
27
+ ccoding-0.2.2.dist-info/RECORD,,