Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
0a67e03
feat(ssh): connect, execute and stream shells from the Rust core
iscekic Nov 13, 2026
95b848c
feat(terminal): add native terminal view on Termux and SwiftTerm
iscekic Nov 13, 2026
5bb3b8a
merge: bring the native terminal view into the SSH branch
iscekic Nov 13, 2026
8975331
fix(terminal): register the feed class constructor and rename the she…
iscekic Nov 13, 2026
3c5a25e
fix(terminal): focus the terminal for IME input and log the first tra…
iscekic Nov 13, 2026
0068580
feat(terminal): expose clipboard paste and demo it in the SSH screen
iscekic Nov 13, 2026
5568473
feat(terminal): report shell closure and fix clipboard copy and paste
iscekic Nov 13, 2026
57b805c
fix(terminal): reject input after shell closure and report transport …
iscekic Nov 13, 2026
fa931e1
Merge remote-tracking branch 'origin/main' into feat/connect
iscekic Nov 13, 2026
fb94eeb
fix(ios): compile the terminal sources and format the shell writer
iscekic Nov 13, 2026
6315051
fix(ios): disambiguate the swiftterm colour and expose the sink methods
iscekic Nov 13, 2026
52afdf0
fix(ios): build the exec record through its fields
iscekic Nov 13, 2026
15a9430
fix(ios): pass terminal bytes as slices and emit feed events as the s…
iscekic Nov 13, 2026
32553ed
refactor(ios): use the current shared object emit label
iscekic Nov 13, 2026
60c2373
fix(ios): run terminal view commands as main-queue async functions
iscekic Nov 13, 2026
b7a245c
feat(ios): bring the terminal view to parity with Android
iscekic Nov 13, 2026
e44f1e2
fix(ios): drop the terminal keyboard accessory bar
iscekic Nov 13, 2026
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
48 changes: 48 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,52 @@ jobs:
- run: cargo clippy --all-targets -- -D warnings
- run: cargo test

rust-integration:
name: Rust integration
runs-on: ubuntu-latest
defaults:
run:
working-directory: rust
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: rust

- name: Build the SSH test server
run: docker build -t ssh-core-test -f ssh-core/tests/docker/Dockerfile ssh-core/tests

- name: Start the SSH test servers
run: |
docker run -d --name ssh-core-password -p 2222:2222 ssh-core-test password
docker run -d --name ssh-core-twofactor -p 2223:2222 ssh-core-test two-factor
docker run -d --name ssh-core-silent -p 2224:2222 ssh-core-test silent
for port in 2222 2223 2224; do
for attempt in $(seq 1 30); do
if (exec 3<>"/dev/tcp/127.0.0.1/$port") 2>/dev/null; then break; fi
sleep 1
done
done

- name: Run the integration suite
env:
SSH_TEST_HOST: 127.0.0.1
run: cargo test --test integration

- name: Show the server logs on failure
if: failure()
run: |
for name in ssh-core-password ssh-core-twofactor ssh-core-silent; do
echo "== $name"
docker logs "$name" || true
done

- name: Stop the SSH test servers
if: always()
run: docker rm -f ssh-core-password ssh-core-twofactor ssh-core-silent


android:
name: Android
runs-on: ubuntu-latest
Expand Down Expand Up @@ -154,5 +200,7 @@ jobs:
-configuration Debug \
-sdk iphonesimulator \
-destination 'generic/platform=iOS Simulator' \
ARCHS=arm64 \
ONLY_ACTIVE_ARCH=YES \
CODE_SIGNING_ALLOWED=NO \
build
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,17 @@ npx expo prebuild --platform android
npx expo run:android
```

## Terminal input

Shell input and resize requests share one ordered queue. Shutdown stops accepting
new input immediately, and remote closure stops the writer. A closed channel
returns `ChannelClosed`; transport failures and writes through a closed or
unbound Android terminal are reported through `onClosed` with an error message.

The SSH integration tests run only when `SSH_TEST_HOST` is set. The
`shell_preserves_rapid_single_byte_writes` test sends 16,384 individual writes,
interleaves resizes, and compares the remote file byte for byte.

## License

Apache-2.0. See [LICENSE](./LICENSE).
18 changes: 18 additions & 0 deletions THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Third-party notices

## Termux terminal-emulator and terminal-view

`android/src/main/java/com/termux/**` and `android/src/main/res/{drawable,values}` contain source
files copied from [termux/termux-app](https://github.com/termux/termux-app) tag `v0.118.0`
(`terminal-emulator` and `terminal-view` modules), which are published under the Apache License 2.0
as an exception to the repository's GPLv3 license. See the upstream `LICENSE.md`.

Local changes: `TerminalSession` exchanges bytes through a `TerminalTransport` instead of spawning a
local pty through JNI, `TerminalView` gained `getSelectedText()` and `setSelectionColor()`,
`TerminalRenderer` paints a configurable selection background, and the two `textselection` files
import this library's `R`.

## SwiftTerm

The iOS terminal view is built on [SwiftTerm](https://github.com/migueldeicaza/SwiftTerm)
(`exactVersion 1.10.1`), MIT licensed.
4 changes: 4 additions & 0 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,8 @@ android {
dependencies {
// The uniffi Kotlin bindings call into libssh_core.so through JNA direct mapping.
implementation 'net.java.dev.jna:jna:5.17.0@aar'
// The bindings expose async functions and callback interfaces as coroutines.
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2'
implementation 'androidx.annotation:annotation:1.9.0'
implementation 'com.facebook.react:react-android'
}
10 changes: 10 additions & 0 deletions android/src/main/java/ai/kilo/reactnativessh/IdSequence.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package ai.kilo.reactnativessh

import java.util.concurrent.atomic.AtomicLong

/** Process-local identifiers for sessions, shells and prompt requests. */
internal class IdSequence(private val prefix: String) {
private val counter = AtomicLong()

fun next(): String = "$prefix${counter.incrementAndGet()}"
}
56 changes: 56 additions & 0 deletions android/src/main/java/ai/kilo/reactnativessh/JsSessionHandler.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package ai.kilo.reactnativessh

import ai.kilo.reactnativessh.uniffi.Prompt
import ai.kilo.reactnativessh.uniffi.SessionHandler

/**
* Bridges the core's session decisions to JavaScript events.
*
* Each request is registered before the event is emitted and removed as soon
* as the decision arrives, so `answerHostKey`/`answerPrompts` never see a stale
* id and a dropped connection releases the pending wait.
*/
internal class JsSessionHandler(
private val sessionId: String,
private val requestHostKey: suspend (HostKeyRequestRecord) -> Boolean,
private val requestPrompts: suspend (KeyboardInteractiveRequestRecord) -> List<String>?,
private val notifyDisconnect: (DisconnectRecord) -> Unit,
) : SessionHandler {
override suspend fun verifyHostKey(
host: String,
port: UShort,
algorithm: String,
publicKeyBase64: String,
fingerprintSha256: String,
): Boolean = requestHostKey(
HostKeyRequestRecord(
sessionId = sessionId,
requestId = requestIds.next(),
host = host,
port = port.toInt(),
algorithm = algorithm,
publicKeyBase64 = publicKeyBase64,
fingerprintSha256 = fingerprintSha256,
)
)

override suspend fun keyboardInteractive(
name: String,
instructions: String,
prompts: List<Prompt>,
): List<String>? = requestPrompts(
KeyboardInteractiveRequestRecord(
sessionId = sessionId,
requestId = requestIds.next(),
name = name,
instructions = instructions,
prompts = prompts.map { PromptRecord(it.text, it.echo) },
)
)

override fun onDisconnect(reason: String) {
notifyDisconnect(DisconnectRecord(sessionId, reason))
}

private val requestIds = IdSequence("request-")
}
Loading
Loading