chore: scaffold Rust workspace and native GPUI shell
Rust checks / checks (push) Successful in 3m35s
Rust checks / checks (push) Successful in 3m35s
This commit is contained in:
@@ -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
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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(),
|
||||
)),
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user