From efe6fc91d3944f0ab14df4d7bb29ee702bdc4fc6 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Mar 2026 16:56:25 +0000 Subject: [PATCH 01/15] Add screenshot feature (Ctrl+Shift+S) Save the current displayed frame as a PNG file to the current directory with a timestamped filename (scrcpy_YYYYMMDD-HHMMSS.png). Uses FFmpeg's PNG encoder with libswscale for YUV-to-RGB conversion, and applies the current display orientation to match what's shown on screen. https://claude.ai/code/session_013cas8E79mZumBkkCngSPsC --- app/meson.build | 2 + app/src/input_manager.c | 7 ++ app/src/screenshot.c | 249 ++++++++++++++++++++++++++++++++++++++++ app/src/screenshot.h | 17 +++ doc/shortcuts.md | 1 + 5 files changed, 276 insertions(+) create mode 100644 app/src/screenshot.c create mode 100644 app/src/screenshot.h diff --git a/app/meson.build b/app/meson.build index f7df69eb22..65c3ae99ea 100644 --- a/app/meson.build +++ b/app/meson.build @@ -32,6 +32,7 @@ src = [ 'src/recorder.c', 'src/scrcpy.c', 'src/screen.c', + 'src/screenshot.c', 'src/server.c', 'src/version.c', 'src/hid/hid_gamepad.c', @@ -117,6 +118,7 @@ dependencies = [ dependency('libavcodec', version: '>= 57.37', static: static), dependency('libavutil', static: static), dependency('libswresample', static: static), + dependency('libswscale', static: static), dependency('sdl2', version: '>= 2.0.5', static: static), ] diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 3e4dd0f32f..17eb8e5b2f 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -9,6 +9,7 @@ #include "android/keycodes.h" #include "input_events.h" #include "screen.h" +#include "screenshot.h" #include "shortcut_mod.h" #include "util/log.h" @@ -399,6 +400,12 @@ sc_input_manager_process_key(struct sc_input_manager *im, } } + // Ctrl+Shift+S: screenshot (independent of MOD key) + if (ctrl && shift && sdl_keycode == SDLK_s && !repeat && down && video) { + sc_screenshot_save(im->screen->frame, im->screen->orientation); + return; + } + if (is_shortcut) { enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP; switch (sdl_keycode) { diff --git a/app/src/screenshot.c b/app/src/screenshot.c new file mode 100644 index 0000000000..9ceff4ad62 --- /dev/null +++ b/app/src/screenshot.c @@ -0,0 +1,249 @@ +#include "screenshot.h" + +#include +#include + +#include +#include +#include +#include +#include + +#include "util/log.h" + +static bool +generate_filename(char *buf, size_t size) { + time_t now = time(NULL); + struct tm *tm = localtime(&now); + if (!tm) { + return false; + } + int ret = snprintf(buf, size, "scrcpy_%04d%02d%02d-%02d%02d%02d.png", + tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, + tm->tm_hour, tm->tm_min, tm->tm_sec); + return ret > 0 && (size_t) ret < size; +} + +static AVFrame * +convert_to_rgb(const AVFrame *frame) { + AVFrame *rgb_frame = av_frame_alloc(); + if (!rgb_frame) { + LOG_OOM(); + return NULL; + } + + rgb_frame->format = AV_PIX_FMT_RGB24; + rgb_frame->width = frame->width; + rgb_frame->height = frame->height; + + if (av_image_alloc(rgb_frame->data, rgb_frame->linesize, + rgb_frame->width, rgb_frame->height, + AV_PIX_FMT_RGB24, 1) < 0) { + LOG_OOM(); + av_frame_free(&rgb_frame); + return NULL; + } + + struct SwsContext *sws = sws_getContext( + frame->width, frame->height, frame->format, + rgb_frame->width, rgb_frame->height, AV_PIX_FMT_RGB24, + SWS_BILINEAR, NULL, NULL, NULL); + if (!sws) { + LOGE("Could not create sws context for screenshot"); + av_freep(&rgb_frame->data[0]); + av_frame_free(&rgb_frame); + return NULL; + } + + sws_scale(sws, (const uint8_t *const *) frame->data, frame->linesize, + 0, frame->height, rgb_frame->data, rgb_frame->linesize); + + sws_freeContext(sws); + return rgb_frame; +} + +static AVFrame * +apply_orientation(const AVFrame *frame, enum sc_orientation orientation) { + if (orientation == SC_ORIENTATION_0) { + return NULL; // no transformation needed + } + + bool swap = sc_orientation_is_swap(orientation); + bool hmirror = sc_orientation_is_mirror(orientation); + enum sc_orientation rotation = sc_orientation_get_rotation(orientation); + + int src_w = frame->width; + int src_h = frame->height; + int dst_w = swap ? src_h : src_w; + int dst_h = swap ? src_w : src_h; + + AVFrame *out = av_frame_alloc(); + if (!out) { + LOG_OOM(); + return NULL; + } + + out->format = frame->format; + out->width = dst_w; + out->height = dst_h; + + if (av_image_alloc(out->data, out->linesize, dst_w, dst_h, + out->format, 1) < 0) { + LOG_OOM(); + av_frame_free(&out); + return NULL; + } + + const uint8_t *src = frame->data[0]; + uint8_t *dst = out->data[0]; + int src_stride = frame->linesize[0]; + int dst_stride = out->linesize[0]; + int bpp = 3; // RGB24 + + for (int y = 0; y < src_h; y++) { + for (int x = 0; x < src_w; x++) { + int sx = x; + int sy = y; + + // Apply mirror first + if (hmirror) { + sx = src_w - 1 - sx; + } + + // Apply rotation + int dx, dy; + switch (rotation) { + case SC_ORIENTATION_0: + dx = sx; + dy = sy; + break; + case SC_ORIENTATION_90: + dx = src_h - 1 - sy; + dy = sx; + break; + case SC_ORIENTATION_180: + dx = src_w - 1 - sx; + dy = src_h - 1 - sy; + break; + case SC_ORIENTATION_270: + dx = sy; + dy = src_w - 1 - sx; + break; + default: + dx = sx; + dy = sy; + break; + } + + memcpy(dst + dy * dst_stride + dx * bpp, + src + y * src_stride + x * bpp, + bpp); + } + } + + return out; +} + +static bool +encode_png(const AVFrame *frame, const char *filename) { + const AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_PNG); + if (!codec) { + LOGE("PNG encoder not found"); + return false; + } + + AVCodecContext *ctx = avcodec_alloc_context3(codec); + if (!ctx) { + LOG_OOM(); + return false; + } + + ctx->width = frame->width; + ctx->height = frame->height; + ctx->pix_fmt = AV_PIX_FMT_RGB24; + ctx->time_base = (AVRational){1, 1}; + + if (avcodec_open2(ctx, codec, NULL) < 0) { + LOGE("Could not open PNG encoder"); + avcodec_free_context(&ctx); + return false; + } + + AVPacket *pkt = av_packet_alloc(); + if (!pkt) { + LOG_OOM(); + avcodec_free_context(&ctx); + return false; + } + + bool ok = false; + + int ret = avcodec_send_frame(ctx, frame); + if (ret < 0) { + LOGE("Could not send frame to PNG encoder"); + goto end; + } + + ret = avcodec_receive_packet(ctx, pkt); + if (ret < 0) { + LOGE("Could not receive packet from PNG encoder"); + goto end; + } + + FILE *f = fopen(filename, "wb"); + if (!f) { + LOGE("Could not open file: %s", filename); + goto end; + } + + if (fwrite(pkt->data, 1, pkt->size, f) != (size_t) pkt->size) { + LOGE("Could not write screenshot to %s", filename); + fclose(f); + goto end; + } + + fclose(f); + ok = true; + +end: + av_packet_free(&pkt); + avcodec_free_context(&ctx); + return ok; +} + +bool +sc_screenshot_save(const AVFrame *frame, enum sc_orientation orientation) { + if (!frame || !frame->data[0]) { + LOGW("No frame available for screenshot"); + return false; + } + + char filename[64]; + if (!generate_filename(filename, sizeof(filename))) { + LOGE("Could not generate screenshot filename"); + return false; + } + + AVFrame *rgb_frame = convert_to_rgb(frame); + if (!rgb_frame) { + return false; + } + + // Apply orientation to the RGB frame + AVFrame *oriented = apply_orientation(rgb_frame, orientation); + const AVFrame *final_frame = oriented ? oriented : rgb_frame; + + bool ok = encode_png(final_frame, filename); + if (ok) { + LOGI("Screenshot saved to %s", filename); + } + + if (oriented) { + av_freep(&oriented->data[0]); + av_frame_free(&oriented); + } + av_freep(&rgb_frame->data[0]); + av_frame_free(&rgb_frame); + + return ok; +} diff --git a/app/src/screenshot.h b/app/src/screenshot.h new file mode 100644 index 0000000000..424a49f0c6 --- /dev/null +++ b/app/src/screenshot.h @@ -0,0 +1,17 @@ +#ifndef SC_SCREENSHOT_H +#define SC_SCREENSHOT_H + +#include "common.h" + +#include +#include + +#include "options.h" + +// Save the current frame as a PNG screenshot. +// The frame is expected to be in YUV420P format. +// The orientation is applied to rotate/flip the output to match the display. +bool +sc_screenshot_save(const AVFrame *frame, enum sc_orientation orientation); + +#endif diff --git a/doc/shortcuts.md b/doc/shortcuts.md index d22eb47304..17741783d3 100644 --- a/doc/shortcuts.md +++ b/doc/shortcuts.md @@ -52,6 +52,7 @@ _[Super] is typically the Windows or Cmd key._ | Synchronize clipboards and paste⁵ | MOD+v | Inject computer clipboard text | MOD+Shift+v | Open keyboard settings (HID keyboard only) | MOD+k + | Save screenshot | Ctrl+Shift+s | Enable/disable FPS counter (on stdout) | MOD+i | Pinch-to-zoom/rotate | Ctrl+_click-and-move_ | Tilt vertically (slide with 2 fingers) | Shift+_click-and-move_ From b5faac80b24fe03cf3a21e26a0cf9af86f76e050 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Mar 2026 17:02:16 +0000 Subject: [PATCH 02/15] Save screenshots to ~/Downloads instead of current directory https://claude.ai/code/session_013cas8E79mZumBkkCngSPsC --- app/src/screenshot.c | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/src/screenshot.c b/app/src/screenshot.c index 9ceff4ad62..04700878b5 100644 --- a/app/src/screenshot.c +++ b/app/src/screenshot.c @@ -1,6 +1,8 @@ #include "screenshot.h" #include +#include +#include #include #include @@ -13,12 +15,19 @@ static bool generate_filename(char *buf, size_t size) { + const char *home = getenv("HOME"); + if (!home) { + home = "."; + } + time_t now = time(NULL); struct tm *tm = localtime(&now); if (!tm) { return false; } - int ret = snprintf(buf, size, "scrcpy_%04d%02d%02d-%02d%02d%02d.png", + int ret = snprintf(buf, size, + "%s/Downloads/scrcpy_%04d%02d%02d-%02d%02d%02d.png", + home, tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec); return ret > 0 && (size_t) ret < size; From d25dea2408d70f0df65905456dbcecf24310eba4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Mar 2026 19:30:57 +0000 Subject: [PATCH 03/15] Add screenshot flash overlay and debug logging - White flash fades out over 300ms when screenshot is captured - Fix filename buffer (64 -> 512) to fit ~/Downloads path - Add LOGD debug logs throughout screenshot save flow - Add flash state to display struct with timing via sc_tick https://claude.ai/code/session_013cas8E79mZumBkkCngSPsC --- app/src/display.c | 25 +++++++++++++++++++++++++ app/src/display.h | 8 ++++++++ app/src/input_manager.c | 10 +++++++++- app/src/screenshot.c | 20 +++++++++++++++++++- 4 files changed, 61 insertions(+), 2 deletions(-) diff --git a/app/src/display.c b/app/src/display.c index 15f9a1f19e..1152ade22c 100644 --- a/app/src/display.c +++ b/app/src/display.c @@ -346,6 +346,31 @@ sc_display_render(struct sc_display *display, const SDL_Rect *geometry, } } + // Draw screenshot flash overlay if active + if (display->flash_active) { + sc_tick elapsed = sc_tick_now() - display->flash_start; + sc_tick duration = SC_TICK_FROM_MS(300); + if (elapsed < duration) { + // Fade from white (alpha 180) to transparent over 300ms + uint8_t alpha = (uint8_t) (180 * (duration - elapsed) / duration); + SDL_SetRenderDrawBlendMode(display->renderer, SDL_BLENDMODE_BLEND); + SDL_SetRenderDrawColor(display->renderer, 255, 255, 255, alpha); + SDL_RenderFillRect(display->renderer, NULL); + LOGD("Screenshot flash: alpha=%d, elapsed=%" PRItick "ms", + alpha, SC_TICK_TO_MS(elapsed)); + } else { + display->flash_active = false; + LOGD("Screenshot flash finished"); + } + } + SDL_RenderPresent(display->renderer); return SC_DISPLAY_RESULT_OK; } + +void +sc_display_flash(struct sc_display *display) { + display->flash_start = sc_tick_now(); + display->flash_active = true; + LOGD("Screenshot flash triggered"); +} diff --git a/app/src/display.h b/app/src/display.h index 49110994f9..2671381071 100644 --- a/app/src/display.h +++ b/app/src/display.h @@ -11,6 +11,7 @@ #include "coords.h" #include "opengl.h" #include "options.h" +#include "util/tick.h" #ifdef __APPLE__ # define SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE @@ -36,6 +37,10 @@ struct sc_display { } pending; bool has_frame; + + // Screenshot flash overlay + sc_tick flash_start; + bool flash_active; }; enum sc_display_result { @@ -61,4 +66,7 @@ enum sc_display_result sc_display_render(struct sc_display *display, const SDL_Rect *geometry, enum sc_orientation orientation); +void +sc_display_flash(struct sc_display *display); + #endif diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 17eb8e5b2f..d7baf4dd4e 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -402,7 +402,15 @@ sc_input_manager_process_key(struct sc_input_manager *im, // Ctrl+Shift+S: screenshot (independent of MOD key) if (ctrl && shift && sdl_keycode == SDLK_s && !repeat && down && video) { - sc_screenshot_save(im->screen->frame, im->screen->orientation); + LOGD("Screenshot shortcut triggered (Ctrl+Shift+S)"); + bool ok = sc_screenshot_save(im->screen->frame, + im->screen->orientation); + if (ok) { + LOGD("Screenshot saved successfully, triggering flash overlay"); + sc_display_flash(&im->screen->display); + } else { + LOGD("Screenshot save failed"); + } return; } diff --git a/app/src/screenshot.c b/app/src/screenshot.c index 04700878b5..9e40bf8881 100644 --- a/app/src/screenshot.c +++ b/app/src/screenshot.c @@ -222,29 +222,47 @@ encode_png(const AVFrame *frame, const char *filename) { bool sc_screenshot_save(const AVFrame *frame, enum sc_orientation orientation) { + LOGD("Screenshot save requested (orientation=%d)", orientation); + if (!frame || !frame->data[0]) { LOGW("No frame available for screenshot"); return false; } - char filename[64]; + LOGD("Frame available: %dx%d format=%d", + frame->width, frame->height, frame->format); + + char filename[512]; if (!generate_filename(filename, sizeof(filename))) { LOGE("Could not generate screenshot filename"); return false; } + LOGD("Screenshot target path: %s", filename); + AVFrame *rgb_frame = convert_to_rgb(frame); if (!rgb_frame) { return false; } + LOGD("Converted to RGB: %dx%d", rgb_frame->width, rgb_frame->height); + // Apply orientation to the RGB frame AVFrame *oriented = apply_orientation(rgb_frame, orientation); const AVFrame *final_frame = oriented ? oriented : rgb_frame; + if (oriented) { + LOGD("Orientation applied: %dx%d", oriented->width, oriented->height); + } else { + LOGD("No orientation transform needed"); + } + + LOGD("Encoding PNG..."); bool ok = encode_png(final_frame, filename); if (ok) { LOGI("Screenshot saved to %s", filename); + } else { + LOGE("Failed to encode/save screenshot"); } if (oriented) { From eef0cc02674143d32dd2c371d2aec00a3deec63d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Mar 2026 19:42:13 +0000 Subject: [PATCH 04/15] Make screenshot flash more visible - full white for 100ms then fade over 500ms https://claude.ai/code/session_013cas8E79mZumBkkCngSPsC --- app/src/display.c | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/app/src/display.c b/app/src/display.c index 1152ade22c..2fa137f6fd 100644 --- a/app/src/display.c +++ b/app/src/display.c @@ -349,10 +349,17 @@ sc_display_render(struct sc_display *display, const SDL_Rect *geometry, // Draw screenshot flash overlay if active if (display->flash_active) { sc_tick elapsed = sc_tick_now() - display->flash_start; - sc_tick duration = SC_TICK_FROM_MS(300); + sc_tick duration = SC_TICK_FROM_MS(500); if (elapsed < duration) { - // Fade from white (alpha 180) to transparent over 300ms - uint8_t alpha = (uint8_t) (180 * (duration - elapsed) / duration); + // Full white for first 100ms, then fade out + uint8_t alpha; + sc_tick fade_start = SC_TICK_FROM_MS(100); + if (elapsed < fade_start) { + alpha = 255; + } else { + alpha = (uint8_t) (255 * (duration - elapsed) + / (duration - fade_start)); + } SDL_SetRenderDrawBlendMode(display->renderer, SDL_BLENDMODE_BLEND); SDL_SetRenderDrawColor(display->renderer, 255, 255, 255, alpha); SDL_RenderFillRect(display->renderer, NULL); From b36935f14aed1c6148493555490760858a1cfde2 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Mar 2026 19:45:53 +0000 Subject: [PATCH 05/15] Add Ctrl+Shift+L shortcut to open logcat in a new terminal window Opens adb logcat in Terminal.app on Mac, or x-terminal-emulator/xterm/ gnome-terminal on Linux. https://claude.ai/code/session_013cas8E79mZumBkkCngSPsC --- app/src/input_manager.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index d7baf4dd4e..4d2dde0f4b 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -400,6 +400,20 @@ sc_input_manager_process_key(struct sc_input_manager *im, } } + // Ctrl+Shift+L: open logcat in a new terminal window + if (ctrl && shift && sdl_keycode == SDLK_l && !repeat && down) { + LOGI("Opening logcat in new terminal window..."); +#ifdef __APPLE__ + system("osascript -e 'tell application \"Terminal\" to do script " + "\"adb logcat\"' &"); +#else + system("x-terminal-emulator -e adb logcat &" + " || xterm -e adb logcat &" + " || gnome-terminal -- adb logcat &"); +#endif + return; + } + // Ctrl+Shift+S: screenshot (independent of MOD key) if (ctrl && shift && sdl_keycode == SDLK_s && !repeat && down && video) { LOGD("Screenshot shortcut triggered (Ctrl+Shift+S)"); From 2eafb947a9dbd59d25b542fce59afbc915878969 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Mar 2026 19:48:06 +0000 Subject: [PATCH 06/15] Auto-detect foreground app for logcat filtering (Ctrl+Shift+L) Queries the device for the currently resumed activity and opens logcat filtered to that app's PID. Falls back to unfiltered logcat if detection fails. https://claude.ai/code/session_013cas8E79mZumBkkCngSPsC --- app/src/input_manager.c | 51 +++++++++++++++++++++++++++++++++++------ 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 4d2dde0f4b..2afa9dbc95 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -400,17 +400,54 @@ sc_input_manager_process_key(struct sc_input_manager *im, } } - // Ctrl+Shift+L: open logcat in a new terminal window + // Ctrl+Shift+L: open logcat filtered to foreground app if (ctrl && shift && sdl_keycode == SDLK_l && !repeat && down) { - LOGI("Opening logcat in new terminal window..."); + // Get the foreground app's package name from the device + FILE *fp = popen( + "adb shell \"dumpsys activity activities" + " | grep mResumedActivity" + " | head -1" + " | sed 's|.* \\([^ ]*/\\).*|\\1|'" + " | sed 's|/||'\"", "r"); + char pkg[256] = {0}; + if (fp) { + if (fgets(pkg, sizeof(pkg), fp)) { + // Strip trailing whitespace/newline + size_t len = strlen(pkg); + while (len > 0 && (pkg[len-1] == '\n' || pkg[len-1] == '\r' + || pkg[len-1] == ' ')) { + pkg[--len] = '\0'; + } + } + pclose(fp); + } + + if (pkg[0]) { + LOGI("Opening logcat for foreground app: %s", pkg); + char cmd[1024]; #ifdef __APPLE__ - system("osascript -e 'tell application \"Terminal\" to do script " - "\"adb logcat\"' &"); + snprintf(cmd, sizeof(cmd), + "osascript -e 'tell application \"Terminal\" to do script " + "\"adb logcat --pid=$(adb shell pidof %s)\"' &", pkg); #else - system("x-terminal-emulator -e adb logcat &" - " || xterm -e adb logcat &" - " || gnome-terminal -- adb logcat &"); + snprintf(cmd, sizeof(cmd), + "x-terminal-emulator -e sh -c " + "\"adb logcat --pid=\\$(adb shell pidof %s)\" &" + " || xterm -e sh -c " + "\"adb logcat --pid=\\$(adb shell pidof %s)\" &", + pkg, pkg); #endif + system(cmd); + } else { + LOGI("Could not detect foreground app, opening unfiltered logcat"); +#ifdef __APPLE__ + system("osascript -e 'tell application \"Terminal\" to do script " + "\"adb logcat\"' &"); +#else + system("x-terminal-emulator -e adb logcat &" + " || xterm -e adb logcat &"); +#endif + } return; } From 92a3d645c49c8078591fd6036d7104ff056c5990 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Mar 2026 19:50:44 +0000 Subject: [PATCH 07/15] Log all D-pad key events (UP/DOWN/LEFT/RIGHT/CENTER press/release) Prints [DPAD] direction PRESS/RELEASE with repeat count to the terminal, useful for debugging navigation in Android apps. https://claude.ai/code/session_013cas8E79mZumBkkCngSPsC --- app/src/keyboard_sdk.c | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/app/src/keyboard_sdk.c b/app/src/keyboard_sdk.c index 466a1aebba..648d9b3d54 100644 --- a/app/src/keyboard_sdk.c +++ b/app/src/keyboard_sdk.c @@ -289,6 +289,25 @@ sc_key_processor_process_key(struct sc_key_processor *kp, struct sc_control_msg msg; if (convert_input_key(event, &msg, kb->key_inject_mode, kb->repeat)) { + // Log D-pad events for debugging + enum android_keycode kc = msg.inject_keycode.keycode; + const char *dpad_name = NULL; + switch (kc) { + case AKEYCODE_DPAD_UP: dpad_name = "UP"; break; + case AKEYCODE_DPAD_DOWN: dpad_name = "DOWN"; break; + case AKEYCODE_DPAD_LEFT: dpad_name = "LEFT"; break; + case AKEYCODE_DPAD_RIGHT: dpad_name = "RIGHT"; break; + case AKEYCODE_DPAD_CENTER: dpad_name = "CENTER"; break; + default: break; + } + if (dpad_name) { + const char *action_str = + msg.inject_keycode.action == AKEY_EVENT_ACTION_DOWN + ? "PRESS" : "RELEASE"; + LOGI("[DPAD] %s %s (repeat=%d)", dpad_name, action_str, + (int) kb->repeat); + } + if (!sc_controller_push_msg(kb->controller, &msg)) { LOGW("Could not request 'inject keycode'"); } From 1716daef25a78b3d3dc2f5fc69dea26d19e11852 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Mar 2026 19:52:17 +0000 Subject: [PATCH 08/15] Query and log focused view after each D-pad event After each D-pad key release, asynchronously queries the device for the currently focused view/window and logs it as [FOCUS]. Helps debug how the app responds to D-pad navigation. https://claude.ai/code/session_013cas8E79mZumBkkCngSPsC --- app/src/keyboard_sdk.c | 48 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/app/src/keyboard_sdk.c b/app/src/keyboard_sdk.c index 648d9b3d54..e29e5f19b5 100644 --- a/app/src/keyboard_sdk.c +++ b/app/src/keyboard_sdk.c @@ -2,7 +2,9 @@ #include #include +#include #include +#include #include #include @@ -267,6 +269,45 @@ convert_input_key(const struct sc_key_event *event, struct sc_control_msg *msg, return true; } +static void * +query_focused_view(void *arg) { + (void) arg; + + // Small delay to let the app process the key event + struct timespec ts = {0, 50000000}; // 50ms + nanosleep(&ts, NULL); + + // Query the currently focused view from the device + FILE *fp = popen( + "adb shell dumpsys activity top 2>/dev/null" + " | grep -E 'mCurrentFocus|mFocusedView|focus'", "r"); + if (!fp) { + return NULL; + } + + char line[512]; + while (fgets(line, sizeof(line), fp)) { + // Strip trailing newline + size_t len = strlen(line); + if (len > 0 && line[len - 1] == '\n') { + line[len - 1] = '\0'; + } + // Skip empty lines + if (line[0] == '\0') { + continue; + } + // Trim leading whitespace for cleaner output + char *p = line; + while (*p == ' ') p++; + if (*p) { + LOGI("[FOCUS] %s", p); + } + } + + pclose(fp); + return NULL; +} + static void sc_key_processor_process_key(struct sc_key_processor *kp, const struct sc_key_event *event, @@ -306,6 +347,13 @@ sc_key_processor_process_key(struct sc_key_processor *kp, ? "PRESS" : "RELEASE"; LOGI("[DPAD] %s %s (repeat=%d)", dpad_name, action_str, (int) kb->repeat); + + // On key release, query what the app focused + if (msg.inject_keycode.action == AKEY_EVENT_ACTION_UP) { + pthread_t tid; + pthread_create(&tid, NULL, query_focused_view, NULL); + pthread_detach(tid); + } } if (!sc_controller_push_msg(kb->controller, &msg)) { From 4ee43db080006fa09548acc66ddc34a068442757 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Mar 2026 19:55:53 +0000 Subject: [PATCH 09/15] Show actual focused UI element after each D-pad event Uses uiautomator to query which view has focus after each D-pad key release. Logs the resource ID, class name, text content, and bounds of the focused element, e.g.: [FOCUS] com.app:id/settings_btn (Button) text="Settings" [0,100][200,150] https://claude.ai/code/session_013cas8E79mZumBkkCngSPsC --- app/src/keyboard_sdk.c | 83 +++++++++++++++++++++++++++++++++++------- 1 file changed, 70 insertions(+), 13 deletions(-) diff --git a/app/src/keyboard_sdk.c b/app/src/keyboard_sdk.c index e29e5f19b5..0006a72485 100644 --- a/app/src/keyboard_sdk.c +++ b/app/src/keyboard_sdk.c @@ -273,34 +273,91 @@ static void * query_focused_view(void *arg) { (void) arg; - // Small delay to let the app process the key event - struct timespec ts = {0, 50000000}; // 50ms + // Delay to let the app process the key event + struct timespec ts = {0, 100000000}; // 100ms nanosleep(&ts, NULL); - // Query the currently focused view from the device + // Use uiautomator to find the focused element - this gives us + // the actual view ID, class, text, and bounds FILE *fp = popen( - "adb shell dumpsys activity top 2>/dev/null" - " | grep -E 'mCurrentFocus|mFocusedView|focus'", "r"); + "adb shell uiautomator dump /dev/tty 2>/dev/null" + " | tr '>' '\\n'" + " | grep 'focused=\"true\"'", "r"); if (!fp) { return NULL; } - char line[512]; + char line[1024]; while (fgets(line, sizeof(line), fp)) { - // Strip trailing newline size_t len = strlen(line); if (len > 0 && line[len - 1] == '\n') { line[len - 1] = '\0'; } - // Skip empty lines if (line[0] == '\0') { continue; } - // Trim leading whitespace for cleaner output - char *p = line; - while (*p == ' ') p++; - if (*p) { - LOGI("[FOCUS] %s", p); + + // Parse out useful attributes: resource-id, class, text, bounds + char *rid = strstr(line, "resource-id=\""); + char *cls = strstr(line, "class=\""); + char *txt = strstr(line, "text=\""); + char *bnd = strstr(line, "bounds=\""); + + char id_buf[128] = "?"; + char cls_buf[128] = "?"; + char txt_buf[128] = ""; + char bnd_buf[64] = ""; + + if (rid) { + rid += 13; // skip resource-id=" + char *end = strchr(rid, '"'); + if (end) { + size_t n = (size_t)(end - rid); + if (n >= sizeof(id_buf)) n = sizeof(id_buf) - 1; + memcpy(id_buf, rid, n); + id_buf[n] = '\0'; + } + } + if (cls) { + cls += 7; // skip class=" + char *end = strchr(cls, '"'); + if (end) { + // Get just the simple class name (after last dot) + char *dot = end; + while (dot > cls && *dot != '.') dot--; + if (*dot == '.') dot++; + size_t n = (size_t)(end - dot); + if (n >= sizeof(cls_buf)) n = sizeof(cls_buf) - 1; + memcpy(cls_buf, dot, n); + cls_buf[n] = '\0'; + } + } + if (txt) { + txt += 6; // skip text=" + char *end = strchr(txt, '"'); + if (end && end != txt) { + size_t n = (size_t)(end - txt); + if (n >= sizeof(txt_buf)) n = sizeof(txt_buf) - 1; + memcpy(txt_buf, txt, n); + txt_buf[n] = '\0'; + } + } + if (bnd) { + bnd += 8; // skip bounds=" + char *end = strchr(bnd, '"'); + if (end) { + size_t n = (size_t)(end - bnd); + if (n >= sizeof(bnd_buf)) n = sizeof(bnd_buf) - 1; + memcpy(bnd_buf, bnd, n); + bnd_buf[n] = '\0'; + } + } + + if (txt_buf[0]) { + LOGI("[FOCUS] %s (%s) text=\"%s\" %s", + id_buf, cls_buf, txt_buf, bnd_buf); + } else { + LOGI("[FOCUS] %s (%s) %s", id_buf, cls_buf, bnd_buf); } } From 74067cddf8661bf2f3c137ddd72f7e17ed65cf7a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 00:02:22 +0000 Subject: [PATCH 10/15] Enhanced dpad logging: show focus transitions, window, and element details Now logs: - Focus transitions: "Focus moved: btn_alarm (Button) -> btn_settings (Button)" - Focused window/activity name - Element details: bounds, package, content-desc, selected/enabled/clickable/scrollable - Direction context: "[DPAD RIGHT] Focus moved: ..." - Previous vs current focus comparison to detect unchanged focus https://claude.ai/code/session_013cas8E79mZumBkkCngSPsC --- app/src/keyboard_sdk.c | 221 ++++++++++++++++++++++++++--------------- 1 file changed, 140 insertions(+), 81 deletions(-) diff --git a/app/src/keyboard_sdk.c b/app/src/keyboard_sdk.c index 0006a72485..0d7937c6ce 100644 --- a/app/src/keyboard_sdk.c +++ b/app/src/keyboard_sdk.c @@ -269,96 +269,152 @@ convert_input_key(const struct sc_key_event *event, struct sc_control_msg *msg, return true; } +// Store previous focus state so we can show transitions +static char prev_focus_id[128] = ""; +static char prev_focus_cls[128] = ""; +static char prev_focus_txt[128] = ""; + +// Parse a single XML attribute value into dst buffer +static void +parse_xml_attr(const char *line, const char *attr_name, char *dst, size_t dst_size) { + dst[0] = '\0'; + char *p = strstr(line, attr_name); + if (!p) return; + p += strlen(attr_name); + char *end = strchr(p, '"'); + if (!end) return; + size_t n = (size_t)(end - p); + if (n >= dst_size) n = dst_size - 1; + memcpy(dst, p, n); + dst[n] = '\0'; +} + +// Get just the simple class name after the last dot +static void +simplify_class_name(char *cls) { + char *dot = strrchr(cls, '.'); + if (dot) { + memmove(cls, dot + 1, strlen(dot + 1) + 1); + } +} + +// Format a focus element as a readable string +static void +format_focus_str(char *dst, size_t dst_size, + const char *id, const char *cls, const char *txt) { + if (txt[0]) { + snprintf(dst, dst_size, "%s (%s) \"%s\"", id[0] ? id : "(no-id)", cls, txt); + } else { + snprintf(dst, dst_size, "%s (%s)", id[0] ? id : "(no-id)", cls); + } +} + +struct dpad_query_ctx { + char direction[16]; +}; + static void * query_focused_view(void *arg) { - (void) arg; + struct dpad_query_ctx *ctx = (struct dpad_query_ctx *)arg; + char direction[16]; + snprintf(direction, sizeof(direction), "%s", ctx->direction); + free(ctx); // Delay to let the app process the key event - struct timespec ts = {0, 100000000}; // 100ms + struct timespec ts = {0, 150000000}; // 150ms nanosleep(&ts, NULL); - // Use uiautomator to find the focused element - this gives us - // the actual view ID, class, text, and bounds + // 1) Log the focused window/activity FILE *fp = popen( + "adb shell dumpsys window 2>/dev/null" + " | grep -E 'mCurrentFocus|mFocusedWindow'", "r"); + if (fp) { + char line[512]; + while (fgets(line, sizeof(line), fp)) { + size_t len = strlen(line); + if (len > 0 && line[len - 1] == '\n') line[len - 1] = '\0'; + char *p = line; + while (*p == ' ') p++; + if (*p) { + LOGI("[WINDOW] %s", p); + } + } + pclose(fp); + } + + // 2) Query focused element via uiautomator + fp = popen( "adb shell uiautomator dump /dev/tty 2>/dev/null" " | tr '>' '\\n'" " | grep 'focused=\"true\"'", "r"); if (!fp) { + LOGW("[FOCUS] Could not query UI hierarchy"); return NULL; } - char line[1024]; + char line[2048]; + bool found_focus = false; while (fgets(line, sizeof(line), fp)) { size_t len = strlen(line); - if (len > 0 && line[len - 1] == '\n') { - line[len - 1] = '\0'; - } - if (line[0] == '\0') { - continue; - } - - // Parse out useful attributes: resource-id, class, text, bounds - char *rid = strstr(line, "resource-id=\""); - char *cls = strstr(line, "class=\""); - char *txt = strstr(line, "text=\""); - char *bnd = strstr(line, "bounds=\""); - - char id_buf[128] = "?"; - char cls_buf[128] = "?"; - char txt_buf[128] = ""; - char bnd_buf[64] = ""; - - if (rid) { - rid += 13; // skip resource-id=" - char *end = strchr(rid, '"'); - if (end) { - size_t n = (size_t)(end - rid); - if (n >= sizeof(id_buf)) n = sizeof(id_buf) - 1; - memcpy(id_buf, rid, n); - id_buf[n] = '\0'; - } - } - if (cls) { - cls += 7; // skip class=" - char *end = strchr(cls, '"'); - if (end) { - // Get just the simple class name (after last dot) - char *dot = end; - while (dot > cls && *dot != '.') dot--; - if (*dot == '.') dot++; - size_t n = (size_t)(end - dot); - if (n >= sizeof(cls_buf)) n = sizeof(cls_buf) - 1; - memcpy(cls_buf, dot, n); - cls_buf[n] = '\0'; - } - } - if (txt) { - txt += 6; // skip text=" - char *end = strchr(txt, '"'); - if (end && end != txt) { - size_t n = (size_t)(end - txt); - if (n >= sizeof(txt_buf)) n = sizeof(txt_buf) - 1; - memcpy(txt_buf, txt, n); - txt_buf[n] = '\0'; - } - } - if (bnd) { - bnd += 8; // skip bounds=" - char *end = strchr(bnd, '"'); - if (end) { - size_t n = (size_t)(end - bnd); - if (n >= sizeof(bnd_buf)) n = sizeof(bnd_buf) - 1; - memcpy(bnd_buf, bnd, n); - bnd_buf[n] = '\0'; + if (len > 0 && line[len - 1] == '\n') line[len - 1] = '\0'; + if (line[0] == '\0') continue; + + char id[128], cls[128], txt[128], bnd[64], desc[256], pkg[128]; + char selected[16], enabled[16], clickable[16], scrollable[16]; + + parse_xml_attr(line, "resource-id=\"", id, sizeof(id)); + parse_xml_attr(line, "class=\"", cls, sizeof(cls)); + parse_xml_attr(line, "text=\"", txt, sizeof(txt)); + parse_xml_attr(line, "bounds=\"", bnd, sizeof(bnd)); + parse_xml_attr(line, "content-desc=\"", desc, sizeof(desc)); + parse_xml_attr(line, "package=\"", pkg, sizeof(pkg)); + parse_xml_attr(line, "selected=\"", selected, sizeof(selected)); + parse_xml_attr(line, "enabled=\"", enabled, sizeof(enabled)); + parse_xml_attr(line, "clickable=\"", clickable, sizeof(clickable)); + parse_xml_attr(line, "scrollable=\"", scrollable, sizeof(scrollable)); + + simplify_class_name(cls); + + // Build current focus description + char curr_str[256]; + format_focus_str(curr_str, sizeof(curr_str), id, cls, txt); + + // Build previous focus description + char prev_str[256]; + format_focus_str(prev_str, sizeof(prev_str), + prev_focus_id, prev_focus_cls, prev_focus_txt); + + // Show the transition + if (prev_focus_cls[0]) { + if (strcmp(prev_focus_id, id) != 0 + || strcmp(prev_focus_txt, txt) != 0) { + LOGI("[DPAD %s] Focus moved: %s -> %s", + direction, prev_str, curr_str); + } else { + LOGI("[DPAD %s] Focus unchanged: %s", direction, curr_str); } + } else { + LOGI("[DPAD %s] Focus: %s", direction, curr_str); } - if (txt_buf[0]) { - LOGI("[FOCUS] %s (%s) text=\"%s\" %s", - id_buf, cls_buf, txt_buf, bnd_buf); - } else { - LOGI("[FOCUS] %s (%s) %s", id_buf, cls_buf, bnd_buf); + // Log element details + LOGI("[FOCUS] bounds=%s pkg=%s", bnd, pkg); + if (desc[0]) { + LOGI("[FOCUS] content-desc=\"%s\"", desc); } + LOGI("[FOCUS] selected=%s enabled=%s clickable=%s scrollable=%s", + selected, enabled, clickable, scrollable); + + // Save for next comparison + snprintf(prev_focus_id, sizeof(prev_focus_id), "%s", id); + snprintf(prev_focus_cls, sizeof(prev_focus_cls), "%s", cls); + snprintf(prev_focus_txt, sizeof(prev_focus_txt), "%s", txt); + + found_focus = true; + } + + if (!found_focus) { + LOGW("[DPAD %s] No focused element found in UI hierarchy", direction); } pclose(fp); @@ -399,17 +455,20 @@ sc_key_processor_process_key(struct sc_key_processor *kp, default: break; } if (dpad_name) { - const char *action_str = - msg.inject_keycode.action == AKEY_EVENT_ACTION_DOWN - ? "PRESS" : "RELEASE"; - LOGI("[DPAD] %s %s (repeat=%d)", dpad_name, action_str, - (int) kb->repeat); - - // On key release, query what the app focused - if (msg.inject_keycode.action == AKEY_EVENT_ACTION_UP) { - pthread_t tid; - pthread_create(&tid, NULL, query_focused_view, NULL); - pthread_detach(tid); + if (msg.inject_keycode.action == AKEY_EVENT_ACTION_DOWN) { + LOGI("[DPAD] %s PRESS (repeat=%d)", dpad_name, + (int) kb->repeat); + } else { + LOGI("[DPAD] %s RELEASE -> querying focus...", dpad_name); + // On release, spawn thread to query what changed + struct dpad_query_ctx *ctx = malloc(sizeof(*ctx)); + if (ctx) { + snprintf(ctx->direction, sizeof(ctx->direction), + "%s", dpad_name); + pthread_t tid; + pthread_create(&tid, NULL, query_focused_view, ctx); + pthread_detach(tid); + } } } From e3ea6d6bd5bcc9ab94ec2d9730243a3f511892e2 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 00:15:55 +0000 Subject: [PATCH 11/15] Replace uiautomator dump with dumpsys activity top for dpad focus tracking uiautomator dump was unreliable - it never found focused elements in many apps. Switch to parsing the View Hierarchy from dumpsys activity top, which marks the focused view with a leading asterisk (*). Also removed unused XML parsing helpers. https://claude.ai/code/session_013cas8E79mZumBkkCngSPsC --- app/src/keyboard_sdk.c | 147 ++++++++++++++++------------------------- 1 file changed, 58 insertions(+), 89 deletions(-) diff --git a/app/src/keyboard_sdk.c b/app/src/keyboard_sdk.c index 0d7937c6ce..515cd10eac 100644 --- a/app/src/keyboard_sdk.c +++ b/app/src/keyboard_sdk.c @@ -271,33 +271,9 @@ convert_input_key(const struct sc_key_event *event, struct sc_control_msg *msg, // Store previous focus state so we can show transitions static char prev_focus_id[128] = ""; -static char prev_focus_cls[128] = ""; +static char prev_focus_cls[1024] = ""; static char prev_focus_txt[128] = ""; -// Parse a single XML attribute value into dst buffer -static void -parse_xml_attr(const char *line, const char *attr_name, char *dst, size_t dst_size) { - dst[0] = '\0'; - char *p = strstr(line, attr_name); - if (!p) return; - p += strlen(attr_name); - char *end = strchr(p, '"'); - if (!end) return; - size_t n = (size_t)(end - p); - if (n >= dst_size) n = dst_size - 1; - memcpy(dst, p, n); - dst[n] = '\0'; -} - -// Get just the simple class name after the last dot -static void -simplify_class_name(char *cls) { - char *dot = strrchr(cls, '.'); - if (dot) { - memmove(cls, dot + 1, strlen(dot + 1) + 1); - } -} - // Format a focus element as a readable string static void format_focus_str(char *dst, size_t dst_size, @@ -321,75 +297,81 @@ query_focused_view(void *arg) { free(ctx); // Delay to let the app process the key event - struct timespec ts = {0, 150000000}; // 150ms + struct timespec ts = {0, 200000000}; // 200ms nanosleep(&ts, NULL); - // 1) Log the focused window/activity + // Use dumpsys activity top to find the focused view. + // In the View Hierarchy, focused views are marked with a leading asterisk. FILE *fp = popen( - "adb shell dumpsys window 2>/dev/null" - " | grep -E 'mCurrentFocus|mFocusedWindow'", "r"); - if (fp) { - char line[512]; - while (fgets(line, sizeof(line), fp)) { - size_t len = strlen(line); - if (len > 0 && line[len - 1] == '\n') line[len - 1] = '\0'; - char *p = line; - while (*p == ' ') p++; - if (*p) { - LOGI("[WINDOW] %s", p); - } - } - pclose(fp); - } - - // 2) Query focused element via uiautomator - fp = popen( - "adb shell uiautomator dump /dev/tty 2>/dev/null" - " | tr '>' '\\n'" - " | grep 'focused=\"true\"'", "r"); + "adb shell dumpsys activity top 2>/dev/null", "r"); if (!fp) { - LOGW("[FOCUS] Could not query UI hierarchy"); + LOGW("[FOCUS] Could not run dumpsys activity top"); return NULL; } char line[2048]; + bool in_view_hierarchy = false; bool found_focus = false; + char focused_line[1024] = ""; + while (fgets(line, sizeof(line), fp)) { size_t len = strlen(line); if (len > 0 && line[len - 1] == '\n') line[len - 1] = '\0'; - if (line[0] == '\0') continue; - char id[128], cls[128], txt[128], bnd[64], desc[256], pkg[128]; - char selected[16], enabled[16], clickable[16], scrollable[16]; + // Detect start of View Hierarchy section + if (strstr(line, "View Hierarchy:")) { + in_view_hierarchy = true; + continue; + } - parse_xml_attr(line, "resource-id=\"", id, sizeof(id)); - parse_xml_attr(line, "class=\"", cls, sizeof(cls)); - parse_xml_attr(line, "text=\"", txt, sizeof(txt)); - parse_xml_attr(line, "bounds=\"", bnd, sizeof(bnd)); - parse_xml_attr(line, "content-desc=\"", desc, sizeof(desc)); - parse_xml_attr(line, "package=\"", pkg, sizeof(pkg)); - parse_xml_attr(line, "selected=\"", selected, sizeof(selected)); - parse_xml_attr(line, "enabled=\"", enabled, sizeof(enabled)); - parse_xml_attr(line, "clickable=\"", clickable, sizeof(clickable)); - parse_xml_attr(line, "scrollable=\"", scrollable, sizeof(scrollable)); + // End of View Hierarchy on blank line or next section + if (in_view_hierarchy && line[0] != ' ' && line[0] != '\0' + && line[0] != '*') { + // If we already found focus, stop + if (found_focus) break; + in_view_hierarchy = false; + continue; + } - simplify_class_name(cls); + if (!in_view_hierarchy) continue; + + // Look for lines containing "hasFocus" or starting with asterisk + // The focused view line typically looks like: + // * com.example.app/com.example.ClassName{...} + // or contains hasFocus=true in the properties + char *trimmed = line; + while (*trimmed == ' ') trimmed++; + + if (trimmed[0] == '*') { + // This is the focused view + snprintf(focused_line, sizeof(focused_line), "%s", trimmed + 1); + // Trim leading space after asterisk + char *fl = focused_line; + while (*fl == ' ') fl++; + if (fl != focused_line) { + memmove(focused_line, fl, strlen(fl) + 1); + } + found_focus = true; + } + } + pclose(fp); - // Build current focus description - char curr_str[256]; - format_focus_str(curr_str, sizeof(curr_str), id, cls, txt); + if (found_focus) { + // Parse the view info from the focused line + // Format is typically: ClassName{hash VFED..C. ........ X,Y-X,Y #id app:id/name} + char curr_str[1024]; + snprintf(curr_str, sizeof(curr_str), "%s", focused_line); // Build previous focus description char prev_str[256]; format_focus_str(prev_str, sizeof(prev_str), prev_focus_id, prev_focus_cls, prev_focus_txt); - // Show the transition + // Check if focus changed if (prev_focus_cls[0]) { - if (strcmp(prev_focus_id, id) != 0 - || strcmp(prev_focus_txt, txt) != 0) { + if (strcmp(prev_focus_cls, curr_str) != 0) { LOGI("[DPAD %s] Focus moved: %s -> %s", - direction, prev_str, curr_str); + direction, prev_focus_cls, curr_str); } else { LOGI("[DPAD %s] Focus unchanged: %s", direction, curr_str); } @@ -397,27 +379,14 @@ query_focused_view(void *arg) { LOGI("[DPAD %s] Focus: %s", direction, curr_str); } - // Log element details - LOGI("[FOCUS] bounds=%s pkg=%s", bnd, pkg); - if (desc[0]) { - LOGI("[FOCUS] content-desc=\"%s\"", desc); - } - LOGI("[FOCUS] selected=%s enabled=%s clickable=%s scrollable=%s", - selected, enabled, clickable, scrollable); - - // Save for next comparison - snprintf(prev_focus_id, sizeof(prev_focus_id), "%s", id); - snprintf(prev_focus_cls, sizeof(prev_focus_cls), "%s", cls); - snprintf(prev_focus_txt, sizeof(prev_focus_txt), "%s", txt); - - found_focus = true; - } - - if (!found_focus) { - LOGW("[DPAD %s] No focused element found in UI hierarchy", direction); + // Save current as previous (store full line in cls for comparison) + snprintf(prev_focus_cls, sizeof(prev_focus_cls), "%s", curr_str); + prev_focus_id[0] = '\0'; + prev_focus_txt[0] = '\0'; + } else { + LOGW("[DPAD %s] No focused view found in View Hierarchy", direction); } - pclose(fp); return NULL; } From d35f1c176a18943933f362751143172a7a8a1758 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 00:22:17 +0000 Subject: [PATCH 12/15] Read focus info from app's own logcat instead of dumpsys dumpsys activity top wasn't finding focused views either. The app already logs focus state via logcat with tag DPAD (e.g. "focus=MaterialButton(id=preset3m)"). Now we read that directly with "adb logcat -d -s DPAD:D" which is the most accurate source since it comes from the app's own focus handling. https://claude.ai/code/session_013cas8E79mZumBkkCngSPsC --- app/src/keyboard_sdk.c | 119 +++++++++++++++-------------------------- 1 file changed, 44 insertions(+), 75 deletions(-) diff --git a/app/src/keyboard_sdk.c b/app/src/keyboard_sdk.c index 515cd10eac..d9774efc49 100644 --- a/app/src/keyboard_sdk.c +++ b/app/src/keyboard_sdk.c @@ -270,20 +270,7 @@ convert_input_key(const struct sc_key_event *event, struct sc_control_msg *msg, } // Store previous focus state so we can show transitions -static char prev_focus_id[128] = ""; static char prev_focus_cls[1024] = ""; -static char prev_focus_txt[128] = ""; - -// Format a focus element as a readable string -static void -format_focus_str(char *dst, size_t dst_size, - const char *id, const char *cls, const char *txt) { - if (txt[0]) { - snprintf(dst, dst_size, "%s (%s) \"%s\"", id[0] ? id : "(no-id)", cls, txt); - } else { - snprintf(dst, dst_size, "%s (%s)", id[0] ? id : "(no-id)", cls); - } -} struct dpad_query_ctx { char direction[16]; @@ -296,95 +283,77 @@ query_focused_view(void *arg) { snprintf(direction, sizeof(direction), "%s", ctx->direction); free(ctx); - // Delay to let the app process the key event - struct timespec ts = {0, 200000000}; // 200ms + // Delay to let the app process the key event and write logcat + struct timespec ts = {0, 300000000}; // 300ms nanosleep(&ts, NULL); - // Use dumpsys activity top to find the focused view. - // In the View Hierarchy, focused views are marked with a leading asterisk. + // Read the app's own DPAD logs from logcat. + // The app logs lines like: + // D DPAD: dispatchKeyEvent: key=KEYCODE_DPAD_LEFT action=UP + // dest=Timer focus=MaterialButton(id=preset3m) + // We grab the most recent focus= value from action=UP lines. FILE *fp = popen( - "adb shell dumpsys activity top 2>/dev/null", "r"); + "adb logcat -d -s DPAD:D 2>/dev/null" + " | grep 'action=UP'" + " | tail -1", "r"); if (!fp) { - LOGW("[FOCUS] Could not run dumpsys activity top"); + LOGW("[FOCUS] Could not read logcat"); return NULL; } char line[2048]; - bool in_view_hierarchy = false; - bool found_focus = false; - char focused_line[1024] = ""; + char focus_info[512] = ""; + char dest_info[128] = ""; - while (fgets(line, sizeof(line), fp)) { + if (fgets(line, sizeof(line), fp)) { size_t len = strlen(line); if (len > 0 && line[len - 1] == '\n') line[len - 1] = '\0'; - // Detect start of View Hierarchy section - if (strstr(line, "View Hierarchy:")) { - in_view_hierarchy = true; - continue; - } - - // End of View Hierarchy on blank line or next section - if (in_view_hierarchy && line[0] != ' ' && line[0] != '\0' - && line[0] != '*') { - // If we already found focus, stop - if (found_focus) break; - in_view_hierarchy = false; - continue; + // Extract focus=... from the line + char *f = strstr(line, "focus="); + if (f) { + f += 6; // skip "focus=" + snprintf(focus_info, sizeof(focus_info), "%s", f); } - if (!in_view_hierarchy) continue; - - // Look for lines containing "hasFocus" or starting with asterisk - // The focused view line typically looks like: - // * com.example.app/com.example.ClassName{...} - // or contains hasFocus=true in the properties - char *trimmed = line; - while (*trimmed == ' ') trimmed++; - - if (trimmed[0] == '*') { - // This is the focused view - snprintf(focused_line, sizeof(focused_line), "%s", trimmed + 1); - // Trim leading space after asterisk - char *fl = focused_line; - while (*fl == ' ') fl++; - if (fl != focused_line) { - memmove(focused_line, fl, strlen(fl) + 1); + // Extract dest=... from the line + char *d = strstr(line, "dest="); + if (d) { + d += 5; // skip "dest=" + char *space = strchr(d, ' '); + if (space) { + size_t n = (size_t)(space - d); + if (n >= sizeof(dest_info)) n = sizeof(dest_info) - 1; + memcpy(dest_info, d, n); + dest_info[n] = '\0'; + } else { + snprintf(dest_info, sizeof(dest_info), "%s", d); } - found_focus = true; } } pclose(fp); - if (found_focus) { - // Parse the view info from the focused line - // Format is typically: ClassName{hash VFED..C. ........ X,Y-X,Y #id app:id/name} - char curr_str[1024]; - snprintf(curr_str, sizeof(curr_str), "%s", focused_line); - - // Build previous focus description - char prev_str[256]; - format_focus_str(prev_str, sizeof(prev_str), - prev_focus_id, prev_focus_cls, prev_focus_txt); - - // Check if focus changed + if (focus_info[0]) { + // Show transition if (prev_focus_cls[0]) { - if (strcmp(prev_focus_cls, curr_str) != 0) { + if (strcmp(prev_focus_cls, focus_info) != 0) { LOGI("[DPAD %s] Focus moved: %s -> %s", - direction, prev_focus_cls, curr_str); + direction, prev_focus_cls, focus_info); } else { - LOGI("[DPAD %s] Focus unchanged: %s", direction, curr_str); + LOGI("[DPAD %s] Focus unchanged: %s", direction, focus_info); } } else { - LOGI("[DPAD %s] Focus: %s", direction, curr_str); + LOGI("[DPAD %s] Focus: %s", direction, focus_info); + } + + if (dest_info[0]) { + LOGI("[DPAD %s] Screen: %s", direction, dest_info); } - // Save current as previous (store full line in cls for comparison) - snprintf(prev_focus_cls, sizeof(prev_focus_cls), "%s", curr_str); - prev_focus_id[0] = '\0'; - prev_focus_txt[0] = '\0'; + // Save for next comparison + snprintf(prev_focus_cls, sizeof(prev_focus_cls), "%s", focus_info); } else { - LOGW("[DPAD %s] No focused view found in View Hierarchy", direction); + LOGW("[DPAD %s] No focus info found in app logcat", direction); } return NULL; From 747f73f32aca6d39cc86e804e94d08b3f3c0e89a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 19:29:06 +0000 Subject: [PATCH 13/15] Fix DPAD focus query to use dumpsys instead of app-specific logcat The previous approach used `adb logcat -d -s DPAD:D` which only worked if the foreground app logged focus info with a custom "DPAD" tag. Most apps (including the launcher) don't do this, causing "No focus info found in app logcat" warnings on every DPAD press. Now uses `dumpsys activity top` to query the actual focused view from the system, which works universally with any app. Falls back to `dumpsys window windows` if no view-level focus is found. https://claude.ai/code/session_013cas8E79mZumBkkCngSPsC --- app/src/keyboard_sdk.c | 95 ++++++++++++++++++++++++++---------------- 1 file changed, 58 insertions(+), 37 deletions(-) diff --git a/app/src/keyboard_sdk.c b/app/src/keyboard_sdk.c index d9774efc49..4e08d21f6e 100644 --- a/app/src/keyboard_sdk.c +++ b/app/src/keyboard_sdk.c @@ -283,57 +283,58 @@ query_focused_view(void *arg) { snprintf(direction, sizeof(direction), "%s", ctx->direction); free(ctx); - // Delay to let the app process the key event and write logcat + // Delay to let the app process the key event struct timespec ts = {0, 300000000}; // 300ms nanosleep(&ts, NULL); - // Read the app's own DPAD logs from logcat. - // The app logs lines like: - // D DPAD: dispatchKeyEvent: key=KEYCODE_DPAD_LEFT action=UP - // dest=Timer focus=MaterialButton(id=preset3m) - // We grab the most recent focus= value from action=UP lines. + // Use dumpsys activity top to get the currently focused view. + // This works universally with any app, unlike logcat-based approaches + // which require the app to log focus info with a specific tag. + // + // We look for "mCurrentFocus" or "mFocused" lines in the view hierarchy, + // and also grab the activity/fragment info from the top of the output. FILE *fp = popen( - "adb logcat -d -s DPAD:D 2>/dev/null" - " | grep 'action=UP'" - " | tail -1", "r"); + "adb shell dumpsys activity top 2>/dev/null" + " | grep -E '(mCurrentFocus|mFocusedView|ACTIVITY|mFocused|" + "getCurrentFocus|isFocused.*true)'" + " | tail -5", "r"); if (!fp) { - LOGW("[FOCUS] Could not read logcat"); + LOGW("[FOCUS] Could not run dumpsys"); return NULL; } - char line[2048]; char focus_info[512] = ""; - char dest_info[128] = ""; + char activity_info[256] = ""; + char line[2048]; - if (fgets(line, sizeof(line), fp)) { + while (fgets(line, sizeof(line), fp)) { size_t len = strlen(line); if (len > 0 && line[len - 1] == '\n') line[len - 1] = '\0'; - // Extract focus=... from the line - char *f = strstr(line, "focus="); - if (f) { - f += 6; // skip "focus=" - snprintf(focus_info, sizeof(focus_info), "%s", f); + // Grab ACTIVITY line for context + if (strstr(line, "ACTIVITY")) { + // Trim leading whitespace + char *p = line; + while (*p == ' ' || *p == '\t') p++; + snprintf(activity_info, sizeof(activity_info), "%s", p); + continue; } - // Extract dest=... from the line - char *d = strstr(line, "dest="); - if (d) { - d += 5; // skip "dest=" - char *space = strchr(d, ' '); - if (space) { - size_t n = (size_t)(space - d); - if (n >= sizeof(dest_info)) n = sizeof(dest_info) - 1; - memcpy(dest_info, d, n); - dest_info[n] = '\0'; - } else { - snprintf(dest_info, sizeof(dest_info), "%s", d); - } + // Grab focus info - prefer mFocusedView or isFocused lines + if (strstr(line, "mFocused") || strstr(line, "isFocused") + || strstr(line, "mCurrentFocus") + || strstr(line, "getCurrentFocus")) { + char *p = line; + while (*p == ' ' || *p == '\t') p++; + snprintf(focus_info, sizeof(focus_info), "%s", p); } } pclose(fp); if (focus_info[0]) { + if (activity_info[0]) { + LOGI("[DPAD %s] %s", direction, activity_info); + } // Show transition if (prev_focus_cls[0]) { if (strcmp(prev_focus_cls, focus_info) != 0) { @@ -345,15 +346,35 @@ query_focused_view(void *arg) { } else { LOGI("[DPAD %s] Focus: %s", direction, focus_info); } - - if (dest_info[0]) { - LOGI("[DPAD %s] Screen: %s", direction, dest_info); - } - // Save for next comparison snprintf(prev_focus_cls, sizeof(prev_focus_cls), "%s", focus_info); } else { - LOGW("[DPAD %s] No focus info found in app logcat", direction); + // Fallback: try dumpsys window to get at least the focused window + FILE *fp2 = popen( + "adb shell dumpsys window windows 2>/dev/null" + " | grep -E 'mCurrentFocus|mFocusedApp'" + " | head -2", "r"); + if (fp2) { + char win_info[512] = ""; + while (fgets(line, sizeof(line), fp2)) { + size_t l = strlen(line); + if (l > 0 && line[l - 1] == '\n') line[l - 1] = '\0'; + char *p = line; + while (*p == ' ' || *p == '\t') p++; + if (*p) { + snprintf(win_info, sizeof(win_info), "%s", p); + } + } + pclose(fp2); + + if (win_info[0]) { + LOGI("[DPAD %s] Window: %s", direction, win_info); + } else { + LOGW("[DPAD %s] No focus info available", direction); + } + } else { + LOGW("[DPAD %s] No focus info available", direction); + } } return NULL; From 7c5da277a8eac393b75087f6c6ee71ee281cb965 Mon Sep 17 00:00:00 2001 From: Akiva Jeger Date: Fri, 24 Apr 2026 17:16:52 +0300 Subject: [PATCH 14/15] Add labelled toolbar above the mirror with screenshot/logs buttons Reserves a 72-px strip at the top of the scrcpy window and draws four square buttons in the top-right corner, each with a tiny icon and a 3-letter text label rendered from an embedded 5x7 bitmap font: [SYS] full system log dump (adb logcat -d -b all) [APP] foreground-app log dump (adb logcat -d --pid=...) [LOG] open live logcat in Terminal (same as Ctrl+Shift+L) [CAM] screenshot to ~/Downloads + auto-copy PNG to clipboard Click the buttons to trigger the actions. All sizing/resize logic in screen.c is taught about the toolbar so the mirrored content stays correctly centered below it. Mouse events over the toolbar strip are swallowed so they never reach the device. Screenshot saves now also copy the PNG to the system clipboard (osascript on macOS, xclip/wl-copy on Linux) so images can be pasted straight into Slack/Messages/etc. README documents the Dock-shortcut setup for macOS. --- README.md | 80 ++++++++++++ app/src/display.c | 283 +++++++++++++++++++++++++++++++++++++++- app/src/display.h | 7 +- app/src/input_manager.c | 252 ++++++++++++++++++++++++++++------- app/src/screen.c | 104 +++++++++++++-- app/src/screen.h | 10 ++ app/src/screenshot.c | 42 ++++++ 7 files changed, 717 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index 2ee767368a..afbbf641a6 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,86 @@ Note that USB debugging is not required to run scrcpy in [OTG mode](doc/otg.md). - [Windows](doc/windows.md) (read [how to run](doc/windows.md#run)) - [macOS](doc/macos.md) +## Fork features (this repo) + +This fork adds a small toolbar above the mirrored device screen with four +buttons (left → right): + + 1. **Dump full system logs** — runs `adb logcat -d -b all` and writes the + output to `~/Downloads/scrcpy_syslogs_.txt`. Unfiltered, all + buffers (main, system, crash, events, radio, …). + 2. **Dump foreground-app logs** — runs `adb logcat -d --pid=$(adb shell pidof + )` and writes to + `~/Downloads/scrcpy_logs_.txt`. Falls back to the full buffer + if the foreground package can't be detected. + 3. **Open live logcat** — opens a Terminal window streaming `adb logcat + --pid=…` for the foreground app (same as `Ctrl+Shift+L`). + 4. **Screenshot** — saves a PNG to `~/Downloads/scrcpy_.png` **and + copies it to the system clipboard** so you can paste it straight into + Slack/Messages/etc. (same as `Ctrl+Shift+S`). + +All dumps are point-in-time snapshots of the device's in-memory log ring +buffers; older entries that have already been overwritten are not recoverable +(Android doesn't persist logs to disk). Run `adb logcat -g` to see the exact +buffer sizes on your device. + +### macOS: launch scrcpy from the Dock + +A minimal `Scrcpy.app` bundle lets you launch the build from the Dock without +opening a terminal. To set it up after cloning and building (`meson setup +build && ninja -C build`): + +```bash +# 1. Create the .app skeleton +mkdir -p "/Applications/Scrcpy.app/Contents/MacOS" \ + "/Applications/Scrcpy.app/Contents/Resources" + +# 2. Info.plist +cat > "/Applications/Scrcpy.app/Contents/Info.plist" <<'PLIST' + + + + + CFBundleExecutablescrcpy-launcher + CFBundleIdentifiercom.local.scrcpy + CFBundleNameScrcpy + CFBundleDisplayNameScrcpy + CFBundleVersion1.0 + CFBundleShortVersionString1.0 + CFBundlePackageTypeAPPL + CFBundleIconFileicon + + +PLIST + +# 3. Launcher script — edit the cd path and PATH to match your setup. +# PATH must include adb (Finder-launched apps don't inherit your shell PATH). +cat > "/Applications/Scrcpy.app/Contents/MacOS/scrcpy-launcher" <<'SH' +#!/usr/bin/env bash +export PATH="$HOME/Library/Android/sdk/platform-tools:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:$PATH" +cd "$HOME/scrcpy" # ← adjust to where you cloned this repo +exec ./run build "$@" +SH +chmod +x "/Applications/Scrcpy.app/Contents/MacOS/scrcpy-launcher" + +# 4. Icon (converts the repo icon to .icns) +cp app/data/icon.png "/Applications/Scrcpy.app/Contents/Resources/icon.png" +sips -s format icns "/Applications/Scrcpy.app/Contents/Resources/icon.png" \ + --out "/Applications/Scrcpy.app/Contents/Resources/icon.icns" + +# 5. Refresh Finder's icon cache, then launch once +touch /Applications/Scrcpy.app +open /Applications/Scrcpy.app +``` + +Once it's running, right-click the Dock icon → **Options → Keep in Dock** to +pin it. From then on, clicking the Dock icon runs `./run build` from the repo +and opens the mirrored device window. + +If the Dock icon "opens and immediately quits", it usually means `adb` isn't +on the `PATH` baked into the launcher — edit `scrcpy-launcher` and add the +directory where `which adb` points. + ## Must-know tips diff --git a/app/src/display.c b/app/src/display.c index 2fa137f6fd..9d69a042f8 100644 --- a/app/src/display.c +++ b/app/src/display.c @@ -2,11 +2,110 @@ #include #include +#include +#include #include #include #include "util/log.h" +// -------- Mini 5x7 bitmap font for toolbar button labels -------- +// +// Each glyph occupies 5 columns × 7 rows. Rows are encoded low-5-bits, +// bit 4 = leftmost pixel. + +struct sc_mini_glyph { + char c; + uint8_t rows[7]; +}; + +static const struct sc_mini_glyph SC_MINI_FONT[] = { + {'A', {0x0E, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11}}, + {'C', {0x0E, 0x11, 0x10, 0x10, 0x10, 0x11, 0x0E}}, + {'G', {0x0E, 0x11, 0x10, 0x17, 0x11, 0x11, 0x0E}}, + {'L', {0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1F}}, + {'M', {0x11, 0x1B, 0x15, 0x15, 0x11, 0x11, 0x11}}, + {'O', {0x0E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E}}, + {'P', {0x1E, 0x11, 0x11, 0x1E, 0x10, 0x10, 0x10}}, + {'S', {0x0F, 0x10, 0x10, 0x0E, 0x01, 0x01, 0x1E}}, + {'Y', {0x11, 0x11, 0x0A, 0x04, 0x04, 0x04, 0x04}}, +}; + +static const uint8_t * +sc_mini_font_lookup(char c) { + for (size_t i = 0; + i < sizeof(SC_MINI_FONT) / sizeof(SC_MINI_FONT[0]); ++i) { + if (SC_MINI_FONT[i].c == c) { + return SC_MINI_FONT[i].rows; + } + } + return NULL; +} + +// Width in pixels of a string rendered at the given scale (1px gap between +// glyphs). +static int +sc_mini_text_width(const char *s, int scale) { + int n = 0; + for (const char *p = s; *p; ++p) { + ++n; + } + if (n == 0) { + return 0; + } + return (n * 5 + (n - 1)) * scale; +} + +static void +sc_mini_draw_text(SDL_Renderer *r, int x, int y, int scale, const char *s) { + int cx = x; + for (const char *p = s; *p; ++p) { + const uint8_t *glyph = sc_mini_font_lookup(*p); + if (glyph) { + for (int row = 0; row < 7; ++row) { + uint8_t bits = glyph[row]; + for (int col = 0; col < 5; ++col) { + if (bits & (1 << (4 - col))) { + SDL_Rect px = { + .x = cx + col * scale, + .y = y + row * scale, + .w = scale, + .h = scale, + }; + SDL_RenderFillRect(r, &px); + } + } + } + } + cx += 6 * scale; // 5 px glyph + 1 px gap + } +} + +// Draw a label centered inside (x, y, w, h), picking the largest integer +// scale that still fits with a small margin. +static void +sc_mini_draw_button_label(SDL_Renderer *r, int x, int y, int w, int h, + const char *text) { + if (h < 9 || w < 6 || !text || !*text) { + return; + } + int unit_w = sc_mini_text_width(text, 1); + if (unit_w <= 0) { + return; + } + int scale_by_h = (h - 2) / 7; + int scale_by_w = (w - 2) / unit_w; + int scale = scale_by_h < scale_by_w ? scale_by_h : scale_by_w; + if (scale < 1) { + scale = 1; + } + int tw = sc_mini_text_width(text, scale); + int th = 7 * scale; + int tx = x + (w - tw) / 2; + int ty = y + (h - th) / 2; + sc_mini_draw_text(r, tx, ty, scale, text); +} + static bool sc_display_init_novideo_icon(struct sc_display *display, SDL_Surface *icon_novideo) { @@ -300,7 +399,12 @@ sc_display_update_texture(struct sc_display *display, const AVFrame *frame) { enum sc_display_result sc_display_render(struct sc_display *display, const SDL_Rect *geometry, - enum sc_orientation orientation) { + enum sc_orientation orientation, + const SDL_Rect *toolbar_bg, + const SDL_Rect *toolbar_button, + const SDL_Rect *toolbar_logs_button, + const SDL_Rect *toolbar_save_logs_button, + const SDL_Rect *toolbar_save_all_logs_button) { SDL_RenderClear(display->renderer); if (display->pending.flags) { @@ -346,6 +450,183 @@ sc_display_render(struct sc_display *display, const SDL_Rect *geometry, } } + // Draw toolbar (dark strip with labelled buttons) + if (toolbar_bg) { + SDL_SetRenderDrawBlendMode(display->renderer, SDL_BLENDMODE_NONE); + SDL_SetRenderDrawColor(display->renderer, 40, 40, 40, 255); + SDL_RenderFillRect(display->renderer, toolbar_bg); + + // Each button reserves the bottom ~38% of its rect for a text label. +#define SC_BTN_LABEL_NUM 2 +#define SC_BTN_LABEL_DEN 5 +#define SC_BTN_GAP_NUM 1 +#define SC_BTN_GAP_DEN 20 + + // Helper: split a button rect into (icon_area, label_area). + #define SPLIT_BUTTON(BTN, ICON, LBL) \ + int _lh = (BTN)->h * SC_BTN_LABEL_NUM / SC_BTN_LABEL_DEN; \ + int _gap = (BTN)->h * SC_BTN_GAP_NUM / SC_BTN_GAP_DEN; \ + SDL_Rect ICON = { \ + .x = (BTN)->x, .y = (BTN)->y, \ + .w = (BTN)->w, .h = (BTN)->h - _lh - _gap, \ + }; \ + SDL_Rect LBL = { \ + .x = (BTN)->x, .y = (BTN)->y + (BTN)->h - _lh, \ + .w = (BTN)->w, .h = _lh, \ + } + + if (toolbar_button) { + // Screenshot button: lens icon + "CAM" label + SDL_SetRenderDrawColor(display->renderer, 80, 80, 80, 255); + SDL_RenderFillRect(display->renderer, toolbar_button); + + SPLIT_BUTTON(toolbar_button, icon, label); + + int lens_size = icon.w * 2 / 5; + if (lens_size < 1) { + lens_size = 1; + } + SDL_Rect lens = { + .x = icon.x + (icon.w - lens_size) / 2, + .y = icon.y + (icon.h - lens_size) / 2, + .w = lens_size, + .h = lens_size, + }; + SDL_SetRenderDrawColor(display->renderer, 220, 220, 220, 255); + SDL_RenderFillRect(display->renderer, &lens); + sc_mini_draw_button_label(display->renderer, label.x, label.y, + label.w, label.h, "CAM"); + } + + if (toolbar_logs_button) { + // Open-live-logs button: three bars + "LOG" label + SDL_SetRenderDrawColor(display->renderer, 80, 80, 80, 255); + SDL_RenderFillRect(display->renderer, toolbar_logs_button); + + SPLIT_BUTTON(toolbar_logs_button, icon, label); + + int pad = icon.w / 4; + if (pad < 1) pad = 1; + int bar_w = icon.w - 2 * pad; + int bar_h = icon.h / 8; + if (bar_h < 1) bar_h = 1; + int gap = bar_h * 2; + int total = 3 * bar_h + 2 * gap; + int y0 = icon.y + (icon.h - total) / 2; + SDL_SetRenderDrawColor(display->renderer, 220, 220, 220, 255); + for (int i = 0; i < 3; ++i) { + SDL_Rect bar = { + .x = icon.x + pad, + .y = y0 + i * (bar_h + gap), + .w = bar_w, + .h = bar_h, + }; + SDL_RenderFillRect(display->renderer, &bar); + } + sc_mini_draw_button_label(display->renderer, label.x, label.y, + label.w, label.h, "LOG"); + } + + if (toolbar_save_all_logs_button) { + // Full-system-log dump: bars + arrow + "SYS" label + SDL_SetRenderDrawColor(display->renderer, 80, 80, 80, 255); + SDL_RenderFillRect(display->renderer, toolbar_save_all_logs_button); + + SPLIT_BUTTON(toolbar_save_all_logs_button, icon, label); + + int bw = icon.w; + int bh = icon.h; + int pad = bw / 5; + if (pad < 1) pad = 1; + int bar_w = bw - 2 * pad; + int bar_h = bh / 12; + if (bar_h < 1) bar_h = 1; + int gap = bar_h; + int bars_total = 3 * bar_h + 2 * gap; + int arrow_h = bh / 2; + int content_h = bars_total + arrow_h + bar_h; + int y0 = icon.y + (bh - content_h) / 2; + + SDL_SetRenderDrawColor(display->renderer, 220, 220, 220, 255); + for (int i = 0; i < 3; ++i) { + SDL_Rect bar = { + .x = icon.x + pad, + .y = y0 + i * (bar_h + gap), + .w = bar_w, + .h = bar_h, + }; + SDL_RenderFillRect(display->renderer, &bar); + } + int arrow_y = y0 + bars_total + bar_h; + int row_h = arrow_h / 3; + if (row_h < 1) row_h = 1; + int insets[3] = {bw / 8, bw / 4, bw * 3 / 8}; + for (int i = 0; i < 3; ++i) { + int inset = insets[i]; + int w = bw - 2 * inset; + if (w < 1) w = 1; + SDL_Rect row = { + .x = icon.x + inset, + .y = arrow_y + i * row_h, + .w = w, + .h = row_h, + }; + SDL_RenderFillRect(display->renderer, &row); + } + sc_mini_draw_button_label(display->renderer, label.x, label.y, + label.w, label.h, "SYS"); + } + + if (toolbar_save_logs_button) { + // App-filtered log dump: down-arrow + "APP" label + SDL_SetRenderDrawColor(display->renderer, 80, 80, 80, 255); + SDL_RenderFillRect(display->renderer, toolbar_save_logs_button); + + SPLIT_BUTTON(toolbar_save_logs_button, icon, label); + + int bw = icon.w; + int bh = icon.h; + int stem_w = bw / 3; + if (stem_w < 1) stem_w = 1; + int stem_h = bh * 2 / 5; + if (stem_h < 1) stem_h = 1; + int pad_top = bh / 8; + int pad_bot = bh / 8; + int tri_space = bh - pad_top - stem_h - pad_bot; + int row_h = tri_space / 3; + if (row_h < 1) row_h = 1; + + SDL_SetRenderDrawColor(display->renderer, 220, 220, 220, 255); + + SDL_Rect stem = { + .x = icon.x + (bw - stem_w) / 2, + .y = icon.y + pad_top, + .w = stem_w, + .h = stem_h, + }; + SDL_RenderFillRect(display->renderer, &stem); + + int tri_y = stem.y + stem_h; + int insets[3] = {bw / 8, bw / 4, bw * 3 / 8}; + for (int i = 0; i < 3; ++i) { + int inset = insets[i]; + int w = bw - 2 * inset; + if (w < 1) w = 1; + SDL_Rect row = { + .x = icon.x + inset, + .y = tri_y + i * row_h, + .w = w, + .h = row_h, + }; + SDL_RenderFillRect(display->renderer, &row); + } + sc_mini_draw_button_label(display->renderer, label.x, label.y, + label.w, label.h, "APP"); + } + +#undef SPLIT_BUTTON + } + // Draw screenshot flash overlay if active if (display->flash_active) { sc_tick elapsed = sc_tick_now() - display->flash_start; diff --git a/app/src/display.h b/app/src/display.h index 2671381071..a991fdfb25 100644 --- a/app/src/display.h +++ b/app/src/display.h @@ -64,7 +64,12 @@ sc_display_update_texture(struct sc_display *display, const AVFrame *frame); enum sc_display_result sc_display_render(struct sc_display *display, const SDL_Rect *geometry, - enum sc_orientation orientation); + enum sc_orientation orientation, + const SDL_Rect *toolbar_bg, + const SDL_Rect *toolbar_button, + const SDL_Rect *toolbar_logs_button, + const SDL_Rect *toolbar_save_logs_button, + const SDL_Rect *toolbar_save_all_logs_button); void sc_display_flash(struct sc_display *display); diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 2afa9dbc95..f835d0d685 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -3,6 +3,7 @@ #include #include #include +#include #include #include "android/input.h" @@ -367,6 +368,155 @@ inverse_point(struct sc_point point, struct sc_size size, return point; } +// Returns the foreground app's package name, or an empty string if not found. +// Caller must provide a buffer; output is NUL-terminated. +static void +get_foreground_package(char *pkg, size_t pkg_size) { + if (pkg_size == 0) { + return; + } + pkg[0] = '\0'; + FILE *fp = popen( + "adb shell \"dumpsys activity activities" + " | grep mResumedActivity" + " | head -1" + " | sed 's|.* \\([^ ]*/\\).*|\\1|'" + " | sed 's|/||'\"", "r"); + if (!fp) { + return; + } + if (fgets(pkg, pkg_size, fp)) { + size_t len = strlen(pkg); + while (len > 0 && (pkg[len-1] == '\n' || pkg[len-1] == '\r' + || pkg[len-1] == ' ')) { + pkg[--len] = '\0'; + } + } + pclose(fp); +} + +// Dump the current logcat buffer (filtered to foreground app when possible) +// to ~/Downloads/scrcpy_logs_YYYYMMDD-HHMMSS.txt. +static void +save_logcat_to_downloads(void) { + const char *home = getenv("HOME"); + if (!home) { + home = "."; + } + + time_t now = time(NULL); + struct tm *tm = localtime(&now); + if (!tm) { + LOGW("Could not resolve local time for log filename"); + return; + } + + char filename[512]; + int n = snprintf(filename, sizeof(filename), + "%s/Downloads/scrcpy_logs_%04d%02d%02d-%02d%02d%02d.txt", + home, + tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, + tm->tm_hour, tm->tm_min, tm->tm_sec); + if (n <= 0 || (size_t) n >= sizeof(filename)) { + LOGW("Could not build log filename"); + return; + } + + char pkg[256]; + get_foreground_package(pkg, sizeof(pkg)); + + char cmd[1024]; + int rc; + if (pkg[0]) { + LOGI("Saving logcat for %s to %s", pkg, filename); + rc = snprintf(cmd, sizeof(cmd), + "adb logcat -d --pid=$(adb shell pidof %s)" + " > \"%s\" 2>&1 &", + pkg, filename); + } else { + LOGI("Saving full logcat buffer to %s", filename); + rc = snprintf(cmd, sizeof(cmd), + "adb logcat -d > \"%s\" 2>&1 &", filename); + } + if (rc <= 0 || (size_t) rc >= sizeof(cmd)) { + LOGW("Could not build logcat save command"); + return; + } + system(cmd); +} + +// Dump the full system log buffers (main+system+crash+events+radio...) to +// ~/Downloads/scrcpy_syslogs_YYYYMMDD-HHMMSS.txt. Unfiltered, unlike +// save_logcat_to_downloads() which is scoped to the foreground app's PID. +static void +save_full_system_logcat_to_downloads(void) { + const char *home = getenv("HOME"); + if (!home) { + home = "."; + } + + time_t now = time(NULL); + struct tm *tm = localtime(&now); + if (!tm) { + LOGW("Could not resolve local time for syslog filename"); + return; + } + + char filename[512]; + int n = snprintf(filename, sizeof(filename), + "%s/Downloads/scrcpy_syslogs_%04d%02d%02d-%02d%02d%02d.txt", + home, + tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, + tm->tm_hour, tm->tm_min, tm->tm_sec); + if (n <= 0 || (size_t) n >= sizeof(filename)) { + LOGW("Could not build syslog filename"); + return; + } + + char cmd[1024]; + int rc = snprintf(cmd, sizeof(cmd), + "adb logcat -d -b all > \"%s\" 2>&1 &", filename); + if (rc <= 0 || (size_t) rc >= sizeof(cmd)) { + LOGW("Could not build full logcat save command"); + return; + } + LOGI("Saving full system logcat (all buffers) to %s", filename); + system(cmd); +} + +static void +open_logcat_for_foreground_app(void) { + char pkg[256]; + get_foreground_package(pkg, sizeof(pkg)); + + if (pkg[0]) { + LOGI("Opening logcat for foreground app: %s", pkg); + char cmd[1024]; +#ifdef __APPLE__ + snprintf(cmd, sizeof(cmd), + "osascript -e 'tell application \"Terminal\" to do script " + "\"adb logcat --pid=$(adb shell pidof %s)\"' &", pkg); +#else + snprintf(cmd, sizeof(cmd), + "x-terminal-emulator -e sh -c " + "\"adb logcat --pid=\\$(adb shell pidof %s)\" &" + " || xterm -e sh -c " + "\"adb logcat --pid=\\$(adb shell pidof %s)\" &", + pkg, pkg); +#endif + system(cmd); + } else { + LOGI("Could not detect foreground app, opening unfiltered logcat"); +#ifdef __APPLE__ + system("osascript -e 'tell application \"Terminal\" to do script " + "\"adb logcat\"' &"); +#else + system("x-terminal-emulator -e adb logcat &" + " || xterm -e adb logcat &"); +#endif + } +} + static void sc_input_manager_process_key(struct sc_input_manager *im, const SDL_KeyboardEvent *event) { @@ -402,52 +552,7 @@ sc_input_manager_process_key(struct sc_input_manager *im, // Ctrl+Shift+L: open logcat filtered to foreground app if (ctrl && shift && sdl_keycode == SDLK_l && !repeat && down) { - // Get the foreground app's package name from the device - FILE *fp = popen( - "adb shell \"dumpsys activity activities" - " | grep mResumedActivity" - " | head -1" - " | sed 's|.* \\([^ ]*/\\).*|\\1|'" - " | sed 's|/||'\"", "r"); - char pkg[256] = {0}; - if (fp) { - if (fgets(pkg, sizeof(pkg), fp)) { - // Strip trailing whitespace/newline - size_t len = strlen(pkg); - while (len > 0 && (pkg[len-1] == '\n' || pkg[len-1] == '\r' - || pkg[len-1] == ' ')) { - pkg[--len] = '\0'; - } - } - pclose(fp); - } - - if (pkg[0]) { - LOGI("Opening logcat for foreground app: %s", pkg); - char cmd[1024]; -#ifdef __APPLE__ - snprintf(cmd, sizeof(cmd), - "osascript -e 'tell application \"Terminal\" to do script " - "\"adb logcat --pid=$(adb shell pidof %s)\"' &", pkg); -#else - snprintf(cmd, sizeof(cmd), - "x-terminal-emulator -e sh -c " - "\"adb logcat --pid=\\$(adb shell pidof %s)\" &" - " || xterm -e sh -c " - "\"adb logcat --pid=\\$(adb shell pidof %s)\" &", - pkg, pkg); -#endif - system(cmd); - } else { - LOGI("Could not detect foreground app, opening unfiltered logcat"); -#ifdef __APPLE__ - system("osascript -e 'tell application \"Terminal\" to do script " - "\"adb logcat\"' &"); -#else - system("x-terminal-emulator -e adb logcat &" - " || xterm -e adb logcat &"); -#endif - } + open_logcat_for_foreground_app(); return; } @@ -695,6 +800,20 @@ sc_input_manager_get_position(struct sc_input_manager *im, int32_t x, }; } +// Returns true if (window-space) point lands inside the toolbar strip. +static bool +sc_input_manager_in_toolbar(struct sc_input_manager *im, int32_t wx, + int32_t wy) { + if (!im->screen->video) { + return false; + } + int32_t x = wx, y = wy; + sc_screen_hidpi_scale_coords(im->screen, &x, &y); + (void) x; + const SDL_Rect *tb = &im->screen->toolbar_rect; + return y < tb->y + tb->h; +} + static void sc_input_manager_process_mouse_motion(struct sc_input_manager *im, const SDL_MouseMotionEvent *event) { @@ -703,6 +822,11 @@ sc_input_manager_process_mouse_motion(struct sc_input_manager *im, return; } + if (sc_input_manager_in_toolbar(im, event->x, event->y)) { + // Don't forward motion over the toolbar strip to the device + return; + } + struct sc_mouse_motion_event evt = { .position = sc_input_manager_get_position(im, event->x, event->y), .pointer_id = im->vfinger_down ? SC_POINTER_ID_GENERIC_FINGER @@ -791,6 +915,42 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, bool paused = im->screen->paused; bool down = event->type == SDL_MOUSEBUTTONDOWN; + // Handle clicks on the toolbar strip (outside the mirrored content) + if (sc_input_manager_in_toolbar(im, event->x, event->y)) { + if (down && event->button == SDL_BUTTON_LEFT && im->screen->video) { + int32_t x = event->x, y = event->y; + sc_screen_hidpi_scale_coords(im->screen, &x, &y); + const SDL_Rect *btn = &im->screen->toolbar_button_rect; + const SDL_Rect *logs = &im->screen->toolbar_logs_button_rect; + const SDL_Rect *save = &im->screen->toolbar_save_logs_button_rect; + const SDL_Rect *save_all = + &im->screen->toolbar_save_all_logs_button_rect; + if (x >= btn->x && x < btn->x + btn->w + && y >= btn->y && y < btn->y + btn->h) { + LOGD("Screenshot toolbar button clicked"); + bool ok = sc_screenshot_save(im->screen->frame, + im->screen->orientation); + if (ok) { + sc_display_flash(&im->screen->display); + } + } else if (x >= logs->x && x < logs->x + logs->w + && y >= logs->y && y < logs->y + logs->h) { + LOGD("Logs toolbar button clicked"); + open_logcat_for_foreground_app(); + } else if (x >= save->x && x < save->x + save->w + && y >= save->y && y < save->y + save->h) { + LOGD("Save-logs toolbar button clicked"); + save_logcat_to_downloads(); + } else if (x >= save_all->x && x < save_all->x + save_all->w + && y >= save_all->y && y < save_all->y + save_all->h) { + LOGD("Save-all-logs toolbar button clicked"); + save_full_system_logcat_to_downloads(); + } + } + // Swallow all mouse button events in the toolbar region + return; + } + enum sc_mouse_button button = sc_mouse_button_from_sdl(event->button); if (button == SC_MOUSE_BUTTON_UNKNOWN) { return; diff --git a/app/src/screen.c b/app/src/screen.c index da17df0ed2..de91be6fc6 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -11,8 +11,23 @@ #define DISPLAY_MARGINS 96 +// Height of the toolbar strip above the content, in window pixels +#define SC_TOOLBAR_HEIGHT 72 + #define DOWNCAST(SINK) container_of(SINK, struct sc_screen, frame_sink) +// Convert the toolbar height from window pixels to drawable pixels +static int +get_toolbar_drawable_height(struct sc_screen *screen) { + int ww, wh, dw, dh; + SDL_GetWindowSize(screen->window, &ww, &wh); + SDL_GL_GetDrawableSize(screen->window, &dw, &dh); + if (wh <= 0) { + return SC_TOOLBAR_HEIGHT; + } + return (int) ((int64_t) SC_TOOLBAR_HEIGHT * dh / wh); +} + static inline struct sc_size get_oriented_size(struct sc_size size, enum sc_orientation orientation) { struct sc_size oriented_size; @@ -170,15 +185,57 @@ sc_screen_update_content_rect(struct sc_screen *screen) { int dh; SDL_GL_GetDrawableSize(screen->window, &dw, &dh); + int tb = get_toolbar_drawable_height(screen); + int usable_dh = dh - tb; + if (usable_dh < 1) { + usable_dh = 1; + } + + // Toolbar spans the full width at the top + screen->toolbar_rect.x = 0; + screen->toolbar_rect.y = 0; + screen->toolbar_rect.w = dw; + screen->toolbar_rect.h = tb; + + // Buttons in the top-right corner of the toolbar + int btn_margin = tb > 0 ? tb / 4 : 0; + int btn_size = tb - 2 * btn_margin; + if (btn_size < 1) { + btn_size = 1; + } + // Screenshot (rightmost) + screen->toolbar_button_rect.x = dw - btn_margin - btn_size; + screen->toolbar_button_rect.y = btn_margin; + screen->toolbar_button_rect.w = btn_size; + screen->toolbar_button_rect.h = btn_size; + // Logs button, placed to the left of the screenshot button + screen->toolbar_logs_button_rect.x = + screen->toolbar_button_rect.x - btn_margin - btn_size; + screen->toolbar_logs_button_rect.y = btn_margin; + screen->toolbar_logs_button_rect.w = btn_size; + screen->toolbar_logs_button_rect.h = btn_size; + // Save-logs button, placed to the left of the logs button + screen->toolbar_save_logs_button_rect.x = + screen->toolbar_logs_button_rect.x - btn_margin - btn_size; + screen->toolbar_save_logs_button_rect.y = btn_margin; + screen->toolbar_save_logs_button_rect.w = btn_size; + screen->toolbar_save_logs_button_rect.h = btn_size; + // Save-all-logs button (full system dump), placed to the left of save-logs + screen->toolbar_save_all_logs_button_rect.x = + screen->toolbar_save_logs_button_rect.x - btn_margin - btn_size; + screen->toolbar_save_all_logs_button_rect.y = btn_margin; + screen->toolbar_save_all_logs_button_rect.w = btn_size; + screen->toolbar_save_all_logs_button_rect.h = btn_size; + struct sc_size content_size = screen->content_size; - // The drawable size is the window size * the HiDPI scale - struct sc_size drawable_size = {dw, dh}; + // The drawable size is the window size * the HiDPI scale, minus toolbar + struct sc_size drawable_size = {dw, usable_dh}; SDL_Rect *rect = &screen->rect; if (is_optimal_size(drawable_size, content_size)) { rect->x = 0; - rect->y = 0; + rect->y = tb; rect->w = drawable_size.width; rect->h = drawable_size.height; return; @@ -191,9 +248,9 @@ sc_screen_update_content_rect(struct sc_screen *screen) { rect->w = drawable_size.width; rect->h = drawable_size.width * content_size.height / content_size.width; - rect->y = (drawable_size.height - rect->h) / 2; + rect->y = tb + (drawable_size.height - rect->h) / 2; } else { - rect->y = 0; + rect->y = tb; rect->h = drawable_size.height; rect->w = drawable_size.height * content_size.width / content_size.height; @@ -214,14 +271,19 @@ sc_screen_render(struct sc_screen *screen, bool update_content_rect) { } enum sc_display_result res = - sc_display_render(&screen->display, &screen->rect, screen->orientation); + sc_display_render(&screen->display, &screen->rect, screen->orientation, + &screen->toolbar_rect, &screen->toolbar_button_rect, + &screen->toolbar_logs_button_rect, + &screen->toolbar_save_logs_button_rect, + &screen->toolbar_save_all_logs_button_rect); (void) res; // any error already logged } static void sc_screen_render_novideo(struct sc_screen *screen) { enum sc_display_result res = - sc_display_render(&screen->display, NULL, SC_ORIENTATION_0); + sc_display_render(&screen->display, NULL, SC_ORIENTATION_0, + NULL, NULL, NULL, NULL, NULL); (void) res; // any error already logged } @@ -494,6 +556,8 @@ sc_screen_show_initial_window(struct sc_screen *screen) { struct sc_size window_size = get_initial_optimal_size(screen->content_size, screen->req.width, screen->req.height); + // Reserve space for the toolbar strip above the content + window_size.height += SC_TOOLBAR_HEIGHT; set_window_size(screen, window_size); SDL_SetWindowPosition(screen->window, x, y); @@ -543,13 +607,18 @@ resize_for_content(struct sc_screen *screen, struct sc_size old_content_size, assert(screen->video); struct sc_size window_size = get_window_size(screen); + // Work on the content area (window minus toolbar) for aspect-ratio fit + uint16_t content_area_height = window_size.height > SC_TOOLBAR_HEIGHT + ? window_size.height - SC_TOOLBAR_HEIGHT + : 1; struct sc_size target_size = { .width = (uint32_t) window_size.width * new_content_size.width / old_content_size.width, - .height = (uint32_t) window_size.height * new_content_size.height + .height = (uint32_t) content_area_height * new_content_size.height / old_content_size.height, }; target_size = get_optimal_size(target_size, new_content_size, true); + target_size.height += SC_TOOLBAR_HEIGHT; set_window_size(screen, target_size); } @@ -764,16 +833,24 @@ sc_screen_resize_to_fit(struct sc_screen *screen) { struct sc_point point = get_window_position(screen); struct sc_size window_size = get_window_size(screen); + // Exclude toolbar from the available content area for aspect fit + struct sc_size content_area = { + .width = window_size.width, + .height = window_size.height > SC_TOOLBAR_HEIGHT + ? window_size.height - SC_TOOLBAR_HEIGHT : 1, + }; + struct sc_size optimal_size = - get_optimal_size(window_size, screen->content_size, false); + get_optimal_size(content_area, screen->content_size, false); // Center the window related to the device screen assert(optimal_size.width <= window_size.width); - assert(optimal_size.height <= window_size.height); + assert(optimal_size.height <= content_area.height); uint32_t new_x = point.x + (window_size.width - optimal_size.width) / 2; - uint32_t new_y = point.y + (window_size.height - optimal_size.height) / 2; + uint32_t new_y = point.y + (content_area.height - optimal_size.height) / 2; - SDL_SetWindowSize(screen->window, optimal_size.width, optimal_size.height); + SDL_SetWindowSize(screen->window, optimal_size.width, + optimal_size.height + SC_TOOLBAR_HEIGHT); SDL_SetWindowPosition(screen->window, new_x, new_y); LOGD("Resized to optimal size: %ux%u", optimal_size.width, optimal_size.height); @@ -793,7 +870,8 @@ sc_screen_resize_to_pixel_perfect(struct sc_screen *screen) { } struct sc_size content_size = screen->content_size; - SDL_SetWindowSize(screen->window, content_size.width, content_size.height); + SDL_SetWindowSize(screen->window, content_size.width, + content_size.height + SC_TOOLBAR_HEIGHT); LOGD("Resized to pixel-perfect: %ux%u", content_size.width, content_size.height); } diff --git a/app/src/screen.h b/app/src/screen.h index 6621b2d2d1..b8d7a4b12e 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -60,6 +60,16 @@ struct sc_screen { enum sc_orientation orientation; // rectangle of the content (excluding black borders) struct SDL_Rect rect; + // toolbar strip above the content (drawable coords) + struct SDL_Rect toolbar_rect; + // screenshot button inside the toolbar (drawable coords) + struct SDL_Rect toolbar_button_rect; + // logs button inside the toolbar (drawable coords) + struct SDL_Rect toolbar_logs_button_rect; + // "save logs to file" button inside the toolbar (drawable coords) + struct SDL_Rect toolbar_save_logs_button_rect; + // "save full system logs (all buffers)" button (drawable coords) + struct SDL_Rect toolbar_save_all_logs_button_rect; bool has_frame; bool fullscreen; bool maximized; diff --git a/app/src/screenshot.c b/app/src/screenshot.c index 9e40bf8881..2c21b8d133 100644 --- a/app/src/screenshot.c +++ b/app/src/screenshot.c @@ -13,6 +13,47 @@ #include "util/log.h" +// Copy a saved PNG file onto the system clipboard as an image so it can be +// pasted directly into other apps. +static void +copy_png_to_clipboard(const char *filename) { +#ifdef __APPLE__ + // The AppleScript type code «class PNGf» contains the guillemets «» + // which are U+00AB / U+00BB — encoded as UTF-8 below. + char cmd[1024]; + int ret = snprintf(cmd, sizeof(cmd), + "osascript -e 'set the clipboard to " + "(read (POSIX file \"%s\") as \xc2\xab""class PNGf\xc2\xbb)' " + ">/dev/null 2>&1 &", + filename); + if (ret <= 0 || (size_t) ret >= sizeof(cmd)) { + LOGW("Could not build clipboard command"); + return; + } + int rc = system(cmd); + if (rc != 0) { + LOGW("Could not copy screenshot to clipboard (rc=%d)", rc); + } else { + LOGI("Screenshot copied to clipboard"); + } +#else + // On Linux try xclip, then wl-copy. Errors are non-fatal. + char cmd[1024]; + int ret = snprintf(cmd, sizeof(cmd), + "(xclip -selection clipboard -t image/png -i \"%s\"" + " || wl-copy --type image/png < \"%s\") >/dev/null 2>&1 &", + filename, filename); + if (ret <= 0 || (size_t) ret >= sizeof(cmd)) { + LOGW("Could not build clipboard command"); + return; + } + int rc = system(cmd); + if (rc != 0) { + LOGW("Could not copy screenshot to clipboard (rc=%d)", rc); + } +#endif +} + static bool generate_filename(char *buf, size_t size) { const char *home = getenv("HOME"); @@ -261,6 +302,7 @@ sc_screenshot_save(const AVFrame *frame, enum sc_orientation orientation) { bool ok = encode_png(final_frame, filename); if (ok) { LOGI("Screenshot saved to %s", filename); + copy_png_to_clipboard(filename); } else { LOGE("Failed to encode/save screenshot"); } From 83c102842528731400da4ce52ce10f892bb355d1 Mon Sep 17 00:00:00 2001 From: Akiva Jeger Date: Wed, 29 Apr 2026 21:33:01 +0300 Subject: [PATCH 15/15] Add HOME button to toolbar A fifth labelled button on the leftmost end of the toolbar. Clicking it sends the Android HOME keycode (down + up) via the existing action_home path, so it behaves identically to pressing the device's home button or the Ctrl+H keyboard shortcut. Icon: a small house drawn from rect primitives (peaked roof + body with a door cutout). Label: "HOME" (added H and E to the embedded mini font). --- app/src/display.c | 68 ++++++++++++++++++++++++++++++++++++++++- app/src/display.h | 3 +- app/src/input_manager.c | 8 +++++ app/src/screen.c | 11 +++++-- app/src/screen.h | 2 ++ 5 files changed, 88 insertions(+), 4 deletions(-) diff --git a/app/src/display.c b/app/src/display.c index 9d69a042f8..6bc8a85076 100644 --- a/app/src/display.c +++ b/app/src/display.c @@ -22,7 +22,9 @@ struct sc_mini_glyph { static const struct sc_mini_glyph SC_MINI_FONT[] = { {'A', {0x0E, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11}}, {'C', {0x0E, 0x11, 0x10, 0x10, 0x10, 0x11, 0x0E}}, + {'E', {0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x1F}}, {'G', {0x0E, 0x11, 0x10, 0x17, 0x11, 0x11, 0x0E}}, + {'H', {0x11, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11}}, {'L', {0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1F}}, {'M', {0x11, 0x1B, 0x15, 0x15, 0x11, 0x11, 0x11}}, {'O', {0x0E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E}}, @@ -404,7 +406,8 @@ sc_display_render(struct sc_display *display, const SDL_Rect *geometry, const SDL_Rect *toolbar_button, const SDL_Rect *toolbar_logs_button, const SDL_Rect *toolbar_save_logs_button, - const SDL_Rect *toolbar_save_all_logs_button) { + const SDL_Rect *toolbar_save_all_logs_button, + const SDL_Rect *toolbar_home_button) { SDL_RenderClear(display->renderer); if (display->pending.flags) { @@ -624,6 +627,69 @@ sc_display_render(struct sc_display *display, const SDL_Rect *geometry, label.w, label.h, "APP"); } + if (toolbar_home_button) { + // Home button: house icon + "HOME" label + SDL_SetRenderDrawColor(display->renderer, 80, 80, 80, 255); + SDL_RenderFillRect(display->renderer, toolbar_home_button); + + SPLIT_BUTTON(toolbar_home_button, icon, label); + + int pad_x = icon.w / 8; + int pad_y = icon.h / 8; + int hx = icon.x + pad_x; + int hy = icon.y + pad_y; + int hw = icon.w - 2 * pad_x; + int hh = icon.h - 2 * pad_y; + + int roof_h = hh * 2 / 5; + int body_h = hh - roof_h; + + SDL_SetRenderDrawColor(display->renderer, 220, 220, 220, 255); + + // Roof: 4 stacked rows widening towards the bottom + int row_h = roof_h / 4; + if (row_h < 1) row_h = 1; + int insets[4] = {hw * 3 / 8, hw / 4, hw / 8, 0}; + for (int i = 0; i < 4; ++i) { + int inset = insets[i]; + int w = hw - 2 * inset; + if (w < 1) w = 1; + SDL_Rect row = { + .x = hx + inset, + .y = hy + i * row_h, + .w = w, + .h = row_h, + }; + SDL_RenderFillRect(display->renderer, &row); + } + + // Body: filled rect under the roof + SDL_Rect body = { + .x = hx, + .y = hy + roof_h, + .w = hw, + .h = body_h, + }; + SDL_RenderFillRect(display->renderer, &body); + + // Door cutout (button bg color) + int door_w = hw / 4; + int door_h = body_h * 2 / 3; + if (door_w >= 1 && door_h >= 1) { + SDL_Rect door = { + .x = hx + (hw - door_w) / 2, + .y = hy + roof_h + body_h - door_h, + .w = door_w, + .h = door_h, + }; + SDL_SetRenderDrawColor(display->renderer, 80, 80, 80, 255); + SDL_RenderFillRect(display->renderer, &door); + } + + sc_mini_draw_button_label(display->renderer, label.x, label.y, + label.w, label.h, "HOME"); + } + #undef SPLIT_BUTTON } diff --git a/app/src/display.h b/app/src/display.h index a991fdfb25..002c327458 100644 --- a/app/src/display.h +++ b/app/src/display.h @@ -69,7 +69,8 @@ sc_display_render(struct sc_display *display, const SDL_Rect *geometry, const SDL_Rect *toolbar_button, const SDL_Rect *toolbar_logs_button, const SDL_Rect *toolbar_save_logs_button, - const SDL_Rect *toolbar_save_all_logs_button); + const SDL_Rect *toolbar_save_all_logs_button, + const SDL_Rect *toolbar_home_button); void sc_display_flash(struct sc_display *display); diff --git a/app/src/input_manager.c b/app/src/input_manager.c index f835d0d685..e32b36dfc0 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -925,6 +925,7 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, const SDL_Rect *save = &im->screen->toolbar_save_logs_button_rect; const SDL_Rect *save_all = &im->screen->toolbar_save_all_logs_button_rect; + const SDL_Rect *home = &im->screen->toolbar_home_button_rect; if (x >= btn->x && x < btn->x + btn->w && y >= btn->y && y < btn->y + btn->h) { LOGD("Screenshot toolbar button clicked"); @@ -945,6 +946,13 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, && y >= save_all->y && y < save_all->y + save_all->h) { LOGD("Save-all-logs toolbar button clicked"); save_full_system_logcat_to_downloads(); + } else if (x >= home->x && x < home->x + home->w + && y >= home->y && y < home->y + home->h) { + LOGD("Home toolbar button clicked"); + if (im->controller && im->kp && !im->screen->paused) { + action_home(im, SC_ACTION_DOWN); + action_home(im, SC_ACTION_UP); + } } } // Swallow all mouse button events in the toolbar region diff --git a/app/src/screen.c b/app/src/screen.c index de91be6fc6..1ea5698b64 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -226,6 +226,12 @@ sc_screen_update_content_rect(struct sc_screen *screen) { screen->toolbar_save_all_logs_button_rect.y = btn_margin; screen->toolbar_save_all_logs_button_rect.w = btn_size; screen->toolbar_save_all_logs_button_rect.h = btn_size; + // Home button (sends Android HOME), leftmost so it's the easiest to hit + screen->toolbar_home_button_rect.x = + screen->toolbar_save_all_logs_button_rect.x - btn_margin - btn_size; + screen->toolbar_home_button_rect.y = btn_margin; + screen->toolbar_home_button_rect.w = btn_size; + screen->toolbar_home_button_rect.h = btn_size; struct sc_size content_size = screen->content_size; // The drawable size is the window size * the HiDPI scale, minus toolbar @@ -275,7 +281,8 @@ sc_screen_render(struct sc_screen *screen, bool update_content_rect) { &screen->toolbar_rect, &screen->toolbar_button_rect, &screen->toolbar_logs_button_rect, &screen->toolbar_save_logs_button_rect, - &screen->toolbar_save_all_logs_button_rect); + &screen->toolbar_save_all_logs_button_rect, + &screen->toolbar_home_button_rect); (void) res; // any error already logged } @@ -283,7 +290,7 @@ static void sc_screen_render_novideo(struct sc_screen *screen) { enum sc_display_result res = sc_display_render(&screen->display, NULL, SC_ORIENTATION_0, - NULL, NULL, NULL, NULL, NULL); + NULL, NULL, NULL, NULL, NULL, NULL); (void) res; // any error already logged } diff --git a/app/src/screen.h b/app/src/screen.h index b8d7a4b12e..00f8e4e3b4 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -70,6 +70,8 @@ struct sc_screen { struct SDL_Rect toolbar_save_logs_button_rect; // "save full system logs (all buffers)" button (drawable coords) struct SDL_Rect toolbar_save_all_logs_button_rect; + // "home" button — sends Android HOME keycode (drawable coords) + struct SDL_Rect toolbar_home_button_rect; bool has_frame; bool fullscreen; bool maximized;