chore: scaffold Rust workspace and native GPUI shell
Rust checks / checks (push) Successful in 3m35s

This commit is contained in:
2026-10-08 12:19:45 +02:00
parent 9602f82ca9
commit eb364d25e2
15 changed files with 8294 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "clipbin-core"
description = "Recorder settings and state independent of the Clipbin desktop interface."
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
publish.workspace = true
[lints]
workspace = true
+7
View File
@@ -0,0 +1,7 @@
//! Recorder settings and state shared with the desktop interface.
mod settings;
mod status;
pub use settings::{Settings, VideoCodec};
pub use status::RecordingStatus;
+54
View File
@@ -0,0 +1,54 @@
use std::{fmt, time::Duration};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum VideoCodec {
#[default]
H264,
Hevc,
Av1,
}
impl fmt::Display for VideoCodec {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::H264 => "H.264",
Self::Hevc => "HEVC",
Self::Av1 => "AV1",
})
}
}
/// In-memory defaults from SPEC.md. Persistence and editing come with the recorder.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Settings {
pub width: u32,
pub height: u32,
pub frames_per_second: u32,
pub codec: VideoCodec,
pub system_audio_enabled: bool,
pub microphone_enabled: bool,
pub start_on_login: bool,
pub clip_duration: Duration,
pub bookmark_before: Duration,
pub bookmark_after: Duration,
/// Protected bookmarks and exported clips are excluded from this limit.
pub unprotected_session_limit_bytes: u64,
}
impl Default for Settings {
fn default() -> Self {
Self {
width: 1920,
height: 1080,
frames_per_second: 60,
codec: VideoCodec::H264,
system_audio_enabled: true,
microphone_enabled: false,
start_on_login: true,
clip_duration: Duration::from_secs(120),
bookmark_before: Duration::from_secs(120),
bookmark_after: Duration::from_secs(30),
unprotected_session_limit_bytes: 50_000_000_000,
}
}
}
+27
View File
@@ -0,0 +1,27 @@
/// The recorder will own these transitions once capture is implemented.
///
/// A game being detected or permission being granted is insufficient to enter
/// Recording: a usable capture source and successful output must be confirmed.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum RecordingStatus {
#[default]
Idle,
WaitingForCapture,
Starting,
Recording,
Failed {
reason: String,
},
}
impl RecordingStatus {
pub fn label(&self) -> &'static str {
match self {
Self::Idle => "Not recording",
Self::WaitingForCapture => "Waiting for capture",
Self::Starting => "Starting",
Self::Recording => "Recording",
Self::Failed { .. } => "Recording failed",
}
}
}
+16
View File
@@ -0,0 +1,16 @@
[package]
name = "clipbin-desktop"
description = "The native GPUI interface for Clipbin game recordings."
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
publish.workspace = true
[dependencies]
clipbin-core.workspace = true
gpui.workspace = true
[lints]
workspace = true
+48
View File
@@ -0,0 +1,48 @@
use gpui::{
App, AppContext, Application, Bounds, KeyBinding, TitlebarOptions, WindowBounds, WindowOptions,
actions, px, size,
};
mod view;
actions!(clipbin, [Quit]);
fn quit(_: &Quit, cx: &mut App) {
cx.quit();
}
fn main() {
Application::new().run(|cx: &mut App| {
cx.on_action(quit);
cx.bind_keys([KeyBinding::new("ctrl-q", Quit, None)]);
// This initial shell has no background recorder yet.
cx.on_window_closed(|cx| {
if cx.windows().is_empty() {
cx.quit();
}
})
.detach();
let bounds = Bounds::centered(None, size(px(1080.0), px(720.0)), cx);
let result = cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
titlebar: Some(TitlebarOptions {
title: Some("Clipbin".into()),
..Default::default()
}),
..Default::default()
},
|_, cx| cx.new(|_| view::ClipbinView::default()),
);
if let Err(error) = result {
eprintln!("Could not open Clipbin: {error}");
cx.quit();
return;
}
cx.activate(true);
});
}
+91
View File
@@ -0,0 +1,91 @@
use clipbin_core::{RecordingStatus, Settings};
use gpui::{Context, IntoElement, Render, Window, div, prelude::*, rgb};
#[derive(Default)]
pub struct ClipbinView {
settings: Settings,
status: RecordingStatus,
}
impl Render for ClipbinView {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let settings = &self.settings;
div()
.size_full()
.flex()
.flex_col()
.gap_6()
.p_8()
.bg(rgb(0x101318))
.text_color(rgb(0xe6e9ef))
.child(
div()
.flex()
.justify_between()
.items_center()
.child(div().text_3xl().child("Clipbin"))
.child(
div()
.id("quit")
.px_4()
.py_2()
.rounded_md()
.bg(rgb(0x232a35))
.cursor_pointer()
.on_click(|_, _, cx| cx.quit())
.child("Quit"),
),
)
.child(
div()
.flex()
.flex_col()
.gap_2()
.p_4()
.rounded_lg()
.border_1()
.border_color(rgb(0x343d4c))
.child(div().text_lg().child(self.status.label()))
.child(
div()
.text_color(rgb(0xa9b2c2))
.child("Capture is not available in this build."),
),
)
.child(
div()
.flex_1()
.flex()
.flex_col()
.justify_center()
.items_center()
.gap_3()
.rounded_lg()
.bg(rgb(0x181d25))
.child(div().text_2xl().child("No sessions yet"))
.child(
div().text_color(rgb(0xa9b2c2)).child(
"Your recorded sessions and bookmarked moments will appear here.",
),
),
)
.child(
div()
.flex()
.flex_col()
.gap_2()
.text_sm()
.text_color(rgb(0xa9b2c2))
.child(format!(
"Recording defaults: {} × {} · {} FPS · Hardware {}",
settings.width, settings.height, settings.frames_per_second, settings.codec,
))
.child(format!(
"Session budget: {} GB of unprotected footage · Clip: {} seconds",
settings.unprotected_session_limit_bytes / 1_000_000_000,
settings.clip_duration.as_secs(),
)),
)
}
}