solstone-linux 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/AGENTS.md +0 -3
- solstone_linux-0.2.0/CHANGELOG.md +84 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/INSTALL.md +11 -5
- solstone_linux-0.2.0/PKG-INFO +90 -0
- solstone_linux-0.2.0/README.md +75 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/pyproject.toml +1 -1
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/scripts/release.sh +7 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/src/solstone_linux/__init__.py +1 -1
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/src/solstone_linux/activity.py +20 -10
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/src/solstone_linux/cli.py +45 -72
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/src/solstone_linux/observer.py +32 -6
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/src/solstone_linux/screencast.py +51 -3
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/src/solstone_linux/sync.py +1 -4
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/src/solstone_linux/tray.py +20 -12
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/src/solstone_linux/upload.py +44 -53
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/tests/test_activity.py +35 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/tests/test_cli.py +177 -0
- solstone_linux-0.2.0/tests/test_observer_emits_stream_silent_event.py +60 -0
- solstone_linux-0.2.0/tests/test_screencast_stop_filters_silent_streams.py +123 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/tests/test_tray.py +21 -0
- solstone_linux-0.2.0/tests/test_upload.py +119 -0
- solstone_linux-0.1.0/CHANGELOG.md +0 -27
- solstone_linux-0.1.0/PKG-INFO +0 -73
- solstone_linux-0.1.0/README.md +0 -58
- solstone_linux-0.1.0/contrib/icons/hicolor/index.theme +0 -12
- solstone_linux-0.1.0/src/solstone_linux/icons/hicolor/index.theme +0 -12
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/.gitignore +0 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/CLAUDE.md +0 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/LICENSE +0 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/Makefile +0 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/contrib/icons/hicolor/scalable/status/solstone-error.svg +0 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/contrib/icons/hicolor/scalable/status/solstone-paused.svg +0 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/contrib/icons/hicolor/scalable/status/solstone-recording.svg +0 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/contrib/icons/hicolor/scalable/status/solstone-syncing.svg +0 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/scripts/extract_changelog.sh +0 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/src/solstone_linux/audio_detect.py +0 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/src/solstone_linux/audio_mute.py +0 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/src/solstone_linux/audio_recorder.py +0 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/src/solstone_linux/chat_bridge.py +0 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/src/solstone_linux/config.py +0 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/src/solstone_linux/dbus_service.py +0 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/src/solstone_linux/dbusmenu.py +0 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/src/solstone_linux/doctor.py +0 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/src/solstone_linux/icons/hicolor/scalable/status/solstone-error.svg +0 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/src/solstone_linux/icons/hicolor/scalable/status/solstone-paused.svg +0 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/src/solstone_linux/icons/hicolor/scalable/status/solstone-recording.svg +0 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/src/solstone_linux/icons/hicolor/scalable/status/solstone-syncing.svg +0 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/src/solstone_linux/install_guard.py +0 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/src/solstone_linux/monitor_positions.py +0 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/src/solstone_linux/recovery.py +0 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/src/solstone_linux/session_env.py +0 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/src/solstone_linux/sni.py +0 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/src/solstone_linux/solstone-linux.service.in +0 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/src/solstone_linux/streams.py +0 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/tests/__init__.py +0 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/tests/test_chat_bridge.py +0 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/tests/test_config.py +0 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/tests/test_dbus_service.py +0 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/tests/test_dbusmenu.py +0 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/tests/test_doctor.py +0 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/tests/test_extract_changelog.py +0 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/tests/test_install_guard.py +0 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/tests/test_monitor_positions.py +0 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/tests/test_observer.py +0 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/tests/test_screencast.py +0 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/tests/test_session_env.py +0 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/tests/test_streams.py +0 -0
- {solstone_linux-0.1.0 → solstone_linux-0.2.0}/tests/test_sync.py +0 -0
|
@@ -146,13 +146,10 @@ Tests use pytest with standard mocking. No system dependencies required for test
|
|
|
146
146
|
## Brand canon
|
|
147
147
|
|
|
148
148
|
- **solstone-linux is an observer.** Owner-facing canon describes solstone as observers + journal; sol is the keeper who lives in and tends your journal. In engineering architecture, `observers + sol agent + journal` is the running software this repo's code talks to. This repo implements one of those observers.
|
|
149
|
-
- **The canon lives elsewhere.** Owner-facing terminology comes from sol pbc's internal brand canon (system anatomy + voice terminology guides). This repo's branded prose follows it; the canon itself is not vendored here.
|
|
150
149
|
- **Use co-experience language in branded prose.** In README, INSTALL, onboarding text, settings copy, and error messages, describe solstone-linux as something that experiences screen and audio along with the owner. Never describe it as watching, recording, monitoring, or tracking the owner.
|
|
151
150
|
- **Keep code language in code-only contexts.** Internal architecture terms such as `Capture loop`, the capture pipeline, module names, and data-path names are canon-permitted here and must not be renamed just to match branded prose.
|
|
152
151
|
- **Edit with the surface in mind.** If the owner sees the string, follow the canon. If the text is naming code, pipelines, modules, or storage artifacts for engineers, the existing internal vocabulary stays.
|
|
153
152
|
|
|
154
|
-
Canon source of truth: sol pbc's internal brand canon (system-anatomy guide).
|
|
155
|
-
|
|
156
153
|
## License
|
|
157
154
|
|
|
158
155
|
AGPL-3.0-only -- Copyright (c) 2026 sol pbc
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to solstone-linux are documented here.
|
|
4
|
+
The format is based on Keep a Changelog (https://keepachangelog.com/),
|
|
5
|
+
and this project adheres to Semantic Versioning.
|
|
6
|
+
|
|
7
|
+
## [0.2.0] - 2026-06-13
|
|
8
|
+
|
|
9
|
+
setup is now hands-off: the first time the observer runs, it connects itself
|
|
10
|
+
to your journal automatically, with no separate key step.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **first run sets itself up.** earlier versions asked you to create and paste
|
|
15
|
+
a key to connect the observer to your journal. now the observer introduces
|
|
16
|
+
itself to your journal on first run and remembers the connection on its own.
|
|
17
|
+
you go straight from install to observing, with no manual key step.
|
|
18
|
+
|
|
19
|
+
## [0.1.1] - 2026-06-02
|
|
20
|
+
|
|
21
|
+
A focused maintenance release: two reliability fixes and a round of
|
|
22
|
+
install-instruction corrections from fresh-machine testing on Fedora,
|
|
23
|
+
Debian, and openSUSE.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- **Idle monitors no longer silently drop observations.** When a monitor
|
|
28
|
+
produced no frames during a segment (a static screen with nothing
|
|
29
|
+
changing on it), GStreamer still wrote a header-only WebM file. Those
|
|
30
|
+
empty files were finalized, uploaded, and then failed to process in your
|
|
31
|
+
journal — so that monitor's segment was lost without any signal. The
|
|
32
|
+
observer now drops these empty recordings at the source and emits an
|
|
33
|
+
`observe.stream_silent` event (logged at WARNING) so the gap is visible
|
|
34
|
+
instead of silent.
|
|
35
|
+
- **Install no longer clobbers your system icon theme.** On GNOME,
|
|
36
|
+
`install-service` was writing a stray `index.theme` into the shared
|
|
37
|
+
hicolor icon directory, which shadowed the system index and caused
|
|
38
|
+
unrelated app icons to render as the solstone diamond. The installer now
|
|
39
|
+
drops only the solstone status icons (the system index already declares
|
|
40
|
+
their directory) and self-heals any previously broken install on the next
|
|
41
|
+
`install-service` run. A foreign or unreadable `index.theme` is left
|
|
42
|
+
untouched.
|
|
43
|
+
|
|
44
|
+
### Documentation
|
|
45
|
+
|
|
46
|
+
- Corrected the Fedora and Debian system-dependency lines after fresh-box
|
|
47
|
+
install testing: dropped packages that do not exist in their repos
|
|
48
|
+
(`gstreamer1-plugin-pipewire` on Fedora, `gir1.2-gdk-4.0` on Debian) and
|
|
49
|
+
hoisted the cairo / pycairo build toolchain onto the main install line so
|
|
50
|
+
a fresh install succeeds in one shot. Added `gstreamer1.0-tools` to the
|
|
51
|
+
Debian line — `gst-launch-1.0` is required for screen recording and is
|
|
52
|
+
not pulled in transitively.
|
|
53
|
+
- Added a verified openSUSE dependency block and mirrored the corrected
|
|
54
|
+
dependency lists between `README.md` and `INSTALL.md`.
|
|
55
|
+
- Updated the install path to lead with `pipx install solstone-linux`, then
|
|
56
|
+
`solstone-linux install-service`, then `solstone-linux setup`.
|
|
57
|
+
|
|
58
|
+
### Internal
|
|
59
|
+
|
|
60
|
+
- The release script now tags the commit and cuts a GitHub release only on
|
|
61
|
+
a production PyPI run; a TestPyPI run no longer leaves a tag or public
|
|
62
|
+
release behind.
|
|
63
|
+
|
|
64
|
+
## [0.1.0] - 2026-05-19
|
|
65
|
+
|
|
66
|
+
First public release of solstone-linux — the Linux desktop observer
|
|
67
|
+
for your solstone journal.
|
|
68
|
+
|
|
69
|
+
solstone-linux runs as a systemd user service in your GNOME Wayland
|
|
70
|
+
session. It experiences screen and audio along with you, holds short
|
|
71
|
+
segments locally, and uploads them to your journal in the background.
|
|
72
|
+
|
|
73
|
+
### Install paths
|
|
74
|
+
|
|
75
|
+
- From PyPI: `pipx install --system-site-packages solstone-linux`,
|
|
76
|
+
then `solstone-linux install-service` to register the systemd unit.
|
|
77
|
+
- From a clone: `git clone` this repo and run `make install-service`
|
|
78
|
+
for development or unreleased changes.
|
|
79
|
+
|
|
80
|
+
Both paths rely on host packages for PyGObject, GStreamer with the
|
|
81
|
+
PipeWire plugin, PipeWire itself, `pactl`, and `xdg-desktop-portal`
|
|
82
|
+
with ScreenCast support. PyGObject and the GStreamer bindings ride
|
|
83
|
+
along from system site-packages — that is why `--system-site-packages`
|
|
84
|
+
matters.
|
|
@@ -4,7 +4,7 @@ these instructions are for a coding agent and human working together. solstone-l
|
|
|
4
4
|
|
|
5
5
|
solstone must already be installed and running. if it isn't, start there: https://solstone.app/install
|
|
6
6
|
|
|
7
|
-
> **most users
|
|
7
|
+
> **most users install solstone-linux from PyPI in three commands** on the machine that will host the observer: `pipx install solstone-linux`, `solstone-linux install-service`, then `solstone-linux setup` (which prompts for your journal URL and auto-registers). if the observer machine can't reach your solstone host, mint a key from there first with `sol observer create <name>` and paste it during setup. the instructions below are for developers building from source or troubleshooting the install.
|
|
8
8
|
|
|
9
9
|
## before you begin
|
|
10
10
|
|
|
@@ -34,16 +34,18 @@ The observer connects to your solstone journal over HTTPS — colocation is opti
|
|
|
34
34
|
|
|
35
35
|
## install sequence
|
|
36
36
|
|
|
37
|
+
this is the developer/from-source path; most installs should use the `pipx install solstone-linux` + `solstone-linux install-service` + `solstone-linux setup` flow described in the callout above.
|
|
38
|
+
|
|
37
39
|
1. install system dependencies for your distro, including `pipx`. if you need sudo, walk your human through it.
|
|
38
40
|
|
|
39
41
|
**fedora:**
|
|
40
42
|
```
|
|
41
|
-
sudo dnf install python3-gobject gtk4 gstreamer1-plugins-base
|
|
43
|
+
sudo dnf install python3-gobject python3-cairo gtk4 gstreamer1-plugins-base pipewire-gstreamer alsa-lib-devel pulseaudio-utils pipewire-pulseaudio xdg-desktop-portal pipx gcc python3-devel pkgconf-pkg-config cairo-devel cairo-gobject-devel
|
|
42
44
|
```
|
|
43
45
|
|
|
44
46
|
**debian / ubuntu:**
|
|
45
47
|
```
|
|
46
|
-
sudo apt install python3-gi
|
|
48
|
+
sudo apt install python3-gi python3-cairo gir1.2-gtk-4.0 gstreamer1.0-pipewire gstreamer1.0-tools libasound2-dev pulseaudio-utils pipewire-pulse xdg-desktop-portal pipx gcc python3-dev pkg-config libcairo2-dev
|
|
47
49
|
```
|
|
48
50
|
|
|
49
51
|
**arch:**
|
|
@@ -60,7 +62,11 @@ The observer connects to your solstone journal over HTTPS — colocation is opti
|
|
|
60
62
|
```
|
|
61
63
|
note: package names diverge from Fedora — `typelib-1_0-Gtk-4_0` (not `gtk4`), `gstreamer-plugin-pipewire` (singular), and `alsa-devel` (not `alsa-lib-devel`).
|
|
62
64
|
|
|
63
|
-
|
|
65
|
+
the `cairo` headers + `gcc` + Python dev headers in the Fedora/Debian lines above are there because `pycairo` builds from source during install; without them the install fails with a compile error before you'd reach the Troubleshooting section. they're the same fixes listed under Troubleshooting, hoisted up so a fresh install works in one shot.
|
|
66
|
+
|
|
67
|
+
`uv` / `pipx`: Fedora packages both (`sudo dnf install uv pipx`); Debian/Ubuntu package `pipx` but not `uv`. the PyPI install flow only needs `pipx` — `uv` is optional and used by the from-source dev workflow in the Makefile.
|
|
68
|
+
|
|
69
|
+
2. cloning into `$(sol root)/observers` is only a developer convenience for keeping observer checkouts colocated with a local solstone clone. for remote-sol setups, clone anywhere — the observer runs independently of your journal at runtime:
|
|
64
70
|
```
|
|
65
71
|
cd "$(sol root)/observers"
|
|
66
72
|
git clone https://github.com/solpbc/solstone-linux.git
|
|
@@ -73,7 +79,7 @@ The observer connects to your solstone journal over HTTPS — colocation is opti
|
|
|
73
79
|
```
|
|
74
80
|
solstone-linux setup
|
|
75
81
|
```
|
|
76
|
-
this prompts for the journal URL and
|
|
82
|
+
this prompts for the journal URL and registers the observer with your journal.
|
|
77
83
|
|
|
78
84
|
4. verify the service is running:
|
|
79
85
|
```
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: solstone-linux
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Standalone Linux desktop observer for solstone
|
|
5
|
+
License-Expression: AGPL-3.0-only
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: dbus-next
|
|
9
|
+
Requires-Dist: numpy
|
|
10
|
+
Requires-Dist: pygobject
|
|
11
|
+
Requires-Dist: requests
|
|
12
|
+
Requires-Dist: soundcard
|
|
13
|
+
Requires-Dist: soundfile
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# solstone-linux
|
|
17
|
+
|
|
18
|
+
Standalone Linux desktop observer for [solstone](https://solpbc.org). Experiences your screen and audio along with you on a GNOME Wayland session, stores segments locally, and syncs to your solstone journal.
|
|
19
|
+
|
|
20
|
+
**Note:** Activity detection (idle timeout, screen lock, power save) currently requires a GNOME desktop. On other desktops (KDE, Sway, Hyprland, XFCE), the observer still experiences your screen and audio, but activity-based segment boundaries won't trigger.
|
|
21
|
+
|
|
22
|
+
## System Dependencies
|
|
23
|
+
|
|
24
|
+
**Fedora:**
|
|
25
|
+
```
|
|
26
|
+
sudo dnf install python3-gobject python3-cairo gtk4 gstreamer1-plugins-base pipewire-gstreamer alsa-lib-devel pulseaudio-utils pipewire-pulseaudio xdg-desktop-portal pipx gcc python3-devel pkgconf-pkg-config cairo-devel cairo-gobject-devel
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Debian / Ubuntu:**
|
|
30
|
+
```
|
|
31
|
+
sudo apt install python3-gi python3-cairo gir1.2-gtk-4.0 gstreamer1.0-pipewire gstreamer1.0-tools libasound2-dev pulseaudio-utils pipewire-pulse xdg-desktop-portal pipx gcc python3-dev pkg-config libcairo2-dev
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Arch:**
|
|
35
|
+
```
|
|
36
|
+
sudo pacman -S python-gobject gtk4 gstreamer gst-plugin-pipewire libpulse alsa-lib xdg-desktop-portal pipx
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**openSUSE:**
|
|
40
|
+
```
|
|
41
|
+
sudo zypper install python3-gobject python3-gobject-Gdk typelib-1_0-Gtk-4_0 gtk4-tools gstreamer-plugins-base gstreamer-plugin-pipewire pipewire-pulseaudio pulseaudio-utils alsa-devel xdg-desktop-portal python3-pipx
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
solstone (the journal) must already be installed and running on the host this observer reports to. If it isn't, start with the [journal install](https://solstone.app/install).
|
|
47
|
+
|
|
48
|
+
On the machine that will host the observer:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pipx install solstone-linux
|
|
52
|
+
solstone-linux install-service
|
|
53
|
+
solstone-linux setup
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`setup` prompts for your journal URL and registers the observer for you. If this machine can't reach your solstone host directly, mint a key from there with `sol observer create <name>` and paste it during setup.
|
|
57
|
+
|
|
58
|
+
### Developers building from source
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
git clone https://github.com/solpbc/solstone-linux.git
|
|
62
|
+
cd solstone-linux
|
|
63
|
+
make install-service
|
|
64
|
+
solstone-linux setup
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
See `INSTALL.md` for distro packages, tray notes, and troubleshooting details.
|
|
68
|
+
|
|
69
|
+
## Setup
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
solstone-linux setup
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Run
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# Foreground
|
|
79
|
+
solstone-linux run
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Status
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
solstone-linux status
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## License
|
|
89
|
+
|
|
90
|
+
AGPL-3.0-only — Copyright (c) 2026 sol pbc
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# solstone-linux
|
|
2
|
+
|
|
3
|
+
Standalone Linux desktop observer for [solstone](https://solpbc.org). Experiences your screen and audio along with you on a GNOME Wayland session, stores segments locally, and syncs to your solstone journal.
|
|
4
|
+
|
|
5
|
+
**Note:** Activity detection (idle timeout, screen lock, power save) currently requires a GNOME desktop. On other desktops (KDE, Sway, Hyprland, XFCE), the observer still experiences your screen and audio, but activity-based segment boundaries won't trigger.
|
|
6
|
+
|
|
7
|
+
## System Dependencies
|
|
8
|
+
|
|
9
|
+
**Fedora:**
|
|
10
|
+
```
|
|
11
|
+
sudo dnf install python3-gobject python3-cairo gtk4 gstreamer1-plugins-base pipewire-gstreamer alsa-lib-devel pulseaudio-utils pipewire-pulseaudio xdg-desktop-portal pipx gcc python3-devel pkgconf-pkg-config cairo-devel cairo-gobject-devel
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
**Debian / Ubuntu:**
|
|
15
|
+
```
|
|
16
|
+
sudo apt install python3-gi python3-cairo gir1.2-gtk-4.0 gstreamer1.0-pipewire gstreamer1.0-tools libasound2-dev pulseaudio-utils pipewire-pulse xdg-desktop-portal pipx gcc python3-dev pkg-config libcairo2-dev
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**Arch:**
|
|
20
|
+
```
|
|
21
|
+
sudo pacman -S python-gobject gtk4 gstreamer gst-plugin-pipewire libpulse alsa-lib xdg-desktop-portal pipx
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**openSUSE:**
|
|
25
|
+
```
|
|
26
|
+
sudo zypper install python3-gobject python3-gobject-Gdk typelib-1_0-Gtk-4_0 gtk4-tools gstreamer-plugins-base gstreamer-plugin-pipewire pipewire-pulseaudio pulseaudio-utils alsa-devel xdg-desktop-portal python3-pipx
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
solstone (the journal) must already be installed and running on the host this observer reports to. If it isn't, start with the [journal install](https://solstone.app/install).
|
|
32
|
+
|
|
33
|
+
On the machine that will host the observer:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pipx install solstone-linux
|
|
37
|
+
solstone-linux install-service
|
|
38
|
+
solstone-linux setup
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`setup` prompts for your journal URL and registers the observer for you. If this machine can't reach your solstone host directly, mint a key from there with `sol observer create <name>` and paste it during setup.
|
|
42
|
+
|
|
43
|
+
### Developers building from source
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
git clone https://github.com/solpbc/solstone-linux.git
|
|
47
|
+
cd solstone-linux
|
|
48
|
+
make install-service
|
|
49
|
+
solstone-linux setup
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
See `INSTALL.md` for distro packages, tray notes, and troubleshooting details.
|
|
53
|
+
|
|
54
|
+
## Setup
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
solstone-linux setup
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Run
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# Foreground
|
|
64
|
+
solstone-linux run
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Status
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
solstone-linux status
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## License
|
|
74
|
+
|
|
75
|
+
AGPL-3.0-only — Copyright (c) 2026 sol pbc
|
|
@@ -89,6 +89,13 @@ fi
|
|
|
89
89
|
TWINE_USERNAME=__token__ TWINE_PASSWORD="$TOKEN" \
|
|
90
90
|
"${RUN[@]}" uvx twine upload "${REPOSITORY_ARGS[@]}" dist/*
|
|
91
91
|
|
|
92
|
+
# Tag + GitHub release only for production. A TestPyPI dry-run should not leave
|
|
93
|
+
# a git tag or a public release behind.
|
|
94
|
+
if [[ "$TARGET" != "PyPI" ]]; then
|
|
95
|
+
echo "skipping git tag + GitHub release (TestPyPI run)"
|
|
96
|
+
exit 0
|
|
97
|
+
fi
|
|
98
|
+
|
|
92
99
|
TAG="v${VERSION}"
|
|
93
100
|
"${RUN[@]}" git tag -a "$TAG" -m "solstone-linux ${VERSION}"
|
|
94
101
|
if ! "${RUN[@]}" git push origin "$TAG"; then
|
|
@@ -24,6 +24,7 @@ from dbus_next.errors import (
|
|
|
24
24
|
logger = logging.getLogger(__name__)
|
|
25
25
|
|
|
26
26
|
_DBUS_PROBE_TIMEOUT_SEC = 2.0
|
|
27
|
+
_POWER_SAVE_WARNED_BACKENDS: set[str] = set()
|
|
27
28
|
|
|
28
29
|
_SERVICE_MISSING_ERRORS = (
|
|
29
30
|
"org.freedesktop.DBus.Error.ServiceUnknown",
|
|
@@ -212,6 +213,22 @@ async def is_power_save_active(bus: MessageBus) -> bool:
|
|
|
212
213
|
|
|
213
214
|
Returns True if power save is active, False otherwise.
|
|
214
215
|
"""
|
|
216
|
+
|
|
217
|
+
def log_backend_failure_once(backend: str, bus_name: str, path: str, exc) -> None:
|
|
218
|
+
level = logger.warning
|
|
219
|
+
if backend in _POWER_SAVE_WARNED_BACKENDS:
|
|
220
|
+
level = logger.debug
|
|
221
|
+
else:
|
|
222
|
+
_POWER_SAVE_WARNED_BACKENDS.add(backend)
|
|
223
|
+
level(
|
|
224
|
+
"is_power_save_active %s backend failed: service=%s path=%s: %s: %s",
|
|
225
|
+
backend,
|
|
226
|
+
bus_name,
|
|
227
|
+
path,
|
|
228
|
+
type(exc).__name__,
|
|
229
|
+
exc,
|
|
230
|
+
)
|
|
231
|
+
|
|
215
232
|
# Try GNOME Mutter DisplayConfig first
|
|
216
233
|
try:
|
|
217
234
|
intro = await bus.introspect(DISPLAY_CONFIG_BUS, DISPLAY_CONFIG_PATH)
|
|
@@ -227,11 +244,10 @@ async def is_power_save_active(bus: MessageBus) -> bool:
|
|
|
227
244
|
OSError,
|
|
228
245
|
) as exc:
|
|
229
246
|
if not _is_service_missing(exc):
|
|
230
|
-
|
|
231
|
-
"
|
|
247
|
+
log_backend_failure_once(
|
|
248
|
+
"Mutter",
|
|
232
249
|
DISPLAY_CONFIG_BUS,
|
|
233
250
|
DISPLAY_CONFIG_PATH,
|
|
234
|
-
type(exc).__name__,
|
|
235
251
|
exc,
|
|
236
252
|
)
|
|
237
253
|
|
|
@@ -248,13 +264,7 @@ async def is_power_save_active(bus: MessageBus) -> bool:
|
|
|
248
264
|
OSError,
|
|
249
265
|
) as exc:
|
|
250
266
|
if not _is_service_missing(exc):
|
|
251
|
-
|
|
252
|
-
"is_power_save_active KDE backend failed: service=%s path=%s: %s: %s",
|
|
253
|
-
KDE_POWER_BUS,
|
|
254
|
-
KDE_POWER_PATH,
|
|
255
|
-
type(exc).__name__,
|
|
256
|
-
exc,
|
|
257
|
-
)
|
|
267
|
+
log_backend_failure_once("KDE", KDE_POWER_BUS, KDE_POWER_PATH, exc)
|
|
258
268
|
return False
|
|
259
269
|
|
|
260
270
|
|
|
@@ -134,44 +134,23 @@ def cmd_setup(args: argparse.Namespace) -> int:
|
|
|
134
134
|
)
|
|
135
135
|
return 0
|
|
136
136
|
|
|
137
|
-
print(f"Stream: {config.stream}")
|
|
138
137
|
save_config(config)
|
|
139
138
|
|
|
140
139
|
if not config.key:
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
data = json.loads(result.stdout)
|
|
153
|
-
config.key = data["key"]
|
|
154
|
-
save_config(config)
|
|
155
|
-
print(f"Registered (key: {config.key[:8]}...)")
|
|
156
|
-
else:
|
|
157
|
-
print("CLI registration failed, trying HTTP...")
|
|
158
|
-
except (subprocess.TimeoutExpired, json.JSONDecodeError, KeyError, OSError):
|
|
159
|
-
print("CLI registration failed, trying HTTP...")
|
|
160
|
-
|
|
161
|
-
if not config.key:
|
|
162
|
-
print("Registering with your journal...")
|
|
163
|
-
client = UploadClient(config)
|
|
164
|
-
if client.ensure_registered(config):
|
|
165
|
-
config = load_config()
|
|
166
|
-
print(f"Registered (key: {config.key[:8]}...)")
|
|
167
|
-
else:
|
|
168
|
-
print(
|
|
169
|
-
"Warning: registration failed. Run setup again when your journal is available."
|
|
170
|
-
)
|
|
171
|
-
if non_interactive:
|
|
172
|
-
return 1
|
|
140
|
+
print("Registering with your journal...")
|
|
141
|
+
client = UploadClient(config)
|
|
142
|
+
if client.ensure_registered(config):
|
|
143
|
+
print(f"Registered (key: {config.key[:8]}...)")
|
|
144
|
+
print(f"Stream: {config.stream}")
|
|
145
|
+
else:
|
|
146
|
+
print(
|
|
147
|
+
"Warning: registration failed. Run setup again when your journal is available."
|
|
148
|
+
)
|
|
149
|
+
if non_interactive:
|
|
150
|
+
return 1
|
|
173
151
|
else:
|
|
174
152
|
print(f"Already registered (key: {config.key[:8]}...)")
|
|
153
|
+
print(f"Stream: {config.stream}")
|
|
175
154
|
|
|
176
155
|
print(f"\nConfig saved to {config.config_path}")
|
|
177
156
|
print(f"Captures will go to {config.captures_dir}")
|
|
@@ -203,46 +182,24 @@ def _cmd_setup_interactive() -> int:
|
|
|
203
182
|
except ValueError as e:
|
|
204
183
|
print(f"Error deriving stream name: {e}", file=sys.stderr)
|
|
205
184
|
return 1
|
|
206
|
-
print(f"Stream: {config.stream}")
|
|
207
185
|
|
|
208
186
|
# Save config before registration (so URL is persisted)
|
|
209
187
|
config.ensure_dirs()
|
|
210
188
|
save_config(config)
|
|
211
189
|
|
|
212
|
-
# Auto-register — try sol CLI first (no server needed), fall back to HTTP
|
|
213
190
|
if not config.key:
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
)
|
|
224
|
-
if result.returncode == 0:
|
|
225
|
-
data = json.loads(result.stdout)
|
|
226
|
-
config.key = data["key"]
|
|
227
|
-
save_config(config)
|
|
228
|
-
print(f"Registered (key: {config.key[:8]}...)")
|
|
229
|
-
else:
|
|
230
|
-
print("CLI registration failed, trying HTTP...")
|
|
231
|
-
except (subprocess.TimeoutExpired, json.JSONDecodeError, KeyError, OSError):
|
|
232
|
-
print("CLI registration failed, trying HTTP...")
|
|
233
|
-
|
|
234
|
-
if not config.key:
|
|
235
|
-
print("Registering with your journal...")
|
|
236
|
-
client = UploadClient(config)
|
|
237
|
-
if client.ensure_registered(config):
|
|
238
|
-
config = load_config()
|
|
239
|
-
print(f"Registered (key: {config.key[:8]}...)")
|
|
240
|
-
else:
|
|
241
|
-
print(
|
|
242
|
-
"Warning: registration failed. Run setup again when your journal is available."
|
|
243
|
-
)
|
|
191
|
+
print("Registering with your journal...")
|
|
192
|
+
client = UploadClient(config)
|
|
193
|
+
if client.ensure_registered(config):
|
|
194
|
+
print(f"Registered (key: {config.key[:8]}...)")
|
|
195
|
+
print(f"Stream: {config.stream}")
|
|
196
|
+
else:
|
|
197
|
+
print(
|
|
198
|
+
"Warning: registration failed. Run setup again when your journal is available."
|
|
199
|
+
)
|
|
244
200
|
else:
|
|
245
201
|
print(f"Already registered (key: {config.key[:8]}...)")
|
|
202
|
+
print(f"Stream: {config.stream}")
|
|
246
203
|
|
|
247
204
|
print(f"\nConfig saved to {config.config_path}")
|
|
248
205
|
print(f"Captures will go to {config.captures_dir}")
|
|
@@ -321,15 +278,31 @@ def cmd_install_service(args: argparse.Namespace) -> int:
|
|
|
321
278
|
shutil.copy2(svg, status_dir / svg.name)
|
|
322
279
|
print(f"Installed {status_dir / svg.name}")
|
|
323
280
|
|
|
324
|
-
#
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
281
|
+
# Self-heal: earlier installs copied a solstone index.theme into this
|
|
282
|
+
# shared hicolor dir. Because the user icon dir out-ranks
|
|
283
|
+
# /usr/share/icons, that file shadowed the system hicolor index (which
|
|
284
|
+
# declares ~649 dirs) with one that declared only scalable/status, so
|
|
285
|
+
# every unrelated app-icon lookup fell back to hicolor, missed, and
|
|
286
|
+
# rendered as our diamond. Remove only our own file — matched on the
|
|
287
|
+
# exact "Name=solstone" line — and never touch a foreign index.theme.
|
|
288
|
+
legacy_index = icon_dest / "index.theme"
|
|
289
|
+
if legacy_index.exists():
|
|
290
|
+
try:
|
|
291
|
+
content = legacy_index.read_text()
|
|
292
|
+
except (OSError, UnicodeDecodeError):
|
|
293
|
+
print(f"Left existing icon theme index in place: {legacy_index}")
|
|
294
|
+
else:
|
|
295
|
+
if "Name=solstone" in content.splitlines():
|
|
296
|
+
legacy_index.unlink()
|
|
297
|
+
print(f"Removed stale solstone icon theme index: {legacy_index}")
|
|
329
298
|
|
|
330
|
-
#
|
|
299
|
+
# Refresh the icon cache (non-fatal). --ignore-theme-index keeps it
|
|
300
|
+
# quiet now that this dir ships no index.theme of its own.
|
|
331
301
|
try:
|
|
332
|
-
subprocess.run(
|
|
302
|
+
subprocess.run(
|
|
303
|
+
["gtk-update-icon-cache", "--ignore-theme-index", str(icon_dest)],
|
|
304
|
+
check=False,
|
|
305
|
+
)
|
|
333
306
|
except FileNotFoundError:
|
|
334
307
|
pass
|
|
335
308
|
|
|
@@ -42,7 +42,7 @@ from .audio_recorder import AudioRecorder
|
|
|
42
42
|
from .chat_bridge import run_chat_bridge
|
|
43
43
|
from .config import Config
|
|
44
44
|
from .recovery import write_segment_metadata
|
|
45
|
-
from .screencast import Screencaster, StreamInfo
|
|
45
|
+
from .screencast import Screencaster, SilentStream, StreamInfo
|
|
46
46
|
from .sync import SyncService
|
|
47
47
|
from .upload import UploadClient
|
|
48
48
|
|
|
@@ -162,6 +162,7 @@ class Observer:
|
|
|
162
162
|
self._client = UploadClient(self.config)
|
|
163
163
|
if self.config.server_url:
|
|
164
164
|
self._client.ensure_registered(self.config)
|
|
165
|
+
self.stream = self.config.stream
|
|
165
166
|
self._sync = SyncService(self.config, self._client)
|
|
166
167
|
|
|
167
168
|
from .dbus_service import BUS_NAME, OBJECT_PATH, ObserverService
|
|
@@ -317,7 +318,9 @@ class Observer:
|
|
|
317
318
|
# Stop screencast first (closes file handles)
|
|
318
319
|
if self.current_mode == MODE_SCREENCAST:
|
|
319
320
|
logger.info("Stopping previous screencast")
|
|
320
|
-
await self.screencaster.stop()
|
|
321
|
+
healthy, silent = await self.screencaster.stop()
|
|
322
|
+
for s in silent:
|
|
323
|
+
self._emit_stream_silent(s)
|
|
321
324
|
self.current_streams = []
|
|
322
325
|
|
|
323
326
|
# Save audio if we have enough threshold hits
|
|
@@ -389,6 +392,24 @@ class Observer:
|
|
|
389
392
|
|
|
390
393
|
return True
|
|
391
394
|
|
|
395
|
+
def _emit_stream_silent(self, silent: SilentStream) -> None:
|
|
396
|
+
if self._client is None:
|
|
397
|
+
return
|
|
398
|
+
segment_dir_basename = self.segment_dir.name if self.segment_dir else ""
|
|
399
|
+
duration_seconds = int(time.time() - self.start_at) if self.start_at else 0
|
|
400
|
+
self._client.relay_event(
|
|
401
|
+
"observe",
|
|
402
|
+
"stream_silent",
|
|
403
|
+
connector=silent.connector,
|
|
404
|
+
position=silent.position,
|
|
405
|
+
node_id=silent.node_id,
|
|
406
|
+
file_bytes=silent.file_bytes,
|
|
407
|
+
segment_dir=segment_dir_basename,
|
|
408
|
+
duration_seconds=duration_seconds,
|
|
409
|
+
host=HOST,
|
|
410
|
+
platform=PLATFORM,
|
|
411
|
+
)
|
|
412
|
+
|
|
392
413
|
def emit_status(self):
|
|
393
414
|
"""Emit observe.status event with current state (fire-and-forget)."""
|
|
394
415
|
if not self._client:
|
|
@@ -437,7 +458,6 @@ class Observer:
|
|
|
437
458
|
activity=activity_info,
|
|
438
459
|
host=HOST,
|
|
439
460
|
platform=PLATFORM,
|
|
440
|
-
stream=self.stream,
|
|
441
461
|
)
|
|
442
462
|
|
|
443
463
|
def _refresh_tray(self):
|
|
@@ -551,7 +571,9 @@ class Observer:
|
|
|
551
571
|
if self._paused:
|
|
552
572
|
if self.segment_dir:
|
|
553
573
|
if self.current_mode == MODE_SCREENCAST:
|
|
554
|
-
await self.screencaster.stop()
|
|
574
|
+
healthy, silent = await self.screencaster.stop()
|
|
575
|
+
for s in silent:
|
|
576
|
+
self._emit_stream_silent(s)
|
|
555
577
|
self.current_streams = []
|
|
556
578
|
if self.threshold_hits >= MIN_HITS_FOR_SAVE:
|
|
557
579
|
self._save_audio_segment(
|
|
@@ -603,7 +625,9 @@ class Observer:
|
|
|
603
625
|
and not self.screencaster.is_healthy()
|
|
604
626
|
):
|
|
605
627
|
logger.warning("Screencast recording failed, stopping gracefully")
|
|
606
|
-
await self.screencaster.stop()
|
|
628
|
+
healthy, silent = await self.screencaster.stop()
|
|
629
|
+
for s in silent:
|
|
630
|
+
self._emit_stream_silent(s)
|
|
607
631
|
self.current_streams = []
|
|
608
632
|
self.current_mode = MODE_IDLE
|
|
609
633
|
|
|
@@ -692,7 +716,9 @@ class Observer:
|
|
|
692
716
|
# Stop screencast first (closes file handles)
|
|
693
717
|
if self.current_mode == MODE_SCREENCAST:
|
|
694
718
|
logger.info("Stopping screencast for shutdown")
|
|
695
|
-
await self.screencaster.stop()
|
|
719
|
+
healthy, silent = await self.screencaster.stop()
|
|
720
|
+
for s in silent:
|
|
721
|
+
self._emit_stream_silent(s)
|
|
696
722
|
await asyncio.sleep(0.5)
|
|
697
723
|
|
|
698
724
|
# Save final audio if threshold met
|