mirror of
https://github.com/Cogential/frametanium.git
synced 2026-10-18 01:00:27 +02:00
Searches GitHub (steam-frame topic, "steam frame" in names/descriptions) and the awesome-steam-frame list, checks each project's releases for a download the Frame can install, and lists them with New badges, in the headset app (X / Discover row) and on the web page. Cached, rechecked only when a repo changes, and limited per run so update checks keep enough of GitHub's unauthenticated rate limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VHDaz86aayqMULHsjByEmT
1169 lines
45 KiB
C
1169 lines
45 KiB
C
/* Frametanium for the headset: a controller-driven front end for the Frametanium service (manager.py), which
|
|
* does the downloading and installing. It talks to the service on 127.0.0.1 with plain HTTP and reads simple
|
|
* tab-separated replies.
|
|
*
|
|
* Screens: the app list; an app's menu (install/update, versions, previous version, pre-releases, remove); adding
|
|
* an app (type a GitHub repo on an on-screen keyboard, paste a link, or pick a suggestion); discover (new Steam Frame
|
|
* projects on GitHub with a release to install); and the found repo, where you pick its download and add it.
|
|
*
|
|
* Controls: D-pad/left stick to choose, A select, B back/close, Y add an app, X discover, Select update all, Start
|
|
* check for updates. Keyboard: arrows, Enter, Esc, and typing on the on-screen keyboard screen. */
|
|
|
|
#include <SDL.h>
|
|
#include <arpa/inet.h>
|
|
#include <fcntl.h>
|
|
#include <netinet/in.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/time.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
|
|
#include "third_party/font8x8_basic.h"
|
|
|
|
#define DEFAULT_PORT 8643
|
|
#define MAX_APPS 64
|
|
#define MAX_RELEASES 40
|
|
#define MAX_SUGGEST 32
|
|
#define MAX_ASSETS 16
|
|
#define MAX_DISC 64
|
|
|
|
typedef struct {
|
|
char id[64], name[128], installed[64], latest[64], status[32], message[256], repo[128], mode[16], job[16];
|
|
float progress;
|
|
int can_rollback, prereleases, builtin;
|
|
} App;
|
|
|
|
typedef struct {
|
|
char tag[64], name[128], date[16];
|
|
int prerelease;
|
|
} Release;
|
|
|
|
typedef struct {
|
|
char repo[128], name[128], note[160];
|
|
} Suggestion;
|
|
|
|
typedef struct {
|
|
char repo[128], name[128], description[300], tag[64], date[16], asset[128];
|
|
int stars, arm64, prerelease, is_new;
|
|
} Disc;
|
|
|
|
typedef struct {
|
|
char repo[128], name[128], description[300], tag[64], date[16], existing[64];
|
|
int prereleases, nassets, asset;
|
|
char assets[MAX_ASSETS][128];
|
|
int arm64[MAX_ASSETS];
|
|
long sizes[MAX_ASSETS];
|
|
} Found;
|
|
|
|
static SDL_Renderer* ren;
|
|
static int W, H, S; /* screen size and text scale */
|
|
static int service_port = DEFAULT_PORT;
|
|
|
|
/* ---------------------------------------------------------------- text */
|
|
|
|
static void color(Uint32 rgb) {
|
|
SDL_SetRenderDrawColor(ren, (rgb >> 16) & 0xff, (rgb >> 8) & 0xff, rgb & 0xff, 255);
|
|
}
|
|
|
|
static int text(const char* s, int x, int y, int scale, Uint32 rgb) {
|
|
static SDL_Rect rects[4096];
|
|
int n = 0, cx = x;
|
|
color(rgb);
|
|
for (; *s; s++) {
|
|
unsigned char c = (unsigned char)*s;
|
|
if (c >= 128) continue; /* the font is ASCII only: leave other characters out */
|
|
for (int row = 0; row < 8; row++) {
|
|
unsigned char bits = (unsigned char)font8x8_basic[c][row];
|
|
for (int col = 0; col < 8; col++)
|
|
if (bits & (1 << col)) {
|
|
rects[n++] = (SDL_Rect){ cx + col * scale, y + row * scale, scale, scale };
|
|
if (n == 4096) {
|
|
SDL_RenderFillRects(ren, rects, n);
|
|
n = 0;
|
|
}
|
|
}
|
|
}
|
|
cx += 8 * scale;
|
|
}
|
|
if (n) SDL_RenderFillRects(ren, rects, n);
|
|
return cx - x;
|
|
}
|
|
|
|
/* Draws s cut to max_px (ending in "..." when cut). */
|
|
static void text_fit(const char* s, int x, int y, int scale, int max_px, Uint32 rgb) {
|
|
char buf[400];
|
|
int max_chars = max_px / (8 * scale);
|
|
snprintf(buf, sizeof buf, "%s", s);
|
|
if ((int)strlen(buf) > max_chars && max_chars > 3) {
|
|
buf[max_chars - 3] = '\0';
|
|
strcat(buf, "...");
|
|
}
|
|
text(buf, x, y, scale, rgb);
|
|
}
|
|
|
|
/* Draws s word-wrapped into lines of max_px, at most max_lines; returns the height used. */
|
|
static int text_wrap(const char* s, int x, int y, int scale, int max_px, int max_lines, Uint32 rgb) {
|
|
int per = max_px / (8 * scale), lines = 0;
|
|
const char* p = s;
|
|
while (*p && lines < max_lines) {
|
|
int len = (int)strlen(p);
|
|
int take = len;
|
|
if (len > per) {
|
|
take = per;
|
|
while (take > 0 && p[take] != ' ') take--;
|
|
if (take == 0) take = per;
|
|
}
|
|
char buf[400];
|
|
snprintf(buf, sizeof buf, "%.*s", take, p);
|
|
if (lines == max_lines - 1 && len > take && take > 3) strcpy(buf + take - 3, "...");
|
|
text(buf, x, y + lines * 12 * scale, scale, rgb);
|
|
p += take;
|
|
while (*p == ' ') p++;
|
|
lines++;
|
|
}
|
|
return lines * 12 * scale;
|
|
}
|
|
|
|
static void fill(int x, int y, int w, int h, Uint32 rgb) {
|
|
color(rgb);
|
|
SDL_RenderFillRect(ren, &(SDL_Rect){ x, y, w, h });
|
|
}
|
|
|
|
/* ---------------------------------------------------------------- service */
|
|
|
|
static void read_port(void) {
|
|
char path[512], buf[2048];
|
|
snprintf(path, sizeof path, "%s/.config/frametanium/config.json", getenv("HOME") ? getenv("HOME") : "");
|
|
FILE* f = fopen(path, "r");
|
|
if (!f) return;
|
|
size_t n = fread(buf, 1, sizeof buf - 1, f);
|
|
fclose(f);
|
|
buf[n] = '\0';
|
|
const char* p = strstr(buf, "\"port\"");
|
|
if (p && (p = strchr(p, ':'))) {
|
|
int v = atoi(p + 1);
|
|
if (v > 0 && v < 65536) service_port = v;
|
|
}
|
|
}
|
|
|
|
/* Sends a request to the service; returns the body (caller frees) or NULL if it isn't reachable. */
|
|
static char* http(const char* method, const char* path, int timeout_s) {
|
|
int s = socket(AF_INET, SOCK_STREAM, 0);
|
|
if (s < 0) return NULL;
|
|
struct timeval tv = { .tv_sec = timeout_s };
|
|
setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
|
|
struct sockaddr_in a = { .sin_family = AF_INET, .sin_port = htons(service_port) };
|
|
a.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
|
if (connect(s, (struct sockaddr*)&a, sizeof a) != 0) {
|
|
close(s);
|
|
return NULL;
|
|
}
|
|
char req[2048];
|
|
int n = snprintf(req, sizeof req, "%s %s HTTP/1.0\r\nHost: localhost\r\nContent-Length: 0\r\n\r\n", method, path);
|
|
if (n >= (int)sizeof req || write(s, req, n) != n) {
|
|
close(s);
|
|
return NULL;
|
|
}
|
|
size_t cap = 65536, len = 0;
|
|
char* buf = malloc(cap);
|
|
ssize_t r;
|
|
while ((r = read(s, buf + len, cap - len - 1)) > 0) {
|
|
len += r;
|
|
if (len + 1 >= cap) buf = realloc(buf, cap *= 2);
|
|
}
|
|
close(s);
|
|
buf[len] = '\0';
|
|
char* body = strstr(buf, "\r\n\r\n");
|
|
if (!body) {
|
|
free(buf);
|
|
return NULL;
|
|
}
|
|
memmove(buf, body + 4, strlen(body + 4) + 1);
|
|
return buf;
|
|
}
|
|
|
|
/* Percent-encodes s for a query string. */
|
|
static void urlenc(char* out, size_t sz, const char* s) {
|
|
static const char hex[] = "0123456789ABCDEF";
|
|
size_t o = 0;
|
|
for (; *s && o + 4 < sz; s++) {
|
|
unsigned char c = (unsigned char)*s;
|
|
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || strchr("-_.~", c)) {
|
|
out[o++] = c;
|
|
} else {
|
|
out[o++] = '%';
|
|
out[o++] = hex[c >> 4];
|
|
out[o++] = hex[c & 15];
|
|
}
|
|
}
|
|
out[o] = '\0';
|
|
}
|
|
|
|
/* Splits one tab-separated line into fields (in place). */
|
|
static int split(char* line, char** f, int max) {
|
|
int n = 0;
|
|
f[n++] = line;
|
|
for (char* p = line; *p && n < max; p++)
|
|
if (*p == '\t') {
|
|
*p = '\0';
|
|
f[n++] = p + 1;
|
|
}
|
|
return n;
|
|
}
|
|
|
|
static void copy(char* dst, size_t sz, const char* src) {
|
|
snprintf(dst, sz, "%s", strcmp(src, "-") == 0 ? "" : src);
|
|
}
|
|
|
|
/* Lines of a reply, one at a time (strtok_r so callers can nest). */
|
|
#define EACH_LINE(body, line, save) for (char* line = strtok_r(body, "\n", &save); line; line = strtok_r(NULL, "\n", &save))
|
|
|
|
static int load_apps(App* apps, char* notice, size_t nsz, int* notice_busy) {
|
|
char* body = http("GET", "/api/state?fmt=tsv", 5);
|
|
if (!body) return -1;
|
|
int n = 0;
|
|
char* save;
|
|
notice[0] = '\0';
|
|
*notice_busy = 0;
|
|
EACH_LINE(body, line, save) {
|
|
char* f[13];
|
|
int k = split(line, f, 13);
|
|
if (k >= 3 && !strcmp(f[0], "#")) {
|
|
copy(notice, nsz, f[2]);
|
|
*notice_busy = !strcmp(f[1], "installing");
|
|
continue;
|
|
}
|
|
if (k < 13 || n >= MAX_APPS) continue;
|
|
App* p = &apps[n++];
|
|
copy(p->id, sizeof p->id, f[0]);
|
|
copy(p->name, sizeof p->name, f[1]);
|
|
copy(p->installed, sizeof p->installed, f[2]);
|
|
copy(p->latest, sizeof p->latest, f[3]);
|
|
copy(p->status, sizeof p->status, f[4]);
|
|
p->progress = (float)atof(f[5]);
|
|
copy(p->message, sizeof p->message, f[6]);
|
|
p->can_rollback = atoi(f[7]);
|
|
copy(p->repo, sizeof p->repo, f[8]);
|
|
copy(p->mode, sizeof p->mode, f[9]);
|
|
p->prereleases = atoi(f[10]);
|
|
p->builtin = atoi(f[11]);
|
|
copy(p->job, sizeof p->job, f[12]);
|
|
}
|
|
free(body);
|
|
return n;
|
|
}
|
|
|
|
static int load_releases(const char* id, Release* rels, char* err, size_t errsz) {
|
|
char path[256];
|
|
snprintf(path, sizeof path, "/api/releases?fmt=tsv&id=%s", id);
|
|
char* body = http("GET", path, 25);
|
|
err[0] = '\0';
|
|
if (!body) {
|
|
snprintf(err, errsz, "Frametanium service isn't running.");
|
|
return 0;
|
|
}
|
|
int n = 0;
|
|
char* save;
|
|
EACH_LINE(body, line, save) {
|
|
char* f[4];
|
|
int k = split(line, f, 4);
|
|
if (k >= 2 && strcmp(f[0], "!") == 0) {
|
|
snprintf(err, errsz, "%s", f[1]);
|
|
break;
|
|
}
|
|
if (k < 4 || n >= MAX_RELEASES) continue;
|
|
Release* r = &rels[n++];
|
|
copy(r->tag, sizeof r->tag, f[0]);
|
|
copy(r->name, sizeof r->name, f[1]);
|
|
copy(r->date, sizeof r->date, f[2]);
|
|
r->prerelease = atoi(f[3]);
|
|
}
|
|
free(body);
|
|
if (n == 0 && !err[0]) snprintf(err, errsz, "No versions with a download for the Frame.");
|
|
return n;
|
|
}
|
|
|
|
static int load_suggestions(Suggestion* sug) {
|
|
char* body = http("GET", "/api/suggestions?fmt=tsv", 5);
|
|
if (!body) return 0;
|
|
int n = 0;
|
|
char* save;
|
|
EACH_LINE(body, line, save) {
|
|
char* f[4];
|
|
if (split(line, f, 4) < 4 || n >= MAX_SUGGEST || atoi(f[3])) continue;
|
|
Suggestion* s = &sug[n++];
|
|
copy(s->repo, sizeof s->repo, f[0]);
|
|
copy(s->name, sizeof s->name, f[1]);
|
|
copy(s->note, sizeof s->note, f[2]);
|
|
}
|
|
free(body);
|
|
return n;
|
|
}
|
|
|
|
/* New Steam Frame projects found on GitHub; running says a search is going, msg is its progress. */
|
|
static int load_discover(Disc* d, int* running, char* msg, size_t msz, int* nnew) {
|
|
char* body = http("GET", "/api/discover?fmt=tsv", 5);
|
|
*running = 0;
|
|
*nnew = 0;
|
|
msg[0] = '\0';
|
|
if (!body) return -1;
|
|
int n = 0;
|
|
char* save;
|
|
EACH_LINE(body, line, save) {
|
|
char* f[11];
|
|
int k = split(line, f, 11);
|
|
if (k >= 4 && !strcmp(f[0], "#")) {
|
|
*running = atoi(f[1]);
|
|
copy(msg, msz, f[2]);
|
|
*nnew = atoi(f[3]);
|
|
continue;
|
|
}
|
|
if (k < 10 || n >= MAX_DISC) continue;
|
|
Disc* x = &d[n++];
|
|
copy(x->repo, sizeof x->repo, f[0]);
|
|
copy(x->name, sizeof x->name, f[1]);
|
|
copy(x->description, sizeof x->description, f[2]);
|
|
x->stars = atoi(f[3]);
|
|
copy(x->tag, sizeof x->tag, f[4]);
|
|
copy(x->date, sizeof x->date, f[5]);
|
|
copy(x->asset, sizeof x->asset, f[6]);
|
|
x->arm64 = atoi(f[7]);
|
|
x->prerelease = atoi(f[8]);
|
|
x->is_new = atoi(f[9]);
|
|
}
|
|
free(body);
|
|
return n;
|
|
}
|
|
|
|
/* Looks a repo up; fills fd and returns 1, or writes the problem to err and returns 0. */
|
|
static int inspect(const char* repo, Found* fd, char* err, size_t errsz) {
|
|
char enc[600], path[700];
|
|
urlenc(enc, sizeof enc, repo);
|
|
snprintf(path, sizeof path, "/api/inspect?fmt=tsv&repo=%s", enc);
|
|
char* body = http("GET", path, 30);
|
|
memset(fd, 0, sizeof *fd);
|
|
err[0] = '\0';
|
|
if (!body) {
|
|
snprintf(err, errsz, "The Frametanium service didn't answer.");
|
|
return 0;
|
|
}
|
|
char* save;
|
|
int ok = 0;
|
|
EACH_LINE(body, line, save) {
|
|
char* f[9];
|
|
int k = split(line, f, 9);
|
|
if (k >= 2 && !strcmp(f[0], "!")) {
|
|
snprintf(err, errsz, "%s", f[1]);
|
|
break;
|
|
}
|
|
if (k >= 8 && !strcmp(f[0], "=")) {
|
|
copy(fd->repo, sizeof fd->repo, f[1]);
|
|
copy(fd->name, sizeof fd->name, f[2]);
|
|
copy(fd->description, sizeof fd->description, f[3]);
|
|
copy(fd->tag, sizeof fd->tag, f[4]);
|
|
copy(fd->date, sizeof fd->date, f[5]);
|
|
fd->prereleases = atoi(f[6]);
|
|
copy(fd->existing, sizeof fd->existing, f[7]);
|
|
ok = 1;
|
|
} else if (k >= 3 && ok && fd->nassets < MAX_ASSETS) {
|
|
copy(fd->assets[fd->nassets], sizeof fd->assets[0], f[0]);
|
|
fd->sizes[fd->nassets] = atol(f[1]);
|
|
fd->arm64[fd->nassets] = atoi(f[2]);
|
|
fd->nassets++;
|
|
}
|
|
}
|
|
free(body);
|
|
if (ok && fd->nassets == 0) {
|
|
snprintf(err, errsz, "No downloads for the Frame in its latest release.");
|
|
ok = 0;
|
|
}
|
|
return ok;
|
|
}
|
|
|
|
/* POSTs path; returns the reply's "!" error (or "") in err. */
|
|
static void post(const char* path, char* err, size_t errsz) {
|
|
char* body = http("POST", path, 30);
|
|
if (err) {
|
|
err[0] = '\0';
|
|
if (!body)
|
|
snprintf(err, errsz, "The Frametanium service didn't answer.");
|
|
else if (body[0] == '!' && body[1] == '\t')
|
|
snprintf(err, errsz, "%.*s", (int)strcspn(body + 2, "\n"), body + 2);
|
|
else if (strstr(body, "\"error\": \""))
|
|
snprintf(err, errsz, "%.*s", (int)strcspn(strstr(body, "\"error\": \"") + 10, "\""),
|
|
strstr(body, "\"error\": \"") + 10);
|
|
}
|
|
free(body);
|
|
}
|
|
|
|
static void action(const char* what, const char* id, const char* extra) {
|
|
char path[512];
|
|
snprintf(path, sizeof path, "/api/%s?id=%s%s", what, id, extra ? extra : "");
|
|
post(path, NULL, 0);
|
|
}
|
|
|
|
static void try_start_service(void) {
|
|
pid_t pid = fork();
|
|
if (pid == 0) {
|
|
int devnull = open("/dev/null", O_WRONLY);
|
|
if (devnull >= 0) {
|
|
dup2(devnull, 1);
|
|
dup2(devnull, 2);
|
|
}
|
|
execlp("systemctl", "systemctl", "--user", "start", "frametanium.service", (char*)NULL);
|
|
_exit(127);
|
|
}
|
|
if (pid > 0) waitpid(pid, NULL, 0);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------- drawing */
|
|
|
|
#define BG 0x0f1216
|
|
#define ROW 0x181d23
|
|
#define ROW_SEL 0x1f2a36
|
|
#define TXT 0xe8edf2
|
|
#define MUTED 0x93a0ad
|
|
#define ACCENT 0x4fa3ff
|
|
#define OK_C 0x4cc38a
|
|
#define WARN 0xf0b429
|
|
#define BAD 0xf2555a
|
|
|
|
static int busy_status(const char* s) {
|
|
return !strcmp(s, "downloading") || !strcmp(s, "installing");
|
|
}
|
|
|
|
static Uint32 status_color(const App* p) {
|
|
if (busy_status(p->status)) return ACCENT;
|
|
if (!strcmp(p->status, "update available")) return WARN;
|
|
if (!strcmp(p->status, "up to date")) return OK_C;
|
|
if (!strcmp(p->status, "error")) return BAD;
|
|
return MUTED;
|
|
}
|
|
|
|
static const char* status_label(const App* p) {
|
|
if (!strcmp(p->status, "downloading")) return "Downloading";
|
|
if (!strcmp(p->status, "installing")) return "Installing";
|
|
if (!strcmp(p->status, "update available")) return "Update available";
|
|
if (!strcmp(p->status, "up to date")) return "Up to date";
|
|
if (!strcmp(p->status, "error")) return "Problem";
|
|
return "Not installed";
|
|
}
|
|
|
|
static void clear(void) {
|
|
color(BG);
|
|
SDL_RenderClear(ren);
|
|
}
|
|
|
|
static int title(const char* s) {
|
|
int m = W / 20;
|
|
text_fit(s, m, m, S * 2, W - 2 * m, TXT);
|
|
return m + 22 * S;
|
|
}
|
|
|
|
static void legend(const char* s) {
|
|
int y = H - H / 20 - 8 * S;
|
|
fill(0, y - 12 * S, W, H - y + 12 * S, 0x151a20);
|
|
text_fit(s, W / 20, y, S, W - W / 10, MUTED);
|
|
}
|
|
|
|
/* First visible row so that sel stays on screen. */
|
|
static int scroll_first(int sel, int max_rows) {
|
|
return sel >= max_rows ? sel - max_rows + 1 : 0;
|
|
}
|
|
|
|
static void draw_list(App* apps, int n, int sel, const char* banner, Uint32 banner_rgb, int disc_new) {
|
|
clear();
|
|
int m = W / 20, y = title("Frametanium");
|
|
if (banner && *banner) {
|
|
text_fit(banner, m, y, S, W - 2 * m, banner_rgb);
|
|
y += 14 * S;
|
|
}
|
|
if (n < 0) {
|
|
text("Starting the Frametanium service...", m, y + 8 * S, S, MUTED);
|
|
legend("B Close");
|
|
SDL_RenderPresent(ren);
|
|
return;
|
|
}
|
|
int row_h = 50 * S;
|
|
int max_rows = (H - y - 40 * S) / row_h;
|
|
if (max_rows < 1) max_rows = 1;
|
|
int first = scroll_first(sel, max_rows);
|
|
for (int i = first; i <= n + 1 && i < first + max_rows; i++) {
|
|
int x = m, w = W - 2 * m;
|
|
if (i >= n) { /* the "discover" and "add" rows */
|
|
fill(x, y, w, 26 * S, i == sel ? ROW_SEL : ROW);
|
|
if (i == sel) fill(x, y, 3 * S, 26 * S, ACCENT);
|
|
char label[100];
|
|
if (i == n && disc_new > 0)
|
|
snprintf(label, sizeof label, "* Discover new apps for the Frame (%d new)", disc_new);
|
|
else
|
|
snprintf(label, sizeof label, "%s", i == n ? "* Discover new apps for the Frame" : "+ Add an app from GitHub");
|
|
text(label, x + 10 * S, y + 9 * S, S, ACCENT);
|
|
y += 32 * S;
|
|
continue;
|
|
}
|
|
App* p = &apps[i];
|
|
fill(x, y, w, row_h - 6 * S, i == sel ? ROW_SEL : ROW);
|
|
if (i == sel) fill(x, y, 3 * S, row_h - 6 * S, ACCENT);
|
|
int tx = x + 10 * S, ty = y + 8 * S;
|
|
const char* st = status_label(p);
|
|
int sw = (int)strlen(st) * 8 * S;
|
|
text_fit(p->name, tx, ty, S, w - 30 * S - sw, TXT);
|
|
text(st, x + w - 10 * S - sw, ty, S, status_color(p));
|
|
ty += 13 * S;
|
|
char line[300];
|
|
snprintf(line, sizeof line, "%s Installed: %s Latest: %s", p->repo, p->installed[0] ? p->installed : "-",
|
|
p->latest[0] ? p->latest : "-");
|
|
text_fit(line, tx, ty, S, w - 20 * S, MUTED);
|
|
ty += 12 * S;
|
|
if (busy_status(p->status)) {
|
|
int bw = w - 20 * S;
|
|
fill(tx, ty + 2 * S, bw, 4 * S, 0x2a313a);
|
|
fill(tx, ty + 2 * S, (int)(bw * p->progress), 4 * S, ACCENT);
|
|
} else if (p->message[0]) {
|
|
text_fit(p->message, tx, ty, S, w - 20 * S, !strcmp(p->job, "error") ? BAD : OK_C);
|
|
}
|
|
y += row_h;
|
|
}
|
|
legend("A Open Y Add X Discover Select Update all Start Check B Close");
|
|
SDL_RenderPresent(ren);
|
|
}
|
|
|
|
/* A simple list of choices under a heading. */
|
|
static void draw_menu(const char* heading, const char* sub, const char** items, int n, int sel, const char* err,
|
|
const char* keys) {
|
|
clear();
|
|
int m = W / 20, y = title(heading);
|
|
if (sub && *sub) {
|
|
text_fit(sub, m, y, S, W - 2 * m, MUTED);
|
|
y += 16 * S;
|
|
}
|
|
if (err && *err) {
|
|
y += text_wrap(err, m, y, S, W - 2 * m, 3, BAD) + 4 * S;
|
|
}
|
|
int row_h = 18 * S;
|
|
int max_rows = (H - y - 40 * S) / row_h;
|
|
if (max_rows < 1) max_rows = 1;
|
|
int first = scroll_first(sel, max_rows);
|
|
for (int i = first; i < n && i < first + max_rows; i++) {
|
|
if (i == sel) {
|
|
fill(m, y - 5 * S, W - 2 * m, row_h - 2 * S, ROW_SEL);
|
|
fill(m, y - 5 * S, 3 * S, row_h - 2 * S, ACCENT);
|
|
}
|
|
text_fit(items[i], m + 10 * S, y, S, W - 2 * m - 20 * S, i == sel ? TXT : MUTED);
|
|
y += row_h;
|
|
}
|
|
legend(keys);
|
|
SDL_RenderPresent(ren);
|
|
}
|
|
|
|
static void draw_wait(const char* heading, const char* what) {
|
|
clear();
|
|
int m = W / 20, y = title(heading);
|
|
text_fit(what, m, y + 8 * S, S, W - 2 * m, MUTED);
|
|
SDL_RenderPresent(ren);
|
|
}
|
|
|
|
static void draw_discover(const Disc* d, int n, int sel, int running, const char* msg, const char* err) {
|
|
clear();
|
|
int m = W / 20, y = title("Discover new apps for the Frame");
|
|
const char* sub = running ? (msg[0] ? msg : "Searching GitHub for Steam Frame projects...")
|
|
: msg[0] ? msg : "Steam Frame projects on GitHub with a release you can install.";
|
|
text_fit(sub, m, y, S, W - 2 * m, running ? ACCENT : MUTED);
|
|
y += 16 * S;
|
|
if (err && *err) {
|
|
text_fit(err, m, y, S, W - 2 * m, BAD);
|
|
y += 14 * S;
|
|
}
|
|
if (n <= 0) {
|
|
text(n < 0 ? "The Frametanium service didn't answer." : running ? "" : "Nothing new right now. Start searches again.",
|
|
m, y + 8 * S, S, MUTED);
|
|
}
|
|
int row_h = 50 * S;
|
|
int max_rows = (H - y - 40 * S) / row_h;
|
|
if (max_rows < 1) max_rows = 1;
|
|
int first = scroll_first(sel, max_rows);
|
|
for (int i = first; i < n && i < first + max_rows; i++) {
|
|
const Disc* x = &d[i];
|
|
int w = W - 2 * m, tx = m + 10 * S, ty = y + 8 * S;
|
|
fill(m, y, w, row_h - 6 * S, i == sel ? ROW_SEL : ROW);
|
|
if (i == sel) fill(m, y, 3 * S, row_h - 6 * S, ACCENT);
|
|
int nw = 0;
|
|
if (x->is_new) {
|
|
nw = 6 * 8 * S;
|
|
text("NEW", m + w - 10 * S - 3 * 8 * S, ty, S, ACCENT);
|
|
}
|
|
text_fit(x->name, tx, ty, S, w - 20 * S - nw, TXT);
|
|
ty += 13 * S;
|
|
if (x->description[0]) text_fit(x->description, tx, ty, S, w - 20 * S, MUTED);
|
|
ty += 12 * S;
|
|
char line[400];
|
|
snprintf(line, sizeof line, "%s %s%s %s %d stars%s", x->repo, x->tag, x->prerelease ? " (pre)" : "", x->date,
|
|
x->stars, x->arm64 ? "" : " not marked ARM64");
|
|
text_fit(line, tx, ty, S, w - 20 * S, x->arm64 ? MUTED : WARN);
|
|
y += row_h;
|
|
}
|
|
legend("A Look at it X Not interested Start Search again B Back");
|
|
SDL_RenderPresent(ren);
|
|
}
|
|
|
|
/* ---- on-screen keyboard */
|
|
|
|
static const char* KB_ROWS[] = { "1234567890", "qwertyuiop", "asdfghjkl-", "zxcvbnm_./" };
|
|
static const char* KB_KEYS[] = { "Delete", "Paste", "Clear", "Done" };
|
|
#define KB_SPECIAL_ROW 4
|
|
|
|
static void draw_keyboard(const char* entry, int row, int col, const char* err) {
|
|
clear();
|
|
int m = W / 20, y = title("Add an app from GitHub");
|
|
text("Type the repo, like owner/name (a full github.com link works too):", m, y, S, MUTED);
|
|
y += 16 * S;
|
|
fill(m, y, W - 2 * m, 20 * S, ROW);
|
|
char shown[300];
|
|
snprintf(shown, sizeof shown, "%s_", entry);
|
|
int maxc = (W - 2 * m - 20 * S) / (8 * S);
|
|
int len = (int)strlen(shown);
|
|
text(shown + (len > maxc ? len - maxc : 0), m + 10 * S, y + 6 * S, S, TXT);
|
|
y += 30 * S;
|
|
if (err && *err) {
|
|
text_fit(err, m, y, S, W - 2 * m, BAD);
|
|
}
|
|
y += 14 * S;
|
|
int key = 30 * S, gap = 4 * S;
|
|
int kw = 10 * key + 9 * gap;
|
|
int x0 = (W - kw) / 2;
|
|
for (int r = 0; r < 4; r++) {
|
|
for (int c = 0; c < 10; c++) {
|
|
int x = x0 + c * (key + gap), on = r == row && c == col;
|
|
fill(x, y, key, key, on ? ACCENT : ROW_SEL);
|
|
char ch[2] = { KB_ROWS[r][c], 0 };
|
|
text(ch, x + key / 2 - 4 * S, y + key / 2 - 4 * S, S, on ? 0xffffff : TXT);
|
|
}
|
|
y += key + gap;
|
|
}
|
|
int sw = (kw - 3 * gap) / 4;
|
|
for (int c = 0; c < 4; c++) {
|
|
int x = x0 + c * (sw + gap), on = row == KB_SPECIAL_ROW && c == col;
|
|
fill(x, y, sw, key, on ? ACCENT : ROW_SEL);
|
|
int tw = (int)strlen(KB_KEYS[c]) * 8 * S;
|
|
text(KB_KEYS[c], x + (sw - tw) / 2, y + key / 2 - 4 * S, S, on ? 0xffffff : TXT);
|
|
}
|
|
legend("A Type X Delete Start Done B Cancel");
|
|
SDL_RenderPresent(ren);
|
|
}
|
|
|
|
static void draw_found(const Found* fd, const char* err) {
|
|
clear();
|
|
int m = W / 20, y = title(fd->name);
|
|
text_fit(fd->repo, m, y, S, W - 2 * m, ACCENT);
|
|
y += 14 * S;
|
|
if (fd->description[0]) y += text_wrap(fd->description, m, y, S, W - 2 * m, 3, MUTED) + 6 * S;
|
|
char line[400];
|
|
snprintf(line, sizeof line, "Latest release: %s (%s)%s", fd->tag, fd->date,
|
|
fd->prereleases ? " pre-release" : "");
|
|
text_fit(line, m, y, S, W - 2 * m, TXT);
|
|
y += 20 * S;
|
|
if (fd->existing[0]) {
|
|
text("It's already on your list.", m, y, S, OK_C);
|
|
legend("A Show it B Back");
|
|
SDL_RenderPresent(ren);
|
|
return;
|
|
}
|
|
text("Download:", m, y, S, MUTED);
|
|
y += 14 * S;
|
|
fill(m, y - 5 * S, W - 2 * m, 18 * S, ROW_SEL);
|
|
snprintf(line, sizeof line, "%s %s (%.1f MB)%s %s", fd->nassets > 1 ? "<" : " ", fd->assets[fd->asset],
|
|
fd->sizes[fd->asset] / 1048576.0, fd->arm64[fd->asset] ? "" : " not ARM64", fd->nassets > 1 ? ">" : "");
|
|
text_fit(line, m + 10 * S, y, S, W - 2 * m - 20 * S, fd->arm64[fd->asset] ? TXT : WARN);
|
|
y += 22 * S;
|
|
if (fd->nassets > 1) {
|
|
snprintf(line, sizeof line, "%d downloads to choose from; Left/Right to switch.", fd->nassets);
|
|
text(line, m, y, S, MUTED);
|
|
y += 14 * S;
|
|
}
|
|
text_wrap("Frametanium unpacks it and either adds it to your Steam library or, if it comes with an install.sh, "
|
|
"runs that. Only add apps you trust.", m, y, S, W - 2 * m, 3, MUTED);
|
|
y += 40 * S;
|
|
if (err && *err) text_wrap(err, m, y, S, W - 2 * m, 3, BAD);
|
|
legend(fd->nassets > 1 ? "A Add and install Left/Right Download B Back" : "A Add and install B Back");
|
|
SDL_RenderPresent(ren);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------- main */
|
|
|
|
enum { NONE, UP, DOWN, LEFT, RIGHT, OK, BACK, ADD, UPDATE_ALL, REFRESH, DELETE };
|
|
enum { LIST, MENU, VERSIONS, CONFIRM, ADDING, KEYBOARD, FOUND, DISCOVER };
|
|
|
|
static int map_button(int b) {
|
|
switch (b) {
|
|
case SDL_CONTROLLER_BUTTON_DPAD_UP: return UP;
|
|
case SDL_CONTROLLER_BUTTON_DPAD_DOWN: return DOWN;
|
|
case SDL_CONTROLLER_BUTTON_DPAD_LEFT: return LEFT;
|
|
case SDL_CONTROLLER_BUTTON_DPAD_RIGHT: return RIGHT;
|
|
case SDL_CONTROLLER_BUTTON_A: return OK;
|
|
case SDL_CONTROLLER_BUTTON_B: return BACK;
|
|
case SDL_CONTROLLER_BUTTON_Y: return ADD;
|
|
case SDL_CONTROLLER_BUTTON_X: return DELETE;
|
|
case SDL_CONTROLLER_BUTTON_BACK: return UPDATE_ALL;
|
|
case SDL_CONTROLLER_BUTTON_START: return REFRESH;
|
|
default: return NONE;
|
|
}
|
|
}
|
|
|
|
static int map_key(SDL_Keycode k, int typing) {
|
|
switch (k) {
|
|
case SDLK_UP: return UP;
|
|
case SDLK_DOWN: return DOWN;
|
|
case SDLK_LEFT: return LEFT;
|
|
case SDLK_RIGHT: return RIGHT;
|
|
case SDLK_RETURN: return typing ? REFRESH : OK; /* Enter finishes typing */
|
|
case SDLK_ESCAPE: return BACK;
|
|
case SDLK_BACKSPACE: return typing ? DELETE : BACK;
|
|
default: break;
|
|
}
|
|
if (typing) return NONE;
|
|
switch (k) {
|
|
case SDLK_a: return ADD;
|
|
case SDLK_u: return UPDATE_ALL;
|
|
case SDLK_r: return REFRESH;
|
|
case SDLK_x: return DELETE;
|
|
default: return NONE;
|
|
}
|
|
}
|
|
|
|
int main(void) {
|
|
/* PM_SCREENSHOT=<file.bmp> draws one frame into memory, saves it and exits (testing without a display);
|
|
* PM_SIZE=WxH sets its size, PM_SCREEN=menu|versions|add|keyboard|found|discover picks the screen (for the first app;
|
|
* PM_REPO is the repo for "found", PM_TYPED the text for "keyboard"). */
|
|
const char* shot = getenv("PM_SCREENSHOT");
|
|
SDL_Surface* shot_surface = NULL;
|
|
if (shot) {
|
|
int sw = 1280, sh = 720;
|
|
if (getenv("PM_SIZE")) sscanf(getenv("PM_SIZE"), "%dx%d", &sw, &sh);
|
|
SDL_Init(0);
|
|
shot_surface = SDL_CreateRGBSurfaceWithFormat(0, sw, sh, 32, SDL_PIXELFORMAT_ARGB8888);
|
|
ren = SDL_CreateSoftwareRenderer(shot_surface);
|
|
} else {
|
|
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) != 0) {
|
|
fprintf(stderr, "SDL_Init: %s\n", SDL_GetError());
|
|
return 1;
|
|
}
|
|
Uint32 flags = getenv("PM_WINDOWED") ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP;
|
|
SDL_Window* win = SDL_CreateWindow("Frametanium", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 1280, 720, flags);
|
|
if (!win) return 1;
|
|
ren = SDL_CreateRenderer(win, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
|
|
if (!ren) ren = SDL_CreateRenderer(win, -1, SDL_RENDERER_SOFTWARE);
|
|
}
|
|
if (!ren) return 1;
|
|
SDL_ShowCursor(SDL_DISABLE);
|
|
SDL_StopTextInput();
|
|
read_port();
|
|
|
|
static App apps[MAX_APPS];
|
|
static Release rels[MAX_RELEASES];
|
|
static Suggestion sug[MAX_SUGGEST];
|
|
static Found fd;
|
|
static Disc disc[MAX_DISC];
|
|
int napps = -1, sel = 0, nrels = 0, rsel = 0, msel = 0, asel = 0, nsug = 0, tried_start = 0;
|
|
int ndisc = 0, dsel = 0, disc_running = 0, disc_new = 0, found_from = ADDING;
|
|
char disc_msg[300] = "";
|
|
Uint32 next_disc_poll = 0;
|
|
int screen = LIST, kb_row = 1, kb_col = 0, notice_busy = 0;
|
|
char err[300] = "", banner[300] = "", notice[300] = "", entry[256] = "", want_id[64] = "";
|
|
char hint_name[128] = "";
|
|
Uint32 next_poll = 0, axis_ready = 0;
|
|
const char* menu_items[8];
|
|
int menu_acts[8], nmenu = 0;
|
|
char menu_text[8][160];
|
|
enum { M_INSTALL, M_VERSIONS, M_PREVIOUS, M_PRE, M_REMOVE };
|
|
|
|
if (shot && getenv("PM_SCREEN")) {
|
|
const char* sc = getenv("PM_SCREEN");
|
|
napps = load_apps(apps, notice, sizeof notice, ¬ice_busy);
|
|
if (!strcmp(sc, "menu")) screen = MENU;
|
|
if (!strcmp(sc, "versions") && napps > 0) {
|
|
nrels = load_releases(apps[0].id, rels, err, sizeof err);
|
|
screen = VERSIONS;
|
|
}
|
|
if (!strcmp(sc, "add")) {
|
|
nsug = load_suggestions(sug);
|
|
screen = ADDING;
|
|
}
|
|
if (!strcmp(sc, "keyboard")) {
|
|
snprintf(entry, sizeof entry, "%s", getenv("PM_TYPED") ? getenv("PM_TYPED") : "");
|
|
kb_row = 2, kb_col = 3;
|
|
screen = KEYBOARD;
|
|
}
|
|
if (!strcmp(sc, "found")) {
|
|
inspect(getenv("PM_REPO") ? getenv("PM_REPO") : "", &fd, err, sizeof err);
|
|
screen = FOUND;
|
|
}
|
|
if (!strcmp(sc, "confirm")) screen = CONFIRM;
|
|
if (!strcmp(sc, "discover")) screen = DISCOVER;
|
|
ndisc = load_discover(disc, &disc_running, disc_msg, sizeof disc_msg, &disc_new);
|
|
next_poll = 0xffffffff;
|
|
}
|
|
|
|
for (;;) {
|
|
int act = NONE;
|
|
const char* typed = NULL;
|
|
SDL_Event e;
|
|
while (SDL_PollEvent(&e)) {
|
|
if (e.type == SDL_QUIT) goto done;
|
|
if (e.type == SDL_CONTROLLERDEVICEADDED) SDL_GameControllerOpen(e.cdevice.which);
|
|
if (e.type == SDL_CONTROLLERBUTTONDOWN) act = map_button(e.cbutton.button);
|
|
if (e.type == SDL_KEYDOWN) act = map_key(e.key.keysym.sym, screen == KEYBOARD);
|
|
if (e.type == SDL_TEXTINPUT && screen == KEYBOARD) typed = e.text.text;
|
|
if (e.type == SDL_CONTROLLERAXISMOTION && SDL_GetTicks() >= axis_ready && abs(e.caxis.value) > 24000) {
|
|
if (e.caxis.axis == SDL_CONTROLLER_AXIS_LEFTY) act = e.caxis.value < 0 ? UP : DOWN;
|
|
if (e.caxis.axis == SDL_CONTROLLER_AXIS_LEFTX) act = e.caxis.value < 0 ? LEFT : RIGHT;
|
|
if (act != NONE) axis_ready = SDL_GetTicks() + 220;
|
|
}
|
|
}
|
|
|
|
if (SDL_GetTicks() >= next_poll) {
|
|
napps = load_apps(apps, notice, sizeof notice, ¬ice_busy);
|
|
if (napps < 0 && !tried_start) {
|
|
tried_start = 1;
|
|
try_start_service();
|
|
}
|
|
int busy = notice_busy;
|
|
for (int i = 0; i < napps; i++) busy |= busy_status(apps[i].status);
|
|
next_poll = SDL_GetTicks() + (busy ? 400 : 2000);
|
|
if (want_id[0]) /* jump to an app that was just added */
|
|
for (int i = 0; i < napps; i++)
|
|
if (!strcmp(apps[i].id, want_id)) {
|
|
sel = i;
|
|
want_id[0] = '\0';
|
|
}
|
|
if (sel > napps + 1) sel = napps > 0 ? napps : 0;
|
|
snprintf(banner, sizeof banner, "%s",
|
|
napps < 0 && tried_start && SDL_GetTicks() > 8000
|
|
? "The Frametanium service isn't running. Restart the Frame and try again."
|
|
: notice);
|
|
}
|
|
if (SDL_GetTicks() >= next_disc_poll) {
|
|
ndisc = load_discover(disc, &disc_running, disc_msg, sizeof disc_msg, &disc_new);
|
|
next_disc_poll = SDL_GetTicks() + (screen == DISCOVER && disc_running ? 1500 : 30000);
|
|
if (dsel >= ndisc) dsel = ndisc > 0 ? ndisc - 1 : 0;
|
|
}
|
|
App* cur = sel < napps ? &apps[sel] : NULL;
|
|
|
|
switch (screen) {
|
|
case LIST:
|
|
if (act == UP && sel > 0) sel--;
|
|
if (act == DOWN && sel < napps + 1) sel++;
|
|
if (act == BACK) goto done;
|
|
if (napps < 0) break;
|
|
if (act == OK && cur) {
|
|
screen = MENU;
|
|
msel = 0;
|
|
err[0] = '\0';
|
|
} else if (act == DELETE || (act == OK && sel == napps)) {
|
|
screen = DISCOVER;
|
|
dsel = 0;
|
|
err[0] = '\0';
|
|
next_disc_poll = 0;
|
|
} else if (act == ADD || (act == OK && !cur)) {
|
|
draw_wait("Add an app from GitHub", "Loading suggestions...");
|
|
nsug = load_suggestions(sug);
|
|
asel = 0;
|
|
err[0] = '\0';
|
|
screen = ADDING;
|
|
}
|
|
if (act == REFRESH) post("/api/refresh", NULL, 0);
|
|
if (act == UPDATE_ALL) post("/api/update_all", NULL, 0);
|
|
if (act != NONE) next_poll = SDL_GetTicks() + 150;
|
|
break;
|
|
|
|
case MENU:
|
|
if (!cur) {
|
|
screen = LIST;
|
|
break;
|
|
}
|
|
nmenu = 0;
|
|
if (cur->latest[0]) {
|
|
if (!cur->installed[0])
|
|
snprintf(menu_text[nmenu], 160, "Install %s", cur->latest);
|
|
else if (!strcmp(cur->status, "update available"))
|
|
snprintf(menu_text[nmenu], 160, "Update to %s", cur->latest);
|
|
else
|
|
snprintf(menu_text[nmenu], 160, "Reinstall %s", cur->latest);
|
|
menu_acts[nmenu++] = M_INSTALL;
|
|
}
|
|
snprintf(menu_text[nmenu], 160, "Choose a version...");
|
|
menu_acts[nmenu++] = M_VERSIONS;
|
|
if (cur->can_rollback) {
|
|
snprintf(menu_text[nmenu], 160, "Go back to the previous version");
|
|
menu_acts[nmenu++] = M_PREVIOUS;
|
|
}
|
|
snprintf(menu_text[nmenu], 160, "Pre-releases: %s", cur->prereleases ? "included" : "skipped");
|
|
menu_acts[nmenu++] = M_PRE;
|
|
snprintf(menu_text[nmenu], 160, "Remove...");
|
|
menu_acts[nmenu++] = M_REMOVE;
|
|
for (int i = 0; i < nmenu; i++) menu_items[i] = menu_text[i];
|
|
if (msel >= nmenu) msel = nmenu - 1;
|
|
if (act == UP && msel > 0) msel--;
|
|
if (act == DOWN && msel < nmenu - 1) msel++;
|
|
if (act == BACK) screen = LIST;
|
|
if (act == OK) {
|
|
int m = menu_acts[msel];
|
|
if (m == M_INSTALL) {
|
|
action("install", cur->id, NULL);
|
|
screen = LIST;
|
|
} else if (m == M_VERSIONS) {
|
|
draw_wait(cur->name, "Loading versions...");
|
|
nrels = load_releases(cur->id, rels, err, sizeof err);
|
|
rsel = 0;
|
|
for (int i = 0; i < nrels; i++)
|
|
if (!strcmp(rels[i].tag, cur->installed)) rsel = i;
|
|
screen = VERSIONS;
|
|
} else if (m == M_PREVIOUS) {
|
|
action("rollback", cur->id, NULL);
|
|
screen = LIST;
|
|
} else if (m == M_PRE) {
|
|
action("settings", cur->id, cur->prereleases ? "&prereleases=0" : "&prereleases=1");
|
|
cur->prereleases = !cur->prereleases;
|
|
} else if (m == M_REMOVE) {
|
|
screen = CONFIRM;
|
|
}
|
|
next_poll = SDL_GetTicks() + 150;
|
|
}
|
|
break;
|
|
|
|
case VERSIONS:
|
|
if (act == UP && rsel > 0) rsel--;
|
|
if (act == DOWN && rsel < nrels - 1) rsel++;
|
|
if (act == BACK) screen = MENU;
|
|
if (act == OK && nrels > 0 && cur) {
|
|
char extra[100], enc[80];
|
|
urlenc(enc, sizeof enc, rels[rsel].tag);
|
|
snprintf(extra, sizeof extra, "&tag=%s", enc);
|
|
action("install", cur->id, extra);
|
|
screen = LIST;
|
|
next_poll = 0;
|
|
}
|
|
break;
|
|
|
|
case CONFIRM:
|
|
if (act == BACK) screen = MENU;
|
|
if (act == OK && cur) {
|
|
action("remove", cur->id, NULL);
|
|
screen = LIST;
|
|
next_poll = SDL_GetTicks() + 300;
|
|
}
|
|
break;
|
|
|
|
case ADDING: {
|
|
int has_clip = SDL_HasClipboardText();
|
|
int nitems = 1 + has_clip + nsug;
|
|
if (act == UP && asel > 0) asel--;
|
|
if (act == DOWN && asel < nitems - 1) asel++;
|
|
if (act == BACK) screen = LIST;
|
|
if (act == OK) {
|
|
const char* repo = NULL;
|
|
hint_name[0] = '\0';
|
|
if (asel == 0) {
|
|
screen = KEYBOARD;
|
|
err[0] = '\0';
|
|
} else if (has_clip && asel == 1) {
|
|
char* clip = SDL_GetClipboardText();
|
|
snprintf(entry, sizeof entry, "%s", clip ? clip : "");
|
|
SDL_free(clip);
|
|
repo = entry;
|
|
} else {
|
|
Suggestion* s = &sug[asel - 1 - has_clip];
|
|
repo = s->repo;
|
|
snprintf(hint_name, sizeof hint_name, "%s", s->name);
|
|
}
|
|
if (repo) {
|
|
draw_wait("Add an app from GitHub", "Looking it up on GitHub...");
|
|
if (inspect(repo, &fd, err, sizeof err)) {
|
|
if (hint_name[0]) snprintf(fd.name, sizeof fd.name, "%s", hint_name);
|
|
err[0] = '\0';
|
|
found_from = ADDING;
|
|
screen = FOUND;
|
|
}
|
|
}
|
|
}
|
|
if (screen == KEYBOARD) SDL_StartTextInput();
|
|
break;
|
|
}
|
|
|
|
case DISCOVER:
|
|
if (act == UP && dsel > 0) dsel--;
|
|
if (act == DOWN && dsel < ndisc - 1) dsel++;
|
|
if (act == BACK) {
|
|
if (disc_new > 0) post("/api/discover/seen", NULL, 0); /* everything listed has now been seen */
|
|
screen = LIST;
|
|
next_disc_poll = SDL_GetTicks() + 300;
|
|
}
|
|
if (act == REFRESH && !disc_running) {
|
|
post("/api/discover/refresh", NULL, 0);
|
|
next_disc_poll = SDL_GetTicks() + 400;
|
|
}
|
|
if (act == DELETE && dsel < ndisc) {
|
|
char enc[300], path[400];
|
|
urlenc(enc, sizeof enc, disc[dsel].repo);
|
|
snprintf(path, sizeof path, "/api/discover/hide?repo=%s", enc);
|
|
post(path, NULL, 0);
|
|
next_disc_poll = 0;
|
|
}
|
|
if (act == OK && dsel < ndisc) {
|
|
draw_wait(disc[dsel].name, "Looking it up on GitHub...");
|
|
if (inspect(disc[dsel].repo, &fd, err, sizeof err)) {
|
|
snprintf(fd.name, sizeof fd.name, "%s", disc[dsel].name);
|
|
err[0] = '\0';
|
|
found_from = DISCOVER;
|
|
screen = FOUND;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case KEYBOARD: {
|
|
size_t len = strlen(entry);
|
|
if (typed && len + strlen(typed) < sizeof entry - 1) strcat(entry, typed);
|
|
if (act == UP) kb_row = kb_row > 0 ? kb_row - 1 : KB_SPECIAL_ROW;
|
|
if (act == DOWN) kb_row = kb_row < KB_SPECIAL_ROW ? kb_row + 1 : 0;
|
|
int cols = kb_row == KB_SPECIAL_ROW ? 4 : 10;
|
|
if (kb_col >= cols) kb_col = cols - 1;
|
|
if (act == LEFT) kb_col = (kb_col + cols - 1) % cols;
|
|
if (act == RIGHT) kb_col = (kb_col + 1) % cols;
|
|
int done_typing = act == REFRESH;
|
|
if (act == DELETE && len) entry[len - 1] = '\0';
|
|
if (act == OK) {
|
|
if (kb_row < KB_SPECIAL_ROW) {
|
|
if (len < sizeof entry - 2) {
|
|
entry[len] = KB_ROWS[kb_row][kb_col];
|
|
entry[len + 1] = '\0';
|
|
}
|
|
} else if (kb_col == 0 && len) {
|
|
entry[len - 1] = '\0';
|
|
} else if (kb_col == 1) {
|
|
char* clip = SDL_GetClipboardText();
|
|
if (clip && len + strlen(clip) < sizeof entry - 1) strcat(entry, clip);
|
|
SDL_free(clip);
|
|
} else if (kb_col == 2) {
|
|
entry[0] = '\0';
|
|
} else if (kb_col == 3) {
|
|
done_typing = 1;
|
|
}
|
|
}
|
|
if (act == BACK) {
|
|
SDL_StopTextInput();
|
|
screen = ADDING;
|
|
}
|
|
if (done_typing && entry[0]) {
|
|
draw_wait("Add an app from GitHub", "Looking it up on GitHub...");
|
|
if (inspect(entry, &fd, err, sizeof err)) {
|
|
err[0] = '\0';
|
|
SDL_StopTextInput();
|
|
found_from = ADDING;
|
|
screen = FOUND;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
case FOUND:
|
|
if (act == BACK) {
|
|
screen = found_from;
|
|
err[0] = '\0';
|
|
}
|
|
if (fd.existing[0]) {
|
|
if (act == OK) {
|
|
snprintf(want_id, sizeof want_id, "%s", fd.existing);
|
|
screen = LIST;
|
|
next_poll = 0;
|
|
}
|
|
break;
|
|
}
|
|
if (act == LEFT && fd.nassets > 1) fd.asset = (fd.asset + fd.nassets - 1) % fd.nassets;
|
|
if (act == RIGHT && fd.nassets > 1) fd.asset = (fd.asset + 1) % fd.nassets;
|
|
if (act == OK) {
|
|
char r[300], a[300], n[300], path[1100];
|
|
urlenc(r, sizeof r, fd.repo);
|
|
urlenc(a, sizeof a, fd.assets[fd.asset]);
|
|
urlenc(n, sizeof n, fd.name);
|
|
snprintf(path, sizeof path, "/api/add?fmt=tsv&install=1&repo=%s&asset=%s&name=%s", r, a, n);
|
|
draw_wait(fd.name, "Adding it...");
|
|
char* body = http("POST", path, 30);
|
|
if (!body) {
|
|
snprintf(err, sizeof err, "The Frametanium service didn't answer.");
|
|
} else if (body[0] == '!') {
|
|
snprintf(err, sizeof err, "%.*s", (int)strcspn(body + 2, "\n"), body + 2);
|
|
} else {
|
|
if (body[0] == '=') snprintf(want_id, sizeof want_id, "%.*s", (int)strcspn(body + 2, "\n"), body + 2);
|
|
screen = LIST;
|
|
entry[0] = '\0';
|
|
next_poll = 0;
|
|
next_disc_poll = 0;
|
|
}
|
|
free(body);
|
|
}
|
|
break;
|
|
}
|
|
|
|
SDL_GetRendererOutputSize(ren, &W, &H);
|
|
S = H >= 1000 ? 3 : 2;
|
|
cur = sel < napps ? &apps[sel] : NULL;
|
|
if (screen == MENU && cur) {
|
|
char sub[300];
|
|
snprintf(sub, sizeof sub, "%s Installed: %s Latest: %s", cur->repo,
|
|
cur->installed[0] ? cur->installed : "-", cur->latest[0] ? cur->latest : "-");
|
|
draw_menu(cur->name, sub, menu_items, nmenu, msel, cur->message[0] && !strcmp(cur->job, "error") ? cur->message : "",
|
|
"A Select B Back");
|
|
} else if (screen == VERSIONS && cur) {
|
|
const char* items[MAX_RELEASES];
|
|
static char lines[MAX_RELEASES][200];
|
|
for (int i = 0; i < nrels; i++) {
|
|
snprintf(lines[i], sizeof lines[i], "%s %s%s%s%s", rels[i].tag, rels[i].date,
|
|
rels[i].prerelease ? " pre-release" : "", !strcmp(rels[i].tag, cur->latest) ? " (latest)" : "",
|
|
!strcmp(rels[i].tag, cur->installed) ? " (installed)" : "");
|
|
items[i] = lines[i];
|
|
}
|
|
draw_menu(cur->name, "Choose a version to install:", items, nrels, rsel, err, "A Install this version B Back");
|
|
} else if (screen == CONFIRM && cur) {
|
|
const char* items[] = { "Yes, remove it" };
|
|
char sub[300];
|
|
snprintf(sub, sizeof sub, "Remove %s?", cur->name);
|
|
draw_menu(cur->name, sub, items, 1, 0,
|
|
cur->installed[0] ? "Its files are deleted. Saves and settings kept outside its folder stay." : "",
|
|
"A Remove B Cancel");
|
|
} else if (screen == ADDING) {
|
|
const char* items[2 + MAX_SUGGEST];
|
|
static char lines[MAX_SUGGEST][300];
|
|
int k = 0, has_clip = SDL_HasClipboardText();
|
|
items[k++] = "Type a GitHub repo...";
|
|
if (has_clip) items[k++] = "Paste a link from the clipboard";
|
|
for (int i = 0; i < nsug; i++) {
|
|
snprintf(lines[i], sizeof lines[i], "%s - %s", sug[i].name, sug[i].note);
|
|
items[k++] = lines[i];
|
|
}
|
|
draw_menu("Add an app from GitHub", nsug ? "Type a repo, or pick one made for the Frame:" : "", items, k, asel,
|
|
err, "A Select B Back");
|
|
} else if (screen == KEYBOARD) {
|
|
draw_keyboard(entry, kb_row, kb_col, err);
|
|
} else if (screen == FOUND) {
|
|
draw_found(&fd, err);
|
|
} else if (screen == DISCOVER) {
|
|
draw_discover(disc, ndisc, dsel, disc_running, disc_msg, err);
|
|
} else {
|
|
if (screen != LIST) screen = LIST;
|
|
int is_err = strstr(banner, "isn't running") != NULL;
|
|
draw_list(apps, napps, sel, banner, is_err ? WARN : OK_C, disc_new);
|
|
}
|
|
if (shot) {
|
|
SDL_SaveBMP(shot_surface, shot);
|
|
goto done;
|
|
}
|
|
SDL_Delay(16);
|
|
}
|
|
done:
|
|
SDL_Quit();
|
|
return 0;
|
|
}
|