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/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/display.c b/app/src/display.c index 15f9a1f19e..6bc8a85076 100644 --- a/app/src/display.c +++ b/app/src/display.c @@ -2,11 +2,112 @@ #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}}, + {'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}}, + {'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 +401,13 @@ 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, + const SDL_Rect *toolbar_home_button) { SDL_RenderClear(display->renderer); if (display->pending.flags) { @@ -346,6 +453,278 @@ 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"); + } + + 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 + } + + // 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(500); + if (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); + 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..002c327458 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 { @@ -59,6 +64,15 @@ 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, + const SDL_Rect *toolbar_home_button); + +void +sc_display_flash(struct sc_display *display); #endif diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 3e4dd0f32f..e32b36dfc0 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -3,12 +3,14 @@ #include #include #include +#include #include #include "android/input.h" #include "android/keycodes.h" #include "input_events.h" #include "screen.h" +#include "screenshot.h" #include "shortcut_mod.h" #include "util/log.h" @@ -366,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) { @@ -399,6 +550,26 @@ 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) { + open_logcat_for_foreground_app(); + 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)"); + 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; + } + if (is_shortcut) { enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP; switch (sdl_keycode) { @@ -629,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) { @@ -637,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 @@ -725,6 +915,50 @@ 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; + 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"); + 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(); + } 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 + 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/keyboard_sdk.c b/app/src/keyboard_sdk.c index 466a1aebba..4e08d21f6e 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,117 @@ 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_cls[1024] = ""; + +struct dpad_query_ctx { + char direction[16]; +}; + +static void * +query_focused_view(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, 300000000}; // 300ms + nanosleep(&ts, NULL); + + // 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 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 run dumpsys"); + return NULL; + } + + char focus_info[512] = ""; + char activity_info[256] = ""; + char line[2048]; + + while (fgets(line, sizeof(line), fp)) { + size_t len = strlen(line); + if (len > 0 && line[len - 1] == '\n') line[len - 1] = '\0'; + + // 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; + } + + // 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) { + LOGI("[DPAD %s] Focus moved: %s -> %s", + direction, prev_focus_cls, focus_info); + } else { + LOGI("[DPAD %s] Focus unchanged: %s", direction, focus_info); + } + } else { + LOGI("[DPAD %s] Focus: %s", direction, focus_info); + } + // Save for next comparison + snprintf(prev_focus_cls, sizeof(prev_focus_cls), "%s", focus_info); + } else { + // 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; +} + static void sc_key_processor_process_key(struct sc_key_processor *kp, const struct sc_key_event *event, @@ -289,6 +402,35 @@ 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) { + 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); + } + } + } + if (!sc_controller_push_msg(kb->controller, &msg)) { LOGW("Could not request 'inject keycode'"); } diff --git a/app/src/screen.c b/app/src/screen.c index da17df0ed2..1ea5698b64 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,63 @@ 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; + // 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 - 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 +254,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 +277,20 @@ 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, + &screen->toolbar_home_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, NULL); (void) res; // any error already logged } @@ -494,6 +563,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 +614,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 +840,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 +877,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..00f8e4e3b4 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -60,6 +60,18 @@ 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; + // "home" button — sends Android HOME keycode (drawable coords) + struct SDL_Rect toolbar_home_button_rect; bool has_frame; bool fullscreen; bool maximized; diff --git a/app/src/screenshot.c b/app/src/screenshot.c new file mode 100644 index 0000000000..2c21b8d133 --- /dev/null +++ b/app/src/screenshot.c @@ -0,0 +1,318 @@ +#include "screenshot.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#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"); + if (!home) { + home = "."; + } + + time_t now = time(NULL); + struct tm *tm = localtime(&now); + if (!tm) { + return false; + } + 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; +} + +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) { + LOGD("Screenshot save requested (orientation=%d)", orientation); + + if (!frame || !frame->data[0]) { + LOGW("No frame available for screenshot"); + return false; + } + + 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); + copy_png_to_clipboard(filename); + } else { + LOGE("Failed to encode/save screenshot"); + } + + 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_