Make launcher runnable

This commit is contained in:
Albert Armea 2025-12-27 10:50:54 -05:00
parent e2013eb694
commit 2965afacae
15 changed files with 98 additions and 30 deletions

1
.gitignore vendored
View file

@ -1 +1,2 @@
/target /target
/dev-runtime

12
Cargo.lock generated
View file

@ -68,7 +68,7 @@ version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@ -79,7 +79,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [ dependencies = [
"anstyle", "anstyle",
"once_cell_polyfill", "once_cell_polyfill",
"windows-sys 0.61.2", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@ -257,7 +257,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.61.2", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@ -856,7 +856,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@ -1037,7 +1037,7 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
"windows-sys 0.61.2", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@ -1390,7 +1390,7 @@ dependencies = [
"getrandom", "getrandom",
"once_cell", "once_cell",
"rustix", "rustix",
"windows-sys 0.61.2", "windows-sys 0.59.0",
] ]
[[package]] [[package]]

View file

@ -62,7 +62,7 @@ bitflags = "2.4"
nix = { version = "0.29", features = ["signal", "process", "user", "socket"] } nix = { version = "0.29", features = ["signal", "process", "user", "socket"] }
# CLI # CLI
clap = { version = "4.5", features = ["derive"] } clap = { version = "4.5", features = ["derive", "env"] }
# GTK4 UI # GTK4 UI
gtk4 = "0.9" gtk4 = "0.9"

View file

@ -31,7 +31,8 @@ TODO:
tl;dr: tl;dr:
1. any Linux with Wayland (optional: TPM-based FDE plus BIOS password to prevent tampering) 1. any Linux with Wayland (optional: TPM-based FDE plus BIOS password to prevent tampering)
2. System dependencies (Ubuntu: `apt install curl sway swayidle pkg-config libcairo2-dev libxkbcommon-dev`) 2. System dependencies:
- **Ubuntu/Debian**: `apt install build-essential pkg-config libglib2.0-dev libgtk-4-dev libcairo2-dev libpango1.0-dev libgdk-pixbuf-xlib-2.0-dev libwayland-dev libx11-dev libxkbcommon-dev libgirepository1.0-dev libgtk4-layer-shell-dev librust-gtk4-layer-shell-sys-dev sway swayidle`
3. Rust (`curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh`) 3. Rust (`curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh`)
4. binaries (TODO: deployable package that depends on Sway and installs the config) 4. binaries (TODO: deployable package that depends on Sway and installs the config)
5. test session on login 5. test session on login

View file

@ -10,7 +10,7 @@ use crate::volume::VolumeStatus;
use gtk4::glib; use gtk4::glib;
use gtk4::prelude::*; use gtk4::prelude::*;
use gtk4_layer_shell::{Edge, Layer, LayerShell}; use gtk4_layer_shell::{Edge, Layer, LayerShell};
use shepherd_api::commands::Command; use shepherd_api::Command;
use shepherd_ipc::IpcClient; use shepherd_ipc::IpcClient;
use std::cell::RefCell; use std::cell::RefCell;
use std::path::PathBuf; use std::path::PathBuf;

View file

@ -20,8 +20,8 @@ use tracing_subscriber::EnvFilter;
#[command(name = "shepherd-hud")] #[command(name = "shepherd-hud")]
#[command(about = "GTK4 layer-shell HUD for shepherdd", long_about = None)] #[command(about = "GTK4 layer-shell HUD for shepherdd", long_about = None)]
struct Args { struct Args {
/// Socket path for shepherdd connection /// Socket path for shepherdd connection (or set SHEPHERD_SOCKET env var)
#[arg(short, long, default_value = "/run/shepherdd/shepherdd.sock")] #[arg(short, long, env = "SHEPHERD_SOCKET", default_value = "/run/shepherdd/shepherdd.sock")]
socket: PathBuf, socket: PathBuf,
/// Log level /// Log level

View file

@ -3,8 +3,7 @@
//! The HUD subscribes to events from shepherdd and tracks session state. //! The HUD subscribes to events from shepherdd and tracks session state.
use chrono::Local; use chrono::Local;
use shepherd_api::events::{Event, EventPayload}; use shepherd_api::{Event, EventPayload, SessionEndReason};
use shepherd_api::types::SessionEndReason;
use shepherd_util::{EntryId, SessionId}; use shepherd_util::{EntryId, SessionId};
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::watch; use tokio::sync::watch;

View file

@ -105,7 +105,7 @@ impl LauncherApp {
fn build_ui(app: &gtk4::Application, socket_path: PathBuf) { fn build_ui(app: &gtk4::Application, socket_path: PathBuf) {
// Load CSS // Load CSS
let provider = gtk4::CssProvider::new(); let provider = gtk4::CssProvider::new();
provider.load_from_string(LAUNCHER_CSS); provider.load_from_data(LAUNCHER_CSS);
gtk4::style_context_add_provider_for_display( gtk4::style_context_add_provider_for_display(
&gtk4::gdk::Display::default().expect("Could not get default display"), &gtk4::gdk::Display::default().expect("Could not get default display"),
&provider, &provider,

View file

@ -1,7 +1,7 @@
//! IPC client wrapper for the launcher UI //! IPC client wrapper for the launcher UI
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use shepherd_api::{Command, Event, Response, ResponsePayload, ResponseResult}; use shepherd_api::{Command, Event, ReasonCode, Response, ResponsePayload, ResponseResult};
use shepherd_ipc::IpcClient; use shepherd_ipc::IpcClient;
use shepherd_util::EntryId; use shepherd_util::EntryId;
use std::path::Path; use std::path::Path;
@ -182,7 +182,7 @@ impl DaemonClient {
ResponsePayload::LaunchDenied { reasons } => { ResponsePayload::LaunchDenied { reasons } => {
let message = reasons let message = reasons
.iter() .iter()
.map(|r| r.message.as_deref().unwrap_or("Denied")) .map(|r| reason_to_message(r))
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(", "); .join(", ");
self.state.set(LauncherState::Error { message }); self.state.set(LauncherState::Error { message });
@ -237,3 +237,15 @@ impl CommandClient {
client.send(Command::ListEntries { at_time: None }).await.map_err(Into::into) client.send(Command::ListEntries { at_time: None }).await.map_err(Into::into)
} }
} }
/// Convert a ReasonCode enum variant to a human-readable message
fn reason_to_message(reason: &ReasonCode) -> &'static str {
match reason {
ReasonCode::OutsideTimeWindow { .. } => "Outside allowed time window",
ReasonCode::QuotaExhausted { .. } => "Daily quota exhausted",
ReasonCode::CooldownActive { .. } => "Cooldown period active",
ReasonCode::SessionActive { .. } => "Another session is active",
ReasonCode::UnsupportedKind { .. } => "Entry type not supported",
ReasonCode::Disabled { .. } => "Entry disabled",
}
}

View file

@ -6,6 +6,7 @@ use gtk4::subclass::prelude::*;
use shepherd_api::EntryView; use shepherd_api::EntryView;
use shepherd_util::EntryId; use shepherd_util::EntryId;
use std::cell::RefCell; use std::cell::RefCell;
use std::rc::Rc;
use crate::tile::LauncherTile; use crate::tile::LauncherTile;
@ -15,7 +16,7 @@ mod imp {
pub struct LauncherGrid { pub struct LauncherGrid {
pub flow_box: gtk4::FlowBox, pub flow_box: gtk4::FlowBox,
pub tiles: RefCell<Vec<LauncherTile>>, pub tiles: RefCell<Vec<LauncherTile>>,
pub on_launch: RefCell<Option<Box<dyn Fn(EntryId) + 'static>>>, pub on_launch: Rc<RefCell<Option<Box<dyn Fn(EntryId) + 'static>>>>,
} }
impl Default for LauncherGrid { impl Default for LauncherGrid {
@ -23,7 +24,7 @@ mod imp {
Self { Self {
flow_box: gtk4::FlowBox::new(), flow_box: gtk4::FlowBox::new(),
tiles: RefCell::new(Vec::new()), tiles: RefCell::new(Vec::new()),
on_launch: RefCell::new(None), on_launch: Rc::new(RefCell::new(None)),
} }
} }
} }
@ -120,7 +121,7 @@ impl LauncherGrid {
} }
}); });
imp.flow_box.append(&tile); imp.flow_box.insert(&tile, -1);
imp.tiles.borrow_mut().push(tile); imp.tiles.borrow_mut().push(tile);
} }
} }

View file

@ -20,8 +20,8 @@ use tracing_subscriber::EnvFilter;
#[command(name = "shepherd-launcher")] #[command(name = "shepherd-launcher")]
#[command(about = "GTK4 launcher UI for shepherdd", long_about = None)] #[command(about = "GTK4 launcher UI for shepherdd", long_about = None)]
struct Args { struct Args {
/// Socket path for shepherdd connection /// Socket path for shepherdd connection (or set SHEPHERD_SOCKET env var)
#[arg(short, long, default_value = "/run/shepherdd/shepherdd.sock")] #[arg(short, long, env = "SHEPHERD_SOCKET", default_value = "/run/shepherdd/shepherdd.sock")]
socket: PathBuf, socket: PathBuf,
/// Log level /// Log level

View file

@ -26,7 +26,7 @@ tracing = { workspace = true }
tracing-subscriber = { workspace = true } tracing-subscriber = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }
clap = { version = "4.4", features = ["derive"] } clap = { version = "4.5", features = ["derive", "env"] }
[dev-dependencies] [dev-dependencies]
tempfile = { workspace = true } tempfile = { workspace = true }

View file

@ -38,12 +38,12 @@ struct Args {
#[arg(short, long, default_value = "/etc/shepherdd/config.toml")] #[arg(short, long, default_value = "/etc/shepherdd/config.toml")]
config: PathBuf, config: PathBuf,
/// Socket path override /// Socket path override (or set SHEPHERD_SOCKET env var)
#[arg(short, long)] #[arg(short, long, env = "SHEPHERD_SOCKET")]
socket: Option<PathBuf>, socket: Option<PathBuf>,
/// Data directory override /// Data directory override (or set SHEPHERD_DATA_DIR env var)
#[arg(short, long)] #[arg(short, long, env = "SHEPHERD_DATA_DIR")]
data_dir: Option<PathBuf>, data_dir: Option<PathBuf>,
/// Log level /// Log level

46
run-dev
View file

@ -1,3 +1,47 @@
#!/usr/bin/env bash #!/usr/bin/env bash
cargo build && WLR_BACKENDS=wayland WLR_LIBINPUT_NO_DEVICES=1 sway -c ./sway.conf set -e
# Set up dev runtime directory
DEV_RUNTIME="./dev-runtime"
DATA_DIR="$DEV_RUNTIME/data"
SOCKET_PATH="$DEV_RUNTIME/shepherd.sock"
mkdir -p "$DATA_DIR"
# Export environment variables for shepherd binaries
export SHEPHERD_SOCKET="$SOCKET_PATH"
export SHEPHERD_DATA_DIR="$DATA_DIR"
# Build all binaries
echo "Building shepherd binaries..."
cargo build
# Function to cleanup background processes on exit
cleanup() {
echo "Cleaning up..."
if [ ! -z "$SHEPHERDD_PID" ]; then
kill $SHEPHERDD_PID 2>/dev/null || true
fi
if [ ! -z "$HUD_PID" ]; then
kill $HUD_PID 2>/dev/null || true
fi
}
trap cleanup EXIT
# Start the background daemon
echo "Starting shepherdd..."
./target/debug/shepherdd -c ./config.example.toml &
SHEPHERDD_PID=$!
# Give the daemon a moment to initialize
sleep 1
# Start the HUD in the background
echo "Starting shepherd-hud..."
./target/debug/shepherd-hud &
HUD_PID=$!
# Start sway with the launcher
echo "Starting sway with shepherd-launcher..."
WLR_BACKENDS=wayland WLR_LIBINPUT_NO_DEVICES=1 sway -c ./sway.conf

View file

@ -3,6 +3,7 @@
### Variables ### Variables
set $launcher ./target/debug/shepherd-launcher set $launcher ./target/debug/shepherd-launcher
set $hud ./target/debug/shepherd-hud
### Output configuration ### Output configuration
# Set up displays (adjust as needed for your hardware) # Set up displays (adjust as needed for your hardware)
@ -35,7 +36,13 @@ bindsym Mod4+Shift+Escape exit
### Window rules for kiosk behavior ### Window rules for kiosk behavior
# Make all windows fullscreen by default # Shepherd HUD should always be visible on top (likely uses layer-shell protocol)
# Layer shell surfaces are automatically handled by sway and don't need window rules
# Make launcher windows fullscreen
for_window [app_id="shepherd-launcher"] fullscreen enable
# Make other windows fullscreen by default
for_window [class=".*"] fullscreen enable for_window [class=".*"] fullscreen enable
for_window [app_id=".*"] fullscreen enable for_window [app_id=".*"] fullscreen enable
@ -62,9 +69,12 @@ seat * hide_cursor 5000
# Use only one workspace for true kiosk mode # Use only one workspace for true kiosk mode
workspace 1 output * workspace 1 output *
### Application launcher ### Application startup
# Start the shepherd-launcher on startup # Note: shepherdd (daemon) and shepherd-hud are started by run-dev script
# before sway launches, so they're already running at this point
# Start the shepherd-launcher on startup (the main "home" screen)
exec_always $launcher exec_always $launcher
### Disable workspace switching ### Disable workspace switching