5 Commits
Author SHA1 Message Date
Julian 69ac066bad Release v0.1.4 2026-10-04 20:56:19 +02:00
Julian 58836dfadc fix #6, systemd --user not available in Desktop Modes nested session
fixed with fallback in service.rs
adjusted documentation, ssh is now recommended
added to troublshooting doc
2026-10-04 20:54:58 +02:00
Julian fb78f43862 updated future section in readme 2026-10-04 01:58:15 +02:00
Julian 8e02b62ca9 proper steam frame controller icons (thanks to kenney's input prompt pack!) 2026-10-04 01:57:17 +02:00
Julian 1e01b830b4 bump java version to 17 2026-10-04 01:51:37 +02:00
15 changed files with 134 additions and 37 deletions

No files matched your search

Generated
+1 -1
View File
@@ -327,7 +327,7 @@ dependencies = [
[[package]]
name = "framemate-agent"
version = "0.1.3"
version = "0.1.4"
dependencies = [
"anyhow",
"axum",
+1 -1
View File
@@ -5,7 +5,7 @@ resolver = "3"
members = ["crates/agent"]
[workspace.package]
version = "0.1.3"
version = "0.1.4"
edition = "2024"
license = "GPL-3.0-or-later"
+9 -3
View File
@@ -54,8 +54,8 @@ You need a Steam Frame and an Android phone on the same network.
### 1. Agent on the Steam Frame
The agent ships as a Flatpak. Open a terminal on the Frame (Desktop Mode → Konsole, or via SSH)
and run:
The agent ships as a Flatpak. Open a terminal on the Frame via SSH (recommended), or Desktop
Mode → Konsole ([see here](TROUBLESHOOTING.md#installing-from-desktop-mode)), and run:
```sh
curl -LO https://github.com/nailuj05/framemate/releases/latest/download/framemate-agent.flatpak
@@ -69,10 +69,13 @@ rm framemate-agent.flatpak
- `install-service` registers a user service, so the agent starts with every boot – in
Game Mode too – and restarts it right away. It then runs a self check and prints the
address and access token (e.g. `MCK55-EGGCG`) you'll enter in the app.
In Desktop Mode it can't start the agent right away (the nested desktop has no access to the
user's systemd); the agent then starts with the next restart, and the command prints how to
start it immediately.
- `flatpak run --user dev.framemate.Agent check` repeats the self check (useful when the app can't
connect); `flatpak run --user dev.framemate.Agent token` prints just the token.
To **update**, download the new `framemate-agent.flatpak` and run the same three commands
To **update**, download the new `framemate-agent.flatpak` and run the same commands
again. To **remove** it:
```sh
@@ -174,6 +177,8 @@ FrameMate name, logo and application identifiers (forks must use their own).
- [ ] PWA for the mobile client
- [ ] Feed on the App notifying you of newly frame verified games (in your library)
- [ ] Control Downloads (pause, resume, reorder)
- [ ] View and Transfer Screenshots
- [ ] Mobile Notifications in VR
- [ ] Turn off controller, headset, etc.
## AI usage
@@ -185,6 +190,7 @@ AI was also used for exploring how the Video Stream for Mirroring could be acces
- Icons: [Material Symbols](https://github.com/google/material-design-icons) (Apache-2.0); see
[`app/src/lib/icons/LICENSES.md`](app/src/lib/icons/LICENSES.md).
- Steam Frame Controller Icons: [Kenney Input Prompts](https://kenney.nl/assets/input-prompts) (CC0)
- Game artwork is loaded from Steam's public CDN.
- FrameMate Icon made by me
+13
View File
@@ -29,6 +29,19 @@ Make sure of the following:
If you encounter an error during installation please send me the logs and the commands you ran in a github issue.
Should the app say "Wrong token", get it with `flatpak run --user dev.framemate.Agent token`. Note that reinstalling keeps the token.
### Installing from Desktop Mode
If `install-service` says "systemd isn't reachable from this terminal": Desktop Mode on the Frame is a nested desktop without access to your user's systemd, so
`install-service` can't start the agent from there. The agent is still installed and starts with
the next restart of the Frame. To start it right away, run the command it prints:
```sh
env XDG_RUNTIME_DIR=/run/user/$(id -u) DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u)/bus \
flatpak run --user dev.framemate.Agent install-service
```
Installing over SSH doesn't have this problem.
## Other issues
You encountered a different issue or think you found a bug, please let me know!
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "framemate-app",
"version": "0.1.3",
"version": "0.1.4",
"description": "",
"type": "module",
"scripts": {
+1 -1
View File
@@ -823,7 +823,7 @@ dependencies = [
[[package]]
name = "framemate-app"
version = "0.1.3"
version = "0.1.4"
dependencies = [
"tauri",
"tauri-build",
+1 -1
View File
@@ -4,7 +4,7 @@
[package]
name = "framemate-app"
description = "FrameMate companion app"
version = "0.1.3"
version = "0.1.4"
license = "GPL-3.0-or-later"
edition = "2024"
@@ -66,8 +66,8 @@ android {
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
buildFeatures {
buildConfig = true
@@ -76,7 +76,7 @@ android {
kotlin {
compilerOptions {
jvmTarget = JvmTarget.JVM_1_8
jvmTarget = JvmTarget.JVM_17
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "FrameMate",
"version": "0.1.3",
"version": "0.1.4",
"identifier": "dev.framemate.app",
"build": {
"beforeDevCommand": "bun run dev",
-1
View File
@@ -14,7 +14,6 @@ glyph), width/height removed, `fill="currentColor"` added.
| back.svg | `arrow_back` | 700 | https://cdn.jsdelivr.net/npm/@material-symbols/svg-700@0.47.6/sharp/arrow_back.svg |
| bell-badge.svg | `notifications_unread` (fill) | 400 | https://cdn.jsdelivr.net/npm/@material-symbols/svg-400@0.47.6/sharp/notifications_unread-fill.svg |
| cast.svg | `cast` | 700 | https://cdn.jsdelivr.net/npm/@material-symbols/svg-700@0.47.6/sharp/cast.svg |
| controller.svg | `stadia_controller` (fill) | 400 | https://cdn.jsdelivr.net/npm/@material-symbols/svg-400@0.47.6/sharp/stadia_controller-fill.svg |
| download.svg | `download` | 700 | https://cdn.jsdelivr.net/npm/@material-symbols/svg-700@0.47.6/sharp/download.svg |
| friends.svg | `group` (fill) | 400 | https://cdn.jsdelivr.net/npm/@material-symbols/svg-400@0.47.6/sharp/group-fill.svg |
| headset.svg | `head_mounted_device` (fill) | 400 | https://cdn.jsdelivr.net/npm/@material-symbols/svg-400@0.47.6/sharp/head_mounted_device-fill.svg |
+12
View File
@@ -0,0 +1,12 @@
<svg viewBox="8 8 48 48" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="-819.2" x2="819.2" spreadMethod="pad" gradientTransform="matrix(-0.00244140625 -0.0042266845703125 0.005279541015625 -0.0030517578125 36 36)" id="controller-left-shade">
<stop offset="0" stop-color="#FFFFFF"/>
<stop offset="1" stop-color="#DAE2E9"/>
</linearGradient>
</defs>
<g>
<path stroke="none" fill="#FFFFFF" d="M35.35 41.05 Q35.7 48.15 34.7 51.85 33.55 56.1 30.5 56 26.55 55.9 24.45 50.75 22.45 45.75 22 35.75 21.75 30.65 19.9 27.6 18 24.7 18 21 18 15.65 22.1 11.8 26.2 8 32 8 37.8 8 41.9 11.8 46 15.65 46 21 46 26.4 41.9 30.2 L38.45 32.65 37.75 32.95 37.7 32.95 Q34.95 34.15 35.2 38.75 L35.35 41.05 M33 27.5 Q33 26.45 32.25 25.7 31.5 25 30.5 25 29.45 25 28.7 25.7 28 26.45 28 27.5 28 28.55 28.7 29.25 29.45 30 30.5 30 31.5 30 32.25 29.25 33 28.55 33 27.5 M40 21.5 Q40 20.05 38.95 19 37.9 18 36.5 18 35.05 18 34 19 33 20.05 33 21.5 33 22.95 34 23.95 35.05 25 36.5 25 37.9 25 38.95 23.95 40 22.95 40 21.5 M25.6 14 Q24.95 14 24.5 14.55 24 15.1 24 15.85 L24 17 22.85 17 Q22.15 17 21.55 17.5 21 17.95 21 18.6 L21 19.4 Q21 20.05 21.55 20.5 22.15 21 22.85 21 L24 21 24 22.15 Q24 22.85 24.5 23.45 24.95 24 25.6 24 L26.4 24 Q27.05 24 27.5 23.45 28 22.85 28 22.15 L28 21 29.15 21 Q29.9 21 30.45 20.5 31 20.05 31 19.4 L31 18.6 Q31 17.95 30.45 17.5 29.9 17 29.15 17 L28 17 28 15.85 Q28 15.1 27.5 14.55 27.05 14 26.4 14 L25.6 14"/>
<path stroke="none" fill="url(#controller-left-shade)" d="M35.35 41.05 L35.2 38.75 Q34.95 34.15 37.7 32.95 L37.75 32.95 38.45 32.65 39.85 37.75 Q40.45 39.8 38.3 40.35 L36.2 40.9 35.35 41.05"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

+12
View File
@@ -0,0 +1,12 @@
<svg viewBox="8 8 48 48" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="-819.2" x2="819.2" spreadMethod="pad" gradientTransform="matrix(0.00244140625 -0.0042266845703125 -0.005279541015625 -0.0030517578125 28 36)" id="controller-right-shade">
<stop offset="0" stop-color="#FFFFFF"/>
<stop offset="1" stop-color="#DAE2E9"/>
</linearGradient>
</defs>
<g>
<path stroke="none" fill="#FFFFFF" d="M25.55 32.65 Q23.7 31.7 22.1 30.2 18 26.4 18 21 18 15.65 22.1 11.8 26.2 8 32 8 37.8 8 41.9 11.8 46 15.65 46 21 46 24.7 44.1 27.6 42.25 30.65 42 35.75 41.55 45.75 39.55 50.75 37.45 55.9 33.5 56 30.45 56.1 29.3 51.85 28.3 48.15 28.65 41.05 L28.8 38.75 Q29.05 34.15 26.3 32.95 L26.25 32.95 25.55 32.65 M39 20.5 Q39 21.55 39.75 22.25 40.5 23 41.5 23 42.55 23 43.3 22.25 44 21.55 44 20.5 44 19.45 43.3 18.7 42.55 18 41.5 18 40.5 18 39.75 18.7 39 19.45 39 20.5 M23 25.5 Q23 26.95 24.05 27.95 25.1 29 26.5 29 27.95 29 29 27.95 30 26.95 30 25.5 30 24.05 29 23 27.95 22 26.5 22 25.1 22 24.05 23 23 24.05 23 25.5 M35 16.5 Q35 17.55 35.75 18.25 36.5 19 37.5 19 38.55 19 39.3 18.25 40 17.55 40 16.5 40 15.45 39.3 14.7 38.55 14 37.5 14 36.5 14 35.75 14.7 35 15.45 35 16.5 M31 20.5 Q31 21.55 31.75 22.25 32.5 23 33.5 23 34.55 23 35.3 22.25 36 21.55 36 20.5 36 19.45 35.3 18.7 34.55 18 33.5 18 32.5 18 31.75 18.7 31 19.45 31 20.5 M35 24.5 Q35 25.55 35.75 26.25 36.5 27 37.5 27 38.55 27 39.3 26.25 40 25.55 40 24.5 40 23.45 39.3 22.7 38.55 22 37.5 22 36.5 22 35.75 22.7 35 23.45 35 24.5"/>
<path stroke="none" fill="url(#controller-right-shade)" d="M28.65 41.05 L27.8 40.9 25.7 40.35 Q23.55 39.8 24.15 37.75 L25.55 32.65 26.25 32.95 26.3 32.95 Q29.05 34.15 28.8 38.75 L28.65 41.05"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="36 -924 888 888" fill="currentColor"><path d="M189-160q-60 0-102.5-43T42-307q0-9 1-18t3-18l84-336q14-54 57-87.5t98-33.5h390q55 0 98 33.5t57 87.5l84 336q2 9 3.5 18.5T919-306q0 61-43.5 103.5T771-160q-42 0-78-22t-54-60l-28-58q-5-10-15-15t-21-5H385q-11 0-21 5t-15 15l-28 58q-18 38-54 60t-78 22Zm372.5-378.68q8.5-8.67 8.5-21.5 0-12.82-8.68-21.32-8.67-8.5-21.5-8.5-12.82 0-21.32 8.68-8.5 8.67-8.5 21.5 0 12.82 8.68 21.32 8.67 8.5 21.5 8.5 12.82 0 21.32-8.68Zm80-80q8.5-8.67 8.5-21.5 0-12.82-8.68-21.32-8.67-8.5-21.5-8.5-12.82 0-21.32 8.68-8.5 8.67-8.5 21.5 0 12.82 8.68 21.32 8.67 8.5 21.5 8.5 12.82 0 21.32-8.68Zm0 160q8.5-8.67 8.5-21.5 0-12.82-8.68-21.32-8.67-8.5-21.5-8.5-12.82 0-21.32 8.68-8.5 8.67-8.5 21.5 0 12.82 8.68 21.32 8.67 8.5 21.5 8.5 12.82 0 21.32-8.68Zm80-80q8.5-8.67 8.5-21.5 0-12.82-8.68-21.32-8.67-8.5-21.5-8.5-12.82 0-21.32 8.68-8.5 8.67-8.5 21.5 0 12.82 8.68 21.32 8.67 8.5 21.5 8.5 12.82 0 21.32-8.68ZM358-472.08q7-7.09 7-17.92v-45h45q10.83 0 17.92-7.12 7.08-7.11 7.08-18 0-10.88-7.08-17.88-7.09-7-17.92-7h-45v-45q0-10.83-7.12-17.92-7.11-7.08-18-7.08-10.88 0-17.88 7.08-7 7.09-7 17.92v45h-45q-10.83 0-17.92 7.12-7.08 7.11-7.08 18 0 10.88 7.08 17.88 7.09 7 17.92 7h45v45q0 10.83 7.12 17.92 7.11 7.08 18 7.08 10.88 0 17.88-7.08Z"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

+1 -1
View File
@@ -72,7 +72,7 @@
<div class="controllers">
{#each controllers as c (c.path)}
<div class="controller">
<Icon name="controller" size={28} />
<Icon name={c.model?.endsWith("_Right") ? "controller-right" : "controller-left"} size={28} />
<div>
<div>{controllerName(c.model)}</div>
<div class="muted small">
+78 -22
View File
@@ -1,7 +1,7 @@
//! `install-service` / `uninstall-service`: a systemd user unit that runs `flatpak run …`, since
//! Flatpaks can't autostart in Game Mode (XDG autostart only runs in Plasma).
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use anyhow::Context;
use zbus::Connection;
@@ -9,20 +9,14 @@ use zbus::Connection;
const UNIT: &str = "framemate-agent.service";
pub async fn install() -> anyhow::Result<()> {
let exec = match std::env::var("FLATPAK_ID") {
let flatpak = flatpak_run();
let exec = match &flatpak {
// `flatpak run` moves the app into its own scope outside this unit's cgroup, so
// stopping the unit would only kill the launcher; --die-with-parent ties them together.
Ok(app_id) => {
let installation = std::fs::read_to_string("/.flatpak-info")
.ok()
.and_then(|info| installation_flag(&info));
if installation.is_none() {
eprintln!("warning: couldn't tell whether the agent is a --user or --system install");
}
let flag = installation.map(|f| format!("{f} ")).unwrap_or_default();
Some((flag, app_id)) => {
format!("/usr/bin/flatpak run {flag}--die-with-parent --command=framemate-agent {app_id}")
}
Err(_) => std::env::current_exe()?.display().to_string(),
None => std::env::current_exe()?.display().to_string(),
};
let unit = format!(
"# Installed by `framemate-agent install-service`; remove with `uninstall-service`.\n\
@@ -44,8 +38,17 @@ pub async fn install() -> anyhow::Result<()> {
// Create the token now, so the starting service and a following `token` call can't race.
crate::config::load_or_create_token()?;
let systemd = Systemd::connect().await?;
systemd.call("Reload", &()).await?;
let systemd = match Systemd::reachable().await {
Ok(systemd) => systemd,
Err(e) => {
enable_without_systemd(&path)?;
println!("Installed {UNIT}, but systemd isn't reachable from this terminal ({e:#}).");
println!("That happens in the Frame's Desktop Mode. The agent starts with the next restart");
println!("of the Frame. To start it now, run this instead:\n");
println!("{}", rerun_on_user_bus(&flatpak, "install-service"));
return Ok(());
}
};
systemd.call("EnableUnitFiles", &(&[UNIT][..], false, true)).await?;
systemd.call("RestartUnit", &(UNIT, "replace")).await?;
println!("Installed and started {UNIT} ({}).", path.display());
@@ -56,21 +59,71 @@ pub async fn install() -> anyhow::Result<()> {
}
pub async fn uninstall() -> anyhow::Result<()> {
let systemd = Systemd::connect().await?;
let path = unit_path()?;
let systemd = match Systemd::reachable().await {
Ok(systemd) => systemd,
Err(e) => {
remove_file(&wants_link(&path))?;
remove_file(&path)?;
println!("Removed {UNIT}, but systemd isn't reachable from this terminal ({e:#}).");
println!("A running agent stops with the next restart of the Frame. To stop it now, run:\n");
println!("{}", rerun_on_user_bus(&flatpak_run(), "uninstall-service"));
return Ok(());
}
};
// Ignore errors: the unit may not be loaded or enabled.
let _ = systemd.call("StopUnit", &(UNIT, "replace")).await;
let _ = systemd.call("DisableUnitFiles", &(&[UNIT][..], false)).await;
let path = unit_path()?;
match std::fs::remove_file(&path) {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => return Err(e).with_context(|| format!("removing {}", path.display())),
}
remove_file(&path)?;
systemd.call("Reload", &()).await?;
println!("Removed {UNIT}.");
Ok(())
}
/// `(installation flag incl. trailing space, app id)` when running as a Flatpak.
fn flatpak_run() -> Option<(String, String)> {
let app_id = std::env::var("FLATPAK_ID").ok()?;
let installation = std::fs::read_to_string("/.flatpak-info").ok().and_then(|info| installation_flag(&info));
if installation.is_none() {
eprintln!("warning: couldn't tell whether the agent is a --user or --system install");
}
Some((installation.map(|f| format!("{f} ")).unwrap_or_default(), app_id))
}
/// What `systemctl enable` does on disk; systemd picks it up at the next start of the session.
fn enable_without_systemd(unit: &Path) -> anyhow::Result<()> {
let link = wants_link(unit);
std::fs::create_dir_all(link.parent().unwrap())?;
remove_file(&link)?;
std::os::unix::fs::symlink(unit, &link).with_context(|| format!("linking {}", link.display()))
}
fn wants_link(unit: &Path) -> PathBuf {
unit.with_file_name("default.target.wants").join(UNIT)
}
fn remove_file(path: &Path) -> anyhow::Result<()> {
match std::fs::remove_file(path) {
Err(e) if e.kind() != std::io::ErrorKind::NotFound => {
Err(e).with_context(|| format!("removing {}", path.display()))
}
_ => Ok(()),
}
}
/// The Frame's Desktop Mode is a nested Plasma session whose own session bus has no systemd
/// behind it (issue #6). Inside the sandbox only that bus is visible, so the command has to be
/// started with the real user bus.
fn rerun_on_user_bus(flatpak: &Option<(String, String)>, command: &str) -> String {
let run = match flatpak {
Some((flag, app_id)) => format!("flatpak run {flag}{app_id}"),
None => "framemate-agent".into(),
};
format!(
" env XDG_RUNTIME_DIR=/run/user/$(id -u) DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u)/bus \\\n {run} {command}"
)
}
/// `--user` or `--system`, from `app-path` in the sandbox's `/.flatpak-info`. Plain `flatpak run`
/// fails when no system installation exists (fresh Frames, Flatpak 1.15.8).
fn installation_flag(flatpak_info: &str) -> Option<&'static str> {
@@ -94,8 +147,11 @@ fn unit_path() -> anyhow::Result<PathBuf> {
struct Systemd(Connection);
impl Systemd {
async fn connect() -> anyhow::Result<Self> {
Ok(Self(Connection::session().await.context("connecting to the session bus")?))
/// Connects and checks that systemd answers on this session bus.
async fn reachable() -> anyhow::Result<Self> {
let systemd = Self(Connection::session().await.context("connecting to the session bus")?);
systemd.call("Reload", &()).await?;
Ok(systemd)
}
async fn call<B>(&self, method: &str, body: &B) -> anyhow::Result<()>