Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 168 additions & 34 deletions source/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
* (L=Shift, Y=Ctrl, X=Alt — see source/keyboard.c). No more
* SELECT-driven sticky-Ctrl state machine.
* - SELECT now emits Esc, R now toggles IME mode (EN ↔ CN).
* SELECT is overloaded: when the SSH session is dead (hard disconnect
* after lid-close sleep), pressing SELECT reconnects instead of emit-
* ting Esc — so the user can recover without exiting DSSH (M13).
* - Bottom screen is the full custom soft keyboard (source/softkb.c)
* — no more system swkbd applet popup. X key is for Alt now.
* - Touch screen drives the soft keyboard. Tapped letter goes through
Expand Down Expand Up @@ -110,11 +113,44 @@ static time_t g_last_tx_at = 0;
* scrollback peek) right before sending a key. This way the user always
* sees what they just typed, even if they were glancing at history. */
static void send_to_ssh(ssh_client_t *ssh, terminal_t *term,
const char *bytes, int n) {
const char *bytes, int n, mascot_t *mc) {
if (!ssh || !ssh_is_connected(ssh) || n <= 0) return;
if (term && term->sb_offset != 0) terminal_scroll_view(term, -term->sb_offset);
ssh_write(ssh, bytes, n);
g_last_tx_at = time(NULL);
/* Kick the typing animation once per send so the crab pecks its
* claws on every keystroke / handwriting commit / voice glyph. */
if (mc) mascot_type_kick(mc);
}

/* Establish (or re-establish) the SSH session using the loaded config.
* Used both for the initial connect at startup and for the SELECT-key
* reconnect after a hard disconnect (lid-close sleep kills the TCP).
* On success: returns a new ssh_client_t, sizes the PTY, writes the
* green "connected." banner + a status string. On failure: returns
* NULL, writes the red SSH-error banner + the diagnostic in err. */
static ssh_client_t *reconnect_ssh(const ssh_config_t *cfg,
terminal_t *term,
char *status_buf, int status_sz,
char *err, int err_sz) {
ssh_client_t *ssh = ssh_connect_pubkey(
cfg->host, cfg->port, cfg->user,
cfg->key_path, NULL,
cfg->passphrase[0] ? cfg->passphrase : NULL,
err, err_sz);

if (!ssh) {
char line[256];
snprintf(line, sizeof(line), "\x1b[31mSSH error:\x1b[0m %s\r\n", err);
terminal_write(term, line);
snprintf(status_buf, status_sz, "ssh err");
return NULL;
}

terminal_write(term, "\x1b[32mconnected.\x1b[0m\r\n");
ssh_set_pty_size(ssh, R_TOP_COLS, R_TOP_ROWS);
snprintf(status_buf, status_sz, "connected %s:%d", cfg->host, cfg->port);
return ssh;
}

int main(int argc, char *argv[]) {
Expand Down Expand Up @@ -234,25 +270,9 @@ int main(int argc, char *argv[]) {
C3D_FrameEnd(0);
}

ssh = ssh_connect_pubkey(
cfg.host, cfg.port, cfg.user,
cfg.key_path, NULL,
cfg.passphrase[0] ? cfg.passphrase : NULL,
err, sizeof(err));

if (!ssh) {
char line[256];
snprintf(line, sizeof(line), "\x1b[31mSSH error:\x1b[0m %s\r\n", err);
terminal_write(term, line);
snprintf(status_buf, sizeof(status_buf), "ssh err");
status_color = COLOR_ERR;
} else {
terminal_write(term, "\x1b[32mconnected.\x1b[0m\r\n");
ssh_set_pty_size(ssh, R_TOP_COLS, R_TOP_ROWS);
snprintf(status_buf, sizeof(status_buf), "connected %s:%d",
cfg.host, cfg.port);
status_color = COLOR_OK;
}
ssh = reconnect_ssh(&cfg, term, status_buf, sizeof(status_buf),
err, sizeof(err));
status_color = ssh ? COLOR_OK : COLOR_ERR;

idle_loop:
{
Expand Down Expand Up @@ -314,14 +334,40 @@ int main(int argc, char *argv[]) {
* AI transcribe; we open the modal once that happens. */
voice_state_t prev_state = voice_state(voice);
voice_tick(voice, ssh);
voice_state_t cur_state = voice_state(voice);
if (prev_state != VOICE_AI_SHOWING &&
voice_state(voice) == VOICE_AI_SHOWING) {
cur_state == VOICE_AI_SHOWING) {
ai_modal_open(aim,
voice_ai_question(voice),
voice_ai_answer(voice));
}
ai_modal_tick(aim);

/* Voice ↔ mascot link (level-driven, every frame). RECORDING
* → mic pose; TRANSCRIBING → ? pose; TYPING → no static pose
* (the per-glyph mascot_type_kick below drives the typing
* peck instead, same animation the soft keyboard uses). We
* must drive this every frame rather than on state edges,
* because voice_toggle() (IDLE→RECORDING) and begin_transcribe()
* (RECORDING→TRANSCRIBING) run inside the key-handler /
* voice_tick before we sample prev_state, so an edge-triggered
* link would sample prev==cur and skip the exact transition
* we need to catch. */
int rec = (cur_state == VOICE_RECORDING);
int thk = (cur_state == VOICE_TRANSCRIBING);
if (rec) { mascot_set_recording(mc, 1); mascot_set_thinking(mc, 0); }
else if (thk) { mascot_set_thinking(mc, 1); mascot_set_recording(mc, 0); }
else { mascot_set_recording(mc, 0); mascot_set_thinking(mc, 0); }

/* Voice TYPING streams glyphs straight to ssh_write inside
* voice.c, bypassing send_to_ssh — so the per-glyph typing
* kick wired into send_to_ssh wouldn't fire. Latch on the
* voice side, consume here and kick manually so the crab
* pecks in sync with each streamed character. */
if (cur_state == VOICE_TYPING && voice_consume_typed(voice)) {
mascot_type_kick(mc);
}

/* ── SSH receive ── */
if (ssh && ssh_is_connected(ssh)) {
ssh_keepalive_tick(ssh);
Expand All @@ -333,24 +379,109 @@ int main(int argc, char *argv[]) {
feed_terminal(term, rbuf, n);
last_rx_at = time(NULL);
} else if (n < 0) {
/* Hard disconnect — silent. Mascot raises ✕ via
* the ssh_dead flag below; no terminal banner. */
/* Hard disconnect. Tear down the session, mark it
* dead so the mascot raises ✕, and show a one-shot
* red banner telling the user the connection broke
* and how to recover (SELECT reconnect). This only
* fires on the 0→1 transition of ssh_dead, so it
* won't spam the terminal every frame. */
ssh_disconnect(ssh);
ssh = NULL;
ssh_dead = 1;
terminal_write(term,
"\r\n\x1b[31mConnection lost\r\n\x1b[0m"
"\x1b[33mPress SELECT to reconnect\r\n\x1b[0m");
}
}

/* ALERT = hard disconnect, OR active interactive stall. */
/* ALERT = hard disconnect, OR active interactive stall.
* While voice input is active (RECORDING/TRANSCRIBING/TYPING)
* the user is talking to the mic, not typing, so the SSH
* channel going quiet is expected. Skip the stall detector
* entirely during voice so it can neither raise nor lower
* the alert — the crab is owned by the voice-state link
* above (mic / think poses). A real hard disconnect is
* still caught the moment voice goes idle again. */
time_t now_t = time(NULL);
int interactive_stall =
ssh && ssh_is_connected(ssh) &&
g_last_tx_at > last_rx_at &&
(now_t - g_last_tx_at) > STALL_TX_NORX_S;
int want_alert = ssh_dead || interactive_stall;
if (want_alert != stall_alert) {
stall_alert = want_alert;
mascot_set_alert(mc, stall_alert);
voice_state_t vs = voice_state(voice);
int voice_busy = (vs == VOICE_RECORDING || vs == VOICE_TRANSCRIBING ||
vs == VOICE_TYPING);
if (!voice_busy) {
int interactive_stall =
ssh && ssh_is_connected(ssh) &&
g_last_tx_at > last_rx_at &&
(now_t - g_last_tx_at) > STALL_TX_NORX_S;
int want_alert = ssh_dead || interactive_stall;
if (want_alert != stall_alert) {
stall_alert = want_alert;
mascot_set_alert(mc, stall_alert);
}
}

/* ── SELECT reconnect (only when the session is dead) ──
* A hard disconnect (lid-close sleep) leaves ssh_dead=1.
* Pressing SELECT here reuses the loaded config to open a
* fresh session, so the user recovers without relaunching.
* While connected, SELECT falls through to keyboard_handle_input
* as Esc (see the input_down mask below).
*
* select_consumed records that THIS frame's SELECT triggered
* a reconnect. It can't be derived from ssh_dead because a
* successful reconnect clears ssh_dead to 0 in this same
* frame — without an independent flag, that just-used SELECT
* would slip through to keyboard_handle_input as a stray Esc
* into the freshly opened session. */
int select_consumed = 0;
if (ssh_dead && !modal_open && (down & KEY_SELECT)) {
select_consumed = 1;
terminal_write(term,
"\x1b[33mReconnecting...\x1b[0m\r\n");
/* Put the crab into its "looking up / waiting" pose before
* we flush the frame below, so the user sees it react. */
mascot_set_reconnecting(mc, 1);
/* ssh_connect_pubkey below is a blocking handshake that
* takes a few seconds. The normal per-frame render at
* the bottom of this loop won't run until it returns, so
* the line above would stay hidden in the terminal buffer
* for the whole wait. Flush one frame right now so the
* user actually sees "Reconnecting..." while the handshake
* is in flight, rather than a frozen blank screen. */
C3D_FrameBegin(C3D_FRAME_SYNCDRAW);
C2D_TargetClear(top, C2D_Color32(0x1a, 0x1b, 0x26, 0xff));
C2D_SceneBegin(top);
renderer_draw_terminal(r, term);
C2D_TargetClear(bot, C2D_Color32(0x18, 0x18, 0x25, 0xff));
C2D_SceneBegin(bot);
softkb_draw(kb, r, kbd);
/* Bottom row: clock + mascot, mirroring the main render
* path so the reconnect frame isn't missing anything. */
if (!softkb_in_debug(kb)) {
char clock_buf[24];
time_t now = time(NULL);
struct tm lt;
localtime_r(&now, &lt);
snprintf(clock_buf, sizeof(clock_buf),
"%02d-%02d %02d:%02d",
lt.tm_mon + 1, lt.tm_mday,
lt.tm_hour, lt.tm_min);
renderer_draw_text_px(2, 221, clock_buf, COLOR_DIM);
if (softkb_mascot_enabled(kb)) mascot_draw(mc);
}
C3D_FrameEnd(0);
ssh = reconnect_ssh(&cfg, term, status_buf, sizeof(status_buf),
err, sizeof(err));
mascot_set_reconnecting(mc, 0);
if (ssh) {
ssh_dead = 0;
stall_alert = 0;
mascot_celebrate(mc);
last_rx_at = time(NULL);
g_last_tx_at = last_rx_at;
} else {
mascot_sadden(mc);
terminal_write(term,
"\x1b[31mReconnect failed; press SELECT to retry.\x1b[0m\r\n");
}
}

/* ── Physical keys (Esc / Enter / BS / D-pad / R / scroll) ──
Expand All @@ -362,8 +493,11 @@ int main(int argc, char *argv[]) {
* modifier release detection coherent for the post-modal
* world) but registers no edge events. */
u32 input_down = modal_open ? 0u : down;
/* Mask SELECT when the session is dead or was just consumed by
* the reconnect above, so it isn't emitted as Esc. */
if (ssh_dead || select_consumed) input_down &= ~KEY_SELECT;
const char *out = keyboard_handle_input(kbd, term, input_down, held, cpad.dy);
if (out && !modal_open) send_to_ssh(ssh, term, out, (int)strlen(out));
if (out && !modal_open) send_to_ssh(ssh, term, out, (int)strlen(out), mc);

/* ── Touch / soft keyboard ── */
int touch_pressed = (held & KEY_TOUCH) ? 1 : 0;
Expand Down Expand Up @@ -402,7 +536,7 @@ int main(int argc, char *argv[]) {
* down-edge internally from prev_pressed. On no-touch
* frames this also runs the release-fade path. */
const char *kt = softkb_touch(kb, kbd, tx, ty, touch_pressed);
if (kt) send_to_ssh(ssh, term, kt, (int)strlen(kt));
if (kt) send_to_ssh(ssh, term, kt, (int)strlen(kt), mc);
}

/* Mascot ticks only when it's actually being shown. When
Expand Down
Loading