{"id":51486051,"url":"https://github.com/junknet/homeattach","last_synced_at":"2026-07-07T07:00:19.821Z","repository":{"id":369823605,"uuid":"1288084122","full_name":"junknet/HomeAttach","owner":"junknet","description":"Android app: SSH into a home PC, see live abduco terminal sessions, attach with a real VT100 terminal","archived":false,"fork":false,"pushed_at":"2026-07-07T05:49:18.000Z","size":285,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-07-07T06:20:03.480Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/junknet.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-07-03T09:02:05.000Z","updated_at":"2026-07-07T05:49:22.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/junknet/HomeAttach","commit_stats":null,"previous_names":["junknet/homeattach"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/junknet/HomeAttach","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/junknet%2FHomeAttach","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/junknet%2FHomeAttach/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/junknet%2FHomeAttach/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/junknet%2FHomeAttach/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/junknet","download_url":"https://codeload.github.com/junknet/HomeAttach/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/junknet%2FHomeAttach/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35218117,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-07T02:00:07.222Z","response_time":90,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-07-07T07:00:18.568Z","updated_at":"2026-07-07T07:00:19.692Z","avatar_url":"https://github.com/junknet.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# HomeAttach\n\nAndroid app that SSHes into a home Linux PC, shows a live list of shared\nterminal sessions running there, and lets you tap one to open a real\ninteractive VT100 terminal into it — attach to whatever's running at home,\nfrom anywhere, with a real terminal (vim/htop/codex render correctly).\n\nThe PC side is `sharepty` (`server/sharepty/`, a small fork of dtach): a\ntransparent shared-pty supervisor. It is a pure byte pipe — it never\ninterprets escape sequences, never opens the alternate screen, never touches\nthe mouse — so a locally attached terminal behaves byte-for-byte like a bare\nshell. On top of that it adds what sharing actually needs:\n\n- an in-memory **ring buffer** of session output (default 8 MB) with\n  monotonically increasing byte offsets;\n- **replay on attach** (`-T \u003cbytes\u003e` tail / `-F \u003coffset\u003e` resume), so a phone\n  attaching mid-session gets recent history and can resume gap-free after a\n  network drop;\n- **passive clients** (`-P`): remote clients never drive the pty window size;\n- **focus grants** (`-W \u003ccols\u003e \u003crows\u003e`): the focused client's size is applied\n  and the child gets SIGWINCH, so full-screen TUIs repaint at the new size.\n\nSharing is **opt-in only**. Local terminal tabs are never wrapped in\nanything; a session exists only when you explicitly start one.\n\n## main\n\n- `tsess` — interactive picker for the PC: list sessions, pick a number to\n  attach, or type a new name to create one (this is the opt-in entry point).\n- `tsess-attach \u003cname\u003e [tail|from=\u003coffset\u003e]` — passive attach with replay;\n  what the app runs over SSH.\n- `tsess-list` — machine-readable TSV\n  (`name\\tcmd\\tcwd\\towner\\tcols\\trows\\tstatus`) that the app polls.\n- `tsess-focus \u003cname\u003e \u003cowner\u003e \u003ccols\u003e \u003crows\u003e` — grant focus: resize + WINCH.\n- `tsess-release \u003cname\u003e \u003cowner\u003e` — hand size ownership back to the PC client.\n- `tsess-kill \u003cname\u003e` — end a session outright (app's swipe action).\n- `tsess-state` — shared helpers + the focus/release implementation.\n\nSession lifecycle is owned by the supervisor process: when the command inside\nexits (or is killed), the socket and the ring buffer disappear with it.\n\n## main\n\n1. **Build sharepty**:\n   ```\n   cd server/sharepty \u0026\u0026 make\n   ```\n\n2. **Install**: copy the scripts and the binary somewhere on `$PATH`, e.g.:\n   ```\n   cp server/tsess server/tsess-* server/sharepty/sharepty ~/.local/bin/\n   chmod +x ~/.local/bin/tsess ~/.local/bin/tsess-* ~/.local/bin/sharepty\n   ```\n   The app execs `tsess-list`/`tsess-kill`/`tsess-focus`/`tsess-release`/\n   `tsess-watch`/`tsess-attach` from `$HOME/.local/bin`. Non-login SSH exec\n   shells do not source `.zshrc`, so relying on `PATH` is intentionally\n   avoided.\n\n3. **Optional shell alias** for quick opt-in from any tab:\n   ```sh\n   s() { ~/.local/bin/tsess; }\n   ```\n   Run `s codex`-style workflows by picking/typing a name, then start your\n   TUI inside. Everything else on the PC stays a completely bare shell.\n\n4. **Quick SSH Setup (QR Code Scan)**:\n   We provide a helper script to automatically configure SSH keys and display a configuration QR code. Simply run:\n   ```sh\n   ./server/tsess-qr-config\n   ```\n   This script generates a dedicated keypair, authorizes it, and prints a QR code in your terminal containing the full connection payload. On your phone, open the app's Settings, tap **Scan QR Code**, and scan the terminal screen. The host, port, username, and private key will auto-fill instantly!\n\n   *(Alternative manual setup)*:\n   If you prefer configuring manually, generate a keypair:\n   ```sh\n   ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_homeattach -N \"\" -C \"homeattach-android-app\"\n   cat ~/.ssh/id_ed25519_homeattach.pub \u003e\u003e ~/.ssh/authorized_keys\n   ```\n   Then paste the private key's contents into the app's Settings screen (stored in `EncryptedSharedPreferences`).\n\n5. sshd needs to be reachable from wherever the phone actually is. On the\n   phone, set the app's host to the PC's real external address, IPv4 or\n   IPv6 (note: SLAAC/privacy-extension IPv6 addresses can change — a\n   dynamic-DNS setup or a stable local ULA/WireGuard address is more\n   durable than hardcoding an address that rotates).\n\n## main\n\n- A pty has exactly one window size (\"mirror\" semantics): the focused\n  client's size wins, and every size change broadcasts a screen clear to all\n  clients so the WINCH repaint lands on a clean slate instead of layering\n  over frames drawn for the old size. A client narrower than the current\n  size still sees wrapped lines until it takes focus\n  (`tsess-attach \u003cname\u003e 256k focus`); per-client re-rendering would require\n  a server-side terminal state machine, which this design deliberately\n  avoids.\n- Replay is raw bytes. For inline-output programs (shells, Claude Code) it\n  reconstructs scrollback on the phone; for alternate-screen TUIs (codex,\n  vim) the WINCH redraw paints the current frame and the app's own\n  scrollback/transcript keys cover the rest.\n- The ring is memory owned by the supervisor: nothing is written to disk,\n  and history dies with the session (`0600` socket in\n  `$XDG_RUNTIME_DIR/homeattach-\u003cuid\u003e/`).\n\n## main\n\n```\ncd server/sharepty \u0026\u0026 make test\n```\ncovers replay, offset resume, ring overflow clamping, passive/active size\narbitration, focus WINCH redraw, input push, and socket lifecycle.\n\n## main\n\nKotlin + Jetpack Compose, `com.homeattach.app`. Build/run the usual way:\n```\n./gradlew :app:assembleDebug\nadb install -r app/build/outputs/apk/debug/app-debug.apk\n```\nKey libraries: `com.github.mwiede:jsch` (SSH; note the BouncyCastle\ndependency it needs for ed25519 keys on Android — see the comment in\n`SshClient.kt`) plus the vendored Apache-2.0 Jackpal `emulatorview` terminal\ncomponent under `app/src/main/java/jackpal/androidterm/emulatorview/`.\n\n## Public release updates\n\nThe app uses public GitHub Releases for self-update checks:\n\n- `Settings -\u003e Check update` calls GitHub's latest-release API for the\n  repository baked into the build.\n- If the latest tag is newer than `BuildConfig.VERSION_NAME`, the app\n  downloads the release APK into its private cache and opens Android's system\n  installer. Android still requires user confirmation; there is no silent\n  update path for a normal sideloaded app.\n- Host, port, username and SSH key are saved by the Android app in encrypted\n  app storage. They survive normal APK upgrades as long as the package name\n  stays `com.homeattach.app` and every upgrade is signed with the same\n  certificate. Uninstalling the app, clearing app data, or switching from a\n  debug-signed install to a differently signed release install loses that\n  local Android data.\n- Build the signed APK locally, then upload it to a public GitHub Release.\n  The APK must be signed with the same local release keystore every time, or\n  Android will reject it as a different app.\n\nLocal release signing lives in ignored `.env` or `local.properties`; use\n`.env.example` as the template.\n\nBuild and publish a public release:\n\n```sh\n./gradlew :app:assembleRelease\nsha256sum app/build/outputs/apk/release/app-release.apk\ngit tag v1.0.1\ngit push origin v1.0.1\n```\n\nThen create the GitHub Release for `v1.0.1` and upload:\n\n```text\napp/build/outputs/apk/release/app-release.apk\n```\n\nBefore making the repository public, verify that only placeholders remain:\n\n```sh\nrg -n \"BEGIN .*PRIVATE|240e:|/home/[A-Za-z0-9_-]+\" . --glob '!README.md'\ngit status --short --ignored\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjunknet%2Fhomeattach","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjunknet%2Fhomeattach","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjunknet%2Fhomeattach/lists"}