Type into the session: hardware keys to the compositor, and a log path that always exists

The session script redirects its own output into BL_LOG with exec, and a
redirection a non-interactive shell cannot open ends that shell — so a denied
storage permission would have killed the session rather than just losing the
log. Downloads is used once proven writable, files/logs otherwise.

Keyboard events now reach wl_keyboard, which is what the client's first-run
login needs on a device with a keyboard attached.
This commit is contained in:
The412Banner committed 2026-09-19 13:49:29 -04:00
1 parent 9b3dd05f4f
commit c22cbc9f43
3 files changed
+115 -3

No files matched your search

@@ -22,6 +22,7 @@ import com.the412banner.steamdeck.core.EnvironmentComponent
import com.the412banner.steamdeck.core.FileUtils
import com.the412banner.steamdeck.core.ProcessHelper
import com.the412banner.steamdeck.gpu.TurnipDriver
import com.the412banner.steamdeck.input.EvdevKeys
import com.the412banner.steamdeck.input.FakeInputWriter
import com.the412banner.steamdeck.input.PadBridge
import com.the412banner.steamdeck.runtime.LinuxNetworkLinkComponent
@@ -173,7 +174,7 @@ class SessionActivity : Activity(), SurfaceHolder.Callback {
val sessionRoot = LinuxRuntime.sessionRoot(this).apply { mkdirs() }
stageSessionFiles(root)
val logDir = LinuxRuntime.debugLogDir().apply { mkdirs() }
val logDir = logDirectory()
val stamp = SimpleDateFormat("yyyyMMdd-HHmmss", Locale.US).format(Date())
sessionLog = File(logDir, "session-$stamp.log")
@@ -273,6 +274,29 @@ class SessionActivity : Activity(), SurfaceHolder.Callback {
Log.i(TAG, "session pid $sessionPid, log ${sessionLog.path}")
}
/**
* Where the session writes its log. Downloads is the point — a failed run is handed over as a
* folder rather than dug out of app-private storage — but the session script redirects its own
* output there with `exec`, and a redirection a non-interactive shell cannot open ends that
* shell. So a public directory is used only once it is proven writable; otherwise the app's
* own files directory, which is bound into the session anyway, stands in.
*/
private fun logDirectory(): File {
val public = LinuxRuntime.debugLogDir()
if (public.isDirectory || public.mkdirs()) {
val probe = File(public, ".writable")
try {
if (probe.createNewFile() || probe.isFile) {
probe.delete()
return public
}
} catch (ignored: Exception) {
}
}
Log.w(TAG, "$public is not writable (storage permission?); logging to files/logs")
return File(filesDir, "logs").apply { mkdirs() }
}
/**
* The libraries and scripts the session runs, refreshed from the apk at every launch.
*
@@ -361,12 +385,28 @@ class SessionActivity : Activity(), SurfaceHolder.Callback {
}
private fun sessionLogName(): String =
if (::sessionLog.isInitialized) "${LinuxRuntime.DEBUG_LOG_DIR}/${sessionLog.name}" else "-"
if (::sessionLog.isInitialized) sessionLog.path else "-"
// ── Input ───────────────────────────────────────────────────────────────────────────────
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
if (padBridge?.onKeyEvent(event) == true) return true
// A hardware keyboard, forwarded to the compositor's wl_keyboard. This is how an account
// name and password get typed on the client's first run; the client's own on-screen
// keyboard covers a device without one, driven by touch or the pad.
if (compositorStarted && event.device != null && !PadBridge.isFromController(event.device)) {
val down = event.action == KeyEvent.ACTION_DOWN
if (down || event.action == KeyEvent.ACTION_UP) {
// An unmapped key may still be a real one on a foreign layout: its scan code is
// the evdev code the kernel gave Android in the first place.
var evdev = EvdevKeys.fromKeyCode(event.keyCode)
if (evdev < 0 && event.scanCode > 0) evdev = event.scanCode
if (evdev > 0) {
WaylandCompositor.nativeSendKey(evdev, if (down) 1 else 0)
return true
}
}
}
return super.dispatchKeyEvent(event)
}
@@ -0,0 +1,72 @@
package com.the412banner.steamdeck.input;
import android.view.KeyEvent;
/**
* Android key codes to Linux evdev codes, for the keys we inject into the compositor's
* wl_keyboard. A hardware keyboard is how an account name and password are typed on the client's
* first run, so this matters before any game does.
*
* <p>evdev's order is not alphabetical: the numbers are the physical positions of a PC keyboard,
* which is what the compositor's xkb keymap is written against. Table taken from Bannerlator's
* Wayland path unchanged.
*/
public final class EvdevKeys {
private EvdevKeys() {}
public static int fromKeyCode(int kc) {
switch (kc) {
// Letters (evdev order is NOT alphabetical)
case KeyEvent.KEYCODE_A: return 30; case KeyEvent.KEYCODE_B: return 48;
case KeyEvent.KEYCODE_C: return 46; case KeyEvent.KEYCODE_D: return 32;
case KeyEvent.KEYCODE_E: return 18; case KeyEvent.KEYCODE_F: return 33;
case KeyEvent.KEYCODE_G: return 34; case KeyEvent.KEYCODE_H: return 35;
case KeyEvent.KEYCODE_I: return 23; case KeyEvent.KEYCODE_J: return 36;
case KeyEvent.KEYCODE_K: return 37; case KeyEvent.KEYCODE_L: return 38;
case KeyEvent.KEYCODE_M: return 50; case KeyEvent.KEYCODE_N: return 49;
case KeyEvent.KEYCODE_O: return 24; case KeyEvent.KEYCODE_P: return 25;
case KeyEvent.KEYCODE_Q: return 16; case KeyEvent.KEYCODE_R: return 19;
case KeyEvent.KEYCODE_S: return 31; case KeyEvent.KEYCODE_T: return 20;
case KeyEvent.KEYCODE_U: return 22; case KeyEvent.KEYCODE_V: return 47;
case KeyEvent.KEYCODE_W: return 17; case KeyEvent.KEYCODE_X: return 45;
case KeyEvent.KEYCODE_Y: return 21; case KeyEvent.KEYCODE_Z: return 44;
// Digit row
case KeyEvent.KEYCODE_1: return 2; case KeyEvent.KEYCODE_2: return 3;
case KeyEvent.KEYCODE_3: return 4; case KeyEvent.KEYCODE_4: return 5;
case KeyEvent.KEYCODE_5: return 6; case KeyEvent.KEYCODE_6: return 7;
case KeyEvent.KEYCODE_7: return 8; case KeyEvent.KEYCODE_8: return 9;
case KeyEvent.KEYCODE_9: return 10; case KeyEvent.KEYCODE_0: return 11;
// Whitespace / edit
case KeyEvent.KEYCODE_ENTER: return 28; case KeyEvent.KEYCODE_NUMPAD_ENTER: return 28;
case KeyEvent.KEYCODE_SPACE: return 57; case KeyEvent.KEYCODE_TAB: return 15;
case KeyEvent.KEYCODE_DEL: return 14; /* backspace */
case KeyEvent.KEYCODE_FORWARD_DEL: return 111; case KeyEvent.KEYCODE_ESCAPE: return 1;
// Modifiers
case KeyEvent.KEYCODE_SHIFT_LEFT: return 42; case KeyEvent.KEYCODE_SHIFT_RIGHT: return 54;
case KeyEvent.KEYCODE_CTRL_LEFT: return 29; case KeyEvent.KEYCODE_CTRL_RIGHT: return 97;
case KeyEvent.KEYCODE_ALT_LEFT: return 56; case KeyEvent.KEYCODE_ALT_RIGHT: return 100;
case KeyEvent.KEYCODE_CAPS_LOCK: return 58;
// Arrows / nav
case KeyEvent.KEYCODE_DPAD_UP: return 103; case KeyEvent.KEYCODE_DPAD_DOWN: return 108;
case KeyEvent.KEYCODE_DPAD_LEFT: return 105; case KeyEvent.KEYCODE_DPAD_RIGHT: return 106;
case KeyEvent.KEYCODE_MOVE_HOME: return 102; case KeyEvent.KEYCODE_MOVE_END: return 107;
case KeyEvent.KEYCODE_PAGE_UP: return 104; case KeyEvent.KEYCODE_PAGE_DOWN: return 109;
case KeyEvent.KEYCODE_INSERT: return 110;
// Punctuation
case KeyEvent.KEYCODE_GRAVE: return 41; case KeyEvent.KEYCODE_MINUS: return 12;
case KeyEvent.KEYCODE_EQUALS: return 13; case KeyEvent.KEYCODE_LEFT_BRACKET: return 26;
case KeyEvent.KEYCODE_RIGHT_BRACKET: return 27; case KeyEvent.KEYCODE_BACKSLASH: return 43;
case KeyEvent.KEYCODE_SEMICOLON: return 39; case KeyEvent.KEYCODE_APOSTROPHE: return 40;
case KeyEvent.KEYCODE_SLASH: return 53; case KeyEvent.KEYCODE_COMMA: return 51;
case KeyEvent.KEYCODE_PERIOD: return 52;
// Function row
case KeyEvent.KEYCODE_F1: return 59; case KeyEvent.KEYCODE_F2: return 60;
case KeyEvent.KEYCODE_F3: return 61; case KeyEvent.KEYCODE_F4: return 62;
case KeyEvent.KEYCODE_F5: return 63; case KeyEvent.KEYCODE_F6: return 64;
case KeyEvent.KEYCODE_F7: return 65; case KeyEvent.KEYCODE_F8: return 66;
case KeyEvent.KEYCODE_F9: return 67; case KeyEvent.KEYCODE_F10: return 68;
case KeyEvent.KEYCODE_F11: return 87; case KeyEvent.KEYCODE_F12: return 88;
default: return -1;
}
}
}
+1 -1
View File
@@ -17,5 +17,5 @@
<string name="catalog_unreachable">Could not reach the runtime catalog</string>
<string name="install_failed">Install failed — nothing was changed</string>
<string name="session_starting">Starting the session…</string>
<string name="session_ended">The session ended (%1$d). Log: Download/%2$s</string>
<string name="session_ended">The session ended (%1$d)\n%2$s</string>
</resources>